Merge branch 'development'

This commit is contained in:
Edwin Noorlander 2025-11-21 20:23:36 +01:00
commit e4047aaf09
507 changed files with 43673 additions and 2391 deletions

25
.gitignore vendored Normal file
View File

@ -0,0 +1,25 @@
# Dependencies
node_modules/
package-lock.json
# Build outputs
*.log
.DS_Store
Thumbs.db
# IDE files
.vscode/
.idea/
*.swp
*.swo
# Cache
.cache/
.sass-cache/
# Temporary files
*.tmp
*.temp
# No content
content/*

346
README.md
View File

@ -1,169 +1,251 @@
# CodePress CMS
A lightweight, file-based Content Management System built with PHP and Bootstrap.
Een lichtgewicht, file-based content management systeem gebouwd met PHP.
## Overview
## ✨ Features
CodePress is a modern, secure CMS that manages content through files instead of a database. It's designed for simplicity, flexibility, and ease of use while maintaining powerful features for content management.
- 📝 **Multi-format Content** - Ondersteunt Markdown, PHP en HTML bestanden
- 🧭 **Dynamic Navigation** - Automatische menu generatie met dropdowns
- 🔍 **Search Functionality** - Volledige tekst zoek door alle content
- 🧭 **Breadcrumb Navigation** - Intuïtieve navigatiepaden
- 🔗 **Auto-linking** - Automatische links tussen pagina's
- 📱 **Responsive Design** - Werkt perfect op alle apparaten
- ⚙️ **JSON Configuratie** - Eenvoudige configuratie via JSON
- 🎨 **Bootstrap 5** - Moderne UI framework
- 🔒 **Security** - Beveiligde content management
## Features
## 🚀 Quick Start
### 🎯 Core Features
- **File-based content** - No database required
- **Multi-format support** - Markdown, PHP, HTML files
- **Dynamic navigation** - Auto-generated from directory structure
- **Search functionality** - Full-text search with snippets
- **Breadcrumb navigation** - Clear page hierarchy
- **Responsive design** - Works on all devices
1. **Upload** bestanden naar webserver
2. **Stel permissies** in voor webserver
3. **Configureer** (optioneel) via `config.json`
4. **Bezoek** website via browser
### 🔧 Advanced Features
- **Collapsible folders** - Accordion-style navigation
- **WCAG compliant** - Accessible design with proper contrast
- **File metadata** - Creation and modification dates in footer
- **Progressive styling** - Visual hierarchy with nested levels
- **Security** - Protected PHP files and sensitive directories
## Quick Start
1. **Requirements**: PHP 8.4+
2. **Installation**:
```bash
git clone https://git.noorlander.info/E.Noorlander/CodePress.git
cd CodePress
```
3. **Start server**:
```bash
# For Apache: Set DocumentRoot to public/
# For Development:
php -S localhost:8080 -t public router.php
```
4. **Visit**: `http://localhost:8080`
## Project Structure
## 📁 Project Structuur
```
codepress/
├── public/ # Web-accessible directory (DocumentRoot)
│ ├── index.php # Main entry point
│ ├── .htaccess # Apache security and routing
│ └── router.php # PHP development server router
├── content/ # Content files (MD/PHP/HTML) - outside web root
├── engine/ # CMS engine and assets
│ ├── core/ # PHP application logic
│ │ ├── index.php # CMS class and logic
│ │ └── config.php # Site configuration
│ ├── templates/ # HTML templates
│ └── assets/ # Static assets (CSS, JS, fonts)
│ ├── css/ # Bootstrap and custom CSS
│ ├── js/ # JavaScript files
│ └── fonts/ # Font files
├── .htaccess # Root security
└── README.md # This documentation
├── engine/
│ ├── core/
│ │ ├── config.php # Configuratie loader
│ │ └── index.php # CMS engine
│ └── templates/ # Template bestanden
│ ├── layout.mustache
│ ├── assets/
│ │ ├── header.mustache
│ │ ├── navigation.mustache
│ │ └── footer.mustache
│ ├── markdown_content.mustache
│ ├── php_content.mustache
│ └── html_content.mustache
├── content/ # Content bestanden
│ ├── map1/
│ │ ├── pagina1.md
│ │ └── pagina2.php
│ └── homepage.md
├── public/ # Web root
│ ├── assets/
│ │ ├── css/
│ │ ├── js/
│ │ └── favicon.svg
│ └── index.php
├── config.json # Configuratie
└── README.md
```
## Security
## ⚙️ Configuratie
CodePress includes built-in security features:
- **Content isolation** - Content files stored outside web root
- **.htaccess protection** for sensitive files and directories
- **Direct access blocking** - Content files not accessible via URL
- **Security headers** for XSS protection
- **PHP file blocking** in content directory
- **Offline capable** - All assets (Bootstrap) stored locally
- **Directory access control**
### Basis Configuratie (`config.json`)
## Content Management
### Supported File Types
#### Markdown (.md)
```markdown
# Page Title
Content with **bold** and *italic* text.
[Link to page](?page=other/page)
```json
{
"site_title": "CodePress",
"content_dir": "content",
"templates_dir": "engine/templates",
"default_page": "auto",
"homepage": "homepage",
"author": {
"name": "Edwin Noorlander",
"website": "https://noorlander.info",
"git": "https://git.noorlander.info/E.Noorlander/CodePress.git"
},
"seo": {
"description": "CodePress CMS - Lightweight file-based content management system",
"keywords": "cms, php, content management, file-based"
}
}
```
#### PHP (.php)
```php
<?php
$title = "Dynamic Page";
$date = date('Y-m-d');
?>
### Configuratie Opties
<h1><?php echo $title; ?></h1>
<p>Date: <?php echo $date; ?></p>
- **`site_title`** - Naam van de website
- **`content_dir`** - Map met content bestanden
- **`templates_dir`** - Map met template bestanden
- **`default_page`** - Standaard pagina (`"auto"` voor automatische detectie)
- **`homepage`** - Homepage (`"auto"` voor automatische detectie)
- **`author`** - Auteur informatie met links
- **`seo`** - SEO instellingen
## 📝 Content Types
### Markdown (.md)
- Auto-linking tussen pagina's
- GitHub Flavored Markdown ondersteuning
- Syntax highlighting voor code blocks
- Automatische titel extractie
### PHP (.php)
- Volledige PHP ondersteuning
- Dynamische content generatie
- Database integratie mogelijk
- Session management beschikbaar
### HTML (.html)
- Statische HTML pagina's
- Bootstrap componenten
- Custom CSS en JavaScript
- Volledige HTML5 validatie
## 🎨 Design Features
### Navigation
- **Tab-style navigatie** met Bootstrap
- **Dropdown menus** voor mappen en sub-mappen
- **Home knop** met icoon
- **Active state** indicatie
- **Responsive** hamburger menu
### Layout
- **Flexbox layout** voor moderne structuur
- **Fixed header** met logo en zoekfunctie
- **Breadcrumb navigatie** tussen header en content
- **Fixed footer** met metadata
- **Scrollable content** gebied
### Responsive
- **Mobile-first** aanpak
- **Touch-friendly** interactie
- **Adaptieve** breedtes
- **Consistente** ervaring
## 🔧 Vereisten
- **PHP 8.4+** of hoger
- **Webserver** (Apache, Nginx, etc.)
- **Schrijfrechten** voor PHP bestanden
- **Mod_rewrite** (optioneel voor pretty URLs)
## 🛠️ Installatie
### Via Composer
```bash
composer create-project codepress
cd codepress
```
#### HTML (.html)
```html
<h1>Static HTML Page</h1>
<div class="alert alert-info">
Custom HTML content
</div>
### Handmatig
1. **Download** de bestanden
2. **Upload** naar webserver
3. **Stel permissies** in
4. **Configureer** `config.json`
### Webserver Configuratie
#### Apache
```apache
<Directory "/var/www/codepress">
AllowOverride All
Require all granted
</Directory>
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule ^(.*)$ index.php [QSA,L]
</IfModule>
```
### Directory Structure
```
content/
├── home.md # Homepage
├── about/
│ └── company.md # About page
├── blog/
│ ├── tech/
│ │ └── post.md # Blog post
│ └── index.md # Blog overview
└── projects/
└── web-design.md # Project page
#### Nginx
```nginx
server {
root /var/www/codepress/public;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
}
```
## Configuration
## 🏗️ PHP Classes
Edit `config.php` to customize:
### SimpleTemplate Class
```php
return [
'site_title' => 'Your Site Name',
'site_description' => 'Your site description',
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates',
'default_page' => 'home',
'markdown_enabled' => true,
'php_enabled' => true,
];
```
Lightweight template rendering engine die Mustache-style syntax ondersteunt zonder externe dependencies.
## Branches
**Methods:**
- `render($template, $data)` - Rendert template met data
- `replacePartial($matches)` - Vervangt `{{>partial}}` placeholders
- **main**: Clean CodePress CMS with documentation
- **e.noorlander**: Personal blog content and customizations
- **development**: Active development branch
**Features:**
- `{{>partial}}` - Partial includes
- `{{#variable}}...{{/variable}}` - Conditionele blocks
- `{{^variable}}...{{/variable}}` - Negatieve conditionele blocks
- `{{{variable}}}` - Unescaped HTML content
- `{{variable}}` - Escaped content
## Development
### CodePressCMS Class
### Adding Features
1. Create feature branch from `development`
2. Implement changes
3. Test thoroughly
4. Submit merge request
Hoofd CMS class die alle content management functionaliteit beheert.
### Contributing
- Follow PSR-12 coding standards
- Test security implications
- Update documentation
- Maintain backward compatibility
**Public Methods:**
- `__construct($config)` - Initialiseer CMS met configuratie
- `getPage()` - Haalt huidige pagina content op
- `getMenu()` - Genereert navigatiestructuur
- `render()` - Rendert volledige pagina met templates
## License
**Private Methods:**
- `buildMenu()` - Bouwt menu structuur van content directory
- `scanDirectory($dir, $prefix)` - Scant directory voor content
- `performSearch($query)` - Voert zoekopdracht uit
- `parseMarkdown($content)` - Converteert Markdown naar HTML
- `parsePHP($filePath)` - Verwerkt PHP bestanden
- `parseHTML($content)` - Verwerkt HTML bestanden
- `getBreadcrumb()` - Genereert breadcrumb navigatie
- `renderMenu($items, $level)` - Rendert menu HTML
- `getContentType($page)` - Bepaalt content type
- `autoLinkPageTitles($content)` - Auto-link pagina titels
This project is developed for specific use cases. Contact the maintainer for licensing information.
**Features:**
- Multi-format content support (MD, PHP, HTML)
- Dynamische navigatie met dropdowns
- Zoekfunctionaliteit met snippets
- Breadcrumb navigatie
- Auto-linking tussen pagina's
- File metadata tracking
- Responsive template rendering
## Support
## 📖 Documentatie
- **Documentation**: See `content/home.md`
- **Issues**: Report on GitLab
- **Community**: Join discussions
- **[Handleiding (NL)](guide/nl.md)** - Gedetailleerde handleiding
- **[Handleiding (EN)](guide/en.md)** - English documentation
- **[AGENTS.md](AGENTS.md)** - Ontwikkelaar instructies
## 🤝 Bijdragen
Bijdragen zijn welkom! Zie [AGENTS.md](AGENTS.md) voor ontwikkelrichtlijnen.
## 📄 Licentie
Open-source licentie. Zie de repository voor meer informatie.
## 🔗 Links
- **Website**: https://noorlander.info
- **Repository**: https://git.noorlander.info/E.Noorlander/CodePress.git
- **Issues**: https://git.noorlander.info/E.Noorlander/CodePress/issues
---
*Built with ❤️ and PHP*
*Gebouwd met ❤️ door Edwin Noorlander*

6
composer.json Normal file
View File

@ -0,0 +1,6 @@
{
"require": {
"mustache/mustache": "^3.0",
"league/commonmark": "^2.7"
}
}

691
composer.lock generated Normal file
View File

@ -0,0 +1,691 @@
{
"_readme": [
"This file locks the dependencies of your project to a known state",
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "3cf7d71c2b61afde676a52c0c83f8bfe",
"packages": [
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.0.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": [
"access",
"data",
"dot",
"notation"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
"time": "2024-07-08T12:26:09+00:00"
},
{
"name": "league/commonmark",
"version": "2.7.1",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca",
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/config": "^1.1.1",
"php": "^7.4 || ^8.0",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.31.1",
"commonmark/commonmark.js": "0.31.1",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
"ext-json": "*",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.8-dev"
}
},
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"homepage": "https://commonmark.thephpleague.com",
"keywords": [
"commonmark",
"flavored",
"gfm",
"github",
"github-flavored",
"markdown",
"md",
"parser"
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"time": "2025-07-20T12:47:49+00:00"
},
{
"name": "league/config",
"version": "v1.2.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/config.git",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"shasum": ""
},
"require": {
"dflydev/dot-access-data": "^3.0.1",
"nette/schema": "^1.2",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.2-dev"
}
},
"autoload": {
"psr-4": {
"League\\Config\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Define configuration arrays with strict schemas and access values with dot notation",
"homepage": "https://config.thephpleague.com",
"keywords": [
"array",
"config",
"configuration",
"dot",
"dot-access",
"nested",
"schema"
],
"support": {
"docs": "https://config.thephpleague.com/",
"issues": "https://github.com/thephpleague/config/issues",
"rss": "https://github.com/thephpleague/config/releases.atom",
"source": "https://github.com/thephpleague/config"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
}
],
"time": "2022-12-11T20:36:23+00:00"
},
{
"name": "mustache/mustache",
"version": "v3.0.0",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/mustache.php.git",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "A Mustache implementation in PHP.",
"homepage": "https://github.com/bobthecow/mustache.php",
"keywords": [
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
},
"time": "2025-06-28T18:28:20+00:00"
},
{
"name": "nette/schema",
"version": "v1.3.3",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004",
"reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004",
"shasum": ""
},
"require": {
"nette/utils": "^4.0",
"php": "8.1 - 8.5"
},
"require-dev": {
"nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.8"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "📐 Nette Schema: validating data structures against a given Schema.",
"homepage": "https://nette.org",
"keywords": [
"config",
"nette"
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.3.3"
},
"time": "2025-10-30T22:57:59+00:00"
},
{
"name": "nette/utils",
"version": "v4.0.8",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"shasum": ""
},
"require": {
"php": "8.0 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
},
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.8"
},
"time": "2025-08-06T21:43:34+00:00"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"support": {
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
"time": "2019-01-08T18:20:26+00:00"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-09-25T14:21:43+00:00"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.33.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2025-01-02T08:10:11+00:00"
}
],
"packages-dev": [],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"prefer-stable": false,
"prefer-lowest": false,
"platform": {},
"platform-dev": {},
"plugin-api-version": "2.9.0"
}

BIN
composer.phar Executable file

Binary file not shown.

25
config.json Normal file
View File

@ -0,0 +1,25 @@
{
"site_title": "CodePress",
"content_dir": "public/content",
"templates_dir": "engine/templates",
"default_page": "welkom",
"theme": {
"primary_color": "#0d6efd",
"navbar_style": "bg-primary"
},
"seo": {
"description": "CodePress CMS - Lightweight file-based content management system",
"keywords": "cms, php, content management, file-based"
},
"author": {
"name": "Edwin Noorlander",
"website": "https://noorlander.info",
"git": "https://git.noorlander.info/E.Noorlander/CodePress.git"
},
"features": {
"auto_link_pages": true,
"search_enabled": true,
"breadcrumbs_enabled": true
}
}

View File

@ -1,16 +0,0 @@
<?php
return [
'site_title' => 'CodePress',
'site_description' => 'A simple PHP CMS',
'base_url' => '/',
'content_dir' => __DIR__ . '/public/content',
'templates_dir' => __DIR__ . '/templates',
'cache_dir' => __DIR__ . '/cache',
'default_page' => 'home',
'error_404' => '404',
'markdown_enabled' => true,
'php_enabled' => true,
'bootstrap_version' => '5.3.0',
'jquery_version' => '3.7.1'
];

View File

@ -1,37 +0,0 @@
# CodePress Blog
Welkom op de persoonlijke blog van Edwin Noorlander. Hier deel ik mijn gedachten, ervaringen en kennis over technologie, open-source software en digitale transformatie.
## Categorieën
### Over Mij
- [Welkom, ik ben Edwin](?page=blog/over-mij/welkom " title="Lees meer over Edwin Noorlander") - Mijn persoonlijke verhaal en achtergrond
### Open Source
- [De Toekomst van ICT](?page=blog/open-source/de-toekomst-van-ict) - Hoe open source software de werkvloer transformeert
- [Standaardisatie](?page=blog/open-source/standaardisatie) - Het belang van standaarden en de tegenstelling tussen commerciële bedrijven en open-source gemeenschappen
### Leren & Ontwikkeling
- [Kennis boven Aantallen](?page=blog/leren/kennis-boven-aantallen) - Het cruciale belang van kennis boven personeelsaantallen in de werkkracht
### ICT & Bedrijfsvoering
- [Excel als Database](?page=blog/ict/excel-als-database) - Waarom grote bedrijven en overheden Access niet toestaan
### Micro-electronica
- [Wat is Arduino](?page=blog/micro-electronica/wat-is-arduino) - Open-source elektronisch platform voor interactieve projecten
- [Leren gaat niet over perfectie](?page=blog/micro-electronica/leren-gaat-niet-over-perfectie) - Passie voor electronica en open-source hardware/software
### Hardware & ICT
- [De ware aard van ICT](?page=blog/hardware/de-ware-aard-van-ict) - Meer dan alleen computers en software
### Retro Gaming
- [Commodore 64](?page=blog/retro-gaming/commodore-64) - Een les in creativiteit en innovatie in de game-industrie
### Linux & Open Source
- [Open Source Voorbeelden](?page=blog/linux/open-source-voorbeelden) - Vijf voorbeelden van open-source software in gebruik bij commerciële bedrijven
## Over Edwin Noorlander
Ik ben adviseur bij de overheid met een passie voor technologie, innovatie en open-source software. Met een achtergrond in micro-elektronica en diverse programmeertalen, deel ik graag mijn kennis en ervaringen om anderen te inspireren.
**Meer weten?** Bezoek mijn [persoonlijke website](https://noorlander.info) of volg mijn projecten op [GitLab](https://git.noorlander.info).

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,92 @@
<?php
/**
* SimpleTemplate - Lightweight template rendering engine
*
* Features:
* - Variable replacement with {{variable}} syntax
* - Unescaped HTML content with {{{variable}}} syntax
* - Conditional blocks with {{#variable}}...{{/variable}}
* - Negative conditionals with {{^variable}}...{{/variable}}
* - Partial includes with {{>partial}}
* - Simple string-based rendering (no external dependencies)
*/
class SimpleTemplate {
private $data;
/**
* Render template with data
*
* @param string $template Template content with placeholders
* @param array $data Data to populate template
* @return string Rendered template
*/
public static function render($template, $data) {
$instance = new self();
$instance->data = $data;
return $instance->renderTemplate($template);
}
/**
* Process template and replace placeholders
*
* @param string $template Template content
* @return string Processed template
*/
private function renderTemplate($template) {
// Handle partial includes first ({{>partial}})
$template = preg_replace_callback('/{{>([^}]+)}}/', [$this, 'replacePartial'], $template);
// Handle conditional blocks
foreach ($this->data as $key => $value) {
if (is_array($value) || (is_string($value) && !empty($value))) {
// Handle {{#key}}...{{/key}} blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match($pattern, $template, $matches)) {
$replacement = $matches[1];
$template = preg_replace($pattern, $replacement, $template);
}
// Handle {{^key}}...{{/key}} blocks (negative condition)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
} else {
// Handle empty blocks
$pattern = '/{{#' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s';
$template = preg_replace($pattern, '', $template);
// Handle {{^key}}...{{/key}} blocks (show when empty)
$pattern = '/{{\^' . preg_quote($key, '/') . '}}(.*?){{\/' . preg_quote($key, '/') . '}}/s';
if (preg_match_all($pattern, $template, $matches)) {
foreach ($matches[1] as $match) {
$template = preg_replace('/{{\^' . preg_quote($key, '/') . '}}.*?{{\/' . preg_quote($key, '/') . '}}/s', $match, $template, 1);
}
}
}
}
// Handle variable replacements
foreach ($this->data as $key => $value) {
// Handle triple braces for unescaped HTML content
if (strpos($template, '{{{' . $key . '}}}') !== false) {
$template = str_replace('{{{' . $key . '}}}', $value, $template);
}
// Handle double braces for escaped content
elseif (strpos($template, '{{' . $key . '}}') !== false) {
$template = str_replace('{{' . $key . '}}', htmlspecialchars($value, ENT_QUOTES, 'UTF-8'), $template);
}
}
return $template;
}
/**
* Replace partial includes with data values
*
* @param array $matches Regex matches from preg_replace_callback
* @return string Replacement content
*/
private function replacePartial($matches) {
$partialName = $matches[1];
return isset($this->data[$partialName]) ? $this->data[$partialName] : $matches[0];
}
}

View File

@ -1,16 +1,41 @@
<?php
return [
// Default configuration
$defaultConfig = [
'site_title' => 'CodePress',
'site_description' => 'A simple PHP CMS',
'base_url' => '/',
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates',
'cache_dir' => __DIR__ . '/../../cache',
'default_page' => 'home',
'error_404' => '404',
'markdown_enabled' => true,
'php_enabled' => true,
'bootstrap_version' => '5.3.0',
'jquery_version' => '3.7.1'
'default_page' => 'auto',
'homepage' => 'auto'
];
// Check for config.json in project root
$projectRoot = __DIR__ . '/../../';
$configJsonPath = $projectRoot . 'config.json';
if (file_exists($configJsonPath)) {
$jsonContent = file_get_contents($configJsonPath);
$jsonConfig = json_decode($jsonContent, true);
if (json_last_error() === JSON_ERROR_NONE && is_array($jsonConfig)) {
// Merge JSON config with defaults, converting relative paths to absolute
$mergedConfig = array_merge($defaultConfig, $jsonConfig);
// Convert relative paths to absolute paths (inline function to avoid redeclaration)
$isAbsolutePath = function($path) {
return (strpos($path, '/') === 0) || (preg_match('/^[A-Za-z]:/', $path));
};
if (isset($mergedConfig['content_dir']) && !$isAbsolutePath($mergedConfig['content_dir'])) {
$mergedConfig['content_dir'] = $projectRoot . $mergedConfig['content_dir'];
}
if (isset($mergedConfig['templates_dir']) && !$isAbsolutePath($mergedConfig['templates_dir'])) {
$mergedConfig['templates_dir'] = $projectRoot . $mergedConfig['templates_dir'];
}
return $mergedConfig;
}
}
// Fallback to default config
return $defaultConfig;

View File

@ -1,476 +1,33 @@
<?php
/**
* CodePress CMS Core Loader
*
* This file serves as the central entry point for the CodePress CMS core system.
* It loads all essential classes and configuration needed for the CMS to function.
*
* Architecture:
* - config.php: Configuration loader that merges default settings with config.json
* - SimpleTemplate.php: Lightweight template engine for rendering HTML with placeholders
* - CodePressCMS.php: Main CMS class handling content, navigation, search, and rendering
*
* Usage:
* This file is included by public/index.php, which then:
* 1. Loads configuration from config.php
* 2. Creates a new CodePressCMS instance with the config
* 3. Calls render() to output the complete page
*
* The separation allows for:
* - Clean public entry point (public/index.php)
* - Reusable core components
* - Proper class organization with PHPDoc documentation
*/
// Load configuration system - handles default settings and config.json merging
require_once 'config.php';
$config = include 'config.php';
// Load template engine - renders HTML with {{variable}} placeholders and conditionals
require_once 'class/SimpleTemplate.php';
class CodePressCMS {
private $config;
private $menu = [];
private $searchResults = [];
public function __construct($config) {
$this->config = $config;
$this->buildMenu();
if (isset($_GET['search'])) {
$this->performSearch($_GET['search']);
}
}
private function buildMenu() {
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
}
private function scanDirectory($dir, $prefix) {
if (!is_dir($dir)) return [];
$items = scandir($dir);
sort($items);
$result = [];
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$result[] = [
'type' => 'folder',
'title' => ucfirst($item),
'path' => $relativePath,
'children' => $this->scanDirectory($path, $relativePath)
];
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$result[] = [
'type' => 'file',
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath
];
}
}
return $result;
}
private function performSearch($query) {
$this->searchResults = [];
$this->searchInDirectory($this->config['content_dir'], '', $query);
}
private function searchInDirectory($dir, $prefix, $query) {
if (!is_dir($dir)) return;
$items = scandir($dir);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$this->searchInDirectory($path, $relativePath, $query);
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$content = file_get_contents($path);
if (stripos($content, $query) !== false || stripos($item, $query) !== false) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$this->searchResults[] = [
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath,
'snippet' => $this->createSnippet($content, $query)
];
}
}
}
}
private function createSnippet($content, $query) {
$content = strip_tags($content);
$pos = stripos($content, $query);
if ($pos === false) return substr($content, 0, 100) . '...';
$start = max(0, $pos - 50);
$snippet = substr($content, $start, 150);
return '...' . $snippet . '...';
}
public function getPage() {
if (isset($_GET['search'])) {
return $this->getSearchResults();
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
$filePath = $this->config['content_dir'] . '/' . $page;
$actualFilePath = null;
if (file_exists($filePath . '.md')) {
$actualFilePath = $filePath . '.md';
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif (file_exists($filePath . '.php')) {
$actualFilePath = $filePath . '.php';
$result = $this->parsePHP($actualFilePath);
} elseif (file_exists($filePath . '.html')) {
$actualFilePath = $filePath . '.html';
$result = $this->parseHTML(file_get_contents($actualFilePath));
} elseif (file_exists($filePath)) {
$actualFilePath = $filePath;
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
if ($extension === 'md') {
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif ($extension === 'php') {
$result = $this->parsePHP($actualFilePath);
} elseif ($extension === 'html') {
$result = $this->parseHTML(file_get_contents($actualFilePath));
}
}
if (isset($result) && $actualFilePath) {
$result['file_info'] = $this->getFileInfo($actualFilePath);
return $result;
}
return $this->getError404();
}
private function getFileInfo($filePath) {
if (!file_exists($filePath)) {
return null;
}
$stats = stat($filePath);
$created = date('d-m-Y H:i', $stats['ctime']);
$modified = date('d-m-Y H:i', $stats['mtime']);
return [
'created' => $created,
'modified' => $modified,
'size' => $this->formatFileSize($stats['size'])
];
}
private function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
private function getSearchResults() {
$query = $_GET['search'];
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
if (empty($this->searchResults)) {
$content .= '<p>No results found.</p>';
} else {
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
foreach ($this->searchResults as $result) {
$content .= '<div class="card mb-3">';
$content .= '<div class="card-body">';
$content .= '<h5 class="card-title"><a href="' . htmlspecialchars($result['url']) . '">' . htmlspecialchars($result['title']) . '</a></h5>';
$content .= '<p class="card-text text-muted">' . htmlspecialchars($result['path']) . '</p>';
$content .= '<p class="card-text">' . htmlspecialchars($result['snippet']) . '</p>';
$content .= '</div></div>';
}
}
return [
'title' => 'Search Results',
'content' => $content
];
}
private function parseMarkdown($content) {
$lines = explode("\n", $content);
$title = '';
$body = '';
$inBody = false;
foreach ($lines as $line) {
if (!$inBody && preg_match('/^#\s+(.+)$/', $line, $matches)) {
$title = $matches[1];
$inBody = true;
} elseif ($inBody || trim($line) !== '') {
$body .= $line . "\n";
$inBody = true;
}
}
$body = preg_replace('/### (.+)/', '<h3>$1</h3>', $body);
$body = preg_replace('/## (.+)/', '<h2>$1</h2>', $body);
$body = preg_replace('/# (.+)/', '<h1>$1</h1>', $body);
$body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body);
$body = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $body);
// Auto-link page titles to existing content pages (before markdown link processing)
$body = $this->autoLinkPageTitles($body);
// Convert Markdown links to HTML links
$body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $body);
// Convert relative internal links to CMS format
$body = preg_replace('/href="\/blog\/([^"]+)"/', 'href="?page=blog/$1"', $body);
$body = preg_replace('/href="\/([^"]+)"/', 'href="?page=$1"', $body);
$body = preg_replace('/\n\n/', '</p><p>', $body);
$body = '<p>' . $body . '</p>';
$body = preg_replace('/<p><\/p>/', '', $body);
$body = preg_replace('/<p>(<h[1-6]>)/', '$1', $body);
$body = preg_replace('/(<\/h[1-6]>)<\/p>/', '$1', $body);
return [
'title' => $title ?: 'Untitled',
'content' => $body
];
}
private function autoLinkPageTitles($content) {
// Get all available pages with their titles
$pages = $this->getAllPageTitles();
foreach ($pages as $pagePath => $pageTitle) {
// Create a pattern that matches the exact page title (case-insensitive)
// Use word boundaries to avoid partial matches
$pattern = '/\b' . preg_quote($pageTitle, '/') . '\b/i';
// Replace with link, but avoid linking inside existing links, headings, or markdown
$replacement = function($matches) use ($pageTitle, $pagePath) {
$text = $matches[0];
// Check if we're inside an existing link or markdown syntax
if (preg_match('/\[.*?\]\(.*?\)/', $text) ||
preg_match('/\[.*?\]:/', $text) ||
preg_match('/<a[^>]*>/', $text) ||
preg_match('/href=/', $text)) {
return $text; // Don't link existing links
}
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $text . '</a>';
};
$content = preg_replace_callback($pattern, $replacement, $content);
}
return $content;
}
private function getAllPageTitles() {
$pages = [];
$this->scanForPageTitles($this->config['content_dir'], '', $pages);
return $pages;
}
private function scanForPageTitles($dir, $prefix, &$pages) {
if (!is_dir($dir)) return;
$items = scandir($dir);
sort($items);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$this->scanForPageTitles($path, $relativePath, $pages);
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$title = $this->extractPageTitle($path);
if ($title && !empty(trim($title))) {
$pagePath = preg_replace('/\.[^.]+$/', '', $relativePath);
$pages[$pagePath] = $title;
}
}
}
}
private function extractPageTitle($filePath) {
$content = file_get_contents($filePath);
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
if ($extension === 'md') {
// Extract first H1 from Markdown
if (preg_match('/^#\s+(.+)$/m', $content, $matches)) {
return trim($matches[1]);
}
} elseif ($extension === 'php') {
// Extract title from PHP file
if (preg_match('/\$title\s*=\s*["\']([^"\']+)["\']/', $content, $matches)) {
return trim($matches[1]);
}
} elseif ($extension === 'html') {
// Extract title from HTML file
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
return trim(strip_tags($matches[1]));
}
if (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
return trim(strip_tags($matches[1]));
}
}
return null;
}
private function parsePHP($filePath) {
ob_start();
$title = 'Untitled';
include $filePath;
$content = ob_get_clean();
return [
'title' => $title,
'content' => $content
];
}
private function parseHTML($content) {
$title = 'Untitled';
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
} elseif (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
}
return [
'title' => $title,
'content' => $content
];
}
private function getError404() {
return [
'title' => 'Page Not Found',
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
];
}
public function getMenu() {
return $this->menu;
}
public function render() {
$page = $this->getPage();
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
$template = str_replace('{{menu}}', $menuHtml, $template);
echo $template;
}
private function getBreadcrumb() {
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">Search</li></ol></nav>';
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">Home</li></ol></nav>';
}
$parts = explode('/', $page);
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
$path = '';
foreach ($parts as $i => $part) {
$path .= ($path ? '/' : '') . $part;
$title = ucfirst($part);
if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
} else {
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
}
}
$breadcrumb .= '</ol></nav>';
return $breadcrumb;
}
private function renderMenu($items, $level = 0) {
$html = '';
foreach ($items as $item) {
if ($item['type'] === 'folder') {
$hasChildren = !empty($item['children']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
// Check if this folder contains the active page
$containsActive = $this->folderContainsActivePage($item['children']);
$ariaExpanded = $containsActive ? 'true' : 'false';
$collapseClass = $containsActive ? 'collapse show' : 'collapse';
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="' . $ariaExpanded . '">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 ' . $collapseClass . '" id="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
}
$html .= '</li>';
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
return $html;
}
private function folderContainsActivePage($children) {
foreach ($children as $child) {
if ($child['type'] === 'folder') {
if (!empty($child['children']) && $this->folderContainsActivePage($child['children'])) {
return true;
}
} else {
if (isset($_GET['page']) && $_GET['page'] === $child['path']) {
return true;
}
}
}
return false;
}
}
$cms = new CodePressCMS($config);
$cms->render();
// Load main CMS class - handles content parsing, navigation, search, and page rendering
require_once 'class/CodePressCMS.php';

View File

@ -0,0 +1,24 @@
<footer class="bg-light border-top py-3">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center">
<div class="file-info">
<i class="bi bi-file-text"></i>
<span class="page-title" title="{{page_title}}">{{page_title}}</span>
{{{file_info_block}}}
</div>
<div class="site-info">
<small class="text-muted">
<a href="?guide" class="guide-link" title="Handleiding">
<i class="bi bi-book"></i>
</a>
<span class="ms-2">|</span>
Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a>
</small>
</div>
</div>
</div>
</div>
</div>
</footer>

View File

@ -0,0 +1,19 @@
<header class="navbar navbar-expand-lg navbar-dark bg-primary">
<div class="container-fluid">
<a class="navbar-brand" href="?page={{default_page}}">
<img src="/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
{{site_title}}
</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#searchBar" aria-controls="searchBar" aria-expanded="false" aria-label="Toggle search">
<i class="bi bi-search"></i>
</button>
<div class="collapse navbar-collapse" id="searchBar">
<form class="d-flex ms-auto" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</header>

View File

@ -0,0 +1,16 @@
<nav class="navigation-section border-bottom navigation-50-opacity">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col">
<ul class="nav nav-tabs flex-wrap">
<li class="nav-item">
<a class="nav-link" href="?page={{homepage}}">
<i class="bi bi-house"></i> {{homepage_title}}
</a>
</li>
{{{menu}}}
</ul>
</div>
</div>
</div>
</nav>

View File

@ -0,0 +1,5 @@
<div class="html-content">
<div class="content-body">
{{{content}}}
</div>
</div>

View File

@ -1,440 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="/engine/assets/favicon.svg">
<link href="/engine/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="/engine/assets/css/bootstrap-icons.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
position: relative;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
min-height: calc(100vh - 70px); /* Minus header height */
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto;
flex-shrink: 0;
transition: transform 0.3s ease;
position: fixed;
top: 70px;
left: 0;
height: calc(100vh - 140px); /* 70px header + 70px footer */
z-index: 999;
transform: translateX(0);
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: absolute;
top: 15px;
right: 15px;
z-index: 1001;
background: none;
border: none;
cursor: pointer;
transition: all 0.3s ease;
font-size: 20px;
color: #6c757d;
width: 30px;
height: 30px;
display: flex;
align-items: center;
justify-content: center;
border-radius: 4px;
}
.sidebar-toggle:hover {
color: #0d6efd;
background-color: #f8f9fa;
}
.sidebar-toggle-inner {
/* Toggle inside sidebar */
}
.sidebar-toggle-outer {
position: fixed;
top: 90px;
left: 20px;
z-index: 1001;
background-color: white;
border: 1px solid #dee2e6;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.sidebar.collapsed .sidebar-toggle-inner {
right: auto;
left: 15px;
}
.sidebar.collapsed ~ .main-content .sidebar-toggle-outer {
display: block !important;
}
.sidebar:not(.collapsed) ~ .main-content .sidebar-toggle-outer {
display: none !important;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: all 0.3s ease;
margin-left: 250px;
}
.sidebar.collapsed ~ .main-content {
margin-left: 0;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: margin-left 0.3s ease;
}
.folder-toggle {
font-weight: bold;
color: #212529 !important;
cursor: pointer;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
.folder-toggle:hover {
background-color: #e9ecef !important;
}
.folder-toggle[aria-expanded="true"] {
background-color: #dee2e6 !important;
color: #212529 !important;
font-weight: 600;
}
/* Progressive background colors for nested folders */
.nav .nav .folder-toggle {
background-color: #f1f3f4 !important;
}
.nav .nav .nav .folder-toggle {
background-color: #eaedee !important;
}
.nav .nav .nav .nav .folder-toggle {
background-color: #e3e7e8 !important;
}
.nav .nav .nav .nav .nav .folder-toggle {
background-color: #dce1e2 !important;
}
.nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #d5dbdd !important;
}
.nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #ced5d8 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c7cfd3 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c0c9ce !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #b9c3c9 !important;
}
.folder-toggle .arrow {
margin-right: 8px;
transition: transform 0.2s;
font-size: 0.8em;
}
.folder-toggle[aria-expanded="true"] .arrow {
transform: rotate(90deg);
}
.page-link {
color: #495057 !important;
font-weight: 500;
padding: 0.5rem 0.75rem;
padding-left: 2rem;
background-color: #ffffff !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
/* Progressive background colors for nested pages */
.nav .nav .page-link {
background-color: #fafbfc !important;
}
.nav .nav .nav .page-link {
background-color: #f5f7f8 !important;
}
.nav .nav .nav .nav .page-link {
background-color: #f0f3f4 !important;
}
.nav .nav .nav .nav .nav .page-link {
background-color: #ebefef !important;
}
.nav .nav .nav .nav .nav .nav .page-link {
background-color: #e6eaea !important;
}
.nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #e1e5e5 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #dce0e0 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d7dbdb !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d2d6d6 !important;
}
.page-link:hover {
color: #212529 !important;
background-color: #f8f9fa !important;
}
.nav-link.active {
background-color: #0d6efd !important;
color: #212529 !important;
font-weight: 600;
}
.file-info {
font-size: 0.9rem;
color: #6c757d;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
transition: all 0.2s ease;
}
.auto-link:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
.search-form {
max-width: 300px;
}
.card-title a {
text-decoration: none;
color: inherit;
}
.card-title a:hover {
text-decoration: underline;
}
.folder-disabled {
font-weight: 500;
color: #6c757d !important;
cursor: not-allowed;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
opacity: 0.7;
}
.folder-disabled .arrow {
margin-right: 8px;
font-size: 0.8em;
opacity: 0.6;
}
@media (max-width: 768px) {
.sidebar {
width: 200px;
}
}
</style>
</head>
<body>
<header class="bg-primary text-white py-3">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col">
<div class="d-flex align-items-center">
<img src="/engine/assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<h1 class="h3 mb-0">{{site_title}}</h1>
</div>
</div>
<div class="col-auto">
<form class="d-flex" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</div>
</header>
<div class="main-wrapper">
<div class="content-wrapper">
<nav class="sidebar" id="sidebar">
<div class="sidebar-toggle sidebar-toggle-inner" id="sidebarToggleInner">
<i class="bi bi-list"></i>
</div>
<div class="pt-3">
<ul class="nav flex-column">
{{menu}}
</ul>
</div>
</nav>
<main class="main-content">
<div class="sidebar-toggle sidebar-toggle-outer" id="sidebarToggleOuter" style="display: none;">
<i class="bi bi-list"></i>
</div>
<div>
{{breadcrumb}}
</div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h2>{{page_title}}</h2>
</div>
<div class="content">
{{content}}
</div>
</main>
</div>
</div>
<footer class="bg-light border-top py-3" style="position: fixed; bottom: 0; left: 0; right: 0; z-index: 998;">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center">
<div class="file-info">
{{file_info}}
</div>
<div class="site-info">
<small class="text-muted">Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a></small>
</div>
</div>
</div>
</div>
</div>
</footer>
<script src="/engine/assets/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle functionality
const sidebarToggleInner = document.getElementById('sidebarToggleInner');
const sidebarToggleOuter = document.getElementById('sidebarToggleOuter');
const sidebar = document.getElementById('sidebar');
// Initialize sidebar state (open by default)
sidebar.classList.remove('collapsed');
const innerIcon = sidebarToggleInner.querySelector('i');
const outerIcon = sidebarToggleOuter.querySelector('i');
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
function toggleSidebar() {
sidebar.classList.toggle('collapsed');
// Change icons
if (sidebar.classList.contains('collapsed')) {
innerIcon.classList.remove('bi-x');
innerIcon.classList.add('bi-list');
outerIcon.classList.remove('bi-x');
outerIcon.classList.add('bi-list');
} else {
innerIcon.classList.remove('bi-list');
innerIcon.classList.add('bi-x');
outerIcon.classList.remove('bi-list');
outerIcon.classList.add('bi-x');
}
}
sidebarToggleInner.addEventListener('click', toggleSidebar);
sidebarToggleOuter.addEventListener('click', toggleSidebar);
// Folders are now automatically expanded by PHP if they contain the active page
// Close other folders when opening a new one
const folderToggles = document.querySelectorAll('.folder-toggle');
folderToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-bs-target');
const targetCollapse = document.querySelector(targetId);
const isExpanded = this.getAttribute('aria-expanded') === 'true';
if (!isExpanded && targetCollapse) {
// Close all other folders first
folderToggles.forEach(otherToggle => {
if (otherToggle !== this) {
const otherTargetId = otherToggle.getAttribute('data-bs-target');
if (otherTargetId) {
const otherCollapse = document.querySelector(otherTargetId);
if (otherCollapse) {
otherCollapse.classList.remove('show');
otherToggle.setAttribute('aria-expanded', 'false');
// Reset arrow
const otherArrow = otherToggle.querySelector('.arrow');
if (otherArrow) {
otherArrow.style.transform = '';
}
}
}
}
});
// Open this folder
targetCollapse.classList.add('show');
this.setAttribute('aria-expanded', 'true');
// Rotate arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = 'rotate(90deg)';
}
} else if (isExpanded && targetCollapse) {
// Close this folder
targetCollapse.classList.remove('show');
this.setAttribute('aria-expanded', 'false');
// Reset arrow
const arrow = this.querySelector('.arrow');
if (arrow) {
arrow.style.transform = '';
}
}
});
});
});
</script>
</body>
</html>

