* @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 * @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 = '/\[\.(?[a-zA-Z0-9_-]+)?:(?.*)\]/mi'; const REGEX_WORDS = '/^[a-zA-Z0-9_\- ]+/im'; const REGEX_BRACKET_VALUE = '/(?![a-zA-Z0-9_\- ])\((?.*)\)/m'; const REGEX_IMG = '/.*?)[\'\"].*?\s*|src\s*\=\s*[\'\"](?.*?)[\'\"].*?\s*|alt\s*\=\s*[\'\"](?.*?)[\'\"].*?\s*|title\s*\=\s*[\'\"](?.*?)[\'\"].*?\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; } }