mirror of
https://github.com/YunoHost/doc.git
synced 2024-09-03 20:06:26 +02:00
402 lines
17 KiB
PHP
402 lines
17 KiB
PHP
|
<?php
|
||
|
/**
|
||
|
* Presentation Plugin, Deckset Parser API
|
||
|
*
|
||
|
* PHP version 7
|
||
|
*
|
||
|
* @category API
|
||
|
* @package Grav\Plugin\PresentationPlugin
|
||
|
* @subpackage Grav\Plugin\PresentationPlugin\API
|
||
|
* @author Ole Vik <git@olevik.net>
|
||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||
|
* @link https://github.com/OleVik/grav-plugin-presentation
|
||
|
*/
|
||
|
|
||
|
namespace Grav\Plugin\PresentationPlugin\API;
|
||
|
|
||
|
use Grav\Common\Grav;
|
||
|
use Grav\Common\Utils;
|
||
|
|
||
|
/**
|
||
|
* Deckset Parser API
|
||
|
*
|
||
|
* Deckset Parser API for parsing content
|
||
|
*
|
||
|
* @category Extensions
|
||
|
* @package Grav\Plugin\PresentationPlugin\API
|
||
|
* @author Ole Vik <git@olevik.net>
|
||
|
* @license http://www.opensource.org/licenses/mit-license.html MIT License
|
||
|
* @link https://github.com/OleVik/grav-plugin-presentation
|
||
|
*/
|
||
|
class DecksetParser extends Parser implements ParserInterface
|
||
|
{
|
||
|
/**
|
||
|
* Instantiate Parser API
|
||
|
*
|
||
|
* @param array $config Plugin configuration
|
||
|
* @param Transport $transport Transport API
|
||
|
*/
|
||
|
public function __construct($config, $transport)
|
||
|
{
|
||
|
$this->config = $config;
|
||
|
$this->transport = $transport;
|
||
|
// @deprecated 2.0.0
|
||
|
$this->props = [];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Regular expressions
|
||
|
*/
|
||
|
const REGEX_SHORTCODES = '/\[\.(?<property>[a-zA-Z0-9_-]+)?:(?<value>.*)\]/mi';
|
||
|
const REGEX_WORDS = '/^[a-zA-Z0-9_\- ]+/im';
|
||
|
const REGEX_BRACKET_VALUE = '/(?![a-zA-Z0-9_\- ])\((?<property>.*)\)/m';
|
||
|
const REGEX_IMG = '/<img\s*(?:class\s*\=\s*[\'\"](?<class>.*?)[\'\"].*?\s*|src\s*\=\s*[\'\"](?<src>.*?)[\'\"].*?\s*|alt\s*\=\s*[\'\"](?<alt>.*?)[\'\"].*?\s*|title\s*\=\s*[\'\"](?<title>.*?)[\'\"].*?\s*|width\s*\=\s*[\'\"](?<width>.*?)[\'\"].*?\s*|height\s*\=\s*[\'\"](?<height>.*?)[\'\"].*?\s*)+.*?>/im';
|
||
|
const REGEX_IMGS = '/(?:<p>\s*?)?((<a .*<img.*<\/a>|<img.*\s*)*)(?:\s*<\/p>)?/mi';
|
||
|
const REGEX_IMG_PERCENTAGE = '/^(?:\w*\s*)(?<percentage>\d*%$)/mU';
|
||
|
const REGEX_VIDEO = '/(?:<video).*(?:alt="(?<alt>.*)").*(?:src="(?<src>.*)").*(?:<\/video>)/iUm';
|
||
|
const REGEX_AUDIO = '/(?:<audio).*(?<controls>controls.*)\s*(?:alt="(?<alt>.*)").*(?:src="(?<src>.*)").*(?:<\/audio>)/i';
|
||
|
const REGEX_NOTES = '/\^.*/im';
|
||
|
|
||
|
/**
|
||
|
* Parse shortcodes
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
* @param string $id Slide ID
|
||
|
* @param array $page Page instance
|
||
|
*
|
||
|
* @return array Processed content and shortcodes
|
||
|
*/
|
||
|
public function processShortcodes(string $content, string $id, array $page)
|
||
|
{
|
||
|
$base = parent::processShortcodes($content, $id, $page);
|
||
|
if (!empty($base['content'])) {
|
||
|
$content = $base['content'];
|
||
|
}
|
||
|
if (preg_match(self::REGEX_NOTES, $content)) {
|
||
|
$content = self::processNotes($content);
|
||
|
}
|
||
|
if (preg_match(self::REGEX_IMG, $content)) {
|
||
|
$processed = self::processImages($content);
|
||
|
if (!empty($processed['style'])) {
|
||
|
$css = self::collapseToCssString($processed['style']);
|
||
|
$this->transport->setStyle($id, "{\n$css\n}");
|
||
|
}
|
||
|
if (!empty($processed['data'])) {
|
||
|
foreach ($processed['data'] as $attribute => $value) {
|
||
|
$this->transport->setDataAttribute($id, $attribute, $value);
|
||
|
}
|
||
|
}
|
||
|
if (!empty($processed['aria'])) {
|
||
|
foreach ($processed['aria'] as $attribute => $value) {
|
||
|
$this->transport->setAriaAttribute($id, $attribute, $value);
|
||
|
}
|
||
|
}
|
||
|
$content = $processed['content'];
|
||
|
}
|
||
|
if (preg_match(self::REGEX_VIDEO, $content)) {
|
||
|
$processed = self::processVideos($content);
|
||
|
if (!empty($processed['style'])) {
|
||
|
$css = self::collapseToCssString($processed['style']);
|
||
|
$this->transport->setStyle($id, "{\n$css\n}");
|
||
|
}
|
||
|
if (!empty($processed['data'])) {
|
||
|
foreach ($processed['data'] as $attribute => $value) {
|
||
|
$this->transport->setDataAttribute($id, $attribute, $value);
|
||
|
}
|
||
|
}
|
||
|
$content = $processed['content'];
|
||
|
}
|
||
|
if (preg_match(self::REGEX_AUDIO, $content)) {
|
||
|
$processed = self::processAudio($content);
|
||
|
$content = $processed['content'];
|
||
|
}
|
||
|
preg_match_all(
|
||
|
self::REGEX_SHORTCODES,
|
||
|
$content,
|
||
|
$matches,
|
||
|
PREG_SET_ORDER,
|
||
|
0
|
||
|
);
|
||
|
if (!empty($matches)) {
|
||
|
foreach ($matches as $match) {
|
||
|
$property = $match['property'];
|
||
|
$value = $match['value'];
|
||
|
$content = str_replace($match[0], '', $content);
|
||
|
if ($property == 'text') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}");
|
||
|
} elseif ($property == 'text-emphasis') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'i');
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'em');
|
||
|
} elseif ($property == 'text-strong') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'b');
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'strong');
|
||
|
} elseif ($property == 'header') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'h1,h2,h3,h4,h5,h6');
|
||
|
} elseif ($property == 'header-emphasis') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'h1 i,h2 i,h3 i,h4 i,h5 i,h6 i');
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'h1 em,h2 em,h3 em,h4 em,h5 em,h6 em');
|
||
|
} elseif ($property == 'header-strong') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'h1 b,h2 b,h3 b,h4 b,h5 b,h6 b');
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'h1 strong,h2 strong,h3 strong,h4 strong,h5 strong,h6 strong');
|
||
|
} elseif ($property == 'footer-style') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'footer');
|
||
|
} elseif ($property == 'background-color') {
|
||
|
if ($this->transport->getDataAttribute($id, 'background-image') || $this->transport->getDataAttribute($id, 'background-video')) {
|
||
|
$this->transport->setDataAttribute($id, 'background-color', $value);
|
||
|
} else {
|
||
|
$this->transport->setStyle($id, "{\n$property:$value;\n}");
|
||
|
}
|
||
|
} elseif ($property == 'list') {
|
||
|
$css = self::collapseToCssString(self::listShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'ul,ol');
|
||
|
} elseif ($property == 'code') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'code,pre');
|
||
|
} elseif ($property == 'quote') {
|
||
|
$css = self::collapseToCssString(self::genericShortcode($value));
|
||
|
$this->transport->setStyle($id, "{\n$css\n}", 'blockquote');
|
||
|
} elseif ($property == 'build-lists') {
|
||
|
$content = self::buildListShortcode($content);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
return ['content' => $content, 'shortcodes' => $base['shortcodes']];
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse Deckset generic shortcodes
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
*
|
||
|
* @return array Processed content and properties
|
||
|
*/
|
||
|
public static function genericShortcode(string $content)
|
||
|
{
|
||
|
$return = array();
|
||
|
$pieces = explode(',', $content);
|
||
|
foreach ($pieces as $piece) {
|
||
|
$piece = trim($piece);
|
||
|
if (Utils::startsWith($piece, '#')) {
|
||
|
$return['color'] = $piece;
|
||
|
} elseif (Utils::startsWith($piece, 'alignment')) {
|
||
|
preg_match(self::REGEX_BRACKET_VALUE, $piece, $matches);
|
||
|
$return['text-align'] = $matches['property'];
|
||
|
} elseif (Utils::startsWith($piece, 'line-height')) {
|
||
|
preg_match(self::REGEX_BRACKET_VALUE, $piece, $matches);
|
||
|
$return['line-height'] = $matches['property'];
|
||
|
} elseif (Utils::startsWith($piece, 'text-scale')) {
|
||
|
preg_match(self::REGEX_BRACKET_VALUE, $piece, $matches);
|
||
|
$scale = (float) $matches['property'];
|
||
|
$return['font-size'] = (16 * $scale) . 'px';
|
||
|
} elseif (preg_match(self::REGEX_WORDS, $piece)) {
|
||
|
$return['font-family'] = $piece;
|
||
|
}
|
||
|
}
|
||
|
return $return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse Deckset list shortcodes
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
*
|
||
|
* @return array Processed content and properties
|
||
|
*/
|
||
|
public static function listShortcode(string $content)
|
||
|
{
|
||
|
$return = array();
|
||
|
$pieces = explode(',', $content);
|
||
|
foreach ($pieces as $piece) {
|
||
|
$piece = trim($piece);
|
||
|
if (Utils::startsWith($piece, '#')) {
|
||
|
$return['color'] = $piece;
|
||
|
} elseif (Utils::startsWith($piece, 'alignment')) {
|
||
|
preg_match(self::REGEX_BRACKET_VALUE, $piece, $matches);
|
||
|
$return['text-align'] = $matches['property'];
|
||
|
} elseif (Utils::startsWith($piece, 'bullet-character')) {
|
||
|
preg_match(self::REGEX_BRACKET_VALUE, $piece, $matches);
|
||
|
$return['list-style-type'] = $matches['property'];
|
||
|
}
|
||
|
}
|
||
|
return $return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse Deckset build-list shortcode
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
*
|
||
|
* @return array Processed content and properties
|
||
|
*/
|
||
|
public static function buildListShortcode(string $content)
|
||
|
{
|
||
|
$content = str_replace('<li>', '<li class="fragment">', $content);
|
||
|
return $content;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse Deckset Media Background and Inline Images
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
*
|
||
|
* @return array Processed content and properties
|
||
|
*/
|
||
|
public static function processImages(string $content)
|
||
|
{
|
||
|
preg_match_all(self::REGEX_IMG, $content, $images, PREG_SET_ORDER, 0);
|
||
|
$return = array();
|
||
|
$return['content'] = $content;
|
||
|
$count = count($images);
|
||
|
if ($count == 1) {
|
||
|
$return['style'] = $return['data'] = [
|
||
|
'background-repeat' => 'no-repeat',
|
||
|
'background-position' => 'center',
|
||
|
'background-size' => 'contain'
|
||
|
];
|
||
|
if ($images[0]['alt'] == '') {
|
||
|
$return['data']['background-image'] = $images[0]['src'];
|
||
|
} elseif ($images[0]['alt'] == 'fit' || $images[0]['alt'] == 'original') {
|
||
|
$return['data']['background-image'] = $images[0]['src'];
|
||
|
} elseif (preg_match(self::REGEX_IMG_PERCENTAGE, $images[0]['alt'])) {
|
||
|
preg_match_all(self::REGEX_IMG_PERCENTAGE, $images[0]['alt'], $alt, PREG_SET_ORDER, 0);
|
||
|
$return['style']['background-image'] = 'url(' . $images[0]['src'] . ')';
|
||
|
$return['style']['background-size'] = $alt[0]['percentage'];
|
||
|
} elseif ($images[0]['alt'] == 'left') {
|
||
|
$return['style'] = [
|
||
|
'background-image' => 'url(' . $images[0]['src'] . ')',
|
||
|
'background-size' => '50%',
|
||
|
'background-position' => 'center left',
|
||
|
'padding-left' => '50% !important'
|
||
|
];
|
||
|
} elseif ($images[0]['alt'] == 'right') {
|
||
|
$return['style']['background-image'] = 'url(' . $images[0]['src'] . ')';
|
||
|
$return['style']['background-size'] = '50%';
|
||
|
$return['style']['background-position'] = 'center right';
|
||
|
$return['style']['padding-right'] = '50% !important';
|
||
|
}
|
||
|
if (!empty($images[0]['title'])) {
|
||
|
$return['aria'] = [
|
||
|
'role' => 'img',
|
||
|
'label' => $images[0]['title']
|
||
|
];
|
||
|
}
|
||
|
} elseif ($count == 2) {
|
||
|
$return['style']['background-image'] = 'url(' . $images[0]['src'] . '), url(' . $images[1]['src'] . ')';
|
||
|
$return['style']['background-position'] = 'left, right';
|
||
|
$return['style']['background-size'] = '50% auto, 50% auto';
|
||
|
} elseif ($count >= 3) {
|
||
|
$return['style']['background-image'] = 'url(' . $images[0]['src'] . '), url(' . $images[1]['src'] . '), url(' . $images[2]['src'] . ')';
|
||
|
$return['style']['background-position'] = 'left, center, right';
|
||
|
$return['style']['background-size'] = '33% auto, 33% auto, 33% auto';
|
||
|
}
|
||
|
if ($images[0]['alt'] != 'inline') {
|
||
|
$return['content'] = preg_replace(self::REGEX_IMGS, '', $return['content']);
|
||
|
}
|
||
|
if (preg_match(self::REGEX_WORDS, $return['content']) && !Utils::contains($images[0]['alt'], 'original')) {
|
||
|
$return['style']['background-color'] = 'rgba(48, 85, 165, 0.5)';
|
||
|
$return['style']['background-blend-mode'] = 'screen';
|
||
|
}
|
||
|
return $return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse Deckset Media Video
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
*
|
||
|
* @return array Processed content and properties
|
||
|
*/
|
||
|
public static function processVideos(string $content)
|
||
|
{
|
||
|
preg_match_all(self::REGEX_VIDEO, $content, $videos, PREG_SET_ORDER, 0);
|
||
|
$return = array();
|
||
|
$return['content'] = $content;
|
||
|
$count = count($videos);
|
||
|
if ($count == 1) {
|
||
|
if ($videos[0]['alt'] == '') {
|
||
|
$return['data'] = [
|
||
|
'background-video' => $videos[0]['src'],
|
||
|
'background-size' => 'contain'
|
||
|
];
|
||
|
}
|
||
|
}
|
||
|
if ($videos[0]['alt'] != 'inline') {
|
||
|
$return['content'] = preg_replace(self::REGEX_VIDEO, '', $return['content']);
|
||
|
}
|
||
|
return $return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse Deckset Media Audio
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
*
|
||
|
* @return array Processed content and properties
|
||
|
*/
|
||
|
public static function processAudio(string $content)
|
||
|
{
|
||
|
preg_match_all(self::REGEX_AUDIO, $content, $audios, PREG_SET_ORDER, 0);
|
||
|
$return = array();
|
||
|
$return['content'] = $content;
|
||
|
foreach ($audios as $audio) {
|
||
|
$tag = $audio[0];
|
||
|
if (Utils::contains($audio['alt'], 'autoplay')) {
|
||
|
$tag = str_replace($audio['controls'], $audio['controls'] . ' autoplay ', $tag);
|
||
|
}
|
||
|
if (Utils::contains($audio['alt'], 'loop')) {
|
||
|
$tag = str_replace($audio['controls'], $audio['controls'] . ' loop ', $tag);
|
||
|
}
|
||
|
if (Utils::contains($audio['alt'], 'muted')) {
|
||
|
$tag = str_replace($audio['controls'], $audio['controls'] . ' muted ', $tag);
|
||
|
}
|
||
|
$tag = str_replace($audio['controls'], $audio['controls'] . ' controlsList="nodownload"', $tag);
|
||
|
$return['content'] = str_replace($audio[0], $tag, $return['content']);
|
||
|
}
|
||
|
return $return;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Parse Deckset Presenter Notes
|
||
|
*
|
||
|
* @param string $content Content in Page
|
||
|
*
|
||
|
* @return array Processed content
|
||
|
*/
|
||
|
public static function processNotes(string $content)
|
||
|
{
|
||
|
preg_match_all(self::REGEX_NOTES, $content, $notes, PREG_SET_ORDER, 0);
|
||
|
$matches = array();
|
||
|
foreach ($notes as $note) {
|
||
|
$note = $note[0];
|
||
|
$content = str_replace($note, '', $content);
|
||
|
$matches[] = str_replace('^ ', '<p>', $note);
|
||
|
}
|
||
|
$content = preg_replace('/^<p>\s.*$/im', '', $content);
|
||
|
$notesHolder = '<aside class="notes">' . implode("", $matches) . '</aside>';
|
||
|
return $content . $notesHolder;
|
||
|
}
|
||
|
|
||
|
/**
|
||
|
* Convert an array of properties and values to a CSS string
|
||
|
*
|
||
|
* @param array $array Array of strings to process
|
||
|
*
|
||
|
* @return string Concatenated properties and values
|
||
|
*/
|
||
|
public static function collapseToCssString(array $array)
|
||
|
{
|
||
|
$return = '';
|
||
|
foreach ($array as $property => $value) {
|
||
|
$return .= $property . ': ' . $value . ';';
|
||
|
}
|
||
|
return $return;
|
||
|
}
|
||
|
}
|