ckeditor_font-8.x-1.x-dev/src/Plugin/CKEditor5Plugin/Font.php
src/Plugin/CKEditor5Plugin/Font.php
<?php
declare(strict_types=1);
namespace Drupal\ckeditor_font\Plugin\CKEditor5Plugin;
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableTrait;
use Drupal\ckeditor5\Plugin\CKEditor5PluginDefault;
use Drupal\ckeditor5\Plugin\CKEditor5PluginConfigurableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\editor\EditorInterface;
/**
* CKEditor 5 Font (Family & Size) plugin.
*
* @internal
* Plugin classes are internal.
*/
class Font extends CKEditor5PluginDefault implements CKEditor5PluginConfigurableInterface {
use CKEditor5PluginConfigurableTrait;
/**
* The default configuration for this plugin.
*
* @var string[][]
*/
const DEFAULT_SIZES = [
'tiny',
'small',
'default',
'big',
'huge',
];
const DEFAULT_FONTS = [
'default',
'Arial, Helvetica, sans-serif',
'Courier New, Courier, monospace',
'Georgia, serif',
'Lucida Sans Unicode, Lucida Grande, sans-serif',
'Tahoma, Geneva, sans-serif',
'Times New Roman, Times, serif',
'Trebuchet MS, Helvetica, sans-serif',
'Verdana, Geneva, sans-serif',
];
const DEFAULT_CONFIGURATION = [
'font_sizes' => [],
'font_names' => [],
'supportAllFamilyValues' => FALSE,
'supportAllSizeValues' => FALSE,
];
/**
* {@inheritdoc}
*/
public function defaultConfiguration() {
return static::DEFAULT_CONFIGURATION;
}
/**
* {@inheritdoc}
*/
public function getDynamicPluginConfig(array $static_plugin_config, EditorInterface $editor): array {
// Get static config.
$font_all_options['fontSize']['options'] = (empty($this->getConfiguration()['font_sizes'])) ? static::DEFAULT_SIZES : $this->getConfiguration()['font_sizes'];
$font_all_options['fontSize']['supportAllValues'] = $this->getConfiguration()['supportAllSizeValues'];
$font_all_options['fontFamily']['options'] = (empty($this->getConfiguration()['font_names'])) ? static::DEFAULT_FONTS : $this->getConfiguration()['font_names'];
$font_all_options['fontFamily']['supportAllValues'] = $this->getConfiguration()['supportAllFamilyValues'];
// Convert config to ['title']['model'] sub-array.
return $font_all_options;
}
/**
* {@inheritdoc}
*
* @see \Drupal\editor\Form\EditorImageDialog
* @see editor_image_upload_settings_form()
*/
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
$form['font_sizes'] = [
'#title' => $this->t('Font sizes'),
'#type' => 'textarea',
'#description' => $this->t('Enter font sizes on new lines. Sizes must be added with the following syntax:<br><code>123px|Size label</code><br><code>123em|Size label</code><br><code>123%|Size label</code>'),
'#element_validate' => [
[$this, 'validateFontSizeValue'],
],
];
$font_size_selectors = '';
if (!empty($this->configuration['font_sizes'])) {
foreach ($this->configuration['font_sizes'] as $font_size) {
// If using the awkward supportAllSizeValues, show just the
// value without the schema.
if ($this->configuration['supportAllSizeValues']) {
$font_size_selectors .= sprintf("%s\n", $font_size);
}
else {
$font_size_selectors .= sprintf("%s|%s\n", $font_size['model'], $font_size['title']);
}
}
}
$form['font_sizes']['#default_value'] = $font_size_selectors;
$form['supportAllSizeValues'] = [
'#type' => 'checkbox',
'#title' => $this->t('Support all Font Size values'),
'#description' => $this->t("By default the plugin removes any font-size value that does not match the plugin's configuration. It means that if you paste content with font sizes that the editor does not understand, the font-size attribute will be removed and the content will be displayed with the default size."),
'#default_value' => $this->configuration['supportAllSizeValues'],
];
$form['font_names'] = [
'#title' => $this->t('Font families'),
'#type' => 'textarea',
'#description' => $this->t('Enter fonts on new lines. Fonts must be added with the following syntax:<br><code>Primary font, fallback1, fallback2</code>. Font label is automatically derived from the first font family.'),
'#element_validate' => [
[$this, 'validateFontValue'],
],
];
if (!empty($this->configuration['font_names'])) {
$font_name_selectors = '';
foreach ($this->configuration['font_names'] as $font_name) {
$font_name_selectors .= sprintf("%s\n", $font_name);
}
$form['font_names']['#default_value'] = $font_name_selectors;
}
else {
$form['font_names']['#default_value'] = [];
}
$form['supportAllFamilyValues'] = [
'#type' => 'checkbox',
'#title' => $this->t('Support all Font Family values'),
'#description' => $this->t("By default the plugin removes any font-family value that does not match the plugin's configuration. It means that if you paste content with font families that the editor does not understand, the font-family attribute will be removed and the content will be displayed with the default font."),
'#default_value' => $this->configuration['supportAllFamilyValues'],
];
return $form;
}
/**
* The #element_validate handler for the "supportAllSizeValues" element.
*
* @param array $form
* The array representation of the Form.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function validateSupportAllSizeValues(array $form, FormStateInterface $form_state) {
// Run a separate regex for font_sizes
// Empty will fail because the CKE5 default contains non-numerical values
// Check for font values that are non-numerical, throw error.
// See https://ckeditor.com/docs/ckeditor5/latest/api/module_font_fontsize-FontSizeConfig.html#member-supportAllValues
$fontSizes = $form_state->getValue('font_sizes');
if (empty($fontSizes)) {
$form_state->setError($form['supportAllSizeValues'], t("\'Support all Font Size values\' setting cannot be used with an empty Font Sizes configuration."));
}
elseif ($this->generateFontSetting($fontSizes, 'supportAllSizes') === FALSE) {
$form_state->setError($form['supportAllSizeValues'], t("\'Support all Font Size values\' setting cannot be used with non-numeric Font Sizes configuration."));
}
}
/**
* The handler for the "font_names" element in settingsForm().
*
* @param array $element
* The CKEditor Element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function validateFontValue(array $element, FormStateInterface $form_state) {
if ($this->generateFontSetting($element['#value'], 'family') === FALSE) {
$form_state->setError($element, t('The provided list of fonts is syntactically incorrect.'));
}
}
/**
* The handler for the "font_sizes" element in settingsForm().
*
* @param array $element
* The CKEditor Element.
* @param \Drupal\Core\Form\FormStateInterface $form_state
* The form state.
*/
public function validateFontSizeValue(array $element, FormStateInterface $form_state) {
if ($this->generateFontSetting($element['#value'], 'size') === FALSE) {
$form_state->setError($element, t('The provided list of font sizes is syntactically incorrect.'));
}
}
/**
* Checks if the font size declaration follows acceptable values.
*
* @param mixed $font
* The font size declaration.
*
* @return mixed
* Returns FALSE if the format is wrong, otherwise
* it returns the $font value.
*/
public static function validateSupportAllFontSize(string $font) {
// Checks for if fontsize value is numerical OR numerical|label value.
// This is an aggressive implementation, as generateFontValue
// should split label from value, but good to have.
$pattern = '@(^[0-9]*)+(\|)+(\S*)|(^[0-9]*$)@';
if (!preg_match($pattern, $font)) {
return FALSE;
}
return $font;
}
/**
* Determine whether the font size declaration follows acceptable values.
*
* @param mixed $font
* The font size declaration.
*
* @return mixed
* Returns FALSE if the format is wrong, otherwise
* it returns the $font value.
*/
public static function validateFontSize(string $font) {
// Match for patterns:
// 123px/pt/em/rem/%
// see: d.o/node/3312951.
$fontSizeKeywords = ['xx-small', 'x-small', 'small', 'medium', 'large',
'x-large', 'xx-large', 'xxx-large', 'larger', 'smaller',
'inherit', 'initial', 'revert', 'revert-layer', 'unset',
];
// //(^((\s*((small|medium|large)+)|\d+(\.?\d+)?(px|em|%|pt|rem))))\|.*|$
$pattern = '@(^((\s*((' . implode('|', $fontSizeKeywords) . ')+)|\d+(\.?\d+)?(px|em|%|pt|rem)?)))@';
if (!preg_match($pattern, $font)) {
return FALSE;
}
return $font;
}
/**
* Migrated font families may have a pipe and label, remove them.
*
* @param mixed $font
* The font size declaration.
*
* @return string
* Returns $font after some processing
*/
public static function validateFontFamily(string $font) {
$font = preg_replace('@\|.*$@', "", $font);
return $font;
}
/**
* {@inheritdoc}
*/
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
// First, validate the supportAllSizeValues, as we need the values
// of font_sizes and supportAllSizeValues to properly confirm the value.
if ($form_state->getValue('supportAllSizeValues')) {
$this->validateSupportAllSizeValues($form, $form_state);
}
// Convert font_sizes string into configuration value.
$font_sizes = $this->generateFontSetting($form_state->getValue('font_sizes'), 'size', $form_state->getValue('supportAllSizeValues'));
if (!empty($font_sizes)) {
$form_state->setValue('font_sizes', $font_sizes);
}
else {
$form_state->setValue('font_sizes', []);
}
// Convert font_names string into configuration value.
$font_names = $this->generateFontSetting($form_state->getValue('font_names'), 'family');
if (!empty($font_names)) {
$form_state->setValue('font_names', $font_names);
}
else {
$form_state->setValue('font_names', []);
}
}
/**
* {@inheritdoc}
*/
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
$this->configuration['supportAllSizeValues'] = $form_state->getValue('supportAllSizeValues') ? TRUE : FALSE;
$this->configuration['supportAllFamilyValues'] = $form_state->getValue('supportAllFamilyValues') ? TRUE : FALSE;
$this->configuration['font_names'] = $form_state->getValue('font_names');
$this->configuration['font_sizes'] = $form_state->getValue('font_sizes');
}
/**
* Builds and validates each option into a CKE5 array.
*
* @param string $fonts
* A newline delimited string of fonts.
* Syntax differs for each, but standardizes to be:
* Font Configuration|Font Label.
* @param string $type
* A selector for which configuration to validate.
* Acceptable values: size, family, supportAllSizes.
* @param int $supportAllValues
* Handle the supportAllSizeValues configuration,
* which demands an int for each value.
*
* @return mixed
* Returns a font setting.
*/
public static function generateFontSetting(string $fonts, string $type, int $supportAllValues = 0) {
$fonts = trim($fonts);
// Prepare return value and init values.
$font_values = [];
// Early-return when empty.
if (empty($fonts)) {
return $font_values;
}
// Standardize newline.
$fonts = str_replace(["\r\n", "\r"], "\n", $fonts);
// Loop through each value.
foreach (explode("\n", $fonts) as $font) {
$font = trim($font);
// Ignore empty lines.
if (empty($font)) {
continue;
}
$font_value = [];
switch ($type) {
case 'family':
$font_value = self::validateFontFamily($font);
break;
case 'size':
[$size, $label] = explode('|', $font);
if (empty(self::validateFontSize($size))) {
return FALSE;
}
if ($supportAllValues) {
// If supportAllValues exists, then caste to int.
$size = intval($size);
$font_value = $size;
}
else {
// Build subarray for CKEditor5 consumption.
$font_value['title'] = $label ? $label : $size;
$font_value['model'] = $size;
}
break;
case 'supportAllSizes':
// Break out label from size.
// Replace with str_contains in PHP8.
[$size, $label] = explode('|', $font);
if (empty(self::validateSupportAllFontSize($size))) {
return FALSE;
}
break;
}
$font_values[] = $font_value;
}
// Special casing for supportAllSizes.
if ($type == 'supportAllSizes') {
return TRUE;
}
// Otherwise, return generated array.
return $font_values;
}
}
