Allow text fields to hold and process short codes, and for shortcodes to be defined via config.
There are two parts to defining a shortcode out of the box: handlers, and config.
Handlers must implement the new Nightjar\ConfigCodes\Handler
interface, which defines what attributes are available or
required, whether or not the shortcode accepts or requires content, and of course processes a code to supply substitute
output.
interface Handler {
public static function getParameters(): array; // Attribute name => is required (bool)
public static function getRequiresContent(): ?bool;
public function process(array $arguments = [], ?string $content = null): ?string;
}
Configuration happens in combination with Injector config to deifne a use case for the handler. The default setup is to
reference Nightjar\ConfigCodes\Registry\ConfigReader.shortcodes
, under which handlers are listed by name, each of which in
turn list key-value pairs of code (key) to Injector bean name (value).
With a hypothetical language change handler for a site where the main language is <html lang="en">
but there are other
official languages the site might use for phrases in content that need to be wrapped in <span lang="??">...</span>
,
we could use the below configuration in content: [mi]Kia ora[/mi], welcome to my site
(It's just an example, leaving arguments about adopted or borrowed words aside e.g. cafe in English was a French word, but is English too now).
SilverStripe\Core\Injector\Injector:
Shortcodes.TeReoMaori:
class: My\App\LanguageChangeHandler
constructor: [mi]
Shortcodes.Cymraeg:
class: My\App\LanguageChangeHandler
constructor: [cy]
Nightjar\ConfigCodes\Registry\ConfigReader:
shortcodes:
default:
mi: Shortcodes.TeReoMaori
cy: Shortcodes.Cymraeg
A handler that fetches fields from a DataObject class, where the ID attribute is required, comes 'out of the box' with this module, and can be used repeatedly for different codes without having to deifne a new handler in PHP for each case:
SilverStripe\Core\Injector\Injector:
Shortcodes.Member:
class: Nightjar\ConfigCodes\Handler\DataObjectPropertyDisplay
constructor: [SilverStripe\Security\Member, Surname, UpperCase]
Shortcodes.Page:
class: Nightjar\ConfigCodes\Handler\DataObjectPropertyDisplay
constructor: [Page, Title]
Nightjar\ConfigCodes\Registry\ConfigReader:
shortcodes:
default:
member: Shortcodes.Member
page: Shortcodes.Page
This can then be used in HTMLText fields; E.g Direct all complaints to: [member id=1]
which might output in dev
Direct all complaints to: ADMIN
when using the SS_DEFAULT_ADMIN_USERNAME & SS_DEFAULT_ADMIN_PASSWORD environment variables,
which creates the member FirstName: Default, Surname: Admin (at ID 1 if there are no other members, of course).
Injector definitions have been loaded for each string field type.
- Varchar -> ShortcodeVarchar
- Text -> ShortcodeText
- HTMLVarchar -> ShortcodeHTMLVarchar
- HTMLText -> ShortcodeHTMLText
For eacy type, a plain version that does not process shortcodes - e.g ShortcodeVarchar
, and a "Parsed" sub-config that
parses shortcodes - e.g ShortcodeVarchar.Parse
The HTML string field variants add little over the framework supplied classes, and exist mainly give a consistent API with the non-HTML variants.
The replacements can be used either directly for specific application:
private static $db => [
'Description' => 'ShortcodeVarchar.Parse',
];
Or by applying globally to every field for site-wide application:
SilverStripe\Core\Injector\Injector:
Varchar: '%$ShortcodeVarchar.Parse'
HTMLVarchar: '%$ShortcodeHTMLVarchar.Parse'
Text: '%$ShortcodeText.Parse'
HTMLText: '%$ShortcodeHTMLText.Parse'
With both options it is important to note limitations in Using escaping functions below.
With global application for Text
it is important to read Setting the default cast below.
The default for Silverstripe CMS is to escape HTML. The way this is done also by default escapes double quote ("
) and
single quote ('
). This generally makes the output safe for inclusion into tag attributes, rather than text nodes.
However, the Silverstripe frameworks ShortcodeParser uses DOMDocument (via a wrapper or two) to parse shortcodes, to ensure they aren't being abused to form tag names, etc. which is explicitly disallowed.
PHPs DOMDocument
does not respect input formatting on output. Loading "
into the system will save out as "
due to the encoding being deemed "unnecessary" as per the HTML spec, due to our input string being loaded as a text
node. However for us this poses a problem if the input might be targetted for output in an attribute, where an
unencoded "
will pose a problem. To work around this we can either use the HTMLATTR
escape method, or avoid
shortoode parsing if applicable via $Value
in the template (getValue()
in PHP). HTMLATTR
will also escape
shortcode output, as it is called/applied after parsing. $Value.XML
will escape output without parsing shortcodes, if
the default cast is set as below (next heading).
This issue is commonly run into when outputting JSON encoded data into a data attribute.
Output of non-string types (FormField::getSchemaData()
) will cause errors due typehints on the new ShortcodeText
field. We can specify the default default_cast
in order to remedy this. If your project already changes the default
cast this may not be necessary, or you might need to adapt it accordingly.
SilverStripe\View\ViewableData:
default_cast: SilverStripe\ORM\FieldType\DBText
This means that returing non-object strings will not process shortcodes though. If you want a return a string that contains some which needs processing, then the method should return a new VarcharText with the return value set to it, instead of a primitive string.
- return "a plain string with a [code] in it"; // Template will not process the shortcode
+ $output = new ShortcodeText();
+ $output->setValue("a plain string with a [code] in it");
+ $output->setProcessShortcodes(true);
+ return $output; // Template will render the shortcode substitution.
Alternatively this could be done via the casting
config.
See SilverStripe\View\ViewableData::$casting
or good examples in SilverStripe\Forms\FormField::$casting
There is a UI in development for the CMS to improve the shortcode usage experience beyond having to manually remember all the valid shortcodes, valid attributes & attribute types for each, and typing these in by hand for each usage.
Accessibility and minor use case improvements remain unfinished.
The field currently only works for input[type=text]
form fields, but should be easy to extend to textarea
also.
A plugin for TinyMCE is planned in order to offer the same functionality there for consistency.
The UI can be trialled via the config:
SilverStripe\Forms\TextField:
extensions:
- Nightjar\ConfigCodes\FormField\ShortcodableExtension
- Make 'plain' the default output (
forTemplate
) - make shortcode unable to be applied within or over a shortcode - https://docs.silverstripe.org/en/4/developer_guides/extending/shortcodes/#limitations
- serverside validation there are no nested shortcodes?
- make single line edit only (for input type text).
- make enter key submit like would happen with an
input[type=text]
field - hot key
- focus indicator
- recieve focus from label activation
- tool tip on how to use
- interface for setting shortcode options (i.e. attributes)
- pass config through from data attribute
- un-hardcode test shortcode
- Disabled state visual style
- javascript tests
- JS Injector override input element (how to save then though? no hidden
input
element to write into) - Make shortcodes apply to rich editor
- Make content adjustments apply from editor to Slate
- FIX: Tip doesn't do what it does in a React context - it won't close without re-clicking the button that opens it.
- Figure out how aria-describedBy can reference the popover visual help - can't happen with Tip because it doesn't render the content by default.
- How is the accessibility on the toolbar? Does it matter, if we hint on shortcut keys elsewhere? All functionality is available via the Editor - so as long as that opens with a tooltip it should be OK, right?
- support translations
- shortcodes that do not accept content should probably be 'void' elements (Slate)
- demarcate invalid editor fields
- ViewableData::obj() will cast a value that doesn't exist into a full value object and make it "truthy". Check in DataObjectPropertyDisplay::__construct that things exist as we expect.
- Remove shortcode if it is empty and press backspace or delete
- Remove button should work on void type shortcodes
- Leave focus on void type shortcode if cancelling or editing