* @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\Uri; use Grav\Common\Utils; use Grav\Plugin\PresentationPlugin\Utilities; use Thunder\Shortcode\Parser\RegularParser; use Thunder\Shortcode\Parser\RegexParser; use Thunder\Shortcode\Parser\WordpressParser; use Thunder\Shortcode\Processor\Processor; use Thunder\Shortcode\Shortcode\ShortcodeInterface; use Thunder\Shortcode\HandlerContainer\HandlerContainer; /** * Parser API * * 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 Parser implements ParserInterface { /** * Regular expressions */ const REGEX_MEDIA_P = '/

\s*(\s*|\s*/i'; /** * 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 = []; } /** * Parse shortcodes * * @param string $content Markdown content in Page * @param string $id Slide ID * @param array $page Page configuration * * @return array Processed content and shortcodes */ public function processShortcodes(string $content, string $id, array $page) { $handlers = new HandlerContainer(); $path = $page['path']; $handlers->setDefault( function (ShortcodeInterface $sc) use ($id, $path) { $name = $sc->getName(); $value = $sc->getParameter($name, $sc->getBbCode()) ?? ''; if (Utils::startsWith($name, 'class')) { $this->transport->setClass($id, $value); } elseif (Utils::startsWith($name, 'style')) { $property = str_replace('style-', '', $name); $this->styleProcessor($id, $property, $value, [$path]); } elseif (Utils::startsWith($name, 'styles')) { $property = str_replace('styles-', '', $name); $this->styleProcessor($id, $property, $value, [$path]); } elseif (Utils::startsWith($name, 'data')) { $property = str_replace('data-', '', $name); $this->transport->setDataAttribute($id, $property, $value); } elseif (Utils::startsWith($name, 'hide')) { $this->transport->setDataAttribute($id, 'hide', "true"); } return; } ); $parser = "Thunder\Shortcode\Parser\\" . $this->config['shortcode_parser']; $parser = new $parser(); $processor = new Processor($parser, $handlers); return [ 'content' => $processor->process($content), 'shortcodes' => $parser->parse($content) ]; } /** * Process key-value pairs of options * * @param array $data Key-value pairs of options * @param string $id Target id-attribute * @param array $paths Paths to use for file-finding * @param string $mode Which Transport to perform * * @return boolean */ public function processor(array $data, string $id, array $paths = [], string $mode = 'style') { if (empty($data)) { return false; } foreach ($data as $key => $value) { if (Utils::startsWith('style-', $key)) { $mode = 'style'; } elseif (Utils::startsWith('styles-', $key)) { $mode = 'style'; } elseif (Utils::startsWith('data-', $key)) { $mode = 'data'; } elseif (Utils::startsWith('aria-', $key)) { $mode = 'aria'; } $key = str_replace($mode . '-', '', $key); if ($mode == 'style') { if (empty($value)) { continue; } $this->styleProcessor($id, $key, $value, $paths); } elseif ($mode == 'data') { $this->transport->setDataAttribute($id, $key, $value); } elseif ($mode == 'aria') { $this->transport->setAriaAttribute($id, $key, $value); } } return true; } /** * Process style * * @param string $id Target id-attribute * @param string $property CSS property name * @param string $value CSS property value * @param array $paths Locations to search for asset in * * @return void */ public function styleProcessor(string $id, string $property, string $value, array $paths = []) { if ($property == 'background-image') { if (!Uri::isValidUrl($value)) { $locations = array_merge( $paths, [ '', 'user/pages', 'user/pages/images', ] ); $locations = Utilities::explodeFileLocations($locations, GRAV_ROOT, '/', '/'); $file = Utilities::fileFinder($value, $locations); $file = str_ireplace(GRAV_ROOT, '', $file); $value = $file; } $this->transport->setStyle($id, "{\n$property: url($value);\n}"); } elseif ($property == 'header-font-family') { $this->transport->setStyle($id, "{\nfont-family:$value;\n}", 'h1,h2,h3,h4,h5,h6'); } elseif ($property == 'header-color') { $this->transport->setStyle($id, "{\ncolor:$value;\n}", 'h1,h2,h3,h4,h5,h6'); } elseif ($property == 'block-font-family') { $this->transport->setStyle($id, "{\nfont-family:$value;\n}"); } elseif ($property == 'block-color') { $this->transport->setStyle($id, "{\ncolor:$value;\n}"); } else { $this->transport->setStyle($id, "{\n$property:$value;\n}"); } } /** * Set modular scales in CSS * * @param string $id Slide id-attribute * @param string $scale Modular Scale Ratio * @param float $modifier Optional multiplication-parameter * * @return void */ public function setModularScale(string $id, string $scale, float $modifier = null) { $scale = (float) $scale; $steps = array(6, 5, 4, 3, 2, 1, 0); for ($i = 1; $i <= 6; $i++) { $value = self::modularScale($steps[$i], 16, $scale, true); $value = $modifier != null ? $value * $modifier : $value; $this->transport->setStyle($id, '{font-size:' . $value . 'em;}', 'h' . $i); } } /** * Get font-size in pixels * * @param integer $step Step in scale * @param integer $base Base font-size * @param float $ratio Rhythm * @param bool $relative Output relative units * * @return float Modular Scale breakpoint */ public static function modularScale(int $step, int $base, float $ratio, bool $relative = null) { if ($relative == true) { return round((pow($ratio, $step) * $base) / $base, 3); } else { return round((pow($ratio, $step) * $base), 2); } } /** * Remove wrapping paragraph from img-element * * @param string $content Markdown content in Page * * @return string Processed content */ public static function unwrapImage(string $content) { $unwrap = self::REGEX_MEDIA_P; $content = preg_replace($unwrap, "$1", $content); return $content; } /** * Parse shortcodes * * @param string $content Markdown content in Page * @param string $id Slide id-attribute * * @deprecated 2.0.0 * * @return array Processed contents and properties */ public function interpretShortcodes(string $content, string $id) { $handlers = new HandlerContainer(); $handlers->setDefault( function (ShortcodeInterface $sc) { $return = array(); $name = $sc->getName(); $value = $sc->getParameter($name, $sc->getBbCode()); if (Utils::startsWith($name, 'class')) { $this->props['class'] = $value; } elseif (Utils::startsWith($name, 'style')) { $name = str_replace('style-', '', $name); $this->props['styles'][$name] = $value; } elseif (Utils::startsWith($name, 'data')) { $this->props['styles'][$name] = $value; } elseif (Utils::startsWith($name, 'hide')) { $this->props['hide'] = [true]; } return; } ); $parser = "Thunder\Shortcode\Parser\\" . $this->config['shortcode_parser']; $processor = new Processor(new $parser(), $handlers); return ['content' => $processor->process($content), 'props' => $this->props]; } /** * Create HTML for fragments * * @param string $content Markdown content in Page * * @deprecated 2.0.0 * * @return string Processed contents */ public function processFragments(string $content) { $content = preg_replace( self::REGEX_FRAGMENT_SHORTCODE, '\\2', $content ); return $content; } /** * Process styles and data-attributes * * @param array $styles List of key-value pairs * @param string $route Route to Page for relative assets * @param string $id Slide id-attribute * @param string $base Base path to prepend to file * * @deprecated 2.0.0 * * @return string Processed styles, in inline string */ public function processStylesData(array $styles, string $route, string $id, string $base = "") { $inline = $data = ''; foreach ($styles as $property => $value) { if ($property == 'background-image') { if (!Uri::isValidUrl($value)) { $locations = array( '', 'user/pages', 'user/pages/images', ); $locations = Utilities::explodeFileLocations($locations, GRAV_ROOT, '/', '/'); $file = Utilities::fileFinder($value, $locations); $file = str_ireplace(GRAV_ROOT, '', $file); $value = $base . $file; } $inline .= $property . ': url(' . $value . ');'; } elseif (Utils::startsWith($property, 'data')) { $data .= ' ' . $property . '="' . $value . '"'; if ($property == 'data-textsize-scale') { $this->transport->setClass($id, 'textsizing'); } } elseif ($property == 'header-font-family') { $this->transport->setStyle($id, "{\nfont-family:$value;\n}", 'h1,h2,h3,h4,h5,h6'); } elseif ($property == 'header-color') { $this->transport->setStyle($id, "{\ncolor:$value;\n}", 'h1,h2,h3,h4,h5,h6'); } elseif ($property == 'block-font-family') { $this->transport->setStyle($id, "{\nfont-family:$value;\n}"); } elseif ($property == 'block-color') { $this->transport->setStyle($id, "{\ncolor:$value;\n}"); } else { $inline .= $property . ': ' . $value . ';'; } } return array( 'style' => $inline, 'data' => $data ); } }