artabro/wire/modules/Inputfield/InputfieldTinyMCE/InputfieldTinyMCEFormats.php
2024-08-27 11:35:37 +02:00

503 lines
No EOL
14 KiB
PHP
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php namespace ProcessWire;
/**
* InputfieldTinyMCEFormats
*
* Helper for managing TinyMCE style_formats and related settings
*
* ProcessWire 3.x, Copyright 2023 by Ryan Cramer
* https://processwire.com
*
*/
class InputfieldTinyMCEFormats extends InputfieldTinyMCEClass {
/**
* HTML5 inline elements that should be "inline" designation in style_formats
*
* Text can be highlighted and then applied to any of these selements from the Styles dropdown.
* See also: notes in $inlineBlocks variable below.
*
* @var string
*
*/
static protected $inlines =
'/abbr/acronym/b/bdi/bdo/big/br/button/cite/code/' .
'/del/dfn/em/i/ins/kbd/label/mark/meter/' .
'/q/s/samp/small/span/strong/' .
'/sub/sup/time/u/tt/var/wbr/';
/**
* HTML5 block elements that should use "block" designation in style_formats
*
* These elements can be inserted from Styles dropdown, if defined in style_formats.
* Block elements not defined here must exist before the style can be applied.
*
* @var string
*
*/
static protected $blocks =
'/h1/h2/h3/h4/h5/h6/p/hr/';
// '/address/article/aside/blockquote/dd/details/div/dl/dt/' .
// '/footer/h1/h2/h3/h4/h5/h6/header/hgroup/hr/li/main/nav/ol/p/pre/' .
// '/section/table/';
/**
* HTML5 block or inline elements that should use "selector" designation in style_formats
*
* These elements (and any others not defined above) cannot be inserted by selection but
* existing elements can be applied. For reference only, nothing uses this variable.
*
* @var string
*
*/
static protected $inlineBlocks =
'/a/fieldset/figcaption/figure/form/dialog/form/' .
'/audio/canvas/data/datalist/img/iframe/embed/input/map/noscript/object/output/' .
'/picture/progress/ruby/select/slot/svg/template/textarea/video/';
/**
* Get block_formats
*
* @return string
*
*/
public function getBlockFormats() {
// 'block_formats' => 'Paragraph=p; Heading 1=h1; Heading 2=h2; Heading 3=h3; Heading 4=h4; Heading 5=h5; Heading 6=h6;',
$values = array('Paragraph=p;');
$headlines = $this->inputfield->get('headlines');
foreach($headlines as $h) {
$n = ltrim($h, 'h');
$values[$h] = "Heading $n=$h;";
}
return implode(' ', $values);
}
/**
* Get style_formats
*
* @param array $defaults
* @return array|mixed
*
*/
public function getStyleFormats(array $defaults) {
/*
'style_formats' => array(
array(
'title' => 'Headings',
'items' => array(
array('title' => 'Heading 1', 'format' => 'h1'),
array('title' => 'Heading 2', 'format' => 'h2'),
array('title' => 'Heading 3', 'format' => 'h3'),
array('title' => 'Heading 4', 'format' => 'h4'),
array('title' => 'Heading 5', 'format' => 'h5'),
array('title' => 'Heading 6', 'format' => 'h6')
)
),
*/
$headlines = $this->inputfield->headlines;
$headlines = array_flip($headlines);
$formats = $defaults['style_formats'];
foreach($formats as $key => $format) {
if(!is_array($format)) continue;
if($format['title'] === 'Headings') {
foreach($format['items'] as $k => $item) {
if(empty($item['format'])) continue;
$tag = $item['format'];
if(!isset($headlines[$tag])) unset($formats[$key]['items'][$k]);
}
$formats[$key]['items'] = array_values($formats[$key]['items']);
break;
}
}
return $formats;
}
/**
* Merge the given style formats
*
* @param array $styleFormats
* @param array $addFormats
* @return array
*
*/
public function mergeStyleFormats(array $styleFormats, array $addFormats) {
$a = array();
foreach($styleFormats as $value) {
if(empty($value['title'])) continue;
$title = $value['title'];
$a[$title] = $value;
}
$styleFormats = $a;
foreach($addFormats as $value) {
if(empty($value['title'])) continue;
$title = $value['title'];
if(isset($styleFormats[$title])) {
if(isset($styleFormats[$title]['items'])) {
if(isset($value['items'])) {
$styleFormats[$title]['items'] = array_merge($styleFormats[$title]['items'], $value['items']);
}
} else {
$styleFormats[$title] = array(
'title' => $title,
'items' => $value,
);
}
} else {
$styleFormats[$title] = $value;
}
}
return array_values($styleFormats);
}
/**
* Add CSS that converts to style_formats and content_style
*
* Easier-to-use alternative to the importcss plugin
*
* @param string $css From the styleFormatsCSS setting
* @param array $settings
* @param array $defaults
*
*/
public function applyStyleFormatsCSS($css, array &$settings, $defaults) {
$contentStyle = ''; // output for content_style
// ensures each CSS rule has its own line
$css = trim(str_replace('}', "}\n", $css));
// converts each CSS rule to be on single line with no newlines between "key:value;" rules
//$css = preg_replace('!\s*([{;:]|/\*|\*/)\s*!s', '\1', $css);
$css = preg_replace('!\s*([{;:]|/\*)\s*!s', '\1', $css);
//$css = preg_replace('!\}\s+/\*!s', '}/*', $css);
$lines = explode("\n", $css);
$numRemove = 0;
$formats = array(
// 'Headings' => array(),
// 'Blocks' => array(),
// 'Inline' => array(),
// 'Align' => array(),
// 'Other' => array(), // converts to root level (no parent)
);
while(count($lines)) {
$line = array_shift($lines);
$line = trim($line);
$title = '';
if(empty($line)) continue;
if(strpos($line, '{') && strpos($line, '}') === false) {
// grab next line if a rule was started but not closed
$line .= array_shift($lines);
}
if(strpos($line, '{') === false) continue; // line does not start a rule
if(strpos($line, '/*') && preg_match('!/\*(.+)?\*/!', $line, $matches)) {
// line has comment indicating text label
$title = trim($matches[1]);
$line = str_replace($matches[0], '', $line);
}
list($selector, $styles) = explode('{', $line, 2);
list($styles,) = explode('}', $styles, 2);
$selector = trim($selector);
if(strpos($selector, '(') !== false) {
// Alternate title assignment i.e. #Align(Center)
list($selector, $title2) = explode('(', $selector, 2);
list($title2, $selector2) = explode(')', $title2, 2);
$selector = trim("$selector $selector2");
if(!empty($title2)) $title = trim($title2);
}
if(strpos($selector, '#') === 0) {
// indicates a submenu parent, i.e. #Blocks
if(strpos($selector, ' ')) {
list($parent, $selector) = explode(' ', $selector, 2);
} else {
list($parent, $selector) = array($selector, '');
}
$selector = trim($selector);
$parent = ucfirst(strtolower(ltrim($parent, '#')));
} else {
$parent = 'Other';
}
if(strpos($selector, '.') !== false) {
// element with class, i.e. span.red-text or just .red-text
list($element, $class) = explode('.', $selector, 2);
$class = str_replace('.', ' ', $class);
} else {
// element only (no class), i.e. ins or del
$element = $selector;
$class = '';
}
$stylesStr = ''; // minified styles string
$inlineStyles = array(); // styles to also forced as inline styles on element
foreach(explode(';', $styles) as $style) {
// i.e. color: red
if(strpos($style, ':') === false) continue;
list($k, $v) = explode(':', $style);
list($k, $v) = array(trim($k), trim($v));
if(strtoupper($k) === $k) {
// uppercase styles i.e. 'COLOR: red' become inline styles of element
$k = strtolower($k);
$inlineStyles[$k] = $v;
}
$stylesStr .= "$k:$v;";
}
$contentStyleSelector = ($class ? "$element." . str_replace(' ', '.', $class) : $element);
if(stripos($stylesStr, 'display:none') !== false) {
$numRemove++;
if(empty($title)) $title = '*'; // indicates remove all in parent
$remove = true;
} else {
$contentStyle .= "$contentStyleSelector { $stylesStr } ";
$remove = false;
}
if(empty($element)) $element = '*';
$format = array(
'title' => ($title ? $title : $selector)
);
if($remove) {
$format['remove'] = true;
} else if(stripos(self::$inlines, "/$element/") !== false) {
$format['inline'] = $element;
} else if(strpos(self::$blocks, "/$element/") !== false) {
$format['block'] = $element;
} else {
$format['selector'] = $element;
}
if($class) $format['classes'] = $class;
if(count($inlineStyles)) $format['styles'] = $inlineStyles;
if(!isset($formats[$parent])) $formats[$parent] = array();
$formats[$parent][] = $format;
}
$styleFormats = array();
foreach($formats as $parent => $format) {
if($parent === 'Other') {
$styleFormats[$parent] = $format;
} else if(!isset($styleFormats[$parent])) {
$styleFormats[$parent] = array(
'title' => $parent,
'items' => $format,
);
}
}
$other = isset($styleFormats['Other']) ? $styleFormats['Other'] : array();
unset($styleFormats['Other']);
$styleFormats = array_values($styleFormats);
if(count($other)) $styleFormats = array_merge($styleFormats, $other);
// add to settings
if(isset($settings['style_formats'])) {
$settings['style_formats'] = $this->mergeStyleFormats($settings['style_formats'], $styleFormats);
} else if(isset($defaults['style_formats'])) {
$settings['style_formats'] = $this->mergeStyleFormats($defaults['style_formats'], $styleFormats);
} else {
$settings['style_formats'] = $styleFormats;
}
if(isset($settings['content_style'])) {
$settings['content_style'] .= $contentStyle;
} else if(isset($defaults['content_style'])) {
$settings['content_style'] = $defaults['content_style'] . $contentStyle;
} else {
$settings['content_style'] = $contentStyle;
}
if($numRemove) {
$settings['style_formats'] = $this->applyRemoveStyleFormats($settings['style_formats']);
}
// reindex to ensure keys remain numeric and in order so json_encode doesnt use string keys
$settings['style_formats'] = array_values($settings['style_formats']);
}
/**
* Remove style formats that have a 'remove=true' property
*
* @param array $styleFormats
* @return array
*
*/
protected function applyRemoveStyleFormats(array $styleFormats) {
foreach($styleFormats as $key => $styleFormat) {
if(!empty($styleFormat['remove'])) {
// remove all in format or remove root level format
unset($styleFormats[$key]);
continue;
} else if(empty($styleFormat['items'])) {
// root level format with no items
continue;
}
$removeTitles = array();
foreach($styleFormat['items'] as $item) {
if(empty($item['remove'])) continue;
$title = strtolower($item['title']);
if($title === '*') {
unset($styleFormats[$key]); // remove all in parent
} else {
$removeTitles[$title] = $title; // remove by title
}
}
if(empty($styleFormats[$key]) || empty($removeTitles)) continue;
foreach($styleFormat['items'] as $itemKey => $item) {
$title = strtolower($item['title']);
if(!isset($removeTitles[$title])) continue;
unset($styleFormats[$key]['items'][$itemKey]); // remove item matching title
if(empty($styleFormats[$key]['items'])) {
unset($styleFormats[$key]); // remove parent when it has no items
break;
}
}
// reindex to prevent json_encode from converting keys to strings
if(isset($styleFormats[$key]['items'])) {
$styleFormats[$key]['items'] = array_values($styleFormats[$key]['items']);
}
}
return $styleFormats;
}
/**
* Get TinyMCE "invalid_styles" setting and prepare as array value
*
* Parses value in space-separated string format (commas optional):
* ~~~~~
* line-height, color, a=background|background-color, td=height
* ~~~~~
* In the above, line-height and color are disabled for all elements,
* background and background color are disabled for "a" elements,
* and height is disabled for "td" elements.
*
* @param string|array $value
* @param array|string $defaultValue
* @param bool $merge Merge with given defaultValue?
* @return array|string
*
*/
public function getInvalidStyles($value, $defaultValue, $merge = false) {
if(!is_array($defaultValue)) $defaultValue = array('*' => $defaultValue);
if($value === null) $value = $this->inputfield->invalid_styles;
if($value === 'default') return $defaultValue;
if(is_string($value) && strpos($value, ',') !== false) $value = str_replace(',', ' ', $value);
if($merge) {
if(is_string($defaultValue)) {
$defaultValue = $this->invalidStylesStrToArray($defaultValue);
}
if(is_array($value)) {
$invalidStyles = array_merge($defaultValue, $value);
} else {
$invalidStyles = $this->invalidStylesStrToArray("$value", $defaultValue);
}
} else if(is_array($value)) {
$invalidStyles = $value;
} else {
$invalidStyles = $this->invalidStylesStrToArray("$value");
}
return $invalidStyles;
}
/**
* Convert invalid_styles string to array
*
* @param string $value i.e. "line-height color a=background|background-color td=height"
* @param array $a Optionally merge with these styles
* @return array
*
*/
public function invalidStylesStrToArray($value, array $a = array()) {
// $value i.e. "line-height color a=background|background-color td=height"
if(strpos($value, ',') !== false) $value = str_replace(',', ' ', $value);
if(strpos($value, "\n") !== false) $value = str_replace("\n", ' ', $value);
foreach(explode(' ', strtolower($value)) as $style) {
if(empty($style)) continue;
if(strpos($style, '=')) {
list($element, $style) = explode('=', $style, 2);
$styleNames = explode('|', $style);
} else {
$element = '*';
$styleNames[] = $style;
}
if(strpos($element, '|')) {
$elements = explode('|', $element);
} else {
$elements = array($element);
}
foreach($elements as $element) {
if(isset($invalidStyles[$element])) {
$a[$element] = array_unique(array_merge($a[$element], $styleNames));
} else {
$a[$element] = $styleNames;
}
}
}
foreach($a as $element => $styles) {
$a[$element] = implode(' ', $styles); // convert to string
}
return $a;
}
/**
* Convert invalid_styles array to string
*
* @param array $a
* @return string
*
*/
public function invalidStylesArrayToStr(array $a) {
$str = '';
$elementsByStyle = array();
foreach($a as $element => $styles) {
if($element === '*') {
$str .= " $styles";
} else if(strpos($styles, ' ') === false) {
if(!isset($elementsByStyle[$styles])) $elementsByStyle[$styles] = array();
$elementsByStyle[$styles][] = $element;
} else {
$str .= " $element=" . str_replace(' ', '|', $styles);
}
}
foreach($elementsByStyle as $style => $elements) {
$str .= " " . implode('|', $elements) . "=$style";
}
return trim($str);
}
}