diff --git a/.agent/rules/00_constitution.md b/.agent/rules/00_constitution.md
new file mode 100644
index 000000000..6ac84000a
--- /dev/null
+++ b/.agent/rules/00_constitution.md
@@ -0,0 +1,30 @@
+---
+trigger: always_on
+description: Absolute source of truth for the MT-site project.
+category: governance
+---
+# **AI CONTEXT SPECIFICATIONS & PROJECT CONSTITUTION**
+
+## π§ Rationale
+
+Establishing an absolute source of truth is critical for maintaining consistency and quality. This constitution ensures all documentation remains professional, beautiful, and highly portable.
+
+## π οΈ Implementation
+
+$$SYSTEM\_CRITICAL$$
+Notice to the Agent: This document constitutes the unique and absolute source of truth for the MT-site project.
+
+**Core Mission:**
+Make `MT-site` the most beautiful, accessible, and high-performance documentation hub for MySQLTuner.
+
+**Key Pillars:**
+
+- **Visual Excellence**: Every page must feel premium, using modern dark-mode aesthetics and smooth interactions.
+- **Extreme Portability**: The site must remain executable on any standard web server with PHP support. Zero complexity in deployment.
+- **Actionable Documentation**: Content must be clear, well-structured, and automatically synchronized with the core script.
+- **Production Performance**: Zero layout shifts and sub-100ms navigation for a "native app" feel.
+
+## β
Verification
+
+- All UI/UX changes must be reviewed for aesthetic compliance.
+- Use `/compliance-sentinel` to audit deviations.
diff --git a/.agent/rules/01_objective.md b/.agent/rules/01_objective.md
new file mode 100644
index 000000000..36bb44a61
--- /dev/null
+++ b/.agent/rules/01_objective.md
@@ -0,0 +1,28 @@
+---
+trigger: always_on
+description: Identity and Mission of the MT-site project.
+category: governance
+---
+# **1. π― OPERATIONAL OBJECTIVE**
+
+## π§ Rationale
+
+Dynamic context tracking allows the agent to focus on making the documentation site a world-class resource.
+
+## π οΈ Implementation
+
+$$DYNAMIC\_CONTEXT$$
+
+* **Status:** [IN PROGRESS]
+* **Priority Task:** Transform MT-site into a premium documentation hub with server-side Markdown rendering and a technical dark-mode aesthetic.
+
+**Success Criteria:**
+
+1. **Aesthetics:** 100% "WOW" factor. Dark mode with glassmorphism and premium typography (Inter/Outfit).
+2. **Architecture:** PHP-based zero-dependency router. No heavy frameworks.
+3. **Synchronization:** Automated doc synchronization with `MySQLTuner-perl`.
+4. **Resiliency:** 100% functional on standard PHP hosting (IONOS, etc.).
+
+## β
Verification
+
+- Run `/local-preview` to verify visual standards.
diff --git a/.agent/rules/02_architecture.md b/.agent/rules/02_architecture.md
new file mode 100644
index 000000000..9a487d4ec
--- /dev/null
+++ b/.agent/rules/02_architecture.md
@@ -0,0 +1,34 @@
+---
+trigger: always_on
+description: Technical environment and architecture map.
+category: governance
+---
+# **2. ποΈ TECHNICAL ENVIRONMENT & ARCHITECTURE**
+
+## π§ Rationale
+
+Preserving the lightweight PHP architecture ensures maximum portability and speed.
+
+## π οΈ Implementation
+
+$$IMMUTABLE$$
+
+| File/Folder | Functionality | Criticality |
+| :--- | :--- | :--- |
+| index.php | Main router and entry point | π΄ CRITICAL |
+| includes/ | Core layouts (header, footer, sidebar) | π΄ CRITICAL |
+| public/docs/ | Markdown documentation sources | π΄ CRITICAL |
+| assets/css/ | Global styling and design system | π‘ HIGH |
+| scripts/ | Automation scripts (sync_docs.py) | π‘ HIGH |
+
+**Technology Stack:**
+
+- **Language**: PHP (Server-side rendering)
+- **Engine**: Parsedown (Markdown processing)
+- **Styling**: Vanilla CSS (Modern technical aesthetic)
+- **Typography**: Inter & Outfit (Google Fonts)
+- **Icons**: Emoji & Custom SVG
+
+## β
Verification
+
+- Ensure any new library is single-file and PHP-based.
diff --git a/.agent/rules/03_execution_rules.md b/.agent/rules/03_execution_rules.md
new file mode 100644
index 000000000..81f9d7aa8
--- /dev/null
+++ b/.agent/rules/03_execution_rules.md
@@ -0,0 +1,25 @@
+---
+trigger: always_on
+description: Execution rules and constraints for MT-site.
+category: governance
+---
+# **3. βοΈ EXECUTION RULES & CONSTRAINTS**
+
+## **3.1. Formal Prohibitions**
+
+1. **NO FRAMEWORKS**: Use of heavy JS frameworks (React/Vue) is prohibited. Stick to PHP + Vanilla JS.
+2. **ZERO DEPENDENCY**: No NPM/Node.js dependencies in production.
+3. **PORTABILITY FIRST**: All paths must be relative or dynamically determined to work across different hostings.
+4. **NO CONTENT DELETION**: Documentation content should only be updated or archived, never deleted without cause.
+
+## **3.2. Aesthetic Guidelines**
+
+1. **DARK MODE ONLY**: The site is primary dark-mode. High contrast but easy on the eyes.
+2. **GLASSMORPHISM**: Use subtle background blurs and borders for overlays and headers.
+3. **TYPOGRAPHY**: Titles in `Outfit`, body in `Inter`.
+4. **ANIMATIONS**: Use CSS transitions for page loads and hover states (`fade-in`, `slide-up`).
+
+## **3.3. Development Workflow**
+
+1. **TDD (Visual)**: Before finalizing a UI change, verify it via `/local-preview`.
+2. **SYNC CHECK**: After updating documentation, ensure the sync script is functional.
diff --git a/.agent/rules/04_best_practices.md b/.agent/rules/04_best_practices.md
new file mode 100644
index 000000000..4eeda65b4
--- /dev/null
+++ b/.agent/rules/04_best_practices.md
@@ -0,0 +1,40 @@
+---
+trigger: always_on
+description: Best practices for MT-site development and aesthetics.
+category: governance
+---
+# **4. π CORE BEST PRACTICES**
+
+## π§ Rationale
+
+Consistent high standards ensure the site remains premium and maintainable.
+
+## π οΈ Implementation
+
+### 1. Visual "Aesthetic" Audit
+
+- Use the **Browser Agent** to check:
+ - Contrast ratios.
+ - Hover animations.
+ - Responsive layout (Mobile/Desktop).
+ - Loading states (no white flashes).
+
+### 2. Micro-Animations
+
+- Apply `transition: all 0.3s ease;` to interactive elements.
+- Use `opacity` and `transform: translateY` for entrance animations.
+
+### 3. SEO & Accessibility
+
+- Every page must have a unique `
` and `meta description`.
+- Semantic HTML (``, ``, ``) is mandatory.
+
+### 4. Performance
+
+- Optimize images (WebP preferred).
+- Minimal external requests (Google Fonts are the exception).
+- CSS should be under 50KB.
+
+## β
Verification
+
+- Use `visual-audit` skill for automated compliance checks.
diff --git a/.agent/rules/05_memory_protocol.md b/.agent/rules/05_memory_protocol.md
new file mode 100644
index 000000000..897096852
--- /dev/null
+++ b/.agent/rules/05_memory_protocol.md
@@ -0,0 +1,18 @@
+---
+trigger: always_on
+description: Contextual consistency and history protocols.
+category: governance
+---
+# **5. π STATE MEMORY & HISTORY**
+
+## **Contextual Consistency Protocols**
+
+1. **History Update**: Add new entries to the top of `Changelog` if the action is correct and tested.
+2. **Rotation**: FIFO Rotation (Max 600 lines). Remove oldest entries beyond 600 lines.
+3. **Changelog**: All changes MUST be traced and documented inside `Changelog`.
+
+### **History Entry example**
+
+1.0.1 2026-02-01
+
+- feat: establish .agent governance and aesthetic verification.
diff --git a/.agent/rules/remembers.md b/.agent/rules/remembers.md
new file mode 100644
index 000000000..60f13b833
--- /dev/null
+++ b/.agent/rules/remembers.md
@@ -0,0 +1,10 @@
+# **Session Memory Buffer**
+
+## π§ Rationale
+
+Tracks transient session context and lessons learned to maintain alignment between manual interventions.
+
+## π οΈ Implementation
+
+- [2026-02-01] Established initial .agent governance for MT-site.
+- [2026-02-01] Defined "clean and aesthetic" standards.
diff --git a/.agent/skills/visual-audit/SKILL.md b/.agent/skills/visual-audit/SKILL.md
new file mode 100644
index 000000000..16b405aee
--- /dev/null
+++ b/.agent/skills/visual-audit/SKILL.md
@@ -0,0 +1,25 @@
+---
+trigger: explicit_call
+description: Use the browser agent to audit the site's aesthetics.
+category: skill
+---
+# Skill: Visual Audit
+
+## π§ Rationale
+
+Ensures the site meets the visual standards defined in the project constitution.
+
+## π οΈ Implementation
+
+1. **Initialize Preview**: Run `/local-preview` to ensure the site is running.
+2. **Browser Audit**: Open `http://localhost:8080` using the `browser_subagent`.
+3. **Checks**:
+ - Verify dark mode color palette (e.g., `#0f172a` background).
+ - Check for glassmorphism effects (backdrop-filter).
+ - Verify font loading (Inter/Outfit).
+ - Check responsive behavior.
+4. **Report**: Summarize findings and suggest improvements if "WOW" factor is missing.
+
+## β
Verification
+
+- Compare visual results with `specification.md` design goals.
diff --git a/.agent/workflows/doc-sync.md b/.agent/workflows/doc-sync.md
new file mode 100644
index 000000000..87440c165
--- /dev/null
+++ b/.agent/workflows/doc-sync.md
@@ -0,0 +1,18 @@
+---
+description: Synchronize .agent/README.md with current Rules, Skills, and Workflows
+---
+# Doc-Sync Workflow
+
+## π§ Rationale
+
+Keeps the project's technical summary up-to-date with all available governance assets.
+
+## π οΈ Implementation
+
+1. Scan `.agent/rules/`, `.agent/skills/`, and `.agent/workflows/`.
+2. Update `.agent/README.md` with a structured list of these items.
+3. Ensure absolute paths are converted to relative paths for portability.
+
+## β
Verification
+
+- Verify `.agent/README.md` is updated and matches the filesystem.
diff --git a/.agent/workflows/git-flow.md b/.agent/workflows/git-flow.md
new file mode 100644
index 000000000..251f44f45
--- /dev/null
+++ b/.agent/workflows/git-flow.md
@@ -0,0 +1,51 @@
+---
+description: Automate git-flow release process for MT-site
+---
+# Git-Flow Release Workflow (MT-site)
+
+1. **Run Release Preflight Workflow**
+ - Execute the `/release-preflight` workflow.
+
+ ```bash
+ /release-preflight
+ ```
+
+2. **Commit Current Changes**
+ - Commit pending changes including governance and fixes.
+
+ ```bash
+ git add .
+ # Use conventional commit
+ git commit -m "feat: release $CURRENT_VER"
+ ```
+
+3. **Create Tag for Current Version**
+
+ ```bash
+ TAG_MSG=$(awk "/^$CURRENT_VER/,/^([0-9]+\.[0-9]+\.[0-9]+)/ {if (\$0 !~ /^([0-9]+\.[0-9]+\.[0-9]+)/) print}" Changelog | sed '/^$/d')
+ git tag -a v$CURRENT_VER -m "Release $CURRENT_VER" -m "$TAG_MSG"
+ ```
+
+4. **Push Branch and Tag**
+
+ ```bash
+ git push origin main
+ git push origin v$CURRENT_VER
+ ```
+
+5. **Post-Push: Increment Version**
+
+ ```bash
+ NEW_VER=$(echo $CURRENT_VER | awk -F. '{print $1"."$2"."($3+1)}')
+ echo $NEW_VER > CURRENT_VERSION.txt
+ DATE=$(date +%Y-%m-%d)
+ echo -e "$NEW_VER $DATE\n\n- \n" > tmp_changelog && cat Changelog >> tmp_changelog && mv tmp_changelog Changelog
+ ```
+
+6. **Commit Version Bump**
+
+ ```bash
+ git add CURRENT_VERSION.txt Changelog
+ git commit -m "chore: bump version to $NEW_VER"
+ git push origin main
+ ```
diff --git a/.agent/workflows/local-preview.md b/.agent/workflows/local-preview.md
new file mode 100644
index 000000000..9fce3b729
--- /dev/null
+++ b/.agent/workflows/local-preview.md
@@ -0,0 +1,27 @@
+---
+description: Start a local PHP development server to preview the site.
+---
+# Local Preview Workflow
+
+1. Start the PHP built-in server in the background:
+// turbo
+
+```bash
+php -S localhost:8000 > /tmp/php_mt_site.log 2>&1 &
+```
+
+1. Wait for the server to initialize.
+
+2. Provide the user with the preview link:
+
+
+3. To stop the server:
+// turbo
+
+```bash
+pkill -f "php -S localhost:8000"
+```
+
+## β
Verification
+
+- Use `read_url_content` or `browser_subagent` to confirm the server is responsive.
diff --git a/.agent/workflows/release-preflight.md b/.agent/workflows/release-preflight.md
new file mode 100644
index 000000000..b04769ea6
--- /dev/null
+++ b/.agent/workflows/release-preflight.md
@@ -0,0 +1,39 @@
+---
+trigger: explicit_call
+description: Pre-flight checks before triggering a git-flow release
+category: tool
+---
+# Release Preflight Workflow (MT-site)
+
+## 1. Extract Versions
+
+```bash
+TXT_VER=$(cat CURRENT_VERSION.txt | tr -d '[:space:]')
+LOG_VER=$(head -n 1 Changelog | awk '{print $1}')
+```
+
+## 2. Validate Consistency
+
+```bash
+if [ "$LOG_VER" != "$TXT_VER" ]; then
+ echo "FAIL: Version Mismatch detected!"
+ echo "Txt: $TXT_VER"
+ echo "Changelog: $LOG_VER"
+ exit 1
+else
+ echo "SUCCESS: Versions match ($TXT_VER)."
+fi
+```
+
+## 3. Aesthetic Sanity Check
+
+- Run `/local-preview` and check for 404s.
+- Run `/visual-audit` and confirm "PASS".
+
+## 4. Documentation Sync
+
+- Run `/doc-sync` to ensure .agent/README.md is up-to-date.
+
+## 5. Proceed to Release
+
+If all checks pass, proceed with `/git-flow`.
diff --git a/.github/workflows/deploy-ionos.yml b/.github/workflows/deploy-ionos.yml
new file mode 100644
index 000000000..5480e18de
--- /dev/null
+++ b/.github/workflows/deploy-ionos.yml
@@ -0,0 +1,46 @@
+name: Deploy to IONOS
+
+# DΓ©clenche le workflow Γ chaque push sur la branche 'main'
+# Adaptez 'main' si votre branche principale a un autre nom (ex: master, prod)
+on:
+ push:
+ branches:
+ - gh-pages # ou master, prod, etc.
+
+jobs:
+ deploy:
+ name: Deploy PHP Site to IONOS
+ runs-on: ubuntu-latest # Utilise la dernière version d'Ubuntu disponible
+
+ steps:
+ - name: Checkout de votre code
+ uses: actions/checkout@v4 # Action pour rΓ©cupΓ©rer le code de votre dΓ©pΓ΄t
+
+ # Optionnel: Si vous avez des dΓ©pendances PHP gΓ©rΓ©es avec Composer
+ # Si votre site est purement statique ou PHP simple sans composer, cette Γ©tape n'est pas nΓ©cessaire.
+ # - name: Set up PHP
+ # uses: shivammathur/setup-php@v2
+ # with:
+ # php-version: '8.1' # SpΓ©cifiez votre version de PHP
+ # extensions: mbstring, xml, curl, zip # Ajoutez les extensions PHP nΓ©cessaires
+ # tools: composer # Si vous utilisez Composer
+
+ # Optionnel: Installer les dΓ©pendances Composer
+ # - name: Install Composer dependencies
+ # if: steps.setup-php.outputs.php-version != '' # S'exΓ©cute seulement si PHP est configurΓ©
+ # run: composer install --prefer-dist --no-progress --no-dev
+
+ - name: Deploy files via SFTP/FTP to IONOS
+ uses: wlixcc/SFTP-Deploy-Action@v1.2.6
+ with:
+ server: ${{ secrets.IONOS_FTP_SERVER }}
+ username: ${{ secrets.IONOS_FTP_USERNAME }}
+ password: ${{ secrets.IONOS_FTP_PASSWORD }}
+ sftp_only: true
+ port: ${{ secrets.IONOS_FTP_PORT || '22' }} # Utilise le port 22 par dΓ©faut pour SFTP si IONOS_FTP_PORT n'est pas dΓ©fini. Mettre '21' pour FTP.
+ local_path: ./ # Dossier local Γ tΓ©lΓ©verser. './' pour la racine du dΓ©pΓ΄t. Adaptez si votre site est dans un sous-dossier (ex: ./public/, ./dist/).
+ remote_path: ${{ secrets.IONOS_REMOTE_PATH }}/
+ rsyncArgs: "--delete --exclude=.git --exclude=.github --exclude=.agent"
+ sftpArgs: "-o ConnectTimeout=5"
+
+ # Note: IONOS might have cached DirectoryIndex. Explicitly check if index.html is gone.
diff --git a/.htaccess b/.htaccess
new file mode 100644
index 000000000..448c5db4a
--- /dev/null
+++ b/.htaccess
@@ -0,0 +1,35 @@
+# MySQLTuner Documentation Site - Apache Configuration
+
+# Set index.php as the primary entry point
+DirectoryIndex index.php index.html
+
+
+ RewriteEngine On
+ RewriteBase /
+
+ # 1. Redirect index.html to root /
+ RewriteRule ^index\.html$ / [R=301,L]
+
+ # 2. Redirect index.php to root / (only if no query string to avoid infinite loops)
+ RewriteCond %{THE_REQUEST} ^[A-Z]{3,9}\ /index\.php\ HTTP/
+ RewriteCond %{QUERY_STRING} ^$
+ RewriteRule ^index\.php$ / [R=301,L]
+
+ # 3. Handle Pretty URLs: map /pagename to index.php?p=pagename
+ RewriteCond %{REQUEST_FILENAME} !-d
+ RewriteCond %{REQUEST_FILENAME} !-f
+ RewriteRule ^([^/]+)/?$ index.php?p=$1 [L,QSA]
+
+
+# Prevent access to .git and other sensitive files (Safety)
+
+
+ Require all denied
+
+
+ Require all denied
+
+
+
+# Handle common asset paths
+# No specific rules needed for assets/ or public/ as they are real directories/files
diff --git a/CNAME b/CNAME
index 2318b83f8..4c6769fcf 100644
--- a/CNAME
+++ b/CNAME
@@ -1 +1 @@
-mysqltuner.com
+mysqltuner.lightpath.fr
\ No newline at end of file
diff --git a/CURRENT_VERSION.txt b/CURRENT_VERSION.txt
new file mode 100644
index 000000000..02754b2d6
--- /dev/null
+++ b/CURRENT_VERSION.txt
@@ -0,0 +1 @@
+2.8.33
\ No newline at end of file
diff --git a/Changelog b/Changelog
new file mode 100644
index 000000000..a549dc6ed
--- /dev/null
+++ b/Changelog
@@ -0,0 +1,38 @@
+2.8.33 2026-02-01
+
+- feat: synchronize site versioning with core script logic (2.8.33).
+- feat: implement robust Pretty URLs logic and routing in index.php.
+- feat: fix link variable shadowing in header/sidebar components.
+- feat: implement automated redirect from index.html to root.
+- chore: establish .agent governance and aesthetic verification.
+
+1.0.3 2026-02-01
+
+- feat: established project governance with 7-tier AFF structure.
+- feat: implemented local verification tools (`/local-preview`, `/visual-audit`).
+- fix: repaired broken image icons and badges in `overview.md`.
+
+1.0.2 2026-02-01
+
+- feat: migrated architecture to standalone PHP for zero-dependency portability.
+- feat: implemented server-side Markdown rendering using Parsedown.
+- feat: integrated official `mtlogo2.png` brand asset into header and sidebar.
+- refactor: extracted layout components into PHP includes (header, sidebar, footer).
+- chore: removed Node.js/Vite build artifacts and dependencies.
+- docs: updated specification to codify PHP architecture and brand assets.
+
+1.0.1 2026-02-01
+
+- feat: comprehensive modern redesign of the UI (Navy & Cyan technical aesthetic).
+- feat: implementation of glassmorphism effects on cards and sidebar.
+- feat: redesigned Hero section with dynamic typography and glow effects.
+- feat: enhanced mobile responsiveness and navigation logic.
+- feat: added smooth transitions and scroll-to-top on route changes.
+
+1.0.0 2026-02-01
+
+- feat: complete overhaul of the MySQLTuner website.
+- feat: implementation of a modern documentation hub with sidebar navigation.
+- feat: automated synchronization of docs from the perl repository.
+- feat: dynamic release notes archive and FAQ section.
+- feat: premium dark mode aesthetics and high-performance Vite build system.
diff --git a/assets/css/style.css b/assets/css/style.css
new file mode 100644
index 000000000..7ad8ee783
--- /dev/null
+++ b/assets/css/style.css
@@ -0,0 +1,530 @@
+@import url('https://fonts.googleapis.com/css2?family=Outfit:wght@400;600;800&display=swap');
+
+:root {
+ /* Colors - Sleek Technical Dark */
+ --bg-color: #020617;
+ --bg-card: rgba(15, 23, 42, 0.6);
+ --text-main: #f8fafc;
+ --text-muted: #94a3b8;
+ --accent-primary: #38bdf8;
+ /* Modern Cyan */
+ --accent-secondary: #818cf8;
+ /* Indigo */
+ --accent-success: #10b981;
+ /* Emerald */
+ --accent-glow: rgba(56, 189, 248, 0.15);
+
+ --border-color: rgba(255, 255, 255, 0.08);
+ --glass-bg: rgba(15, 23, 42, 0.8);
+
+ /* Typography */
+ --font-sans: 'Inter', system-ui, -apple-system, sans-serif;
+ --font-heading: 'Outfit', var(--font-sans);
+ --font-mono: 'Fira Code', monospace;
+
+ /* Spacing */
+ --section-padding: 6rem 1rem;
+ --card-radius: 16px;
+ --container-max: 1200px;
+}
+
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+
+html {
+ scroll-behavior: smooth;
+}
+
+body {
+ background-color: var(--bg-color);
+ color: var(--text-main);
+ font-family: var(--font-sans);
+ line-height: 1.6;
+ overflow-x: hidden;
+ -webkit-font-smoothing: antialiased;
+}
+
+/* Typography */
+h1,
+h2,
+h3,
+h4 {
+ font-family: var(--font-heading);
+ font-weight: 800;
+ letter-spacing: -0.02em;
+ margin-bottom: 1.5rem;
+ line-height: 1.1;
+}
+
+.gradient-text {
+ background: linear-gradient(135deg, #fff 30%, var(--accent-primary) 100%);
+ -webkit-background-clip: text;
+ background-clip: text;
+ -webkit-text-fill-color: transparent;
+}
+
+/* Layout Utilities */
+.container {
+ max-width: var(--container-max);
+ margin: 0 auto;
+ padding: 0 2rem;
+}
+
+.grid {
+ display: grid;
+ gap: 2.5rem;
+}
+
+.grid-3 {
+ grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
+}
+
+/* Glassmorphism */
+.glass {
+ background: var(--glass-bg);
+ backdrop-filter: blur(12px);
+ -webkit-backdrop-filter: blur(12px);
+ border: 1px solid var(--border-color);
+}
+
+/* Components */
+.btn {
+ display: inline-flex;
+ align-items: center;
+ padding: 0.875rem 2rem;
+ border-radius: 12px;
+ font-weight: 600;
+ text-decoration: none;
+ transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
+ cursor: pointer;
+ font-size: 0.95rem;
+ letter-spacing: 0.01em;
+}
+
+.btn-primary {
+ background: var(--accent-primary);
+ color: #020617;
+ box-shadow: 0 4px 20px -4px var(--accent-glow);
+}
+
+.btn-primary:hover {
+ transform: translateY(-2px) scale(1.02);
+ box-shadow: 0 8px 30px -4px var(--accent-glow);
+ filter: brightness(1.1);
+}
+
+.btn-outline {
+ border: 1px solid var(--border-color);
+ color: var(--text-main);
+ background: rgba(255, 255, 255, 0.03);
+}
+
+.btn-outline:hover {
+ background: rgba(255, 255, 255, 0.1);
+ border-color: var(--accent-primary);
+ transform: translateY(-2px);
+}
+
+/* Cards */
+.card {
+ background: var(--bg-card);
+ border-radius: var(--card-radius);
+ padding: 2.5rem;
+ border: 1px solid var(--border-color);
+ transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
+ position: relative;
+ overflow: hidden;
+}
+
+.card:hover {
+ transform: translateY(-8px);
+ border-color: rgba(56, 189, 248, 0.4);
+ box-shadow: 0 20px 40px -20px rgba(0, 0, 0, 0.5);
+}
+
+.card::before {
+ content: '';
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ background: radial-gradient(circle at top right, rgba(56, 189, 248, 0.1), transparent 50%);
+ opacity: 0;
+ transition: opacity 0.4s;
+}
+
+.card:hover::before {
+ opacity: 1;
+}
+
+/* Hero bg */
+.hero {
+ position: relative;
+ min-height: 90vh;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+ overflow: hidden;
+}
+
+.hero-bg {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ height: 100%;
+ z-index: -1;
+ background: radial-gradient(circle at 50% -20%, rgba(56, 189, 248, 0.15), transparent 70%),
+ radial-gradient(circle at bottom right, rgba(129, 140, 248, 0.1), transparent 50%);
+}
+
+.hero-glow {
+ position: absolute;
+ top: -10%;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 80%;
+ height: 40%;
+ background: var(--accent-primary);
+ filter: blur(150px);
+ opacity: 0.1;
+ pointer-events: none;
+}
+
+/* Sections */
+section {
+ padding: var(--section-padding);
+}
+
+/* Documentation Content Styling */
+.doc-content {
+ color: var(--text-main);
+ line-height: 1.8;
+}
+
+.doc-content h1 {
+ font-size: clamp(2.5rem, 5vw, 3.5rem);
+ color: #fff;
+ margin-bottom: 2rem;
+}
+
+.doc-content h2 {
+ font-size: 1.875rem;
+ margin-top: 4rem;
+ margin-bottom: 1.5rem;
+ padding-bottom: 0.75rem;
+ border-bottom: 1px solid var(--border-color);
+}
+
+.doc-content h3 {
+ font-size: 1.5rem;
+ margin-top: 3rem;
+ color: var(--accent-primary);
+}
+
+.doc-content p {
+ margin-bottom: 1.5rem;
+ color: var(--text-muted);
+ font-size: 1.1rem;
+}
+
+.doc-content ul,
+.doc-content ol {
+ margin-bottom: 1.5rem;
+ padding-left: 1.5rem;
+ color: var(--text-muted);
+}
+
+.doc-content li {
+ margin-bottom: 0.75rem;
+}
+
+.doc-content code {
+ background: rgba(255, 255, 255, 0.08);
+ padding: 0.2rem 0.5rem;
+ border-radius: 6px;
+ font-family: var(--font-mono);
+ font-size: 0.9em;
+ color: var(--accent-primary);
+}
+
+.doc-content pre {
+ background: #0b1120;
+ padding: 1.75rem;
+ border-radius: 12px;
+ margin: 2rem 0;
+ overflow-x: auto;
+ border: 1px solid var(--border-color);
+ box-shadow: inset 0 2px 4px rgba(0, 0, 0, 0.3);
+}
+
+.doc-content pre code {
+ background: transparent;
+ padding: 0;
+ color: #e2e8f0;
+}
+
+.doc-content a {
+ color: var(--accent-primary);
+ text-decoration: none;
+ border-bottom: 1px solid transparent;
+ transition: border-color 0.2s;
+}
+
+.doc-content a:hover {
+ border-color: var(--accent-primary);
+}
+
+/* Responsive fixes */
+@media (max-width: 768px) {
+ :root {
+ --section-padding: 4rem 1rem;
+ }
+
+ .container {
+ padding: 0 1.5rem;
+ }
+}
+
+/* Animation */
+@keyframes fadeIn {
+ from {
+ opacity: 0;
+ transform: translateY(20px);
+ }
+
+ to {
+ opacity: 1;
+ transform: translateY(0);
+ }
+}
+
+.fade-in {
+ animation: fadeIn 0.8s cubic-bezier(0.4, 0, 0.2, 1) forwards;
+}
+
+/* Navigation & Branding */
+.top-nav {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 2rem 0;
+ position: absolute;
+ top: 0;
+ left: 50%;
+ transform: translateX(-50%);
+ width: 100%;
+ z-index: 10;
+}
+
+.logo-wrap {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+}
+
+.nav-actions {
+ display: flex;
+ gap: 1.5rem;
+}
+
+/* Badge */
+.badge {
+ display: inline-block;
+ padding: 0.4rem 1rem;
+ background: rgba(56, 189, 248, 0.1);
+ border: 1px solid rgba(56, 189, 248, 0.2);
+ border-radius: 100px;
+ color: var(--accent-primary);
+ font-size: 0.8rem;
+ font-weight: 700;
+ text-transform: uppercase;
+ letter-spacing: 0.05em;
+ margin-bottom: 2rem;
+}
+
+/* Card Improvements */
+.card-icon {
+ font-size: 2.5rem;
+ margin-bottom: 1.5rem;
+ display: inline-block;
+}
+
+/* Footer Adjustments */
+footer p {
+ opacity: 0.6;
+}
+
+/* Doc Page Layout Adjustments */
+body.is-docs .hero {
+ display: none;
+}
+
+body.is-docs #home-view {
+ display: none;
+}
+
+body.is-docs #doc-view {
+ display: block;
+}.sidebar {
+ width: 280px;
+ height: 100vh;
+ position: sticky;
+ top: 0;
+ padding: 3rem 1.5rem;
+ overflow-y: auto;
+ display: none;
+ z-index: 50;
+ transition: all 0.3s ease;
+}
+
+body.is-docs .sidebar {
+ display: flex;
+ flex-direction: column;
+}
+
+body.is-docs #app {
+ display: flex;
+}
+
+.sidebar-brand {
+ display: flex;
+ align-items: center;
+ gap: 1rem;
+ margin-bottom: 3.5rem;
+ text-decoration: none;
+ color: var(--text-main);
+}
+
+.logo-icon {
+ width: 32px;
+ height: 32px;
+ background: var(--accent-primary);
+ border-radius: 8px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ color: #020617;
+ font-weight: 800;
+ font-family: var(--font-heading);
+}
+
+.sidebar-brand span {
+ font-family: var(--font-heading);
+ font-weight: 800;
+ font-size: 1.1rem;
+}
+
+.sidebar-nav {
+ list-style: none;
+ flex: 1;
+}
+
+.nav-group {
+ margin-bottom: 2.5rem;
+}
+
+.nav-group-title {
+ font-size: 0.7rem;
+ text-transform: uppercase;
+ color: var(--text-muted);
+ letter-spacing: 0.1em;
+ margin-bottom: 1rem;
+ font-weight: 700;
+ opacity: 0.8;
+}
+
+.nav-item {
+ margin-bottom: 0.4rem;
+}
+
+.nav-link {
+ display: block;
+ padding: 0.6rem 1rem;
+ border-radius: 10px;
+ color: var(--text-muted);
+ text-decoration: none;
+ font-size: 0.9rem;
+ font-weight: 500;
+ transition: all 0.2s cubic-bezier(0.4, 0, 0.2, 1);
+ border: 1px solid transparent;
+}
+
+.nav-link:hover {
+ color: var(--text-main);
+ background: rgba(255, 255, 255, 0.05);
+ transform: translateX(4px);
+}
+
+.nav-link.active {
+ color: var(--accent-primary);
+ background: rgba(56, 189, 248, 0.08);
+ border-color: rgba(56, 189, 248, 0.2);
+ font-weight: 600;
+}
+
+.sidebar-footer {
+ margin-top: auto;
+ padding-top: 2rem;
+ border-top: 1px solid var(--border-color);
+}
+
+.github-link {
+ display: flex;
+ align-items: center;
+ gap: 0.75rem;
+ color: var(--text-muted);
+ text-decoration: none;
+ font-size: 0.875rem;
+ transition: color 0.2s;
+}
+
+.github-link:hover {
+ color: var(--text-main);
+}
+
+/* Mobile Toggle */
+.mobile-nav-toggle {
+ position: fixed;
+ bottom: 2rem;
+ right: 2rem;
+ z-index: 100;
+ background: var(--accent-primary);
+ color: #020617;
+ width: 3.5rem;
+ height: 3.5rem;
+ border-radius: 16px;
+ display: none;
+ align-items: center;
+ justify-content: center;
+ box-shadow: 0 10px 25px -5px var(--accent-glow);
+ cursor: pointer;
+ transition: transform 0.2s;
+}
+
+.mobile-nav-toggle:active {
+ transform: scale(0.9);
+}
+
+@media (max-width: 768px) {
+ .sidebar {
+ position: fixed;
+ left: -280px;
+ height: 100vh;
+ display: flex !important;
+ /* Force flex for mobile interaction */
+ }
+
+ .sidebar.open {
+ transform: translateX(280px);
+ box-shadow: 20px 0 50px rgba(0, 0, 0, 0.5);
+ }
+
+ body.is-docs .mobile-nav-toggle {
+ display: flex;
+ }
+}
\ No newline at end of file
diff --git a/assets/img/hero-bg.png b/assets/img/hero-bg.png
new file mode 100644
index 000000000..213af10d6
Binary files /dev/null and b/assets/img/hero-bg.png differ
diff --git a/assets/img/logo.png b/assets/img/logo.png
new file mode 100644
index 000000000..92b4a234f
Binary files /dev/null and b/assets/img/logo.png differ
diff --git a/assets/img/mtlogo2.png b/assets/img/mtlogo2.png
new file mode 100644
index 000000000..92b4a234f
Binary files /dev/null and b/assets/img/mtlogo2.png differ
diff --git a/assets/js/app.js b/assets/js/app.js
new file mode 100644
index 000000000..dd300ca43
--- /dev/null
+++ b/assets/js/app.js
@@ -0,0 +1,30 @@
+document.addEventListener('DOMContentLoaded', () => {
+ console.log('MySQLTuner Website Initialized (PHP Version)');
+
+ // Sidebar toggle for mobile
+ const toggle = document.getElementById('sidebar-toggle');
+ const sidebar = document.querySelector('.sidebar');
+
+ if (toggle && sidebar) {
+ toggle.addEventListener('click', () => {
+ sidebar.classList.toggle('open');
+ });
+ }
+
+ // Auto-close sidebar on mobile after clicking a link
+ document.querySelectorAll('.nav-link').forEach(link => {
+ link.addEventListener('click', () => {
+ if (sidebar) sidebar.classList.remove('open');
+ });
+ });
+
+ // Smooth scroll for anchors
+ document.querySelectorAll('a[href^="#"]').forEach(anchor => {
+ anchor.addEventListener('click', function (e) {
+ e.preventDefault();
+ document.querySelector(this.getAttribute('href')).scrollIntoView({
+ behavior: 'smooth'
+ });
+ });
+ });
+});
diff --git a/includes/Parsedown.php b/includes/Parsedown.php
new file mode 100644
index 000000000..38edfe92e
--- /dev/null
+++ b/includes/Parsedown.php
@@ -0,0 +1,1994 @@
+textElements($text);
+
+ # convert to markup
+ $markup = $this->elements($Elements);
+
+ # trim line breaks
+ $markup = trim($markup, "\n");
+
+ return $markup;
+ }
+
+ protected function textElements($text)
+ {
+ # make sure no definitions are set
+ $this->DefinitionData = array();
+
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ # remove surrounding line breaks
+ $text = trim($text, "\n");
+
+ # split text into lines
+ $lines = explode("\n", $text);
+
+ # iterate through lines to identify blocks
+ return $this->linesElements($lines);
+ }
+
+ #
+ # Setters
+ #
+
+ function setBreaksEnabled($breaksEnabled)
+ {
+ $this->breaksEnabled = $breaksEnabled;
+
+ return $this;
+ }
+
+ protected $breaksEnabled;
+
+ function setMarkupEscaped($markupEscaped)
+ {
+ $this->markupEscaped = $markupEscaped;
+
+ return $this;
+ }
+
+ protected $markupEscaped;
+
+ function setUrlsLinked($urlsLinked)
+ {
+ $this->urlsLinked = $urlsLinked;
+
+ return $this;
+ }
+
+ protected $urlsLinked = true;
+
+ function setSafeMode($safeMode)
+ {
+ $this->safeMode = (bool) $safeMode;
+
+ return $this;
+ }
+
+ protected $safeMode;
+
+ function setStrictMode($strictMode)
+ {
+ $this->strictMode = (bool) $strictMode;
+
+ return $this;
+ }
+
+ protected $strictMode;
+
+ protected $safeLinksWhitelist = array(
+ 'http://',
+ 'https://',
+ 'ftp://',
+ 'ftps://',
+ 'mailto:',
+ 'tel:',
+ 'data:image/png;base64,',
+ 'data:image/gif;base64,',
+ 'data:image/jpeg;base64,',
+ 'irc:',
+ 'ircs:',
+ 'git:',
+ 'ssh:',
+ 'news:',
+ 'steam:',
+ );
+
+ #
+ # Lines
+ #
+
+ protected $BlockTypes = array(
+ '#' => array('Header'),
+ '*' => array('Rule', 'List'),
+ '+' => array('List'),
+ '-' => array('SetextHeader', 'Table', 'Rule', 'List'),
+ '0' => array('List'),
+ '1' => array('List'),
+ '2' => array('List'),
+ '3' => array('List'),
+ '4' => array('List'),
+ '5' => array('List'),
+ '6' => array('List'),
+ '7' => array('List'),
+ '8' => array('List'),
+ '9' => array('List'),
+ ':' => array('Table'),
+ '<' => array('Comment', 'Markup'),
+ '=' => array('SetextHeader'),
+ '>' => array('Quote'),
+ '[' => array('Reference'),
+ '_' => array('Rule'),
+ '`' => array('FencedCode'),
+ '|' => array('Table'),
+ '~' => array('FencedCode'),
+ );
+
+ # ~
+
+ protected $unmarkedBlockTypes = array(
+ 'Code',
+ );
+
+ #
+ # Blocks
+ #
+
+ protected function lines(array $lines)
+ {
+ return $this->elements($this->linesElements($lines));
+ }
+
+ protected function linesElements(array $lines)
+ {
+ $Elements = array();
+ $CurrentBlock = null;
+
+ foreach ($lines as $line)
+ {
+ if (chop($line) === '')
+ {
+ if (isset($CurrentBlock))
+ {
+ $CurrentBlock['interrupted'] = (isset($CurrentBlock['interrupted'])
+ ? $CurrentBlock['interrupted'] + 1 : 1
+ );
+ }
+
+ continue;
+ }
+
+ while (($beforeTab = strstr($line, "\t", true)) !== false)
+ {
+ $shortage = 4 - mb_strlen($beforeTab, 'utf-8') % 4;
+
+ $line = $beforeTab
+ . str_repeat(' ', $shortage)
+ . substr($line, strlen($beforeTab) + 1)
+ ;
+ }
+
+ $indent = strspn($line, ' ');
+
+ $text = $indent > 0 ? substr($line, $indent) : $line;
+
+ # ~
+
+ $Line = array('body' => $line, 'indent' => $indent, 'text' => $text);
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']))
+ {
+ $methodName = 'block' . $CurrentBlock['type'] . 'Continue';
+ $Block = $this->$methodName($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $CurrentBlock = $Block;
+
+ continue;
+ }
+ else
+ {
+ if ($this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
+ $CurrentBlock = $this->$methodName($CurrentBlock);
+ }
+ }
+ }
+
+ # ~
+
+ $marker = $text[0];
+
+ # ~
+
+ $blockTypes = $this->unmarkedBlockTypes;
+
+ if (isset($this->BlockTypes[$marker]))
+ {
+ foreach ($this->BlockTypes[$marker] as $blockType)
+ {
+ $blockTypes []= $blockType;
+ }
+ }
+
+ #
+ # ~
+
+ foreach ($blockTypes as $blockType)
+ {
+ $Block = $this->{"block$blockType"}($Line, $CurrentBlock);
+
+ if (isset($Block))
+ {
+ $Block['type'] = $blockType;
+
+ if ( ! isset($Block['identified']))
+ {
+ if (isset($CurrentBlock))
+ {
+ $Elements[] = $this->extractElement($CurrentBlock);
+ }
+
+ $Block['identified'] = true;
+ }
+
+ if ($this->isBlockContinuable($blockType))
+ {
+ $Block['continuable'] = true;
+ }
+
+ $CurrentBlock = $Block;
+
+ continue 2;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock) and $CurrentBlock['type'] === 'Paragraph')
+ {
+ $Block = $this->paragraphContinue($Line, $CurrentBlock);
+ }
+
+ if (isset($Block))
+ {
+ $CurrentBlock = $Block;
+ }
+ else
+ {
+ if (isset($CurrentBlock))
+ {
+ $Elements[] = $this->extractElement($CurrentBlock);
+ }
+
+ $CurrentBlock = $this->paragraph($Line);
+
+ $CurrentBlock['identified'] = true;
+ }
+ }
+
+ # ~
+
+ if (isset($CurrentBlock['continuable']) and $this->isBlockCompletable($CurrentBlock['type']))
+ {
+ $methodName = 'block' . $CurrentBlock['type'] . 'Complete';
+ $CurrentBlock = $this->$methodName($CurrentBlock);
+ }
+
+ # ~
+
+ if (isset($CurrentBlock))
+ {
+ $Elements[] = $this->extractElement($CurrentBlock);
+ }
+
+ # ~
+
+ return $Elements;
+ }
+
+ protected function extractElement(array $Component)
+ {
+ if ( ! isset($Component['element']))
+ {
+ if (isset($Component['markup']))
+ {
+ $Component['element'] = array('rawHtml' => $Component['markup']);
+ }
+ elseif (isset($Component['hidden']))
+ {
+ $Component['element'] = array();
+ }
+ }
+
+ return $Component['element'];
+ }
+
+ protected function isBlockContinuable($Type)
+ {
+ return method_exists($this, 'block' . $Type . 'Continue');
+ }
+
+ protected function isBlockCompletable($Type)
+ {
+ return method_exists($this, 'block' . $Type . 'Complete');
+ }
+
+ #
+ # Code
+
+ protected function blockCode($Line, $Block = null)
+ {
+ if (isset($Block) and $Block['type'] === 'Paragraph' and ! isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['indent'] >= 4)
+ {
+ $text = substr($Line['body'], 4);
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'pre',
+ 'element' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeContinue($Line, $Block)
+ {
+ if ($Line['indent'] >= 4)
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
+
+ unset($Block['interrupted']);
+ }
+
+ $Block['element']['element']['text'] .= "\n";
+
+ $text = substr($Line['body'], 4);
+
+ $Block['element']['element']['text'] .= $text;
+
+ return $Block;
+ }
+ }
+
+ protected function blockCodeComplete($Block)
+ {
+ return $Block;
+ }
+
+ #
+ # Comment
+
+ protected function blockComment($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (strpos($Line['text'], '') !== false)
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+ }
+
+ protected function blockCommentContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']))
+ {
+ return;
+ }
+
+ $Block['element']['rawHtml'] .= "\n" . $Line['body'];
+
+ if (strpos($Line['text'], '-->') !== false)
+ {
+ $Block['closed'] = true;
+ }
+
+ return $Block;
+ }
+
+ #
+ # Fenced Code
+
+ protected function blockFencedCode($Line)
+ {
+ $marker = $Line['text'][0];
+
+ $openerLength = strspn($Line['text'], $marker);
+
+ if ($openerLength < 3)
+ {
+ return;
+ }
+
+ $infostring = trim(substr($Line['text'], $openerLength), "\t ");
+
+ if (strpos($infostring, '`') !== false)
+ {
+ return;
+ }
+
+ $Element = array(
+ 'name' => 'code',
+ 'text' => '',
+ );
+
+ if ($infostring !== '')
+ {
+ /**
+ * https://www.w3.org/TR/2011/WD-html5-20110525/elements.html#classes
+ * Every HTML element may have a class attribute specified.
+ * The attribute, if specified, must have a value that is a set
+ * of space-separated tokens representing the various classes
+ * that the element belongs to.
+ * [...]
+ * The space characters, for the purposes of this specification,
+ * are U+0020 SPACE, U+0009 CHARACTER TABULATION (tab),
+ * U+000A LINE FEED (LF), U+000C FORM FEED (FF), and
+ * U+000D CARRIAGE RETURN (CR).
+ */
+ $language = substr($infostring, 0, strcspn($infostring, " \t\n\f\r"));
+
+ $Element['attributes'] = array('class' => "language-$language");
+ }
+
+ $Block = array(
+ 'char' => $marker,
+ 'openerLength' => $openerLength,
+ 'element' => array(
+ 'name' => 'pre',
+ 'element' => $Element,
+ ),
+ );
+
+ return $Block;
+ }
+
+ protected function blockFencedCodeContinue($Line, $Block)
+ {
+ if (isset($Block['complete']))
+ {
+ return;
+ }
+
+ if (isset($Block['interrupted']))
+ {
+ $Block['element']['element']['text'] .= str_repeat("\n", $Block['interrupted']);
+
+ unset($Block['interrupted']);
+ }
+
+ if (($len = strspn($Line['text'], $Block['char'])) >= $Block['openerLength']
+ and chop(substr($Line['text'], $len), ' ') === ''
+ ) {
+ $Block['element']['element']['text'] = substr($Block['element']['element']['text'], 1);
+
+ $Block['complete'] = true;
+
+ return $Block;
+ }
+
+ $Block['element']['element']['text'] .= "\n" . $Line['body'];
+
+ return $Block;
+ }
+
+ protected function blockFencedCodeComplete($Block)
+ {
+ return $Block;
+ }
+
+ #
+ # Header
+
+ protected function blockHeader($Line)
+ {
+ $level = strspn($Line['text'], '#');
+
+ if ($level > 6)
+ {
+ return;
+ }
+
+ $text = trim($Line['text'], '#');
+
+ if ($this->strictMode and isset($text[0]) and $text[0] !== ' ')
+ {
+ return;
+ }
+
+ $text = trim($text, ' ');
+
+ $Block = array(
+ 'element' => array(
+ 'name' => 'h' . $level,
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $text,
+ 'destination' => 'elements',
+ )
+ ),
+ );
+
+ return $Block;
+ }
+
+ #
+ # List
+
+ protected function blockList($Line, ?array $CurrentBlock = null)
+ {
+ list($name, $pattern) = $Line['text'][0] <= '-' ? array('ul', '[*+-]') : array('ol', '[0-9]{1,9}+[.\)]');
+
+ if (preg_match('/^('.$pattern.'([ ]++|$))(.*+)/', $Line['text'], $matches))
+ {
+ $contentIndent = strlen($matches[2]);
+
+ if ($contentIndent >= 5)
+ {
+ $contentIndent -= 1;
+ $matches[1] = substr($matches[1], 0, -$contentIndent);
+ $matches[3] = str_repeat(' ', $contentIndent) . $matches[3];
+ }
+ elseif ($contentIndent === 0)
+ {
+ $matches[1] .= ' ';
+ }
+
+ $markerWithoutWhitespace = strstr($matches[1], ' ', true);
+
+ $Block = array(
+ 'indent' => $Line['indent'],
+ 'pattern' => $pattern,
+ 'data' => array(
+ 'type' => $name,
+ 'marker' => $matches[1],
+ 'markerType' => ($name === 'ul' ? $markerWithoutWhitespace : substr($markerWithoutWhitespace, -1)),
+ ),
+ 'element' => array(
+ 'name' => $name,
+ 'elements' => array(),
+ ),
+ );
+ $Block['data']['markerTypeRegex'] = preg_quote($Block['data']['markerType'], '/');
+
+ if ($name === 'ol')
+ {
+ $listStart = ltrim(strstr($matches[1], $Block['data']['markerType'], true), '0') ?: '0';
+
+ if ($listStart !== '1')
+ {
+ if (
+ isset($CurrentBlock)
+ and $CurrentBlock['type'] === 'Paragraph'
+ and ! isset($CurrentBlock['interrupted'])
+ ) {
+ return;
+ }
+
+ $Block['element']['attributes'] = array('start' => $listStart);
+ }
+ }
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => array(
+ 'function' => 'li',
+ 'argument' => !empty($matches[3]) ? array($matches[3]) : array(),
+ 'destination' => 'elements'
+ )
+ );
+
+ $Block['element']['elements'] []= & $Block['li'];
+
+ return $Block;
+ }
+ }
+
+ protected function blockListContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']) and empty($Block['li']['handler']['argument']))
+ {
+ return null;
+ }
+
+ $requiredIndent = ($Block['indent'] + strlen($Block['data']['marker']));
+
+ if ($Line['indent'] < $requiredIndent
+ and (
+ (
+ $Block['data']['type'] === 'ol'
+ and preg_match('/^[0-9]++'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
+ ) or (
+ $Block['data']['type'] === 'ul'
+ and preg_match('/^'.$Block['data']['markerTypeRegex'].'(?:[ ]++(.*)|$)/', $Line['text'], $matches)
+ )
+ )
+ ) {
+ if (isset($Block['interrupted']))
+ {
+ $Block['li']['handler']['argument'] []= '';
+
+ $Block['loose'] = true;
+
+ unset($Block['interrupted']);
+ }
+
+ unset($Block['li']);
+
+ $text = isset($matches[1]) ? $matches[1] : '';
+
+ $Block['indent'] = $Line['indent'];
+
+ $Block['li'] = array(
+ 'name' => 'li',
+ 'handler' => array(
+ 'function' => 'li',
+ 'argument' => array($text),
+ 'destination' => 'elements'
+ )
+ );
+
+ $Block['element']['elements'] []= & $Block['li'];
+
+ return $Block;
+ }
+ elseif ($Line['indent'] < $requiredIndent and $this->blockList($Line))
+ {
+ return null;
+ }
+
+ if ($Line['text'][0] === '[' and $this->blockReference($Line))
+ {
+ return $Block;
+ }
+
+ if ($Line['indent'] >= $requiredIndent)
+ {
+ if (isset($Block['interrupted']))
+ {
+ $Block['li']['handler']['argument'] []= '';
+
+ $Block['loose'] = true;
+
+ unset($Block['interrupted']);
+ }
+
+ $text = substr($Line['body'], $requiredIndent);
+
+ $Block['li']['handler']['argument'] []= $text;
+
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $text = preg_replace('/^[ ]{0,'.$requiredIndent.'}+/', '', $Line['body']);
+
+ $Block['li']['handler']['argument'] []= $text;
+
+ return $Block;
+ }
+ }
+
+ protected function blockListComplete(array $Block)
+ {
+ if (isset($Block['loose']))
+ {
+ foreach ($Block['element']['elements'] as &$li)
+ {
+ if (end($li['handler']['argument']) !== '')
+ {
+ $li['handler']['argument'] []= '';
+ }
+ }
+ }
+
+ return $Block;
+ }
+
+ #
+ # Quote
+
+ protected function blockQuote($Line)
+ {
+ if (preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'blockquote',
+ 'handler' => array(
+ 'function' => 'linesElements',
+ 'argument' => (array) $matches[1],
+ 'destination' => 'elements',
+ )
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockQuoteContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['text'][0] === '>' and preg_match('/^>[ ]?+(.*+)/', $Line['text'], $matches))
+ {
+ $Block['element']['handler']['argument'] []= $matches[1];
+
+ return $Block;
+ }
+
+ if ( ! isset($Block['interrupted']))
+ {
+ $Block['element']['handler']['argument'] []= $Line['text'];
+
+ return $Block;
+ }
+ }
+
+ #
+ # Rule
+
+ protected function blockRule($Line)
+ {
+ $marker = $Line['text'][0];
+
+ if (substr_count($Line['text'], $marker) >= 3 and chop($Line['text'], " $marker") === '')
+ {
+ $Block = array(
+ 'element' => array(
+ 'name' => 'hr',
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Setext
+
+ protected function blockSetextHeader($Line, ?array $Block = null)
+ {
+ if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if ($Line['indent'] < 4 and chop(chop($Line['text'], ' '), $Line['text'][0]) === '')
+ {
+ $Block['element']['name'] = $Line['text'][0] === '=' ? 'h1' : 'h2';
+
+ return $Block;
+ }
+ }
+
+ #
+ # Markup
+
+ protected function blockMarkup($Line)
+ {
+ if ($this->markupEscaped or $this->safeMode)
+ {
+ return;
+ }
+
+ if (preg_match('/^<[\/]?+(\w*)(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+(\/)?>/', $Line['text'], $matches))
+ {
+ $element = strtolower($matches[1]);
+
+ if (in_array($element, $this->textLevelElements))
+ {
+ return;
+ }
+
+ $Block = array(
+ 'name' => $matches[1],
+ 'element' => array(
+ 'rawHtml' => $Line['text'],
+ 'autobreak' => true,
+ ),
+ );
+
+ return $Block;
+ }
+ }
+
+ protected function blockMarkupContinue($Line, array $Block)
+ {
+ if (isset($Block['closed']) or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ $Block['element']['rawHtml'] .= "\n" . $Line['body'];
+
+ return $Block;
+ }
+
+ #
+ # Reference
+
+ protected function blockReference($Line)
+ {
+ if (strpos($Line['text'], ']') !== false
+ and preg_match('/^\[(.+?)\]:[ ]*+(\S+?)>?(?:[ ]+["\'(](.+)["\')])?[ ]*+$/', $Line['text'], $matches)
+ ) {
+ $id = strtolower($matches[1]);
+
+ $Data = array(
+ 'url' => $matches[2],
+ 'title' => isset($matches[3]) ? $matches[3] : null,
+ );
+
+ $this->DefinitionData['Reference'][$id] = $Data;
+
+ $Block = array(
+ 'element' => array(),
+ );
+
+ return $Block;
+ }
+ }
+
+ #
+ # Table
+
+ protected function blockTable($Line, ?array $Block = null)
+ {
+ if ( ! isset($Block) or $Block['type'] !== 'Paragraph' or isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (
+ strpos($Block['element']['handler']['argument'], '|') === false
+ and strpos($Line['text'], '|') === false
+ and strpos($Line['text'], ':') === false
+ or strpos($Block['element']['handler']['argument'], "\n") !== false
+ ) {
+ return;
+ }
+
+ if (chop($Line['text'], ' -:|') !== '')
+ {
+ return;
+ }
+
+ $alignments = array();
+
+ $divider = $Line['text'];
+
+ $divider = trim($divider);
+ $divider = trim($divider, '|');
+
+ $dividerCells = explode('|', $divider);
+
+ foreach ($dividerCells as $dividerCell)
+ {
+ $dividerCell = trim($dividerCell);
+
+ if ($dividerCell === '')
+ {
+ return;
+ }
+
+ $alignment = null;
+
+ if ($dividerCell[0] === ':')
+ {
+ $alignment = 'left';
+ }
+
+ if (substr($dividerCell, - 1) === ':')
+ {
+ $alignment = $alignment === 'left' ? 'center' : 'right';
+ }
+
+ $alignments []= $alignment;
+ }
+
+ # ~
+
+ $HeaderElements = array();
+
+ $header = $Block['element']['handler']['argument'];
+
+ $header = trim($header);
+ $header = trim($header, '|');
+
+ $headerCells = explode('|', $header);
+
+ if (count($headerCells) !== count($alignments))
+ {
+ return;
+ }
+
+ foreach ($headerCells as $index => $headerCell)
+ {
+ $headerCell = trim($headerCell);
+
+ $HeaderElement = array(
+ 'name' => 'th',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $headerCell,
+ 'destination' => 'elements',
+ )
+ );
+
+ if (isset($alignments[$index]))
+ {
+ $alignment = $alignments[$index];
+
+ $HeaderElement['attributes'] = array(
+ 'style' => "text-align: $alignment;",
+ );
+ }
+
+ $HeaderElements []= $HeaderElement;
+ }
+
+ # ~
+
+ $Block = array(
+ 'alignments' => $alignments,
+ 'identified' => true,
+ 'element' => array(
+ 'name' => 'table',
+ 'elements' => array(),
+ ),
+ );
+
+ $Block['element']['elements'] []= array(
+ 'name' => 'thead',
+ );
+
+ $Block['element']['elements'] []= array(
+ 'name' => 'tbody',
+ 'elements' => array(),
+ );
+
+ $Block['element']['elements'][0]['elements'] []= array(
+ 'name' => 'tr',
+ 'elements' => $HeaderElements,
+ );
+
+ return $Block;
+ }
+
+ protected function blockTableContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ if (count($Block['alignments']) === 1 or $Line['text'][0] === '|' or strpos($Line['text'], '|'))
+ {
+ $Elements = array();
+
+ $row = $Line['text'];
+
+ $row = trim($row);
+ $row = trim($row, '|');
+
+ preg_match_all('/(?:(\\\\[|])|[^|`]|`[^`]++`|`)++/', $row, $matches);
+
+ $cells = array_slice($matches[0], 0, count($Block['alignments']));
+
+ foreach ($cells as $index => $cell)
+ {
+ $cell = trim($cell);
+
+ $Element = array(
+ 'name' => 'td',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $cell,
+ 'destination' => 'elements',
+ )
+ );
+
+ if (isset($Block['alignments'][$index]))
+ {
+ $Element['attributes'] = array(
+ 'style' => 'text-align: ' . $Block['alignments'][$index] . ';',
+ );
+ }
+
+ $Elements []= $Element;
+ }
+
+ $Element = array(
+ 'name' => 'tr',
+ 'elements' => $Elements,
+ );
+
+ $Block['element']['elements'][1]['elements'] []= $Element;
+
+ return $Block;
+ }
+ }
+
+ #
+ # ~
+ #
+
+ protected function paragraph($Line)
+ {
+ return array(
+ 'type' => 'Paragraph',
+ 'element' => array(
+ 'name' => 'p',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $Line['text'],
+ 'destination' => 'elements',
+ ),
+ ),
+ );
+ }
+
+ protected function paragraphContinue($Line, array $Block)
+ {
+ if (isset($Block['interrupted']))
+ {
+ return;
+ }
+
+ $Block['element']['handler']['argument'] .= "\n".$Line['text'];
+
+ return $Block;
+ }
+
+ #
+ # Inline Elements
+ #
+
+ protected $InlineTypes = array(
+ '!' => array('Image'),
+ '&' => array('SpecialCharacter'),
+ '*' => array('Emphasis'),
+ ':' => array('Url'),
+ '<' => array('UrlTag', 'EmailTag', 'Markup'),
+ '[' => array('Link'),
+ '_' => array('Emphasis'),
+ '`' => array('Code'),
+ '~' => array('Strikethrough'),
+ '\\' => array('EscapeSequence'),
+ );
+
+ # ~
+
+ protected $inlineMarkerList = '!*_&[:<`~\\';
+
+ #
+ # ~
+ #
+
+ public function line($text, $nonNestables = array())
+ {
+ return $this->elements($this->lineElements($text, $nonNestables));
+ }
+
+ protected function lineElements($text, $nonNestables = array())
+ {
+ # standardize line breaks
+ $text = str_replace(array("\r\n", "\r"), "\n", $text);
+
+ $Elements = array();
+
+ $nonNestables = (empty($nonNestables)
+ ? array()
+ : array_combine($nonNestables, $nonNestables)
+ );
+
+ # $excerpt is based on the first occurrence of a marker
+
+ while ($excerpt = strpbrk($text, $this->inlineMarkerList))
+ {
+ $marker = $excerpt[0];
+
+ $markerPosition = strlen($text) - strlen($excerpt);
+
+ $Excerpt = array('text' => $excerpt, 'context' => $text);
+
+ foreach ($this->InlineTypes[$marker] as $inlineType)
+ {
+ # check to see if the current inline type is nestable in the current context
+
+ if (isset($nonNestables[$inlineType]))
+ {
+ continue;
+ }
+
+ $Inline = $this->{"inline$inlineType"}($Excerpt);
+
+ if ( ! isset($Inline))
+ {
+ continue;
+ }
+
+ # makes sure that the inline belongs to "our" marker
+
+ if (isset($Inline['position']) and $Inline['position'] > $markerPosition)
+ {
+ continue;
+ }
+
+ # sets a default inline position
+
+ if ( ! isset($Inline['position']))
+ {
+ $Inline['position'] = $markerPosition;
+ }
+
+ # cause the new element to 'inherit' our non nestables
+
+
+ $Inline['element']['nonNestables'] = isset($Inline['element']['nonNestables'])
+ ? array_merge($Inline['element']['nonNestables'], $nonNestables)
+ : $nonNestables
+ ;
+
+ # the text that comes before the inline
+ $unmarkedText = substr($text, 0, $Inline['position']);
+
+ # compile the unmarked text
+ $InlineText = $this->inlineText($unmarkedText);
+ $Elements[] = $InlineText['element'];
+
+ # compile the inline
+ $Elements[] = $this->extractElement($Inline);
+
+ # remove the examined text
+ $text = substr($text, $Inline['position'] + $Inline['extent']);
+
+ continue 2;
+ }
+
+ # the marker does not belong to an inline
+
+ $unmarkedText = substr($text, 0, $markerPosition + 1);
+
+ $InlineText = $this->inlineText($unmarkedText);
+ $Elements[] = $InlineText['element'];
+
+ $text = substr($text, $markerPosition + 1);
+ }
+
+ $InlineText = $this->inlineText($text);
+ $Elements[] = $InlineText['element'];
+
+ foreach ($Elements as &$Element)
+ {
+ if ( ! isset($Element['autobreak']))
+ {
+ $Element['autobreak'] = false;
+ }
+ }
+
+ return $Elements;
+ }
+
+ #
+ # ~
+ #
+
+ protected function inlineText($text)
+ {
+ $Inline = array(
+ 'extent' => strlen($text),
+ 'element' => array(),
+ );
+
+ $Inline['element']['elements'] = self::pregReplaceElements(
+ $this->breaksEnabled ? '/[ ]*+\n/' : '/(?:[ ]*+\\\\|[ ]{2,}+)\n/',
+ array(
+ array('name' => 'br'),
+ array('text' => "\n"),
+ ),
+ $text
+ );
+
+ return $Inline;
+ }
+
+ protected function inlineCode($Excerpt)
+ {
+ $marker = $Excerpt['text'][0];
+
+ if (preg_match('/^(['.$marker.']++)[ ]*+(.+?)[ ]*+(? strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'code',
+ 'text' => $text,
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmailTag($Excerpt)
+ {
+ $hostnameLabel = '[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?';
+
+ $commonMarkEmail = '[a-zA-Z0-9.!#$%&\'*+\/=?^_`{|}~-]++@'
+ . $hostnameLabel . '(?:\.' . $hostnameLabel . ')*';
+
+ if (strpos($Excerpt['text'], '>') !== false
+ and preg_match("/^<((mailto:)?$commonMarkEmail)>/i", $Excerpt['text'], $matches)
+ ){
+ $url = $matches[1];
+
+ if ( ! isset($matches[2]))
+ {
+ $url = "mailto:$url";
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $matches[1],
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ protected function inlineEmphasis($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ $marker = $Excerpt['text'][0];
+
+ if ($Excerpt['text'][1] === $marker and preg_match($this->StrongRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'strong';
+ }
+ elseif (preg_match($this->EmRegex[$marker], $Excerpt['text'], $matches))
+ {
+ $emphasis = 'em';
+ }
+ else
+ {
+ return;
+ }
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => $emphasis,
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $matches[1],
+ 'destination' => 'elements',
+ )
+ ),
+ );
+ }
+
+ protected function inlineEscapeSequence($Excerpt)
+ {
+ if (isset($Excerpt['text'][1]) and in_array($Excerpt['text'][1], $this->specialCharacters))
+ {
+ return array(
+ 'element' => array('rawHtml' => $Excerpt['text'][1]),
+ 'extent' => 2,
+ );
+ }
+ }
+
+ protected function inlineImage($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]) or $Excerpt['text'][1] !== '[')
+ {
+ return;
+ }
+
+ $Excerpt['text']= substr($Excerpt['text'], 1);
+
+ $Link = $this->inlineLink($Excerpt);
+
+ if ($Link === null)
+ {
+ return;
+ }
+
+ $Inline = array(
+ 'extent' => $Link['extent'] + 1,
+ 'element' => array(
+ 'name' => 'img',
+ 'attributes' => array(
+ 'src' => $Link['element']['attributes']['href'],
+ 'alt' => $Link['element']['handler']['argument'],
+ ),
+ 'autobreak' => true,
+ ),
+ );
+
+ $Inline['element']['attributes'] += $Link['element']['attributes'];
+
+ unset($Inline['element']['attributes']['href']);
+
+ return $Inline;
+ }
+
+ protected function inlineLink($Excerpt)
+ {
+ $Element = array(
+ 'name' => 'a',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => null,
+ 'destination' => 'elements',
+ ),
+ 'nonNestables' => array('Url', 'Link'),
+ 'attributes' => array(
+ 'href' => null,
+ 'title' => null,
+ ),
+ );
+
+ $extent = 0;
+
+ $remainder = $Excerpt['text'];
+
+ if (preg_match('/\[((?:[^][]++|(?R))*+)\]/', $remainder, $matches))
+ {
+ $Element['handler']['argument'] = $matches[1];
+
+ $extent += strlen($matches[0]);
+
+ $remainder = substr($remainder, $extent);
+ }
+ else
+ {
+ return;
+ }
+
+ if (preg_match('/^[(]\s*+((?:[^ ()]++|[(][^ )]+[)])++)(?:[ ]+("[^"]*+"|\'[^\']*+\'))?\s*+[)]/', $remainder, $matches))
+ {
+ $Element['attributes']['href'] = $matches[1];
+
+ if (isset($matches[2]))
+ {
+ $Element['attributes']['title'] = substr($matches[2], 1, - 1);
+ }
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ if (preg_match('/^\s*\[(.*?)\]/', $remainder, $matches))
+ {
+ $definition = strlen($matches[1]) ? $matches[1] : $Element['handler']['argument'];
+ $definition = strtolower($definition);
+
+ $extent += strlen($matches[0]);
+ }
+ else
+ {
+ $definition = strtolower($Element['handler']['argument']);
+ }
+
+ if ( ! isset($this->DefinitionData['Reference'][$definition]))
+ {
+ return;
+ }
+
+ $Definition = $this->DefinitionData['Reference'][$definition];
+
+ $Element['attributes']['href'] = $Definition['url'];
+ $Element['attributes']['title'] = $Definition['title'];
+ }
+
+ return array(
+ 'extent' => $extent,
+ 'element' => $Element,
+ );
+ }
+
+ protected function inlineMarkup($Excerpt)
+ {
+ if ($this->markupEscaped or $this->safeMode or strpos($Excerpt['text'], '>') === false)
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '/' and preg_match('/^<\/\w[\w-]*+[ ]*+>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'element' => array('rawHtml' => $matches[0]),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] === '!' and preg_match('/^/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'element' => array('rawHtml' => $matches[0]),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ if ($Excerpt['text'][1] !== ' ' and preg_match('/^<\w[\w-]*+(?:[ ]*+'.$this->regexHtmlAttribute.')*+[ ]*+\/?>/s', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'element' => array('rawHtml' => $matches[0]),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+ }
+
+ protected function inlineSpecialCharacter($Excerpt)
+ {
+ if (substr($Excerpt['text'], 1, 1) !== ' ' and strpos($Excerpt['text'], ';') !== false
+ and preg_match('/^&(#?+[0-9a-zA-Z]++);/', $Excerpt['text'], $matches)
+ ) {
+ return array(
+ 'element' => array('rawHtml' => '&' . $matches[1] . ';'),
+ 'extent' => strlen($matches[0]),
+ );
+ }
+
+ return;
+ }
+
+ protected function inlineStrikethrough($Excerpt)
+ {
+ if ( ! isset($Excerpt['text'][1]))
+ {
+ return;
+ }
+
+ if ($Excerpt['text'][1] === '~' and preg_match('/^~~(?=\S)(.+?)(?<=\S)~~/', $Excerpt['text'], $matches))
+ {
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'del',
+ 'handler' => array(
+ 'function' => 'lineElements',
+ 'argument' => $matches[1],
+ 'destination' => 'elements',
+ )
+ ),
+ );
+ }
+ }
+
+ protected function inlineUrl($Excerpt)
+ {
+ if ($this->urlsLinked !== true or ! isset($Excerpt['text'][2]) or $Excerpt['text'][2] !== '/')
+ {
+ return;
+ }
+
+ if (strpos($Excerpt['context'], 'http') !== false
+ and preg_match('/\bhttps?+:[\/]{2}[^\s<]+\b\/*+/ui', $Excerpt['context'], $matches, PREG_OFFSET_CAPTURE)
+ ) {
+ $url = $matches[0][0];
+
+ $Inline = array(
+ 'extent' => strlen($matches[0][0]),
+ 'position' => $matches[0][1],
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+
+ return $Inline;
+ }
+ }
+
+ protected function inlineUrlTag($Excerpt)
+ {
+ if (strpos($Excerpt['text'], '>') !== false and preg_match('/^<(\w++:\/{2}[^ >]++)>/i', $Excerpt['text'], $matches))
+ {
+ $url = $matches[1];
+
+ return array(
+ 'extent' => strlen($matches[0]),
+ 'element' => array(
+ 'name' => 'a',
+ 'text' => $url,
+ 'attributes' => array(
+ 'href' => $url,
+ ),
+ ),
+ );
+ }
+ }
+
+ # ~
+
+ protected function unmarkedText($text)
+ {
+ $Inline = $this->inlineText($text);
+ return $this->element($Inline['element']);
+ }
+
+ #
+ # Handlers
+ #
+
+ protected function handle(array $Element)
+ {
+ if (isset($Element['handler']))
+ {
+ if (!isset($Element['nonNestables']))
+ {
+ $Element['nonNestables'] = array();
+ }
+
+ if (is_string($Element['handler']))
+ {
+ $function = $Element['handler'];
+ $argument = $Element['text'];
+ unset($Element['text']);
+ $destination = 'rawHtml';
+ }
+ else
+ {
+ $function = $Element['handler']['function'];
+ $argument = $Element['handler']['argument'];
+ $destination = $Element['handler']['destination'];
+ }
+
+ $Element[$destination] = $this->{$function}($argument, $Element['nonNestables']);
+
+ if ($destination === 'handler')
+ {
+ $Element = $this->handle($Element);
+ }
+
+ unset($Element['handler']);
+ }
+
+ return $Element;
+ }
+
+ protected function handleElementRecursive(array $Element)
+ {
+ return $this->elementApplyRecursive(array($this, 'handle'), $Element);
+ }
+
+ protected function handleElementsRecursive(array $Elements)
+ {
+ return $this->elementsApplyRecursive(array($this, 'handle'), $Elements);
+ }
+
+ protected function elementApplyRecursive($closure, array $Element)
+ {
+ $Element = call_user_func($closure, $Element);
+
+ if (isset($Element['elements']))
+ {
+ $Element['elements'] = $this->elementsApplyRecursive($closure, $Element['elements']);
+ }
+ elseif (isset($Element['element']))
+ {
+ $Element['element'] = $this->elementApplyRecursive($closure, $Element['element']);
+ }
+
+ return $Element;
+ }
+
+ protected function elementApplyRecursiveDepthFirst($closure, array $Element)
+ {
+ if (isset($Element['elements']))
+ {
+ $Element['elements'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['elements']);
+ }
+ elseif (isset($Element['element']))
+ {
+ $Element['element'] = $this->elementsApplyRecursiveDepthFirst($closure, $Element['element']);
+ }
+
+ $Element = call_user_func($closure, $Element);
+
+ return $Element;
+ }
+
+ protected function elementsApplyRecursive($closure, array $Elements)
+ {
+ foreach ($Elements as &$Element)
+ {
+ $Element = $this->elementApplyRecursive($closure, $Element);
+ }
+
+ return $Elements;
+ }
+
+ protected function elementsApplyRecursiveDepthFirst($closure, array $Elements)
+ {
+ foreach ($Elements as &$Element)
+ {
+ $Element = $this->elementApplyRecursiveDepthFirst($closure, $Element);
+ }
+
+ return $Elements;
+ }
+
+ protected function element(array $Element)
+ {
+ if ($this->safeMode)
+ {
+ $Element = $this->sanitiseElement($Element);
+ }
+
+ # identity map if element has no handler
+ $Element = $this->handle($Element);
+
+ $hasName = isset($Element['name']);
+
+ $markup = '';
+
+ if ($hasName)
+ {
+ $markup .= '<' . $Element['name'];
+
+ if (isset($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $name => $value)
+ {
+ if ($value === null)
+ {
+ continue;
+ }
+
+ $markup .= " $name=\"".self::escape($value).'"';
+ }
+ }
+ }
+
+ $permitRawHtml = false;
+
+ if (isset($Element['text']))
+ {
+ $text = $Element['text'];
+ }
+ // very strongly consider an alternative if you're writing an
+ // extension
+ elseif (isset($Element['rawHtml']))
+ {
+ $text = $Element['rawHtml'];
+
+ $allowRawHtmlInSafeMode = isset($Element['allowRawHtmlInSafeMode']) && $Element['allowRawHtmlInSafeMode'];
+ $permitRawHtml = !$this->safeMode || $allowRawHtmlInSafeMode;
+ }
+
+ $hasContent = isset($text) || isset($Element['element']) || isset($Element['elements']);
+
+ if ($hasContent)
+ {
+ $markup .= $hasName ? '>' : '';
+
+ if (isset($Element['elements']))
+ {
+ $markup .= $this->elements($Element['elements']);
+ }
+ elseif (isset($Element['element']))
+ {
+ $markup .= $this->element($Element['element']);
+ }
+ else
+ {
+ if (!$permitRawHtml)
+ {
+ $markup .= self::escape($text, true);
+ }
+ else
+ {
+ $markup .= $text;
+ }
+ }
+
+ $markup .= $hasName ? '' . $Element['name'] . '>' : '';
+ }
+ elseif ($hasName)
+ {
+ $markup .= ' />';
+ }
+
+ return $markup;
+ }
+
+ protected function elements(array $Elements)
+ {
+ $markup = '';
+
+ $autoBreak = true;
+
+ foreach ($Elements as $Element)
+ {
+ if (empty($Element))
+ {
+ continue;
+ }
+
+ $autoBreakNext = (isset($Element['autobreak'])
+ ? $Element['autobreak'] : isset($Element['name'])
+ );
+ // (autobreak === false) covers both sides of an element
+ $autoBreak = !$autoBreak ? $autoBreak : $autoBreakNext;
+
+ $markup .= ($autoBreak ? "\n" : '') . $this->element($Element);
+ $autoBreak = $autoBreakNext;
+ }
+
+ $markup .= $autoBreak ? "\n" : '';
+
+ return $markup;
+ }
+
+ # ~
+
+ protected function li($lines)
+ {
+ $Elements = $this->linesElements($lines);
+
+ if ( ! in_array('', $lines)
+ and isset($Elements[0]) and isset($Elements[0]['name'])
+ and $Elements[0]['name'] === 'p'
+ ) {
+ unset($Elements[0]['name']);
+ }
+
+ return $Elements;
+ }
+
+ #
+ # AST Convenience
+ #
+
+ /**
+ * Replace occurrences $regexp with $Elements in $text. Return an array of
+ * elements representing the replacement.
+ */
+ protected static function pregReplaceElements($regexp, $Elements, $text)
+ {
+ $newElements = array();
+
+ while (preg_match($regexp, $text, $matches, PREG_OFFSET_CAPTURE))
+ {
+ $offset = $matches[0][1];
+ $before = substr($text, 0, $offset);
+ $after = substr($text, $offset + strlen($matches[0][0]));
+
+ $newElements[] = array('text' => $before);
+
+ foreach ($Elements as $Element)
+ {
+ $newElements[] = $Element;
+ }
+
+ $text = $after;
+ }
+
+ $newElements[] = array('text' => $text);
+
+ return $newElements;
+ }
+
+ #
+ # Deprecated Methods
+ #
+
+ function parse($text)
+ {
+ $markup = $this->text($text);
+
+ return $markup;
+ }
+
+ protected function sanitiseElement(array $Element)
+ {
+ static $goodAttribute = '/^[a-zA-Z0-9][a-zA-Z0-9-_]*+$/';
+ static $safeUrlNameToAtt = array(
+ 'a' => 'href',
+ 'img' => 'src',
+ );
+
+ if ( ! isset($Element['name']))
+ {
+ unset($Element['attributes']);
+ return $Element;
+ }
+
+ if (isset($safeUrlNameToAtt[$Element['name']]))
+ {
+ $Element = $this->filterUnsafeUrlInAttribute($Element, $safeUrlNameToAtt[$Element['name']]);
+ }
+
+ if ( ! empty($Element['attributes']))
+ {
+ foreach ($Element['attributes'] as $att => $val)
+ {
+ # filter out badly parsed attribute
+ if ( ! preg_match($goodAttribute, $att))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ # dump onevent attribute
+ elseif (self::striAtStart($att, 'on'))
+ {
+ unset($Element['attributes'][$att]);
+ }
+ }
+ }
+
+ return $Element;
+ }
+
+ protected function filterUnsafeUrlInAttribute(array $Element, $attribute)
+ {
+ foreach ($this->safeLinksWhitelist as $scheme)
+ {
+ if (self::striAtStart($Element['attributes'][$attribute], $scheme))
+ {
+ return $Element;
+ }
+ }
+
+ $Element['attributes'][$attribute] = str_replace(':', '%3A', $Element['attributes'][$attribute]);
+
+ return $Element;
+ }
+
+ #
+ # Static Methods
+ #
+
+ protected static function escape($text, $allowQuotes = false)
+ {
+ return htmlspecialchars($text, $allowQuotes ? ENT_NOQUOTES : ENT_QUOTES, 'UTF-8');
+ }
+
+ protected static function striAtStart($string, $needle)
+ {
+ $len = strlen($needle);
+
+ if ($len > strlen($string))
+ {
+ return false;
+ }
+ else
+ {
+ return strtolower(substr($string, 0, $len)) === strtolower($needle);
+ }
+ }
+
+ static function instance($name = 'default')
+ {
+ if (isset(self::$instances[$name]))
+ {
+ return self::$instances[$name];
+ }
+
+ $instance = new static();
+
+ self::$instances[$name] = $instance;
+
+ return $instance;
+ }
+
+ private static $instances = array();
+
+ #
+ # Fields
+ #
+
+ protected $DefinitionData;
+
+ #
+ # Read-Only
+
+ protected $specialCharacters = array(
+ '\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!', '|', '~'
+ );
+
+ protected $StrongRegex = array(
+ '*' => '/^[*]{2}((?:\\\\\*|[^*]|[*][^*]*+[*])+?)[*]{2}(?![*])/s',
+ '_' => '/^__((?:\\\\_|[^_]|_[^_]*+_)+?)__(?!_)/us',
+ );
+
+ protected $EmRegex = array(
+ '*' => '/^[*]((?:\\\\\*|[^*]|[*][*][^*]+?[*][*])+?)[*](?![*])/s',
+ '_' => '/^_((?:\\\\_|[^_]|__[^_]*__)+?)_(?!_)\b/us',
+ );
+
+ protected $regexHtmlAttribute = '[a-zA-Z_:][\w:.-]*+(?:\s*+=\s*+(?:[^"\'=<>`\s]+|"[^"]*+"|\'[^\']*+\'))?+';
+
+ protected $voidElements = array(
+ 'area', 'base', 'br', 'col', 'command', 'embed', 'hr', 'img', 'input', 'link', 'meta', 'param', 'source',
+ );
+
+ protected $textLevelElements = array(
+ 'a', 'br', 'bdo', 'abbr', 'blink', 'nextid', 'acronym', 'basefont',
+ 'b', 'em', 'big', 'cite', 'small', 'spacer', 'listing',
+ 'i', 'rp', 'del', 'code', 'strike', 'marquee',
+ 'q', 'rt', 'ins', 'font', 'strong',
+ 's', 'tt', 'kbd', 'mark',
+ 'u', 'xm', 'sub', 'nobr',
+ 'sup', 'ruby',
+ 'var', 'span',
+ 'wbr', 'time',
+ );
+}
diff --git a/includes/footer.php b/includes/footer.php
new file mode 100644
index 000000000..5ad6fd7d4
--- /dev/null
+++ b/includes/footer.php
@@ -0,0 +1,14 @@
+
+
+
+
+