<?php
namespace Mos\TextFilter;
/**
* Filter and format content.
*
*/
class CTextFilter
{
/**
* Supported filters.
*/
private $filters = [
"jsonfrontmatter",
"yamlfrontmatter",
"bbcode",
"clickable",
"shortcode",
"markdown",
"nl2br",
"purify",
];
/**
* Current document parsed.
*/
private $current;
/**
* Call each filter.
*
* @deprecated deprecated since version 1.2 in favour of parse().
*
* @param string $text the text to filter.
* @param string|array $filters as comma separated list of filter,
* or filters sent in as array.
*
* @return string the formatted text.
*/
public function doFilter($text, $filters)
{
// Define all valid filters with their callback function.
$callbacks = [
'bbcode' => 'bbcode2html',
'clickable' => 'makeClickable',
'shortcode' => 'shortCode',
'markdown' => 'markdown',
'nl2br' => 'nl2br',
'purify' => 'purify',
];
// Make an array of the comma separated string $filters
if (is_array($filters)) {
$filter = $filters;
} else {
$filters = strtolower($filters);
$filter = preg_replace('/\s/', '', explode(',', $filters));
}
// For each filter, call its function with the $text as parameter.
foreach ($filter as $key) {
if (!isset($callbacks[$key])) {
throw new Exception("The filter '$filters' is not a valid filter string due to '$key'.");
}
$text = call_user_func_array([$this, $callbacks[$key]], [$text]);
}
return $text;
}
/**
* Return an array of all filters supported.
*
* @return array with strings of filters supported.
*/
public function getFilters()
{
return $this->filters;
}
/**
* Check if filter is supported.
*
* @param string $filter to use.
*
* @throws mos/TextFilter/Exception when filter does not exists.
*
* @return boolean true if filter exists, false othwerwise.
*/
public function hasFilter($filter)
{
return in_array($filter, $this->filters);
}
/**
* Call a specific filter and store its details.
*
* @param string $filter to use.
*
* @throws mos/TextFilter/Exception when filter does not exists.
*
* @return string the formatted text.
*/
private function parseFactory($filter)
{
// Define single tasks filter with a callback.
$callbacks = [
"bbcode" => "bbcode2html",
"clickable" => "makeClickable",
"shortcode" => "shortCode",
"markdown" => "markdown",
"nl2br" => "nl2br",
"purify" => "purify",
];
// Do the specific filter
$text = $this->current->text;
switch ($filter) {
case "jsonfrontmatter":
$res = $this->jsonFrontMatter($text);
$this->current->text = $res["text"];
$this->current->frontmatter = $res["frontmatter"];
break;
case "yamlfrontmatter":
$res = $this->yamlFrontMatter($text);
$this->current->text = $res["text"];
$this->current->frontmatter = $res["frontmatter"];
break;
case "bbcode":
case "clickable":
case "shortcode":
case "markdown":
case "nl2br":
case "purify":
$this->current->text = call_user_func_array(
[$this, $callbacks[$filter]],
[$text]
);
break;
default:
throw new Exception("The filter '$filter' is not a valid filter string.");
}
}
/**
* Call each filter and return array with details of the formatted content.
*
* @param string $text the text to filter.
* @param array $filter array of filters to use.
*
* @throws mos/TextFilter/Exception when filterd does not exists.
*
* @return array with the formatted text and additional details.
*/
public function parse($text, $filter)
{
$this->current = new \stdClass();
$this->current->frontmatter = null;
$this->current->text = $text;
foreach ($filter as $key) {
$this->parseFactory($key);
}
return $this->current;
}
/**
* Extract front matter from text.
*
* @param string $text the text to be parsed.
* @param string $startToken the start token.
* @param string $stopToken the stop token.
*
* @return array with the formatted text and the front matter.
*/
private function extractFrontMatter($text, $startToken, $stopToken)
{
$tokenLength = strlen($startToken);
$start = strpos($text, $startToken);
$frontmatter = null;
if ($start !== false) {
$stop = strpos($text, $stopToken, $tokenLength - 1);
if ($stop !== false) {
$length = $stop - ($start + $tokenLength);
$frontmatter = substr($text, $start + $tokenLength, $length);
$textStart = substr($text, 0, $start);
$text = $textStart . substr($text, $stop + $tokenLength);
}
}
return [$text, $frontmatter
];
}
/**
* Extract JSON front matter from text.
*
* @param string $text the text to be parsed.
*
* @return array with the formatted text and the front matter.
*/
public function jsonFrontMatter($text)
{
list($text, $frontmatter) = $this->extractFrontMatter($text, "{{{\n", "}}}\n");
if (!empty($frontmatter)) {
$frontmatter = json_decode($frontmatter, true);
if (is_null($frontmatter)) {
throw new Exception("Failed parsing JSON frontmatter.");
}
}
return [
"text" => $text,
"frontmatter" => $frontmatter
];
}
/**
* Extract YAML front matter from text.
*
* @param string $text the text to be parsed.
*
* @return array with the formatted text and the front matter.
*/
public function yamlFrontMatter($text)
{
$needle = "---\n";
list($text, $frontmatter) = $this->extractFrontMatter($text, $needle, $needle);
if (function_exists("yaml_parse") && !empty($frontmatter)) {
$frontmatter = yaml_parse($needle . $frontmatter);
if ($frontmatter === false) {
throw new Exception("Failed parsing YAML frontmatter.");
}
}
return [
"text" => $text,
"frontmatter" => $frontmatter
];
}
/**
* Helper, BBCode formatting converting to HTML.
*
* @param string $text The text to be converted.
*
* @return string the formatted text.
*
* @link http://dbwebb.se/coachen/reguljara-uttryck-i-php-ger-bbcode-formattering
*/
public function bbcode2html($text)
{
$search = [
'/\[b\](.*?)\[\/b\]/is',
'/\[i\](.*?)\[\/i\]/is',
'/\[u\](.*?)\[\/u\]/is',
'/\[img\](https?.*?)\[\/img\]/is',
'/\[url\](https?.*?)\[\/url\]/is',
'/\[url=(https?.*?)\](.*?)\[\/url\]/is'
];
$replace = [
'<strong>$1</strong>',
'<em>$1</em>',
'<u>$1</u>',
'<img src="$1" />',
'<a href="$1">$1</a>',
'<a href="$1">$2</a>'
];
return preg_replace($search, $replace, $text);
}
/**
* Make clickable links from URLs in text.
*
* @param string $text the text that should be formatted.
*
* @return string with formatted anchors.
*
* @link http://dbwebb.se/coachen/lat-php-funktion-make-clickable-automatiskt-skapa-klickbara-lankar
*/
public function makeClickable($text)
{
return preg_replace_callback(
'#\b(?<![href|src]=[\'"])https?://[^\s()<>]+(?:\([\w\d]+\)|([^[:punct:]\s]|/))#',
function ($matches) {
return "<a href='{$matches[0]}'>{$matches[0]}</a>";
},
$text
);
}
/**
* Format text according to HTML Purifier.
*
* @param string $text that should be formatted.
*
* @return string as the formatted html-text.
*/
public function purify($text)
{
$config = \HTMLPurifier_Config::createDefault();
$config->set("Cache.DefinitionImpl", null);
//$config->set('Cache.SerializerPath', '/home/user/absolute/path');
$purifier = new \HTMLPurifier($config);
return $purifier->purify($text);
}
/**
* Format text according to Markdown syntax.
*
* @param string $text the text that should be formatted.
*
* @return string as the formatted html-text.
*/
public function markdown($text)
{
return \Michelf\MarkdownExtra::defaultTransform($text);
}
/**
* For convenience access to nl2br
*
* @param string $text text to be converted.
*
* @return string the formatted text.
*/
public function nl2br($text)
{
return nl2br($text);
}
/**
* Shortcode to to quicker format text as HTML.
*
* @param string $text text to be converted.
*
* @return string the formatted text.
*/
public function shortCode($text)
{
$patterns = [
'/\[(FIGURE)[\s+](.+)\]/',
];
return preg_replace_callback(
$patterns,
function ($matches) {
switch ($matches[1]) {
case 'FIGURE':
return self::ShortCodeFigure($matches[2]);
break;
default:
return "{$matches[1]} is unknown shortcode.";
}
},
$text
);
}
/**
* Init shortcode handling by preparing the option list to an array, for those using arguments.
*
* @param string $options for the shortcode.
*
* @return array with all the options.
*/
public static function shortCodeInit($options)
{
preg_match_all('/[a-zA-Z0-9]+="[^"]+"|\S+/', $options, $matches);
$res = array();
foreach ($matches[0] as $match) {
$pos = strpos($match, '=');
if ($pos === false) {
$res[$match] = true;
} else {
$key = substr($match, 0, $pos);
$val = trim(substr($match, $pos+1), '"');
$res[$key] = $val;
}
}
return $res;
}
/**
* Shortcode for <figure>.
*
* Usage example: [FIGURE src="img/home/me.jpg" caption="Me" alt="Bild på mig" nolink="nolink"]
*
* @param string $options for the shortcode.
*
* @return array with all the options.
*/
public static function shortCodeFigure($options)
{
// Merge incoming options with default and expose as variables
$options= array_merge(
[
'id' => null,
'class' => null,
'src' => null,
'title' => null,
'alt' => null,
'caption' => null,
'href' => null,
'nolink' => false,
],
self::ShortCodeInit($options)
);
extract($options, EXTR_SKIP);
$id = $id ? " id='$id'" : null;
$class = $class ? " class='figure $class'" : " class='figure'";
$title = $title ? " title='$title'" : null;
if (!$alt && $caption) {
$alt = $caption;
}
if (!$href) {
$pos = strpos($src, '?');
$href = $pos ? substr($src, 0, $pos) : $src;
}
$start = null;
$end = null;
if (!$nolink) {
$start = "<a href='{$href}'>";
$end = "</a>";
}
$html = <<<EOD
<figure{$id}{$class}>
{$start}<img src='{$src}' alt='{$alt}'{$title}/>{$end}
<figcaption markdown=1>{$caption}</figcaption>
</figure>
EOD;
return $html;
}
}