feature: Initial commit

This commit is contained in:
Keith Solomon
2026-04-13 20:06:32 -05:00
commit 351dc06ec4
38 changed files with 8138 additions and 0 deletions
+13
View File
@@ -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
+30
View File
@@ -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"
}
}
+11
View File
@@ -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
+8
View File
@@ -0,0 +1,8 @@
/node_modules/
/vendor/
/build/
/dist/
/.phpunit.cache/
/.idea/
Thumbs.db
Desktop.ini
+1
View File
@@ -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}}
+16
View File
@@ -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"
}
}
+384
View File
@@ -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 productiongrade 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 productionready 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 → WPCLI.
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, WPCLI.
* **Constraints:**
* **Security first:** Nonces, `current_user_can`, prepared SQL, sanitize/escape.
* **Compatibility:** PHP 7.48.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 (WPCLI)
* **Goal:** Add WPCLI 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 6max)
├─ .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 statechanging 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; networkaware.
* [ ] Service container bootstrap (optional) for loose coupling.
### 8.2 Admin UI
* [ ] Singlepage 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, frameworkagnostic PHP for plugin core; keep vendor size small.
* Follow WPCS; do not bypass lints without justification.
* Avoid overengineering; 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/nonfunctional requirements, risks, milestones, metrics, and a phased MVP plan. Output `PLAN.md` with tables and a Ganttstyle milestone list."*
* **ScaffolderAgent:**
* *"Generate a WordPress plugin skeleton named `logo-soup`. Include Composer (PSR4), 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 WPCLI 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 WPCLI.
* **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, vendorprefixed 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**
+8
View File
@@ -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.
+1
View File
@@ -0,0 +1 @@
GPL-2.0-or-later
+24
View File
@@ -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 |
+40
View File
@@ -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.
+63
View File
@@ -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"]
}
}
+17
View File
@@ -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',
);
+60
View File
@@ -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;
}
+392
View File
@@ -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));
+116
View File
@@ -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);
}
}
+31
View File
@@ -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
View File
File diff suppressed because it is too large Load Diff
+45
View File
@@ -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__ );
+3233
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -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"
}
}
+20
View File
@@ -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>
+9
View File
@@ -0,0 +1,9 @@
parameters:
level: 6
paths:
- src
- logo-soup.php
- uninstall.php
bootstrapFiles:
- tests/php/bootstrap.php
treatPhpDocTypesAsCertain: false
+8
View File
@@ -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>
+10
View File
@@ -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
View File
@@ -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.
+307
View File
@@ -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
}
}
+188
View File
@@ -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' ), '.' );
}
}
+87
View File
@@ -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 );
}
}
+51
View File
@@ -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;
}
}
+33
View File
@@ -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' );
}
}
+90
View File
@@ -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, '/' );
}
}
+107
View File
@@ -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 );
}
}
+39
View File
@@ -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'] );
}
}
+58
View File
@@ -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'] );
}
}
+85
View File
@@ -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';
+209
View File
@@ -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;
}
}
+16
View File
@@ -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' );
}