View File

@ -0,0 +1,59 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<!-- CMS Meta Tags -->
<meta name="generator" content="{{site_title}} CMS">
<meta name="application-name" content="{{site_title}}">
<meta name="author" content="{{author_name}}">
<meta name="creator" content="{{author_name}}">
<meta name="publisher" content="{{author_name}}">
<!-- SEO Meta Tags -->
<meta name="description" content="{{seo_description}}">
<meta name="keywords" content="{{seo_keywords}}">
<!-- Author Links -->
<link rel="author" href="{{author_website}}">
<link rel="me" href="{{author_git}}">
<!-- Favicon and Styles -->
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
<link href="/assets/css/bootstrap.min.css" rel="stylesheet">
<link href="/assets/css/bootstrap-icons.css" rel="stylesheet">
<link href="/assets/css/style.css" rel="stylesheet">
</head>
<body>
{{>header}}
{{>navigation}}
<div class="breadcrumb-section bg-light border-bottom">
<div class="container-fluid">
<div class="row">
<div class="col-12 py-2">
{{{breadcrumb}}}
</div>
</div>
</div>
</div>
<div class="container-fluid main-content" style="padding-bottom: 80px;">
<div class="container">
<div class="row">
<main class="col-12">
{{>content_template}}
</main>
</div>
</div>
</div>
{{>footer}}
<script src="/assets/js/bootstrap.bundle.min.js"></script>
<script src="/assets/js/app.js"></script>
</body>
</html>

View File

@ -0,0 +1,5 @@
<div class="markdown-content">
<div class="content-body">
{{{content}}}
</div>
</div>

View File

@ -0,0 +1,5 @@
<div class="php-content">
<div class="content-body">
{{{content}}}
</div>
</div>

171
guide/en.md Normal file
View File

