s9e/text-formatter
PHP text formatting library with plugin support for BBCode, Markdown, HTML, and more. Includes predefined bundles, extensive documentation, and a JavaScript port for client-side preview and demos. Install via Composer and integrate customizable parsing/rendering.
Installation
composer require s9e/text-formatter
Basic Usage
use s9e\TextFormatter\TextFormatter;
$formatter = new TextFormatter();
$html = $formatter->parse('[b]Hello[/b]!');
// Output: <strong>Hello</strong>!
First Use Case: Comment System
$configurator = new \s9e\TextFormatter\Configurator();
$configurator->BBCodes; // Enable BBCodes by default
$configurator->Autolink; // Auto-convert URLs
$configurator->Smiley; // Enable emoticons
extract($configurator->finalize());
$comment = "[b]Important[/b]! Don't forget to :smile:";
$html = $renderer->render($parser->parse($comment));
Fatdown for Markdown + BBCodes)Censor, UrlConfig, Embed)Use the Configurator to define reusable formatting rules:
// Reusable config for forum posts
$forumConfigurator = new \s9e\TextFormatter\Configurator();
$forumConfigurator->BBCodes;
$forumConfigurator->Autolink;
$forumConfigurator->Smiley;
$forumConfigurator->Embed->setProvider('youtube', 'https://www.youtube.com/embed/{ID}?autoplay=0');
// Apply URL restrictions
$forumConfigurator->urlConfig->restrictHost('trusted-domain.com');
// Store for later use (e.g., in a service container)
app()->singleton('forum.formatter', function () use ($forumConfigurator) {
extract($forumConfigurator->finalize());
return $renderer;
});
Enable plugins conditionally (e.g., based on user roles):
$configurator = new \s9e\TextFormatter\Configurator();
$user = auth()->user();
if ($user->can('use_markdown')) {
$configurator->Fatdown; // Markdown + BBCodes
} else {
$configurator->BBCodes;
}
if ($user->is_admin()) {
$configurator->Embed; // Only admins get embeds
}
Extend with domain-specific BBCode (e.g., for a wiki):
$configurator = new \s9e\TextFormatter\Configurator();
$configurator->BBCodes->addCustom(
'[wiki page={TEXT}]', // Syntax: [wiki page="User_Guide"]
'<a href="/wiki/{@page}" class="wiki-link">{@page}</a>'
);
// Add a filter to sanitize wiki page names
$configurator->tags['wiki']->attributes['page']->filterChain->append('strtolower');
$configurator->tags['wiki']->attributes['page']->filterChain->append('preg_replace("/[^a-z0-9_]/", "_", $attrValue)');
Restrict links/images by context (e.g., blog vs. comments):
// Blog post config (allow external links)
$blogConfigurator = new \s9e\TextFormatter\Configurator();
$blogConfigurator->Autolink;
// Comment config (restrict to whitelisted domains)
$commentConfigurator = new \s9e\TextFormatter\Configurator();
$commentConfigurator->Autolink;
$commentConfigurator->urlConfig->restrictHost('trusted-site.com');
Pair with the JS port for client-side previews:
// In your Blade template
<script src="https://cdn.jsdelivr.net/npm/s9e-textformatter@latest/dist/s9e.textformatter.min.js"></script>
<script>
const formatter = new s9e.TextFormatter();
formatter.bundle('fatdown'); // Load Markdown + BBCodes
document.getElementById('preview').innerHTML = formatter.parse(document.getElementById('input').value);
</script>
Embed if unused).Configurator instances and cache parsed results:
$cacheKey = md5($text . serialize($configurator->getPlugins()));
return Cache::remember($cacheKey, 3600, function () use ($parser, $renderer, $text) {
return $renderer->render($parser->parse($text));
});
$configurator->logger = new \s9e\TextFormatter\Logger\FileLogger('/path/to/logs');
DOMDocument to check parsed XML:
$xml = $parser->parse('[b]Test[/b]');
$dom = new DOMDocument();
$dom->loadXML($xml);
if ($dom->schemaValidate('path/to/schema.xsd')) { /* ... */ }
*) in disallowHost match substrings, not subdomains. Use disallowHost('*.example.com') for subdomains.restrictHost does not validate HTTPS/HTTP schemes by default (configure via allowScheme).class), the last defined wins. Use unique names or filters to resolve conflicts.Embed) may not handle nested BBCode well. Test combinations thoroughly.filterChain->append() to modify attributes/tags dynamically:
// Truncate long URLs in links
$configurator->tags['a']->attributes['href']->filterChain->append(
'strlen($attrValue) > 50 ? substr($attrValue, 0, 47) . "..." : $attrValue'
);
$configurator->tags['alert']->filterChain[] = function ($tag) {
if (!auth()->user()->can('see_alerts')) {
$tag->invalidate();
}
};
Renderer events to modify HTML output:
$renderer->on('render', function ($html) {
return str_replace('<strong>', '<b class="highlight">', $html);
});
echo e($renderer->render($parser->parse($userInput)));
$configurator->urlConfig->restrictHost('cdn.yourdomain.com');
$configurator->urlConfig->disallowScheme('javascript');
HTML or PHP plugins in user-generated content.// app/Providers/AppServiceProvider.php
public function register()
{
$this->app->bind(\s9e\TextFormatter\TextFormatter::class, function () {
$configurator = new \s9e\TextFormatter\Configurator();
$configurator->BBCodes;
$configurator->Autolink;
extract($configurator->finalize());
return new \s9e\TextFormatter\TextFormatter($parser, $renderer);
});
}
// app/Observers/PostObserver.php
public function saving(Post $post)
{
$formatter = app(\s9e\TextFormatter\TextFormatter::class);
$post->content = $formatter->parse($post->content);
}
// app/Http/Requests/StorePostRequest.php
public function rules()
{
return [
'content' => 'required|s9e_textformatter', // Custom rule
];
How can I help you explore Laravel packages today?