doc/dev/plugins/presentation/presentation.php

597 lines
20 KiB
PHP
Raw Normal View History

<?php
/**
* Presentation Plugin
*
* PHP version 7
*
* @category Extensions
* @package Grav
* @subpackage Presentation
* @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;
use Grav\Common\Grav;
use Grav\Common\Plugin;
use Grav\Common\Utils;
use Grav\Common\Uri;
use Grav\Common\Inflector;
use Grav\Common\Page\Page;
use Grav\Common\Page\Pages;
use Grav\Common\Page\Media;
use Grav\Common\Page\Collection;
use RocketTheme\Toolbox\Event\Event;
use Grav\Plugin\PresentationPlugin\API\Content;
use Grav\Plugin\PresentationPlugin\API\Parser;
use Grav\Plugin\PresentationPlugin\API\Transport;
use Grav\Plugin\PresentationPlugin\API\Poll;
use Grav\Plugin\PresentationPlugin\Utilities;
/**
* Creates slides using Reveal.js
*
* Class PresentationPlugin
*
* @category Extensions
* @package Grav\Plugin
* @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 PresentationPlugin extends Plugin
{
/**
* Protected variables
*
* @var bool $cache Grav cache setting
* @var Transport $transport Transport API
* @var Parser $parser Parser API
* @var Content $content Content API
*/
protected $cache;
protected $cacheTwig;
protected $transport;
protected $parser;
protected $content;
/**
* Register intial event and libraries
*
* @return array
*/
public static function getSubscribedEvents()
{
include __DIR__ . '/vendor/autoload.php';
return [
'onPluginsInitialized' => ['onPluginsInitialized', 0],
];
}
/**
* Initialize the plugin and events
*
* @return void
*/
public function onPluginsInitialized()
{
if ($this->config->get('plugins.presentation.enabled') != true) {
return;
}
if ($this->config->get('system.debugger.enabled')) {
$this->grav['debugger']->startTimer('presentation', 'Presentation');
}
if ($this->isAdmin() && $this->config->get('plugins.admin')) {
$this->enable(
[
'onPagesInitialized' => ['handleAPI', 0],
'onGetPageTemplates' => ['onGetPageTemplates', 0],
'onTwigSiteVariables' => ['twigBaseUrl', 0],
'onAssetsInitialized' => ['onAdminPagesAssetsInitialized', 0]
]
);
}
$this->cache = $this->grav['config']->get('system.cache.enabled');
$this->cacheTwig = $this->grav['config']->get('system.pages.never_cache_twig');
$this->enable(
[
'onShortcodeHandlers' => ['onShortcodeHandlers', 0],
'onPageInitialized' => ['pagePreCache', 0],
'onPageContentProcessed' => ['pageIteration', -10],
'onTwigExtensions' => ['onTwigExtensions', 0],
'onTwigTemplatePaths' => ['templates', 0],
'onTwigSiteVariables' => ['twigBaseUrl', 0],
'onPagesInitialized' => ['handleAPI', 0],
'onAssetsInitialized' => ['onAssetsInitialized', 0],
'onShutdown' => ['onShutdown', 0]
]
);
if ($this->config->get('system.debugger.enabled')) {
$this->grav['debugger']->stopTimer('presentation');
}
}
/**
* Declare config from plugin-config
*
* @return array Plugin configuration
*/
public function config()
{
if ($this->config->get('plugins.presentation')) {
return (array) $this->config->get('plugins.presentation');
}
return false;
}
/**
* Disable caching for related templates
*
* @return void
*/
public function pagePreCache()
{
if ($this->grav['page']->template() == 'presentation' || $this->grav['page']->template() == 'slide') {
$this->grav['config']->set('system.cache.enabled', false);
$this->grav['config']->set('system.pages.never_cache_twig', true);
}
}
/**
* Construct the page
*
* @return void
*/
public function pageIteration()
{
$grav = $this->grav;
$config = $this->config();
if ($grav['page']->template() == 'presentation' || $grav['page']->template() == 'slide') {
if (!isset($this->grav['twig']->twig_vars['reveal_init'])) {
$baseUrl = $this->grav['uri']->rootUrl(true);
$header = (array) $grav['page']->header();
if (isset($header['presentation'])) {
$config = Utils::arrayMergeRecursiveUnique(
$config,
$header['presentation']
);
}
$this->transport = $this->getAPIInstance(
$config['transport']
);
$this->parser = $this->getAPIInstance(
$config['parser'],
$config,
$this->transport
);
$this->content = $this->getAPIInstance(
$config['content'],
$this->grav,
$config,
$this->parser,
$this->transport
);
$styles = $this->config->get('plugins.presentation.style') ??
$this->config->get('plugins.presentation.styles') ??
[];
if (!empty($styles) && is_array($styles) && Utils::arrayIsAssociative($styles)) {
$this->parser->processor(
$styles,
'presentation',
(array) $grav['page']
);
}
$tree = $this->content->buildTree($grav['page']->route());
$slides = $this->content->buildContent($tree);
$grav['page']->setRawContent($slides);
$menu = $this->content->buildMenu($tree);
$menu = Utilities::flattenArray($menu, 1);
$options = Utilities::parseAmbiguousArrayValues(
$this->config->get('plugins.presentation.options')
);
$options = json_encode($options, JSON_PRETTY_PRINT);
$breakpoints = json_encode(
$this->config->get('plugins.presentation.breakpoints')
);
$this->grav['twig']->twig_vars['reveal_init'] = $options;
$grav['assets']->addInlineJs('const reveal_init = ' . $options . ';', null, 'presentation');
$this->grav['twig']->twig_vars['presentation_menu'] = $menu;
$this->grav['twig']->twig_vars['presentation_breakpoints'] = $breakpoints;
if ($grav['page']->template() == 'presentation') {
$grav['assets']->addInlineCss($this->transport->getStyles(), null, 'presentation');
}
}
}
}
/**
* Handle API
*
* @return void
*/
public function handleAPI()
{
$uri = $this->grav['uri'];
$page = $this->grav['page'];
$plugins = $this->config->get('plugins');
if ($uri->path() == '/' . $this->config->get('plugins.presentation.api_route')) {
if ($_GET['action'] == 'poll') {
$this->handlePollAPI(
$uri,
$page,
(array) $this->config->get('plugins.presentation')
);
}
}
if (isset($plugins['admin']) && $plugins['admin']['enabled'] == true) {
$adminRoute = $this->config->get('plugins')['admin']['route'];
if ($uri->path() == $adminRoute . '/' . $this->config->get('plugins.presentation.api_route')) {
if ($_GET['action'] == 'save') {
$this->handleSaveAPI();
}
}
}
}
/**
* Handle Save API
*
* @return void
*/
public function handleSaveAPI()
{
if (!$this->isAdmin() || empty($_POST)) {
return;
}
header('Content-Type: application/json');
header("allow-control-access-origin: * ");
header('HTTP/1.1 200 OK');
try {
$post = file_get_contents('php://input') ?? $_POST;
$post = json_decode($post, true);
$pages = Grav::instance()['pages'];
$page = $pages->find('/' . $post['route']);
$page->rawMarkdown(base64_decode($post['content']));
$page->save();
echo '200 OK';
} catch (\Exception $e) {
echo $e;
}
exit();
}
/**
* Handle Poll API
*
* @param array $config Plugin configuration
*
* @return void
*/
public function handlePollAPI($config)
{
if (isset($config['sync']) && $config['sync'] == 'poll') {
set_time_limit(0);
header('Cache-Control: no-cache, no-store, max-age=0, must-revalidate');
header('Pragma: no-cache');
if (!isset($_GET['mode'])) {
header('HTTP/1.1 400 Bad Request');
exit('400 Bad Request');
}
$res = Grav::instance()['locator'];
$target = $res->findResource('cache://') . '/presentation';
include_once __DIR__ . '/API/PollInterface.php';
include_once __DIR__ . '/API/Poll.php';
$poll = new Poll($target, 'Poll.json');
gc_enable();
if (!isset($config['token']) || empty($config['token'])) {
return;
}
if ($_GET['mode'] == 'set' && isset($_GET['data'])) {
Utilities::authorize($config['token']);
$poll->remove();
header('Content-Type:text/plain');
header('HTTP/1.1 202 Accepted');
$poll->set(urldecode($_GET['data']));
} elseif ($_GET['mode'] == 'get') {
header('Content-Type: application/json');
header('HTTP/1.1 200 OK');
$poll->get();
} elseif ($_GET['mode'] == 'remove') {
Utilities::authorize($config['token']);
header('Content-Type:text/plain');
header('HTTP/1.1 200 OK');
$poll->remove();
}
$poll = null;
unset($poll);
gc_collect_cycles();
gc_disable();
exit();
}
}
/**
* Add templates-directory to Twig paths
*
* @return void
*/
public function templates()
{
$this->grav['twig']->twig_paths[] = __DIR__ . '/templates';
}
/**
* Add root URL to Twig vars
*
* @return void
*/
public function twigBaseUrl()
{
$uri = $this->grav['uri']->rootUrl(true);
$this->grav['twig']->twig_vars['presentation_base_url'] = $uri;
}
/**
* Reset cache on shutdown
*
* @return void
*/
public function onShutdown()
{
$this->grav['config']->set('system.cache.enabled', $this->cache);
$this->grav['config']->set('system.pages.never_cache_twig', $this->cacheTwig);
}
/**
* Add Twig Extensions
*
* @return void
*/
public function onTwigExtensions()
{
include_once __DIR__ . '/twig/CallStaticExtension.php';
$this->grav['twig']->twig->addExtension(new PresentationPlugin\CallStaticTwigExtension());
include_once __DIR__ . '/twig/FileFinderExtension.php';
$this->grav['twig']->twig->addExtension(new PresentationPlugin\FileFinderTwigExtension());
}
/**
* Register Page templates
*
* @param Event $event RocketTheme\Toolbox\Event\Event
*
* @return void
*/
public function onGetPageTemplates(Event $event)
{
$types = $event->types;
$locator = Grav::instance()['locator'];
$locations = [
'plugin://' . $this->name . '/blueprints/',
'theme://blueprints/',
'user://blueprints/'
];
foreach ($locations as $location) {
$types->register(
'presentation',
$locator->findResource($location . 'presentation.yaml')
);
$types->register(
'slide',
$locator->findResource($location . 'slide.yaml')
);
}
}
/**
* Get API Instance
*
* @param string $class Class name
* @param mixed ...$args Class arguments
*
* @return mixed Class Instance
*/
public function getAPIInstance(string $class, ...$args)
{
$caller = '\Grav\Plugin\PresentationPlugin\API\\' . $class;
return new $caller(...$args);
}
/**
* Get list of modular scales
*
* @return array List of modular scales
*/
public static function getModularScale()
{
return array(
['name' => 'unison', 'ratio' => '1:1', 'numerical' => 1],
['name' => 'minor second', 'ratio' => '15:16', 'numerical' => 1.067],
['name' => 'major second', 'ratio' => '8:9', 'numerical' => 1.125],
['name' => 'minor third', 'ratio' => '5:6', 'numerical' => 1.2],
['name' => 'major third', 'ratio' => '4:5', 'numerical' => 1.25],
['name' => 'perfect fourth', 'ratio' => '3:4', 'numerical' => 1.333],
['name' => 'aug. fourth / dim. fifth', 'ratio' => '1:√2', 'numerical' => 1.414],
['name' => 'perfect fifth', 'ratio' => '2:3', 'numerical' => 1.5],
['name' => 'minor sixth', 'ratio' => '5:8', 'numerical' => 1.6],
['name' => 'golden section', 'ratio' => '1:1.618', 'numerical' => 1.618],
['name' => 'major sixth', 'ratio' => '3:5', 'numerical' => 1.667],
['name' => 'minor seventh', 'ratio' => '9:16', 'numerical' => 1.778],
['name' => 'major seventh', 'ratio' => '8:15', 'numerical' => 1.875],
['name' => 'octave', 'ratio' => '1:2', 'numerical' => 2],
['name' => 'major tenth', 'ratio' => '2:5', 'numerical' => 2.5],
['name' => 'major eleventh', 'ratio' => '3:8', 'numerical' => 2.667],
['name' => 'major twelfth', 'ratio' => '1:3', 'numerical' => 3],
['name' => 'double octave', 'ratio' => '1:4', 'numerical' => 4]
);
}
/**
* Parse modular scales for blueprints
*
* @return array Blueprint-friendly list of modular scales
*/
public static function getModularScaleBlueprintOptions()
{
$options = ['' => 'None'];
foreach (self::getModularScale() as $scale) {
$options[(string) $scale['numerical']] = $scale['numerical'] . ' (' . ucwords($scale['name']) . ', ' . $scale['ratio'] . ')';
}
return $options;
}
/**
* Get class names for blueprints
*
* @param string $key Needle to search for
*
* @return array Blueprint-friendly list of class names
*/
public static function getClassNamesBlueprintOptions(string $key)
{
$regex = '/Grav\\\\Plugin\\\\PresentationPlugin\\\\API\\\\(?<api>.*)/i';
$classes = preg_grep($regex, get_declared_classes());
$matches = preg_grep('/' . $key . '/i', $classes);
$options = [
'' => 'None',
$key => $key
];
foreach ($matches as $match) {
$match = str_replace('Grav\Plugin\PresentationPlugin\API\\', '', $match);
$options[$match] = $match;
}
return $options;
}
/**
* Get reveal.js themes
*
* @return array Associative array of styles
*/
public static function getRevealThemes()
{
include __DIR__ . '/vendor/autoload.php';
$inflector = new Inflector();
$options = array('none' => 'None');
$path = 'user://plugins/presentation/node_modules/reveal.js/css/theme';
$location = Grav::instance()['locator']->findResource($path, true);
if (!$location) {
return $options;
}
$files = Utilities::filesFinder($location, ['css']);
if (empty($files)) {
return $options;
}
foreach ($files as $file) {
$key = $file->getBasename('.' . $file->getExtension());
$options[$key] = $inflector->titleize($key);
}
return $options;
}
/**
* Initialize shortcodes
*
* @return void
*/
public function onShortcodeHandlers()
{
$this->grav['shortcode']->registerAllShortcodes(__DIR__ . '/shortcodes');
}
/**
* Add admin assets
*
* @return void
*/
public function onAdminPagesAssetsInitialized()
{
$uri = $this->grav['uri'];
$res = Grav::instance()['locator'];
$path = $res->findResource('plugin://' . $this->name, false);
$adminRoute = $this->config->get('plugins.admin.route');
if (!Utils::contains($uri->path(), $adminRoute . '/pages')) {
return;
}
if ($this->config->get('plugins.presentation.admin_async_save') !== true) {
return;
}
$adminRoute = $uri->rootUrl(true) . $adminRoute;
$inlineJsConstants = array(
'presentationAPIRoute = "' . $adminRoute . '/' . $this->config->get('plugins.presentation.api_route') . '"',
'presentationAPITimeout = ' . ($this->config->get('plugins.presentation.poll_timeout') ?: 2000) * 2.5,
'presentationAPIRetryLimit = ' . ($this->config->get('plugins.presentation.poll_retry_limit') ?: 10),
'presentationAdminAsyncSave = ' . ($this->config->get('plugins.presentation.admin_async_save') ?: 0),
'presentationAdminAsyncSaveTyping = ' . ($this->config->get('plugins.presentation.admin_async_save_typing') ?: 0)
);
$inlineJs = '';
foreach ($inlineJsConstants as $constant) {
$inlineJs .= 'const ' . $constant . ';' . "\n";
}
$this->grav['assets']->addInlineJs($inlineJs);
$this->grav['assets']->addJs(
$path . '/js/save.js'
);
$this->grav['assets']->addJs(
$path . '/node_modules/axios/dist/axios.min.js'
);
$this->grav['assets']->addJs(
$path . '/node_modules/js-base64/base64.min.js'
);
$this->grav['assets']->addJs(
$path . '/node_modules/codemirror/lib/codemirror.js'
);
}
/**
* Add general assets
*
* @return void
*/
public function onAssetsInitialized()
{
if ($this->config->get('plugins.presentation.textsizing')
&& $this->config->get('plugins.presentation.breakpoints')
&& !empty($this->config->get('plugins.presentation.breakpoints'))
) {
$css = '';
$element = '.reveal .slides section section, .reveal.center .slides section section';
$breakpoints = array_keys(
$this->config->get('plugins.presentation.breakpoints')
);
$sizes = array_values(
$this->config->get('plugins.presentation.breakpoints')
);
for ($i = 0; $i < count($breakpoints); $i++) {
$css .= '@media screen and ';
if ($i == 0) {
$css .= '(min-width: 0px) and ';
$css .= '(max-width:' . (intval($breakpoints[$i + 1]) - 1) . 'px) ';
} else {
$css .= '(min-width:' . $breakpoints[$i] . 'px) ';
}
$css .= '{' . $element . '{font-size:' . $sizes[$i] . 'px !important;}}';
$css .= "\n";
}
$this->grav['assets']->addInlineCss($css, null, 'critical');
}
$iframe = '.presentation-iframe {
width: 100%;
width: -moz-available;
width: -webkit-fill-available;
width: fill-available;
height: 100%;
height: -moz-available;
height: -webkit-fill-available;
height: fill-available;
}';
$this->grav['assets']->addInlineCss($iframe);
}
}