@ -0,0 +1,171 @@
# CodePress CMS Guide
## Welcome to CodePress CMS
CodePress is a lightweight, file-based Content Management System built with PHP and Bootstrap.
### Table of Contents
1. [Getting Started](#getting-started)
2. [Content Management](#content-management)
3. [Templates](#templates)
4. [Configuration](#configuration)
---
## Getting Started
### Requirements
- PHP 8.4+
- Web server (Apache/Nginx)
- Modern web browser
### Installation
1. Clone or download the CodePress files
2. Upload to your web server
3. Make sure the `content/` directory is writable
4. Navigate to your website in the browser
### Basic Configuration
The most important settings are in `engine/core/config.php`:
```php
$config = [
'site_title' => 'My Website',
'default_page' => 'home',
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates'
];
```
---
## Content Management
### File Structure
```
content/
├── home.md # Home page
├── blog/
│ ├── index.md # Blog overview
│ ├── article-1.md # Blog article
│ └── category/
│ └── article.md # Article in category
└── about-us/
└── info.md # About us page
```
### Content Types
CodePress supports three content types:
#### Markdown (`.md`)
```markdown
# Page Title
This is the page content in **Markdown** format.
## Subsection
- List item 1
- List item 2
```
#### PHP (`.php`)
```php
<?php
$title = "Dynamic Page";
?>
<h1><?php echo $title; ?></h1>
<p>This is dynamic content with PHP.</p>
```
#### HTML (`.html`)
```html
<h1>HTML Page</h1>
<p>This is static HTML content.</p>
```
### Automatic Linking
CodePress automatically creates links to other pages when you mention page names in your content.
---
## Templates
### Template Structure
CodePress uses Mustache-compatible templates:
- `layout.mustache` - Main template
- `assets/header.mustache` - Header component
- `assets/sidebar.mustache` - Sidebar navigation
- `assets/footer.mustache` - Footer component
### Template Variables
Available variables in templates:
- `{{site_title}}` - Website title
- `{{page_title}}` - Current page title
- `{{content}}` - Page content
- `{{menu}}` - Navigation menu
- `{{breadcrumb}}` - Breadcrumb navigation
---
## Configuration
### Basic Settings
Edit `engine/core/config.php` for your website:
```php
$config = [
'site_title' => 'Your Website Name',
'default_page' => 'home', // Default start page
'content_dir' => __DIR__ . '/../../content',
'templates_dir' => __DIR__ . '/../templates'
];
```
### SEO Friendly URLs
CodePress automatically generates clean URLs:
- `home.md``/home`
- `blog/article.md``/blog/article`
### Search Functionality
The built-in search function searches through:
- File names
- Content of Markdown/PHP/HTML files
---
## Tips and Tricks
### Page Organization
- Use subdirectories for categories
- Give each directory an `index.md` for an overview page
- Keep file names short and descriptive
### Content Optimization
- Use clear headings (H1, H2, H3)
- Add descriptive meta information
- Use internal links for better navigation
### Security
- Keep your CodePress installation updated
- Restrict write permissions on the `content/` directory
- Use HTTPS when possible
---
## Support
### Troubleshooting
- **Empty pages**: Check file permissions
- **Template errors**: Verify template syntax
- **404 errors**: Check file names and paths
### More Information
- Documentation: [CodePress GitHub](https://git.noorlander.info/E.Noorlander/CodePress.git)
- Issues and feature requests: GitHub Issues
---
*This guide is part of CodePress CMS and is automatically displayed when no content is available.*

227
guide/nl.md Normal file
View File

@ -0,0 +1,227 @@
# CodePress CMS Handleiding
## Overzicht
CodePress CMS is een lichtgewicht, file-based content management systeem gebouwd met PHP. Het is ontworpen om eenvoudig te gebruiken, flexibel te zijn en zonder database te werken.
## Functies
### 🏠 **Navigatie**
- **Tab-style navigatie** met Bootstrap styling
- **Dropdown menus** voor mappen en sub-mappen
- **Home knop** met icoon die linkt naar de ingestelde homepage
- **Automatische menu generatie** op basis van content structuur
- **Responsive design** voor mobiele apparaten
### 📄 **Content Types**
- **Markdown (.md)** - Met auto-linking tussen pagina's
- **PHP (.php)** - Voor dynamische content en functionaliteit
- **HTML (.html)** - Voor statische HTML pagina's
- **Automatische template selectie** op basis van bestandstype
### 🔍 **Zoekfunctionaliteit**
- **Volledige tekst zoek** door alle content bestanden
- **Resultaten met snippets** en highlighting
- **Directe navigatie** naar gevonden pagina's
- **SEO-vriendelijke** zoekresultaten
### 🧭 **Configuratie**
- **JSON configuratie** in project root (`config.json`)
- **Dynamische homepage** instelling of automatische detectie
- **SEO instellingen** (description, keywords)
- **Author informatie** met links naar website en Git
- **Thema configuratie** mogelijkheden
### 🎨 **Layout & Design**
- **Flexbox layout** voor moderne, responsive structuur
- **Fixed header** met logo en zoekfunctie
- **Breadcrumb navigatie** tussen header en content
- **Fixed footer** met file info en links
- **Bootstrap 5** styling en componenten
- **Custom CSS** voor specifieke styling
### 📱 **Responsive Features**
- **Mobile-first** aanpak
- **Hamburger menu** voor kleine schermen
- **Touch-friendly** dropdowns en navigatie
- **Adaptieve breedtes** voor verschillende schermgroottes
## Installatie
1. **Upload bestanden** naar webserver
2. **Stel permissies in** voor webserver
3. **Configureer** `config.json` indien nodig
4. **Toegang** tot website via browser
## Configuratie
### Basis Configuratie (`config.json`)
```json
{
"site_title": "CodePress",
"content_dir": "content",
"templates_dir": "engine/templates",
"default_page": "auto",
"homepage": "welkom",
"author": {
"name": "Edwin Noorlander",
"website": "https://noorlander.info",
"git": "https://git.noorlander.info/E.Noorlander/CodePress.git"
},
"seo": {
"description": "CodePress CMS - Lightweight file-based content management system",
"keywords": "cms, php, content management, file-based"
}
}
```
### Configuratie Opties
- **`site_title`** - Naam van de website
- **`content_dir`** - Map met content bestanden
- **`templates_dir`** - Map met template bestanden
- **`default_page`** - Standaard pagina (`"auto"` voor automatische detectie)
- **`homepage`** - Specifieke homepage (`"auto"` voor automatische detectie)
- **`author`** - Auteur informatie met links
- **`seo`** - SEO instellingen
## Content Structuur
### Bestandsstructuur
```
content/
├── map1/
│ ├── submap1/
│ │ ├── pagina1.md
│ │ └── pagina2.php
│ └── pagina3.html
├── map2/
│ └── pagina4.md
├── homepage.md
└── index.html
```
### Bestandsnamen
- **Gebruik lowercase** bestandsnamen
- **Geen spaties** - gebruik `-` of `_` als scheidingsteken
- **Logische extensies** - `.md`, `.php`, `.html`
- **Unieke namen** - geen duplicaten binnen dezelfde map
## Templates
### Template Structuur
```
engine/templates/
├── layout.mustache - Hoofd layout template
├── assets/
│ ├── header.mustache - Header template
│ ├── navigation.mustache - Navigatie template
│ └── footer.mustache - Footer template
├── markdown_content.mustache - Markdown content template
├── php_content.mustache - PHP content template
└── html_content.mustache - HTML content template
```
### Template Variabelen
- **`{{site_title}}`** - Website titel
- **`{{page_title}}`** - Pagina titel
- **`{{content}}`** - Content (HTML)
- **`{{menu}}`** - Navigatie menu
- **`{{breadcrumb}}`** - Breadcrumb navigatie
- **`{{search_query}}`** - Zoekopdracht
- **`{{homepage}}`** - Homepage link
- **`{{author_*}}`** - Auteur informatie
- **`{{seo_*}}`** - SEO informatie
## SEO Optimalisatie
### Meta Tags
De CMS voegt automatisch de volgende meta tags toe:
```html
<meta name="generator" content="CodePress CMS">
<meta name="application-name" content="CodePress">
<meta name="author" content="Edwin Noorlander">
<meta name="description" content="...">
<meta name="keywords" content="...">
<link rel="author" href="https://noorlander.info">
<link rel="me" href="https://git.noorlander.info/E.Noorlander/CodePress.git">
```
### Auto-linking
De CMS linkt automatisch pagina titels naar hun content:
- **Automatische detectie** van pagina titels in tekst
- **Slimme links** met `title` attributen
- **Geen dubbele links** voor dezelfde pagina
- **SEO-vriendelijke** URL structuur
## Veelgestelde Vragen
### Hoe stel ik de homepage in?
1. **Automatisch**: Laat de CMS het eerste bestand kiezen
2. **Handmatig**: Stel `"homepage": "pagina-naam"` in `config.json`
3. **Flexibel**: Werkt met elk bestandstype (md, php, html)
### Hoe werkt de navigatie?
- **Mappen** worden dropdown menus
- **Bestanden** worden directe links
- **Sub-mappen** worden geneste dropdowns
- **Home knop** linkt altijd naar de homepage
### Hoe voeg ik nieuwe content toe?
1. **Upload** bestanden naar de `content/` map
2. **Organiseer** in logische mappen
3. **Gebruik** juiste bestandsnamen en extensies
4. **Herlaad** de pagina om de navigatie te vernieuwen
### Kan ik custom CSS gebruiken?
Ja! Voeg custom CSS toe aan:
- **`/public/assets/css/style.css`** - Voor algemene styling
- **Template bestanden** - Voor specifieke componenten
- **Inline styles** - In content bestanden indien nodig
## Troubleshooting
### Pagina niet gevonden (404)
1. **Controleer** bestandsnaam en pad
2. **Controleer** bestandsextensie (.md, .php, .html)
3. **Controleer** permissies van bestanden
4. **Controleer** `config.json` syntax
### Template niet geladen
1. **Controleer** template bestandsnamen
2. **Controleer** template map permissies
3. **Controleer** PHP error logs
4. **Controleer** `templates_dir` configuratie
### Navigatie niet bijgewerkt
1. **Herlaad** de pagina
2. **Controleer** content map structuur
3. **Controleer** bestandsnamen (geen spaties)
4. **Controleer** PHP cache indien aanwezig
## Ondersteuning
Voor technische ondersteuning:
- **GitHub**: https://git.noorlander.info/E.Noorlander/CodePress.git
- **Website**: https://noorlander.info
- **Issues**: Rapporteer problemen via GitHub issues
## Licentie
CodePress CMS is open-source software. Controleer de licentie in de repository voor meer informatie.

464
index.php
View File

@ -1,464 +0,0 @@
<?php
require_once 'config.php';
$config = include 'config.php';
class CodePressCMS {
private $config;
private $menu = [];
private $searchResults = [];
public function __construct($config) {
$this->config = $config;
$this->buildMenu();
if (isset($_GET['search'])) {
$this->performSearch($_GET['search']);
}
}
private function buildMenu() {
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
}
private function scanDirectory($dir, $prefix) {
if (!is_dir($dir)) return [];
$items = scandir($dir);
sort($items);
$result = [];
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$result[] = [
'type' => 'folder',
'title' => ucfirst($item),
'path' => $relativePath,
'children' => $this->scanDirectory($path, $relativePath)
];
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$result[] = [
'type' => 'file',
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath
];
}
}
return $result;
}
private function performSearch($query) {
$this->searchResults = [];
$this->searchInDirectory($this->config['content_dir'], '', $query);
}
private function searchInDirectory($dir, $prefix, $query) {
if (!is_dir($dir)) return;
$items = scandir($dir);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$this->searchInDirectory($path, $relativePath, $query);
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$content = file_get_contents($path);
if (stripos($content, $query) !== false || stripos($item, $query) !== false) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$this->searchResults[] = [
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath,
'snippet' => $this->createSnippet($content, $query)
];
}
}
}
}
private function createSnippet($content, $query) {
$content = strip_tags($content);
$pos = stripos($content, $query);
if ($pos === false) return substr($content, 0, 100) . '...';
$start = max(0, $pos - 50);
$snippet = substr($content, $start, 150);
return '...' . $snippet . '...';
}
public function getPage() {
if (isset($_GET['search'])) {
return $this->getSearchResults();
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
$filePath = $this->config['content_dir'] . '/' . $page;
$actualFilePath = null;
if (file_exists($filePath . '.md')) {
$actualFilePath = $filePath . '.md';
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif (file_exists($filePath . '.php')) {
$actualFilePath = $filePath . '.php';
$result = $this->parsePHP($actualFilePath);
} elseif (file_exists($filePath . '.html')) {
$actualFilePath = $filePath . '.html';
$result = $this->parseHTML(file_get_contents($actualFilePath));
} elseif (file_exists($filePath)) {
$actualFilePath = $filePath;
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
if ($extension === 'md') {
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif ($extension === 'php') {
$result = $this->parsePHP($actualFilePath);
} elseif ($extension === 'html') {
$result = $this->parseHTML(file_get_contents($actualFilePath));
}
}
if (isset($result) && $actualFilePath) {
$result['file_info'] = $this->getFileInfo($actualFilePath);
return $result;
}
return $this->getError404();
}
private function getFileInfo($filePath) {
if (!file_exists($filePath)) {
return null;
}
$stats = stat($filePath);
$created = date('d-m-Y H:i', $stats['ctime']);
$modified = date('d-m-Y H:i', $stats['mtime']);
return [
'created' => $created,
'modified' => $modified,
'size' => $this->formatFileSize($stats['size'])
];
}
private function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
private function getSearchResults() {
$query = $_GET['search'];
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
if (empty($this->searchResults)) {
$content .= '<p>No results found.</p>';
} else {
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
foreach ($this->searchResults as $result) {
$content .= '<div class="card mb-3">';
$content .= '<div class="card-body">';
$content .= '<h5 class="card-title"><a href="' . htmlspecialchars($result['url']) . '">' . htmlspecialchars($result['title']) . '</a></h5>';
$content .= '<p class="card-text text-muted">' . htmlspecialchars($result['path']) . '</p>';
$content .= '<p class="card-text">' . htmlspecialchars($result['snippet']) . '</p>';
$content .= '</div></div>';
}
}
return [
'title' => 'Search Results',
'content' => $content
];
}
private function parseMarkdown($content) {
$lines = explode("\n", $content);
$title = '';
$body = '';
$inBody = false;
foreach ($lines as $line) {
if (!$inBody && preg_match('/^#\s+(.+)$/', $line, $matches)) {
$title = $matches[1];
$inBody = true;
} elseif ($inBody || trim($line) !== '') {
$body .= $line . "\n";
$inBody = true;
}
}
$body = preg_replace('/### (.+)/', '<h3>$1</h3>', $body);
$body = preg_replace('/## (.+)/', '<h2>$1</h2>', $body);
$body = preg_replace('/# (.+)/', '<h1>$1</h1>', $body);
$body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body);
$body = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $body);
// Auto-link page titles to existing content pages (before markdown link processing)
$body = $this->autoLinkPageTitles($body);
// Convert Markdown links to HTML links
$body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $body);
// Convert relative internal links to CMS format
$body = preg_replace('/href="\/blog\/([^"]+)"/', 'href="?page=blog/$1"', $body);
$body = preg_replace('/href="\/([^"]+)"/', 'href="?page=$1"', $body);
$body = preg_replace('/\n\n/', '</p><p>', $body);
$body = '<p>' . $body . '</p>';
$body = preg_replace('/<p><\/p>/', '', $body);
$body = preg_replace('/<p>(<h[1-6]>)/', '$1', $body);
$body = preg_replace('/(<\/h[1-6]>)<\/p>/', '$1', $body);
return [
'title' => $title ?: 'Untitled',
'content' => $body
];
}
private function autoLinkPageTitles($content) {
// Get all available pages with their titles
$pages = $this->getAllPageTitles();
foreach ($pages as $pagePath => $pageTitle) {
// Create a pattern that matches the exact page title (case-insensitive)
// Use word boundaries to avoid partial matches
$pattern = '/\b' . preg_quote($pageTitle, '/') . '\b/i';
// Replace with link, but avoid linking inside existing links, headings, or markdown
$replacement = function($matches) use ($pageTitle, $pagePath) {
$text = $matches[0];
// Check if we're inside an existing link or markdown syntax
if (preg_match('/\[.*?\]\(.*?\)/', $text) ||
preg_match('/\[.*?\]:/', $text) ||
preg_match('/<a[^>]*>/', $text) ||
preg_match('/href=/', $text)) {
return $text; // Don't link existing links
}
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $text . '</a>';
};
$content = preg_replace_callback($pattern, $replacement, $content);
}
return $content;
}
return '<a href="?page=' . $pagePath . '" class="auto-link" title="Ga naar ' . htmlspecialchars($pageTitle) . '">' . $matches[0] . '</a>';
};
$content = preg_replace_callback($pattern, $replacement, $content);
}
return $content;
}
private function getAllPageTitles() {
$pages = [];
$this->scanForPageTitles($this->config['content_dir'], '', $pages);
return $pages;
}
private function scanForPageTitles($dir, $prefix, &$pages) {
if (!is_dir($dir)) return;
$items = scandir($dir);
sort($items);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$this->scanForPageTitles($path, $relativePath, $pages);
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$title = $this->extractPageTitle($path);
if ($title && !empty(trim($title))) {
$pagePath = preg_replace('/\.[^.]+$/', '', $relativePath);
$pages[$pagePath] = $title;
}
}
}
}
private function extractPageTitle($filePath) {
$content = file_get_contents($filePath);
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
if ($extension === 'md') {
// Extract first H1 from Markdown
if (preg_match('/^#\s+(.+)$/m', $content, $matches)) {
return trim($matches[1]);
}
} elseif ($extension === 'php') {
// Extract title from PHP file
if (preg_match('/\$title\s*=\s*["\']([^"\']+)["\']/', $content, $matches)) {
return trim($matches[1]);
}
} elseif ($extension === 'html') {
// Extract title from HTML file
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
return trim(strip_tags($matches[1]));
}
if (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
return trim(strip_tags($matches[1]));
}
}
return null;
}
private function parsePHP($filePath) {
ob_start();
$title = 'Untitled';
include $filePath;
$content = ob_get_clean();
return [
'title' => $title,
'content' => $content
];
}
private function parseHTML($content) {
$title = 'Untitled';
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
} elseif (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
}
return [
'title' => $title,
'content' => $content
];
}
private function getError404() {
return [
'title' => 'Page Not Found',
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
];
}
public function getMenu() {
return $this->menu;
}
public function render() {
$page = $this->getPage();
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
$template = str_replace('{{menu}}', $menuHtml, $template);
echo $template;
}
private function getBreadcrumb() {
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">Search</li></ol></nav>';
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">Home</li></ol></nav>';
}
$parts = explode('/', $page);
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
$path = '';
foreach ($parts as $i => $part) {
$path .= ($path ? '/' : '') . $part;
$title = ucfirst($part);
if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
} else {
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
}
}
$breadcrumb .= '</ol></nav>';
return $breadcrumb;
}
private function renderMenu($items, $level = 0) {
$html = '';
foreach ($items as $item) {
if ($item['type'] === 'folder') {
$hasChildren = !empty($item['children']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="false">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 collapse" id="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
}
$html .= '</li>';
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
return $html;
}
}
$cms = new CodePressCMS($config);
$cms->render();

25
package.json Normal file
View File

@ -0,0 +1,25 @@
{
"name": "codepress",
"version": "1.0.0",
"description": "A lightweight, file-based Content Management System built with PHP and Bootstrap.",
"main": "index.js",
"scripts": {
"build:css": "npx sass --load-path=node_modules src/scss/main.scss public/assets/css/style.css --style=compressed",
"watch:css": "npx sass --load-path=node_modules --watch src/scss/main.scss public/assets/css/style.css",
"build": "npm run build:css",
"clean": "rm -rf node_modules package-lock.json",
"test": "echo \"Error: no test specified\" && exit 1"
},
"repository": {
"type": "git",
"url": "https://git.noorlander.info/E.Noorlander/CodePress.git"
},
"keywords": [],
"author": "",
"license": "ISC",
"type": "commonjs",
"dependencies": {
"bootstrap": "^5.3.8",
"sass": "^1.94.2"
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

View File

Before

Width:  |  Height:  |  Size: 442 B

After

Width:  |  Height:  |  Size: 442 B

41
public/assets/js/app.js Normal file
View File

@ -0,0 +1,41 @@
// Main application JavaScript
// This file contains general application functionality
// Initialize application when DOM is ready
document.addEventListener('DOMContentLoaded', function() {
console.log('CodePress CMS initialized');
// Handle nested dropdowns for touch devices
const dropdownSubmenus = document.querySelectorAll('.dropdown-submenu');
dropdownSubmenus.forEach(function(submenu) {
const toggle = submenu.querySelector('.dropdown-toggle');
const dropdown = submenu.querySelector('.dropdown-menu');
if (toggle && dropdown) {
// Prevent default link behavior
toggle.addEventListener('click', function(e) {
e.preventDefault();
e.stopPropagation();
// Close other submenus at the same level
const parent = submenu.parentElement;
parent.querySelectorAll('.dropdown-submenu').forEach(function(sibling) {
if (sibling !== submenu) {
sibling.querySelector('.dropdown-menu').classList.remove('show');
}
});
// Toggle current submenu
dropdown.classList.toggle('show');
});
// Close submenu when clicking outside
document.addEventListener('click', function(e) {
if (!submenu.contains(e.target)) {
dropdown.classList.remove('show');
}
});
}
});
});

View File

@ -0,0 +1,7 @@
# Leren
Welkom bij de leersectie. Hier vind je verschillende artikelen over leren en persoonlijke ontwikkeling.
## Beschikbare Artikelen
- [Het Cruciale Belang van Kennis boven Aantallen in de Werkkracht](?page=leren/kennis-boven-aantallen)

View File

@ -0,0 +1,3 @@
<?php
echo phpinfo();
?>

View File

@ -26,7 +26,7 @@ Momenteel werk ik als adviseur bij de overheid, waar ik mijn kennis en ervaring
Mijn ultieme doel is om anderen te inspireren die zich in een vergelijkbare positie bevinden, met een handicap zoals dyslexie en ADHD, om zichzelf te blijven ontwikkelen en te leren wat ze willen leren. Ik geloof dat het nooit te laat is om te beginnen met leren en dat er altijd manieren zijn om je doelen te bereiken, zelfs als de weg er naartoe uitdagend lijkt.
Daarom heb ik deze blog gecreëerd. Hier wil ik graag mijn kennis en ervaringen delen en anderen aanmoedigen om hun passie te volgen en te blijven leren, ongeacht welke obstakels zich voordoen.
Daarom heb ik deze blog gecreëerd. Hier wil ik graag mijn kennis en ervaringen delen en anderen aanmoedigen om hun passie te volgen en te blijven leren, en open-source ondersteunen en bijdragen waar het kan. Ongeacht welke obstakels zich voordoen.
Dankjewel voor het bezoeken van mijn blog, en ik hoop je snel weer terug te zien!

View File

@ -1 +0,0 @@
../engine

View File

@ -1,442 +1,9 @@
<?php
require_once __DIR__ . '/../engine/core/config.php';
require_once __DIR__ . '/../engine/core/index.php';
$config = include __DIR__ . '/../engine/core/config.php';
class CodePressCMS {
private $config;
private $menu = [];
private $searchResults = [];
public function __construct($config) {
$this->config = $config;
$this->buildMenu();
if (isset($_GET['search'])) {
$this->performSearch($_GET['search']);
}
}
private function buildMenu() {
$this->menu = $this->scanDirectory($this->config['content_dir'], '');
}
private function scanDirectory($dir, $prefix) {
if (!is_dir($dir)) return [];
$items = scandir($dir);
sort($items);
$result = [];
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$result[] = [
'type' => 'folder',
'title' => ucfirst($item),
'path' => $relativePath,
'children' => $this->scanDirectory($path, $relativePath)
];
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$result[] = [
'type' => 'file',
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath
];
}
}
return $result;
}
private function performSearch($query) {
$this->searchResults = [];
$this->searchInDirectory($this->config['content_dir'], '', $query);
}
private function searchInDirectory($dir, $prefix, $query) {
if (!is_dir($dir)) return;
$items = scandir($dir);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dir . '/' . $item;
$relativePath = $prefix ? $prefix . '/' . $item : $item;
if (is_dir($path)) {
$this->searchInDirectory($path, $relativePath, $query);
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
$content = file_get_contents($path);
if (stripos($content, $query) !== false || stripos($item, $query) !== false) {
$title = ucfirst(pathinfo($item, PATHINFO_FILENAME));
$this->searchResults[] = [
'title' => $title,
'path' => $relativePath,
'url' => '?page=' . $relativePath,
'snippet' => $this->createSnippet($content, $query)
];
}
}
}
}
private function createSnippet($content, $query) {
$content = strip_tags($content);
$pos = stripos($content, $query);
if ($pos === false) return substr($content, 0, 100) . '...';
$start = max(0, $pos - 50);
$snippet = substr($content, $start, 150);
return '...' . $snippet . '...';
}
public function getPage() {
if (isset($_GET['search'])) {
return $this->getSearchResults();
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
$filePath = $this->config['content_dir'] . '/' . $page;
$actualFilePath = null;
if (file_exists($filePath . '.md')) {
$actualFilePath = $filePath . '.md';
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif (file_exists($filePath . '.php')) {
$actualFilePath = $filePath . '.php';
$result = $this->parsePHP($actualFilePath);
} elseif (file_exists($filePath . '.html')) {
$actualFilePath = $filePath . '.html';
$result = $this->parseHTML(file_get_contents($actualFilePath));
} elseif (is_dir($filePath)) {
// Check for index files in directory
if (file_exists($filePath . '/index.md')) {
$actualFilePath = $filePath . '/index.md';
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif (file_exists($filePath . '/index.php')) {
$actualFilePath = $filePath . '/index.php';
$result = $this->parsePHP($actualFilePath);
} elseif (file_exists($filePath . '/index.html')) {
$actualFilePath = $filePath . '/index.html';
$result = $this->parseHTML(file_get_contents($actualFilePath));
} else {
// Generate directory listing
return $this->generateDirectoryListing($filePath, $page);
}
} elseif (file_exists($filePath)) {
$actualFilePath = $filePath;
$extension = pathinfo($filePath, PATHINFO_EXTENSION);
if ($extension === 'md') {
$result = $this->parseMarkdown(file_get_contents($actualFilePath));
} elseif ($extension === 'php') {
$result = $this->parsePHP($actualFilePath);
} elseif ($extension === 'html') {
$result = $this->parseHTML(file_get_contents($actualFilePath));
}
}
if (isset($result) && $actualFilePath) {
$result['file_info'] = $this->getFileInfo($actualFilePath);
return $result;
}
return $this->getError404();
}
private function getFileInfo($filePath) {
if (!file_exists($filePath)) {
return null;
}
$stats = stat($filePath);
$created = date('d-m-Y H:i', $stats['ctime']);
$modified = date('d-m-Y H:i', $stats['mtime']);
return [
'created' => $created,
'modified' => $modified,
'size' => $this->formatFileSize($stats['size'])
];
}
private function formatFileSize($bytes) {
$units = ['B', 'KB', 'MB', 'GB'];
$bytes = max($bytes, 0);
$pow = floor(($bytes ? log($bytes) : 0) / log(1024));
$pow = min($pow, count($units) - 1);
$bytes /= pow(1024, $pow);
return round($bytes, 2) . ' ' . $units[$pow];
}
private function generateDirectoryListing($dirPath, $urlPath) {
$title = ucfirst(basename($dirPath));
$content = '<div class="row">';
$items = scandir($dirPath);
sort($items);
foreach ($items as $item) {
if ($item[0] === '.') continue;
$path = $dirPath . '/' . $item;
$relativePath = $urlPath . '/' . $item;
$itemName = ucfirst(pathinfo($item, PATHINFO_FILENAME));
if (is_dir($path)) {
$content .= '<div class="col-md-6 mb-4">';
$content .= '<div class="card h-100 border-0 rounded-0 bg-light">';
$content .= '<div class="card-body">';
$content .= '<h3 class="h5 card-title"><a href="?page=' . $relativePath . '" class="text-decoration-none text-dark"><i class="bi bi-folder me-2"></i> ' . $itemName . '</a></h3>';
$content .= '</div></div></div>';
} elseif (preg_match('/\.(md|php|html)$/', $item)) {
// Remove extension from URL for cleaner links
$cleanPath = preg_replace('/\.[^.]+$/', '', $relativePath);
// Get preview content
$preview = '';
$fileContent = file_get_contents($path);
// Extract title if possible
$fileTitle = $itemName;
if (preg_match('/^#\s+(.+)$/m', $fileContent, $matches)) {
$fileTitle = trim($matches[1]);
}
// Extract preview text (first paragraph)
$fileContent = strip_tags($this->parseMarkdown($fileContent)['content']);
$preview = substr($fileContent, 0, 150) . '...';
$content .= '<div class="col-md-6 mb-4">';
$content .= '<div class="card h-100 border rounded-0">';
$content .= '<div class="card-body">';
$content .= '<h3 class="h5 card-title"><a href="?page=' . $cleanPath . '" class="text-decoration-none text-primary">' . $fileTitle . '</a></h3>';
$content .= '<p class="card-text text-muted small">' . $preview . '</p>';
$content .= '<a href="?page=' . $cleanPath . '" class="btn btn-sm btn-outline-primary rounded-0">Lees meer</a>';
$content .= '</div></div></div>';
}
}
$content .= '</div>';
return [
'title' => $title,
'content' => $content
];
}
private function getSearchResults() {
$query = $_GET['search'];
$content = '<h2>Search Results for: "' . htmlspecialchars($query) . '"</h2>';
if (empty($this->searchResults)) {
$content .= '<p>No results found.</p>';
} else {
$content .= '<p>Found ' . count($this->searchResults) . ' results:</p>';
foreach ($this->searchResults as $result) {
$content .= '<div class="card mb-3">';
$content .= '<div class="card-body">';
$content .= '<h5 class="card-title"><a href="' . htmlspecialchars($result['url']) . '">' . htmlspecialchars($result['title']) . '</a></h5>';
$content .= '<p class="card-text text-muted">' . htmlspecialchars($result['path']) . '</p>';
$content .= '<p class="card-text">' . htmlspecialchars($result['snippet']) . '</p>';
$content .= '</div></div>';
}
}
return [
'title' => 'Search Results',
'content' => $content
];
}
private function parseMarkdown($content) {
$lines = explode("\n", $content);
$title = '';
$body = '';
$inBody = false;
foreach ($lines as $line) {
if (!$inBody && preg_match('/^#\s+(.+)$/', $line, $matches)) {
$title = $matches[1];
$inBody = true;
} elseif ($inBody || trim($line) !== '') {
$body .= $line . "\n";
$inBody = true;
}
}
$body = preg_replace('/### (.+)/', '<h3>$1</h3>', $body);
$body = preg_replace('/## (.+)/', '<h2>$1</h2>', $body);
$body = preg_replace('/# (.+)/', '<h1>$1</h1>', $body);
$body = preg_replace('/\*\*(.+?)\*\*/', '<strong>$1</strong>', $body);
$body = preg_replace('/\*(.+?)\*/', '<em>$1</em>', $body);
// Convert Markdown links to HTML links
$body = preg_replace('/\[([^\]]+)\]\(([^)]+)\)/', '<a href="$2">$1</a>', $body);
// Convert relative internal links to CMS format
$body = preg_replace('/href="\/blog\/([^"]+)"/', 'href="?page=blog/$1"', $body);
$body = preg_replace('/href="\/([^"]+)"/', 'href="?page=$1"', $body);
$body = preg_replace('/\n\n/', '</p><p>', $body);
$body = '<p>' . $body . '</p>';
$body = preg_replace('/<p><\/p>/', '', $body);
$body = preg_replace('/<p>(<h[1-6]>)/', '$1', $body);
$body = preg_replace('/(<\/h[1-6]>)<\/p>/', '$1', $body);
return [
'title' => $title ?: 'Untitled',
'content' => $body
];
}
private function parsePHP($filePath) {
ob_start();
$title = 'Untitled';
include $filePath;
$content = ob_get_clean();
return [
'title' => $title,
'content' => $content
];
}
private function parseHTML($content) {
$title = 'Untitled';
if (preg_match('/<title>(.*?)<\/title>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
} elseif (preg_match('/<h1[^>]*>(.*?)<\/h1>/i', $content, $matches)) {
$title = strip_tags($matches[1]);
}
return [
'title' => $title,
'content' => $content
];
}
private function getError404() {
return [
'title' => 'Page Not Found',
'content' => '<h1>404 - Page Not Found</h1><p>The page you are looking for does not exist.</p>'
];
}
public function getMenu() {
return $this->menu;
}
public function render() {
$page = $this->getPage();
$menu = $this->getMenu();
$breadcrumb = $this->getBreadcrumb();
$template = file_get_contents($this->config['templates_dir'] . '/layout.html');
$template = str_replace('{{site_title}}', $this->config['site_title'], $template);
$template = str_replace('{{page_title}}', $page['title'], $template);
$template = str_replace('{{content}}', $page['content'], $template);
$template = str_replace('{{search_query}}', isset($_GET['search']) ? htmlspecialchars($_GET['search']) : '', $template);
$template = str_replace('{{breadcrumb}}', $breadcrumb, $template);
// File info for footer
$fileInfo = '';
if (isset($page['file_info'])) {
$fileInfo = '<i class="bi bi-file-text"></i> Created: ' . htmlspecialchars($page['file_info']['created']) .
' | Modified: ' . htmlspecialchars($page['file_info']['modified']);
}
$template = str_replace('{{file_info}}', $fileInfo, $template);
$menuHtml = $this->renderMenu($menu);
$template = str_replace('{{menu}}', $menuHtml, $template);
echo $template;
}
private function getBreadcrumb() {
if (isset($_GET['search'])) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li><li class="breadcrumb-item active">Search</li></ol></nav>';
}
$page = $_GET['page'] ?? $this->config['default_page'];
$page = preg_replace('/\.[^.]+$/', '', $page);
if ($page === $this->config['default_page']) {
return '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item active">Home</li></ol></nav>';
}
$parts = explode('/', $page);
$breadcrumb = '<nav aria-label="breadcrumb"><ol class="breadcrumb"><li class="breadcrumb-item"><a href="?page=' . $this->config['default_page'] . '">Home</a></li>';
$path = '';
foreach ($parts as $i => $part) {
$path .= ($path ? '/' : '') . $part;
$title = ucfirst($part);
if ($i === count($parts) - 1) {
$breadcrumb .= '<li class="breadcrumb-item active">' . $title . '</li>';
} else {
// Check if directory has index file
$dirPath = $this->config['content_dir'] . '/' . $path;
$hasIndex = file_exists($dirPath . '/index.md') || file_exists($dirPath . '/index.php') || file_exists($dirPath . '/index.html');
// Always make breadcrumb items clickable, CMS will generate index if missing
$breadcrumb .= '<li class="breadcrumb-item"><a href="?page=' . $path . '">' . $title . '</a></li>';
}
}
$breadcrumb .= '</ol></nav>';
return $breadcrumb;
}
private function renderMenu($items, $level = 0) {
$html = '';
foreach ($items as $item) {
if ($item['type'] === 'folder') {
$hasChildren = !empty($item['children']);
$html .= '<li class="nav-item">';
if ($hasChildren) {
$folderId = 'folder-' . str_replace('/', '-', $item['path']);
$html .= '<span class="nav-link folder-toggle" data-bs-toggle="collapse" data-bs-target="#' . $folderId . '" aria-expanded="false">';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
$html .= '<ul class="nav flex-column ms-2 collapse" id="' . $folderId . '">';
$html .= $this->renderMenu($item['children'], $level + 1);
$html .= '</ul>';
} else {
$html .= '<span class="nav-link folder-disabled" disabled>';
$html .= '<i class="arrow bi bi-chevron-right"></i> ' . htmlspecialchars($item['title']);
$html .= '</span>';
}
$html .= '</li>';
} else {
$active = (isset($_GET['page']) && $_GET['page'] === $item['path']) ? 'active' : '';
$html .= '<li class="nav-item">';
$html .= '<a class="nav-link page-link ' . $active . '" href="' . htmlspecialchars($item['url']) . '">' . htmlspecialchars($item['title']) . '</a>';
$html .= '</li>';
}
}
return $html;
}
}
// Block direct access to content files
$requestUri = $_SERVER['REQUEST_URI'] ?? '';
if (strpos($requestUri, '/content/') !== false) {

View File

@ -1 +0,0 @@
[Wed Nov 19 17:58:28 2025] Failed to listen on localhost:8080 (reason: Address already in use)

352
src/scss/main.scss Normal file
View File

@ -0,0 +1,352 @@
// Custom variables (must come before Bootstrap import)
$primary: #0d6efd;
$navigation-opacity: 0.5; // 50% opacity
// Import Bootstrap
@import "bootstrap/scss/bootstrap";
// Custom navigation background with 50% opacity of primary color
.navigation-50-opacity {
background-color: rgba($primary, $navigation-opacity) !important;
}
// Custom CodePress styles
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
margin: 0;
padding: 0;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
/* Header */
header.navbar {
flex-shrink: 0;
height: 66px;
}
/* Navigation section */
.navigation-section {
background-color: #ffffff;
border-bottom: 1px solid #dee2e6;
padding: 0.5rem 0;
flex-shrink: 0;
}
/* Tab styling */
.nav-tabs {
border-bottom: none;
}
.nav-tabs .nav-link {
border: 1px solid transparent;
border-bottom: none;
border-radius: 0; // Geen ronde hoeken
color: #6c757d;
background-color: transparent; // Transparante achtergrond voor eerste laag
margin-right: 0.25rem;
transition: all 0.2s ease;
}
.nav-tabs .nav-link:hover {
color: #495057;
background-color: rgba(0, 0, 0, 0.05); // Subtiel hover effect
border-color: transparent;
}
.nav-tabs .nav-link.active {
color: #495057;
background-color: #ffffff; // Alleen active heeft witte achtergrond
border-color: #dee2e6 #dee2e6 transparent;
font-weight: 500;
}
/* Dropdown in tabs - eerste laag */
.nav-tabs .nav-item.dropdown .nav-link {
background-color: transparent; // Transparante achtergrond
border: 1px solid transparent;
border-radius: 0; // Geen ronde hoeken
}
.nav-tabs .nav-item.dropdown .nav-link:hover {
background-color: rgba(0, 0, 0, 0.05); // Subtiel hover effect
border-color: transparent;
}
/* Dropdown menu styling */
.nav-tabs .dropdown-menu {
margin-top: 0.25rem;
border-radius: 0.375rem;
box-shadow: 0 0.125rem 0.25rem rgba(0, 0, 0, 0.075);
min-width: 200px;
max-width: 300px;
width: auto;
}
/* Dropdown items styling */
.nav-tabs .dropdown-menu .dropdown-item {
white-space: normal;
padding: 0.5rem 1rem;
line-height: 1.4;
}
/* Long text truncation for very long titles */
.nav-tabs .dropdown-menu .dropdown-item {
overflow: hidden;
text-overflow: ellipsis;
max-width: 280px;
}
/* Breadcrumb section */
.breadcrumb-section {
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
flex-shrink: 0;
}
/* Breadcrumb styling consistency */
.breadcrumb-section .breadcrumb {
margin-bottom: 0;
}
.breadcrumb-section .breadcrumb-item {
color: #6c757d !important;
}
.breadcrumb-section .breadcrumb-item a {
color: #0d6efd !important;
text-decoration: none;
}
.breadcrumb-section .breadcrumb-item a:hover {
color: #0a58ca !important;
text-decoration: underline;
}
.breadcrumb-section .breadcrumb-item.active {
color: #6c757d !important;
}
/* Main content area */
.main-content {
flex: 1;
overflow-y: auto;
padding: 1rem 0;
}
/* Content specific styling */
.markdown-content,
.php-content,
.html-content {
margin-bottom: 2rem;
}
.markdown-content .content-body,
.php-content .content-body,
.html-content .content-body {
padding: 2rem;
line-height: 1.6;
}
/* Footer */
footer {
flex-shrink: 0;
}
/* Navigation dropdown styling */
.navigation-section .btn {
font-weight: 500;
}
.navigation-section .collapse .nav-link {
color: #495057;
padding: 0.375rem 0.75rem;
border-radius: 0.25rem;
transition: all 0.2s ease;
}
.navigation-section .collapse .nav-link:hover {
background-color: #e9ecef;
color: #212529;
}
.navigation-section .collapse .nav-link.active {
background-color: #0d6efd;
color: white;
}
.navigation-section .collapse .nav-link i {
margin-right: 0.5rem;
}
/* File info styling */
.file-info {
font-size: 0.9rem;
color: #6c757d;
flex: 1;
min-width: 0;
}
.file-info i {
margin-right: 5px;
}
.page-title {
color: #6c757d;
max-width: 250px;
display: inline-block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
vertical-align: middle;
margin-right: 10px;
}
.page-title:hover {
color: #495057;
cursor: default;
}
.file-details {
color: #6c757d;
font-size: 0.85rem;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.guide-link {
color: #6c757d !important;
text-decoration: none;
font-size: 0.9rem;
}
.guide-link:hover {
color: #0d6efd !important;
text-decoration: none;
}
.guide-link i {
font-size: 1rem;
color: #6c757d !important;
}
.guide-link:hover i {
color: #0d6efd !important;
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
transition: all 0.2s ease;
}
.auto-link:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
/* Search form */
.search-form {
max-width: 300px;
}
/* Card styling */
.card-title a {
text-decoration: none;
color: inherit;
}
.card-title a:hover {
text-decoration: underline;
}
/* Dropdown submenu styling */
.dropdown-menu .dropdown-menu {
top: 0;
left: 100%;
margin-top: -1px;
margin-left: -1px;
}
/* Show nested dropdowns on hover */
.dropdown-submenu {
position: relative;
}
.dropdown-submenu .dropdown-menu {
top: 0;
left: 100%;
margin-top: -1px;
}
.dropdown-submenu:hover > .dropdown-menu {
display: block;
}
/* For touch devices - click to open */
.dropdown-submenu > .dropdown-toggle:active::after {
transform: rotate(90deg);
}
/* Responsive adjustments */
@media (max-width: 768px) {
.main-content {
padding-top: 120px; /* More space for mobile */
}
.markdown-content .content-body,
.php-content .content-body,
.html-content .content-body {
padding: 1.5rem;
}
.nav-tabs .dropdown-menu {
min-width: 180px;
max-width: 250px;
}
.nav-tabs .dropdown-menu .dropdown-item {
max-width: 230px;
font-size: 0.9rem;
}
}
@media (max-width: 576px) {
.markdown-content .content-body,
.php-content .content-body,
.html-content .content-body {
padding: 1rem;
}
.nav-tabs .dropdown-menu {
min-width: 160px;
max-width: 200px;
}
.nav-tabs .dropdown-menu .dropdown-item {
max-width: 180px;
font-size: 0.85rem;
padding: 0.4rem 0.8rem;
}
}

View File

@ -1,381 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{page_title}} - {{site_title}}</title>
<link rel="icon" type="image/svg+xml" href="assets/favicon.svg">
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
<link href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.11.0/font/bootstrap-icons.css" rel="stylesheet">
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html, body {
height: 100%;
}
body {
display: flex;
flex-direction: column;
min-height: 100vh;
}
.main-wrapper {
flex: 1;
display: flex;
flex-direction: column;
}
.content-wrapper {
flex: 1;
display: flex;
overflow: hidden;
}
.sidebar {
width: 250px;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
overflow-y: auto;
flex-shrink: 0;
transition: transform 0.3s ease;
position: relative;
z-index: 1000;
}
.sidebar.collapsed {
transform: translateX(-250px);
}
.sidebar-toggle {
position: fixed;
top: 80px;
left: 10px;
z-index: 1001;
background-color: #0d6efd;
color: white;
border: none;
border-radius: 5px;
padding: 8px 12px;
cursor: pointer;
box-shadow: 0 2px 5px rgba(0,0,0,0.2);
transition: all 0.3s ease;
}
.sidebar-toggle:hover {
background-color: #0a58ca;
transform: scale(1.05);
}
.sidebar-toggle.shifted {
left: 270px;
}
.main-content.shifted {
margin-left: 0;
}
.main-content {
flex: 1;
overflow-y: auto;
padding: 20px;
transition: margin-left 0.3s ease;
}
.folder-toggle {
font-weight: bold;
color: #212529 !important;
cursor: pointer;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
.folder-toggle:hover {
background-color: #e9ecef !important;
}
.folder-toggle[aria-expanded="true"] {
background-color: #dee2e6 !important;
color: #212529 !important;
font-weight: 600;
}
/* Progressive background colors for nested folders */
.nav .nav .folder-toggle {
background-color: #f1f3f4 !important;
}
.nav .nav .nav .folder-toggle {
background-color: #eaedee !important;
}
.nav .nav .nav .nav .folder-toggle {
background-color: #e3e7e8 !important;
}
.nav .nav .nav .nav .nav .folder-toggle {
background-color: #dce1e2 !important;
}
.nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #d5dbdd !important;
}
.nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #ced5d8 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c7cfd3 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #c0c9ce !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .folder-toggle {
background-color: #b9c3c9 !important;
}
.folder-toggle .arrow {
margin-right: 8px;
transition: transform 0.2s;
font-size: 0.8em;
}
.folder-toggle[aria-expanded="true"] .arrow {
transform: rotate(90deg);
}
.page-link {
color: #495057 !important;
font-weight: 500;
padding: 0.5rem 0.75rem;
padding-left: 2rem;
background-color: #ffffff !important;
margin: 0.125rem 0;
transition: background-color 0.2s ease;
}
/* Progressive background colors for nested pages */
.nav .nav .page-link {
background-color: #fafbfc !important;
}
.nav .nav .nav .page-link {
background-color: #f5f7f8 !important;
}
.nav .nav .nav .nav .page-link {
background-color: #f0f3f4 !important;
}
.nav .nav .nav .nav .nav .page-link {
background-color: #ebefef !important;
}
.nav .nav .nav .nav .nav .nav .page-link {
background-color: #e6eaea !important;
}
.nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #e1e5e5 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #dce0e0 !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d7dbdb !important;
}
.nav .nav .nav .nav .nav .nav .nav .nav .nav .nav .page-link {
background-color: #d2d6d6 !important;
}
.page-link:hover {
color: #212529 !important;
background-color: #f8f9fa !important;
}
.nav-link.active {
background-color: #0d6efd !important;
color: #212529 !important;
font-weight: 600;
}
.file-info {
font-size: 0.9rem;
color: #6c757d;
}
.site-info a {
color: #0d6efd;
text-decoration: none;
}
.site-info a:hover {
text-decoration: underline;
}
.auto-link {
color: #0d6efd;
text-decoration: none;
border-bottom: 2px dashed #0d6efd;
font-weight: 500;
transition: all 0.2s ease;
}
.auto-link:hover {
color: #0a58ca;
text-decoration: none;
border-bottom-style: solid;
border-bottom-color: #0a58ca;
}
.search-form {
max-width: 300px;
}
.card-title a {
text-decoration: none;
color: inherit;
}
.card-title a:hover {
text-decoration: underline;
}
.folder-disabled {
font-weight: 500;
color: #6c757d !important;
cursor: not-allowed;
display: flex;
align-items: center;
padding: 0.5rem 0.75rem;
background-color: #f8f9fa !important;
margin: 0.125rem 0;
opacity: 0.7;
}
.folder-disabled .arrow {
margin-right: 8px;
font-size: 0.8em;
opacity: 0.6;
}
@media (max-width: 768px) {
.sidebar {
width: 200px;
}
}
</style>
</head>
<body>
<header class="bg-primary text-white py-3">
<div class="container-fluid">
<div class="row align-items-center">
<div class="col">
<div class="d-flex align-items-center">
<img src="assets/icon.svg" alt="CodePress Logo" width="32" height="32" class="me-2">
<h1 class="h3 mb-0">{{site_title}}</h1>
</div>
</div>
<div class="col-auto">
<form class="d-flex" method="GET" action="">
<input class="form-control me-2" type="search" name="search" placeholder="Search..." value="{{search_query}}">
<button class="btn btn-outline-light" type="submit">Search</button>
</form>
</div>
</div>
</div>
</header>
<div class="main-wrapper">
<div class="content-wrapper">
<button class="sidebar-toggle" id="sidebarToggle">
<i class="bi bi-list"></i>
</button>
<nav class="sidebar" id="sidebar">
<div class="pt-3">
<ul class="nav flex-column">
{{menu}}
</ul>
</div>
</nav>
<main class="main-content">
<div>
{{breadcrumb}}
</div>
<div class="d-flex justify-content-between flex-wrap flex-md-nowrap align-items-center pt-3 pb-2 mb-3 border-bottom">
<h2>{{page_title}}</h2>
</div>
<div class="content">
{{content}}
</div>
</main>
</div>
</div>
<footer class="bg-light border-top py-3">
<div class="container-fluid">
<div class="row">
<div class="col-md-12">
<div class="d-flex justify-content-between align-items-center">
<div class="file-info">
{{file_info}}
</div>
<div class="site-info">
<small class="text-muted">Powered by <a href="https://git.noorlander.info/E.Noorlander/CodePress.git" target="_blank" rel="noopener">CodePress CMS</a></small>
</div>
</div>
</div>
</div>
</div>
</footer>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"></script>
<script>
document.addEventListener('DOMContentLoaded', function() {
// Sidebar toggle functionality
const sidebarToggle = document.getElementById('sidebarToggle');
const sidebar = document.getElementById('sidebar');
const mainContent = document.querySelector('.main-content');
sidebarToggle.addEventListener('click', function() {
sidebar.classList.toggle('collapsed');
mainContent.classList.toggle('shifted');
sidebarToggle.classList.toggle('shifted');
// Change icon
const icon = this.querySelector('i');
if (sidebar.classList.contains('collapsed')) {
icon.classList.remove('bi-list');
icon.classList.add('bi-chevron-right');
} else {
icon.classList.remove('bi-chevron-right');
icon.classList.add('bi-list');
}
});
// Open folders that contain the current active page
const activeLink = document.querySelector('.nav-link.active');
if (activeLink) {
let parent = activeLink.closest('.collapse');
while (parent) {
const toggle = document.querySelector('[data-bs-target="#' + parent.id + '"]');
if (toggle) {
const collapse = new bootstrap.Collapse(parent, {
show: true
});
toggle.setAttribute('aria-expanded', 'true');
}
parent = parent.parentElement.closest('.collapse');
}
}
// Close other folders when opening a new one
const folderToggles = document.querySelectorAll('.folder-toggle');
folderToggles.forEach(toggle => {
toggle.addEventListener('click', function(e) {
const targetId = this.getAttribute('data-bs-target');
const isExpanded = this.getAttribute('aria-expanded') === 'true';
if (!isExpanded) {
// Close all other folders
folderToggles.forEach(otherToggle => {
if (otherToggle !== this) {
const otherTargetId = otherToggle.getAttribute('data-bs-target');
if (otherTargetId) {
const otherCollapse = document.querySelector(otherTargetId);
if (otherCollapse) {
const bsCollapse = bootstrap.Collapse.getInstance(otherCollapse);
if (bsCollapse) {
bsCollapse.hide();
} else {
new bootstrap.Collapse(otherCollapse, {
hide: true
});
}
otherToggle.setAttribute('aria-expanded', 'false');
}
}
}
});
}
});
});
});
</script>
</body>
</html>

22
vendor/autoload.php vendored Normal file
View File

@ -0,0 +1,22 @@
<?php
// autoload.php @generated by Composer
if (PHP_VERSION_ID < 50600) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
$err = 'Composer 2.3.0 dropped support for autoloading on PHP <5.6 and you are running '.PHP_VERSION.', please upgrade PHP or use Composer 2.2 LTS via "composer self-update --2.2". Aborting.'.PHP_EOL;
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, $err);
} elseif (!headers_sent()) {
echo $err;
}
}
throw new RuntimeException($err);
}
require_once __DIR__ . '/composer/autoload_real.php';
return ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c::getLoader();

579
vendor/composer/ClassLoader.php vendored Normal file
View File

