feat(demo): add story 1 — Sorano: Rock and Time

This commit is contained in:
2026-06-20 21:19:57 +02:00
parent 42ed59a6b3
commit 8f87155c1d
5508 changed files with 1595740 additions and 124 deletions
@@ -0,0 +1,7 @@
<?php
// This file is for older Grav versions (needed during plugin update).
if (!class_exists(\Grav\Plugin\Shortcodes\Shortcode::class, false)) {
require_once __DIR__ . '/shortcodes/Shortcode.php';
}
@@ -0,0 +1,15 @@
<?php
// This file is for older Grav versions (needed during plugin update).
@trigger_error(
'\\Grav\\Plugin\\ShortcodeManager class is deprecated, use \\Grav\\Plugin\\ShortcodeCore\\ShortcodeManager instead',
E_USER_DEPRECATED
);
if (!class_exists(\Grav\Plugin\ShortcodeCore\ShortcodeManager::class, false)) {
require_once __DIR__ . '/plugin/ShortcodeManager.php';
}
// Create alias for the deprecated class.
class_alias(\Grav\Plugin\ShortcodeCore\ShortcodeManager::class, \Grav\Plugin\ShortcodeManager::class);
@@ -0,0 +1,7 @@
<?php
// This file is for older Grav versions (needed during plugin update).
if (!class_exists(\Grav\Plugin\ShortcodeCore\ShortcodeObject::class, false)) {
require_once __DIR__ . '/shortcodes/ShortcodeObject.php';
}
@@ -0,0 +1,24 @@
<?php
namespace Grav\Plugin\ShortcodeCore;
// Check if the new class has been autoloaded. If not, trigger deprecation error.
if (!class_exists(\Grav\Plugin\Shortcodes\Shortcode::class, false)) {
@trigger_error(
Shortcode::class . ' class is deprecated, use \\Grav\\Plugin\\Shortcodes\\Shortcode instead',
E_USER_DEPRECATED
);
}
// Create alias for the deprecated class.
class_alias(\Grav\Plugin\Shortcodes\Shortcode::class, Shortcode::class);
// Make sure that both IDE and composer knows about the deprecated class.
if (false) {
/**
* @deprecated 4.2.1 This was a bad idea, reverting back to the old class.
*/
abstract class Shortcode extends \Grav\Plugin\Shortcodes\Shortcode
{
}
}
@@ -0,0 +1,403 @@
<?php
namespace Grav\Plugin\ShortcodeCore;
use Grav\Common\Config\Config;
use Grav\Common\Data\Data;
use Grav\Common\Grav;
use Grav\Common\Page\Interfaces\PageInterface;
use Thunder\Shortcode\EventContainer\EventContainer;
use Thunder\Shortcode\HandlerContainer\HandlerContainer;
use Thunder\Shortcode\Parser\RegexParser;
use Thunder\Shortcode\Parser\RegularParser;
use Thunder\Shortcode\Parser\WordpressParser;
use Thunder\Shortcode\Processor\Processor;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
use Thunder\Shortcode\Syntax\CommonSyntax;
class ShortcodeManager
{
/** @var Grav $grav */
protected $grav;
/** @var Config */
protected $config;
/** @var PageInterface $page */
protected $page;
/** @var HandlerContainer $handlers */
protected $handlers;
/** @var HandlerContainer $raw_handlers */
protected $raw_handlers;
/** @var EventContainer $events */
protected $events;
/** @var array */
protected $assets;
/** @var array */
protected $states;
/** @var array */
protected $objects;
/**
* initialize some internal instance variables
*/
public function __construct()
{
$this->grav = Grav::instance();
$this->config = $this->grav['config'];
$this->handlers = new HandlerContainer();
$this->raw_handlers = new HandlerContainer();
$this->events = new EventContainer();
$this->states = [];
$this->assets = [];
$this->objects = [];
}
/**
* add CSS and JS assets to the Manager so that they can be saved to cache
* for subsequent cached pages
*
* @param mixed $actionOrAsset the type of asset (JS or CSS) or, if the second parameter is omitted,
* a collection or an array of asset.
* @param string $asset the asset path in question
*/
public function addAssets($actionOrAsset, $asset = null)
{
if ($asset == null) {
if (is_array($actionOrAsset)) {
$this->assets[''] = array_merge($this->assets[''] ?? array(), $actionOrAsset);
} else {
$this->assets[''] [] = $actionOrAsset;
}
} else {
if (isset($this->assets[$actionOrAsset]) && in_array($asset, $this->assets[$actionOrAsset], true)) {
return;
}
$this->assets[$actionOrAsset] [] = $asset;
}
}
/**
* return a multi-dimensional array of all the assets
*
* @return array the assets array
*/
public function getAssets()
{
return $this->assets;
}
/**
* reset the assets
*/
public function resetAssets()
{
$this->assets = [];
}
/**
* adds ad object
* @param string $key The key to look up the object
* @param object $object The object to store
*/
public function addObject($key, $object)
{
$new = [$object->name() => $object];
if (array_key_exists($key, $this->objects)) {
$current = (array)$this->objects[$key];
$this->objects[$key] = $current + $new;
} else {
$this->objects[$key] = $new;
}
}
/**
* sets all the objects
* @param array $objects The objects array
*/
public function setObjects($objects)
{
$this->objects = $objects;
}
/**
* return all the objects
* @return array The objects array
*/
public function getObjects()
{
return $this->objects;
}
/**
* reset the objects
*/
public function resetObjects()
{
$this->objects = [];
}
/**
* reset the states
*/
public function resetStates()
{
$this->states = [];
}
/**
* Reset all accumulated state (objects, assets, states).
* Useful for batch processing scenarios like search indexing
* where memory needs to be freed between pages.
*/
public function resetAll()
{
$this->objects = [];
$this->assets = [];
$this->states = [];
}
/**
* returns the current handler container object
*
* @return HandlerContainer
*/
public function getHandlers()
{
return $this->handlers;
}
/**
* returns the current raw handler container object
*
* @return HandlerContainer
*/
public function getRawHandlers()
{
return $this->raw_handlers;
}
/**
* returns the current event container object
*
* @return EventContainer
*/
public function getEvents()
{
return $this->events;
}
/**
* Register an individual shortcode with the manager so it can be operated on by the Shortcode library
*
* @param string $name the name of the shortcode (should match the classname)
* @param string|null $directory directory where the shortcode is located
*/
public function registerShortcode($name, $directory = null)
{
$className = 'Grav\\Plugin\\Shortcodes\\' . basename($name, '.php');
if (!class_exists($className) && $directory) {
$path = rtrim($directory, '/').'/'.$name;
require_once $path;
}
// Make sure the class exists, extends Shortcode and is not abstract.
if (class_exists($className) && is_subclass_of($className, \Grav\Plugin\Shortcodes\Shortcode::class)) {
$reflection = new \ReflectionClass($className);
if (!$reflection->isAbstract()) {
/** @var \Grav\Plugin\Shortcodes\Shortcode $shortcode */
$shortcode = new $className();
$shortcode->init();
}
}
}
/**
* register all files as shortcodes in a particular directory
* @param string $directory directory where the shortcodes are located
* @param array $options Extra options
*/
public function registerAllShortcodes($directory, array $options = [])
{
try {
$ignore = $options['ignore'] ?? [];
foreach (new \DirectoryIterator($directory) as $file) {
if ($file->isDot() || \in_array($file->getBasename('.php'), $ignore, true)) {
continue;
}
$this->registerShortcode($file->getFilename(), $directory);
}
} catch (\UnexpectedValueException $e) {
Grav::instance()['log']->error('ShortcodeCore Plugin: Directory not found => ' . $directory);
}
}
/**
* setup the markdown parser to handle shortcodes properly
*
* @param object $markdown the markdown parser object
*/
public function setupMarkdown($markdown)
{
$markdown->addBlockType('[', 'ShortCodes', true, false);
$markdown->blockShortCodes = function($Line) {
$valid_shortcodes = implode('|', $this->handlers->getNames());
$regex = '/^\[\/?(?:' . $valid_shortcodes . ')[^\]]*\]$/';
if (preg_match($regex, trim($Line['body']), $matches)) {
return [
'markup' => $Line['body'],
];
}
return null;
};
}
/**
* process the content by running over all the known shortcodes with the
* chosen parser
*
* @param PageInterface $page the page to work on
* @param Data $config configuration merged with the page config
* @param null $handlers
* @return string
*/
public function processContent(PageInterface $page, Data $config, $handlers = null)
{
$parser = $this->getParser($config->get('parser'));
if (!$handlers) {
$handlers = $this->handlers;
}
if ($page && $config->get('enabled')) {
$this->page = $page;
$content = $page->getRawContent();
$processor = new Processor(new $parser(new CommonSyntax()), $handlers);
$processor = $processor->withEventContainer($this->events);
return $processor->process($content);
}
return null;
}
public function processRawContent(PageInterface $page, Data $config)
{
return $this->processContent($page, $config, $this->raw_handlers);
}
/**
* Allow the processing of shortcodes directly on a string
* For example when used by Twig directly
*
* @param $str
* @param null $handlers
* @return string
*/
public function processShortcodes($str, $handlers = null)
{
$parser = $this->getParser($this->config->get('parser'));
if (!$handlers) {
$handlers = $this->handlers;
}
$processor = new Processor(new $parser(new CommonSyntax()), $handlers);
return $processor->process($str);
}
public function processShortcodesRaw($str)
{
return $this->processShortcodes($str, $this->raw_handlers);
}
/**
* set a state of a particular item with a hash for retrieval later
*
* @param string $hash a unique hash code
* @param object $item some item to store
*/
public function setStates($hash, $item)
{
$this->states[$hash][] = $item;
}
/**
* returns the shortcode of a specific hash
*
* @param string $hash unique id of state
* @return ShortcodeInterface shortcode stored for this hash
*/
public function getStates($hash)
{
if (array_key_exists($hash, $this->states)) {
return $this->states[$hash];
}
return null;
}
/**
* helper method to create a unique shortcode based on the content
*
* @param ShortcodeInterface $shortcode
* @return string
*/
public function getId(ShortcodeInterface $shortcode)
{
return substr(md5($shortcode->getShortcodeText()), -10);
}
/**
* Sets the current page context
*
* @param PageInterface $page
*/
public function setPage(PageInterface $page)
{
$this->page = $page;
}
/** gets the current page context if set */
public function getPage()
{
return $this->page;
}
/**
* Get the appropriate parser object
*
* @param $parser
* @return string
*/
protected function getParser($parser)
{
switch($parser)
{
case 'regular':
$parser = RegularParser::class;
break;
case 'wordpress':
$parser = WordpressParser::class;
break;
default:
$parser = RegexParser::class;
break;
}
return $parser;
}
}
@@ -0,0 +1,33 @@
<?php
namespace Grav\Plugin\ShortcodeCore;
class ShortcodeObject
{
protected $obj_name;
protected $obj_object;
public function __construct($name, $object)
{
$this->obj_name = $name;
$this->obj_object = $object;
}
public function __toString()
{
return $this->obj_object;
}
public function name()
{
return $this->obj_name;
}
public function object()
{
return $this->obj_object;
}
}
// Make sure we also autoload the deprecated class.
class_exists(\Grav\Plugin\Shortcodes\ShortcodeObject::class);
@@ -0,0 +1,27 @@
<?php
namespace Grav\Plugin\ShortcodeCore;
use Grav\Common\Grav;
class ShortcodeTwigVar
{
public function __call($name, $args)
{
/** @var ShortcodeManager $shortcode */
$shortcode = Grav::instance()['shortcode'];
$objects = $shortcode->getObjects();
if ($objects) {
return $objects[$name] ?? [];
}
$page_meta = Grav::instance()['page']->getContentMeta('shortcodeMeta');
if (isset($page_meta['shortcode'])) {
$objects = (array) $page_meta['shortcode'];
return $objects[$name] ?? [];
}
return [];
}
}
@@ -0,0 +1,26 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class AlignShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('center', static function(ShortcodeInterface $sc) {
return '<div style="text-align: center;">' . $sc->getContent() . '</div>';
});
$this->shortcode->getHandlers()->add('left', static function(ShortcodeInterface $sc) {
return '<div style="text-align: left;">' . $sc->getContent() . '</div>';
});
$this->shortcode->getHandlers()->add('right', static function(ShortcodeInterface $sc) {
return '<div style="text-align: right;">' . $sc->getContent() . '</div>';
});
$this->shortcode->getHandlers()->add('justify', static function(ShortcodeInterface $sc) {
return '<div style="text-align: justify;">' . $sc->getContent() . '</div>';
});
}
}
@@ -0,0 +1,16 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class ColorShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('color', function(ShortcodeInterface $sc) {
$color = $sc->getParameter('color', $this->getBbCode($sc));
return '<span style="color: ' . $color . ';">' . $sc->getContent() . '</span>';
});
}
}
@@ -0,0 +1,27 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class ColumnsShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('columns', static function(ShortcodeInterface $sc) {
$column_count = (int)$sc->getParameter('count', 2);
$column_width = $sc->getParameter('width', 'auto');
$column_gap = $sc->getParameter('gap', 'normal');
$column_rule = $sc->getParameter('rule', false);
$css_style = 'columns:' . $column_count . ' ' . $column_width . ';-moz-columns:' . $column_count . ' ' . $column_width . ';';
$css_style .= 'column-gap:' . $column_gap . ';-moz-column-gap:' . $column_gap . ';';
if ($column_rule) {
$css_style .= 'column-rule:' . $column_rule . ';-moz-column-rule:' . $column_rule . ';';
}
return '<div class="sc-columns" style="' . $css_style . '">' . $sc->getContent() . '</div>';
});
}
}
@@ -0,0 +1,26 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class DetailsShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('details', function(ShortcodeInterface $sc) {
// Get summary/title
$summary = $sc->getParameter('summary', $this->getBbCode($sc));
$summaryHTML = $summary ? '<summary>' . $summary . '</summary>' : '';
// Get classes for details
$class = $sc->getParameter('class', $this->getBbCode($sc));
$classHTML = (isset($class) and $class !== $summary) ? 'class="' . $class . '"' : '';
// Get content
$content = $sc->getContent();
// Return the details/summary block
return '<details ' . $classHTML . '>' . $summaryHTML . $content . '</details>';
});
}
}
@@ -0,0 +1,22 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class DivShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('div', static function(ShortcodeInterface $sc) {
$id = $sc->getParameter('id');
$class = $sc->getParameter('class');
$style = $sc->getParameter('style');
$id_output = $id ? ' id="' . $id . '" ': '';
$class_output = $class ? ' class="' . $class . '"' : '';
$style_output = $style ? ' style="' . $style . '"' : '';
return '<div ' . $id_output . $class_output . $style_output . '>' . $sc->getContent() . '</div>';
});
}
}
@@ -0,0 +1,27 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Grav\Common\Utils;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class FigureShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('figure', function(ShortcodeInterface $sc) {
$id = $sc->getParameter('id');
$class = $sc->getParameter('class');
$caption = $sc->getParameter('caption');
$page = $this->grav['page'];
// Process any markdown on caption
$caption = Utils::processMarkdown($caption, false, $page);
$id_output = $id ? 'id="' . $id . '" ': '';
$class_output = $class ? 'class="' . $class . '"' : '';
$caption_output = $caption ? '<figcaption>' . $caption . '</figcaption>' : '';
return '<figure ' . $id_output . ' ' . $class_output . '>'.$sc->getContent(). $caption_output . '</figure>';
});
}
}
@@ -0,0 +1,53 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Grav\Common\Utils;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class FontAwesomeShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('fa', function(ShortcodeInterface $sc) {
// Load assets if required
if ($this->config->get('plugins.shortcode-core.fontawesome.load', false)) {
$this->shortcode->addAssets('css', $this->config->get('plugins.shortcode-core.fontawesome.url'));
}
if ($this->config->get('plugins.shortcode-core.fontawesome.v5', false)) {
$v5classes = ['fab', 'fal', 'fas', 'far', 'fad'];
} else {
$v5classes = [];
}
// Get shortcode content and parameters
$str = $sc->getContent();
$icon = $sc->getParameter('icon', $sc->getParameter('fa', $this->getBbCode($sc)));
if (!Utils::startsWith($icon, 'fa-')) {
$icon = 'fa-'.$icon;
}
if ($icon) {
$fa_class = 'fa';
$extras = explode(',', $sc->getParameter('extras', ''));
foreach($extras as $extra) {
if(!empty($extra)) {
if(in_array($extra, $v5classes, true)) {
$fa_class = $extra;
continue;
}
if(!Utils::startsWith($extra, 'fa-')) {
$extra = 'fa-' . $extra;
}
$icon .= ' ' . $extra;
}
}
return '<i class="' . $fa_class . ' ' . $icon . '">' . $str . '</i>';
}
return '';
});
}
}
@@ -0,0 +1,48 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class HShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('h1', function(ShortcodeInterface $sc) {
return $this->header(1, $sc);
});
$this->shortcode->getHandlers()->add('h2', function(ShortcodeInterface $sc) {
return $this->header(2, $sc);
});
$this->shortcode->getHandlers()->add('h3', function(ShortcodeInterface $sc) {
return $this->header(3, $sc);
});
$this->shortcode->getHandlers()->add('h4', function(ShortcodeInterface $sc) {
return $this->header(4, $sc);
});
$this->shortcode->getHandlers()->add('h5', function(ShortcodeInterface $sc) {
return $this->header(5, $sc);
});
$this->shortcode->getHandlers()->add('h6', function(ShortcodeInterface $sc) {
return $this->header(6, $sc);
});
}
protected function header($level, ShortcodeInterface $sc)
{
$id = $sc->getParameter('id');
$class = $sc->getParameter('class');
$tag = 'h' . $level;
$id_output = $id ? ' id="' . $id . '" ': '';
$class_output = $class ? ' class="' . $class . '"' : '';
return "<{$tag}{$id_output}{$class_output}>{$sc->getContent()}</{$tag}>";
}
}
@@ -0,0 +1,31 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Grav\Common\Language\Language;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class LanguageShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('lang', function(ShortcodeInterface $sc) {
$lang = $this->getBbCode($sc);
if ($lang) {
$list = explode(',', $lang);
array_walk($list, 'trim');
/** @var Language $language */
$language = $this->grav['language'];
$current = $language->getLanguage();
if (in_array($current, $list)) {
return $sc->getContent();
}
}
return '';
});
}
}
@@ -0,0 +1,360 @@
<?php
/**
* Based on Lorem Ipsum Generator by Josh Sherman
*
* Licensed under The MIT License.
* Redistribution of these files must retain the above copyright notice.
*
* @author Josh Sherman <hello@joshtronic.com>
* @copyright Copyright 2014, 2015, 2016, 2017, 2018, 2019 Josh Sherman
* @license http://www.opensource.org/licenses/mit-license.html
* @link https://github.com/joshtronic/php-loremipsum
*/
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class LoremShortcode extends Shortcode
{
/**
* First
*
* Whether or not we should be starting the string with "Lorem ipsum..."
*
* @access private
* @var boolean
*/
private $first = true;
/**
* Words
*
* A lorem ipsum vocabulary of sorts. Not a complete list as I'm unsure if
* a complete list exists and if so, where to get it.
*
* @access private
* @var array
*/
public $words = [
// Lorem ipsum...
'lorem', 'ipsum', 'dolor', 'sit', 'amet', 'consectetur', 'adipiscing', 'elit',
// and the rest of the vocabulary
'a', 'ac', 'accumsan', 'ad', 'aenean', 'aliquam', 'aliquet', 'ante',
'aptent', 'arcu', 'at', 'auctor', 'augue', 'bibendum', 'blandit',
'class', 'commodo', 'condimentum', 'congue', 'consequat', 'conubia',
'convallis', 'cras', 'cubilia', 'curabitur', 'curae', 'cursus',
'dapibus', 'diam', 'dictum', 'dictumst', 'dignissim', 'dis', 'donec',
'dui', 'duis', 'efficitur', 'egestas', 'eget', 'eleifend', 'elementum',
'enim', 'erat', 'eros', 'est', 'et', 'etiam', 'eu', 'euismod', 'ex',
'facilisi', 'facilisis', 'fames', 'faucibus', 'felis', 'fermentum',
'feugiat', 'finibus', 'fringilla', 'fusce', 'gravida', 'habitant',
'habitasse', 'hac', 'hendrerit', 'himenaeos', 'iaculis', 'id',
'imperdiet', 'in', 'inceptos', 'integer', 'interdum', 'justo',
'lacinia', 'lacus', 'laoreet', 'lectus', 'leo', 'libero', 'ligula',
'litora', 'lobortis', 'luctus', 'maecenas', 'magna', 'magnis',
'malesuada', 'massa', 'mattis', 'mauris', 'maximus', 'metus', 'mi',
'molestie', 'mollis', 'montes', 'morbi', 'mus', 'nam', 'nascetur',
'natoque', 'nec', 'neque', 'netus', 'nibh', 'nisi', 'nisl', 'non',
'nostra', 'nulla', 'nullam', 'nunc', 'odio', 'orci', 'ornare',
'parturient', 'pellentesque', 'penatibus', 'per', 'pharetra',
'phasellus', 'placerat', 'platea', 'porta', 'porttitor', 'posuere',
'potenti', 'praesent', 'pretium', 'primis', 'proin', 'pulvinar',
'purus', 'quam', 'quis', 'quisque', 'rhoncus', 'ridiculus', 'risus',
'rutrum', 'sagittis', 'sapien', 'scelerisque', 'sed', 'sem', 'semper',
'senectus', 'sociosqu', 'sodales', 'sollicitudin', 'suscipit',
'suspendisse', 'taciti', 'tellus', 'tempor', 'tempus', 'tincidunt',
'torquent', 'tortor', 'tristique', 'turpis', 'ullamcorper', 'ultrices',
'ultricies', 'urna', 'ut', 'varius', 'vehicula', 'vel', 'velit',
'venenatis', 'vestibulum', 'vitae', 'vivamus', 'viverra', 'volutpat',
'vulputate',
];
public function init()
{
$this->shortcode->getHandlers()->add('lorem', function(ShortcodeInterface $sc) {
$paragraphs = $sc->getParameter('p', $this->getBbCode($sc));
$paragraph_tag = $sc->getParameter('tag', 'p');
$sentences = $sc->getParameter('s');
$words = $sc->getParameter('w');
if ($words) {
return $this->words($words);
}
if ($sentences) {
return $this->sentences($sentences);
}
return $this->paragraphs($paragraphs ?? 1, $paragraph_tag);
});
}
/**
* Word
*
* Generates a single word of lorem ipsum.
*
* @access public
* @param mixed $tags string or array of HTML tags to wrap output with
* @return string generated lorem ipsum word
*/
public function word($tags = false)
{
return $this->words(1, $tags);
}
/**
* Words Array
*
* Generates an array of lorem ipsum words.
*
* @access public
* @param integer $count how many words to generate
* @param mixed $tags string or array of HTML tags to wrap output with
* @return array generated lorem ipsum words
*/
public function wordsArray($count = 1, $tags = false)
{
return $this->words($count, $tags, true);
}
/**
* Words
*
* Generates words of lorem ipsum.
*
* @access public
* @param integer $count how many words to generate
* @param mixed $tags string or array of HTML tags to wrap output with
* @param boolean $array whether an array or a string should be returned
* @return mixed string or array of generated lorem ipsum words
*/
public function words($count = 1, $tags = false, $array = false)
{
$words = [];
$word_count = 0;
// Shuffles and appends the word list to compensate for count
// arguments that exceed the size of our vocabulary list
while ($word_count < $count) {
$shuffle = true;
while ($shuffle) {
$this->shuffle();
// Checks that the last word of the list and the first word of
// the list that's about to be appended are not the same
if (!$word_count || $words[$word_count - 1] != $this->words[0]) {
$words = array_merge($words, $this->words);
$word_count = count($words);
$shuffle = false;
}
}
}
$words = array_slice($words, 0, (int) $count);
return $this->output($words, $tags, $array);
}
/**
* Sentence
*
* Generates a full sentence of lorem ipsum.
*
* @access public
* @param mixed $tags string or array of HTML tags to wrap output with
* @return string generated lorem ipsum sentence
*/
public function sentence($tags = false)
{
return $this->sentences(1, $tags);
}
/**
* Sentences Array
*
* Generates an array of lorem ipsum sentences.
*
* @access public
* @param integer $count how many sentences to generate
* @param mixed $tags string or array of HTML tags to wrap output with
* @return array generated lorem ipsum sentences
*/
public function sentencesArray($count = 1, $tags = false)
{
return $this->sentences($count, $tags, true);
}
/**
* Sentences
*
* Generates sentences of lorem ipsum.
*
* @access public
* @param integer $count how many sentences to generate
* @param mixed $tags string or array of HTML tags to wrap output with
* @param boolean $array whether an array or a string should be returned
* @return mixed string or array of generated lorem ipsum sentences
*/
public function sentences($count = 1, $tags = false, $array = false)
{
$sentences = [];
for ($i = 0; $i < $count; $i++) {
$sentences[] = $this->wordsArray($this->gauss(24.46, 5.08));
}
$this->punctuate($sentences);
return $this->output($sentences, $tags, $array);
}
/**
* Paragraph
*
* Generates a full paragraph of lorem ipsum.
*
* @access public
* @param mixed $tags string or array of HTML tags to wrap output with
* @return string generated lorem ipsum paragraph
*/
public function paragraph($tags = false)
{
return $this->paragraphs(1, $tags);
}
/**
* Paragraph Array
*
* Generates an array of lorem ipsum paragraphs.
*
* @access public
* @param integer $count how many paragraphs to generate
* @param mixed $tags string or array of HTML tags to wrap output with
* @return array generated lorem ipsum paragraphs
*/
public function paragraphsArray($count = 1, $tags = false)
{
return $this->paragraphs($count, $tags, true);
}
/**
* Paragraphss
*
* Generates paragraphs of lorem ipsum.
*
* @access public
* @param integer $count how many paragraphs to generate
* @param mixed $tags string or array of HTML tags to wrap output with
* @param boolean $array whether an array or a string should be returned
* @return mixed string or array of generated lorem ipsum paragraphs
*/
public function paragraphs($count = 1, $tags = false, $array = false)
{
$paragraphs = [];
for ($i = 0; $i < $count; $i++) {
$paragraphs[] = $this->sentences($this->gauss(5.8, 1.93));
}
return $this->output($paragraphs, $tags, $array, "\n\n");
}
/**
* Gaussian Distribution
*
* This is some smart kid stuff. I went ahead and combined the N(0,1) logic
* with the N(m,s) logic into this single function. Used to calculate the
* number of words in a sentence, the number of sentences in a paragraph
* and the distribution of commas in a sentence.
*
* @access private
* @param double $mean average value
* @param double $std_dev stadnard deviation
* @return double calculated distribution
*/
private function gauss($mean, $std_dev)
{
$x = mt_rand() / mt_getrandmax();
$y = mt_rand() / mt_getrandmax();
$z = sqrt(-2 * log($x)) * cos(2 * pi() * $y);
return $z * $std_dev + $mean;
}
/**
* Shuffle
*
* Shuffles the words, forcing "Lorem ipsum..." at the beginning if it is
* the first time we are generating the text.
*
* @access private
*/
private function shuffle()
{
if ($this->first) {
$this->first = array_slice($this->words, 0, 8);
$this->words = array_slice($this->words, 8);
shuffle($this->words);
$this->words = $this->first + $this->words;
$this->first = false;
} else {
shuffle($this->words);
}
}
/**
* Punctuate
*
* Applies punctuation to a sentence. This includes a period at the end,
* the injection of commas as well as capitalizing the first letter of the
* first word of the sentence.
*
* @access private
* @param array $sentences the sentences we would like to punctuate
*/
private function punctuate(&$sentences)
{
foreach ($sentences as $key => $sentence) {
$words = count($sentence);
// Only worry about commas on sentences longer than 4 words
if ($words > 4) {
$mean = log($words, 6);
$std_dev = $mean / 6;
$commas = round($this->gauss($mean, $std_dev));
for ($i = 1; $i <= $commas; $i++) {
$word = round($i * $words / ($commas + 1));
if ($word < ($words - 1) && $word > 0) {
$sentence[$word] .= ',';
}
}
}
$sentences[$key] = ucfirst(implode(' ', $sentence) . '.');
}
}
/**
* Output
*
* Does the rest of the processing of the strings. This includes wrapping
* the strings in HTML tags, handling transformations with the ability of
* back referencing and determining if the passed array should be converted
* into a string or not.
*
* @access private
* @param string|string[] $strings an array of generated strings
* @param mixed $tags string or array of HTML tags to wrap output with
* @param boolean $array whether an array or a string should be returned
* @param string $delimiter the string to use when calling implode()
* @return string|string[] string or array of generated lorem ipsum text
*/
private function output($strings, $tags, $array, $delimiter = ' ')
{
if ($tags) {
if (!is_array($tags)) {
$tags = [$tags];
} else {
// Flips the array so we can work from the inside out
$tags = array_reverse($tags);
}
foreach ($strings as $key => $string) {
foreach ($tags as $tag) {
// Detects / applies back reference
if ($tag[0] === '<') {
$string = str_replace('$1', $string, $tag);
} else {
$string = sprintf('<%1$s>%2$s</%1$s>', $tag, $string);
}
$strings[$key] = $string;
}
}
}
if (!$array) {
$strings = implode($delimiter, $strings);
}
return $strings;
}
}
@@ -0,0 +1,27 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class MarkShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('mark', function(ShortcodeInterface $sc) {
$style = $sc->getParameter('style', $this->getBbCode($sc));
$class = $sc->getParameter('class', 'default');
$css_class = 'class="mark-class-' . $class . '"';
if ($style === 'block') {
$css_style = 'style="display:block;"';
$content = trim($sc->getContent(), "\n");
} else {
$css_style = '';
$content = $sc->getContent();
}
return "<mark {$css_class} {$css_style}>{$content}</mark>";
});
}
}
@@ -0,0 +1,24 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class NoticeShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('notice', function(ShortcodeInterface $sc) {
$css_enabled = $this->grav['config']->get('plugins.shortcode-core.css.notice_enabled', true);
if ($css_enabled) {
$this->shortcode->addAssets('css', 'plugin://shortcode-core/css/shortcode-notice.css');
}
$output = $this->twig->processTemplate('shortcodes/notice.html.twig', [
'type' => $sc->getParameter('notice', $this->getBbCode($sc)) ?: 'info',
'content' => $sc->getContent(),
]);
return $output;
});
}
}
@@ -0,0 +1,18 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\EventHandler\FilterRawEventHandler;
use Thunder\Shortcode\Events;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class RawShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('raw', static function(ShortcodeInterface $sc) {
return trim($sc->getContent());
});
$this->shortcode->getEvents()->addListener(Events::FILTER_SHORTCODES, new FilterRawEventHandler(['raw']));
}
}
@@ -0,0 +1,66 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
/**
* `[read-file file="theme://foo.md" /]` — include the contents of a file
* located by a Grav stream URI into the page.
*
* Delegates to `\Grav\Common\Helpers\FileReader::read()` so it inherits the
* same hardening as the Twig `read_file()` function: stream-only paths,
* `security.read_file.allowed_streams` allow-list, extension allow-list,
* canonical realpath containment, max size cap. Anything outside those
* constraints (raw filesystem paths, traversal, encoded `..`, disallowed
* extensions, oversize files, missing files) returns the empty string and
* the shortcode renders as nothing — just like the Twig function returns
* `false`, which Twig prints as empty.
*
* Registered as a *raw* handler, so it runs before Markdown — included
* Markdown files become part of the page's Markdown source and get rendered
* normally. HTML / SVG / JSON files come through verbatim.
*
* Grav 1.7 fallback: `FileReader` was introduced in Grav 2.0.0-rc.2. On Grav
* 1.7 the helper isn't available, so the shortcode emits an HTML comment
* pointing at the version requirement. The comment isn't visible to readers
* but is easy for an author or theme dev to find when viewing source — much
* better than the alternative (no handler registered, so `[read-file ...]`
* leaks as literal text into the rendered page). On 1.7 sites that need
* file inclusion from page content, use the legacy Twig `read_file()`
* function instead.
*/
class ReadFileShortcode extends Shortcode
{
public function init()
{
// Always register the handler — even on Grav 1.7 — so a stray
// `[read-file ...]` in content doesn't leak as literal text.
// `FileReader` ships with Grav 2.0.0-rc.2+; older Gravs get a
// diagnostic HTML comment.
$hasFileReader = class_exists(\Grav\Common\Helpers\FileReader::class);
// Closure intentionally non-static — `$this->getBbCode($sc)` provides
// the `[read-file=theme://foo.md /]` BBCode-style shorthand alongside
// the `[read-file file="theme://foo.md" /]` named-attribute form.
$handler = function (ShortcodeInterface $sc) use ($hasFileReader) {
if (!$hasFileReader) {
return '<!-- [read-file] requires Grav >= 2.0.0-rc.2 -->';
}
$file = $sc->getParameter('file', $this->getBbCode($sc));
if (!is_string($file) || $file === '') {
return '';
}
$contents = \Grav\Common\Helpers\FileReader::read($file);
return $contents === false ? '' : $contents;
};
// Register on the raw-handler container so the shortcode runs in the
// pre-Markdown pass. Included `.md` files flow into the page's
// Markdown source and render normally; HTML / SVG / JSON pass
// through to the post-Markdown HTML untouched.
$this->shortcode->getRawHandlers()->add('read-file', $handler);
}
}
@@ -0,0 +1,73 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class SafeEmailShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('safe-email', function(ShortcodeInterface $sc) {
// Load assets if required
if ($this->config->get('plugins.shortcode-core.fontawesome.load', false)) {
$this->shortcode->addAssets('css', $this->config->get('plugins.shortcode-core.fontawesome.url'));
}
// Get shortcode content and parameters
$addr_str = $sc->getContent();
$icon = $sc->getParameter('icon', false);
$icon_base = "fa fa-";
$autolink = $sc->getParameter('autolink', false);
$subject = $sc->getParameter('subject', false);
// Add subject, if any, to the link target.
$link_str = $addr_str;
if ($subject) {
$subject = html_entity_decode($subject);
$link_str .= '?subject=' . rawurlencode($subject);
}
// Encode display text and link target
$email_disp = static::encodeText($addr_str);
$email_link = static::encodeText($link_str);
// Handle autolinking
if ($autolink) {
$output = '<a href="mailto:' . $email_link . '">' . $email_disp . '</a>';
} else {
$output = $email_disp;
}
// Handle icon option
if ($icon) {
if ($this->config->get('plugins.shortcode-core.fontawesome.v5', false)) {
if (preg_match("/^(?P<weight>fa[srlbd]) fa-(?<icon>.+)/", $icon, $icon_parts)) {
$icon_base = $icon_parts["weight"] . " fa-";
$icon = $icon_parts["icon"];
}
}
$output = '<i class="'. $icon_base . $icon . '"></i> ' . $output;
}
return $output;
});
}
/**
* encodes text as numeric HTML entities
* @param string $text the text to encode
* @return string the encoded text
*/
private static function encodeText($text)
{
$encoded = '';
$str_len = strlen($text);
for ($i = 0; $i < $str_len; $i++) {
$encoded .= '&#' . ord($text[$i]). ';';
}
return $encoded;
}
}
@@ -0,0 +1,28 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class SectionShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('section', function(ShortcodeInterface $sc) {
$name = $sc->getParameter('name');
$page = $sc->getParameter('page');
$content = $sc->getContent();
if (empty($content) && isset($page)) {
if ($target = $this->grav['pages']->find($page)) {
if ($shortcodeObject = $target->contentMeta()['shortcodeMeta']['shortcode'][$sc->getName()][$name] ?? false) {
return (string) $shortcodeObject;
}
}
}
$object = new \Grav\Plugin\ShortcodeCore\ShortcodeObject($name, $sc->getContent());
$this->shortcode->addObject($sc->getName(), $object);
});
}
}
@@ -0,0 +1,91 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Grav\Common\Config\Config;
use Grav\Common\Grav;
use Grav\Common\Twig\Twig;
use Grav\Plugin\ShortcodeCore\ShortcodeManager;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
abstract class Shortcode
{
/** @var ShortcodeManager */
protected $shortcode;
/** @var Grav */
protected $grav;
/** @var Config */
protected $config;
/** @var Twig */
protected $twig;
/**
* Shortcode constructor.
*/
public function __construct()
{
$this->grav = Grav::instance();
$this->shortcode = $this->grav['shortcode'];
$this->config = $this->grav['config'];
$this->twig = $this->grav['twig'];
}
/**
* Initialize shortcode handler
*/
public function init()
{
user_error(__METHOD__ . '() method will be abstract in the future, please override it!', E_USER_DEPRECATED);
// FIXME: This code had to be put back because of some plugins do not properly initialize themselves.
$this->shortcode->getHandlers()->add('u', static function(ShortcodeInterface $shortcode) {
return $shortcode->getContent();
});
}
/**
* Returns the name of the class if required
*
* @return string the name of the class
*/
public function getName()
{
return get_class($this);
}
/**
* @return string
*/
public function getParser()
{
return $this->config->get('plugins.shortcode-core.parser');
}
/**
* @param ShortcodeInterface $sc
* @param string|null $default
* @return string|null
*/
public function getBbCode(ShortcodeInterface $sc, $default = null)
{
$code = $default;
if ($this->getParser() === 'wordpress') {
$params = $sc->getParameters();
if (is_array($params)) {
$keys = array_keys($params);
$code = trim(array_shift($keys), '=');
}
} else {
$code = $sc->getBbCode();
}
return $code;
}
}
// Make sure we also autoload the deprecated class.
class_exists(\Grav\Plugin\ShortcodeCore\Shortcode::class);
@@ -0,0 +1,24 @@
<?php
namespace Grav\Plugin\Shortcodes;
// Check if the new class has been autoloaded. If not, trigger deprecation error.
if (!class_exists(\Grav\Plugin\ShortcodeCore\ShortcodeObject::class, false)) {
@trigger_error(
ShortcodeObject::class . ' class is deprecated, use \\Grav\\Plugin\\ShortcodeCore\\ShortcodeObject instead',
E_USER_DEPRECATED
);
}
// Create alias for the deprecated class.
class_alias(\Grav\Plugin\ShortcodeCore\ShortcodeObject::class, ShortcodeObject::class);
// Make sure that both IDE and composer knows about the deprecated class.
if (false) {
/**
* @deprecated 4.2.0 Use \Grav\Plugin\ShortcodeCore\ShortcodeObject instead
*/
class ShortcodeObject extends \Grav\Plugin\ShortcodeCore\ShortcodeObject
{
}
}
@@ -0,0 +1,19 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class SizeShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('size', function(ShortcodeInterface $sc) {
$size = $sc->getParameter('size', $this->getBbCode($sc));
if (is_numeric($size)) {
$size .= 'px';
}
return '<span style="font-size: ' . $size . ';">' . $sc->getContent() . '</span>';
});
}
}
@@ -0,0 +1,22 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class SpanShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('span', static function(ShortcodeInterface $sc) {
$id = $sc->getParameter('id');
$class = $sc->getParameter('class');
$style = $sc->getParameter('style');
$id_output = $id ? 'id="' . $id . '" ': '';
$class_output = $class ? 'class="' . $class . '"' : '';
$style_output = $style ? 'style="' . $style . '"' : '';
return '<span ' . $id_output . ' ' . $class_output . ' ' . $style_output . '>' . $sc->getContent() . '</span>';
});
}
}
@@ -0,0 +1,52 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Grav\Common\Language\Language;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
/**
* Translate a language string by key, the safe in-content replacement for
* `{{ 'SOME.KEY'|t }}`. The key is author-supplied; the value comes from the
* site's language files, which are trusted (authored by developers and
* translators), so the result is returned as-is — translations may legitimately
* contain HTML. Untrusted, user-controlled values belong in [uri] instead,
* which escapes its output.
*
* Usage:
* [translate]PLUGIN_ERROR.ERROR_MESSAGE[/translate]
* [translate=PLUGIN_ERROR.ERROR_MESSAGE /]
* [translate key="MY.GREETING" Andy /] (extra params become substitutions)
*/
class TranslateShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('translate', function (ShortcodeInterface $sc) {
// Key from [translate=KEY], the `key` param, or the wrapped content.
$key = $sc->getParameter('key', $this->getBbCode($sc));
if ($key === null || $key === '') {
$key = $sc->getContent();
}
$key = trim((string) $key);
if ($key === '') {
return '';
}
// Any remaining bare parameters are passed through as ordered
// substitution arguments (sprintf-style placeholders in the string).
$args = [];
foreach ($sc->getParameters() as $name => $value) {
if ($name === 'key' || $name === $key) {
continue;
}
$args[] = $value === null ? $name : $value;
}
/** @var Language $language */
$language = $this->grav['language'];
return $language->translate(array_merge([$key], $args));
});
}
}
@@ -0,0 +1,14 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
class UnderlineShortcode extends Shortcode
{
public function init()
{
$this->shortcode->getHandlers()->add('u', static function(ShortcodeInterface $sc) {
return '<span style="text-decoration: underline;">' . $sc->getContent() . '</span>';
});
}
}
@@ -0,0 +1,48 @@
<?php
namespace Grav\Plugin\Shortcodes;
use Grav\Common\Uri;
use Thunder\Shortcode\Shortcode\ShortcodeInterface;
/**
* Print a value from the current URL, the safe in-content replacement for
* `{{ uri.param('foo') }}` and friends. The value is user-controlled (it comes
* straight from the request), so it is ALWAYS HTML-escaped on output — that
* filtering is the whole point of using the shortcode instead of raw Twig in
* content. There is deliberately no raw/unescaped opt-out.
*
* Usage:
* [uri param="user" /] {{ uri.param('user') }}
* [uri query="q" /] {{ uri.query('q') }}
* [uri param="ref" default="home" /]
*/
class UriShortcode extends Shortcode
{
/** Sources we expose, mapped to the Uri accessor that reads them. */
private const SOURCES = ['param', 'query'];
public function init()
{
$this->shortcode->getHandlers()->add('uri', function (ShortcodeInterface $sc) {
/** @var Uri $uri */
$uri = $this->grav['uri'];
$value = null;
foreach (self::SOURCES as $source) {
$name = $sc->getParameter($source);
if ($name !== null && $name !== '') {
// Uri::param() / Uri::query() — read the named value.
$value = $uri->{$source}((string) $name);
break;
}
}
if ($value === null || $value === false || $value === '') {
$value = $sc->getParameter('default', '');
}
// User-controlled input: escape unconditionally.
return htmlspecialchars((string) $value, ENT_QUOTES, 'UTF-8');
});
}
}