docs(plugins): fill up rest of manifest and hooks reference + creating a plugin

This commit is contained in:
Yassine Doghri 2024-06-10 14:55:10 +00:00
parent cc6495dc7c
commit e417d45b14
12 changed files with 867 additions and 624 deletions

View File

@ -12,15 +12,15 @@
},
"dependencies": {
"@astrojs/check": "^0.7.0",
"@astrojs/starlight": "^0.22.4",
"@astrojs/starlight-tailwind": "^2.0.2",
"@astrojs/starlight": "^0.24.0",
"@astrojs/starlight-tailwind": "^2.0.3",
"@astrojs/tailwind": "^5.1.0",
"@fontsource/inter": "^5.0.18",
"@fontsource/rubik": "^5.0.20",
"astro": "^4.8.6",
"astro": "^4.10.1",
"autoprefixer": "^10.4.19",
"cssnano": "^7.0.1",
"postcss-preset-env": "^9.5.13",
"cssnano": "^7.0.2",
"postcss-preset-env": "^9.5.14",
"sharp": "^0.33.4",
"tailwindcss": "^3.4.3",
"typescript": "^5.4.5"

File diff suppressed because it is too large Load Diff

View File

@ -2,24 +2,29 @@
title: Creating a Plugin
---
import { FileTree, Steps } from "@astrojs/starlight/components";
import { FileTree, Steps, Badge } from "@astrojs/starlight/components";
In order to get started, you first need to
[setup your Castopod dev environment](https://code.castopod.org/adaures/castopod/-/blob/develop/CONTRIBUTING-DEV.md).
## Using the create command
## 1. Create the plugin folder
To quickly get you started, you can create a plugin using the following CLI
command:
You'll first need to create your [plugin folder](./#plugin-folder-structure) in
the `plugins/` directory.
### Using the create command <Badge text="Recommended" size="small" />
To quickly get you started, you can have a folder generated for you using the
following CLI command:
```sh
php spark plugins:create
```
👉 Follow the CLI instructions: you will be prompted for metadata and hooks
definitions to generate the [plugin folder](./#plugin-folder-structure) for you.
👉 You will be prompted for metadata and hooks usage to have a skeleton plugin
project generated for you!
## Manual setup
### Manual setup
<Steps>
1. create a plugin folder inside a vendor directory
@ -54,3 +59,58 @@ definitions to generate the [plugin folder](./#plugin-folder-structure) for you.
</FileTree>
</Steps>
## 2. Build your plugin
Now that your plugin folder is set, you can start working on your Plugin's logic
by implementing [the hooks](./hooks) needed.
### Settings forms
You can prompt users for data through settings forms.
These forms can be built declaratively using the
[settings attribute](./manifest#settings) in your manifest.
```json
// manifest.json
{
"settings": {
"general": {
"field-key": {
"type": "text",
"label": "Enter a text"
}
},
"podcast": {
"field-key": {
"type": "text",
"label": "Enter a text for this podcast"
}
},
"episode": {
"field-key": {
"type": "type",
"label": "Enter a text for this episode"
}
}
}
}
```
This example will generate settings forms at 3 levels:
- `general`: a general form to prompt data to be used by the plugin
- `podcast`: a form for each podcast to prompt for podcast specific data
- `episode`: a form for each episode to prompt for episode specific data
The data can then be accessed in the Plugin class methods via helper methods
taking in the field key:
```php
$this->getGeneralSetting('field-key');
$this->getPodcastSetting($podcast->id, 'field-key');
$this->getEpisodeSetting($episode->id, 'field-key');
```

View File

@ -1,3 +0,0 @@
---
title: BasePlugin
---

View File

@ -2,8 +2,8 @@
title: Hooks reference
---
Hooks are methods that live in the Plugin class, they are executed in parts of
the Castopod codebase.
Hooks are methods of the Plugin class, they are executed in parts of the
Castopod codebase.
## List
@ -17,6 +17,11 @@ the Castopod codebase.
### rssBeforeChannel
This hook is executed just before rendering the `<channel>` tag in the Podcast
RSS feed using the given Podcast object.
Here is a good place to alter the Podcast object.
```php
public function rssBeforeChannel(Podcast $podcast): void
{
@ -26,8 +31,13 @@ public function rssBeforeChannel(Podcast $podcast): void
### rssAfterChannel
This hook is executed after rendering all of the `<channel>` tags in the Podcast
RSS feed.
Here is a good place to add new tags to the generated channel.
```php
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $rss): void
public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $channel): void
{
// …
}
@ -35,6 +45,11 @@ public function rssAfterChannel(Podcast $podcast, SimpleRSSElement $rss): void
### rssBeforeItem
This hook is executed before rendering an `<item>` tag in the Podcast RSS feed
using the given Episode object.
Here is a good place to alter the Episode object.
```php
public function rssBeforeItem(Episode $episode): void
{
@ -44,8 +59,13 @@ public function rssBeforeItem(Episode $episode): void
### rssAfterItem
This hook is executed after rendering an `<item>`'s tags in the Podcast RSS
feed.
Here is a good place to add new tags to the generated item.
```php
public function rssAfterItem(Epsiode $episode, SimpleRSSElement $rss): void
public function rssAfterItem(Epsiode $episode, SimpleRSSElement $item): void
{
// …
}
@ -53,6 +73,11 @@ public function rssAfterItem(Epsiode $episode, SimpleRSSElement $rss): void
### siteHead
This hook is executed in the public pages' `<head>` tag.
This is a good place to add meta tags and third-party scripts to Castopod's
public pages.
```php
public function siteHead(): void
{

View File

@ -2,7 +2,7 @@
title: Castopod Plugins
---
import { FileTree, Aside } from "@astrojs/starlight/components";
import { FileTree, Aside, Tabs, TabItem } from "@astrojs/starlight/components";
Plugins are ways to extend Castopod's core features.
@ -16,13 +16,13 @@ Plugins are ways to extend Castopod's core features.
- fr.json
- …
- icon.svg
- [manifest.json](./manifest) // required
- [Plugin.php](#plugin-class) // required
- manifest.json // required
- Plugin.php // required
- README.md
</FileTree>
Plugins reside in the `plugins` folder under a **vendor** folder, ie. the
Plugins reside in the `plugins/` directory under a `vendor/` folder, ie. the
organisation or person who authored the plugin.
<FileTree>
@ -35,15 +35,16 @@ organisation or person who authored the plugin.
</FileTree>
### manifest.json (required)
### Plugin manifest (required)
The plugin manifest is a JSON file containing your plugin's metadata and
permissions.
The plugin manifest is a JSON file containing the plugin's metadata and
declarations.
This file will determine whether a plugin is valid or not. The minimal required
data being:
```json
// manifest.json
{
"name": "acme/hello-world",
"version": "1.0.0"
@ -52,12 +53,12 @@ data being:
Checkout the [manifest.json reference](./manifest).
<h3 id="plugin-class">Plugin class (required)</h3>
### Plugin class (required)
This is where your plugin's logic will live.
This is where the plugin's logic lives.
The Plugin class must extend Castopod's BasePlugin class and implement one or
multiple [Hooks](./hooks) (methods).
The Plugin class extends Castopod's BasePlugin class and implements one or more
[Hooks](./hooks) (methods) intended to be run throughout Castopod's codebase.
```php
// Plugin.php
@ -69,7 +70,11 @@ use Modules\Plugins\Core\BasePlugin;
class AcmeHelloWorldPlugin extends BasePlugin
{
// …
// this rssBeforeChannel method is a Hook
public function rssBeforeChannel(Podcast $podcast): void
{
// …
}
}
```
@ -85,14 +90,14 @@ For example, a plugin living under the `acme/hello-world` folder must be named
</Aside>
### README.md
### Plugin README
The `README.md` file is loaded into the plugin's view page for the user to
read.
The `README.md` file is loaded into the plugin's view page for the user to read
through.
It should be used for any additional information to help guide the user in using
the plugin.
### icon.svg
### Plugin icon
The plugin icon is displayed next to its title, it is an SVG file intended to
give a graphical representation of the plugin.
@ -101,8 +106,8 @@ The icon should be squared, and be legible in a 64px by 64px circle.
### Internationalization (i18n)
Translation strings live under the `i18n` folder. Translation files are JSON
files named as locale keys:
Plugins can be translated. Translation strings live inside the `i18n` folder.
Translation files are JSON files named as locale keys:
<FileTree>
@ -118,16 +123,45 @@ Supported locales are:
`br`,`ca`,`de`,`en`,`es`,`fr`,`nn-no`,`pl`,`pt-br`,`sr-latn`,`zh-hans`.
The translation strings allow you to translate the title, description and
settings keys.
settings keys (ie. labels, hints, helpers, etc.).
```json
{
"title": "Hello, World!",
"description": "A Castopod plugin to greet the world!",
"settings": {
"general": {},
"podcast": {},
"episode": {}
}
}
```
<Tabs>
<TabItem label="English">
```json
// i18n/en.json
{
"title": "Hello, World!",
"description": "A Castopod plugin to greet the world!",
"settings": {
"general": {
"field-key": {
"label": "Enter a text",
"hint": "You can enter any type of character."
}
},
"podcast": {},
"episode": {}
}
}
```
</TabItem>
<TabItem label="French">
```json
// i18n/fr.json
{
"title": "Bonjour, le Monde !",
"description": "Un plugin castopod pour saluer le monde !",
"settings": {
"general": {
"field-key": {
"label": "Saisissez un texte",
"hint": "Vous pouvez saisir n'importe quel type de caractère."
}
},
"podcast": {},
"episode": {}
}
}
```
</TabItem>
</Tabs>

View File

@ -7,7 +7,14 @@ a JSON file.
### name (required)
The plugin name, including 'vendor-name/' prefix.
The plugin name, including 'vendor-name/' prefix. Examples:
- acme/hello-world
- adaures/click
The name must be lowercase and consist of words separated by `-`, `.` or `_`.
The complete name should match
`^[a-z0-9]([_.-]?[a-z0-9]+)*\/[a-z0-9]([_.-]?[a-z0-9]+)*$`.
### version (required)
@ -15,8 +22,8 @@ The plugin's semantic version (eg. 1.0.0) - see https://semver.org/
### description
The plugin's description. This helps people discover your plugin as it's listed
in repositories
The plugin's description. This helps people discover your plugin when listed in
repositories.
### authors
@ -25,7 +32,7 @@ a required "name" field and optional "email" and "url" fields:
```json
{
"name": "Jean D'eau",
"name": "Jean Deau",
"email": "jean.deau@example.com",
"url": "https://example.com/"
}
@ -34,7 +41,7 @@ a required "name" field and optional "email" and "url" fields:
Or you can shorten the object into a single string:
```json
"Jean D'eau <jean.deau@example.com> (https://example.com/)"
"Jean Deau <jean.deau@example.com> (https://example.com/)"
```
### homepage
@ -43,17 +50,79 @@ The URL to the project homepage.
### license
You should specify a license for your plugin so that people know how they are
permitted to use it, and any restrictions you're placing on it.
Specify a license for your plugin so that people know how they are permitted to
use it, and any restrictions you're placing on it.
### private
Whether or not to publish the plugin in public directories. If set to `true`,
directories should refuse to publish the plugin.
### keywords
Array of strings to help your plugin get discovered when listed in repositories.
### hooks
List of hooks used by the plugin. If the hook is not specified, Castopod will
not run it.
### settings
Declare settings forms for persisting user data. The plugin's settings forms can
be declared at three levels: `general`, `podcast`, and `episode`.
Each level accepts one or more fields, identified by a key.
```json
{
"settings": {
"general": { // general settings form
"field-key": {
"type": "text", // default field type: a text input
"label": "Enter a text"
},
},
"podcast": {…}, // settings form for each podcast
"episode": {…}, // settings form for each episode
}
}
```
The `general`, `podcast`, and `episode` settings are of `Fields` object with
each property being a field key and the value being a `Field` object.
#### Field object
A field is a form element:
| Property | Type | Note |
| ------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------ |
| `type` | `checkbox` \| `datetime` \| `email` \| `markdown` \| `number` \| `radio-group` \| `select-multiple` \| `select` \| `text` \| `textarea` \| `toggler` \| `url` | Default is `text` |
| `label` (required) | `string` | Can be translated (see i18n) |
| `hint` | `string` | Can be translated (see i18n) |
| `helper` | `string` | Can be translated (see i18n) |
| `optional` | `boolean` | Default is `false` |
| `options` | `Options` | Required for `radio-group`, `select-multiple`, and `select` types. |
#### Options object
The `Options` object properties are option keys and the value is an `Option`.
##### Option object
| Property | Type | Note |
| ------------------ | -------- | ---------------------------- |
| `label` (required) | `string` | Can be translated (see i18n) |
| `hint` | `string` | Can be translated (see i18n) |
### files
Array of file patterns that describes the entries to be included when your
plugin is installed.
### repository
Repository where the plugin's code lives. Helpful for people who want to
contribute.

View File

@ -4,6 +4,8 @@ declare(strict_types=1);
namespace Modules\Plugins\Manifest;
use Override;
/**
* @property string $key
* @property 'text'|'email'|'url'|'markdown'|'number'|'switch' $type
@ -45,6 +47,22 @@ class Field extends ManifestObject
*/
protected array $options = [];
#[Override]
public function loadData(array $data): void
{
if (array_key_exists('options', $data)) {
$newOptions = [];
foreach ($data['options'] as $key => $option) {
$option['value'] = $key;
$newOptions[] = $option;
}
$data['options'] = $newOptions;
}
parent::loadData($data);
}
/**
* @return array{label:string,value:string,hint:string}[]
*/

View File

@ -1,16 +0,0 @@
<?php
declare(strict_types=1);
namespace Modules\Plugins\Manifest;
use Override;
class Fields extends ManifestObject
{
#[Override]
public function loadData(array $data): void
{
dd($data);
}
}

View File

@ -195,12 +195,11 @@
"type": "boolean"
},
"options": {
"type": "array",
"items": {
"$ref": "#/$defs/option"
"type": "object",
"patternProperties": {
"^[A-Za-z0-9]+[\\w\\-\\:\\.]*$": { "$ref": "#/$defs/option" }
},
"minItems": 1,
"uniqueItems": true
"additionalProperties": false
}
},
"required": ["label"],
@ -215,14 +214,11 @@
"label": {
"type": "string"
},
"value": {
"type": "string"
},
"hint": {
"type": "string"
}
},
"required": ["label", "value"],
"required": ["label"],
"additionalProperties": false
},
"field-multiple-implies-options-is-required": {

View File

@ -14,129 +14,107 @@
"keywords": ["seo", "analytics"],
"hooks": ["rssAfterChannel"],
"settings": {
"general": [
{
"general": {
"name": {
"type": "radio-group",
"key": "name",
"label": "Name",
"options": [
{ "label": "Foo", "value": "foo", "hint": "This is a hint." },
{ "label": "Bar", "value": "bar" }
]
"options": {
"foo": { "label": "Foo", "hint": "This is a hint." },
"bar": { "label": "Bar" }
}
},
{
"email": {
"type": "email",
"key": "email",
"label": "Email"
},
{
"url": {
"type": "url",
"key": "url",
"label": "Your website URL"
},
{
"toggler": {
"type": "toggler",
"key": "toggler",
"label": "Toggle this?"
},
{
"number": {
"type": "number",
"key": "number",
"label": "Number"
},
{
"datetime": {
"type": "datetime",
"key": "datetime",
"label": "Enter a date",
"optional": true
},
{
"select": {
"type": "select",
"key": "select",
"label": "Select something",
"options": [
{
"label": "Foo",
"value": "foo"
"options": {
"foo": {
"label": "Foo"
},
{
"label": "Bar",
"value": "bar"
"bar": {
"label": "Bar"
},
{
"label": "Baz",
"value": "baz"
"baz": {
"label": "Baz"
}
]
}
},
{
"select-multiple": {
"type": "select-multiple",
"key": "select-multiple",
"label": "Select multiple things",
"options": [
{
"label": "Foo",
"value": "foo"
"options": {
"foo": {
"label": "Foo"
},
{
"label": "Bar",
"value": "bar"
"bar": {
"label": "Bar"
},
{
"label": "Baz",
"value": "baz"
"baz": {
"label": "Baz"
}
]
}
},
{
"radio-group": {
"type": "radio-group",
"key": "radio-group",
"label": "Radio Group",
"helper": "This is a helper.",
"options": [
{
"label": "Foo",
"value": "foo"
"options": {
"foo": {
"label": "Foo"
},
{
"label": "Bar",
"value": "bar"
"bar": {
"label": "Bar"
},
{
"label": "Baz",
"value": "baz"
"baz": {
"label": "Baz"
}
]
}
},
{
"texting": {
"type": "textarea",
"key": "texting",
"label": "Your text",
"hint": "This is a hint."
},
{
"hello": {
"type": "markdown",
"key": "hello",
"label": "Name Podcast",
"hint": "This is a hint.",
"optional": true
}
],
"podcast": [
{
},
"podcast": {
"name": {
"type": "text",
"key": "name",
"label": "Name Podcast",
"hint": "This is a hint."
}
],
"episode": [
{
},
"episode": {
"name": {
"type": "text",
"key": "name",
"label": "Name Episode",
"helper": "This is a helper."
}
]
}
}
}

View File

@ -18,11 +18,11 @@
"name": {
"type": "radio-group",
"label": "Name",
"options": [
{ "label": "Foo", "value": "foo", "hint": "This is a hint." },
{ "label": "Bar", "value": "bar" },
{ "label": "Baz", "value": "baz" }
]
"options": {
"foo": { "label": "Foo", "hint": "This is a hint." },
"bar": { "label": "Bar" },
"baz": { "label": "Baz" }
}
},
"email": {
"type": "email",
@ -48,57 +48,48 @@
"select": {
"type": "select",
"label": "Select something",
"options": [
{
"label": "Foo",
"value": "foo"
"options": {
"foo": {
"label": "Foo"
},
{
"label": "Bar",
"value": "bar"
"bar": {
"label": "Bar"
},
{
"label": "Baz",
"value": "baz"
"baz": {
"label": "Baz"
}
]
}
},
"select-multiple": {
"type": "select-multiple",
"label": "Select multiple things",
"options": [
{
"label": "Foo",
"value": "foo"
"options": {
"foo": {
"label": "Foo"
},
{
"label": "Bar",
"value": "bar"
"bar": {
"label": "Bar"
},
{
"label": "Baz",
"value": "baz"
"baz": {
"label": "Baz"
}
]
}
},
"radio-group": {
"type": "radio-group",
"label": "Radio Group",
"helper": "This is a helper.",
"options": [
{
"label": "Foo",
"value": "foo"
"options": {
"foo": {
"label": "Foo"
},
{
"label": "Bar",
"value": "bar"
"bar": {
"label": "Bar"
},
{
"label": "Baz",
"value": "baz"
"baz": {
"label": "Baz"
}
]
}
},
"textarea": {
"type": "textarea",