@ -0,0 +1,579 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer\Autoload;
/**
* ClassLoader implements a PSR-0, PSR-4 and classmap class loader.
*
* $loader = new \Composer\Autoload\ClassLoader();
*
* // register classes with namespaces
* $loader->add('Symfony\Component', __DIR__.'/component');
* $loader->add('Symfony', __DIR__.'/framework');
*
* // activate the autoloader
* $loader->register();
*
* // to enable searching the include path (eg. for PEAR packages)
* $loader->setUseIncludePath(true);
*
* In this example, if you try to use a class in the Symfony\Component
* namespace or one of its children (Symfony\Component\Console for instance),
* the autoloader will first look for the class under the component/
* directory, and it will then fallback to the framework/ directory if not
* found before giving up.
*
* This class is loosely based on the Symfony UniversalClassLoader.
*
* @author Fabien Potencier <fabien@symfony.com>
* @author Jordi Boggiano <j.boggiano@seld.be>
* @see https://www.php-fig.org/psr/psr-0/
* @see https://www.php-fig.org/psr/psr-4/
*/
class ClassLoader
{
/** @var \Closure(string):void */
private static $includeFile;
/** @var string|null */
private $vendorDir;
// PSR-4
/**
* @var array<string, array<string, int>>
*/
private $prefixLengthsPsr4 = array();
/**
* @var array<string, list<string>>
*/
private $prefixDirsPsr4 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr4 = array();
// PSR-0
/**
* List of PSR-0 prefixes
*
* Structured as array('F (first letter)' => array('Foo\Bar (full prefix)' => array('path', 'path2')))
*
* @var array<string, array<string, list<string>>>
*/
private $prefixesPsr0 = array();
/**
* @var list<string>
*/
private $fallbackDirsPsr0 = array();
/** @var bool */
private $useIncludePath = false;
/**
* @var array<string, string>
*/
private $classMap = array();
/** @var bool */
private $classMapAuthoritative = false;
/**
* @var array<string, bool>
*/
private $missingClasses = array();
/** @var string|null */
private $apcuPrefix;
/**
* @var array<string, self>
*/
private static $registeredLoaders = array();
/**
* @param string|null $vendorDir
*/
public function __construct($vendorDir = null)
{
$this->vendorDir = $vendorDir;
self::initializeIncludeClosure();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixes()
{
if (!empty($this->prefixesPsr0)) {
return call_user_func_array('array_merge', array_values($this->prefixesPsr0));
}
return array();
}
/**
* @return array<string, list<string>>
*/
public function getPrefixesPsr4()
{
return $this->prefixDirsPsr4;
}
/**
* @return list<string>
*/
public function getFallbackDirs()
{
return $this->fallbackDirsPsr0;
}
/**
* @return list<string>
*/
public function getFallbackDirsPsr4()
{
return $this->fallbackDirsPsr4;
}
/**
* @return array<string, string> Array of classname => path
*/
public function getClassMap()
{
return $this->classMap;
}
/**
* @param array<string, string> $classMap Class to filename map
*
* @return void
*/
public function addClassMap(array $classMap)
{
if ($this->classMap) {
$this->classMap = array_merge($this->classMap, $classMap);
} else {
$this->classMap = $classMap;
}
}
/**
* Registers a set of PSR-0 directories for a given prefix, either
* appending or prepending to the ones previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 root directories
* @param bool $prepend Whether to prepend the directories
*
* @return void
*/
public function add($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
if ($prepend) {
$this->fallbackDirsPsr0 = array_merge(
$paths,
$this->fallbackDirsPsr0
);
} else {
$this->fallbackDirsPsr0 = array_merge(
$this->fallbackDirsPsr0,
$paths
);
}
return;
}
$first = $prefix[0];
if (!isset($this->prefixesPsr0[$first][$prefix])) {
$this->prefixesPsr0[$first][$prefix] = $paths;
return;
}
if ($prepend) {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$paths,
$this->prefixesPsr0[$first][$prefix]
);
} else {
$this->prefixesPsr0[$first][$prefix] = array_merge(
$this->prefixesPsr0[$first][$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-4 directories for a given namespace, either
* appending or prepending to the ones previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
* @param bool $prepend Whether to prepend the directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function addPsr4($prefix, $paths, $prepend = false)
{
$paths = (array) $paths;
if (!$prefix) {
// Register directories for the root namespace.
if ($prepend) {
$this->fallbackDirsPsr4 = array_merge(
$paths,
$this->fallbackDirsPsr4
);
} else {
$this->fallbackDirsPsr4 = array_merge(
$this->fallbackDirsPsr4,
$paths
);
}
} elseif (!isset($this->prefixDirsPsr4[$prefix])) {
// Register directories for a new namespace.
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = $paths;
} elseif ($prepend) {
// Prepend directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$paths,
$this->prefixDirsPsr4[$prefix]
);
} else {
// Append directories for an already registered namespace.
$this->prefixDirsPsr4[$prefix] = array_merge(
$this->prefixDirsPsr4[$prefix],
$paths
);
}
}
/**
* Registers a set of PSR-0 directories for a given prefix,
* replacing any others previously set for this prefix.
*
* @param string $prefix The prefix
* @param list<string>|string $paths The PSR-0 base directories
*
* @return void
*/
public function set($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr0 = (array) $paths;
} else {
$this->prefixesPsr0[$prefix[0]][$prefix] = (array) $paths;
}
}
/**
* Registers a set of PSR-4 directories for a given namespace,
* replacing any others previously set for this namespace.
*
* @param string $prefix The prefix/namespace, with trailing '\\'
* @param list<string>|string $paths The PSR-4 base directories
*
* @throws \InvalidArgumentException
*
* @return void
*/
public function setPsr4($prefix, $paths)
{
if (!$prefix) {
$this->fallbackDirsPsr4 = (array) $paths;
} else {
$length = strlen($prefix);
if ('\\' !== $prefix[$length - 1]) {
throw new \InvalidArgumentException("A non-empty PSR-4 prefix must end with a namespace separator.");
}
$this->prefixLengthsPsr4[$prefix[0]][$prefix] = $length;
$this->prefixDirsPsr4[$prefix] = (array) $paths;
}
}
/**
* Turns on searching the include path for class files.
*
* @param bool $useIncludePath
*
* @return void
*/
public function setUseIncludePath($useIncludePath)
{
$this->useIncludePath = $useIncludePath;
}
/**
* Can be used to check if the autoloader uses the include path to check
* for classes.
*
* @return bool
*/
public function getUseIncludePath()
{
return $this->useIncludePath;
}
/**
* Turns off searching the prefix and fallback directories for classes
* that have not been registered with the class map.
*
* @param bool $classMapAuthoritative
*
* @return void
*/
public function setClassMapAuthoritative($classMapAuthoritative)
{
$this->classMapAuthoritative = $classMapAuthoritative;
}
/**
* Should class lookup fail if not found in the current class map?
*
* @return bool
*/
public function isClassMapAuthoritative()
{
return $this->classMapAuthoritative;
}
/**
* APCu prefix to use to cache found/not-found classes, if the extension is enabled.
*
* @param string|null $apcuPrefix
*
* @return void
*/
public function setApcuPrefix($apcuPrefix)
{
$this->apcuPrefix = function_exists('apcu_fetch') && filter_var(ini_get('apc.enabled'), FILTER_VALIDATE_BOOLEAN) ? $apcuPrefix : null;
}
/**
* The APCu prefix in use, or null if APCu caching is not enabled.
*
* @return string|null
*/
public function getApcuPrefix()
{
return $this->apcuPrefix;
}
/**
* Registers this instance as an autoloader.
*
* @param bool $prepend Whether to prepend the autoloader or not
*
* @return void
*/
public function register($prepend = false)
{
spl_autoload_register(array($this, 'loadClass'), true, $prepend);
if (null === $this->vendorDir) {
return;
}
if ($prepend) {
self::$registeredLoaders = array($this->vendorDir => $this) + self::$registeredLoaders;
} else {
unset(self::$registeredLoaders[$this->vendorDir]);
self::$registeredLoaders[$this->vendorDir] = $this;
}
}
/**
* Unregisters this instance as an autoloader.
*
* @return void
*/
public function unregister()
{
spl_autoload_unregister(array($this, 'loadClass'));
if (null !== $this->vendorDir) {
unset(self::$registeredLoaders[$this->vendorDir]);
}
}
/**
* Loads the given class or interface.
*
* @param string $class The name of the class
* @return true|null True if loaded, null otherwise
*/
public function loadClass($class)
{
if ($file = $this->findFile($class)) {
$includeFile = self::$includeFile;
$includeFile($file);
return true;
}
return null;
}
/**
* Finds the path to the file where the class is defined.
*
* @param string $class The name of the class
*
* @return string|false The path if found, false otherwise
*/
public function findFile($class)
{
// class map lookup
if (isset($this->classMap[$class])) {
return $this->classMap[$class];
}
if ($this->classMapAuthoritative || isset($this->missingClasses[$class])) {
return false;
}
if (null !== $this->apcuPrefix) {
$file = apcu_fetch($this->apcuPrefix.$class, $hit);
if ($hit) {
return $file;
}
}
$file = $this->findFileWithExtension($class, '.php');
// Search for Hack files if we are running on HHVM
if (false === $file && defined('HHVM_VERSION')) {
$file = $this->findFileWithExtension($class, '.hh');
}
if (null !== $this->apcuPrefix) {
apcu_add($this->apcuPrefix.$class, $file);
}
if (false === $file) {
// Remember that this class does not exist.
$this->missingClasses[$class] = true;
}
return $file;
}
/**
* Returns the currently registered loaders keyed by their corresponding vendor directories.
*
* @return array<string, self>
*/
public static function getRegisteredLoaders()
{
return self::$registeredLoaders;
}
/**
* @param string $class
* @param string $ext
* @return string|false
*/
private function findFileWithExtension($class, $ext)
{
// PSR-4 lookup
$logicalPathPsr4 = strtr($class, '\\', DIRECTORY_SEPARATOR) . $ext;
$first = $class[0];
if (isset($this->prefixLengthsPsr4[$first])) {
$subPath = $class;
while (false !== $lastPos = strrpos($subPath, '\\')) {
$subPath = substr($subPath, 0, $lastPos);
$search = $subPath . '\\';
if (isset($this->prefixDirsPsr4[$search])) {
$pathEnd = DIRECTORY_SEPARATOR . substr($logicalPathPsr4, $lastPos + 1);
foreach ($this->prefixDirsPsr4[$search] as $dir) {
if (file_exists($file = $dir . $pathEnd)) {
return $file;
}
}
}
}
}
// PSR-4 fallback dirs
foreach ($this->fallbackDirsPsr4 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr4)) {
return $file;
}
}
// PSR-0 lookup
if (false !== $pos = strrpos($class, '\\')) {
// namespaced class name
$logicalPathPsr0 = substr($logicalPathPsr4, 0, $pos + 1)
. strtr(substr($logicalPathPsr4, $pos + 1), '_', DIRECTORY_SEPARATOR);
} else {
// PEAR-like class name
$logicalPathPsr0 = strtr($class, '_', DIRECTORY_SEPARATOR) . $ext;
}
if (isset($this->prefixesPsr0[$first])) {
foreach ($this->prefixesPsr0[$first] as $prefix => $dirs) {
if (0 === strpos($class, $prefix)) {
foreach ($dirs as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
}
}
}
// PSR-0 fallback dirs
foreach ($this->fallbackDirsPsr0 as $dir) {
if (file_exists($file = $dir . DIRECTORY_SEPARATOR . $logicalPathPsr0)) {
return $file;
}
}
// PSR-0 include paths.
if ($this->useIncludePath && $file = stream_resolve_include_path($logicalPathPsr0)) {
return $file;
}
return false;
}
/**
* @return void
*/
private static function initializeIncludeClosure()
{
if (self::$includeFile !== null) {
return;
}
/**
* Scope isolated include.
*
* Prevents access to $this/self from included files.
*
* @param string $file
* @return void
*/
self::$includeFile = \Closure::bind(static function($file) {
include $file;
}, null, null);
}
}

396
vendor/composer/InstalledVersions.php vendored Normal file
View File

@ -0,0 +1,396 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Composer;
use Composer\Autoload\ClassLoader;
use Composer\Semver\VersionParser;
/**
* This class is copied in every Composer installed project and available to all
*
* See also https://getcomposer.org/doc/07-runtime.md#installed-versions
*
* To require its presence, you can require `composer-runtime-api ^2.0`
*
* @final
*/
class InstalledVersions
{
/**
* @var string|null if set (by reflection by Composer), this should be set to the path where this class is being copied to
* @internal
*/
private static $selfDir = null;
/**
* @var mixed[]|null
* @psalm-var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}|array{}|null
*/
private static $installed;
/**
* @var bool
*/
private static $installedIsLocalDir;
/**
* @var bool|null
*/
private static $canGetVendors;
/**
* @var array[]
* @psalm-var array<string, array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static $installedByVendor = array();
/**
* Returns a list of all package names which are present, either by being installed, replaced or provided
*
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackages()
{
$packages = array();
foreach (self::getInstalled() as $installed) {
$packages[] = array_keys($installed['versions']);
}
if (1 === \count($packages)) {
return $packages[0];
}
return array_keys(array_flip(\call_user_func_array('array_merge', $packages)));
}
/**
* Returns a list of all package names with a specific type e.g. 'library'
*
* @param string $type
* @return string[]
* @psalm-return list<string>
*/
public static function getInstalledPackagesByType($type)
{
$packagesByType = array();
foreach (self::getInstalled() as $installed) {
foreach ($installed['versions'] as $name => $package) {
if (isset($package['type']) && $package['type'] === $type) {
$packagesByType[] = $name;
}
}
}
return $packagesByType;
}
/**
* Checks whether the given package is installed
*
* This also returns true if the package name is provided or replaced by another package
*
* @param string $packageName
* @param bool $includeDevRequirements
* @return bool
*/
public static function isInstalled($packageName, $includeDevRequirements = true)
{
foreach (self::getInstalled() as $installed) {
if (isset($installed['versions'][$packageName])) {
return $includeDevRequirements || !isset($installed['versions'][$packageName]['dev_requirement']) || $installed['versions'][$packageName]['dev_requirement'] === false;
}
}
return false;
}
/**
* Checks whether the given package satisfies a version constraint
*
* e.g. If you want to know whether version 2.3+ of package foo/bar is installed, you would call:
*
* Composer\InstalledVersions::satisfies(new VersionParser, 'foo/bar', '^2.3')
*
* @param VersionParser $parser Install composer/semver to have access to this class and functionality
* @param string $packageName
* @param string|null $constraint A version constraint to check for, if you pass one you have to make sure composer/semver is required by your package
* @return bool
*/
public static function satisfies(VersionParser $parser, $packageName, $constraint)
{
$constraint = $parser->parseConstraints((string) $constraint);
$provided = $parser->parseConstraints(self::getVersionRanges($packageName));
return $provided->matches($constraint);
}
/**
* Returns a version constraint representing all the range(s) which are installed for a given package
*
* It is easier to use this via isInstalled() with the $constraint argument if you need to check
* whether a given version of a package is installed, and not just whether it exists
*
* @param string $packageName
* @return string Version constraint usable with composer/semver
*/
public static function getVersionRanges($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
$ranges = array();
if (isset($installed['versions'][$packageName]['pretty_version'])) {
$ranges[] = $installed['versions'][$packageName]['pretty_version'];
}
if (array_key_exists('aliases', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['aliases']);
}
if (array_key_exists('replaced', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['replaced']);
}
if (array_key_exists('provided', $installed['versions'][$packageName])) {
$ranges = array_merge($ranges, $installed['versions'][$packageName]['provided']);
}
return implode(' || ', $ranges);
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['version'])) {
return null;
}
return $installed['versions'][$packageName]['version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as version, use satisfies or getVersionRanges if you need to know if a given version is present
*/
public static function getPrettyVersion($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['pretty_version'])) {
return null;
}
return $installed['versions'][$packageName]['pretty_version'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as reference
*/
public static function getReference($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
if (!isset($installed['versions'][$packageName]['reference'])) {
return null;
}
return $installed['versions'][$packageName]['reference'];
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @param string $packageName
* @return string|null If the package is being replaced or provided but is not really installed, null will be returned as install path. Packages of type metapackages also have a null install path.
*/
public static function getInstallPath($packageName)
{
foreach (self::getInstalled() as $installed) {
if (!isset($installed['versions'][$packageName])) {
continue;
}
return isset($installed['versions'][$packageName]['install_path']) ? $installed['versions'][$packageName]['install_path'] : null;
}
throw new \OutOfBoundsException('Package "' . $packageName . '" is not installed');
}
/**
* @return array
* @psalm-return array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}
*/
public static function getRootPackage()
{
$installed = self::getInstalled();
return $installed[0]['root'];
}
/**
* Returns the raw installed.php data for custom implementations
*
* @deprecated Use getAllRawData() instead which returns all datasets for all autoloaders present in the process. getRawData only returns the first dataset loaded, which may not be what you expect.
* @return array[]
* @psalm-return array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}
*/
public static function getRawData()
{
@trigger_error('getRawData only returns the first dataset loaded, which may not be what you expect. Use getAllRawData() instead which returns all datasets for all autoloaders present in the process.', E_USER_DEPRECATED);
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
self::$installed = include __DIR__ . '/installed.php';
} else {
self::$installed = array();
}
}
return self::$installed;
}
/**
* Returns the raw data of all installed.php which are currently loaded for custom implementations
*
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
public static function getAllRawData()
{
return self::getInstalled();
}
/**
* Lets you reload the static array from another file
*
* This is only useful for complex integrations in which a project needs to use
* this class but then also needs to execute another project's autoloader in process,
* and wants to ensure both projects have access to their version of installed.php.
*
* A typical case would be PHPUnit, where it would need to make sure it reads all
* the data it needs from this class, then call reload() with
* `require $CWD/vendor/composer/installed.php` (or similar) as input to make sure
* the project in which it runs can then also use this class safely, without
* interference between PHPUnit's dependencies and the project's dependencies.
*
* @param array[] $data A vendor/composer/installed.php data set
* @return void
*
* @psalm-param array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $data
*/
public static function reload($data)
{
self::$installed = $data;
self::$installedByVendor = array();
// when using reload, we disable the duplicate protection to ensure that self::$installed data is
// always returned, but we cannot know whether it comes from the installed.php in __DIR__ or not,
// so we have to assume it does not, and that may result in duplicate data being returned when listing
// all installed packages for example
self::$installedIsLocalDir = false;
}
/**
* @return string
*/
private static function getSelfDir()
{
if (self::$selfDir === null) {
self::$selfDir = strtr(__DIR__, '\\', '/');
}
return self::$selfDir;
}
/**
* @return array[]
* @psalm-return list<array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>}>
*/
private static function getInstalled()
{
if (null === self::$canGetVendors) {
self::$canGetVendors = method_exists('Composer\Autoload\ClassLoader', 'getRegisteredLoaders');
}
$installed = array();
$copiedLocalDir = false;
if (self::$canGetVendors) {
$selfDir = self::getSelfDir();
foreach (ClassLoader::getRegisteredLoaders() as $vendorDir => $loader) {
$vendorDir = strtr($vendorDir, '\\', '/');
if (isset(self::$installedByVendor[$vendorDir])) {
$installed[] = self::$installedByVendor[$vendorDir];
} elseif (is_file($vendorDir.'/composer/installed.php')) {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require $vendorDir.'/composer/installed.php';
self::$installedByVendor[$vendorDir] = $required;
$installed[] = $required;
if (self::$installed === null && $vendorDir.'/composer' === $selfDir) {
self::$installed = $required;
self::$installedIsLocalDir = true;
}
}
if (self::$installedIsLocalDir && $vendorDir.'/composer' === $selfDir) {
$copiedLocalDir = true;
}
}
}
if (null === self::$installed) {
// only require the installed.php file if this file is loaded from its dumped location,
// and not from its source location in the composer/composer package, see https://github.com/composer/composer/issues/9937
if (substr(__DIR__, -8, 1) !== 'C') {
/** @var array{root: array{name: string, pretty_version: string, version: string, reference: string|null, type: string, install_path: string, aliases: string[], dev: bool}, versions: array<string, array{pretty_version?: string, version?: string, reference?: string|null, type?: string, install_path?: string, aliases?: string[], dev_requirement: bool, replaced?: string[], provided?: string[]}>} $required */
$required = require __DIR__ . '/installed.php';
self::$installed = $required;
} else {
self::$installed = array();
}
}
if (self::$installed !== array() && !$copiedLocalDir) {
$installed[] = self::$installed;
}
return $installed;
}
}

21
vendor/composer/LICENSE vendored Normal file
View File

@ -0,0 +1,21 @@
Copyright (c) Nils Adermann, Jordi Boggiano
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

110
vendor/composer/autoload_classmap.php vendored Normal file
View File

@ -0,0 +1,110 @@
<?php
// autoload_classmap.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Attribute' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => $vendorDir . '/composer/InstalledVersions.php',
'Mustache_Cache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_AbstractCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_FilesystemCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Cache_NoopCache' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Compiler' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Context' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Engine' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_InvalidArgumentException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_LogicException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_RuntimeException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_SyntaxException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownFilterException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownHelperException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownTemplateException' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_HelperCollection' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_LambdaHelper' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ArrayLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_CascadingLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_FilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_InlineLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_MutableLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ProductionFilesystemLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Loader_StringLoader' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger_AbstractLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Logger_StreamLogger' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Parser' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Source' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Source_FilesystemSource' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Template' => $vendorDir . '/mustache/mustache/src/compat.php',
'Mustache_Tokenizer' => $vendorDir . '/mustache/mustache/src/compat.php',
'Nette\\ArgumentOutOfRangeException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\DeprecatedException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\DirectoryNotFoundException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\FileNotFoundException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\HtmlStringable' => $vendorDir . '/nette/utils/src/HtmlStringable.php',
'Nette\\IOException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\InvalidArgumentException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\InvalidStateException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\Iterators\\CachingIterator' => $vendorDir . '/nette/utils/src/Iterators/CachingIterator.php',
'Nette\\Iterators\\Mapper' => $vendorDir . '/nette/utils/src/Iterators/Mapper.php',
'Nette\\Localization\\ITranslator' => $vendorDir . '/nette/utils/src/compatibility.php',
'Nette\\Localization\\Translator' => $vendorDir . '/nette/utils/src/Translator.php',
'Nette\\MemberAccessException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\NotImplementedException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\NotSupportedException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\OutOfRangeException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\Schema\\Context' => $vendorDir . '/nette/schema/src/Schema/Context.php',
'Nette\\Schema\\DynamicParameter' => $vendorDir . '/nette/schema/src/Schema/DynamicParameter.php',
'Nette\\Schema\\Elements\\AnyOf' => $vendorDir . '/nette/schema/src/Schema/Elements/AnyOf.php',
'Nette\\Schema\\Elements\\Base' => $vendorDir . '/nette/schema/src/Schema/Elements/Base.php',
'Nette\\Schema\\Elements\\Structure' => $vendorDir . '/nette/schema/src/Schema/Elements/Structure.php',
'Nette\\Schema\\Elements\\Type' => $vendorDir . '/nette/schema/src/Schema/Elements/Type.php',
'Nette\\Schema\\Expect' => $vendorDir . '/nette/schema/src/Schema/Expect.php',
'Nette\\Schema\\Helpers' => $vendorDir . '/nette/schema/src/Schema/Helpers.php',
'Nette\\Schema\\Message' => $vendorDir . '/nette/schema/src/Schema/Message.php',
'Nette\\Schema\\Processor' => $vendorDir . '/nette/schema/src/Schema/Processor.php',
'Nette\\Schema\\Schema' => $vendorDir . '/nette/schema/src/Schema/Schema.php',
'Nette\\Schema\\ValidationException' => $vendorDir . '/nette/schema/src/Schema/ValidationException.php',
'Nette\\ShouldNotHappenException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\SmartObject' => $vendorDir . '/nette/utils/src/SmartObject.php',
'Nette\\StaticClass' => $vendorDir . '/nette/utils/src/StaticClass.php',
'Nette\\UnexpectedValueException' => $vendorDir . '/nette/utils/src/exceptions.php',
'Nette\\Utils\\ArrayHash' => $vendorDir . '/nette/utils/src/Utils/ArrayHash.php',
'Nette\\Utils\\ArrayList' => $vendorDir . '/nette/utils/src/Utils/ArrayList.php',
'Nette\\Utils\\Arrays' => $vendorDir . '/nette/utils/src/Utils/Arrays.php',
'Nette\\Utils\\AssertionException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Callback' => $vendorDir . '/nette/utils/src/Utils/Callback.php',
'Nette\\Utils\\DateTime' => $vendorDir . '/nette/utils/src/Utils/DateTime.php',
'Nette\\Utils\\FileInfo' => $vendorDir . '/nette/utils/src/Utils/FileInfo.php',
'Nette\\Utils\\FileSystem' => $vendorDir . '/nette/utils/src/Utils/FileSystem.php',
'Nette\\Utils\\Finder' => $vendorDir . '/nette/utils/src/Utils/Finder.php',
'Nette\\Utils\\Floats' => $vendorDir . '/nette/utils/src/Utils/Floats.php',
'Nette\\Utils\\Helpers' => $vendorDir . '/nette/utils/src/Utils/Helpers.php',
'Nette\\Utils\\Html' => $vendorDir . '/nette/utils/src/Utils/Html.php',
'Nette\\Utils\\IHtmlString' => $vendorDir . '/nette/utils/src/compatibility.php',
'Nette\\Utils\\Image' => $vendorDir . '/nette/utils/src/Utils/Image.php',
'Nette\\Utils\\ImageColor' => $vendorDir . '/nette/utils/src/Utils/ImageColor.php',
'Nette\\Utils\\ImageException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ImageType' => $vendorDir . '/nette/utils/src/Utils/ImageType.php',
'Nette\\Utils\\Iterables' => $vendorDir . '/nette/utils/src/Utils/Iterables.php',
'Nette\\Utils\\Json' => $vendorDir . '/nette/utils/src/Utils/Json.php',
'Nette\\Utils\\JsonException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => $vendorDir . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\Paginator' => $vendorDir . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Random' => $vendorDir . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => $vendorDir . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => $vendorDir . '/nette/utils/src/Utils/ReflectionMethod.php',
'Nette\\Utils\\RegexpException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Strings' => $vendorDir . '/nette/utils/src/Utils/Strings.php',
'Nette\\Utils\\Type' => $vendorDir . '/nette/utils/src/Utils/Type.php',
'Nette\\Utils\\UnknownImageFileException' => $vendorDir . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Validators' => $vendorDir . '/nette/utils/src/Utils/Validators.php',
'PhpToken' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => $vendorDir . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);

11
vendor/composer/autoload_files.php vendored Normal file
View File

@ -0,0 +1,11 @@
<?php
// autoload_files.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'6e3fae29631ef280660b3cdad06f25a8' => $vendorDir . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => $vendorDir . '/symfony/polyfill-php80/bootstrap.php',
);

View File

@ -0,0 +1,9 @@
<?php
// autoload_namespaces.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
);

16
vendor/composer/autoload_psr4.php vendored Normal file
View File

@ -0,0 +1,16 @@
<?php
// autoload_psr4.php @generated by Composer
$vendorDir = dirname(__DIR__);
$baseDir = dirname($vendorDir);
return array(
'Symfony\\Polyfill\\Php80\\' => array($vendorDir . '/symfony/polyfill-php80'),
'Psr\\EventDispatcher\\' => array($vendorDir . '/psr/event-dispatcher/src'),
'Nette\\' => array($vendorDir . '/nette/schema/src', $vendorDir . '/nette/utils/src'),
'Mustache\\' => array($vendorDir . '/mustache/mustache/src'),
'League\\Config\\' => array($vendorDir . '/league/config/src'),
'League\\CommonMark\\' => array($vendorDir . '/league/commonmark/src'),
'Dflydev\\DotAccessData\\' => array($vendorDir . '/dflydev/dot-access-data/src'),
);

50
vendor/composer/autoload_real.php vendored Normal file
View File

@ -0,0 +1,50 @@
<?php
// autoload_real.php @generated by Composer
class ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c
{
private static $loader;
public static function loadClassLoader($class)
{
if ('Composer\Autoload\ClassLoader' === $class) {
require __DIR__ . '/ClassLoader.php';
}
}
/**
* @return \Composer\Autoload\ClassLoader
*/
public static function getLoader()
{
if (null !== self::$loader) {
return self::$loader;
}
require __DIR__ . '/platform_check.php';
spl_autoload_register(array('ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c', 'loadClassLoader'), true, true);
self::$loader = $loader = new \Composer\Autoload\ClassLoader(\dirname(__DIR__));
spl_autoload_unregister(array('ComposerAutoloaderInit071586d19f5409de22b3235d85d8476c', 'loadClassLoader'));
require __DIR__ . '/autoload_static.php';
call_user_func(\Composer\Autoload\ComposerStaticInit071586d19f5409de22b3235d85d8476c::getInitializer($loader));
$loader->register(true);
$filesToLoad = \Composer\Autoload\ComposerStaticInit071586d19f5409de22b3235d85d8476c::$files;
$requireFile = \Closure::bind(static function ($fileIdentifier, $file) {
if (empty($GLOBALS['__composer_autoload_files'][$fileIdentifier])) {
$GLOBALS['__composer_autoload_files'][$fileIdentifier] = true;
require $file;
}
}, null, null);
foreach ($filesToLoad as $fileIdentifier => $file) {
$requireFile($fileIdentifier, $file);
}
return $loader;
}
}

187
vendor/composer/autoload_static.php vendored Normal file
View File

@ -0,0 +1,187 @@
<?php
// autoload_static.php @generated by Composer
namespace Composer\Autoload;
class ComposerStaticInit071586d19f5409de22b3235d85d8476c
{
public static $files = array (
'6e3fae29631ef280660b3cdad06f25a8' => __DIR__ . '/..' . '/symfony/deprecation-contracts/function.php',
'a4a119a56e50fbb293281d9a48007e0e' => __DIR__ . '/..' . '/symfony/polyfill-php80/bootstrap.php',
);
public static $prefixLengthsPsr4 = array (
'S' =>
array (
'Symfony\\Polyfill\\Php80\\' => 23,
),
'P' =>
array (
'Psr\\EventDispatcher\\' => 20,
),
'N' =>
array (
'Nette\\' => 6,
),
'M' =>
array (
'Mustache\\' => 9,
),
'L' =>
array (
'League\\Config\\' => 14,
'League\\CommonMark\\' => 18,
),
'D' =>
array (
'Dflydev\\DotAccessData\\' => 22,
),
);
public static $prefixDirsPsr4 = array (
'Symfony\\Polyfill\\Php80\\' =>
array (
0 => __DIR__ . '/..' . '/symfony/polyfill-php80',
),
'Psr\\EventDispatcher\\' =>
array (
0 => __DIR__ . '/..' . '/psr/event-dispatcher/src',
),
'Nette\\' =>
array (
0 => __DIR__ . '/..' . '/nette/schema/src',
1 => __DIR__ . '/..' . '/nette/utils/src',
),
'Mustache\\' =>
array (
0 => __DIR__ . '/..' . '/mustache/mustache/src',
),
'League\\Config\\' =>
array (
0 => __DIR__ . '/..' . '/league/config/src',
),
'League\\CommonMark\\' =>
array (
0 => __DIR__ . '/..' . '/league/commonmark/src',
),
'Dflydev\\DotAccessData\\' =>
array (
0 => __DIR__ . '/..' . '/dflydev/dot-access-data/src',
),
);
public static $classMap = array (
'Attribute' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Attribute.php',
'Composer\\InstalledVersions' => __DIR__ . '/..' . '/composer/InstalledVersions.php',
'Mustache_Cache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_AbstractCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_FilesystemCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Cache_NoopCache' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Compiler' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Context' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Engine' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_InvalidArgumentException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_LogicException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_RuntimeException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_SyntaxException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownFilterException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownHelperException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Exception_UnknownTemplateException' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_HelperCollection' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_LambdaHelper' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ArrayLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_CascadingLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_FilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_InlineLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_MutableLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_ProductionFilesystemLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Loader_StringLoader' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger_AbstractLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Logger_StreamLogger' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Parser' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Source' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Source_FilesystemSource' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Template' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Mustache_Tokenizer' => __DIR__ . '/..' . '/mustache/mustache/src/compat.php',
'Nette\\ArgumentOutOfRangeException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\DeprecatedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\DirectoryNotFoundException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\FileNotFoundException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\HtmlStringable' => __DIR__ . '/..' . '/nette/utils/src/HtmlStringable.php',
'Nette\\IOException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\InvalidArgumentException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\InvalidStateException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\Iterators\\CachingIterator' => __DIR__ . '/..' . '/nette/utils/src/Iterators/CachingIterator.php',
'Nette\\Iterators\\Mapper' => __DIR__ . '/..' . '/nette/utils/src/Iterators/Mapper.php',
'Nette\\Localization\\ITranslator' => __DIR__ . '/..' . '/nette/utils/src/compatibility.php',
'Nette\\Localization\\Translator' => __DIR__ . '/..' . '/nette/utils/src/Translator.php',
'Nette\\MemberAccessException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\NotImplementedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\NotSupportedException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\OutOfRangeException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\Schema\\Context' => __DIR__ . '/..' . '/nette/schema/src/Schema/Context.php',
'Nette\\Schema\\DynamicParameter' => __DIR__ . '/..' . '/nette/schema/src/Schema/DynamicParameter.php',
'Nette\\Schema\\Elements\\AnyOf' => __DIR__ . '/..' . '/nette/schema/src/Schema/Elements/AnyOf.php',
'Nette\\Schema\\Elements\\Base' => __DIR__ . '/..' . '/nette/schema/src/Schema/Elements/Base.php',
'Nette\\Schema\\Elements\\Structure' => __DIR__ . '/..' . '/nette/schema/src/Schema/Elements/Structure.php',
'Nette\\Schema\\Elements\\Type' => __DIR__ . '/..' . '/nette/schema/src/Schema/Elements/Type.php',
'Nette\\Schema\\Expect' => __DIR__ . '/..' . '/nette/schema/src/Schema/Expect.php',
'Nette\\Schema\\Helpers' => __DIR__ . '/..' . '/nette/schema/src/Schema/Helpers.php',
'Nette\\Schema\\Message' => __DIR__ . '/..' . '/nette/schema/src/Schema/Message.php',
'Nette\\Schema\\Processor' => __DIR__ . '/..' . '/nette/schema/src/Schema/Processor.php',
'Nette\\Schema\\Schema' => __DIR__ . '/..' . '/nette/schema/src/Schema/Schema.php',
'Nette\\Schema\\ValidationException' => __DIR__ . '/..' . '/nette/schema/src/Schema/ValidationException.php',
'Nette\\ShouldNotHappenException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\SmartObject' => __DIR__ . '/..' . '/nette/utils/src/SmartObject.php',
'Nette\\StaticClass' => __DIR__ . '/..' . '/nette/utils/src/StaticClass.php',
'Nette\\UnexpectedValueException' => __DIR__ . '/..' . '/nette/utils/src/exceptions.php',
'Nette\\Utils\\ArrayHash' => __DIR__ . '/..' . '/nette/utils/src/Utils/ArrayHash.php',
'Nette\\Utils\\ArrayList' => __DIR__ . '/..' . '/nette/utils/src/Utils/ArrayList.php',
'Nette\\Utils\\Arrays' => __DIR__ . '/..' . '/nette/utils/src/Utils/Arrays.php',
'Nette\\Utils\\AssertionException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Callback' => __DIR__ . '/..' . '/nette/utils/src/Utils/Callback.php',
'Nette\\Utils\\DateTime' => __DIR__ . '/..' . '/nette/utils/src/Utils/DateTime.php',
'Nette\\Utils\\FileInfo' => __DIR__ . '/..' . '/nette/utils/src/Utils/FileInfo.php',
'Nette\\Utils\\FileSystem' => __DIR__ . '/..' . '/nette/utils/src/Utils/FileSystem.php',
'Nette\\Utils\\Finder' => __DIR__ . '/..' . '/nette/utils/src/Utils/Finder.php',
'Nette\\Utils\\Floats' => __DIR__ . '/..' . '/nette/utils/src/Utils/Floats.php',
'Nette\\Utils\\Helpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/Helpers.php',
'Nette\\Utils\\Html' => __DIR__ . '/..' . '/nette/utils/src/Utils/Html.php',
'Nette\\Utils\\IHtmlString' => __DIR__ . '/..' . '/nette/utils/src/compatibility.php',
'Nette\\Utils\\Image' => __DIR__ . '/..' . '/nette/utils/src/Utils/Image.php',
'Nette\\Utils\\ImageColor' => __DIR__ . '/..' . '/nette/utils/src/Utils/ImageColor.php',
'Nette\\Utils\\ImageException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ImageType' => __DIR__ . '/..' . '/nette/utils/src/Utils/ImageType.php',
'Nette\\Utils\\Iterables' => __DIR__ . '/..' . '/nette/utils/src/Utils/Iterables.php',
'Nette\\Utils\\Json' => __DIR__ . '/..' . '/nette/utils/src/Utils/Json.php',
'Nette\\Utils\\JsonException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\ObjectHelpers' => __DIR__ . '/..' . '/nette/utils/src/Utils/ObjectHelpers.php',
'Nette\\Utils\\Paginator' => __DIR__ . '/..' . '/nette/utils/src/Utils/Paginator.php',
'Nette\\Utils\\Random' => __DIR__ . '/..' . '/nette/utils/src/Utils/Random.php',
'Nette\\Utils\\Reflection' => __DIR__ . '/..' . '/nette/utils/src/Utils/Reflection.php',
'Nette\\Utils\\ReflectionMethod' => __DIR__ . '/..' . '/nette/utils/src/Utils/ReflectionMethod.php',
'Nette\\Utils\\RegexpException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Strings' => __DIR__ . '/..' . '/nette/utils/src/Utils/Strings.php',
'Nette\\Utils\\Type' => __DIR__ . '/..' . '/nette/utils/src/Utils/Type.php',
'Nette\\Utils\\UnknownImageFileException' => __DIR__ . '/..' . '/nette/utils/src/Utils/exceptions.php',
'Nette\\Utils\\Validators' => __DIR__ . '/..' . '/nette/utils/src/Utils/Validators.php',
'PhpToken' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/PhpToken.php',
'Stringable' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/Stringable.php',
'UnhandledMatchError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/UnhandledMatchError.php',
'ValueError' => __DIR__ . '/..' . '/symfony/polyfill-php80/Resources/stubs/ValueError.php',
);
public static function getInitializer(ClassLoader $loader)
{
return \Closure::bind(function () use ($loader) {
$loader->prefixLengthsPsr4 = ComposerStaticInit071586d19f5409de22b3235d85d8476c::$prefixLengthsPsr4;
$loader->prefixDirsPsr4 = ComposerStaticInit071586d19f5409de22b3235d85d8476c::$prefixDirsPsr4;
$loader->classMap = ComposerStaticInit071586d19f5409de22b3235d85d8476c::$classMap;
}, null, ClassLoader::class);
}
}

