Temporary-ish parsing facilities.

This commit is contained in:
flash 2018-05-24 21:31:48 +02:00
parent 60d14f9709
commit 4e2ff47137
28 changed files with 439 additions and 9 deletions

View file

@ -59,6 +59,7 @@ function migrate_up(PDO $conn): void
`user_id` INT(10) UNSIGNED NULL DEFAULT NULL, `user_id` INT(10) UNSIGNED NULL DEFAULT NULL,
`post_ip` BLOB NOT NULL, `post_ip` BLOB NOT NULL,
`post_text` TEXT NOT NULL, `post_text` TEXT NOT NULL,
`post_parse` TINYINT(4) UNSIGNED NOT NULL DEFAULT '0',
`post_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, `post_created` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
`post_edited` TIMESTAMP NULL DEFAULT NULL, `post_edited` TIMESTAMP NULL DEFAULT NULL,
`post_deleted` TIMESTAMP NULL DEFAULT NULL, `post_deleted` TIMESTAMP NULL DEFAULT NULL,

View file

@ -123,7 +123,8 @@ if ($postRequest) {
$forum['forum_id'], $forum['forum_id'],
$app->getUserId(), $app->getUserId(),
IPAddress::remote()->getString(), IPAddress::remote()->getString(),
$postText $postText,
MSZ_FORUM_POST_PARSER_BBCODE
); );
forum_topic_mark_read($app->getUserId(), $topicId, $forum['forum_id']); forum_topic_mark_read($app->getUserId(), $topicId, $forum['forum_id']);

View file

@ -1,5 +1,6 @@
<?php <?php
use Misuzu\Database; use Misuzu\Database;
use Misuzu\Parsers\MarkdownParser;
require_once __DIR__ . '/../misuzu.php'; require_once __DIR__ . '/../misuzu.php';

View file

@ -246,6 +246,8 @@ class Application extends ApplicationBase
$this->templatingInstance->addFilter('colour_get_red'); $this->templatingInstance->addFilter('colour_get_red');
$this->templatingInstance->addFilter('colour_get_green'); $this->templatingInstance->addFilter('colour_get_green');
$this->templatingInstance->addFilter('colour_get_blue'); $this->templatingInstance->addFilter('colour_get_blue');
$this->templatingInstance->addFilter('md', 'parse_markdown');
$this->templatingInstance->addFilter('bbcode', 'parse_bbcode');
$this->templatingInstance->addFunction('git_hash', [Application::class, 'gitCommitHash']); $this->templatingInstance->addFunction('git_hash', [Application::class, 'gitCommitHash']);
$this->templatingInstance->addFunction('git_branch', [Application::class, 'gitBranch']); $this->templatingInstance->addFunction('git_branch', [Application::class, 'gitBranch']);

View file

@ -1,26 +1,42 @@
<?php <?php
use Misuzu\Database; use Misuzu\Database;
define('MSZ_FORUM_POST_PARSER_PLAIN', 0);
define('MSZ_FORUM_POST_PARSER_BBCODE', 1);
define('MSZ_FORUM_POST_PARSER_MARKDOWN', 2);
define('MSZ_FORUM_POST_PARSERS', [
MSZ_FORUM_POST_PARSER_PLAIN,
MSZ_FORUM_POST_PARSER_BBCODE,
MSZ_FORUM_POST_PARSER_MARKDOWN,
]);
function forum_post_is_valid_parser(int $parser): bool
{
return in_array($parser, MSZ_FORUM_POST_PARSERS);
}
function forum_post_create( function forum_post_create(
int $topicId, int $topicId,
int $forumId, int $forumId,
int $userId, int $userId,
string $ipAddress, string $ipAddress,
string $text string $text,
int $parser = MSZ_FORUM_POST_PARSER_PLAIN
): int { ): int {
$dbc = Database::connection(); $dbc = Database::connection();
$createPost = $dbc->prepare(' $createPost = $dbc->prepare('
INSERT INTO `msz_forum_posts` INSERT INTO `msz_forum_posts`
(`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`) (`topic_id`, `forum_id`, `user_id`, `post_ip`, `post_text`, `post_parse`)
VALUES VALUES
(:topic_id, :forum_id, :user_id, INET6_ATON(:post_ip), :post_text) (:topic_id, :forum_id, :user_id, INET6_ATON(:post_ip), :post_text, :post_parse)
'); ');
$createPost->bindValue('topic_id', $topicId); $createPost->bindValue('topic_id', $topicId);
$createPost->bindValue('forum_id', $forumId); $createPost->bindValue('forum_id', $forumId);
$createPost->bindValue('user_id', $userId); $createPost->bindValue('user_id', $userId);
$createPost->bindValue('post_ip', $ipAddress); $createPost->bindValue('post_ip', $ipAddress);
$createPost->bindValue('post_text', $text); $createPost->bindValue('post_text', $text);
$createPost->bindValue('post_parse', $parser);
return $createPost->execute() ? $dbc->lastInsertId() : 0; return $createPost->execute() ? $dbc->lastInsertId() : 0;
} }
@ -50,7 +66,7 @@ function forum_post_find(int $postId): array
define('MSZ_FORUM_POST_LISTING_QUERY_STANDARD', ' define('MSZ_FORUM_POST_LISTING_QUERY_STANDARD', '
SELECT SELECT
p.`post_id`, p.`post_text`, p.`post_created`, p.`post_id`, p.`post_text`, p.`post_created`, p.`post_parse`,
p.`topic_id`, p.`topic_id`,
u.`user_id` as `poster_id`, u.`user_id` as `poster_id`,
u.`username` as `poster_name`, u.`username` as `poster_name`,

View file

@ -0,0 +1,51 @@
<?php
namespace Misuzu\Parsers\BBCode;
use Misuzu\Parsers\Parser;
final class BBCodeParser extends Parser
{
private $tags = [];
public function __construct(array $tags = [])
{
parent::__construct();
if (empty($tags)) {
$tags = [
// Advanced markup
new Tags\CodeTag,
new Tags\QuoteTag,
new Tags\BoxTag,
new Tags\MarkdownTag,
// Slightly more advanced markup
new Tags\AudioTag,
new Tags\VideoTag,
// Basic markup
new Tags\BoldTag,
new Tags\ItalicsTag,
new Tags\UnderlineTag,
new Tags\StrikeTag,
new Tags\SpoilerTag,
new Tags\HeaderTag,
new Tags\ImageTag,
// Finally parse leftover newlines
new Tags\NewLineTag,
];
}
$this->tags = $tags;
}
public function parseText(string $text): string
{
foreach ($this->tags as $tag) {
$text = $tag->parseText($text);
}
return $text;
}
}

View file

@ -0,0 +1,13 @@
<?php
namespace Misuzu\Parsers\BBCode;
abstract class BBCodeSimpleTag
{
abstract public function getPattern(): string;
abstract public function getReplacement(): string;
public function parseText(string $text): string
{
return preg_replace($this->getPattern(), $this->getReplacement(), $text);
}
}

View file

@ -0,0 +1,7 @@
<?php
namespace Misuzu\Parsers\BBCode;
abstract class BBCodeTag
{
abstract public function parseText(string $text): string;
}

View file

@ -0,0 +1,20 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeTag;
final class AudioTag extends BBCodeTag
{
public function parseText(string $text): string
{
return preg_replace_callback(
'#\[audio\]((?:https?:\/\/).*)\[/audio\]#',
function ($matches) {
// todo: domain matches etc.
// sites like soundcloud (and mixcloud, if possible) should be embeddable through this tag
return "<audio controls src='{$matches[1]}'></audio>";
},
$text
);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class BoldTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\[b\](.*)\[\/b\]/";
}
public function getReplacement(): string
{
return '<b>$1</b>';
}
}

View file

@ -0,0 +1,23 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeTag;
final class BoxTag extends BBCodeTag
{
public function parseText(string $text): string
{
return preg_replace_callback(
'/\[box(?:=(.*))?\](.*)\[\/box\]/',
function ($matches) {
$randomId = 'toggle_' . bin2hex(random_bytes(8));
$title = strlen($matches[1]) ? $matches[1] : 'Spoiler';
return '<div class="container container--hidden" id="' . $randomId . '">'
. "<div class='container__title' onclick='toggleContainer(\"{$randomId}\")'>{$title}</div>"
. "<div class='container__content'>{$matches[2]}</div>"
. '</div>';
},
$text
);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeTag;
final class CodeTag extends BBCodeTag
{
public function parseText(string $text): string
{
return preg_replace_callback(
'/\[code(?:\=([a-z]+))?\](.*?)\[\/code\]/s',
function ($matches) {
$class = strlen($matches[1]) ? "lang-{$matches[1]}" : '';
$text = str_replace(['[', ']', '<', '>'], ['&#91;', '&#93;', '&lt;', '&gt;'], $matches[2]);
return "<pre class='bbcode__code'><code class='bbcode__code-container {$class}'>{$text}</code></pre>";
},
$text
);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class HeaderTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\[header\](.*)\[\/header\]/";
}
public function getReplacement(): string
{
return '<h1>$1</h1>';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class ImageTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\[img\]((?:https?:\/\/).*)\[\/img\]/";
}
public function getReplacement(): string
{
return '<img src="$1" alt="$1">';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class ItalicsTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\[i\](.*)\[\/i\]/";
}
public function getReplacement(): string
{
return '<i>$1</i>';
}
}

View file

@ -0,0 +1,19 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\MarkdownParser;
use Misuzu\Parsers\BBCode\BBCodeTag;
final class MarkdownTag extends BBCodeTag
{
public function parseText(string $text): string
{
return preg_replace_callback(
'#\[md\](.*?)\[/md\]#s',
function ($matches) {
return MarkdownParser::getOrCreateInstance()->parseText($matches[1]);
},
$text
);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class NewLineTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\r\n|\r|\n/";
}
public function getReplacement(): string
{
return '<br>';
}
}

View file

@ -0,0 +1,24 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeTag;
final class QuoteTag extends BBCodeTag
{
public function parseText(string $text): string
{
return preg_replace_callback(
'#\[quote(?:=(.*))?\](.*)\[/quote\]#',
function ($matches) {
$prefix = '';
if (!empty($matches[1])) {
$prefix = "<small>{$matches[1]}:</small>";
}
return "<blockquote>{$prefix}{$matches[2]}</blockquote>";
},
$text
);
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class SpoilerTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\[spoiler\](.*)\[\/spoiler\]/";
}
public function getReplacement(): string
{
return '<span class="spoiler-class-name">$1</span>';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class StrikeTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\[s\](.*)\[\/s\]/";
}
public function getReplacement(): string
{
return '<del>$1</del>';
}
}

View file

@ -0,0 +1,17 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeSimpleTag;
final class UnderlineTag extends BBCodeSimpleTag
{
public function getPattern(): string
{
return "/\[u\](.*)\[\/u\]/";
}
public function getReplacement(): string
{
return '<u>$1</u>';
}
}

View file

@ -0,0 +1,26 @@
<?php
namespace Misuzu\Parsers\BBCode\Tags;
use Misuzu\Parsers\BBCode\BBCodeTag;
final class VideoTag extends BBCodeTag
{
private const YOUTUBE_REGEX = '#^https?://(?:www\.)?youtu(?:be\.(?:[a-z]{2,63})|\.be|\be-nocookie\.com)/(?:.*?)v=([a-zA-Z0-9_-]+)#si';
public function parseText(string $text): string
{
return preg_replace_callback(
'#\[video\]((?:https?:\/\/).*)\[/video\]#',
function ($matches) {
if (preg_match(self::YOUTUBE_REGEX, $matches[1], $ytMatches)) {
return '<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/'
. $ytMatches[1]
. '?rel=0" frameborder="0" allow="autoplay; encrypted-media" allowfullscreen></iframe>';
}
return "<video controls src='{$matches[1]}'></video>";
},
$text
);
}
}

View file

@ -0,0 +1,20 @@
<?php
namespace Misuzu\Parsers;
use Parsedown;
final class MarkdownParser extends Parser
{
private $parsedown;
public function __construct()
{
parent::__construct();
$this->parsedown = new Parsedown;
}
public function parseText(string $text): string
{
return $this->parsedown->text($text);
}
}

34
src/Parsers/Parser.php Normal file
View file

@ -0,0 +1,34 @@
<?php
namespace Misuzu\Parsers;
abstract class Parser
{
private static $instances = [];
public function __construct()
{
self::$instances[static::class] = $this;
}
public static function getInstance(): ?Parser
{
if (!array_key_exists(static::class, self::$instances)) {
return null;
}
return self::$instances[static::class];
}
public static function getOrCreateInstance(...$args): Parser
{
$instance = static::getInstance();
if ($instance === null) {
$instance = new static(...$args);
}
return $instance;
}
abstract public function parseText(string $text): string;
}

View file

@ -220,3 +220,13 @@ function pdo_prepare_array(array $keys, bool $useKeys = false, string $format =
return implode(', ', $parts); return implode(', ', $parts);
} }
function parse_markdown(string $text): string
{
return \Misuzu\Parsers\MarkdownParser::getOrCreateInstance()->parseText($text);
}
function parse_bbcode(string $text): string
{
return \Misuzu\Parsers\BBCode\BBCodeParser::getOrCreateInstance()->parseText($text);
}

View file

@ -281,7 +281,13 @@
</a> </a>
</div> </div>
<div class="forum__post__content__text"> <div class="forum__post__content__text">
{{ post.post_text }} {% if post.post_parse == 2 %}
{{ post.post_text|escape|md|raw }}
{% elseif post.post_parse == 1 %}
{{ post.post_text|escape|bbcode|raw }}
{% else %}
{{ post.post_text|escape }}
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View file

@ -97,9 +97,9 @@
return; return;
if (containerIsClosed(elem)) if (containerIsClosed(elem))
openContainer(elem); openContainer(id);
else else
closeContainer(elem); closeContainer(id);
} }
function openContainer(id) { function openContainer(id) {

View file

@ -12,7 +12,7 @@
<div class="container__content news__post__content"> <div class="container__content news__post__content">
<div class="news__post__text"> <div class="news__post__text">
{{ post.post_text|raw }} {{ post.post_text|md|raw }}
</div> </div>
<div class="news__sidebar news__post__details"> <div class="news__sidebar news__post__details">