<?php
|
|
namespace SparkLib;
|
|
|
|
use \MarkdownDocument;
|
|
|
|
/**
|
|
* Wrap up all our usage of Markdown libraries.
|
|
*
|
|
* Basic usage is like so:
|
|
*
|
|
* echo Sparkdown::create($source)
|
|
* ->allowHtmlTags() // optional - probably in-house only - let HTML through
|
|
* ->withCallbacks() // optional - in-house only for now - install callbacks for link shorthand
|
|
* ->withMacros() // optional - implies allowHtmlTags() - add a filter to do <!-- foo(bar) --> macros
|
|
*
|
|
* // optional - adds an additional namespace besides SparkLib\SparkdownMacro to search for macro classes:
|
|
* ->addMacroNamespace('\\SomeNameSpace\\SparkdownMacro')
|
|
*
|
|
* ->getHtml()
|
|
*
|
|
* If you have something like a model object and you're going to want
|
|
* to render it with the same options in more than one place, you may
|
|
* want to write a getHtml() wrapper method on that object instead of
|
|
* repeating the Sparkdown boilerplate.
|
|
*/
|
|
class Sparkdown {
|
|
|
|
protected $_md;
|
|
|
|
protected $_allowHTML = false;
|
|
protected $_macros = false;
|
|
protected $_macroNamespaces = ['\\SparkLib\\SparkdownMacro'];
|
|
protected $_macroContext = [];
|
|
|
|
/**
|
|
* Create a new Sparkdown document from a given source string.
|
|
*/
|
|
public static function create ($source)
|
|
{
|
|
return new static($source);
|
|
}
|
|
|
|
/**
|
|
* Transform a fragment (not necessarily a complete document) of Markdown.
|
|
*
|
|
* Useful for displaying truncated bits of full documents.
|
|
*/
|
|
public static function transformFragment ($source)
|
|
{
|
|
return MarkdownDocument::transformFragment($source, MarkdownDocument::NOHTML);
|
|
}
|
|
|
|
public function __construct ($source)
|
|
{
|
|
$this->_md = MarkdownDocument::createFromString($source);
|
|
}
|
|
|
|
/**
|
|
* Toggle pass-through on HTML tags. This is turned off by default
|
|
* so that no one will use it on user-supplied data and accidentally
|
|
* allow arbitrary HTML from the outside world.
|
|
*
|
|
* @return Sparkdown
|
|
*/
|
|
public function allowHtmlTags ($allow = true)
|
|
{
|
|
if (! is_bool($allow)) {
|
|
throw new Exception('allowHtmlTags() expects a boolean true/false');
|
|
}
|
|
|
|
if ((! $allow) && $this->_macros)
|
|
throw new Exception('withMacros() requires that HTML tags are allowed');
|
|
|
|
$this->_allowHTML = $allow;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Install some callbacks to override link stuff.
|
|
*
|
|
* Right now, these are only for inhouse users. We can expand
|
|
* on this for different sets of users etc.
|
|
*
|
|
* @return Sparkdown
|
|
*/
|
|
public function withCallbacks ()
|
|
{
|
|
$this->_md->setUrlCallback(function ($path) {
|
|
return $this->urlCallback($path);
|
|
});
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set a callback for nofollow attributes on links
|
|
*
|
|
* @return Sparkdown
|
|
*/
|
|
public function setNoFollow ()
|
|
{
|
|
$this->_md->setAttributesCallback(function () {
|
|
return $this->attributesCallback(['rel' => 'nofollow']);
|
|
});
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Return a path for special shorthand in links.
|
|
*
|
|
* New link shorthand should be defined here, following the pattern
|
|
* currently used for tutorials.
|
|
*
|
|
* A hook for this can be installed by withCallbacks().
|
|
*/
|
|
protected function urlCallback ($path)
|
|
{
|
|
$m = [];
|
|
if (preg_match('/^tutorials?\/([0-9]+)$/', $path, $m)) {
|
|
if ($t = \LearnTutorialSaurus::getById($m[1])) {
|
|
return \Learn::externalLink('tutorials')->id($t->url_path);
|
|
}
|
|
}
|
|
|
|
// we didn't find anything
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Return a rel=nofollow for links
|
|
*
|
|
* An example hook for this can be seen at setNoFollow().
|
|
*/
|
|
protected function attributesCallback ($attributes)
|
|
{
|
|
if (is_array($attributes)) {
|
|
$attr_str = '';
|
|
foreach($attributes as $k => $v) {
|
|
$attr_str .= $k . '="' . $v . '" ';
|
|
}
|
|
return $attr_str;
|
|
} else if (is_string($attributes)) {
|
|
return $attributes;
|
|
}
|
|
|
|
// unknown attribute type
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* Toggle processing macros of the form <!-- macro(param string) -->
|
|
*
|
|
* Implies allowHtmlTags().
|
|
*
|
|
* @return Sparkdown
|
|
*/
|
|
public function withMacros ()
|
|
{
|
|
// necessary for comments to pass through:
|
|
$this->allowHtmlTags();
|
|
$this->_macros = true;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Add a namespace to search for macro classes.
|
|
*
|
|
* @return Sparkdown
|
|
*/
|
|
public function addMacroNamespace ($namespace)
|
|
{
|
|
array_unshift($this->_macroNamespaces, $namespace);
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Set a context array to be passed to macros.
|
|
*/
|
|
public function setMacroContext (array $context)
|
|
{
|
|
$this->_macroContext = $context;
|
|
return $this;
|
|
}
|
|
|
|
/**
|
|
* Get the current macro context array.
|
|
*/
|
|
public function getMacroContext ()
|
|
{
|
|
return $this->_macroContext;
|
|
}
|
|
|
|
/**
|
|
* Get the HTML version of the current document.
|
|
*
|
|
* @return string
|
|
*/
|
|
public function getHtml ()
|
|
{
|
|
if ($this->_allowHTML) {
|
|
$this->_md->compile();
|
|
} else {
|
|
$this->_md->compile(MarkdownDocument::NOHTML);
|
|
}
|
|
|
|
if ($this->_macros) {
|
|
return $this->getHtmlWithMacros();
|
|
} else {
|
|
return $this->_md->getHtml();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get the HTML version of the current document, with
|
|
* <!-- module(param string) --> macros processed.
|
|
* Should only be invoked by getHtml() after the document
|
|
* has been compiled.
|
|
*
|
|
* @return string
|
|
*/
|
|
protected function getHtmlWithMacros ()
|
|
{
|
|
$pat = '/
|
|
<!-- [ ] # open comment
|
|
|
|
(?<name> [_a-z]+) # macro name
|
|
|
|
\(
|
|
(?<input> .*?) # params
|
|
\)
|
|
|
|
[ ] --> # close comment
|
|
/x';
|
|
|
|
$transform = function ($matches) {
|
|
foreach ($this->_macroNamespaces as $ns) {
|
|
$class = $ns . '\\' . ucfirst($matches['name']);
|
|
if ($class_exists = class_exists($class))
|
|
break;
|
|
}
|
|
|
|
if (! $class_exists)
|
|
return '';
|
|
|
|
$macro = new $class($matches['input']);
|
|
$macro->setContext($this->_macroContext);
|
|
return $macro->render();
|
|
};
|
|
|
|
// call $transform() on everything matching $pat in the rendered HTML:
|
|
return preg_replace_callback($pat, $transform, $this->_md->getHtml());
|
|
}
|
|
|
|
}
|