705
vendor/composer/installed.json vendored Normal file
View File

@ -0,0 +1,705 @@
{
"packages": [
{
"name": "dflydev/dot-access-data",
"version": "v3.0.3",
"version_normalized": "3.0.3.0",
"source": {
"type": "git",
"url": "https://github.com/dflydev/dflydev-dot-access-data.git",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/dflydev/dflydev-dot-access-data/zipball/a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"reference": "a23a2bf4f31d3518f3ecb38660c95715dfead60f",
"shasum": ""
},
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.0.0"
},
"time": "2024-07-08T12:26:09+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": [
"access",
"data",
"dot",
"notation"
],
"support": {
"issues": "https://github.com/dflydev/dflydev-dot-access-data/issues",
"source": "https://github.com/dflydev/dflydev-dot-access-data/tree/v3.0.3"
},
"install-path": "../dflydev/dot-access-data"
},
{
"name": "league/commonmark",
"version": "2.7.1",
"version_normalized": "2.7.1.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/commonmark.git",
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/commonmark/zipball/10732241927d3971d28e7ea7b5712721fa2296ca",
"reference": "10732241927d3971d28e7ea7b5712721fa2296ca",
"shasum": ""
},
"require": {
"ext-mbstring": "*",
"league/config": "^1.1.1",
"php": "^7.4 || ^8.0",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.31.1",
"commonmark/commonmark.js": "0.31.1",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
"ext-json": "*",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
},
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"time": "2025-07-20T12:47:49+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "2.8-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"homepage": "https://commonmark.thephpleague.com",
"keywords": [
"commonmark",
"flavored",
"gfm",
"github",
"github-flavored",
"markdown",
"md",
"parser"
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/league/commonmark",
"type": "tidelift"
}
],
"install-path": "../league/commonmark"
},
{
"name": "league/config",
"version": "v1.2.0",
"version_normalized": "1.2.0.0",
"source": {
"type": "git",
"url": "https://github.com/thephpleague/config.git",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/thephpleague/config/zipball/754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"reference": "754b3604fb2984c71f4af4a9cbe7b57f346ec1f3",
"shasum": ""
},
"require": {
"dflydev/dot-access-data": "^3.0.1",
"nette/schema": "^1.2",
"php": "^7.4 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.5",
"scrutinizer/ocular": "^1.8.1",
"unleashedtech/php-coding-standard": "^3.1",
"vimeo/psalm": "^4.7.3"
},
"time": "2022-12-11T20:36:23+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-main": "1.2-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"League\\Config\\": "src"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause"
],
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"description": "Define configuration arrays with strict schemas and access values with dot notation",
"homepage": "https://config.thephpleague.com",
"keywords": [
"array",
"config",
"configuration",
"dot",
"dot-access",
"nested",
"schema"
],
"support": {
"docs": "https://config.thephpleague.com/",
"issues": "https://github.com/thephpleague/config/issues",
"rss": "https://github.com/thephpleague/config/releases.atom",
"source": "https://github.com/thephpleague/config"
},
"funding": [
{
"url": "https://www.colinodell.com/sponsor",
"type": "custom"
},
{
"url": "https://www.paypal.me/colinpodell/10.00",
"type": "custom"
},
{
"url": "https://github.com/colinodell",
"type": "github"
}
],
"install-path": "../league/config"
},
{
"name": "mustache/mustache",
"version": "v3.0.0",
"version_normalized": "3.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/bobthecow/mustache.php.git",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/bobthecow/mustache.php/zipball/176b6b21d68516dd5107a63ab71b0050e518b7a4",
"reference": "176b6b21d68516dd5107a63ab71b0050e518b7a4",
"shasum": ""
},
"require": {
"php": ">=5.6"
},
"require-dev": {
"friendsofphp/php-cs-fixer": "~2.19.3",
"yoast/phpunit-polyfills": "^2.0"
},
"time": "2025-06-28T18:28:20+00:00",
"type": "library",
"installation-source": "dist",
"autoload": {
"psr-4": {
"Mustache\\": "src/"
},
"classmap": [
"src/compat.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Justin Hileman",
"email": "justin@justinhileman.info",
"homepage": "http://justinhileman.com"
}
],
"description": "A Mustache implementation in PHP.",
"homepage": "https://github.com/bobthecow/mustache.php",
"keywords": [
"mustache",
"templating"
],
"support": {
"issues": "https://github.com/bobthecow/mustache.php/issues",
"source": "https://github.com/bobthecow/mustache.php/tree/v3.0.0"
},
"install-path": "../mustache/mustache"
},
{
"name": "nette/schema",
"version": "v1.3.3",
"version_normalized": "1.3.3.0",
"source": {
"type": "git",
"url": "https://github.com/nette/schema.git",
"reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/schema/zipball/2befc2f42d7c715fd9d95efc31b1081e5d765004",
"reference": "2befc2f42d7c715fd9d95efc31b1081e5d765004",
"shasum": ""
},
"require": {
"nette/utils": "^4.0",
"php": "8.1 - 8.5"
},
"require-dev": {
"nette/tester": "^2.5.2",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.8"
},
"time": "2025-10-30T22:57:59+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.3-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "📐 Nette Schema: validating data structures against a given Schema.",
"homepage": "https://nette.org",
"keywords": [
"config",
"nette"
],
"support": {
"issues": "https://github.com/nette/schema/issues",
"source": "https://github.com/nette/schema/tree/v1.3.3"
},
"install-path": "../nette/schema"
},
{
"name": "nette/utils",
"version": "v4.0.8",
"version_normalized": "4.0.8.0",
"source": {
"type": "git",
"url": "https://github.com/nette/utils.git",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nette/utils/zipball/c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"reference": "c930ca4e3cf4f17dcfb03037703679d2396d2ede",
"shasum": ""
},
"require": {
"php": "8.0 - 8.5"
},
"conflict": {
"nette/finder": "<3",
"nette/schema": "<1.2.2"
},
"require-dev": {
"jetbrains/phpstorm-attributes": "^1.2",
"nette/tester": "^2.5",
"phpstan/phpstan-nette": "^2.0@stable",
"tracy/tracy": "^2.9"
},
"suggest": {
"ext-gd": "to use Image",
"ext-iconv": "to use Strings::webalize(), toAscii(), chr() and reverse()",
"ext-intl": "to use Strings::webalize(), toAscii(), normalize() and compare()",
"ext-json": "to use Nette\\Utils\\Json",
"ext-mbstring": "to use Strings::lower() etc...",
"ext-tokenizer": "to use Nette\\Utils\\Reflection::getUseStatements()"
},
"time": "2025-08-06T21:43:34+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "4.0-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Nette\\": "src"
},
"classmap": [
"src/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"BSD-3-Clause",
"GPL-2.0-only",
"GPL-3.0-only"
],
"authors": [
{
"name": "David Grudl",
"homepage": "https://davidgrudl.com"
},
{
"name": "Nette Community",
"homepage": "https://nette.org/contributors"
}
],
"description": "🛠 Nette Utils: lightweight utilities for string & array manipulation, image handling, safe JSON encoding/decoding, validation, slug or strong password generating etc.",
"homepage": "https://nette.org",
"keywords": [
"array",
"core",
"datetime",
"images",
"json",
"nette",
"paginator",
"password",
"slugify",
"string",
"unicode",
"utf-8",
"utility",
"validation"
],
"support": {
"issues": "https://github.com/nette/utils/issues",
"source": "https://github.com/nette/utils/tree/v4.0.8"
},
"install-path": "../nette/utils"
},
{
"name": "psr/event-dispatcher",
"version": "1.0.0",
"version_normalized": "1.0.0.0",
"source": {
"type": "git",
"url": "https://github.com/php-fig/event-dispatcher.git",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
"reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
"shasum": ""
},
"require": {
"php": ">=7.2.0"
},
"time": "2019-01-08T18:20:26+00:00",
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "1.0.x-dev"
}
},
"installation-source": "dist",
"autoload": {
"psr-4": {
"Psr\\EventDispatcher\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "PHP-FIG",
"homepage": "http://www.php-fig.org/"
}
],
"description": "Standard interfaces for event handling.",
"keywords": [
"events",
"psr",
"psr-14"
],
"support": {
"issues": "https://github.com/php-fig/event-dispatcher/issues",
"source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
},
"install-path": "../psr/event-dispatcher"
},
{
"name": "symfony/deprecation-contracts",
"version": "v3.6.0",
"version_normalized": "3.6.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/deprecation-contracts.git",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/63afe740e99a13ba87ec199bb07bbdee937a5b62",
"reference": "63afe740e99a13ba87ec199bb07bbdee937a5b62",
"shasum": ""
},
"require": {
"php": ">=8.1"
},
"time": "2024-09-25T14:21:43+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/contracts",
"name": "symfony/contracts"
},
"branch-alias": {
"dev-main": "3.6-dev"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"function.php"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "A generic function and convention to trigger deprecation notices",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/deprecation-contracts/tree/v3.6.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/deprecation-contracts"
},
{
"name": "symfony/polyfill-php80",
"version": "v1.33.0",
"version_normalized": "1.33.0.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-php80.git",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"reference": "0cc9dd0f17f61d8131e7df6b84bd344899fe2608",
"shasum": ""
},
"require": {
"php": ">=7.2"
},
"time": "2025-01-02T08:10:11+00:00",
"type": "library",
"extra": {
"thanks": {
"url": "https://github.com/symfony/polyfill",
"name": "symfony/polyfill"
}
},
"installation-source": "dist",
"autoload": {
"files": [
"bootstrap.php"
],
"psr-4": {
"Symfony\\Polyfill\\Php80\\": ""
},
"classmap": [
"Resources/stubs"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Ion Bazan",
"email": "ion.bazan@gmail.com"
},
{
"name": "Nicolas Grekas",
"email": "p@tchwork.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
"homepage": "https://symfony.com",
"keywords": [
"compatibility",
"polyfill",
"portable",
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-php80/tree/v1.33.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://github.com/nicolas-grekas",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"install-path": "../symfony/polyfill-php80"
}
],
"dev": true,
"dev-package-names": []
}

104
vendor/composer/installed.php vendored Normal file
View File

@ -0,0 +1,104 @@
<?php return array(
'root' => array(
'name' => '__root__',
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'dfe2df141ba6e64e5425699cd553fb9b7e6d6193',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev' => true,
),
'versions' => array(
'__root__' => array(
'pretty_version' => 'dev-main',
'version' => 'dev-main',
'reference' => 'dfe2df141ba6e64e5425699cd553fb9b7e6d6193',
'type' => 'library',
'install_path' => __DIR__ . '/../../',
'aliases' => array(),
'dev_requirement' => false,
),
'dflydev/dot-access-data' => array(
'pretty_version' => 'v3.0.3',
'version' => '3.0.3.0',
'reference' => 'a23a2bf4f31d3518f3ecb38660c95715dfead60f',
'type' => 'library',
'install_path' => __DIR__ . '/../dflydev/dot-access-data',
'aliases' => array(),
'dev_requirement' => false,
),
'league/commonmark' => array(
'pretty_version' => '2.7.1',
'version' => '2.7.1.0',
'reference' => '10732241927d3971d28e7ea7b5712721fa2296ca',
'type' => 'library',
'install_path' => __DIR__ . '/../league/commonmark',
'aliases' => array(),
'dev_requirement' => false,
),
'league/config' => array(
'pretty_version' => 'v1.2.0',
'version' => '1.2.0.0',
'reference' => '754b3604fb2984c71f4af4a9cbe7b57f346ec1f3',
'type' => 'library',
'install_path' => __DIR__ . '/../league/config',
'aliases' => array(),
'dev_requirement' => false,
),
'mustache/mustache' => array(
'pretty_version' => 'v3.0.0',
'version' => '3.0.0.0',
'reference' => '176b6b21d68516dd5107a63ab71b0050e518b7a4',
'type' => 'library',
'install_path' => __DIR__ . '/../mustache/mustache',
'aliases' => array(),
'dev_requirement' => false,
),
'nette/schema' => array(
'pretty_version' => 'v1.3.3',
'version' => '1.3.3.0',
'reference' => '2befc2f42d7c715fd9d95efc31b1081e5d765004',
'type' => 'library',
'install_path' => __DIR__ . '/../nette/schema',
'aliases' => array(),
'dev_requirement' => false,
),
'nette/utils' => array(
'pretty_version' => 'v4.0.8',
'version' => '4.0.8.0',
'reference' => 'c930ca4e3cf4f17dcfb03037703679d2396d2ede',
'type' => 'library',
'install_path' => __DIR__ . '/../nette/utils',
'aliases' => array(),
'dev_requirement' => false,
),
'psr/event-dispatcher' => array(
'pretty_version' => '1.0.0',
'version' => '1.0.0.0',
'reference' => 'dbefd12671e8a14ec7f180cab83036ed26714bb0',
'type' => 'library',
'install_path' => __DIR__ . '/../psr/event-dispatcher',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/deprecation-contracts' => array(
'pretty_version' => 'v3.6.0',
'version' => '3.6.0.0',
'reference' => '63afe740e99a13ba87ec199bb07bbdee937a5b62',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/deprecation-contracts',
'aliases' => array(),
'dev_requirement' => false,
),
'symfony/polyfill-php80' => array(
'pretty_version' => 'v1.33.0',
'version' => '1.33.0.0',
'reference' => '0cc9dd0f17f61d8131e7df6b84bd344899fe2608',
'type' => 'library',
'install_path' => __DIR__ . '/../symfony/polyfill-php80',
'aliases' => array(),
'dev_requirement' => false,
),
),
);

25
vendor/composer/platform_check.php vendored Normal file
View File

@ -0,0 +1,25 @@
<?php
// platform_check.php @generated by Composer
$issues = array();
if (!(PHP_VERSION_ID >= 80100)) {
$issues[] = 'Your Composer dependencies require a PHP version ">= 8.1.0". You are running ' . PHP_VERSION . '.';
}
if ($issues) {
if (!headers_sent()) {
header('HTTP/1.1 500 Internal Server Error');
}
if (!ini_get('display_errors')) {
if (PHP_SAPI === 'cli' || PHP_SAPI === 'phpdbg') {
fwrite(STDERR, 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . implode(PHP_EOL, $issues) . PHP_EOL.PHP_EOL);
} elseif (!headers_sent()) {
echo 'Composer detected issues in your platform:' . PHP_EOL.PHP_EOL . str_replace('You are running '.PHP_VERSION.'.', '', implode(PHP_EOL, $issues)) . PHP_EOL.PHP_EOL;
}
}
throw new \RuntimeException(
'Composer detected issues in your platform: ' . implode(' ', $issues)
);
}

View File

