✨feature: Initial commit
This commit is contained in:
@@ -0,0 +1,13 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
|
||||
[*.{json,yml,yaml,md}]
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"extends": ["airbnb-base"],
|
||||
"env": {
|
||||
"browser": true,
|
||||
"es2021": true
|
||||
},
|
||||
"parserOptions": {
|
||||
"ecmaVersion": "latest",
|
||||
"sourceType": "script",
|
||||
"ecmaFeatures": {
|
||||
"jsx": true
|
||||
}
|
||||
},
|
||||
"globals": {
|
||||
"window": "readonly"
|
||||
},
|
||||
"rules": {
|
||||
"comma-dangle": "off",
|
||||
"func-names": "off",
|
||||
"indent": "off",
|
||||
"max-len": "off",
|
||||
"no-undef": "off",
|
||||
"no-underscore-dangle": "off",
|
||||
"object-shorthand": "off",
|
||||
"operator-linebreak": "off",
|
||||
"prefer-arrow-callback": "off",
|
||||
"prefer-destructuring": "off",
|
||||
"prefer-template": "off"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
* text=auto eol=lf
|
||||
|
||||
/.github export-ignore
|
||||
/.vscode export-ignore
|
||||
/node_modules export-ignore
|
||||
/tests export-ignore
|
||||
/vendor/bin export-ignore
|
||||
phpunit.xml.dist export-ignore
|
||||
package-lock.json export-ignore
|
||||
project-brief.md export-ignore
|
||||
PLAN.md export-ignore
|
||||
@@ -0,0 +1,8 @@
|
||||
/node_modules/
|
||||
/vendor/
|
||||
/build/
|
||||
/dist/
|
||||
/.phpunit.cache/
|
||||
/.idea/
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
@@ -0,0 +1 @@
|
||||
{"version":1,"defects":[],"times":{"LogoSanitizerTest::test_sanitize_collection_skips_invalid_entries":0.005,"SettingsTest::test_sanitize_rejects_unknown_layout":0,"SettingsTest::test_sanitize_normalization_options":0}}
|
||||
Vendored
+16
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"workbench.colorCustomizations": {
|
||||
"tree.indentGuidesStroke": "#3d92ec",
|
||||
"activityBar.background": "#2E2267",
|
||||
"titleBar.activeBackground": "#412F90",
|
||||
"titleBar.activeForeground": "#FBFBFE",
|
||||
"titleBar.inactiveBackground": "#2E2267",
|
||||
"titleBar.inactiveForeground": "#FBFBFE",
|
||||
"statusBar.background": "#2E2267",
|
||||
"statusBar.foreground": "#FBFBFE",
|
||||
"statusBar.debuggingBackground": "#2E2267",
|
||||
"statusBar.debuggingForeground": "#FBFBFE",
|
||||
"statusBar.noFolderBackground": "#2E2267",
|
||||
"statusBar.noFolderForeground": "#FBFBFE"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,384 @@
|
||||
# AGENTS.md — Codex Playbook for Building a WordPress Plugin
|
||||
|
||||
> A precise, no-nonsense blueprint for orchestrating autonomous and semi-autonomous coding agents to plan, scaffold, implement, test, and ship a production‑grade WordPress plugin.
|
||||
|
||||
---
|
||||
|
||||
## 0) Purpose & Scope
|
||||
|
||||
This document defines the **agents**, **tools**, **workflows**, **constraints**, and **acceptance criteria** for using an agentic coding system ("Codex") to create and maintain a WordPress plugin. It is optimized for **real, shippable code**, not demos.
|
||||
|
||||
Outcomes:
|
||||
|
||||
* A production‑ready WordPress plugin following WP Coding Standards.
|
||||
* Clean, testable PHP, JS (React) admin UI, i18n, security hardening, and CI.
|
||||
* Repeatable pipelines from idea → release (with versioning & changelog).
|
||||
|
||||
---
|
||||
|
||||
## 1) High-Level Task Graph
|
||||
|
||||
1. **Discovery & Planning**
|
||||
Requirements intake → constraints → feature list → milestones → risk register.
|
||||
2. **Scaffolding**
|
||||
Repo init → plugin headers → file structure → build tooling.
|
||||
3. **Implementation**
|
||||
Core PHP features → admin screens (React) → REST endpoints → WP‑CLI.
|
||||
4. **Quality & Hardening**
|
||||
PHPCS/WPCS → unit/integration tests → e2e (Playwright) → security checks.
|
||||
5. **Docs & Demos**
|
||||
README, inline docs, usage guide, screenshots/GIFs, sample data.
|
||||
6. **Release**
|
||||
Semantic versioning → tagged release → changelog → release assets.
|
||||
7. **Maintenance**
|
||||
Issue triage → patch releases → perf audits → dependency updates.
|
||||
|
||||
---
|
||||
|
||||
## 2) Agents & Ownership
|
||||
|
||||
### A. PlannerAgent (Lead)
|
||||
|
||||
* **Goal:** Convert business requirements into a concrete implementation plan.
|
||||
* **Inputs:** Product brief, constraints, compatibility targets.
|
||||
* **Outputs:** `PLAN.md` with MVP scope, milestones, risks, success metrics.
|
||||
* **Key checks:** Feasibility, risk mitigation, timeline, test strategy alignment.
|
||||
|
||||
### B. ScaffolderAgent
|
||||
|
||||
* **Goal:** Create plugin skeleton and development toolchain.
|
||||
* **Outputs:**
|
||||
|
||||
* `plugin-name/plugin-name.php` with proper headers
|
||||
* `src/` (PHP), `includes/`, `assets/`, `admin/`, `languages/`
|
||||
* `composer.json`, `package.json`, `webpack.config.js` or `vite.config.ts`
|
||||
* `phpcs.xml`, `.editorconfig`, `.gitattributes`, `.gitignore`
|
||||
* `README.md`, `CHANGELOG.md`, `LICENSE`
|
||||
* **Standards:** PSR-4 autoloading, WPCS ruleset, PHPCS baseline if needed.
|
||||
|
||||
### C. BackendAgent (PHP)
|
||||
|
||||
* **Goal:** Implement core plugin features with hooks, filters, REST, cron, WP‑CLI.
|
||||
* **Constraints:**
|
||||
|
||||
* **Security first:** Nonces, `current_user_can`, prepared SQL, sanitize/escape.
|
||||
* **Compatibility:** PHP 7.4–8.3, WP 6.1+, multisite-safe where relevant.
|
||||
* **Artifacts:** `src/` classes, `includes/`, `uninstall.php`, activation/deactivation hooks.
|
||||
|
||||
### D. FrontendAgent (Admin UI)
|
||||
|
||||
* **Goal:** Build admin screens with React + WP Scripts (or Vite w/ WP deps externals).
|
||||
* **Artifacts:** `admin/` React app, enqueue via `wp_enqueue_script/style`, settings pages, components, forms with wp.data/wc.data if Woo, and REST integration.
|
||||
|
||||
### E. APIGatewayAgent (REST & Webhooks)
|
||||
|
||||
* **Goal:** Define REST routes, permissions callbacks, and (optionally) webhooks.
|
||||
* **Checks:** Nonces or auth tokens, caps mapping, rate limiting where applicable.
|
||||
|
||||
### F. CLIAgent (WP‑CLI)
|
||||
|
||||
* **Goal:** Add WP‑CLI commands for power users & CI automation.
|
||||
* **Artifacts:** `src/CLI/Commands.php` with subcommands, argument validation, exit codes.
|
||||
|
||||
### G. TestAgent (QA)
|
||||
|
||||
* **Goal:** Unit, integration (WP core bootstrap), e2e (Playwright) with headless Chromium.
|
||||
* **Artifacts:** `tests/phpunit.xml`, `tests/`, GitHub Actions CI templates.
|
||||
|
||||
### H. SecPerfAgent (Security & Performance)
|
||||
|
||||
* **Goal:** Threat modeling, perf budgets, db/indexing strategy, cache headers.
|
||||
* **Checks:** Nonce coverage, XSS/CSRF/SQLi audit, escaping map, memory/time profiles.
|
||||
|
||||
### I. DocsAgent
|
||||
|
||||
* **Goal:** Build crisp docs: README, usage, configuration, troubleshooting, examples.
|
||||
|
||||
### J. ReleaseAgent
|
||||
|
||||
* **Goal:** Version bump, changelog, Git tag, GitHub Release, packaged zip artifact.
|
||||
|
||||
---
|
||||
|
||||
## 3) Repository Structure
|
||||
|
||||
```
|
||||
logo-soup/
|
||||
├─ logo-soup.php # Main plugin file (headers + bootstrap)
|
||||
├─ composer.json # PSR-4 autoload, dev tools
|
||||
├─ package.json # Build scripts, lint, test
|
||||
├─ vite.config.ts | webpack.config.js
|
||||
├─ phpcs.xml # WPCS rules
|
||||
├─ phpstan.neon # Static analysis (level 6–max)
|
||||
├─ .editorconfig / .gitattributes / .gitignore
|
||||
├─ README.md / CHANGELOG.md / LICENSE / CONTRIBUTING.md
|
||||
├─ src/ # PHP business logic (PSR-4)
|
||||
│ ├─ Admin/ # Settings pages, controllers
|
||||
│ ├─ REST/ # Endpoints & permissions
|
||||
│ ├─ CLI/ # WP-CLI commands
|
||||
│ ├─ Cron/ # Scheduled events
|
||||
│ └─ Infrastructure/ # Services, DI container, logger
|
||||
├─ includes/ # Legacy-style helpers (thin shims only)
|
||||
├─ admin/ # React app (built → dist)
|
||||
│ ├─ src/ # TS/JS, components
|
||||
│ └─ dist/ # Built assets (git-ignored)
|
||||
├─ assets/ # CSS, images, icons
|
||||
├─ languages/ # .pot and translations
|
||||
├─ tests/
|
||||
│ ├─ phpunit.xml.dist # Bootstrapped with WP test suite
|
||||
│ ├─ php/ # Unit/integration tests
|
||||
│ └─ e2e/ # Playwright tests
|
||||
└─ uninstall.php # Hard removal of options/data when required
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4) Coding Standards & Quality Gates
|
||||
|
||||
* **PHP:** PHPCS with **WordPress Coding Standards** (`wpcs`), PHPStan level ≥ 6.
|
||||
* **JS/TS:** ESLint (airbnb/base), TypeScript strict mode where used, Prettier.
|
||||
* **Commits:** Conventional Commits.
|
||||
* **Branches:** `main` (stable), `develop`, feature branches → PRs.
|
||||
* **CI Required Checks:** lint, static analysis, unit/integration tests, e2e (smoke), `composer validate`. No merge to `main` without green.
|
||||
|
||||
---
|
||||
|
||||
## 5) Security Model
|
||||
|
||||
* Enforce **capability checks** on every privileged action. Prefer granular custom caps.
|
||||
* **Nonces** on all state‑changing forms/requests; **verify before mutate**.
|
||||
* Always **sanitize input** (`sanitize_text_field`, `sanitize_key`, `absint`, custom) and **escape output** (`esc_html`, `esc_attr`, `wp_kses`).
|
||||
* **DB access:** `wpdb->prepare`, avoid dynamic table names; consider custom tables with schema migrations when needed.
|
||||
* **Files:** Validate MIME types/size, use WP Filesystem API.
|
||||
* **Settings:** Use `register_setting` with `sanitize_callback`.
|
||||
* **Secrets:** Never commit secrets; use environment variables or WP constants via `.env` with `vlucas/phpdotenv` (optional).
|
||||
|
||||
---
|
||||
|
||||
## 6) Build & Tooling
|
||||
|
||||
**Composer dev deps (suggested):**
|
||||
|
||||
* `dealerdirect/phpcodesniffer-composer-installer`
|
||||
* `squizlabs/php_codesniffer`
|
||||
* `wp-coding-standards/wpcs`
|
||||
* `phpstan/phpstan`
|
||||
* `phpunit/phpunit`
|
||||
|
||||
**NPM dev deps (suggested):**
|
||||
|
||||
* `@wordpress/scripts` *or* `vite` + `@wordpress/dependency-extraction-webpack-plugin` equivalent via externals
|
||||
* `typescript`, `eslint`, `prettier`, `playwright`
|
||||
|
||||
**Makefile (optional) targets:**
|
||||
|
||||
```
|
||||
make setup # composer install, npm install
|
||||
make build # build admin assets
|
||||
make lint # phpcs, eslint, phpstan
|
||||
make test # phpunit, e2e (smoke)
|
||||
make zip # generate distributable zip under ./dist
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7) CI/CD (GitHub Actions templates)
|
||||
|
||||
* **php.yml:** matrix {php: \[7.4, 8.0, 8.1, 8.2, 8.3]}, run phpcs, phpstan, phpunit.
|
||||
* **js.yml:** node LTS, run eslint, typecheck, build.
|
||||
* **e2e.yml:** spin WP (wp-env/docker), run Playwright smoke on admin.
|
||||
* **release.yml:** on tag `v*`, bump version in headers, generate zip, create release, attach artifact.
|
||||
|
||||
---
|
||||
|
||||
## 8) Implementation Checklists
|
||||
|
||||
### 8.1 Plugin Bootstrap
|
||||
|
||||
* [ ] Header with `Plugin Name`, `Version`, `Requires at least`, `Requires PHP`, `Text Domain`.
|
||||
* [ ] Autoloader (Composer) + safe early exit if direct access.
|
||||
* [ ] Activation/Deactivation hooks; network‑aware.
|
||||
* [ ] Service container bootstrap (optional) for loose coupling.
|
||||
|
||||
### 8.2 Admin UI
|
||||
|
||||
* [ ] Single‑page admin screen registered via `add_menu_page`/`add_submenu_page`.
|
||||
* [ ] Nonce embedded into page for REST mutations.
|
||||
* [ ] `wp_enqueue_script` with dependencies (`wp-element`, `wp-components`, etc.).
|
||||
* [ ] Accessible components, keyboard navigation, focus styles.
|
||||
|
||||
### 8.3 REST API
|
||||
|
||||
* [ ] `register_rest_route` with namespaced routes and `permission_callback`.
|
||||
* [ ] Input validation and output normalization.
|
||||
* [ ] Pagination and error shapes consistent.
|
||||
|
||||
### 8.4 Data Layer
|
||||
|
||||
* [ ] Options API with schema, or custom tables via dbDelta + migrations.
|
||||
* [ ] Caching using transients or object cache; set TTLs.
|
||||
* [ ] Background tasks via Action Scheduler or WP Cron when needed.
|
||||
|
||||
### 8.5 Internationalization (i18n)
|
||||
|
||||
* [ ] Load text domain, generate `.pot`.
|
||||
* [ ] Wrap strings with translation functions, no string concatenation with HTML.
|
||||
|
||||
### 8.6 Uninstall
|
||||
|
||||
* [ ] `uninstall.php` handles irreversible deletion when user opts-in.
|
||||
|
||||
---
|
||||
|
||||
## 9) Prompts & Guardrails for Agents
|
||||
|
||||
**General guardrails** (apply to all agents):
|
||||
|
||||
* Prefer stable, framework‑agnostic PHP for plugin core; keep vendor size small.
|
||||
* Follow WPCS; do not bypass lints without justification.
|
||||
* Avoid over‑engineering; MVP first, extensibility second.
|
||||
* Provide diffs/patches or exact file paths in outputs.
|
||||
* Every change must include: rationale → code → tests → docs.
|
||||
|
||||
**Prompt scaffolds**:
|
||||
|
||||
* **PlannerAgent:**
|
||||
|
||||
* *"Given this brief: `project-brief.md`, enumerate functional/non‑functional requirements, risks, milestones, metrics, and a phased MVP plan. Output `PLAN.md` with tables and a Gantt‑style milestone list."*
|
||||
|
||||
* **ScaffolderAgent:**
|
||||
|
||||
* *"Generate a WordPress plugin skeleton named `logo-soup`. Include Composer (PSR‑4), Vite (or @wordpress/scripts), PHPCS (WPCS), PHPStan. Produce file tree and initial file contents. No placeholder TODOs—write minimal viable code."*
|
||||
|
||||
* **BackendAgent:**
|
||||
|
||||
* *"Implement feature `<feature>` behind capability `<cap>`. Add actions/filters, sanitize/validate, and unit tests. Return diff with file paths. Include acceptance tests for edge cases."*
|
||||
|
||||
* **FrontendAgent:**
|
||||
|
||||
* *"Create admin view `<view>` with React, using WP components. Implement controlled inputs, form validation, REST calls with nonce, and optimistic updates. Provide Playwright tests."*
|
||||
|
||||
* **APIGatewayAgent:**
|
||||
|
||||
* *"Define REST endpoint `<method> /<ns>/<route>` with schema, permission callback respecting `<cap>`. Include unit tests for auth and validation failures."*
|
||||
|
||||
* **CLIAgent:**
|
||||
|
||||
* *"Add WP‑CLI command `wp <ns> <cmd>` with flags `<flags>`. Validate args, handle errors gracefully, and return exit codes. Provide usage examples."*
|
||||
|
||||
* **TestAgent:**
|
||||
|
||||
* *"Write PHPUnit tests for `<class>` with edge cases. Bootstrap WP test suite. Add Playwright test for `<user flow>`. Ensure CI green."*
|
||||
|
||||
* **ReleaseAgent:**
|
||||
|
||||
* *"Prepare release `<version>`. Update plugin header + `readme.txt` stable tag, generate changelog (Conventional Commits), build zip, create Git tag and GitHub Release with assets."*
|
||||
|
||||
---
|
||||
|
||||
## 10) Acceptance Criteria (Definition of Done)
|
||||
|
||||
* ✅ Linting: PHPCS (WPCS) and ESLint pass with 0 errors.
|
||||
* ✅ Tests: PHP unit/integration ≥ 80% critical path coverage; e2e smoke green.
|
||||
* ✅ Security: Nonces, caps, sanitization/escaping verified; no direct SQL without prepare.
|
||||
* ✅ Docs: README with install/usage, configuration, screenshots/GIFs; CHANGELOG.
|
||||
* ✅ Release: Tagged semantic version; distributable zip attached to release.
|
||||
* ✅ Performance: First meaningful paint in admin ≤ 2s on mid hardware; queries indexed.
|
||||
* ✅ Accessibility: Admin UI meets WCAG AA basics (labels, contrast, keyboard nav).
|
||||
|
||||
---
|
||||
|
||||
## 11) Example: Minimal Plugin Header & Bootstrap
|
||||
|
||||
```php
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Example Plugin
|
||||
* Description: Minimal bootstrap showing headers, autoloading, and safety guards.
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 6.1
|
||||
* Requires PHP: 7.4
|
||||
* Author: Your Org
|
||||
* License: GPL-2.0-or-later
|
||||
* Text Domain: example-plugin
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit; // Exit if accessed directly.
|
||||
}
|
||||
|
||||
// Composer autoload (if present).
|
||||
$autoload = __DIR__ . '/vendor/autoload.php';
|
||||
if ( file_exists( $autoload ) ) {
|
||||
require_once $autoload;
|
||||
}
|
||||
|
||||
// Bootstrap.
|
||||
add_action( 'plugins_loaded', static function () {
|
||||
// Initialize services, hooks, etc.
|
||||
} );
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12) Local Dev Environments
|
||||
|
||||
* **wp-env (official):** Zero-config local WP; good for e2e.
|
||||
* **Docker Compose:** MySQL + WP + phpMyAdmin; seed data via WP‑CLI.
|
||||
* **Valet/Local/Lando:** Developer preference; ensure parity with CI PHP versions.
|
||||
|
||||
---
|
||||
|
||||
## 13) Release Engineering
|
||||
|
||||
* **Versioning:** `MAJOR.MINOR.PATCH`, align PHP headers and `readme.txt` `Stable tag`.
|
||||
* **Dist:** Production build, vendor‑prefixed if shipping SDKs, no dev files.
|
||||
* **SVN (wp.org) optional:** Mirror release using `svn cp` into `/tags/<version>`.
|
||||
|
||||
---
|
||||
|
||||
## 14) Risk Register (starter)
|
||||
|
||||
| Risk | Impact | Likelihood | Mitigation |
|
||||
| -------------------- | ------ | ---------- | ------------------------------------------ |
|
||||
| WP Core API changes | Medium | Low | Pin compatibility, test on latest beta |
|
||||
| PHP 7.4 deprecations | Medium | Medium | Polyfills, conditionals, CI matrix |
|
||||
| Admin UI bloat | Medium | Medium | Perf budgets, code splitting, audit |
|
||||
| Security regressions | High | Low | Threat model, security checklist, CI gates |
|
||||
|
||||
---
|
||||
|
||||
## 15) Contribution Guide (short)
|
||||
|
||||
* Fork + feature branch; keep PRs < 500 lines when possible.
|
||||
* Add/adjust tests and docs with any functional change.
|
||||
* Keep public API stable; mark internal APIs with `@internal`.
|
||||
* Link issues to PRs; include before/after screenshots for UI.
|
||||
|
||||
---
|
||||
|
||||
## 16) Quickstart Commands
|
||||
|
||||
```bash
|
||||
# 1) Setup
|
||||
git init && git commit --allow-empty -m "chore: repo init"
|
||||
composer install
|
||||
npm install
|
||||
|
||||
# 2) Develop
|
||||
npm run dev # or: npm run start
|
||||
|
||||
# 3) Lint & test
|
||||
composer phpcs
|
||||
composer phpstan
|
||||
npm run lint
|
||||
npm run test
|
||||
|
||||
# 4) Build & package
|
||||
npm run build
|
||||
make zip # or custom script to produce ./dist/plugin-name-vX.Y.Z.zip
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**End of AGENTS.md**
|
||||
@@ -0,0 +1,8 @@
|
||||
# Changelog
|
||||
|
||||
## 0.1.0
|
||||
|
||||
- Initial release.
|
||||
- Added a configurable `Logo Soup` block for uploading logos, links, and layout options.
|
||||
- Added a plugin settings screen and REST API for default display options.
|
||||
- Added uninstall cleanup, PHPUnit baseline tests, and development tooling scaffolding.
|
||||
@@ -0,0 +1,24 @@
|
||||
# Logo Soup Plan
|
||||
|
||||
## MVP
|
||||
|
||||
- Register a dynamic `Logo Soup` Gutenberg block.
|
||||
- Let editors upload logos, add optional links, and choose display behavior in the editor.
|
||||
- Provide frontend styles for a static grid and a marquee-style carousel.
|
||||
- Add plugin defaults under `Settings > Logo Soup`.
|
||||
- Expose defaults through a secure REST endpoint for future admin UI expansion.
|
||||
|
||||
## Constraints
|
||||
|
||||
- WordPress 6.1+
|
||||
- PHP 7.4+
|
||||
- Input must be sanitized and output escaped.
|
||||
- State-changing requests must verify capability and nonce.
|
||||
|
||||
## Risks
|
||||
|
||||
| Risk | Impact | Mitigation |
|
||||
| --- | --- | --- |
|
||||
| Logo aspect ratios vary heavily | Medium | Normalize row height and image fit in CSS |
|
||||
| Large logo lists create noisy output | Medium | Limit render to sanitized image/link fields only |
|
||||
| Editor-side UX can sprawl quickly | Low | Keep controls focused on upload, link, and layout |
|
||||
@@ -0,0 +1,40 @@
|
||||
# Logo Soup
|
||||
|
||||
Logo Soup is a block-first WordPress plugin for building balanced logo strips and grids directly inside the block editor.
|
||||
|
||||
## Features
|
||||
|
||||
- Upload logos from the editor and reorder them with simple move controls.
|
||||
- Add optional links per logo.
|
||||
- Choose between `carousel` and `grid` layouts.
|
||||
- Configure grayscale, animation speed, and hover pause behavior.
|
||||
- Set site-wide defaults under `Settings > Logo Soup`.
|
||||
|
||||
## Requirements
|
||||
|
||||
- WordPress 6.1+
|
||||
- PHP 7.4+
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
composer install
|
||||
npm install
|
||||
composer phpcs
|
||||
composer phpstan
|
||||
composer test
|
||||
npm run lint:js
|
||||
```
|
||||
|
||||
## Architecture
|
||||
|
||||
- `logo-soup.php`: plugin bootstrap and autoloader.
|
||||
- `src/`: PHP application code.
|
||||
- `blocks/logo-soup/`: dynamic block metadata and built assets.
|
||||
- `tests/php/`: PHPUnit baseline coverage.
|
||||
|
||||
## Security
|
||||
|
||||
- REST mutations require `manage_options` plus a valid `wp_rest` nonce.
|
||||
- All settings are sanitized before saving.
|
||||
- Block output escapes URLs, attributes, and text on render.
|
||||
@@ -0,0 +1,63 @@
|
||||
{
|
||||
"$schema": "https://schemas.wp.org/trunk/block.json",
|
||||
"apiVersion": 2,
|
||||
"name": "logo-soup/showcase",
|
||||
"title": "Logo Soup",
|
||||
"category": "widgets",
|
||||
"icon": "images-alt2",
|
||||
"description": "Upload logos, assign links, and display them in a grid or carousel.",
|
||||
"textdomain": "logo-soup",
|
||||
"editorScript": "file:./build/index.js",
|
||||
"editorStyle": "file:./build/index.css",
|
||||
"style": "file:./build/style-index.css",
|
||||
"attributes": {
|
||||
"logos": {
|
||||
"type": "array",
|
||||
"default": []
|
||||
},
|
||||
"layout": {
|
||||
"type": "string",
|
||||
"default": "carousel"
|
||||
},
|
||||
"grayscale": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"speed": {
|
||||
"type": "number",
|
||||
"default": 22
|
||||
},
|
||||
"pauseOnHover": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"baseSize": {
|
||||
"type": "number",
|
||||
"default": 48
|
||||
},
|
||||
"scaleFactor": {
|
||||
"type": "number",
|
||||
"default": 0.5
|
||||
},
|
||||
"densityAware": {
|
||||
"type": "boolean",
|
||||
"default": true
|
||||
},
|
||||
"densityFactor": {
|
||||
"type": "number",
|
||||
"default": 0.5
|
||||
},
|
||||
"cropToContent": {
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"alignBy": {
|
||||
"type": "string",
|
||||
"default": "bounds"
|
||||
}
|
||||
},
|
||||
"supports": {
|
||||
"html": false,
|
||||
"align": ["wide", "full"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
/**
|
||||
* Block asset metadata.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
return array(
|
||||
'dependencies' => array(
|
||||
'wp-block-editor',
|
||||
'wp-blocks',
|
||||
'wp-components',
|
||||
'wp-element',
|
||||
'wp-i18n',
|
||||
),
|
||||
'version' => '0.1.0',
|
||||
);
|
||||
@@ -0,0 +1,60 @@
|
||||
.logo-soup-editor {
|
||||
border: 1px dashed #a7aaad;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.logo-soup-editor__toolbar {
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.logo-soup-editor__list {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.logo-soup-editor__card {
|
||||
background: #fff;
|
||||
border: 1px solid #dcdcde;
|
||||
border-radius: 10px;
|
||||
padding: 16px;
|
||||
}
|
||||
|
||||
.logo-soup-editor__thumb {
|
||||
align-items: center;
|
||||
background: linear-gradient(135deg, #f8f8f8, #ececec);
|
||||
border-radius: 8px;
|
||||
display: flex;
|
||||
height: 92px;
|
||||
justify-content: center;
|
||||
margin-bottom: 12px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.logo-soup-editor__thumb img {
|
||||
max-height: var(--logo-soup-base-size, 52px);
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.logo-soup-editor__preview.crop-to-content .logo-soup-editor__thumb {
|
||||
padding: 4px;
|
||||
}
|
||||
|
||||
.logo-soup-editor__preview.align-by-visual-center .logo-soup-editor__thumb img {
|
||||
transform: translate(-2%, -2%);
|
||||
}
|
||||
|
||||
.logo-soup-editor__preview.align-by-visual-center-x .logo-soup-editor__thumb img {
|
||||
transform: translateX(-2%);
|
||||
}
|
||||
|
||||
.logo-soup-editor__preview.align-by-visual-center-y .logo-soup-editor__thumb img {
|
||||
transform: translateY(-2%);
|
||||
}
|
||||
|
||||
.logo-soup-editor__actions {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
@@ -0,0 +1,392 @@
|
||||
(function (blocks, blockEditor, components, element, i18n) {
|
||||
const registerBlockType = blocks.registerBlockType;
|
||||
const InspectorControls = blockEditor.InspectorControls;
|
||||
const MediaUpload = blockEditor.MediaUpload;
|
||||
const MediaUploadCheck = blockEditor.MediaUploadCheck;
|
||||
const useBlockProps = blockEditor.useBlockProps;
|
||||
const Button = components.Button;
|
||||
const PanelBody = components.PanelBody;
|
||||
const Placeholder = components.Placeholder;
|
||||
const RangeControl = components.RangeControl;
|
||||
const SelectControl = components.SelectControl;
|
||||
const TextControl = components.TextControl;
|
||||
const ToggleControl = components.ToggleControl;
|
||||
const Fragment = element.Fragment;
|
||||
const el = element.createElement;
|
||||
const __ = i18n.__;
|
||||
|
||||
function updateLogoAtIndex(logos, index, nextLogo) {
|
||||
return logos.map(function (logo, logoIndex) {
|
||||
return logoIndex === index ? nextLogo : logo;
|
||||
});
|
||||
}
|
||||
|
||||
registerBlockType('logo-soup/showcase', {
|
||||
edit: function (props) {
|
||||
const attributes = props.attributes;
|
||||
const setAttributes = props.setAttributes;
|
||||
const logos = attributes.logos || [];
|
||||
const layout = attributes.layout || 'carousel';
|
||||
const grayscale =
|
||||
typeof attributes.grayscale === 'boolean' ? attributes.grayscale : true;
|
||||
const speed = attributes.speed || 22;
|
||||
const baseSize = attributes.baseSize || 48;
|
||||
const scaleFactor =
|
||||
typeof attributes.scaleFactor === 'number'
|
||||
? attributes.scaleFactor
|
||||
: 0.5;
|
||||
const densityAware =
|
||||
typeof attributes.densityAware === 'boolean'
|
||||
? attributes.densityAware
|
||||
: true;
|
||||
const densityFactor =
|
||||
typeof attributes.densityFactor === 'number'
|
||||
? attributes.densityFactor
|
||||
: 0.5;
|
||||
const cropToContent =
|
||||
typeof attributes.cropToContent === 'boolean'
|
||||
? attributes.cropToContent
|
||||
: false;
|
||||
const alignBy = attributes.alignBy || 'bounds';
|
||||
const pauseOnHover =
|
||||
typeof attributes.pauseOnHover === 'boolean'
|
||||
? attributes.pauseOnHover
|
||||
: true;
|
||||
const blockProps = useBlockProps({ className: 'logo-soup-editor' });
|
||||
|
||||
function addLogo(media) {
|
||||
if (!media || !media.url) {
|
||||
return;
|
||||
}
|
||||
|
||||
setAttributes({
|
||||
logos: logos.concat({
|
||||
id: media.id || 0,
|
||||
url: media.url,
|
||||
alt: media.alt || media.title || '',
|
||||
link: '',
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function removeLogo(index) {
|
||||
setAttributes({
|
||||
logos: logos.filter(function (_, logoIndex) {
|
||||
return logoIndex !== index;
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
function moveLogo(index, direction) {
|
||||
const nextIndex = index + direction;
|
||||
const nextLogos = logos.slice();
|
||||
const currentLogo = nextLogos[index];
|
||||
|
||||
if (nextIndex < 0 || nextIndex >= logos.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
nextLogos[index] = nextLogos[nextIndex];
|
||||
nextLogos[nextIndex] = currentLogo;
|
||||
setAttributes({ logos: nextLogos });
|
||||
}
|
||||
|
||||
function renderMediaButton(label) {
|
||||
return el(MediaUploadCheck, null, el(MediaUpload, {
|
||||
onSelect: addLogo,
|
||||
allowedTypes: ['image'],
|
||||
render: function (mediaProps) {
|
||||
return el(
|
||||
Button,
|
||||
{
|
||||
variant: 'primary',
|
||||
onClick: mediaProps.open,
|
||||
},
|
||||
label
|
||||
);
|
||||
},
|
||||
}));
|
||||
}
|
||||
|
||||
function renderLogoCard(logo, index) {
|
||||
const cardKey = String(logo.id || 'logo') + '-' + String(index);
|
||||
|
||||
return el(
|
||||
'div',
|
||||
{
|
||||
className: 'logo-soup-editor__card',
|
||||
key: cardKey,
|
||||
},
|
||||
[
|
||||
el(
|
||||
'div',
|
||||
{
|
||||
className: 'logo-soup-editor__thumb',
|
||||
key: 'thumb',
|
||||
},
|
||||
el('img', {
|
||||
src: logo.url,
|
||||
alt: logo.alt || '',
|
||||
})
|
||||
),
|
||||
el(TextControl, {
|
||||
key: 'alt',
|
||||
label: __('Alt text', 'logo-soup'),
|
||||
value: logo.alt || '',
|
||||
onChange: function (value) {
|
||||
setAttributes({
|
||||
logos: updateLogoAtIndex(logos, index, {
|
||||
id: logo.id,
|
||||
url: logo.url,
|
||||
alt: value,
|
||||
link: logo.link,
|
||||
}),
|
||||
});
|
||||
},
|
||||
}),
|
||||
el(TextControl, {
|
||||
key: 'link',
|
||||
label: __('Link URL', 'logo-soup'),
|
||||
value: logo.link || '',
|
||||
placeholder: 'https://example.com',
|
||||
onChange: function (value) {
|
||||
setAttributes({
|
||||
logos: updateLogoAtIndex(logos, index, {
|
||||
id: logo.id,
|
||||
url: logo.url,
|
||||
alt: logo.alt,
|
||||
link: value,
|
||||
}),
|
||||
});
|
||||
},
|
||||
}),
|
||||
el(
|
||||
'div',
|
||||
{
|
||||
className: 'logo-soup-editor__actions',
|
||||
key: 'actions',
|
||||
},
|
||||
[
|
||||
el(
|
||||
Button,
|
||||
{
|
||||
key: 'up',
|
||||
disabled: index === 0,
|
||||
onClick: function () {
|
||||
moveLogo(index, -1);
|
||||
},
|
||||
},
|
||||
__('Move Up', 'logo-soup')
|
||||
),
|
||||
el(
|
||||
Button,
|
||||
{
|
||||
key: 'down',
|
||||
disabled: index === logos.length - 1,
|
||||
onClick: function () {
|
||||
moveLogo(index, 1);
|
||||
},
|
||||
},
|
||||
__('Move Down', 'logo-soup')
|
||||
),
|
||||
el(
|
||||
Button,
|
||||
{
|
||||
key: 'remove',
|
||||
isDestructive: true,
|
||||
onClick: function () {
|
||||
removeLogo(index);
|
||||
},
|
||||
},
|
||||
__('Remove', 'logo-soup')
|
||||
),
|
||||
]
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
return el(Fragment, null, [
|
||||
el(
|
||||
InspectorControls,
|
||||
{
|
||||
key: 'controls',
|
||||
},
|
||||
el(
|
||||
PanelBody,
|
||||
{
|
||||
title: __('Display', 'logo-soup'),
|
||||
},
|
||||
[
|
||||
el(SelectControl, {
|
||||
key: 'layout',
|
||||
label: __('Layout', 'logo-soup'),
|
||||
value: layout,
|
||||
options: [
|
||||
{ label: __('Carousel', 'logo-soup'), value: 'carousel' },
|
||||
{ label: __('Grid', 'logo-soup'), value: 'grid' },
|
||||
],
|
||||
onChange: function (value) {
|
||||
setAttributes({ layout: value });
|
||||
},
|
||||
}),
|
||||
el(ToggleControl, {
|
||||
key: 'grayscale',
|
||||
label: __('Use grayscale styling', 'logo-soup'),
|
||||
checked: grayscale,
|
||||
onChange: function (value) {
|
||||
setAttributes({ grayscale: value });
|
||||
},
|
||||
}),
|
||||
el(ToggleControl, {
|
||||
key: 'pauseOnHover',
|
||||
label: __('Pause carousel on hover', 'logo-soup'),
|
||||
checked: pauseOnHover,
|
||||
help: __('Applies to carousel layout only.', 'logo-soup'),
|
||||
onChange: function (value) {
|
||||
setAttributes({ pauseOnHover: value });
|
||||
},
|
||||
}),
|
||||
el(RangeControl, {
|
||||
key: 'speed',
|
||||
label: __('Carousel speed (seconds)', 'logo-soup'),
|
||||
value: speed,
|
||||
min: 8,
|
||||
max: 80,
|
||||
onChange: function (value) {
|
||||
setAttributes({ speed: value });
|
||||
},
|
||||
}),
|
||||
el(RangeControl, {
|
||||
key: 'baseSize',
|
||||
label: __('Base size', 'logo-soup'),
|
||||
value: baseSize,
|
||||
min: 16,
|
||||
max: 160,
|
||||
onChange: function (value) {
|
||||
setAttributes({ baseSize: value });
|
||||
},
|
||||
}),
|
||||
el(RangeControl, {
|
||||
key: 'scaleFactor',
|
||||
label: __('Scale factor', 'logo-soup'),
|
||||
value: scaleFactor,
|
||||
min: 0.1,
|
||||
max: 2,
|
||||
step: 0.1,
|
||||
onChange: function (value) {
|
||||
setAttributes({ scaleFactor: value });
|
||||
},
|
||||
}),
|
||||
el(ToggleControl, {
|
||||
key: 'densityAware',
|
||||
label: __('Density aware', 'logo-soup'),
|
||||
checked: densityAware,
|
||||
onChange: function (value) {
|
||||
setAttributes({ densityAware: value });
|
||||
},
|
||||
}),
|
||||
el(RangeControl, {
|
||||
key: 'densityFactor',
|
||||
label: __('Density factor', 'logo-soup'),
|
||||
value: densityFactor,
|
||||
min: 0,
|
||||
max: 1,
|
||||
step: 0.1,
|
||||
onChange: function (value) {
|
||||
setAttributes({ densityFactor: value });
|
||||
},
|
||||
}),
|
||||
el(ToggleControl, {
|
||||
key: 'cropToContent',
|
||||
label: __('Crop to content', 'logo-soup'),
|
||||
checked: cropToContent,
|
||||
onChange: function (value) {
|
||||
setAttributes({ cropToContent: value });
|
||||
},
|
||||
}),
|
||||
el(SelectControl, {
|
||||
key: 'alignBy',
|
||||
label: __('Align by', 'logo-soup'),
|
||||
value: alignBy,
|
||||
options: [
|
||||
{ label: __('Bounds', 'logo-soup'), value: 'bounds' },
|
||||
{
|
||||
label: __('Visual center', 'logo-soup'),
|
||||
value: 'visual-center',
|
||||
},
|
||||
{
|
||||
label: __('Visual center X', 'logo-soup'),
|
||||
value: 'visual-center-x',
|
||||
},
|
||||
{
|
||||
label: __('Visual center Y', 'logo-soup'),
|
||||
value: 'visual-center-y',
|
||||
},
|
||||
],
|
||||
onChange: function (value) {
|
||||
setAttributes({ alignBy: value });
|
||||
},
|
||||
}),
|
||||
]
|
||||
)
|
||||
),
|
||||
el(
|
||||
'div',
|
||||
blockProps,
|
||||
logos.length === 0
|
||||
? el(
|
||||
Placeholder,
|
||||
{
|
||||
label: __('Logo Soup', 'logo-soup'),
|
||||
instructions: __(
|
||||
'Upload a set of logos and optional links.',
|
||||
'logo-soup'
|
||||
),
|
||||
},
|
||||
renderMediaButton(__('Add Logo', 'logo-soup'))
|
||||
)
|
||||
: el(
|
||||
'div',
|
||||
{
|
||||
className:
|
||||
'logo-soup-editor__preview layout-' +
|
||||
layout +
|
||||
(grayscale ? ' is-grayscale' : '') +
|
||||
(cropToContent ? ' crop-to-content' : '') +
|
||||
(densityAware ? ' is-density-aware' : '') +
|
||||
' align-by-' +
|
||||
alignBy,
|
||||
style: {
|
||||
'--logo-soup-base-size': String(baseSize) + 'px',
|
||||
'--logo-soup-scale-factor': String(scaleFactor),
|
||||
'--logo-soup-density-factor': String(densityFactor),
|
||||
},
|
||||
},
|
||||
[
|
||||
el(
|
||||
'div',
|
||||
{
|
||||
className: 'logo-soup-editor__toolbar',
|
||||
key: 'toolbar',
|
||||
},
|
||||
renderMediaButton(__('Add Another Logo', 'logo-soup'))
|
||||
),
|
||||
el(
|
||||
'div',
|
||||
{
|
||||
className: 'logo-soup-editor__list',
|
||||
key: 'list',
|
||||
},
|
||||
logos.map(renderLogoCard)
|
||||
),
|
||||
]
|
||||
)
|
||||
),
|
||||
]);
|
||||
},
|
||||
save: function () {
|
||||
return null;
|
||||
},
|
||||
});
|
||||
}(window.wp.blocks, window.wp.blockEditor, window.wp.components, window.wp.element, window.wp.i18n));
|
||||
@@ -0,0 +1,116 @@
|
||||
.logo-soup {
|
||||
--logo-soup-gap: clamp(1rem, 2vw, 2rem);
|
||||
--logo-soup-base-size: 48px;
|
||||
--logo-soup-effective-size: var(--logo-soup-base-size);
|
||||
--logo-soup-gap-factor: 1;
|
||||
--logo-soup-width-factor: 3.5;
|
||||
--logo-soup-gray-opacity: 0.72;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-soup__track {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
gap: calc(var(--logo-soup-gap) * var(--logo-soup-gap-factor));
|
||||
width: max-content;
|
||||
}
|
||||
|
||||
.logo-soup.layout-carousel .logo-soup__track {
|
||||
animation: logo-soup-scroll var(--logo-soup-speed, 22s) linear infinite;
|
||||
}
|
||||
|
||||
.logo-soup.layout-grid .logo-soup__track {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(
|
||||
auto-fit,
|
||||
minmax(min(100%, calc(var(--logo-soup-effective-size) * 2.4)), 1fr)
|
||||
);
|
||||
justify-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-soup.pause-on-hover:hover .logo-soup__track {
|
||||
animation-play-state: paused;
|
||||
}
|
||||
|
||||
.logo-soup__item {
|
||||
align-items: center;
|
||||
display: inline-flex;
|
||||
flex: 0 0 auto;
|
||||
justify-content: center;
|
||||
min-height: calc(var(--logo-soup-effective-size) + 1.5rem);
|
||||
padding: 0.75rem 1rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.logo-soup.layout-grid .logo-soup__item {
|
||||
padding-inline: calc(var(--logo-soup-gap) * 0.25);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.logo-soup__image {
|
||||
display: block;
|
||||
height: var(--logo-soup-effective-size);
|
||||
max-width: min(calc(var(--logo-soup-effective-size) * var(--logo-soup-width-factor)), 22vw);
|
||||
object-fit: contain;
|
||||
opacity: 0.92;
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: filter 0.2s ease, opacity 0.2s ease, transform 0.2s ease;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.logo-soup.layout-grid .logo-soup__image {
|
||||
margin-inline: auto;
|
||||
max-width: min(100%, calc(var(--logo-soup-effective-size) * var(--logo-soup-width-factor)));
|
||||
}
|
||||
|
||||
.logo-soup.crop-to-content .logo-soup__item {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
|
||||
.logo-soup.align-by-visual-center .logo-soup__image {
|
||||
transform: translate(-2%, -2%);
|
||||
}
|
||||
|
||||
.logo-soup.align-by-visual-center-x .logo-soup__image {
|
||||
transform: translateX(-2%);
|
||||
}
|
||||
|
||||
.logo-soup.align-by-visual-center-y .logo-soup__image {
|
||||
transform: translateY(-2%);
|
||||
}
|
||||
|
||||
.logo-soup.is-grayscale .logo-soup__image {
|
||||
filter: grayscale(100%);
|
||||
opacity: var(--logo-soup-gray-opacity);
|
||||
}
|
||||
|
||||
.logo-soup__item:hover .logo-soup__image,
|
||||
.logo-soup__item:focus .logo-soup__image {
|
||||
filter: grayscale(0%);
|
||||
opacity: 1;
|
||||
transform: translateY(-1px);
|
||||
}
|
||||
|
||||
@keyframes logo-soup-scroll {
|
||||
from {
|
||||
transform: translateX(0%);
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateX(calc(-50% - (var(--logo-soup-gap) / 2)));
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 781px) {
|
||||
.logo-soup__item {
|
||||
min-height: calc(var(--logo-soup-effective-size) + 1rem);
|
||||
padding: 0.5rem 0.75rem;
|
||||
}
|
||||
|
||||
.logo-soup__image {
|
||||
max-width: min(calc(var(--logo-soup-effective-size) * 2.5), 34vw);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"name": "ksolo/logo-soup",
|
||||
"description": "WordPress block plugin for displaying harmonized logo collections.",
|
||||
"type": "wordpress-plugin",
|
||||
"license": "GPL-2.0-or-later",
|
||||
"require": {
|
||||
"php": ">=7.4"
|
||||
},
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"LogoSoup\\": "src/"
|
||||
}
|
||||
},
|
||||
"require-dev": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": "^1.1",
|
||||
"phpstan/phpstan": "^1.12",
|
||||
"phpunit/phpunit": "^9.6",
|
||||
"squizlabs/php_codesniffer": "^3.10",
|
||||
"wp-coding-standards/wpcs": "^3.1"
|
||||
},
|
||||
"scripts": {
|
||||
"phpcs": "phpcs",
|
||||
"phpstan": "phpstan analyse",
|
||||
"test": "phpunit --configuration phpunit.xml.dist"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true
|
||||
}
|
||||
}
|
||||
}
|
||||
Generated
+2287
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin Name: Logo Soup
|
||||
* Description: Build balanced logo grids and marquees directly inside the WordPress block editor.
|
||||
* Version: 0.1.0
|
||||
* Requires at least: 6.1
|
||||
* Requires PHP: 7.4
|
||||
* Author: Ksolo
|
||||
* License: GPL-2.0-or-later
|
||||
* Text Domain: logo-soup
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
if ( ! defined( 'ABSPATH' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
spl_autoload_register(
|
||||
static function ( $autoload_class ) {
|
||||
$prefix = 'LogoSoup\\';
|
||||
|
||||
if ( 0 !== strpos( $autoload_class, $prefix ) ) {
|
||||
return;
|
||||
}
|
||||
|
||||
$relative_class = substr( $autoload_class, strlen( $prefix ) );
|
||||
$parts = explode( '\\', $relative_class );
|
||||
$class_name = array_pop( $parts );
|
||||
$class_slug = 'class-' . strtolower( str_replace( '_', '-', $class_name ) ) . '.php';
|
||||
$directory_path = '';
|
||||
|
||||
if ( ! empty( $parts ) ) {
|
||||
$directory_path = implode( DIRECTORY_SEPARATOR, $parts ) . DIRECTORY_SEPARATOR;
|
||||
}
|
||||
|
||||
$file_path = __DIR__ . '/src/' . $directory_path . $class_slug;
|
||||
|
||||
if ( file_exists( $file_path ) ) {
|
||||
require_once $file_path;
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
LogoSoup\Plugin::boot( __FILE__ );
|
||||
Generated
+3233
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"name": "logo-soup",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"description": "WordPress block plugin for logo showcases.",
|
||||
"scripts": {
|
||||
"lint:js": "eslint \"blocks/**/*.js\"",
|
||||
"format": "prettier --check \"**/*.{js,json,md,css}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-config-airbnb-base": "^15.0.0",
|
||||
"eslint-plugin-import": "^2.31.0",
|
||||
"prettier": "^3.3.3"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?xml version="1.0"?>
|
||||
<ruleset name="Logo Soup">
|
||||
<description>PHPCS rules for the Logo Soup plugin.</description>
|
||||
|
||||
<arg name="basepath" value="."/>
|
||||
<arg name="extensions" value="php"/>
|
||||
<arg name="parallel" value="4"/>
|
||||
|
||||
<file>logo-soup.php</file>
|
||||
<file>src</file>
|
||||
<file>uninstall.php</file>
|
||||
<file>tests/php</file>
|
||||
|
||||
<exclude-pattern>vendor/*</exclude-pattern>
|
||||
<exclude-pattern>tests/php/wp-stubs.php</exclude-pattern>
|
||||
|
||||
<rule ref="WordPress-Core"/>
|
||||
<rule ref="WordPress-Extra"/>
|
||||
<rule ref="WordPress-Docs"/>
|
||||
</ruleset>
|
||||
@@ -0,0 +1,9 @@
|
||||
parameters:
|
||||
level: 6
|
||||
paths:
|
||||
- src
|
||||
- logo-soup.php
|
||||
- uninstall.php
|
||||
bootstrapFiles:
|
||||
- tests/php/bootstrap.php
|
||||
treatPhpDocTypesAsCertain: false
|
||||
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit bootstrap="tests/php/bootstrap.php" colors="true">
|
||||
<testsuites>
|
||||
<testsuite name="Logo Soup">
|
||||
<directory>tests/php</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
</phpunit>
|
||||
@@ -0,0 +1,10 @@
|
||||
# Logo Soup WordPress Block
|
||||
|
||||
## Overview
|
||||
The Logo Soup WordPress plugin is designed to provide users with an easy way to create and manage a collection of logos on their WordPress site. The plugin allows users to upload logos, add links, and display them in a format that implements the library found here: https://github.com/sanity-labs/logo-soup. The plugin is user-friendly and can be easily integrated into any WordPress theme.
|
||||
|
||||
## Features
|
||||
- **Logo Upload**: Users can upload logos directly from the WordPress post/page editor.
|
||||
- **Link Management**: Each logo can have an associated link that directs users to a specified URL when clicked.
|
||||
- **Customizable Display**: Users can choose how the logos are displayed on their site, including options for layout and styling.
|
||||
- **Responsive Design**: The plugin ensures that logos are displayed correctly on all devices, including desktops, tablets, and smartphones.
|
||||
+25
@@ -0,0 +1,25 @@
|
||||
=== Logo Soup ===
|
||||
Contributors: ksolo
|
||||
Requires at least: 6.1
|
||||
Tested up to: 6.8
|
||||
Requires PHP: 7.4
|
||||
Stable tag: 0.1.0
|
||||
License: GPLv2 or later
|
||||
License URI: https://www.gnu.org/licenses/gpl-2.0.html
|
||||
|
||||
Create responsive logo grids and marquees directly in the block editor.
|
||||
|
||||
== Description ==
|
||||
|
||||
Logo Soup adds a dynamic block for uploading logos, attaching optional links, and rendering them in either a responsive grid or animated carousel.
|
||||
|
||||
== Installation ==
|
||||
|
||||
1. Upload the plugin to `/wp-content/plugins/logo-soup`.
|
||||
2. Activate the plugin through the WordPress admin.
|
||||
3. Insert the `Logo Soup` block into any post or page.
|
||||
|
||||
== Changelog ==
|
||||
|
||||
= 0.1.0 =
|
||||
* Initial release.
|
||||
@@ -0,0 +1,307 @@
|
||||
<?php
|
||||
/**
|
||||
* Settings page.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
namespace LogoSoup\Admin;
|
||||
|
||||
use LogoSoup\Settings;
|
||||
|
||||
/**
|
||||
* Renders the admin settings page.
|
||||
*/
|
||||
class Settings_Page {
|
||||
/**
|
||||
* Register page and fields.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register() {
|
||||
add_options_page(
|
||||
__( 'Logo Soup', 'logo-soup' ),
|
||||
__( 'Logo Soup', 'logo-soup' ),
|
||||
'manage_options',
|
||||
'logo-soup',
|
||||
array( __CLASS__, 'render' )
|
||||
);
|
||||
|
||||
add_settings_section(
|
||||
'logo_soup_defaults',
|
||||
__( 'Block Defaults', 'logo-soup' ),
|
||||
'__return_false',
|
||||
'logo_soup'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'default_layout',
|
||||
__( 'Default Layout', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_layout_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'default_speed',
|
||||
__( 'Carousel Speed', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_speed_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'default_gray',
|
||||
__( 'Use Grayscale by Default', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_gray_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'base_size',
|
||||
__( 'Base Size', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_base_size_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'scale_factor',
|
||||
__( 'Scale Factor', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_scale_factor_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'density_aware',
|
||||
__( 'Density Aware', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_density_aware_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'density_factor',
|
||||
__( 'Density Factor', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_density_factor_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'crop_to_content',
|
||||
__( 'Crop To Content', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_crop_to_content_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'align_by',
|
||||
__( 'Align By', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_align_by_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'open_new_tab',
|
||||
__( 'Open Links in New Tab', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_new_tab_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
|
||||
add_settings_field(
|
||||
'cleanup_on_drop',
|
||||
__( 'Cleanup on Uninstall', 'logo-soup' ),
|
||||
array( __CLASS__, 'render_cleanup_field' ),
|
||||
'logo_soup',
|
||||
'logo_soup_defaults'
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render the settings page.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render() {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
<div class="wrap">
|
||||
<h1><?php echo esc_html__( 'Logo Soup Settings', 'logo-soup' ); ?></h1>
|
||||
<form action="options.php" method="post">
|
||||
<?php
|
||||
settings_fields( 'logo_soup' );
|
||||
do_settings_sections( 'logo_soup' );
|
||||
submit_button();
|
||||
?>
|
||||
</form>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render layout field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_layout_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<select name="logo_soup_settings[default_layout]">
|
||||
<option value="carousel" <?php selected( $settings['default_layout'], 'carousel' ); ?>><?php echo esc_html__( 'Carousel', 'logo-soup' ); ?></option>
|
||||
<option value="grid" <?php selected( $settings['default_layout'], 'grid' ); ?>><?php echo esc_html__( 'Grid', 'logo-soup' ); ?></option>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render speed field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_speed_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<input type="number" min="8" max="80" name="logo_soup_settings[default_speed]" value="<?php echo esc_attr( (string) $settings['default_speed'] ); ?>" />
|
||||
<p class="description"><?php echo esc_html__( 'Duration in seconds for one carousel loop.', 'logo-soup' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render gray field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_gray_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<label>
|
||||
<input type="checkbox" name="logo_soup_settings[default_gray]" value="1" <?php checked( ! empty( $settings['default_gray'] ) ); ?> />
|
||||
<?php echo esc_html__( 'Render logos in grayscale until hover.', 'logo-soup' ); ?>
|
||||
</label>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render base size field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_base_size_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<input type="number" min="16" max="160" name="logo_soup_settings[base_size]" value="<?php echo esc_attr( (string) $settings['base_size'] ); ?>" />
|
||||
<p class="description"><?php echo esc_html__( 'Baseline logo size used during normalization.', 'logo-soup' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render scale factor field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_scale_factor_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<input type="number" min="0.1" max="2" step="0.1" name="logo_soup_settings[scale_factor]" value="<?php echo esc_attr( (string) $settings['scale_factor'] ); ?>" />
|
||||
<p class="description"><?php echo esc_html__( 'Controls how strongly aspect ratio affects normalized size.', 'logo-soup' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render density aware field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_density_aware_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<label>
|
||||
<input type="checkbox" name="logo_soup_settings[density_aware]" value="1" <?php checked( ! empty( $settings['density_aware'] ) ); ?> />
|
||||
<?php echo esc_html__( 'Adjust visual weight based on logo pixel density.', 'logo-soup' ); ?>
|
||||
</label>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render density factor field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_density_factor_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<input type="number" min="0" max="1" step="0.1" name="logo_soup_settings[density_factor]" value="<?php echo esc_attr( (string) $settings['density_factor'] ); ?>" />
|
||||
<p class="description"><?php echo esc_html__( '0 disables the effect, 1 makes density the dominant factor.', 'logo-soup' ); ?></p>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render crop-to-content field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_crop_to_content_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<label>
|
||||
<input type="checkbox" name="logo_soup_settings[crop_to_content]" value="1" <?php checked( ! empty( $settings['crop_to_content'] ) ); ?> />
|
||||
<?php echo esc_html__( 'Use the visible logo content instead of the full image bounds.', 'logo-soup' ); ?>
|
||||
</label>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render align-by field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_align_by_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<select name="logo_soup_settings[align_by]">
|
||||
<option value="bounds" <?php selected( $settings['align_by'], 'bounds' ); ?>><?php echo esc_html__( 'Bounds', 'logo-soup' ); ?></option>
|
||||
<option value="visual-center" <?php selected( $settings['align_by'], 'visual-center' ); ?>><?php echo esc_html__( 'Visual Center', 'logo-soup' ); ?></option>
|
||||
<option value="visual-center-x" <?php selected( $settings['align_by'], 'visual-center-x' ); ?>><?php echo esc_html__( 'Visual Center X', 'logo-soup' ); ?></option>
|
||||
<option value="visual-center-y" <?php selected( $settings['align_by'], 'visual-center-y' ); ?>><?php echo esc_html__( 'Visual Center Y', 'logo-soup' ); ?></option>
|
||||
</select>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render new tab field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_new_tab_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<label>
|
||||
<input type="checkbox" name="logo_soup_settings[open_new_tab]" value="1" <?php checked( ! empty( $settings['open_new_tab'] ) ); ?> />
|
||||
<?php echo esc_html__( 'Use target="_blank" for linked logos by default.', 'logo-soup' ); ?>
|
||||
</label>
|
||||
<?php
|
||||
}
|
||||
|
||||
/**
|
||||
* Render cleanup field.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function render_cleanup_field() {
|
||||
$settings = Settings::get();
|
||||
?>
|
||||
<label>
|
||||
<input type="checkbox" name="logo_soup_settings[cleanup_on_drop]" value="1" <?php checked( ! empty( $settings['cleanup_on_drop'] ) ); ?> />
|
||||
<?php echo esc_html__( 'Delete saved settings when uninstalling the plugin.', 'logo-soup' ); ?>
|
||||
</label>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
<?php
|
||||
/**
|
||||
* Block registration.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
namespace LogoSoup\Blocks;
|
||||
|
||||
use LogoSoup\Plugin;
|
||||
use LogoSoup\Settings;
|
||||
use LogoSoup\Support\Logo_Sanitizer;
|
||||
|
||||
/**
|
||||
* Registers and renders the Logo Soup block.
|
||||
*/
|
||||
class Logo_Soup_Block {
|
||||
/**
|
||||
* Register the block.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register() {
|
||||
register_block_type(
|
||||
Plugin::asset_path( 'blocks/logo-soup/block.json' ),
|
||||
array(
|
||||
'render_callback' => array( __CLASS__, 'render' ),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Render callback.
|
||||
*
|
||||
* @param array<string, mixed> $attributes Block attributes.
|
||||
* @return string
|
||||
*/
|
||||
public static function render( $attributes ) {
|
||||
$settings = Settings::get();
|
||||
$logos = Logo_Sanitizer::sanitize_collection( isset( $attributes['logos'] ) ? $attributes['logos'] : array() );
|
||||
$layout = isset( $attributes['layout'] ) ? sanitize_key( $attributes['layout'] ) : $settings['default_layout'];
|
||||
$layout = in_array( $layout, array( 'carousel', 'grid' ), true ) ? $layout : $settings['default_layout'];
|
||||
$speed = isset( $attributes['speed'] ) ? absint( $attributes['speed'] ) : (int) $settings['default_speed'];
|
||||
$speed = max( 8, min( 80, $speed ) );
|
||||
$grayscale = isset( $attributes['grayscale'] ) ? (bool) $attributes['grayscale'] : ! empty( $settings['default_gray'] );
|
||||
$pause_on_hover = isset( $attributes['pauseOnHover'] ) ? (bool) $attributes['pauseOnHover'] : true;
|
||||
$base_size = isset( $attributes['baseSize'] ) ? absint( $attributes['baseSize'] ) : (int) $settings['base_size'];
|
||||
$base_size = max( 16, min( 160, $base_size ) );
|
||||
$scale_factor = self::sanitize_float(
|
||||
isset( $attributes['scaleFactor'] ) ? $attributes['scaleFactor'] : $settings['scale_factor'],
|
||||
0.5,
|
||||
0.1,
|
||||
2
|
||||
);
|
||||
$density_aware = isset( $attributes['densityAware'] ) ? (bool) $attributes['densityAware'] : ! empty( $settings['density_aware'] );
|
||||
$density_factor = self::sanitize_float(
|
||||
isset( $attributes['densityFactor'] ) ? $attributes['densityFactor'] : $settings['density_factor'],
|
||||
0.5,
|
||||
0,
|
||||
1
|
||||
);
|
||||
$crop_to_content = isset( $attributes['cropToContent'] ) ? (bool) $attributes['cropToContent'] : ! empty( $settings['crop_to_content'] );
|
||||
$align_by = isset( $attributes['alignBy'] ) ? sanitize_key( $attributes['alignBy'] ) : $settings['align_by'];
|
||||
$align_by = in_array( $align_by, array( 'bounds', 'visual-center', 'visual-center-x', 'visual-center-y' ), true ) ? $align_by : $settings['align_by'];
|
||||
$open_new_tab = ! empty( $settings['open_new_tab'] );
|
||||
$size_multiplier = 0.75 + ( $scale_factor * 0.5 );
|
||||
$gap_multiplier = 1;
|
||||
$width_factor = 3 + $scale_factor;
|
||||
$opacity_floor = 0.72;
|
||||
|
||||
if ( $density_aware ) {
|
||||
$size_multiplier += ( $density_factor * 0.35 );
|
||||
$gap_multiplier = max( 0.7, 1 - ( $density_factor * 0.22 ) );
|
||||
$width_factor += ( $density_factor * 0.6 );
|
||||
$opacity_floor = max( 0.45, 0.78 - ( $density_factor * 0.18 ) );
|
||||
}
|
||||
|
||||
$effective_size = (int) round( $base_size * $size_multiplier );
|
||||
|
||||
if ( empty( $logos ) ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
$wrapper_classes = array( 'logo-soup', 'layout-' . $layout );
|
||||
|
||||
if ( $grayscale ) {
|
||||
$wrapper_classes[] = 'is-grayscale';
|
||||
}
|
||||
|
||||
if ( $pause_on_hover ) {
|
||||
$wrapper_classes[] = 'pause-on-hover';
|
||||
}
|
||||
|
||||
if ( $density_aware ) {
|
||||
$wrapper_classes[] = 'is-density-aware';
|
||||
}
|
||||
|
||||
if ( $crop_to_content ) {
|
||||
$wrapper_classes[] = 'crop-to-content';
|
||||
}
|
||||
|
||||
$wrapper_classes[] = 'align-by-' . $align_by;
|
||||
|
||||
$style = sprintf(
|
||||
'--logo-soup-speed:%1$ds;--logo-soup-base-size:%2$dpx;--logo-soup-scale-factor:%3$s;--logo-soup-density-factor:%4$s;--logo-soup-effective-size:%5$dpx;--logo-soup-gap-factor:%6$s;--logo-soup-width-factor:%7$s;--logo-soup-gray-opacity:%8$s;',
|
||||
$speed,
|
||||
$base_size,
|
||||
self::format_float( $scale_factor ),
|
||||
self::format_float( $density_factor ),
|
||||
$effective_size,
|
||||
self::format_float( $gap_multiplier ),
|
||||
self::format_float( $width_factor ),
|
||||
self::format_float( $opacity_floor )
|
||||
);
|
||||
|
||||
ob_start();
|
||||
?>
|
||||
<div
|
||||
class="<?php echo esc_attr( implode( ' ', $wrapper_classes ) ); ?>"
|
||||
style="<?php echo esc_attr( $style ); ?>"
|
||||
data-base-size="<?php echo esc_attr( (string) $base_size ); ?>"
|
||||
data-scale-factor="<?php echo esc_attr( self::format_float( $scale_factor ) ); ?>"
|
||||
data-density-aware="<?php echo esc_attr( $density_aware ? 'true' : 'false' ); ?>"
|
||||
data-density-factor="<?php echo esc_attr( self::format_float( $density_factor ) ); ?>"
|
||||
data-crop-to-content="<?php echo esc_attr( $crop_to_content ? 'true' : 'false' ); ?>"
|
||||
data-align-by="<?php echo esc_attr( $align_by ); ?>"
|
||||
>
|
||||
<div class="logo-soup__track">
|
||||
<?php self::render_logo_items( $logos, $open_new_tab ); ?>
|
||||
<?php if ( 'carousel' === $layout ) : ?>
|
||||
<?php self::render_logo_items( $logos, $open_new_tab, true ); ?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
</div>
|
||||
<?php
|
||||
|
||||
return (string) ob_get_clean();
|
||||
}
|
||||
|
||||
/**
|
||||
* Render individual logo items.
|
||||
*
|
||||
* @param array<int, array<string, mixed>> $logos Logos.
|
||||
* @param bool $open_new_tab Open in new tab.
|
||||
* @param bool $duplicate Duplicate set.
|
||||
* @return void
|
||||
*/
|
||||
private static function render_logo_items( array $logos, $open_new_tab, $duplicate = false ) {
|
||||
foreach ( $logos as $index => $logo ) {
|
||||
$tag = ! empty( $logo['link'] ) ? 'a' : 'div';
|
||||
$href_attr = ! empty( $logo['link'] ) ? ' href="' . esc_url( $logo['link'] ) . '"' : '';
|
||||
$rel_attr = ( ! empty( $logo['link'] ) && $open_new_tab ) ? ' rel="noreferrer noopener"' : '';
|
||||
$target = ( ! empty( $logo['link'] ) && $open_new_tab ) ? ' target="_blank"' : '';
|
||||
$key = $duplicate ? 'dup-' . $index : (string) $index;
|
||||
?>
|
||||
<<?php echo esc_html( $tag ); ?> class="logo-soup__item" data-key="<?php echo esc_attr( $key ); ?>"<?php echo $href_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo $target; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?><?php echo $rel_attr; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>>
|
||||
<img class="logo-soup__image" src="<?php echo esc_url( $logo['url'] ); ?>" alt="<?php echo esc_attr( (string) $logo['alt'] ); ?>" loading="lazy" decoding="async" />
|
||||
</<?php echo esc_html( $tag ); ?>>
|
||||
<?php
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a float-like block attribute.
|
||||
*
|
||||
* @param mixed $value Value.
|
||||
* @param float $fallback Fallback.
|
||||
* @param float $min Minimum.
|
||||
* @param float $max Maximum.
|
||||
* @return float
|
||||
*/
|
||||
private static function sanitize_float( $value, $fallback, $min, $max ) {
|
||||
$value = is_numeric( $value ) ? (float) $value : $fallback;
|
||||
$value = max( $min, min( $max, $value ) );
|
||||
|
||||
return round( $value, 2 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Format a float for HTML attributes and CSS variables.
|
||||
*
|
||||
* @param float $value Value.
|
||||
* @return string
|
||||
*/
|
||||
private static function format_float( $value ) {
|
||||
return rtrim( rtrim( number_format( $value, 2, '.', '' ), '0' ), '.' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
/**
|
||||
* REST settings controller.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
namespace LogoSoup\REST;
|
||||
|
||||
use LogoSoup\Settings;
|
||||
use WP_Error;
|
||||
use WP_REST_Request;
|
||||
use WP_REST_Response;
|
||||
use WP_REST_Server;
|
||||
|
||||
/**
|
||||
* REST API for plugin settings.
|
||||
*/
|
||||
class Settings_Controller {
|
||||
/**
|
||||
* Register routes.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_routes() {
|
||||
register_rest_route(
|
||||
'logo-soup/v1',
|
||||
'/settings',
|
||||
array(
|
||||
array(
|
||||
'methods' => WP_REST_Server::READABLE,
|
||||
'callback' => array( __CLASS__, 'get_item' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
),
|
||||
array(
|
||||
'methods' => WP_REST_Server::EDITABLE,
|
||||
'callback' => array( __CLASS__, 'update_item' ),
|
||||
'permission_callback' => array( __CLASS__, 'permissions_check' ),
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check permissions.
|
||||
*
|
||||
* @param WP_REST_Request $request Request object.
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function permissions_check( WP_REST_Request $request ) {
|
||||
if ( ! current_user_can( 'manage_options' ) ) {
|
||||
return new WP_Error( 'logo_soup_forbidden', __( 'You are not allowed to manage Logo Soup settings.', 'logo-soup' ), array( 'status' => 403 ) );
|
||||
}
|
||||
|
||||
if ( 'GET' !== $request->get_method() ) {
|
||||
$nonce = $request->get_header( 'X-WP-Nonce' );
|
||||
|
||||
if ( ! wp_verify_nonce( $nonce, 'wp_rest' ) ) {
|
||||
return new WP_Error( 'logo_soup_bad_nonce', __( 'Invalid REST nonce.', 'logo-soup' ), array( 'status' => 403 ) );
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get settings.
|
||||
*
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function get_item() {
|
||||
return new WP_REST_Response( Settings::get(), 200 );
|
||||
}
|
||||
|
||||
/**
|
||||
* Update settings.
|
||||
*
|
||||
* @param WP_REST_Request $request Request.
|
||||
* @return WP_REST_Response
|
||||
*/
|
||||
public static function update_item( WP_REST_Request $request ) {
|
||||
$updated = Settings::sanitize( $request->get_json_params() );
|
||||
update_option( 'logo_soup_settings', $updated );
|
||||
|
||||
return new WP_REST_Response( $updated, 200 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
/**
|
||||
* Logo sanitizer helpers.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
namespace LogoSoup\Support;
|
||||
|
||||
/**
|
||||
* Sanitizes logo arrays from block attributes.
|
||||
*/
|
||||
class Logo_Sanitizer {
|
||||
/**
|
||||
* Sanitize a list of logos.
|
||||
*
|
||||
* @param mixed $logos Raw logos.
|
||||
* @return array<int, array<string, mixed>>
|
||||
*/
|
||||
public static function sanitize_collection( $logos ) {
|
||||
if ( ! is_array( $logos ) ) {
|
||||
return array();
|
||||
}
|
||||
|
||||
$sanitized = array();
|
||||
|
||||
foreach ( $logos as $logo ) {
|
||||
if ( ! is_array( $logo ) ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$image_url = isset( $logo['url'] ) ? esc_url_raw( $logo['url'] ) : '';
|
||||
$alt = isset( $logo['alt'] ) ? sanitize_text_field( $logo['alt'] ) : '';
|
||||
$link = isset( $logo['link'] ) ? esc_url_raw( $logo['link'] ) : '';
|
||||
$id = isset( $logo['id'] ) ? absint( $logo['id'] ) : 0;
|
||||
|
||||
if ( '' === $image_url ) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$sanitized[] = array(
|
||||
'id' => $id,
|
||||
'url' => $image_url,
|
||||
'alt' => $alt,
|
||||
'link' => $link,
|
||||
);
|
||||
}
|
||||
|
||||
return $sanitized;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
/**
|
||||
* Install routines.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
namespace LogoSoup;
|
||||
|
||||
/**
|
||||
* Handles activation and deactivation hooks.
|
||||
*/
|
||||
class Installer {
|
||||
/**
|
||||
* Activate plugin.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function activate() {
|
||||
if ( false === get_option( 'logo_soup_settings', false ) ) {
|
||||
add_option( 'logo_soup_settings', Settings::defaults() );
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deactivate plugin.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function deactivate() {
|
||||
wp_clear_scheduled_hook( 'logo_soup_unused' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin bootstrap.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
namespace LogoSoup;
|
||||
|
||||
use LogoSoup\Admin\Settings_Page;
|
||||
use LogoSoup\Blocks\Logo_Soup_Block;
|
||||
use LogoSoup\REST\Settings_Controller;
|
||||
|
||||
/**
|
||||
* Main plugin bootstrap class.
|
||||
*/
|
||||
class Plugin {
|
||||
/**
|
||||
* Plugin file path.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
private static $plugin_file = '';
|
||||
|
||||
/**
|
||||
* Boot the plugin.
|
||||
*
|
||||
* @param string $plugin_file Main plugin file path.
|
||||
* @return void
|
||||
*/
|
||||
public static function boot( $plugin_file ) {
|
||||
self::$plugin_file = $plugin_file;
|
||||
|
||||
register_activation_hook( $plugin_file, array( Installer::class, 'activate' ) );
|
||||
register_deactivation_hook( $plugin_file, array( Installer::class, 'deactivate' ) );
|
||||
|
||||
add_action( 'plugins_loaded', array( __CLASS__, 'load_textdomain' ) );
|
||||
add_action( 'init', array( __CLASS__, 'register_settings' ) );
|
||||
add_action( 'init', array( Logo_Soup_Block::class, 'register' ) );
|
||||
add_action( 'admin_menu', array( Settings_Page::class, 'register' ) );
|
||||
add_action( 'rest_api_init', array( Settings_Controller::class, 'register_routes' ) );
|
||||
}
|
||||
|
||||
/**
|
||||
* Load plugin translations.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function load_textdomain() {
|
||||
load_plugin_textdomain( 'logo-soup', false, basename( dirname( self::$plugin_file ) ) . '/languages' );
|
||||
}
|
||||
|
||||
/**
|
||||
* Register the plugin settings.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public static function register_settings() {
|
||||
register_setting(
|
||||
'logo_soup',
|
||||
'logo_soup_settings',
|
||||
array(
|
||||
'type' => 'array',
|
||||
'sanitize_callback' => array( Settings::class, 'sanitize' ),
|
||||
'default' => Settings::defaults(),
|
||||
'show_in_rest' => false,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin URL.
|
||||
*
|
||||
* @param string $path Relative path.
|
||||
* @return string
|
||||
*/
|
||||
public static function asset_url( $path = '' ) {
|
||||
return plugins_url( ltrim( $path, '/' ), self::$plugin_file );
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the plugin path.
|
||||
*
|
||||
* @param string $path Relative path.
|
||||
* @return string
|
||||
*/
|
||||
public static function asset_path( $path = '' ) {
|
||||
return dirname( self::$plugin_file ) . '/' . ltrim( $path, '/' );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
<?php
|
||||
/**
|
||||
* Settings helpers.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
namespace LogoSoup;
|
||||
|
||||
/**
|
||||
* Sanitizes and exposes plugin settings.
|
||||
*/
|
||||
class Settings {
|
||||
/**
|
||||
* Default settings.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function defaults() {
|
||||
return array(
|
||||
'default_layout' => 'carousel',
|
||||
'default_speed' => 22,
|
||||
'default_gray' => true,
|
||||
'base_size' => 48,
|
||||
'scale_factor' => 0.5,
|
||||
'density_aware' => true,
|
||||
'density_factor' => 0.5,
|
||||
'crop_to_content' => false,
|
||||
'align_by' => 'bounds',
|
||||
'open_new_tab' => true,
|
||||
'cleanup_on_drop' => false,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get merged settings.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function get() {
|
||||
$settings = get_option( 'logo_soup_settings', array() );
|
||||
|
||||
if ( ! is_array( $settings ) ) {
|
||||
$settings = array();
|
||||
}
|
||||
|
||||
return wp_parse_args( $settings, self::defaults() );
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize settings.
|
||||
*
|
||||
* @param mixed $input Input data.
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public static function sanitize( $input ) {
|
||||
$defaults = self::defaults();
|
||||
$input = is_array( $input ) ? $input : array();
|
||||
$layout = isset( $input['default_layout'] ) ? sanitize_key( $input['default_layout'] ) : $defaults['default_layout'];
|
||||
$align_by = isset( $input['align_by'] ) ? sanitize_key( $input['align_by'] ) : $defaults['align_by'];
|
||||
|
||||
if ( ! in_array( $layout, array( 'carousel', 'grid' ), true ) ) {
|
||||
$layout = $defaults['default_layout'];
|
||||
}
|
||||
|
||||
if ( ! in_array( $align_by, array( 'bounds', 'visual-center', 'visual-center-x', 'visual-center-y' ), true ) ) {
|
||||
$align_by = $defaults['align_by'];
|
||||
}
|
||||
|
||||
$speed = isset( $input['default_speed'] ) ? absint( $input['default_speed'] ) : $defaults['default_speed'];
|
||||
$speed = max( 8, min( 80, $speed ) );
|
||||
$base_size = isset( $input['base_size'] ) ? absint( $input['base_size'] ) : $defaults['base_size'];
|
||||
$base_size = max( 16, min( 160, $base_size ) );
|
||||
$scale_factor = self::sanitize_float( isset( $input['scale_factor'] ) ? $input['scale_factor'] : $defaults['scale_factor'], 0.5, 0.1, 2 );
|
||||
$density_factor = self::sanitize_float( isset( $input['density_factor'] ) ? $input['density_factor'] : $defaults['density_factor'], 0.5, 0, 1 );
|
||||
|
||||
return array(
|
||||
'default_layout' => $layout,
|
||||
'default_speed' => $speed,
|
||||
'default_gray' => ! empty( $input['default_gray'] ),
|
||||
'base_size' => $base_size,
|
||||
'scale_factor' => $scale_factor,
|
||||
'density_aware' => ! empty( $input['density_aware'] ),
|
||||
'density_factor' => $density_factor,
|
||||
'crop_to_content' => ! empty( $input['crop_to_content'] ),
|
||||
'align_by' => $align_by,
|
||||
'open_new_tab' => ! empty( $input['open_new_tab'] ),
|
||||
'cleanup_on_drop' => ! empty( $input['cleanup_on_drop'] ),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanitize a float-like setting.
|
||||
*
|
||||
* @param mixed $value Raw value.
|
||||
* @param float $fallback Fallback value.
|
||||
* @param float $min Minimum.
|
||||
* @param float $max Maximum.
|
||||
* @return float
|
||||
*/
|
||||
private static function sanitize_float( $value, $fallback, $min, $max ) {
|
||||
$value = is_numeric( $value ) ? (float) $value : $fallback;
|
||||
$value = max( $min, min( $max, $value ) );
|
||||
|
||||
return round( $value, 2 );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
<?php
|
||||
/**
|
||||
* Logo sanitizer tests.
|
||||
*
|
||||
* @package LogoSoup\Tests
|
||||
*/
|
||||
|
||||
use LogoSoup\Support\Logo_Sanitizer;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests collection sanitization.
|
||||
*/
|
||||
class LogoSanitizerTest extends TestCase {
|
||||
/**
|
||||
* Test invalid rows are removed.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_sanitize_collection_skips_invalid_entries() {
|
||||
$sanitized = Logo_Sanitizer::sanitize_collection(
|
||||
array(
|
||||
array(
|
||||
'id' => '4',
|
||||
'url' => 'https://example.com/logo.svg',
|
||||
'alt' => '<b>Alpha</b>',
|
||||
'link' => 'https://example.com',
|
||||
),
|
||||
array(
|
||||
'alt' => 'Missing URL',
|
||||
),
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertCount( 1, $sanitized );
|
||||
$this->assertSame( 'Alpha', $sanitized[0]['alt'] );
|
||||
$this->assertSame( 4, $sanitized[0]['id'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
<?php
|
||||
/**
|
||||
* Settings tests.
|
||||
*
|
||||
* @package LogoSoup\Tests
|
||||
*/
|
||||
|
||||
use LogoSoup\Settings;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/**
|
||||
* Tests settings sanitization.
|
||||
*/
|
||||
class SettingsTest extends TestCase {
|
||||
/**
|
||||
* Test invalid layout falls back to default.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_sanitize_rejects_unknown_layout() {
|
||||
$sanitized = Settings::sanitize(
|
||||
array(
|
||||
'default_layout' => 'not-real',
|
||||
'default_speed' => 300,
|
||||
'default_gray' => 1,
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertSame( 'carousel', $sanitized['default_layout'] );
|
||||
$this->assertSame( 80, $sanitized['default_speed'] );
|
||||
$this->assertTrue( $sanitized['default_gray'] );
|
||||
}
|
||||
|
||||
/**
|
||||
* Test advanced normalization options are sanitized.
|
||||
*
|
||||
* @return void
|
||||
*/
|
||||
public function test_sanitize_normalization_options() {
|
||||
$sanitized = Settings::sanitize(
|
||||
array(
|
||||
'base_size' => 400,
|
||||
'scale_factor' => '1.37',
|
||||
'density_aware' => 1,
|
||||
'density_factor' => '4.2',
|
||||
'crop_to_content' => 1,
|
||||
'align_by' => 'visual-center-y',
|
||||
)
|
||||
);
|
||||
|
||||
$this->assertSame( 160, $sanitized['base_size'] );
|
||||
$this->assertSame( 1.37, $sanitized['scale_factor'] );
|
||||
$this->assertTrue( $sanitized['density_aware'] );
|
||||
$this->assertSame( 1.0, $sanitized['density_factor'] );
|
||||
$this->assertTrue( $sanitized['crop_to_content'] );
|
||||
$this->assertSame( 'visual-center-y', $sanitized['align_by'] );
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
<?php
|
||||
/**
|
||||
* PHPUnit bootstrap.
|
||||
*
|
||||
* @package LogoSoup\Tests
|
||||
*/
|
||||
|
||||
if ( ! function_exists( 'sanitize_key' ) ) {
|
||||
/**
|
||||
* Sanitize key.
|
||||
*
|
||||
* @param string $key Raw key.
|
||||
* @return string
|
||||
*/
|
||||
function sanitize_key( $key ) {
|
||||
$key = strtolower( (string) $key );
|
||||
|
||||
return preg_replace( '/[^a-z0-9_\-]/', '', $key );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'sanitize_text_field' ) ) {
|
||||
/**
|
||||
* Sanitize text.
|
||||
*
|
||||
* @param string $value Raw value.
|
||||
* @return string
|
||||
*/
|
||||
function sanitize_text_field( $value ) {
|
||||
return trim( wp_strip_all_tags( (string) $value ) );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'absint' ) ) {
|
||||
/**
|
||||
* Convert to absolute integer.
|
||||
*
|
||||
* @param mixed $maybeint Value.
|
||||
* @return int
|
||||
*/
|
||||
function absint( $maybeint ) {
|
||||
return abs( (int) $maybeint );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'esc_url_raw' ) ) {
|
||||
/**
|
||||
* Sanitize raw URL.
|
||||
*
|
||||
* @param string $url URL.
|
||||
* @return string
|
||||
*/
|
||||
function esc_url_raw( $url ) {
|
||||
return filter_var( (string) $url, FILTER_SANITIZE_URL );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_parse_args' ) ) {
|
||||
/**
|
||||
* Parse args against defaults.
|
||||
*
|
||||
* @param array $args Args.
|
||||
* @param array $defaults Defaults.
|
||||
* @return array
|
||||
*/
|
||||
function wp_parse_args( $args, $defaults = array() ) {
|
||||
return array_merge( $defaults, $args );
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_strip_all_tags' ) ) {
|
||||
/**
|
||||
* Strip all tags.
|
||||
*
|
||||
* @param string $value Raw text.
|
||||
* @return string
|
||||
*/
|
||||
function wp_strip_all_tags( $value ) {
|
||||
return trim( preg_replace( '/<[^>]*>/', '', (string) $value ) );
|
||||
}
|
||||
}
|
||||
|
||||
require_once dirname( __DIR__, 2 ) . '/tests/php/wp-stubs.php';
|
||||
require_once dirname( __DIR__, 2 ) . '/src/class-settings.php';
|
||||
require_once dirname( __DIR__, 2 ) . '/src/Support/class-logo-sanitizer.php';
|
||||
@@ -0,0 +1,209 @@
|
||||
<?php
|
||||
/**
|
||||
* WordPress stubs for static analysis and local tests.
|
||||
*
|
||||
* @package LogoSoup\Tests
|
||||
*/
|
||||
|
||||
if ( ! class_exists( 'WP_Error' ) ) {
|
||||
class WP_Error {
|
||||
public function __construct( $code = '', $message = '', $data = array() ) {}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WP_REST_Request' ) ) {
|
||||
class WP_REST_Request {
|
||||
public function get_method() {
|
||||
return 'GET';
|
||||
}
|
||||
|
||||
public function get_header( $name ) {
|
||||
return '';
|
||||
}
|
||||
|
||||
public function get_json_params() {
|
||||
return array();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WP_REST_Response' ) ) {
|
||||
class WP_REST_Response {
|
||||
public function __construct( $data = null, $status = 200 ) {}
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! class_exists( 'WP_REST_Server' ) ) {
|
||||
class WP_REST_Server {
|
||||
const READABLE = 'GET';
|
||||
const EDITABLE = 'POST';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( '__' ) ) {
|
||||
function __( $text, $domain = 'default' ) {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'esc_html__' ) ) {
|
||||
function esc_html__( $text, $domain = 'default' ) {
|
||||
return $text;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'esc_attr' ) ) {
|
||||
function esc_attr( $text ) {
|
||||
return (string) $text;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'esc_html' ) ) {
|
||||
function esc_html( $text ) {
|
||||
return (string) $text;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'esc_url' ) ) {
|
||||
function esc_url( $url ) {
|
||||
return (string) $url;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'selected' ) ) {
|
||||
function selected( $value, $current ) {
|
||||
return $value === $current ? 'selected="selected"' : '';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'checked' ) ) {
|
||||
function checked( $is_checked ) {
|
||||
return $is_checked ? 'checked="checked"' : '';
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'plugins_url' ) ) {
|
||||
function plugins_url( $path = '', $plugin = '' ) {
|
||||
return (string) $path;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'load_plugin_textdomain' ) ) {
|
||||
function load_plugin_textdomain( $domain = 'default', $deprecated = false, $plugin_rel_path = '' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'register_activation_hook' ) ) {
|
||||
function register_activation_hook( $file, $callback ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'register_deactivation_hook' ) ) {
|
||||
function register_deactivation_hook( $file, $callback ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'add_action' ) ) {
|
||||
function add_action( $hook, $callback ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'register_setting' ) ) {
|
||||
function register_setting( $group, $name, $args = array() ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'add_options_page' ) ) {
|
||||
function add_options_page( $page_title = '', $menu_title = '', $capability = '', $menu_slug = '', $callback = null ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'add_settings_section' ) ) {
|
||||
function add_settings_section( $id = '', $title = '', $callback = null, $page = '' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'add_settings_field' ) ) {
|
||||
function add_settings_field( $id = '', $title = '', $callback = null, $page = '', $section = '' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'current_user_can' ) ) {
|
||||
function current_user_can( $capability = '' ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'settings_fields' ) ) {
|
||||
function settings_fields( $group ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'do_settings_sections' ) ) {
|
||||
function do_settings_sections( $page ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'submit_button' ) ) {
|
||||
function submit_button() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'register_rest_route' ) ) {
|
||||
function register_rest_route( $route_namespace, $route, $args ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_verify_nonce' ) ) {
|
||||
function wp_verify_nonce( $nonce, $action ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'register_block_type' ) ) {
|
||||
function register_block_type( $path, $args = array() ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'get_option' ) ) {
|
||||
function get_option( $name, $fallback = false ) {
|
||||
return $fallback;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'add_option' ) ) {
|
||||
function add_option( $name, $value ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'update_option' ) ) {
|
||||
function update_option( $name, $value ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'delete_option' ) ) {
|
||||
function delete_option( $name ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ( ! function_exists( 'wp_clear_scheduled_hook' ) ) {
|
||||
function wp_clear_scheduled_hook( $hook ) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
<?php
|
||||
/**
|
||||
* Plugin uninstall cleanup.
|
||||
*
|
||||
* @package LogoSoup
|
||||
*/
|
||||
|
||||
if ( ! defined( 'WP_UNINSTALL_PLUGIN' ) ) {
|
||||
exit;
|
||||
}
|
||||
|
||||
$settings = get_option( 'logo_soup_settings', array() );
|
||||
|
||||
if ( is_array( $settings ) && ! empty( $settings['cleanup_on_drop'] ) ) {
|
||||
delete_option( 'logo_soup_settings' );
|
||||
}
|
||||
Reference in New Issue
Block a user