@ -0,0 +1,74 @@
# Changelog
All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased]
## [3.0.3] - 2024-07-08
### Fixed
- Fixed PHP 8.4 deprecation notices (#47)
## [3.0.2] - 2022-10-27
### Fixed
- Added missing return types to docblocks (#44, #45)
## [3.0.1] - 2021-08-13
### Added
- Adds ReturnTypeWillChange to suppress PHP 8.1 warnings (#40)
## [3.0.0] - 2021-01-01
### Added
- Added support for both `.` and `/`-delimited key paths (#24)
- Added parameter and return types to everything; enabled strict type checks (#18)
- Added new exception classes to better identify certain types of errors (#20)
- `Data` now implements `ArrayAccess` (#17)
- Added ability to merge non-associative array values (#31, #32)
### Changed
- All thrown exceptions are now instances or subclasses of `DataException` (#20)
- Calling `get()` on a missing key path without providing a default will throw a `MissingPathException` instead of returning `null` (#29)
- Bumped supported PHP versions to 7.1 - 8.x (#18)
### Fixed
- Fixed incorrect merging of array values into string values (#32)
- Fixed `get()` method behaving as if keys with `null` values didn't exist
## [2.0.0] - 2017-12-21
### Changed
- Bumped supported PHP versions to 7.0 - 7.4 (#12)
- Switched to PSR-4 autoloading
## [1.1.0] - 2017-01-20
### Added
- Added new `has()` method to check for the existence of the given key (#4, #7)
## [1.0.1] - 2015-08-12
### Added
- Added new optional `$default` parameter to the `get()` method (#2)
## [1.0.0] - 2012-07-17
**Initial release!**
[Unreleased]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.3...main
[3.0.3]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.2...v3.0.3
[3.0.2]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.1...v3.0.2
[3.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v3.0.0...v3.0.1
[3.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v2.0.0...v3.0.0
[2.0.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.1.0...v2.0.0
[1.1.0]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.1...v1.1.0
[1.0.1]: https://github.com/dflydev/dflydev-dot-access-data/compare/v1.0.0...v1.0.1
[1.0.0]: https://github.com/dflydev/dflydev-dot-access-data/releases/tag/v1.0.0

19
vendor/dflydev/dot-access-data/LICENSE vendored Normal file
View File

@ -0,0 +1,19 @@
Copyright (c) 2012 Dragonfly Development Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is furnished
to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

158
vendor/dflydev/dot-access-data/README.md vendored Normal file
View File

@ -0,0 +1,158 @@
Dot Access Data
===============
[![Latest Version](https://img.shields.io/packagist/v/dflydev/dot-access-data.svg?style=flat-square)](https://packagist.org/packages/dflydev/dot-access-data)
[![Total Downloads](https://img.shields.io/packagist/dt/dflydev/dot-access-data.svg?style=flat-square)](https://packagist.org/packages/dflydev/dot-access-data)
[![Software License](https://img.shields.io/badge/License-MIT-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/github/workflow/status/dflydev/dflydev-dot-access-data/Tests/main.svg?style=flat-square)](https://github.com/dflydev/dflydev-dot-access-data/actions?query=workflow%3ATests+branch%3Amain)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/dflydev/dflydev-dot-access-data.svg?style=flat-square)](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data/code-structure/)
[![Quality Score](https://img.shields.io/scrutinizer/g/dflydev/dflydev-dot-access-data.svg?style=flat-square)](https://scrutinizer-ci.com/g/dflydev/dflydev-dot-access-data)
Given a deep data structure, access data by dot notation.
Requirements
------------
* PHP (7.1+)
> For PHP (5.3+) please refer to version `1.0`.
Usage
-----
Abstract example:
```php
use Dflydev\DotAccessData\Data;
$data = new Data;
$data->set('a.b.c', 'C');
$data->set('a.b.d', 'D1');
$data->append('a.b.d', 'D2');
$data->set('a.b.e', ['E0', 'E1', 'E2']);
// C
$data->get('a.b.c');
// ['D1', 'D2']
$data->get('a.b.d');
// ['E0', 'E1', 'E2']
$data->get('a.b.e');
// true
$data->has('a.b.c');
// false
$data->has('a.b.d.j');
// 'some-default-value'
$data->get('some.path.that.does.not.exist', 'some-default-value');
// throws a MissingPathException because no default was given
$data->get('some.path.that.does.not.exist');
```
A more concrete example:
```php
use Dflydev\DotAccessData\Data;
$data = new Data([
'hosts' => [
'hewey' => [
'username' => 'hman',
'password' => 'HPASS',
'roles' => ['web'],
],
'dewey' => [
'username' => 'dman',
'password' => 'D---S',
'roles' => ['web', 'db'],
'nick' => 'dewey dman',
],
'lewey' => [
'username' => 'lman',
'password' => 'LP@$$',
'roles' => ['db'],
],
],
]);
// hman
$username = $data->get('hosts.hewey.username');
// HPASS
$password = $data->get('hosts.hewey.password');
// ['web']
$roles = $data->get('hosts.hewey.roles');
// dewey dman
$nick = $data->get('hosts.dewey.nick');
// Unknown
$nick = $data->get('hosts.lewey.nick', 'Unknown');
// DataInterface instance
$dewey = $data->getData('hosts.dewey');
// dman
$username = $dewey->get('username');
// D---S
$password = $dewey->get('password');
// ['web', 'db']
$roles = $dewey->get('roles');
// No more lewey
$data->remove('hosts.lewey');
// Add DB to hewey's roles
$data->append('hosts.hewey.roles', 'db');
$data->set('hosts.april', [
'username' => 'aman',
'password' => '@---S',
'roles' => ['web'],
]);
// Check if a key exists (true to this case)
$hasKey = $data->has('hosts.dewey.username');
```
`Data` may be used as an array, since it implements `ArrayAccess` interface:
```php
// Get
$data->get('name') === $data['name']; // true
$data['name'] = 'Dewey';
// is equivalent to
$data->set($name, 'Dewey');
isset($data['name']) === $data->has('name');
// Remove key
unset($data['name']);
```
`/` can also be used as a path delimiter:
```php
$data->set('a/b/c', 'd');
echo $data->get('a/b/c'); // "d"
$data->get('a/b/c') === $data->get('a.b.c'); // true
```
License
-------
This library is licensed under the MIT License - see the LICENSE file
for details.
Community
---------
If you have questions or want to help out, join us in the
[#dflydev](irc://irc.freenode.net/#dflydev) channel on irc.freenode.net.

View File

@ -0,0 +1,67 @@
{
"name": "dflydev/dot-access-data",
"type": "library",
"description": "Given a deep data structure, access data by dot notation.",
"homepage": "https://github.com/dflydev/dflydev-dot-access-data",
"keywords": ["dot", "access", "data", "notation"],
"license": "MIT",
"authors": [
{
"name": "Dragonfly Development Inc.",
"email": "info@dflydev.com",
"homepage": "http://dflydev.com"
},
{
"name": "Beau Simensen",
"email": "beau@dflydev.com",
"homepage": "http://beausimensen.com"
},
{
"name": "Carlos Frutos",
"email": "carlos@kiwing.it",
"homepage": "https://github.com/cfrutos"
},
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com"
}
],
"require": {
"php": "^7.1 || ^8.0"
},
"require-dev": {
"phpstan/phpstan": "^0.12.42",
"phpunit/phpunit": "^7.5 || ^8.5 || ^9.3",
"scrutinizer/ocular": "1.6.0",
"squizlabs/php_codesniffer": "^3.5",
"vimeo/psalm": "^4.0.0"
},
"autoload": {
"psr-4": {
"Dflydev\\DotAccessData\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"Dflydev\\DotAccessData\\": "tests/"
}
},
"extra": {
"branch-alias": {
"dev-main": "3.x-dev"
}
},
"scripts": {
"phpcs": "phpcs",
"phpstan": "phpstan analyse",
"phpunit": "phpunit --no-coverage",
"psalm": "psalm",
"test": [
"@phpcs",
"@phpstan",
"@psalm",
"@phpunit"
]
}
}

View File

@ -0,0 +1,286 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData;
use ArrayAccess;
use Dflydev\DotAccessData\Exception\DataException;
use Dflydev\DotAccessData\Exception\InvalidPathException;
use Dflydev\DotAccessData\Exception\MissingPathException;
/**
* @implements ArrayAccess<string, mixed>
*/
class Data implements DataInterface, ArrayAccess
{
private const DELIMITERS = ['.', '/'];
/**
* Internal representation of data data
*
* @var array<string, mixed>
*/
protected $data;
/**
* Constructor
*
* @param array<string, mixed> $data
*/
public function __construct(array $data = [])
{
$this->data = $data;
}
/**
* {@inheritdoc}
*/
public function append(string $key, $value = null): void
{
$currentValue =& $this->data;
$keyPath = self::keyToPathArray($key);
$endKey = array_pop($keyPath);
foreach ($keyPath as $currentKey) {
if (! isset($currentValue[$currentKey])) {
$currentValue[$currentKey] = [];
}
$currentValue =& $currentValue[$currentKey];
}
if (!isset($currentValue[$endKey])) {
$currentValue[$endKey] = [];
}
if (!is_array($currentValue[$endKey])) {
// Promote this key to an array.
// TODO: Is this really what we want to do?
$currentValue[$endKey] = [$currentValue[$endKey]];
}
$currentValue[$endKey][] = $value;
}
/**
* {@inheritdoc}
*/
public function set(string $key, $value = null): void
{
$currentValue =& $this->data;
$keyPath = self::keyToPathArray($key);
$endKey = array_pop($keyPath);
foreach ($keyPath as $currentKey) {
if (!isset($currentValue[$currentKey])) {
$currentValue[$currentKey] = [];
}
if (!is_array($currentValue[$currentKey])) {
throw new DataException(sprintf('Key path "%s" within "%s" cannot be indexed into (is not an array)', $currentKey, self::formatPath($key)));
}
$currentValue =& $currentValue[$currentKey];
}
$currentValue[$endKey] = $value;
}
/**
* {@inheritdoc}
*/
public function remove(string $key): void
{
$currentValue =& $this->data;
$keyPath = self::keyToPathArray($key);
$endKey = array_pop($keyPath);
foreach ($keyPath as $currentKey) {
if (!isset($currentValue[$currentKey])) {
return;
}
$currentValue =& $currentValue[$currentKey];
}
unset($currentValue[$endKey]);
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function get(string $key, $default = null)
{
/** @psalm-suppress ImpureFunctionCall */
$hasDefault = \func_num_args() > 1;
$currentValue = $this->data;
$keyPath = self::keyToPathArray($key);
foreach ($keyPath as $currentKey) {
if (!is_array($currentValue) || !array_key_exists($currentKey, $currentValue)) {
if ($hasDefault) {
return $default;
}
throw new MissingPathException($key, sprintf('No data exists at the given path: "%s"', self::formatPath($keyPath)));
}
$currentValue = $currentValue[$currentKey];
}
return $currentValue === null ? $default : $currentValue;
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function has(string $key): bool
{
$currentValue = $this->data;
foreach (self::keyToPathArray($key) as $currentKey) {
if (
!is_array($currentValue) ||
!array_key_exists($currentKey, $currentValue)
) {
return false;
}
$currentValue = $currentValue[$currentKey];
}
return true;
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function getData(string $key): DataInterface
{
$value = $this->get($key);
if (is_array($value) && Util::isAssoc($value)) {
return new Data($value);
}
throw new DataException(sprintf('Value at "%s" could not be represented as a DataInterface', self::formatPath($key)));
}
/**
* {@inheritdoc}
*/
public function import(array $data, int $mode = self::REPLACE): void
{
$this->data = Util::mergeAssocArray($this->data, $data, $mode);
}
/**
* {@inheritdoc}
*/
public function importData(DataInterface $data, int $mode = self::REPLACE): void
{
$this->import($data->export(), $mode);
}
/**
* {@inheritdoc}
*
* @psalm-mutation-free
*/
public function export(): array
{
return $this->data;
}
/**
* {@inheritdoc}
*
* @return bool
*/
#[\ReturnTypeWillChange]
public function offsetExists($key)
{
return $this->has($key);
}
/**
* {@inheritdoc}
*
* @return mixed
*/
#[\ReturnTypeWillChange]
public function offsetGet($key)
{
return $this->get($key, null);
}
/**
* {@inheritdoc}
*
* @param string $key
* @param mixed $value
*
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetSet($key, $value)
{
$this->set($key, $value);
}
/**
* {@inheritdoc}
*
* @return void
*/
#[\ReturnTypeWillChange]
public function offsetUnset($key)
{
$this->remove($key);
}
/**
* @param string $path
*
* @return string[]
*
* @psalm-return non-empty-list<string>
*
* @psalm-pure
*/
protected static function keyToPathArray(string $path): array
{
if (\strlen($path) === 0) {
throw new InvalidPathException('Path cannot be an empty string');
}
$path = \str_replace(self::DELIMITERS, '.', $path);
return \explode('.', $path);
}
/**
* @param string|string[] $path
*
* @return string
*
* @psalm-pure
*/
protected static function formatPath($path): string
{
if (is_string($path)) {
$path = self::keyToPathArray($path);
}
return implode(' » ', $path);
}
}

View File

@ -0,0 +1,131 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData;
use Dflydev\DotAccessData\Exception\DataException;
use Dflydev\DotAccessData\Exception\InvalidPathException;
interface DataInterface
{
public const PRESERVE = 0;
public const REPLACE = 1;
public const MERGE = 2;
/**
* Append a value to a key (assumes key refers to an array value)
*
* If the key does not yet exist it will be created.
* If the key references a non-array it's existing contents will be added into a new array before appending the new value.
*
* @param string $key
* @param mixed $value
*
* @throws InvalidPathException if the given key is empty
*/
public function append(string $key, $value = null): void;
/**
* Set a value for a key
*
* If the key does not yet exist it will be created.
*
* @param string $key
* @param mixed $value
*
* @throws InvalidPathException if the given key is empty
* @throws DataException if the given key does not target an array
*/
public function set(string $key, $value = null): void;
/**
* Remove a key
*
* No exception will be thrown if the key does not exist
*
* @param string $key
*
* @throws InvalidPathException if the given key is empty
*/
public function remove(string $key): void;
/**
* Get the raw value for a key
*
* If the key does not exist, an optional default value can be returned instead.
* If no default is provided then an exception will be thrown instead.
*
* @param string $key
* @param mixed $default
*
* @return mixed
*
* @throws InvalidPathException if the given key is empty
* @throws InvalidPathException if the given key does not exist and no default value was given
*
* @psalm-mutation-free
*/
public function get(string $key, $default = null);
/**
* Check if the key exists
*
* @param string $key
*
* @return bool
*
* @throws InvalidPathException if the given key is empty
*
* @psalm-mutation-free
*/
public function has(string $key): bool;
/**
* Get a data instance for a key
*
* @param string $key
*
* @return DataInterface
*
* @throws InvalidPathException if the given key is empty
* @throws DataException if the given key does not reference an array
*
* @psalm-mutation-free
*/
public function getData(string $key): DataInterface;
/**
* Import data into existing data
*
* @param array<string, mixed> $data
* @param self::PRESERVE|self::REPLACE|self::MERGE $mode
*/
public function import(array $data, int $mode = self::REPLACE): void;
/**
* Import data from an external data into existing data
*
* @param DataInterface $data
* @param self::PRESERVE|self::REPLACE|self::MERGE $mode
*/
public function importData(DataInterface $data, int $mode = self::REPLACE): void;
/**
* Export data as raw data
*
* @return array<string, mixed>
*
* @psalm-mutation-free
*/
public function export(): array;
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData\Exception;
/**
* Base runtime exception type thrown by this library
*/
class DataException extends \RuntimeException
{
}

View File

@ -0,0 +1,21 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData\Exception;
/**
* Thrown when trying to access an invalid path in the data array
*/
class InvalidPathException extends DataException
{
}

View File

@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData\Exception;
use Throwable;
/**
* Thrown when trying to access a path that does not exist
*/
class MissingPathException extends DataException
{
/** @var string */
protected $path;
public function __construct(string $path, string $message = '', int $code = 0, ?Throwable $previous = null)
{
$this->path = $path;
parent::__construct($message, $code, $previous);
}
public function getPath(): string
{
return $this->path;
}
}

View File

@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/*
* This file is a part of dflydev/dot-access-data.
*
* (c) Dragonfly Development Inc.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace Dflydev\DotAccessData;
class Util
{
/**
* Test if array is an associative array
*
* Note that this function will return true if an array is empty. Meaning
* empty arrays will be treated as if they are associative arrays.
*
* @param array<mixed> $arr
*
* @return bool
*
* @psalm-pure
*/
public static function isAssoc(array $arr): bool
{
return !count($arr) || count(array_filter(array_keys($arr), 'is_string')) == count($arr);
}
/**
* Merge contents from one associtative array to another
*
* @param mixed $to
* @param mixed $from
* @param DataInterface::PRESERVE|DataInterface::REPLACE|DataInterface::MERGE $mode
*
* @return mixed
*
* @psalm-pure
*/
public static function mergeAssocArray($to, $from, int $mode = DataInterface::REPLACE)
{
if ($mode === DataInterface::MERGE && self::isList($to) && self::isList($from)) {
return array_merge($to, $from);
}
if (is_array($from) && is_array($to)) {
foreach ($from as $k => $v) {
if (!isset($to[$k])) {
$to[$k] = $v;
} else {
$to[$k] = self::mergeAssocArray($to[$k], $v, $mode);
}
}
return $to;
}
return $mode === DataInterface::PRESERVE ? $to : $from;
}
/**
* @param mixed $value
*
* @return bool
*
* @psalm-pure
*/
private static function isList($value): bool
{
return is_array($value) && array_values($value) === $value;
}
}

View File

@ -0,0 +1,106 @@
<?php
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPSTORM_META
{
expectedArguments(\League\CommonMark\Util\HtmlElement::__construct(), 0, 'a', 'abbr', 'address', 'area', 'article', 'aside', 'audio', 'b', 'base', 'bdi', 'bdo', 'blockquote', 'body', 'br', 'button', 'canvas', 'caption', 'cite', 'code', 'col', 'colgroup', 'data', 'datalist', 'dd', 'del', 'details', 'dfn', 'dialog', 'div', 'dl', 'dt', 'em', 'embed', 'fieldset', 'figure', 'footer', 'form', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'head', 'header', 'hgroup', 'hr', 'html', 'i', 'iframe', 'img', 'input', 'ins', 'kdb', 'keygen', 'label', 'legend', 'li', 'link', 'main', 'map', 'mark', 'menu', 'menuitem', 'meta', 'meter', 'nav', 'noscript', 'object', 'ol', 'optgroup', 'option', 'output', 'p', 'param', 'pre', 'progress', 'q', 's', 'samp', 'script', 'section', 'select', 'small', 'source', 'span', 'strong', 'style', 'sub', 'summary', 'sup', 'table', 'tbody', 'td', 'template', 'textarea', 'tfoot', 'th', 'thead', 'time', 'tr', 'track', 'u', 'ul', 'var', 'video', 'wbr');
expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\Heading::__construct(), 0, 1, 2, 3, 4, 5, 6);
expectedReturnValues(\League\CommonMark\Extension\CommonMark\Node\Block\Heading::getLevel(), 1, 2, 3, 4, 5, 6);
registerArgumentsSet('league_commonmark_htmlblock_types', \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_1_CODE_CONTAINER, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_2_COMMENT, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_3, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_4, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_5_CDATA, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_6_BLOCK_ELEMENT, \League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::TYPE_7_MISC_ELEMENT);
expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::__construct(), 0, argumentsSet('league_commonmark_htmlblock_types'));
expectedArguments(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::setType(), 0, argumentsSet('league_commonmark_htmlblock_types'));
expectedReturnValues(\League\CommonMark\Extension\CommonMark\Node\Block\HtmlBlock::getType(), argumentsSet('league_commonmark_htmlblock_types'));
expectedArguments(\League\CommonMark\Util\RegexHelper::getHtmlBlockOpenRegex(), 0, argumentsSet('league_commonmark_htmlblock_types'));
expectedArguments(\League\CommonMark\Util\RegexHelper::getHtmlBlockCloseRegex(), 0, argumentsSet('league_commonmark_htmlblock_types'));
registerArgumentsSet('league_commonmark_newline_types', \League\CommonMark\Node\Inline\Newline::HARDBREAK, \League\CommonMark\Node\Inline\Newline::SOFTBREAK);
expectedArguments(\League\CommonMark\Node\Inline\Newline::__construct(), 0, argumentsSet('league_commonmark_newline_types'));
expectedReturnValues(\League\CommonMark\Node\Inline\Newline::getType(), argumentsSet('league_commonmark_newline_types'));
registerArgumentsSet('league_commonmark_options',
'html_input',
'allow_unsafe_links',
'max_nesting_level',
'max_delimiters_per_line',
'renderer',
'renderer/block_separator',
'renderer/inner_separator',
'renderer/soft_break',
'commonmark',
'commonmark/enable_em',
'commonmark/enable_strong',
'commonmark/use_asterisk',
'commonmark/use_underscore',
'commonmark/unordered_list_markers',
'disallowed_raw_html',
'disallowed_raw_html/disallowed_tags',
'external_link',
'external_link/html_class',
'external_link/internal_hosts',
'external_link/nofollow',
'external_link/noopener',
'external_link/noreferrer',
'external_link/open_in_new_window',
'footnote',
'footnote/backref_class',
'footnote/backref_symbol',
'footnote/container_add_hr',
'footnote/container_class',
'footnote/ref_class',
'footnote/ref_id_prefix',
'footnote/footnote_class',
'footnote/footnote_id_prefix',
'heading_permalink',
'heading_permalink/apply_id_to_heading',
'heading_permalink/heading_class',
'heading_permalink/html_class',
'heading_permalink/fragment_prefix',
'heading_permalink/id_prefix',
'heading_permalink/inner_contents',
'heading_permalink/insert',
'heading_permalink/max_heading_level',
'heading_permalink/min_heading_level',
'heading_permalink/symbol',
'heading_permalink/title',
'mentions',
'smartpunct/double_quote_closer',
'smartpunct/double_quote_opener',
'smartpunct/single_quote_closer',
'smartpunct/single_quote_opener',
'slug_normalizer',
'slug_normalizer/instance',
'slug_normalizer/max_length',
'slug_normalizer/unique',
'table',
'table/wrap',
'table/wrap/attributes',
'table/wrap/enabled',
'table/wrap/tag',
'table/alignment_attributes',
'table/alignment_attributes/left',
'table/alignment_attributes/center',
'table/alignment_attributes/right',
'table/max_autocompleted_cells',
'table_of_contents',
'table_of_contents/html_class',
'table_of_contents/max_heading_level',
'table_of_contents/min_heading_level',
'table_of_contents/normalize',
'table_of_contents/placeholder',
'table_of_contents/position',
'table_of_contents/style',
);
expectedArguments(\League\Config\ConfigurationInterface::get(), 0, argumentsSet('league_commonmark_options'));
expectedArguments(\League\Config\ConfigurationInterface::exists(), 0, argumentsSet('league_commonmark_options'));
expectedArguments(\League\Config\MutableConfigurationInterface::set(), 0, argumentsSet('league_commonmark_options'));
}

756
vendor/league/commonmark/CHANGELOG.md vendored Normal file
View File

@ -0,0 +1,756 @@
# Change Log
All notable changes to this project will be documented in this file.
Updates should follow the [Keep a CHANGELOG](https://keepachangelog.com/) principles.
**Upgrading from 1.x?** See <https://commonmark.thephpleague.com/2.0/upgrading/> for additional information.
## [Unreleased][unreleased]
## [2.7.1] - 2025-07-20
### Changed
- Optimized several regular expressions in `RegexHelper` to improve performance (#674, #1086)
### Fixed
- `EmbedProcessor` no longer calls `updateEmbeds()` when there are no embeds to update (#1081)
- Fixed missing `benchmark.php` CSV path validation for non-existent files (#1068, #1085)
## [2.7.0] - 2025-05-05
This is a **security release** to address a potential cross-site scripting (XSS) vulnerability when using the `AttributesExtension` with untrusted user input.
### Added
- Added `attributes/allow` config option to specify which attributes users are allowed to set on elements (default allows virtually all attributes)
### Changed
- The `AttributesExtension` blocks all attributes starting with `on` unless explicitly allowed via the `attributes/allow` config option
- The `allow_unsafe_links` option is now respected by the `AttributesExtension` when users specify `href` and `src` attributes
## [2.6.2] - 2025-04-18
### Fixed
- Fixed Attributes extension parsing regression (#1071)
## [2.6.1] - 2024-12-29
### Fixed
- Rendered list items should only add newlines around block-level children (#1059, #1061)
## [2.6.0] - 2024-12-07
This is a **security release** to address potential denial of service attacks when parsing specially crafted,
malicious input from untrusted sources (like user input).
### Added
- Added `max_delimiters_per_line` config option to prevent denial of service attacks when parsing malicious input
- Added `table/max_autocompleted_cells` config option to prevent denial of service attacks when parsing large tables
- The `AttributesExtension` now supports attributes without values (#985, #986)
- The `AutolinkExtension` exposes two new configuration options to override the default behavior (#969, #987):
- `autolink/allowed_protocols` - an array of protocols to allow autolinking for
- `autolink/default_protocol` - the default protocol to use when none is specified
- Added `RegexHelper::isWhitespace()` method to check if a given character is an ASCII whitespace character
- Added `CacheableDelimiterProcessorInterface` to ensure linear complexity for dynamic delimiter processing
- Added `Bracket` delimiter type to optimize bracket parsing
### Changed
- `[` and `]` are no longer added as `Delimiter` objects on the stack; a new `Bracket` type with its own stack is used instead
- `UrlAutolinkParser` no longer parses URLs with more than 127 subdomains
- Expanded reference links can no longer exceed 100kb, or the size of the input document (whichever is greater)
- Delimiters should always provide a non-null value via `DelimiterInterface::getIndex()`
- We'll attempt to infer the index based on surrounding delimiters where possible
- The `DelimiterStack` now accepts integer positions for any `$stackBottom` argument
- Several small performance optimizations
## [2.5.3] - 2024-08-16
### Changed
- Made compatible with CommonMark spec 0.31.1, including:
- Remove `source`, add `search` to list of recognized block tags
## [2.5.2] - 2024-08-14
### Changed
- Boolean attributes now require an explicit `true` value (#1040)
### Fixed
- Fixed regression where text could be misinterpreted as an attribute (#1040)
## [2.5.1] - 2024-07-24
### Fixed
- Fixed attribute parsing incorrectly parsing mustache-like syntax (#1035)
- Fixed incorrect `Table` start line numbers (#1037)
## [2.5.0] - 2024-07-22
### Added
- The `AttributesExtension` now supports attributes without values (#985, #986)
- The `AutolinkExtension` exposes two new configuration options to override the default behavior (#969, #987):
- `autolink/allowed_protocols` - an array of protocols to allow autolinking for
- `autolink/default_protocol` - the default protocol to use when none is specified
### Changed
- Made compatible with CommonMark spec 0.31.0, including:
- Allow closing fence to be followed by tabs
- Remove restrictive limitation on inline comments
- Unicode symbols now treated like punctuation (for purposes of flankingness)
- Trailing tabs on the last line of indented code blocks will be excluded
- Improved HTML comment matching
- `Paragraph`s only containing link reference definitions will be kept in the AST until the `Document` is finalized
- (These were previously removed immediately after parsing the `Paragraph`)
### Fixed
- Fixed list tightness not being determined properly in some edge cases
- Fixed incorrect ending line numbers for several block types in various scenarios
- Fixed lowercase inline HTML declarations not being accepted
## [2.4.4] - 2024-07-22
### Fixed
- Fixed SmartPunct extension changing already-formatted quotation marks (#1030)
## [2.4.3] - 2024-07-22
### Fixed
- Fixed the Attributes extension not supporting CSS level 3 selectors (#1013)
- Fixed `UrlAutolinkParser` incorrectly parsing text containing `www` anywhere before an autolink (#1025)
## [2.4.2] - 2024-02-02
### Fixed
- Fixed declaration parser being too strict
- `FencedCodeRenderer`: don't add `language-` to class if already prefixed
### Deprecated
- Returning dynamic values from `DelimiterProcessorInterface::getDelimiterUse()` is deprecated
- You should instead implement `CacheableDelimiterProcessorInterface` to help the engine perform caching to avoid performance issues.
- Failing to set a delimiter's index (or returning `null` from `DelimiterInterface::getIndex()`) is deprecated and will not be supported in 3.0
- Deprecated `DelimiterInterface::isActive()` and `DelimiterInterface::setActive()`, as these are no longer used by the engine
- Deprecated `DelimiterStack::removeEarlierMatches()` and `DelimiterStack::searchByCharacter()`, as these are no longer used by the engine
- Passing a `DelimiterInterface` as the `$stackBottom` argument to `DelimiterStack::processDelimiters()` or `::removeAll()` is deprecated and will not be supported in 3.0; pass the integer position instead.
### Fixed
- Fixed NUL characters not being replaced in the input
- Fixed quadratic complexity parsing unclosed inline links
- Fixed quadratic complexity parsing emphasis and strikethrough delimiters
- Fixed issue where having 500,000+ delimiters could trigger a [known segmentation fault issue in PHP's garbage collection](https://bugs.php.net/bug.php?id=68606)
- Fixed quadratic complexity deactivating link openers
- Fixed quadratic complexity parsing long backtick code spans with no matching closers
- Fixed catastrophic backtracking when parsing link labels/titles
## [2.4.1] - 2023-08-30
### Fixed
- Fixed `ExternalLinkProcessor` not fully disabling the `rel` attribute when configured to do so (#992)
## [2.4.0] - 2023-03-24
### Added
- Added generic `CommonMarkException` marker interface for all exceptions thrown by the library
- Added several new specific exception types implementing that marker interface:
- `AlreadyInitializedException`
- `InvalidArgumentException`
- `IOException`
- `LogicException`
- `MissingDependencyException`
- `NoMatchingRendererException`
- `ParserLogicException`
- Added more configuration options to the Heading Permalinks extension (#939):
- `heading_permalink/apply_id_to_heading` - When `true`, the `id` attribute will be applied to the heading element itself instead of the `<a>` tag
- `heading_permalink/heading_class` - class to apply to the heading element
- `heading_permalink/insert` - now accepts `none` to prevent the creation of the `<a>` link
- Added new `table/alignment_attributes` configuration option to control how table cell alignment is rendered (#959)
### Changed
- Change several thrown exceptions from `RuntimeException` to `LogicException` (or something extending it), including:
- `CallbackGenerator`s that fail to set a URL or return an expected value
- `MarkdownParser` when deactivating the last block parser or attempting to get an active block parser when they've all been closed
- Adding items to an already-initialized `Environment`
- Rendering a `Node` when no renderer has been registered for it
- `HeadingPermalinkProcessor` now throws `InvalidConfigurationException` instead of `RuntimeException` when invalid config values are given.
- `HtmlElement::setAttribute()` no longer requires the second parameter for boolean attributes
- Several small micro-optimizations
- Changed Strikethrough to only allow 1 or 2 tildes per the updated GFM spec
### Fixed
- Fixed inaccurate `@throws` docblocks throughout the codebase, including `ConverterInterface`, `MarkdownConverter`, and `MarkdownConverterInterface`.
- These previously suggested that only `\RuntimeException`s were thrown, which was inaccurate as `\LogicException`s were also possible.
## [2.3.9] - 2023-02-15
### Fixed
- Fixed autolink extension not detecting some URIs with underscores (#956)
## [2.3.8] - 2022-12-10
### Fixed
- Fixed parsing issues when `mb_internal_encoding()` is set to something other than `UTF-8` (#951)
## [2.3.7] - 2022-11-03
### Fixed
- Fixed `TaskListItemMarkerRenderer` not including HTML attributes set on the node by other extensions (#947)
## [2.3.6] - 2022-10-30
### Fixed
- Fixed unquoted attribute parsing when closing curly brace is followed by certain characters (like a `.`) (#943)
## [2.3.5] - 2022-07-29
### Fixed
- Fixed error using `InlineParserEngine` when no inline parsers are registered in the `Environment` (#908)
## [2.3.4] - 2022-07-17
### Changed
- Made a number of small tweaks to the embed extension's parsing behavior to fix #898:
- Changed `EmbedStartParser` to always capture embed-like lines in container blocks, regardless of parent block type
- Changed `EmbedProcessor` to also remove `Embed` blocks that aren't direct children of the `Document`
- Increased the priority of `EmbedProcessor` to `1010`
### Fixed
- Fixed `EmbedExtension` not parsing embeds following a list block (#898)
## [2.3.3] - 2022-06-07
### Fixed
- Fixed `DomainFilteringAdapter` not reindexing the embed list (#884, #885)
## [2.3.2] - 2022-06-03
### Fixed
- Fixed FootnoteExtension stripping extra characters from tab-indented footnotes (#881)
## [2.2.5] - 2022-06-03
### Fixed
- Fixed FootnoteExtension stripping extra characters from tab-indented footnotes (#881)
## [2.3.1] - 2022-05-14
### Fixed
- Fixed AutolinkExtension not ignoring trailing strikethrough syntax (#867)
## [2.2.4] - 2022-05-14
### Fixed
- Fixed AutolinkExtension not ignoring trailing strikethrough syntax (#867)
## [2.3.0] - 2022-04-07
### Added
- Added new `EmbedExtension` (#805)
- Added `DocumentRendererInterface` as a replacement for the now-deprecated `MarkdownRendererInterface`
### Deprecated
- Deprecated `MarkdownRendererInterface`; use `DocumentRendererInterface` instead
## [2.2.3] - 2022-02-26
### Fixed
- Fixed front matter parsing with Windows line endings (#821)
## [2.1.3] - 2022-02-26
### Fixed
- Fixed front matter parsing with Windows line endings (#821)
## [2.0.4] - 2022-02-26
### Fixed
- Fixed front matter parsing with Windows line endings (#821)
## [2.2.2] - 2022-02-13
### Fixed
- Fixed double-escaping of image alt text (#806, #810)
- Fixed Psalm typehints for event class names
## [2.2.1] - 2022-01-25
### Fixed
- Fixed `symfony/deprecation-contracts` constraint
### Removed
- Removed deprecation trigger from `MarkdownConverterInterface` to reduce noise
## [2.2.0] - 2022-01-22
### Added
- Added new `ConverterInterface`
- Added new `MarkdownToXmlConverter` class
- Added new `HtmlDecorator` class which can wrap existing renderers with additional HTML tags
- Added new `table/wrap` config to apply an optional wrapping/container element around a table (#780)
### Changed
- `HtmlElement` contents can now consist of any `Stringable`, not just `HtmlElement` and `string`
### Deprecated
- Deprecated `MarkdownConverterInterface` and its `convertToHtml()` method; use `ConverterInterface` and `convert()` instead
## [2.1.2] - 2022-02-13
### Fixed
- Fixed double-escaping of image alt text (#806, #810)
- Fixed Psalm typehints for event class names
## [2.1.1] - 2022-01-02
### Added
- Added missing return type to `Environment::dispatch()` to fix deprecation warning (#778)
## [2.1.0] - 2021-12-05
### Added
- Added support for ext-yaml in FrontMatterExtension (#715)
- Added support for symfony/yaml v6.0 in FrontMatterExtension (#739)
- Added new `heading_permalink/aria_hidden` config option (#741)
### Fixed
- Fixed PHP 8.1 deprecation warning (#759, #762)
## [2.0.3] - 2022-02-13
### Fixed
- Fixed double-escaping of image alt text (#806, #810)
- Fixed Psalm typehints for event class names
## [2.0.2] - 2021-08-14
### Changed
- Bumped minimum version of league/config to support PHP 8.1
### Fixed
- Fixed ability to register block parsers that identify lines starting with letters (#706)
## [2.0.1] - 2021-07-31
### Fixed
- Fixed nested autolinks (#689)
- Fixed description lists being parsed incorrectly (#692)
- Fixed Table of Contents not respecting Heading Permalink prefixes (#690)
## [2.0.0] - 2021-07-24
No changes were introduced since the previous RC2 release.
See all entries below for a list of changes between 1.x and 2.0.
## [2.0.0-rc2] - 2021-07-17
### Fixed
- Fixed Mentions inside of links creating nested links against the spec's rules (#688)
## [2.0.0-rc1] - 2021-07-10
No changes were introduced since the previous release.
## [2.0.0-beta3] - 2021-07-03
### Changed
- Any leading UTF-8 BOM will be stripped from the input
- The `getEnvironment()` method of `CommonMarkConverter` and `GithubFlavoredMarkdownConverter` will always return the concrete, configurable `Environment` for upgrading convenience
- Optimized AST iteration
- Lots of small micro-optimizations
## [2.0.0-beta2] - 2021-06-27
### Added
- Added new `Node::iterator()` method and `NodeIterator` class for faster AST iteration (#683, #684)
### Changed
- Made compatible with CommonMark spec 0.30.0
- Optimized link label parsing
- Optimized AST iteration for a 50% performance boost in some event listeners (#683, #684)
### Fixed
- Fixed processing instructions with EOLs
- Fixed case-insensitive matching for HTML tag types
- Fixed type 7 HTML blocks incorrectly interrupting lazy paragraphs
- Fixed newlines in reference labels not collapsing into spaces
- Fixed link label normalization with escaped newlines
- Fixed unnecessary AST iteration when no default attributes are configured
## [2.0.0-beta1] - 2021-06-20
### Added
- **Added three new extensions:**
- `FrontMatterExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/front-matter/))
- `DescriptionListExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/description-lists/))
- `DefaultAttributesExtension` ([see documentation](https://commonmark.thephpleague.com/extensions/default-attributes/))
- **Added new `XmlRenderer` to simplify AST debugging** ([see documentation](https://commonmark.thephpleague.com/xml/)) (#431)
- **Added the ability to configure disallowed raw HTML tags** (#507)
- **Added the ability for Mentions to use multiple characters for their symbol** (#514, #550)
- **Added the ability to delegate event dispatching to PSR-14 compliant event dispatcher libraries**
- **Added new configuration options:**
- Added `heading_permalink/min_heading_level` and `heading_permalink/max_heading_level` options to control which headings get permalinks (#519)
- Added `heading_permalink/fragment_prefix` to allow customizing the URL fragment prefix (#602)
- Added `footnote/backref_symbol` option for customizing backreference link appearance (#522)
- Added `slug_normalizer/max_length` option to control the maximum length of generated URL slugs
- Added `slug_normalizer/unique` option to control whether unique slugs should be generated per-document or per-environment
- **Added purity markers throughout the codebase** (verified with Psalm)
- Added `Query` class to simplify Node traversal when looking to take action on certain Nodes
- Added new `HtmlFilter` and `StringContainerHelper` utility classes
- Added new `AbstractBlockContinueParser` class to simplify the creation of custom block parsers
- Added several new classes and interfaces:
- `BlockContinue`
- `BlockContinueParserInterface`
- `BlockContinueParserWithInlinesInterface`
- `BlockStart`
- `BlockStartParserInterface`
- `ChildNodeRendererInterface`
- `ConfigurableExtensionInterface`
- `CursorState`
- `DashParser` (extracted from `PunctuationParser`)
- `DelimiterParser`
- `DocumentBlockParser`
- `DocumentPreRenderEvent`
- `DocumentRenderedEvent`
- `EllipsesParser` (extracted from `PunctuationParser`)
- `ExpressionInterface`
- `FallbackNodeXmlRenderer`
- `InlineParserEngineInterface`
- `InlineParserMatch`
- `MarkdownParserState`
- `MarkdownParserStateInterface`
- `MarkdownRendererInterface`
- `Query`
- `RawMarkupContainerInterface`
- `ReferenceableInterface`
- `RenderedContent`
- `RenderedContentInterface`
- `ReplaceUnpairedQuotesListener`
- `SpecReader`
- `TableOfContentsRenderer`
- `UniqueSlugNormalizer`
- `UniqueSlugNormalizerInterface`
- `XmlRenderer`
- `XmlNodeRendererInterface`
- Added several new methods:
- `Cursor::getCurrentCharacter()`
- `Environment::createDefaultConfiguration()`
- `Environment::setEventDispatcher()`
- `EnvironmentInterface::getExtensions()`
- `EnvironmentInterface::getInlineParsers()`
- `EnvironmentInterface::getSlugNormalizer()`
- `FencedCode::setInfo()`
- `Heading::setLevel()`
- `HtmlRenderer::renderDocument()`
- `InlineParserContext::getFullMatch()`
- `InlineParserContext::getFullMatchLength()`
- `InlineParserContext::getMatches()`
- `InlineParserContext::getSubMatches()`
- `LinkParserHelper::parsePartialLinkLabel()`
- `LinkParserHelper::parsePartialLinkTitle()`
- `Node::assertInstanceOf()`
- `RegexHelper::isLetter()`
- `StringContainerInterface::setLiteral()`
- `TableCell::getType()`
- `TableCell::setType()`
- `TableCell::getAlign()`
- `TableCell::setAlign()`
### Changed
- **Changed the converter return type**
- `CommonMarkConverter::convertToHtml()` now returns an instance of `RenderedContentInterface`. This can be cast to a string for backward compatibility with 1.x.
- **Table of Contents items are no longer wrapped with `<p>` tags** (#613)
- **Heading Permalinks now link to element IDs instead of using `name` attributes** (#602)
- **Heading Permalink IDs and URL fragments now have a `content` prefix by default** (#602)
- **Changes to configuration options:**
- `enable_em` has been renamed to `commonmark/enable_em`
- `enable_strong` has been renamed to `commonmark/enable_strong`
- `use_asterisk` has been renamed to `commonmark/use_asterisk`
- `use_underscore` has been renamed to `commonmark/use_underscore`
- `unordered_list_markers` has been renamed to `commonmark/unordered_list_markers`
- `mentions/*/symbol` has been renamed to `mentions/*/prefix`
- `mentions/*/regex` has been renamed to `mentions/*/pattern` and requires partial regular expressions (without delimiters or flags)
- `max_nesting_level` now defaults to `PHP_INT_MAX` and no longer supports floats
- `heading_permalink/slug_normalizer` has been renamed to `slug_normalizer/instance`
- **Event dispatching is now fully PSR-14 compliant**
- **Moved and renamed several classes** - [see the full list here](https://commonmark.thephpleague.com/2.0/upgrading/#classesnamespaces-renamed)
- The `HeadingPermalinkExtension` and `FootnoteExtension` were modified to ensure they never produce a slug which conflicts with slugs created by the other extension
- `SlugNormalizer::normalizer()` now supports optional prefixes and max length options passed in via the `$context` argument
- The `AbstractBlock::$data` and `AbstractInline::$data` arrays were replaced with a `Data` array-like object on the base `Node` class
- **Implemented a new approach to block parsing.** This was a massive change, so here are the highlights:
- Functionality previously found in block parsers and node elements has moved to block parser factories and block parsers, respectively ([more details](https://commonmark.thephpleague.com/2.0/upgrading/#new-block-parsing-approach))
- `ConfigurableEnvironmentInterface::addBlockParser()` is now `EnvironmentBuilderInterface::addBlockParserFactory()`
- `ReferenceParser` was re-implemented and works completely different than before
- The paragraph parser no longer needs to be added manually to the environment
- **Implemented a new approach to inline parsing** where parsers can now specify longer strings or regular expressions they want to parse (instead of just single characters):
- `InlineParserInterface::getCharacters()` is now `getMatchDefinition()` and returns an instance of `InlineParserMatch`
- `InlineParserContext::__construct()` now requires the contents to be provided as a `Cursor` instead of a `string`
- **Implemented delimiter parsing as a special type of inline parser** (via the new `DelimiterParser` class)
- **Changed block and inline rendering to use common methods and interfaces**
- `BlockRendererInterface` and `InlineRendererInterface` were replaced by `NodeRendererInterface` with slightly different parameters. All core renderers now implement this interface.
- `ConfigurableEnvironmentInterface::addBlockRenderer()` and `addInlineRenderer()` were combined into `EnvironmentBuilderInterface::addRenderer()`
- `EnvironmentInterface::getBlockRenderersForClass()` and `getInlineRenderersForClass()` are now just `getRenderersForClass()`
- **Completely refactored the Configuration implementation**
- All configuration-specific classes have been moved into a new `league/config` package with a new namespace
- `Configuration` objects must now be configured with a schema and all options must match that schema - arbitrary keys are no longer permitted
- `Configuration::__construct()` no longer accepts the default configuration values - use `Configuration::merge()` instead
- `ConfigurationInterface` now only contains a `get(string $key)`; this method no longer allows arbitrary default values to be returned if the option is missing
- `ConfigurableEnvironmentInterface` was renamed to `EnvironmentBuilderInterface`
- `ExtensionInterface::register()` now requires an `EnvironmentBuilderInterface` param instead of `ConfigurableEnvironmentInterface`
- **Added missing return types to virtually every class and interface method**
- Re-implemented the GFM Autolink extension using the new inline parser approach instead of document processors
- `EmailAutolinkProcessor` is now `EmailAutolinkParser`
- `UrlAutolinkProcessor` is now `UrlAutolinkParser`
- `HtmlElement` can now properly handle array (i.e. `class`) and boolean (i.e. `checked`) attribute values
- `HtmlElement` automatically flattens any attributes with array values into space-separated strings, removing duplicate entries
- Combined separate classes/interfaces into one:
- `DisallowedRawHtmlRenderer` replaces `DisallowedRawHtmlBlockRenderer` and `DisallowedRawHtmlInlineRenderer`
- `NodeRendererInterface` replaces `BlockRendererInterface` and `InlineRendererInterface`
- Renamed the following methods:
- `Environment` and `ConfigurableEnvironmentInterface`:
- `addBlockParser()` is now `addBlockStartParser()`
- `ReferenceMap` and `ReferenceMapInterface`:
- `addReference()` is now `add()`
- `getReference()` is now `get()`
- `listReferences()` is now `getIterator()`
- Various node (block/inline) classes:
- `getContent()` is now `getLiteral()`
- `setContent()` is now `setLiteral()`
- Moved and renamed the following constants:
- `EnvironmentInterface::HTML_INPUT_ALLOW` is now `HtmlFilter::ALLOW`
- `EnvironmentInterface::HTML_INPUT_ESCAPE` is now `HtmlFilter::ESCAPE`
- `EnvironmentInterface::HTML_INPUT_STRIP` is now `HtmlFilter::STRIP`
- `TableCell::TYPE_HEAD` is now `TableCell::TYPE_HEADER`
- `TableCell::TYPE_BODY` is now `TableCell::TYPE_DATA`
- Changed the visibility of the following properties:
- `AttributesInline::$attributes` is now `private`
- `AttributesInline::$block` is now `private`
- `TableCell::$align` is now `private`
- `TableCell::$type` is now `private`
- `TableSection::$type` is now `private`
- Several methods which previously returned `$this` now return `void`
- `Delimiter::setPrevious()`
- `Node::replaceChildren()`
- `Context::setTip()`
- `Context::setContainer()`
- `Context::setBlocksParsed()`
- `AbstractStringContainer::setContent()`
- `AbstractWebResource::setUrl()`
- Several classes are now marked `final`:
- `ArrayCollection`
- `Emphasis`
- `FencedCode`
- `Heading`
- `HtmlBlock`
- `HtmlElement`
- `HtmlInline`
- `IndentedCode`
- `Newline`
- `Strikethrough`
- `Strong`
- `Text`
- `Heading` nodes no longer directly contain a copy of their inner text
- `StringContainerInterface` can now be used for inlines, not just blocks
- `ArrayCollection` only supports integer keys
- `HtmlElement` now implements `Stringable`
- `Cursor::saveState()` and `Cursor::restoreState()` now use `CursorState` objects instead of arrays
- `NodeWalker::next()` now enters, traverses any children, and leaves all elements which may have children (basically all blocks plus any inlines with children). Previously, it only did this for elements explicitly marked as "containers".
- `InvalidOptionException` was removed
- Anything with a `getReference(): ReferenceInterface` method now implements `ReferencableInterface`
- The `SmartPunct` extension now replaces all unpaired `Quote` elements with `Text` elements towards the end of parsing, making the `QuoteRenderer` unnecessary
- Several changes made to the Footnote extension:
- Footnote identifiers can no longer contain spaces
- Anonymous footnotes can now span subsequent lines
- Footnotes can now contain multiple lines of content, including sub-blocks, by indenting them
- Footnote event listeners now have numbered priorities (but still execute in the same order)
- Footnotes must now be separated from previous content by a blank line
- The line numbers (keys) returned via `MarkdownInput::getLines()` now start at 1 instead of 0
- `DelimiterProcessorCollectionInterface` now extends `Countable`
- `RegexHelper::PARTIAL_` constants must always be used in case-insensitive contexts
- `HeadingPermalinkProcessor` no longer accepts text normalizers via the constructor - these must be provided via configuration instead
- Blocks which can't contain inlines will no longer be asked to render inlines
- `AnonymousFootnoteRefParser` and `HeadingPermalinkProcessor` now implement `EnvironmentAwareInterface` instead of `ConfigurationAwareInterface`
- The second argument to `TextNormalizerInterface::normalize()` must now be an array
- The `title` attribute for `Link` and `Image` nodes is now stored using a dedicated property instead of stashing it in `$data`
- `ListData::$delimiter` now returns either `ListBlock::DELIM_PERIOD` or `ListBlock::DELIM_PAREN` instead of the literal delimiter
### Fixed
- **Fixed parsing of footnotes without content**
- **Fixed rendering of orphaned footnotes and footnote refs**
- **Fixed some URL autolinks breaking too early** (#492)
- Fixed `AbstractStringContainer` not actually being `abstract`
### Removed
- **Removed support for PHP 7.1, 7.2, and 7.3** (#625, #671)
- **Removed all previously-deprecated functionality:**
- Removed the ability to pass custom `Environment` instances into the `CommonMarkConverter` and `GithubFlavoredMarkdownConverter` constructors
- Removed the `Converter` class and `ConverterInterface`
- Removed the `bin/commonmark` script
- Removed the `Html5Entities` utility class
- Removed the `InlineMentionParser` (use `MentionParser` instead)
- Removed `DefaultSlugGenerator` and `SlugGeneratorInterface` from the `Extension/HeadingPermalink/Slug` sub-namespace (use the new ones under `./SlugGenerator` instead)
- Removed the following `ArrayCollection` methods:
- `add()`
- `set()`
- `get()`
- `remove()`
- `isEmpty()`
- `contains()`
- `indexOf()`
- `containsKey()`
- `replaceWith()`
- `removeGaps()`
- Removed the `ConfigurableEnvironmentInterface::setConfig()` method
- Removed the `ListBlock::TYPE_UNORDERED` constant
- Removed the `CommonMarkConverter::VERSION` constant
- Removed the `HeadingPermalinkRenderer::DEFAULT_INNER_CONTENTS` constant
- Removed the `heading_permalink/inner_contents` configuration option
- **Removed now-unused classes:**
- `AbstractStringContainerBlock`
- `BlockRendererInterface`
- `Context`
- `ContextInterface`
- `Converter`
- `ConverterInterface`
- `InlineRendererInterface`
- `PunctuationParser` (was split into two classes: `DashParser` and `EllipsesParser`)
- `QuoteRenderer`
- `UnmatchedBlockCloser`
- Removed the following methods, properties, and constants:
- `AbstractBlock::$open`
- `AbstractBlock::$lastLineBlank`
- `AbstractBlock::isContainer()`
- `AbstractBlock::canContain()`
- `AbstractBlock::isCode()`
- `AbstractBlock::matchesNextLine()`
- `AbstractBlock::endsWithBlankLine()`
- `AbstractBlock::setLastLineBlank()`
- `AbstractBlock::shouldLastLineBeBlank()`
- `AbstractBlock::isOpen()`
- `AbstractBlock::finalize()`
- `AbstractBlock::getData()`
- `AbstractInline::getData()`
- `ConfigurableEnvironmentInterface::addBlockParser()`
- `ConfigurableEnvironmentInterface::mergeConfig()`
- `Delimiter::setCanClose()`
- `EnvironmentInterface::getConfig()`
- `EnvironmentInterface::getInlineParsersForCharacter()`
- `EnvironmentInterface::getInlineParserCharacterRegex()`
- `HtmlRenderer::renderBlock()`
- `HtmlRenderer::renderBlocks()`
- `HtmlRenderer::renderInline()`
- `HtmlRenderer::renderInlines()`
- `Node::isContainer()`
- `RegexHelper::matchAll()` (use the new `matchFirst()` method instead)
- `RegexHelper::REGEX_WHITESPACE`
- Removed the second `$contents` argument from the `Heading` constructor
### Deprecated
**The following things have been deprecated and will not be supported in v3.0:**
- `Environment::mergeConfig()` (set configuration before instantiation instead)
- `Environment::createCommonMarkEnvironment()` and `Environment::createGFMEnvironment()`
- Alternative 1: Use `CommonMarkConverter` or `GithubFlavoredMarkdownConverter` if you don't need to customize the environment
- Alternative 2: Instantiate a new `Environment` and add the necessary extensions yourself
[unreleased]: https://github.com/thephpleague/commonmark/compare/2.7.1...HEAD
[2.7.1]: https://github.com/thephpleague/commonmark/compare/2.7.0...2.7.1
[2.7.0]: https://github.com/thephpleague/commonmark/compare/2.6.2...2.7.0
[2.6.2]: https://github.com/thephpleague/commonmark/compare/2.6.1...2.6.2
[2.6.1]: https://github.com/thephpleague/commonmark/compare/2.6.0...2.6.1
[2.6.0]: https://github.com/thephpleague/commonmark/compare/2.5.3...2.6.0
[2.5.3]: https://github.com/thephpleague/commonmark/compare/2.5.2...2.5.3
[2.5.2]: https://github.com/thephpleague/commonmark/compare/2.5.1...2.5.2
[2.5.1]: https://github.com/thephpleague/commonmark/compare/2.5.0...2.5.1
[2.5.0]: https://github.com/thephpleague/commonmark/compare/2.4.4...2.5.0
[2.4.4]: https://github.com/thephpleague/commonmark/compare/2.4.3...2.4.4
[2.4.3]: https://github.com/thephpleague/commonmark/compare/2.4.2...2.4.3
[2.4.2]: https://github.com/thephpleague/commonmark/compare/2.4.1...2.4.2
[2.4.1]: https://github.com/thephpleague/commonmark/compare/2.4.0...2.4.1
[2.4.0]: https://github.com/thephpleague/commonmark/compare/2.3.9...2.4.0
[2.3.9]: https://github.com/thephpleague/commonmark/compare/2.3.8...2.3.9
[2.3.8]: https://github.com/thephpleague/commonmark/compare/2.3.7...2.3.8
[2.3.7]: https://github.com/thephpleague/commonmark/compare/2.3.6...2.3.7
[2.3.6]: https://github.com/thephpleague/commonmark/compare/2.3.5...2.3.6
[2.3.5]: https://github.com/thephpleague/commonmark/compare/2.3.4...2.3.5
[2.3.4]: https://github.com/thephpleague/commonmark/compare/2.3.3...2.3.4
[2.3.3]: https://github.com/thephpleague/commonmark/compare/2.3.2...2.3.3
[2.3.2]: https://github.com/thephpleague/commonmark/compare/2.3.2...main
[2.3.1]: https://github.com/thephpleague/commonmark/compare/2.3.0...2.3.1
[2.3.0]: https://github.com/thephpleague/commonmark/compare/2.2.3...2.3.0
[2.2.5]: https://github.com/thephpleague/commonmark/compare/2.2.4...2.2.5
[2.2.4]: https://github.com/thephpleague/commonmark/compare/2.2.3...2.2.4
[2.2.3]: https://github.com/thephpleague/commonmark/compare/2.2.2...2.2.3
[2.2.2]: https://github.com/thephpleague/commonmark/compare/2.2.1...2.2.2
[2.2.1]: https://github.com/thephpleague/commonmark/compare/2.2.0...2.2.1
[2.2.0]: https://github.com/thephpleague/commonmark/compare/2.1.1...2.2.0
[2.1.3]: https://github.com/thephpleague/commonmark/compare/2.1.2...2.1.3
[2.1.2]: https://github.com/thephpleague/commonmark/compare/2.1.1...2.1.2
[2.1.1]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.1.1
[2.1.0]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.1.0
[2.0.4]: https://github.com/thephpleague/commonmark/compare/2.0.3...2.0.4
[2.0.3]: https://github.com/thephpleague/commonmark/compare/2.0.2...2.0.3
[2.0.2]: https://github.com/thephpleague/commonmark/compare/2.0.1...2.0.2
[2.0.1]: https://github.com/thephpleague/commonmark/compare/2.0.0...2.0.1
[2.0.0]: https://github.com/thephpleague/commonmark/compare/2.0.0-rc2...2.0.0
[2.0.0-rc2]: https://github.com/thephpleague/commonmark/compare/2.0.0-rc1...2.0.0-rc2
[2.0.0-rc1]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta3...2.0.0-rc1
[2.0.0-beta3]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta2...2.0.0-beta3
[2.0.0-beta2]: https://github.com/thephpleague/commonmark/compare/2.0.0-beta1...2.0.0-beta2
[2.0.0-beta1]: https://github.com/thephpleague/commonmark/compare/1.6...2.0.0-beta1

28
vendor/league/commonmark/LICENSE vendored Normal file
View File

@ -0,0 +1,28 @@
BSD 3-Clause License
Copyright (c) 2014-2022, Colin O'Dell. All rights reserved. Some code based on commonmark.js (copyright 2014-2018, John MacFarlane) and commonmark-java (copyright 2015-2016, Atlassian Pty Ltd)
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
* Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
* Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
* Neither the name of the copyright holder nor the names of its
contributors may be used to endorse or promote products derived from
this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.

224
vendor/league/commonmark/README.md vendored Normal file
View File

@ -0,0 +1,224 @@
# league/commonmark
[![Latest Version](https://img.shields.io/packagist/v/league/commonmark.svg?style=flat-square)](https://packagist.org/packages/league/commonmark)
[![Total Downloads](https://img.shields.io/packagist/dt/league/commonmark.svg?style=flat-square)](https://packagist.org/packages/league/commonmark)
[![Software License](https://img.shields.io/badge/License-BSD--3-brightgreen.svg?style=flat-square)](LICENSE)
[![Build Status](https://img.shields.io/github/actions/workflow/status/thephpleague/commonmark/tests.yml?branch=main&style=flat-square)](https://github.com/thephpleague/commonmark/actions?query=workflow%3ATests+branch%3Amain)
[![Coverage Status](https://img.shields.io/scrutinizer/coverage/g/thephpleague/commonmark.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/commonmark/code-structure)
[![Quality Score](https://img.shields.io/scrutinizer/g/thephpleague/commonmark.svg?style=flat-square)](https://scrutinizer-ci.com/g/thephpleague/commonmark)
[![Psalm Type Coverage](https://shepherd.dev/github/thephpleague/commonmark/coverage.svg)](https://shepherd.dev/github/thephpleague/commonmark)
[![CII Best Practices](https://bestpractices.coreinfrastructure.org/projects/126/badge)](https://bestpractices.coreinfrastructure.org/projects/126)
[![Sponsor development of this project](https://img.shields.io/badge/sponsor%20this%20package-%E2%9D%A4-ff69b4.svg?style=flat-square)](https://www.colinodell.com/sponsor)
![league/commonmark](commonmark-banner.png)
**league/commonmark** is a highly-extensible PHP Markdown parser created by [Colin O'Dell][@colinodell] which supports the full [CommonMark] spec and [GitHub-Flavored Markdown]. It is based on the [CommonMark JS reference implementation][commonmark.js] by [John MacFarlane] \([@jgm]\).
## 📦 Installation & Basic Usage
This project requires PHP 7.4 or higher with the `mbstring` extension. To install it via [Composer] simply run:
``` bash
$ composer require league/commonmark
```
The `CommonMarkConverter` class provides a simple wrapper for converting CommonMark to HTML:
```php
use League\CommonMark\CommonMarkConverter;
$converter = new CommonMarkConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
echo $converter->convert('# Hello World!');
// <h1>Hello World!</h1>
```
Or if you want GitHub-Flavored Markdown, use the `GithubFlavoredMarkdownConverter` class instead:
```php
use League\CommonMark\GithubFlavoredMarkdownConverter;
$converter = new GithubFlavoredMarkdownConverter([
'html_input' => 'strip',
'allow_unsafe_links' => false,
]);
echo $converter->convert('# Hello World!');
// <h1>Hello World!</h1>
```
Please note that only UTF-8 and ASCII encodings are supported. If your Markdown uses a different encoding please convert it to UTF-8 before running it through this library.
> [!CAUTION]
> If you will be parsing untrusted input from users, please consider setting the `html_input` and `allow_unsafe_links` options per the example above. See <https://commonmark.thephpleague.com/security/> for more details. If you also do choose to allow raw HTML input from untrusted users, consider using a library (like [HTML Purifier](https://github.com/ezyang/htmlpurifier)) to provide additional HTML filtering.
## 📓 Documentation
Full documentation on advanced usage, configuration, and customization can be found at [commonmark.thephpleague.com][docs].
## ⏫ Upgrading
Information on how to upgrade to newer versions of this library can be found at <https://commonmark.thephpleague.com/releases>.
## 💻 GitHub-Flavored Markdown
The `GithubFlavoredMarkdownConverter` shown earlier is a drop-in replacement for the `CommonMarkConverter` which adds additional features found in the GFM spec:
- Autolinks
- Disallowed raw HTML
- Strikethrough
- Tables
- Task Lists
See the [Extensions documentation](https://commonmark.thephpleague.com/customization/extensions/) for more details on how to include only certain GFM features if you don't want them all.
## 🗃️ Related Packages
### Integrations
- [CakePHP 3](https://github.com/gourmet/common-mark)
- [Drupal](https://www.drupal.org/project/markdown)
- [Laravel 4+](https://github.com/GrahamCampbell/Laravel-Markdown)
- [Sculpin](https://github.com/bcremer/sculpin-commonmark-bundle)
- [Symfony 2 & 3](https://github.com/webuni/commonmark-bundle)
- [Symfony 4](https://github.com/avensome/commonmark-bundle)
- [Twig Markdown extension](https://github.com/twigphp/markdown-extension)
- [Twig filter and tag](https://github.com/aptoma/twig-markdown)
- [Laravel CommonMark Blog](https://github.com/spekulatius/laravel-commonmark-blog)
### Included Extensions
See [our extension documentation](https://commonmark.thephpleague.com/extensions/overview) for a full list of extensions bundled with this library.
### Community Extensions
Custom parsers/renderers can be bundled into extensions which extend CommonMark. Here are some that you may find interesting:
- [Emoji extension](https://github.com/ElGigi/CommonMarkEmoji) - UTF-8 emoji extension with Github tag.
- [Sup Sub extensions](https://github.com/OWS/commonmark-sup-sub-extensions) - Adds support of superscript and subscript (`<sup>` and `<sub>` HTML tags)
- [YouTube iframe extension](https://github.com/zoonru/commonmark-ext-youtube-iframe) - Replaces youtube link with iframe.
- [Lazy Image extension](https://github.com/simonvomeyser/commonmark-ext-lazy-image) - Adds various options for lazy loading of images.
- [Marker Extension](https://github.com/noah1400/commonmark-marker-extension) - Adds support of highlighted text (`<mark>` HTML tag)
- [Pygments Highlighter extension](https://github.com/DanielEScherzer/commonmark-ext-pygments-highlighter) - Adds support for highlighting code with the Pygments library
Others can be found on [Packagist under the `commonmark-extension` package type](https://packagist.org/packages/league/commonmark?type=commonmark-extension).
If you build your own, feel free to submit a PR to add it to this list!
### Others
Check out the other cool things people are doing with `league/commonmark`: <https://packagist.org/packages/league/commonmark/dependents>
## 🏷️ Versioning
[SemVer](http://semver.org/) is followed closely. Minor and patch releases should not introduce breaking changes to the codebase; however, they might change the resulting AST or HTML output of parsed Markdown (due to bug fixes, spec changes, etc.) As a result, you might get slightly different HTML, but any custom code built onto this library should still function correctly.
Any classes or methods marked `@internal` are not intended for use outside of this library and are subject to breaking changes at any time, so please avoid using them.
## 🛠️ Maintenance & Support
When a new **minor** version (e.g. `2.0` -> `2.1`) is released, the previous one (`2.0`) will continue to receive security and critical bug fixes for *at least* 3 months.
When a new **major** version is released (e.g. `1.6` -> `2.0`), the previous one (`1.6`) will receive critical bug fixes for *at least* 3 months and security updates for 6 months after that new release comes out.
(This policy may change in the future and exceptions may be made on a case-by-case basis.)
**Professional support, including notification of new releases and security updates, is available through a [Tidelift Subscription](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme).**
## 👷‍♀️ Contributing
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure with us.
If you encounter a bug in the spec, please report it to the [CommonMark] project. Any resulting fix will eventually be implemented in this project as well.
Contributions to this library are **welcome**, especially ones that:
* Improve usability or flexibility without compromising our ability to adhere to the [CommonMark spec]
* Mirror fixes made to the [reference implementation][commonmark.js]
* Optimize performance
* Fix issues with adhering to the [CommonMark spec]
Major refactoring to core parsing logic should be avoided if possible so that we can easily follow updates made to [the reference implementation][commonmark.js]. That being said, we will absolutely consider changes which don't deviate too far from the reference spec or which are favored by other popular CommonMark implementations.
Please see [CONTRIBUTING](https://github.com/thephpleague/commonmark/blob/main/.github/CONTRIBUTING.md) for additional details.
## 🧪 Testing
``` bash
$ composer test
```
This will also test league/commonmark against the latest supported spec.
## 🚀 Performance Benchmarks
You can compare the performance of **league/commonmark** to other popular parsers by running the included benchmark tool:
``` bash
$ ./tests/benchmark/benchmark.php
```
## 👥 Credits & Acknowledgements
This code was originally based on the [CommonMark JS reference implementation][commonmark.js] which is written, maintained, and copyrighted by [John MacFarlane]. This project simply wouldn't exist without his work.
And a huge thanks to all of our amazing contributors:
<a href="https://github.com/thephpleague/commonmark/graphs/contributors">
<img src="https://contrib.rocks/image?repo=thephpleague/commonmark" />
</a>
### Sponsors
We'd also like to extend our sincere thanks the following sponsors who support ongoing development of this project:
- [Tidelift](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme) for offering support to both the maintainers and end-users through their [professional support](https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme) program
- [Blackfire](https://www.blackfire.io/) for providing an Open-Source Profiler subscription
- [JetBrains](https://www.jetbrains.com/) for supporting this project with complimentary [PhpStorm](https://www.jetbrains.com/phpstorm/) licenses
Are you interested in sponsoring development of this project? See <https://www.colinodell.com/sponsor> for a list of ways to contribute.
## 📄 License
**league/commonmark** is licensed under the BSD-3 license. See the [`LICENSE`](LICENSE) file for more details.
## 🏛️ Governance
This project is primarily maintained by [Colin O'Dell][@colinodell]. Members of the [PHP League] Leadership Team may occasionally assist with some of these duties.
## 🗺️ Who Uses It?
This project is used by [Drupal](https://www.drupal.org/project/markdown), [Laravel Framework](https://laravel.com/), [Cachet](https://cachethq.io/), [Firefly III](https://firefly-iii.org/), [Neos](https://www.neos.io/), [Daux.io](https://daux.io/), and [more](https://packagist.org/packages/league/commonmark/dependents)!
---
<div align="center">
<b>
<a href="https://tidelift.com/subscription/pkg/packagist-league-commonmark?utm_source=packagist-league-commonmark&utm_medium=referral&utm_campaign=readme">Get professional support for league/commonmark with a Tidelift subscription</a>
</b>
<br>
<sub>
Tidelift helps make open source sustainable for maintainers while giving companies<br>assurances about security, maintenance, and licensing for their dependencies.
</sub>
</div>
[CommonMark]: http://commonmark.org/
[CommonMark spec]: http://spec.commonmark.org/
[commonmark.js]: https://github.com/jgm/commonmark.js
[GitHub-Flavored Markdown]: https://github.github.com/gfm/
[John MacFarlane]: http://johnmacfarlane.net
[docs]: https://commonmark.thephpleague.com/
[docs-examples]: https://commonmark.thephpleague.com/customization/overview/#examples
[docs-example-twitter]: https://commonmark.thephpleague.com/customization/inline-parsing#example-1---twitter-handles
[docs-example-smilies]: https://commonmark.thephpleague.com/customization/inline-parsing#example-2---emoticons
[All Contributors]: https://github.com/thephpleague/commonmark/contributors
[@colinodell]: https://www.twitter.com/colinodell
[@jgm]: https://github.com/jgm
[jgm/stmd]: https://github.com/jgm/stmd
[Composer]: https://getcomposer.org/
[PHP League]: https://thephpleague.com

129
vendor/league/commonmark/composer.json vendored Normal file
View File

@ -0,0 +1,129 @@
{
"name": "league/commonmark",
"type": "library",
"description": "Highly-extensible PHP Markdown parser which fully supports the CommonMark spec and GitHub-Flavored Markdown (GFM)",
"keywords": ["markdown","parser","commonmark","gfm","github","flavored","github-flavored","md"],
"homepage": "https://commonmark.thephpleague.com",
"license": "BSD-3-Clause",
"authors": [
{
"name": "Colin O'Dell",
"email": "colinodell@gmail.com",
"homepage": "https://www.colinodell.com",
"role": "Lead Developer"
}
],
"support": {
"docs": "https://commonmark.thephpleague.com/",
"forum": "https://github.com/thephpleague/commonmark/discussions",
"issues": "https://github.com/thephpleague/commonmark/issues",
"rss": "https://github.com/thephpleague/commonmark/releases.atom",
"source": "https://github.com/thephpleague/commonmark"
},
"require": {
"php": "^7.4 || ^8.0",
"ext-mbstring": "*",
"league/config": "^1.1.1",
"psr/event-dispatcher": "^1.0",
"symfony/deprecation-contracts": "^2.1 || ^3.0",
"symfony/polyfill-php80": "^1.16"
},
"require-dev": {
"ext-json": "*",
"cebe/markdown": "^1.0",
"commonmark/cmark": "0.31.1",
"commonmark/commonmark.js": "0.31.1",
"composer/package-versions-deprecated": "^1.8",
"embed/embed": "^4.4",
"erusev/parsedown": "^1.0",
"github/gfm": "0.29.0",
"michelf/php-markdown": "^1.4 || ^2.0",
"nyholm/psr7": "^1.5",
"phpstan/phpstan": "^1.8.2",
"phpunit/phpunit": "^9.5.21 || ^10.5.9 || ^11.0.0",
"scrutinizer/ocular": "^1.8.1",
"symfony/finder": "^5.3 | ^6.0 | ^7.0",
"symfony/process": "^5.4 | ^6.0 | ^7.0",
"symfony/yaml": "^2.3 | ^3.0 | ^4.0 | ^5.0 | ^6.0 | ^7.0",
"unleashedtech/php-coding-standard": "^3.1.1",
"vimeo/psalm": "^4.24.0 || ^5.0.0 || ^6.0.0"
},
"minimum-stability": "beta",
"suggest": {
"symfony/yaml": "v2.3+ required if using the Front Matter extension"
},
"repositories": [
{
"type": "package",
"package": {
"name": "commonmark/commonmark.js",
"version": "0.31.1",
"dist": {
"url": "https://github.com/commonmark/commonmark.js/archive/0.31.1.zip",
"type": "zip"
}
}
},
{
"type": "package",
"package": {
"name": "commonmark/cmark",
"version": "0.31.1",
"dist": {
"url": "https://github.com/commonmark/cmark/archive/0.31.1.zip",
"type": "zip"
}
}
},
{
"type": "package",
"package": {
"name": "github/gfm",
"version": "0.29.0",
"dist": {
"url": "https://github.com/github/cmark-gfm/archive/0.29.0.gfm.13.zip",
"type": "zip"
}
}
}
],
"autoload": {
"psr-4": {
"League\\CommonMark\\": "src"
}
},
"autoload-dev": {
"psr-4": {
"League\\CommonMark\\Tests\\Unit\\": "tests/unit",
"League\\CommonMark\\Tests\\Functional\\": "tests/functional",
"League\\CommonMark\\Tests\\PHPStan\\": "tests/phpstan"
}
},
"scripts": {
"phpcs": "phpcs",
"phpcbf": "phpcbf",
"phpstan": "phpstan analyse",
"phpunit": "phpunit --no-coverage",
"psalm": "psalm --stats",
"pathological": "tests/pathological/test.php",
"test": [
"@phpcs",
"@phpstan",
"@psalm",
"@phpunit",
"@pathological"
]
},
"extra": {
"branch-alias": {
"dev-main": "2.8-dev"
}
},
"config": {
"allow-plugins": {
"composer/package-versions-deprecated": true,
"dealerdirect/phpcodesniffer-composer-installer": true
},
"sort-packages": true
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Environment\Environment;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
/**
* Converts CommonMark-compatible Markdown to HTML.
*/
final class CommonMarkConverter extends MarkdownConverter
{
/**
* Create a new Markdown converter pre-configured for CommonMark
*
* @param array<string, mixed> $config
*/
public function __construct(array $config = [])
{
$environment = new Environment($config);
$environment->addExtension(new CommonMarkCoreExtension());
parent::__construct($environment);
}
public function getEnvironment(): Environment
{
\assert($this->environment instanceof Environment);
return $this->environment;
}
}

View File

@ -0,0 +1,30 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark;
use League\CommonMark\Exception\CommonMarkException;
use League\CommonMark\Output\RenderedContentInterface;
use League\Config\Exception\ConfigurationExceptionInterface;
/**
* Interface for a service which converts content from one format (like Markdown) to another (like HTML).
*/
interface ConverterInterface
{
/**
* @throws CommonMarkException
* @throws ConfigurationExceptionInterface
*/
public function convert(string $input): RenderedContentInterface;
}

View File

@ -0,0 +1,83 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Node\Node;
final class Bracket
{
private Node $node;
private ?Bracket $previous;
private bool $hasNext = false;
private int $position;
private bool $image;
private bool $active = true;
public function __construct(Node $node, ?Bracket $previous, int $position, bool $image)
{
$this->node = $node;
$this->previous = $previous;
$this->position = $position;
$this->image = $image;
}
public function getNode(): Node
{
return $this->node;
}
public function getPrevious(): ?Bracket
{
return $this->previous;
}
public function hasNext(): bool
{
return $this->hasNext;
}
public function getPosition(): int
{
return $this->position;
}
public function isImage(): bool
{
return $this->image;
}
/**
* Only valid in the context of non-images (links)
*/
public function isActive(): bool
{
return $this->active;
}
/**
* @internal
*/
public function setHasNext(bool $hasNext): void
{
$this->hasNext = $hasNext;
}
/**
* @internal
*/
public function setActive(bool $active): void
{
$this->active = $active;
}
}

View File

@ -0,0 +1,134 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Node\Inline\AbstractStringContainer;
final class Delimiter implements DelimiterInterface
{
/** @psalm-readonly */
private string $char;
/** @psalm-readonly-allow-private-mutation */
private int $length;
/** @psalm-readonly */
private int $originalLength;
/** @psalm-readonly */
private AbstractStringContainer $inlineNode;
/** @psalm-readonly-allow-private-mutation */
private ?DelimiterInterface $previous = null;
/** @psalm-readonly-allow-private-mutation */
private ?DelimiterInterface $next = null;
/** @psalm-readonly */
private bool $canOpen;
/** @psalm-readonly */
private bool $canClose;
/** @psalm-readonly-allow-private-mutation */
private bool $active;
/** @psalm-readonly */
private ?int $index = null;
public function __construct(string $char, int $numDelims, AbstractStringContainer $node, bool $canOpen, bool $canClose, ?int $index = null)
{
$this->char = $char;
$this->length = $numDelims;
$this->originalLength = $numDelims;
$this->inlineNode = $node;
$this->canOpen = $canOpen;
$this->canClose = $canClose;
$this->active = true;
$this->index = $index;
}
public function canClose(): bool
{
return $this->canClose;
}
public function canOpen(): bool
{
return $this->canOpen;
}
public function isActive(): bool
{
return $this->active;
}
public function setActive(bool $active): void
{
$this->active = $active;
}
public function getChar(): string
{
return $this->char;
}
public function getIndex(): ?int
{
return $this->index;
}
public function getNext(): ?DelimiterInterface
{
return $this->next;
}
public function setNext(?DelimiterInterface $next): void
{
$this->next = $next;
}
public function getLength(): int
{
return $this->length;
}
public function setLength(int $length): void
{
$this->length = $length;
}
public function getOriginalLength(): int
{
return $this->originalLength;
}
public function getInlineNode(): AbstractStringContainer
{
return $this->inlineNode;
}
public function getPrevious(): ?DelimiterInterface
{
return $this->previous;
}
public function setPrevious(?DelimiterInterface $previous): void
{
$this->previous = $previous;
}
}

View File

@ -0,0 +1,56 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Node\Inline\AbstractStringContainer;
interface DelimiterInterface
{
public function canClose(): bool;
public function canOpen(): bool;
/**
* @deprecated This method is no longer used internally and will be removed in 3.0
*/
public function isActive(): bool;
/**
* @deprecated This method is no longer used internally and will be removed in 3.0
*/
public function setActive(bool $active): void;
public function getChar(): string;
public function getIndex(): ?int;
public function getNext(): ?DelimiterInterface;
public function setNext(?DelimiterInterface $next): void;
public function getLength(): int;
public function setLength(int $length): void;
public function getOriginalLength(): int;
public function getInlineNode(): AbstractStringContainer;
public function getPrevious(): ?DelimiterInterface;
public function setPrevious(?DelimiterInterface $previous): void;
}

View File

@ -0,0 +1,106 @@
<?php
declare(strict_types=1);
namespace League\CommonMark\Delimiter;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Parser\Inline\InlineParserMatch;
use League\CommonMark\Parser\InlineParserContext;
use League\CommonMark\Util\RegexHelper;
/**
* Delimiter parsing is implemented as an Inline Parser with the lowest-possible priority
*
* @internal
*/
final class DelimiterParser implements InlineParserInterface
{
private DelimiterProcessorCollection $collection;
public function __construct(DelimiterProcessorCollection $collection)
{
$this->collection = $collection;
}
public function getMatchDefinition(): InlineParserMatch
{
return InlineParserMatch::oneOf(...$this->collection->getDelimiterCharacters());
}
public function parse(InlineParserContext $inlineContext): bool
{
$character = $inlineContext->getFullMatch();
$numDelims = 0;
$cursor = $inlineContext->getCursor();
$processor = $this->collection->getDelimiterProcessor($character);
\assert($processor !== null); // Delimiter processor should never be null here
$charBefore = $cursor->peek(-1);
if ($charBefore === null) {
$charBefore = "\n";
}
while ($cursor->peek($numDelims) === $character) {
++$numDelims;
}
if ($numDelims < $processor->getMinLength()) {
return false;
}
$cursor->advanceBy($numDelims);
$charAfter = $cursor->getCurrentCharacter();
if ($charAfter === null) {
$charAfter = "\n";
}
[$canOpen, $canClose] = self::determineCanOpenOrClose($charBefore, $charAfter, $character, $processor);
if (! ($canOpen || $canClose)) {
$inlineContext->getContainer()->appendChild(new Text(\str_repeat($character, $numDelims)));
return true;
}
$node = new Text(\str_repeat($character, $numDelims), [
'delim' => true,
]);
$inlineContext->getContainer()->appendChild($node);
// Add entry to stack to this opener
$delimiter = new Delimiter($character, $numDelims, $node, $canOpen, $canClose, $inlineContext->getCursor()->getPosition());
$inlineContext->getDelimiterStack()->push($delimiter);
return true;
}
/**
* @return bool[]
*/
private static function determineCanOpenOrClose(string $charBefore, string $charAfter, string $character, DelimiterProcessorInterface $delimiterProcessor): array
{
$afterIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charAfter);
$afterIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charAfter);
$beforeIsWhitespace = \preg_match(RegexHelper::REGEX_UNICODE_WHITESPACE_CHAR, $charBefore);
$beforeIsPunctuation = \preg_match(RegexHelper::REGEX_PUNCTUATION, $charBefore);
$leftFlanking = ! $afterIsWhitespace && (! $afterIsPunctuation || $beforeIsWhitespace || $beforeIsPunctuation);
$rightFlanking = ! $beforeIsWhitespace && (! $beforeIsPunctuation || $afterIsWhitespace || $afterIsPunctuation);
if ($character === '_') {
$canOpen = $leftFlanking && (! $rightFlanking || $beforeIsPunctuation);
$canClose = $rightFlanking && (! $leftFlanking || $afterIsPunctuation);
} else {
$canOpen = $leftFlanking && $character === $delimiterProcessor->getOpeningCharacter();
$canClose = $rightFlanking && $character === $delimiterProcessor->getClosingCharacter();
}
return [$canOpen, $canClose];
}
}

View File

@ -0,0 +1,396 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter;
use League\CommonMark\Delimiter\Processor\CacheableDelimiterProcessorInterface;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Node\Inline\AdjacentTextMerger;
use League\CommonMark\Node\Node;
final class DelimiterStack
{
/** @psalm-readonly-allow-private-mutation */
private ?DelimiterInterface $top = null;
/** @psalm-readonly-allow-private-mutation */
private ?Bracket $brackets = null;
/**
* @deprecated This property will be removed in 3.0 once all delimiters MUST have an index/position
*
* @var \SplObjectStorage<DelimiterInterface, int>|\WeakMap<DelimiterInterface, int>
*/
private $missingIndexCache;
private int $remainingDelimiters = 0;
public function __construct(int $maximumStackSize = PHP_INT_MAX)
{
$this->remainingDelimiters = $maximumStackSize;
if (\PHP_VERSION_ID >= 80000) {
/** @psalm-suppress PropertyTypeCoercion */
$this->missingIndexCache = new \WeakMap(); // @phpstan-ignore-line
} else {
$this->missingIndexCache = new \SplObjectStorage(); // @phpstan-ignore-line
}
}
public function push(DelimiterInterface $newDelimiter): void
{
if ($this->remainingDelimiters-- <= 0) {
return;
}
$newDelimiter->setPrevious($this->top);
if ($this->top !== null) {
$this->top->setNext($newDelimiter);
}
$this->top = $newDelimiter;
}
/**
* @internal
*/
public function addBracket(Node $node, int $index, bool $image): void
{
if ($this->brackets !== null) {
$this->brackets->setHasNext(true);
}
$this->brackets = new Bracket($node, $this->brackets, $index, $image);
}
/**
* @psalm-immutable
*/
public function getLastBracket(): ?Bracket
{
return $this->brackets;
}
private function findEarliest(int $stackBottom): ?DelimiterInterface
{
// Move back to first relevant delim.
$delimiter = $this->top;
$lastChecked = null;
while ($delimiter !== null && self::getIndex($delimiter) > $stackBottom) {
$lastChecked = $delimiter;
$delimiter = $delimiter->getPrevious();
}
return $lastChecked;
}
/**
* @internal
*/
public function removeBracket(): void
{
if ($this->brackets === null) {
return;
}
$this->brackets = $this->brackets->getPrevious();
if ($this->brackets !== null) {
$this->brackets->setHasNext(false);
}
}
public function removeDelimiter(DelimiterInterface $delimiter): void
{
if ($delimiter->getPrevious() !== null) {
/** @psalm-suppress PossiblyNullReference */
$delimiter->getPrevious()->setNext($delimiter->getNext());
}
if ($delimiter->getNext() === null) {
// top of stack
$this->top = $delimiter->getPrevious();
} else {
/** @psalm-suppress PossiblyNullReference */
$delimiter->getNext()->setPrevious($delimiter->getPrevious());
}
// Nullify all references from the removed delimiter to other delimiters.
// All references to this particular delimiter in the linked list should be gone,
// but it's possible we're still hanging on to other references to things that
// have been (or soon will be) removed, which may interfere with efficient
// garbage collection by the PHP runtime.
// Explicitly releasing these references should help to avoid possible
// segfaults like in https://bugs.php.net/bug.php?id=68606.
$delimiter->setPrevious(null);
$delimiter->setNext(null);
// TODO: Remove the line below once PHP 7.4 support is dropped, as WeakMap won't hold onto the reference, making this unnecessary
unset($this->missingIndexCache[$delimiter]);
}
private function removeDelimiterAndNode(DelimiterInterface $delimiter): void
{
$delimiter->getInlineNode()->detach();
$this->removeDelimiter($delimiter);
}
private function removeDelimitersBetween(DelimiterInterface $opener, DelimiterInterface $closer): void
{
$delimiter = $closer->getPrevious();
$openerPosition = self::getIndex($opener);
while ($delimiter !== null && self::getIndex($delimiter) > $openerPosition) {
$previous = $delimiter->getPrevious();
$this->removeDelimiter($delimiter);
$delimiter = $previous;
}
}
/**
* @param DelimiterInterface|int|null $stackBottom
*/
public function removeAll($stackBottom = null): void
{
$stackBottomPosition = \is_int($stackBottom) ? $stackBottom : self::getIndex($stackBottom);
while ($this->top && $this->getIndex($this->top) > $stackBottomPosition) {
$this->removeDelimiter($this->top);
}
}
/**
* @deprecated This method is no longer used internally and will be removed in 3.0
*/
public function removeEarlierMatches(string $character): void
{
$opener = $this->top;
while ($opener !== null) {
if ($opener->getChar() === $character) {
$opener->setActive(false);
}
$opener = $opener->getPrevious();
}
}
/**
* @internal
*/
public function deactivateLinkOpeners(): void
{
$opener = $this->brackets;
while ($opener !== null && $opener->isActive()) {
$opener->setActive(false);
$opener = $opener->getPrevious();
}
}
/**
* @deprecated This method is no longer used internally and will be removed in 3.0
*
* @param string|string[] $characters
*/
public function searchByCharacter($characters): ?DelimiterInterface
{
if (! \is_array($characters)) {
$characters = [$characters];
}
$opener = $this->top;
while ($opener !== null) {
if (\in_array($opener->getChar(), $characters, true)) {
break;
}
$opener = $opener->getPrevious();
}
return $opener;
}
/**
* @param DelimiterInterface|int|null $stackBottom
*
* @todo change $stackBottom to an int in 3.0
*/
public function processDelimiters($stackBottom, DelimiterProcessorCollection $processors): void
{
/** @var array<string, int> $openersBottom */
$openersBottom = [];
$stackBottomPosition = \is_int($stackBottom) ? $stackBottom : self::getIndex($stackBottom);
// Find first closer above stackBottom
$closer = $this->findEarliest($stackBottomPosition);
// Move forward, looking for closers, and handling each
while ($closer !== null) {
$closingDelimiterChar = $closer->getChar();
$delimiterProcessor = $processors->getDelimiterProcessor($closingDelimiterChar);
if (! $closer->canClose() || $delimiterProcessor === null) {
$closer = $closer->getNext();
continue;
}
if ($delimiterProcessor instanceof CacheableDelimiterProcessorInterface) {
$openersBottomCacheKey = $delimiterProcessor->getCacheKey($closer);
} else {
$openersBottomCacheKey = $closingDelimiterChar;
}
$openingDelimiterChar = $delimiterProcessor->getOpeningCharacter();
$useDelims = 0;
$openerFound = false;
$potentialOpenerFound = false;
$opener = $closer->getPrevious();
while ($opener !== null && ($openerPosition = self::getIndex($opener)) > $stackBottomPosition && $openerPosition >= ($openersBottom[$openersBottomCacheKey] ?? 0)) {
if ($opener->canOpen() && $opener->getChar() === $openingDelimiterChar) {
$potentialOpenerFound = true;
$useDelims = $delimiterProcessor->getDelimiterUse($opener, $closer);
if ($useDelims > 0) {
$openerFound = true;
break;
}
}
$opener = $opener->getPrevious();
}
if (! $openerFound) {
// Set lower bound for future searches
// TODO: Remove this conditional check in 3.0. It only exists to prevent behavioral BC breaks in 2.x.
if ($potentialOpenerFound === false || $delimiterProcessor instanceof CacheableDelimiterProcessorInterface) {
$openersBottom[$openersBottomCacheKey] = self::getIndex($closer);
}
if (! $potentialOpenerFound && ! $closer->canOpen()) {
// We can remove a closer that can't be an opener,
// once we've seen there's no matching opener.
$next = $closer->getNext();
$this->removeDelimiter($closer);
$closer = $next;
} else {
$closer = $closer->getNext();
}
continue;
}
\assert($opener !== null);
$openerNode = $opener->getInlineNode();
$closerNode = $closer->getInlineNode();
// Remove number of used delimiters from stack and inline nodes.
$opener->setLength($opener->getLength() - $useDelims);
$closer->setLength($closer->getLength() - $useDelims);
$openerNode->setLiteral(\substr($openerNode->getLiteral(), 0, -$useDelims));
$closerNode->setLiteral(\substr($closerNode->getLiteral(), 0, -$useDelims));
$this->removeDelimitersBetween($opener, $closer);
// The delimiter processor can re-parent the nodes between opener and closer,
// so make sure they're contiguous already. Exclusive because we want to keep opener/closer themselves.
AdjacentTextMerger::mergeTextNodesBetweenExclusive($openerNode, $closerNode);
$delimiterProcessor->process($openerNode, $closerNode, $useDelims);
// No delimiter characters left to process, so we can remove delimiter and the now empty node.
if ($opener->getLength() === 0) {
$this->removeDelimiterAndNode($opener);
}
// phpcs:disable SlevomatCodingStandard.ControlStructures.EarlyExit.EarlyExitNotUsed
if ($closer->getLength() === 0) {
$next = $closer->getNext();
$this->removeDelimiterAndNode($closer);
$closer = $next;
}
}
// Remove all delimiters
$this->removeAll($stackBottomPosition);
}
/**
* @internal
*/
public function __destruct()
{
while ($this->top) {
$this->removeDelimiter($this->top);
}
while ($this->brackets) {
$this->removeBracket();
}
}
/**
* @deprecated This method will be dropped in 3.0 once all delimiters MUST have an index/position
*/
private function getIndex(?DelimiterInterface $delimiter): int
{
if ($delimiter === null) {
return -1;
}
if (($index = $delimiter->getIndex()) !== null) {
return $index;
}
if (isset($this->missingIndexCache[$delimiter])) {
return $this->missingIndexCache[$delimiter];
}
$prev = $delimiter->getPrevious();
$next = $delimiter->getNext();
$i = 0;
do {
$i++;
if ($prev === null) {
break;
}
if ($prev->getIndex() !== null) {
return $this->missingIndexCache[$delimiter] = $prev->getIndex() + $i;
}
} while ($prev = $prev->getPrevious());
$j = 0;
do {
$j++;
if ($next === null) {
break;
}
if ($next->getIndex() !== null) {
return $this->missingIndexCache[$delimiter] = $next->getIndex() - $j;
}
} while ($next = $next->getNext());
// No index was defined on this delimiter, and none could be guesstimated based on the stack.
return $this->missingIndexCache[$delimiter] = $this->getIndex($delimiter->getPrevious()) + 1;
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
/**
* Special marker interface for delimiter processors that return dynamic values from getDelimiterUse()
*
* In order to guarantee linear performance of delimiter processing, the delimiter stack must be able to
* cache the lower bound when searching for a matching opener. This gets complicated for delimiter processors
* that use a dynamic number of characters (like with emphasis and its "multiple of 3" rule).
*/
interface CacheableDelimiterProcessorInterface extends DelimiterProcessorInterface
{
/**
* Returns a cache key of the factors that determine the number of characters to use.
*
* In order to guarantee linear performance of delimiter processing, the delimiter stack must be able to
* cache the lower bound when searching for a matching opener. This lower bound is usually quite simple;
* for example, with quotes, it's just the last opener with that characted. However, this gets complicated
* for delimiter processors that use a dynamic number of characters (like with emphasis and its "multiple
* of 3" rule), because the delimiter length being considered may change during processing because of that
* dynamic logic in getDelimiterUse(). Therefore, we cannot safely cache the lower bound for these dynamic
* processors without knowing the factors that determine the number of characters to use.
*
* At a minimum, this should include the delimiter character, plus any other factors used to determine
* the result of getDelimiterUse(). The format of the string is not important so long as it is unique
* (compared to other processors) and consistent for a given set of factors.
*
* If getDelimiterUse() always returns the same hard-coded value, this method should return just
* the delimiter character.
*/
public function getCacheKey(DelimiterInterface $closer): string;
}

View File

@ -0,0 +1,89 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Exception\InvalidArgumentException;
final class DelimiterProcessorCollection implements DelimiterProcessorCollectionInterface
{
/**
* @var array<string,DelimiterProcessorInterface>|DelimiterProcessorInterface[]
*
* @psalm-readonly-allow-private-mutation
*/
private array $processorsByChar = [];
public function add(DelimiterProcessorInterface $processor): void
{
$opening = $processor->getOpeningCharacter();
$closing = $processor->getClosingCharacter();
if ($opening === $closing) {
$old = $this->processorsByChar[$opening] ?? null;
if ($old !== null && $old->getOpeningCharacter() === $old->getClosingCharacter()) {
$this->addStaggeredDelimiterProcessorForChar($opening, $old, $processor);
} else {
$this->addDelimiterProcessorForChar($opening, $processor);
}
} else {
$this->addDelimiterProcessorForChar($opening, $processor);
$this->addDelimiterProcessorForChar($closing, $processor);
}
}
public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface
{
return $this->processorsByChar[$char] ?? null;
}
/**
* @return string[]
*/
public function getDelimiterCharacters(): array
{
return \array_keys($this->processorsByChar);
}
private function addDelimiterProcessorForChar(string $delimiterChar, DelimiterProcessorInterface $processor): void
{
if (isset($this->processorsByChar[$delimiterChar])) {
throw new InvalidArgumentException(\sprintf('Delim processor for character "%s" already exists', $processor->getOpeningCharacter()));
}
$this->processorsByChar[$delimiterChar] = $processor;
}
private function addStaggeredDelimiterProcessorForChar(string $opening, DelimiterProcessorInterface $old, DelimiterProcessorInterface $new): void
{
if ($old instanceof StaggeredDelimiterProcessor) {
$s = $old;
} else {
$s = new StaggeredDelimiterProcessor($opening, $old);
}
$s->add($new);
$this->processorsByChar[$opening] = $s;
}
public function count(): int
{
return \count($this->processorsByChar);
}
}

View File

@ -0,0 +1,46 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Exception\InvalidArgumentException;
interface DelimiterProcessorCollectionInterface extends \Countable
{
/**
* Add the given delim processor to the collection
*
* @param DelimiterProcessorInterface $processor The delim processor to add
*
* @throws InvalidArgumentException Exception will be thrown if attempting to add multiple processors for the same character
*/
public function add(DelimiterProcessorInterface $processor): void;
/**
* Returns the delim processor which handles the given character if one exists
*/
public function getDelimiterProcessor(string $char): ?DelimiterProcessorInterface;
/**
* Returns an array of delimiter characters who have associated processors
*
* @return string[]
*/
public function getDelimiterCharacters(): array;
}

View File

@ -0,0 +1,81 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Node\Inline\AbstractStringContainer;
/**
* Interface for a delimiter processor
*/
interface DelimiterProcessorInterface
{
/**
* Returns the character that marks the beginning of a delimited node.
*
* This must not clash with any other processors being added to the environment.
*/
public function getOpeningCharacter(): string;
/**
* Returns the character that marks the ending of a delimited node.
*
* This must not clash with any other processors being added to the environment.
*
* Note that for a symmetric delimiter such as "*", this is the same as the opening.
*/
public function getClosingCharacter(): string;
/**
* Minimum number of delimiter characters that are needed to active this.
*
* Must be at least 1.
*/
public function getMinLength(): int;
/**
* Determine how many (if any) of the delimiter characters should be used.
*
* This allows implementations to decide how many characters to be used
* based on the properties of the delimiter runs. An implementation can also
* return 0 when it doesn't want to allow this particular combination of
* delimiter runs.
*
* IMPORTANT: Unless this method returns the same hard-coded value in all cases,
* you MUST implement the CacheableDelimiterProcessorInterface interface instead.
*
* @param DelimiterInterface $opener The opening delimiter run
* @param DelimiterInterface $closer The closing delimiter run
*/
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int;
/**
* Process the matched delimiters, e.g. by wrapping the nodes between opener
* and closer in a new node, or appending a new node after the opener.
*
* Note that removal of the delimiter from the delimiter nodes and detaching
* them is done by the caller.
*
* @param AbstractStringContainer $opener The node that contained the opening delimiter
* @param AbstractStringContainer $closer The node that contained the closing delimiter
* @param int $delimiterUse The number of delimiters that were used
*/
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void;
}

View File

@ -0,0 +1,111 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Additional emphasis processing code based on commonmark-java (https://github.com/atlassian/commonmark-java)
* - (c) Atlassian Pty Ltd
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Delimiter\Processor;
use League\CommonMark\Delimiter\DelimiterInterface;
use League\CommonMark\Exception\InvalidArgumentException;
use League\CommonMark\Node\Inline\AbstractStringContainer;
/**
* An implementation of DelimiterProcessorInterface that dispatches all calls to two or more other DelimiterProcessors
* depending on the length of the delimiter run. All child DelimiterProcessors must have different minimum
* lengths. A given delimiter run is dispatched to the child with the largest acceptable minimum length. If no
* child is applicable, the one with the largest minimum length is chosen.
*
* @internal
*/
final class StaggeredDelimiterProcessor implements DelimiterProcessorInterface
{
/** @psalm-readonly */
private string $delimiterChar;
/** @psalm-readonly-allow-private-mutation */
private int $minLength = 0;
/**
* @var array<int, DelimiterProcessorInterface>|DelimiterProcessorInterface[]
*
* @psalm-readonly-allow-private-mutation
*/
private array $processors = []; // keyed by minLength in reverse order
public function __construct(string $char, DelimiterProcessorInterface $processor)
{
$this->delimiterChar = $char;
$this->add($processor);
}
public function getOpeningCharacter(): string
{
return $this->delimiterChar;
}
public function getClosingCharacter(): string
{
return $this->delimiterChar;
}
public function getMinLength(): int
{
return $this->minLength;
}
/**
* Adds the given processor to this staggered delimiter processor
*
* @throws InvalidArgumentException if attempting to add another processors for the same character and minimum length
*/
public function add(DelimiterProcessorInterface $processor): void
{
$len = $processor->getMinLength();
if (isset($this->processors[$len])) {
throw new InvalidArgumentException(\sprintf('Cannot add two delimiter processors for char "%s" and minimum length %d', $this->delimiterChar, $len));
}
$this->processors[$len] = $processor;
\krsort($this->processors);
$this->minLength = \min($this->minLength, $len);
}
public function getDelimiterUse(DelimiterInterface $opener, DelimiterInterface $closer): int
{
return $this->findProcessor($opener->getLength())->getDelimiterUse($opener, $closer);
}
public function process(AbstractStringContainer $opener, AbstractStringContainer $closer, int $delimiterUse): void
{
$this->findProcessor($delimiterUse)->process($opener, $closer, $delimiterUse);
}
private function findProcessor(int $len): DelimiterProcessorInterface
{
// Find the "longest" processor which can handle this length
foreach ($this->processors as $processor) {
if ($processor->getMinLength() <= $len) {
return $processor;
}
}
// Just use the first one in our list
$first = \reset($this->processors);
\assert($first instanceof DelimiterProcessorInterface);
return $first;
}
}

View File

@ -0,0 +1,448 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the CommonMark JS reference parser (https://bitly.com/commonmark-js)
* - (c) John MacFarlane
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Environment;
use League\CommonMark\Delimiter\DelimiterParser;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Event\ListenerData;
use League\CommonMark\Exception\AlreadyInitializedException;
use League\CommonMark\Extension\CommonMark\CommonMarkCoreExtension;
use League\CommonMark\Extension\ConfigurableExtensionInterface;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Extension\GithubFlavoredMarkdownExtension;
use League\CommonMark\Normalizer\SlugNormalizer;
use League\CommonMark\Normalizer\TextNormalizerInterface;
use League\CommonMark\Normalizer\UniqueSlugNormalizer;
use League\CommonMark\Normalizer\UniqueSlugNormalizerInterface;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Block\SkipLinesStartingWithLettersParser;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\CommonMark\Util\HtmlFilter;
use League\CommonMark\Util\PrioritizedList;
use League\Config\Configuration;
use League\Config\ConfigurationAwareInterface;
use League\Config\ConfigurationInterface;
use Nette\Schema\Expect;
use Psr\EventDispatcher\EventDispatcherInterface;
use Psr\EventDispatcher\ListenerProviderInterface;
use Psr\EventDispatcher\StoppableEventInterface;
final class Environment implements EnvironmentInterface, EnvironmentBuilderInterface, ListenerProviderInterface
{
/**
* @var ExtensionInterface[]
*
* @psalm-readonly-allow-private-mutation
*/
private array $extensions = [];
/**
* @var ExtensionInterface[]
*
* @psalm-readonly-allow-private-mutation
*/
private array $uninitializedExtensions = [];
/** @psalm-readonly-allow-private-mutation */
private bool $extensionsInitialized = false;
/**
* @var PrioritizedList<BlockStartParserInterface>
*
* @psalm-readonly
*/
private PrioritizedList $blockStartParsers;
/**
* @var PrioritizedList<InlineParserInterface>
*
* @psalm-readonly
*/
private PrioritizedList $inlineParsers;
/** @psalm-readonly */
private DelimiterProcessorCollection $delimiterProcessors;
/**
* @var array<string, PrioritizedList<NodeRendererInterface>>
*
* @psalm-readonly-allow-private-mutation
*/
private array $renderersByClass = [];
/**
* @var PrioritizedList<ListenerData>
*
* @psalm-readonly-allow-private-mutation
*/
private PrioritizedList $listenerData;
private ?EventDispatcherInterface $eventDispatcher = null;
/** @psalm-readonly */
private Configuration $config;
private ?TextNormalizerInterface $slugNormalizer = null;
/**
* @param array<string, mixed> $config
*/
public function __construct(array $config = [])
{
$this->config = self::createDefaultConfiguration();
$this->config->merge($config);
$this->blockStartParsers = new PrioritizedList();
$this->inlineParsers = new PrioritizedList();
$this->listenerData = new PrioritizedList();
$this->delimiterProcessors = new DelimiterProcessorCollection();
// Performance optimization: always include a block "parser" that aborts parsing if a line starts with a letter
// and is therefore unlikely to match any lines as a block start.
$this->addBlockStartParser(new SkipLinesStartingWithLettersParser(), 249);
}
public function getConfiguration(): ConfigurationInterface
{
return $this->config->reader();
}
/**
* @deprecated Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.
*
* @param array<string, mixed> $config
*/
public function mergeConfig(array $config): void
{
@\trigger_error('Environment::mergeConfig() is deprecated since league/commonmark v2.0 and will be removed in v3.0. Configuration should be set when instantiating the environment instead.', \E_USER_DEPRECATED);
$this->assertUninitialized('Failed to modify configuration.');
$this->config->merge($config);
}
public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
{
$this->assertUninitialized('Failed to add block start parser.');
$this->blockStartParsers->add($parser, $priority);
$this->injectEnvironmentAndConfigurationIfNeeded($parser);
return $this;
}
public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface
{
$this->assertUninitialized('Failed to add inline parser.');
$this->inlineParsers->add($parser, $priority);
$this->injectEnvironmentAndConfigurationIfNeeded($parser);
return $this;
}
public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface
{
$this->assertUninitialized('Failed to add delimiter processor.');
$this->delimiterProcessors->add($processor);
$this->injectEnvironmentAndConfigurationIfNeeded($processor);
return $this;
}
public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface
{
$this->assertUninitialized('Failed to add renderer.');
if (! isset($this->renderersByClass[$nodeClass])) {
$this->renderersByClass[$nodeClass] = new PrioritizedList();
}
$this->renderersByClass[$nodeClass]->add($renderer, $priority);
$this->injectEnvironmentAndConfigurationIfNeeded($renderer);
return $this;
}
/**
* {@inheritDoc}
*/
public function getBlockStartParsers(): iterable
{
if (! $this->extensionsInitialized) {
$this->initializeExtensions();
}
return $this->blockStartParsers->getIterator();
}
public function getDelimiterProcessors(): DelimiterProcessorCollection
{
if (! $this->extensionsInitialized) {
$this->initializeExtensions();
}
return $this->delimiterProcessors;
}
/**
* {@inheritDoc}
*/
public function getRenderersForClass(string $nodeClass): iterable
{
if (! $this->extensionsInitialized) {
$this->initializeExtensions();
}
// If renderers are defined for this specific class, return them immediately
if (isset($this->renderersByClass[$nodeClass])) {
return $this->renderersByClass[$nodeClass];
}
/** @psalm-suppress TypeDoesNotContainType -- Bug: https://github.com/vimeo/psalm/issues/3332 */
while (\class_exists($parent ??= $nodeClass) && $parent = \get_parent_class($parent)) {
if (! isset($this->renderersByClass[$parent])) {
continue;
}
// "Cache" this result to avoid future loops
return $this->renderersByClass[$nodeClass] = $this->renderersByClass[$parent];
}
return [];
}
/**
* {@inheritDoc}
*/
public function getExtensions(): iterable
{
return $this->extensions;
}
/**
* Add a single extension
*
* @return $this
*/
public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface
{
$this->assertUninitialized('Failed to add extension.');
$this->extensions[] = $extension;
$this->uninitializedExtensions[] = $extension;
if ($extension instanceof ConfigurableExtensionInterface) {
$extension->configureSchema($this->config);
}
return $this;
}
private function initializeExtensions(): void
{
// Initialize the slug normalizer
$this->getSlugNormalizer();
// Ask all extensions to register their components
while (\count($this->uninitializedExtensions) > 0) {
foreach ($this->uninitializedExtensions as $i => $extension) {
$extension->register($this);
unset($this->uninitializedExtensions[$i]);
}
}
$this->extensionsInitialized = true;
// Create the special delimiter parser if any processors were registered
if ($this->delimiterProcessors->count() > 0) {
$this->inlineParsers->add(new DelimiterParser($this->delimiterProcessors), PHP_INT_MIN);
}
}
private function injectEnvironmentAndConfigurationIfNeeded(object $object): void
{
if ($object instanceof EnvironmentAwareInterface) {
$object->setEnvironment($this);
}
if ($object instanceof ConfigurationAwareInterface) {
$object->setConfiguration($this->config->reader());
}
}
/**
* @deprecated Instantiate the environment and add the extension yourself
*
* @param array<string, mixed> $config
*/
public static function createCommonMarkEnvironment(array $config = []): Environment
{
$environment = new self($config);
$environment->addExtension(new CommonMarkCoreExtension());
return $environment;
}
/**
* @deprecated Instantiate the environment and add the extension yourself
*
* @param array<string, mixed> $config
*/
public static function createGFMEnvironment(array $config = []): Environment
{
$environment = new self($config);
$environment->addExtension(new CommonMarkCoreExtension());
$environment->addExtension(new GithubFlavoredMarkdownExtension());
return $environment;
}
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface
{
$this->assertUninitialized('Failed to add event listener.');
$this->listenerData->add(new ListenerData($eventClass, $listener), $priority);
if (\is_object($listener)) {
$this->injectEnvironmentAndConfigurationIfNeeded($listener);
} elseif (\is_array($listener) && \is_object($listener[0])) {
$this->injectEnvironmentAndConfigurationIfNeeded($listener[0]);
}
return $this;
}
public function dispatch(object $event): object
{
if (! $this->extensionsInitialized) {
$this->initializeExtensions();
}
if ($this->eventDispatcher !== null) {
return $this->eventDispatcher->dispatch($event);
}
foreach ($this->getListenersForEvent($event) as $listener) {
if ($event instanceof StoppableEventInterface && $event->isPropagationStopped()) {
return $event;
}
$listener($event);
}
return $event;
}
public function setEventDispatcher(EventDispatcherInterface $dispatcher): void
{
$this->eventDispatcher = $dispatcher;
}
/**
* {@inheritDoc}
*
* @return iterable<callable>
*/
public function getListenersForEvent(object $event): iterable
{
foreach ($this->listenerData as $listenerData) {
\assert($listenerData instanceof ListenerData);
/** @psalm-suppress ArgumentTypeCoercion */
if (! \is_a($event, $listenerData->getEvent())) {
continue;
}
yield function (object $event) use ($listenerData) {
if (! $this->extensionsInitialized) {
$this->initializeExtensions();
}
return \call_user_func($listenerData->getListener(), $event);
};
}
}
/**
* @return iterable<InlineParserInterface>
*/
public function getInlineParsers(): iterable
{
if (! $this->extensionsInitialized) {
$this->initializeExtensions();
}
return $this->inlineParsers->getIterator();
}
public function getSlugNormalizer(): TextNormalizerInterface
{
if ($this->slugNormalizer === null) {
$normalizer = $this->config->get('slug_normalizer/instance');
\assert($normalizer instanceof TextNormalizerInterface);
$this->injectEnvironmentAndConfigurationIfNeeded($normalizer);
if ($this->config->get('slug_normalizer/unique') !== UniqueSlugNormalizerInterface::DISABLED && ! $normalizer instanceof UniqueSlugNormalizer) {
$normalizer = new UniqueSlugNormalizer($normalizer);
}
if ($normalizer instanceof UniqueSlugNormalizer) {
if ($this->config->get('slug_normalizer/unique') === UniqueSlugNormalizerInterface::PER_DOCUMENT) {
$this->addEventListener(DocumentParsedEvent::class, [$normalizer, 'clearHistory'], -1000);
}
}
$this->slugNormalizer = $normalizer;
}
return $this->slugNormalizer;
}
/**
* @throws AlreadyInitializedException
*/
private function assertUninitialized(string $message): void
{
if ($this->extensionsInitialized) {
throw new AlreadyInitializedException($message . ' Extensions have already been initialized.');
}
}
public static function createDefaultConfiguration(): Configuration
{
return new Configuration([
'html_input' => Expect::anyOf(HtmlFilter::STRIP, HtmlFilter::ALLOW, HtmlFilter::ESCAPE)->default(HtmlFilter::ALLOW),
'allow_unsafe_links' => Expect::bool(true),
'max_nesting_level' => Expect::type('int')->default(PHP_INT_MAX),
'max_delimiters_per_line' => Expect::type('int')->default(PHP_INT_MAX),
'renderer' => Expect::structure([
'block_separator' => Expect::string("\n"),
'inner_separator' => Expect::string("\n"),
'soft_break' => Expect::string("\n"),
]),
'slug_normalizer' => Expect::structure([
'instance' => Expect::type(TextNormalizerInterface::class)->default(new SlugNormalizer()),
'max_length' => Expect::int()->min(0)->default(255),
'unique' => Expect::anyOf(UniqueSlugNormalizerInterface::DISABLED, UniqueSlugNormalizerInterface::PER_ENVIRONMENT, UniqueSlugNormalizerInterface::PER_DOCUMENT)->default(UniqueSlugNormalizerInterface::PER_DOCUMENT),
]),
]);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Environment;
interface EnvironmentAwareInterface
{
public function setEnvironment(EnvironmentInterface $environment): void;
}

View File

@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Environment;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorInterface;
use League\CommonMark\Exception\AlreadyInitializedException;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Node\Node;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\Config\ConfigurationProviderInterface;
/**
* Interface for building the Environment with any extensions, parsers, listeners, etc. that it may need
*/
interface EnvironmentBuilderInterface extends ConfigurationProviderInterface
{
/**
* Registers the given extension with the Environment
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addExtension(ExtensionInterface $extension): EnvironmentBuilderInterface;
/**
* Registers the given block start parser with the Environment
*
* @param BlockStartParserInterface $parser Block parser instance
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addBlockStartParser(BlockStartParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface;
/**
* Registers the given inline parser with the Environment
*
* @param InlineParserInterface $parser Inline parser instance
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addInlineParser(InlineParserInterface $parser, int $priority = 0): EnvironmentBuilderInterface;
/**
* Registers the given delimiter processor with the Environment
*
* @param DelimiterProcessorInterface $processor Delimiter processors instance
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addDelimiterProcessor(DelimiterProcessorInterface $processor): EnvironmentBuilderInterface;
/**
* Registers the given node renderer with the Environment
*
* @param string $nodeClass The fully-qualified node element class name the renderer below should handle
* @param NodeRendererInterface $renderer The renderer responsible for rendering the type of element given above
* @param int $priority Priority (a higher number will be executed earlier)
*
* @psalm-param class-string<Node> $nodeClass
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addRenderer(string $nodeClass, NodeRendererInterface $renderer, int $priority = 0): EnvironmentBuilderInterface;
/**
* Registers the given event listener
*
* @param class-string $eventClass Fully-qualified class name of the event this listener should respond to
* @param callable $listener Listener to be executed
* @param int $priority Priority (a higher number will be executed earlier)
*
* @return $this
*
* @throws AlreadyInitializedException if the Environment has already been initialized
*/
public function addEventListener(string $eventClass, callable $listener, int $priority = 0): EnvironmentBuilderInterface;
}

View File

@ -0,0 +1,55 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Environment;
use League\CommonMark\Delimiter\Processor\DelimiterProcessorCollection;
use League\CommonMark\Extension\ExtensionInterface;
use League\CommonMark\Node\Node;
use League\CommonMark\Normalizer\TextNormalizerInterface;
use League\CommonMark\Parser\Block\BlockStartParserInterface;
use League\CommonMark\Parser\Inline\InlineParserInterface;
use League\CommonMark\Renderer\NodeRendererInterface;
use League\Config\ConfigurationProviderInterface;
use Psr\EventDispatcher\EventDispatcherInterface;
interface EnvironmentInterface extends ConfigurationProviderInterface, EventDispatcherInterface
{
/**
* Get all registered extensions
*
* @return ExtensionInterface[]
*/
public function getExtensions(): iterable;
/**
* @return iterable<BlockStartParserInterface>
*/
public function getBlockStartParsers(): iterable;
/**
* @return iterable<InlineParserInterface>
*/
public function getInlineParsers(): iterable;
public function getDelimiterProcessors(): DelimiterProcessorCollection;
/**
* @psalm-param class-string<Node> $nodeClass
*
* @return iterable<NodeRendererInterface>
*/
public function getRenderersForClass(string $nodeClass): iterable;
public function getSlugNormalizer(): TextNormalizerInterface;
}

View File

@ -0,0 +1,54 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* Original code based on the Symfony EventDispatcher "Event" contract
* - (c) 2018-2019 Fabien Potencier
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
use Psr\EventDispatcher\StoppableEventInterface;
/**
* Base class for classes containing event data.
*
* This class contains no event data. It is used by events that do not pass
* state information to an event handler when an event is raised.
*
* You can call the method stopPropagation() to abort the execution of
* further listeners in your event listener.
*/
abstract class AbstractEvent implements StoppableEventInterface
{
/** @psalm-readonly-allow-private-mutation */
private bool $propagationStopped = false;
/**
* Returns whether further event listeners should be triggered.
*/
final public function isPropagationStopped(): bool
{
return $this->propagationStopped;
}
/**
* Stops the propagation of the event to further event listeners.
*
* If multiple event listeners are connected to the same event, no
* further event listener will be triggered once any trigger calls
* stopPropagation().
*/
final public function stopPropagation(): void
{
$this->propagationStopped = true;
}
}

View File

@ -0,0 +1,35 @@
<?php
declare(strict_types=1);
/*
* This file is part of the league/commonmark package.
*
* (c) Colin O'Dell <colinodell@gmail.com>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace League\CommonMark\Event;
use League\CommonMark\Node\Block\Document;
/**
* Event dispatched when the document has been fully parsed
*/
final class DocumentParsedEvent extends AbstractEvent
{
/** @psalm-readonly */
private Document $document;
public function __construct(Document $document)
{
$this->document = $document;
}
public function getDocument(): Document
{
return $this->document;
}
}

Some files were not shown because too many files have changed in this diff Show More