6 Commits

Author SHA1 Message Date
Tim
5fc691c02a Create docker setup 2025-05-28 01:25:07 +02:00
Tim
f338d791a7 Remove _blank from links 2025-05-27 21:12:56 +02:00
Tim
e26c2a64b8 Fix markdown snip link 2025-05-27 17:44:18 +02:00
Tim
33bb4e77e5 Upgrade markdown parser for tables and footnotes 2025-05-27 15:48:32 +02:00
Tim
978c075a3e Simplefy the generic parser
(remove code blocks, made obsolete by markdown)
2025-05-27 15:21:01 +02:00
Tim
a741ee102d Start replacing highlight js with tempest-highlight 2025-05-27 00:58:48 +02:00
14 changed files with 378 additions and 55 deletions

View File

@ -0,0 +1,28 @@
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html/public;
index index.php;
#client_max_body_size 100m;
location / {
try_files $uri $uri/ /index.php$is_args$args;
}
location ~ \.php {
try_files $uri /index.php =404;
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
fastcgi_param DOCUMENT_ROOT $realpath_root;
fastcgi_param SCRIPT_NAME $fastcgi_script_name;
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_index index.php;
include fastcgi_params;
}
location ~ /\.(?:ht|git|svn) {
deny all;
}
}

36
.docker/php/Dockerfile Normal file
View File

@ -0,0 +1,36 @@
FROM php:8.4-fpm
ARG TIMEZONE
COPY php.ini /usr/local/etc/php/conf.d/docker-php-config.ini
RUN apt-get update && apt-get install -y \
gnupg \
g++ \
procps \
openssl \
git \
unzip \
zlib1g-dev \
libzip-dev \
libfreetype6-dev \
libpng-dev \
libjpeg-dev \
libicu-dev \
libonig-dev \
libxslt1-dev \
acl \
&& echo 'alias sf="php bin/console"' >> ~/.bashrc
RUN docker-php-ext-configure gd --with-jpeg --with-freetype
RUN docker-php-ext-install \
pdo pdo_mysql zip xsl gd intl opcache exif mbstring
# Set timezone
RUN ln -snf /usr/share/zoneinfo/${TIMEZONE} /etc/localtime && echo ${TIMEZONE} > /etc/timezone \
&& printf '[PHP]\ndate.timezone = "%s"\n', ${TIMEZONE} > /usr/local/etc/php/conf.d/tzone.ini \
&& "date"
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
WORKDIR /var/www/html

13
.docker/php/php.ini Normal file
View File

@ -0,0 +1,13 @@
memory_limit=1024M
opcache.enable=1
opcache.revalidate_freq=10
opcache.validate_timestamps=1
opcache.max_accelerated_files=10000
opcache.memory_consumption=192
opcache.max_wasted_percentage=10
opcache.interned_strings_buffer=1
opcache.fast_shutdown=1
upload_max_filesize = 20M
post_max_size = 20M

View File

@ -14,6 +14,7 @@
"league/pipeline": "^1.0",
"phpdocumentor/reflection-docblock": "^5.6",
"phpstan/phpdoc-parser": "^2.1",
"symfony/asset": "7.2.*",
"symfony/console": "*",
"symfony/dotenv": "*",
"symfony/flex": "^2",
@ -29,6 +30,7 @@
"symfony/uid": "*",
"symfony/validator": "*",
"symfony/yaml": "*",
"tempest/highlight": "^2.11",
"twig/extra-bundle": "^2.12|^3.0",
"twig/twig": "^3.0"
},

129
composer.lock generated
View File

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "638cbc9841226f386ba27215e24c5410",
"content-hash": "6bdfa9b2a81e0169ca083ef4d344b876",
"packages": [
{
"name": "dflydev/dot-access-data",
@ -2370,6 +2370,75 @@
},
"time": "2024-09-11T13:17:53+00:00"
},
{
"name": "symfony/asset",
"version": "v7.2.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/asset.git",
"reference": "cb926cd59fefa1f9b4900b3695f0f846797ba5c0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/asset/zipball/cb926cd59fefa1f9b4900b3695f0f846797ba5c0",
"reference": "cb926cd59fefa1f9b4900b3695f0f846797ba5c0",
"shasum": ""
},
"require": {
"php": ">=8.2"
},
"conflict": {
"symfony/http-foundation": "<6.4"
},
"require-dev": {
"symfony/http-client": "^6.4|^7.0",
"symfony/http-foundation": "^6.4|^7.0",
"symfony/http-kernel": "^6.4|^7.0"
},
"type": "library",
"autoload": {
"psr-4": {
"Symfony\\Component\\Asset\\": ""
},
"exclude-from-classmap": [
"/Tests/"
]
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Fabien Potencier",
"email": "fabien@symfony.com"
},
{
"name": "Symfony Community",
"homepage": "https://symfony.com/contributors"
}
],
"description": "Manages URL generation and versioning of web assets such as CSS stylesheets, JavaScript files and image files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/asset/tree/v7.2.0"
},
"funding": [
{
"url": "https://symfony.com/sponsor",
"type": "custom"
},
{
"url": "https://github.com/fabpot",
"type": "github"
},
{
"url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
"type": "tidelift"
}
],
"time": "2024-10-25T15:15:23+00:00"
},
{
"name": "symfony/cache",
"version": "v7.2.6",
@ -6436,6 +6505,64 @@
],
"time": "2025-04-04T10:10:11+00:00"
},
{
"name": "tempest/highlight",
"version": "2.11.4",
"source": {
"type": "git",
"url": "https://github.com/tempestphp/highlight.git",
"reference": "5a239a92ad6bd3e506ca86a0de3e99ac9dbcb0dd"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/tempestphp/highlight/zipball/5a239a92ad6bd3e506ca86a0de3e99ac9dbcb0dd",
"reference": "5a239a92ad6bd3e506ca86a0de3e99ac9dbcb0dd",
"shasum": ""
},
"require": {
"php": "^8.3"
},
"require-dev": {
"assertchris/ellison": "^1.0.2",
"friendsofphp/php-cs-fixer": "^3.21",
"league/commonmark": "^2.4",
"phpstan/phpstan": "^1.10.0",
"phpunit/phpunit": "^10.0",
"symfony/var-dumper": "^6.4|^7.0"
},
"suggest": {
"assertchris/ellison": "Allows you to analyse sentence complexity",
"league/commonmark": "Adds markdown support"
},
"type": "library",
"autoload": {
"psr-4": {
"Tempest\\Highlight\\": "src/"
}
},
"notification-url": "https://packagist.org/downloads/",
"license": [
"MIT"
],
"authors": [
{
"name": "Brent Roose",
"email": "brendt@stitcher.io"
}
],
"description": "Fast, extensible, server-side code highlighting",
"support": {
"issues": "https://github.com/tempestphp/highlight/issues",
"source": "https://github.com/tempestphp/highlight/tree/2.11.4"
},
"funding": [
{
"url": "https://github.com/brendt",
"type": "github"
}
],
"time": "2025-03-19T05:38:35+00:00"
},
{
"name": "twig/extra-bundle",
"version": "v3.21.0",

37
docker-compose.yaml Normal file
View File

@ -0,0 +1,37 @@
services:
nginx:
image: nginx:stable
ports:
- "8080:80"
volumes:
- .:/var/www/html
- ./.docker/nginx/default.conf:/etc/nginx/conf.d/default.conf
depends_on:
- php
php:
build:
context: ./.docker/php
depends_on:
- db
volumes:
- .:/var/www/html
working_dir: /var/www/html
environment:
DATABASE_URL: mysql://user:password@db:3306/app?serverVersion=11.7.2-MariaDB
APP_ENV: prod
db:
image: mariadb:11.7.2
environment:
MARIADB_ROOT_PASSWORD: password
MARIADB_DATABASE: app
MARIADB_USER: user
MARIADB_PASSWORD: password
volumes:
- db_data:/var/lib/mysql
ports:
- "3306"
volumes:
db_data:

View File

@ -0,0 +1,87 @@
pre, code {
color: #1f2328;
background-color: #ffffff;
}
.hl-keyword {
color: #cf222e;
}
.hl-property {
color: #8250df;
}
.hl-attribute {
font-style: italic;
}
.hl-type {
color: #EA4334;
}
.hl-generic {
color: #9d3af6;
}
.hl-value {
color: #0a3069;
}
.hl-literal {
color: #0a3069;
}
.hl-number {
color: #0a3069;
}
.hl-variable {
color: #953800;
}
.hl-comment {
color: #6e7781;
}
.hl-blur {
filter: blur(2px);
}
.hl-strong {
font-weight: bold;
}
.hl-em {
font-style: italic;
}
.hl-addition {
display: inline-block;
min-width: 100%;
background-color: #00FF0022;
}
.hl-deletion {
display: inline-block;
min-width: 100%;
background-color: #FF000011;
}
.hl-gutter {
display: inline-block;
font-size: 0.9em;
color: #555;
padding: 0 1ch;
margin-right: 1ch;
user-select: none;
}
.hl-gutter-addition {
background-color: #34A853;
color: #fff;
}
.hl-gutter-deletion {
background-color: #EA4334;
color: #fff;
}

View File

@ -20,7 +20,7 @@ abstract class AbstractParser implements ParserInterface
try {
return $this->safeParseView($content);
} catch (\Exception $exception) {
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($exception->getMessage()));
return sprintf('<pre><code>%s</code></pre>', htmlspecialchars($exception->getMessage()));
}
}

View File

@ -17,9 +17,9 @@ class GenericParser extends AbstractParser
$builder = new PipelineBuilder();
$pipeline = $builder
->add(new HtmlEscapeStage())
// ->add(new ReplaceBlocksStage('<pre>', '</pre>', '```'))
// ->add(new ReplaceBlocksStage('<code>', '</code>', '``'))
->add(new ReplaceStage(PHP_EOL, '<br>'))
->add(new ReplaceBlocksStage('<pre><code class="hljs">', '</code></pre>', '```'))
->add(new ReplaceBlocksStage('<code class="hljs">', '</code>', '``'))
->add($this->referenceStage)
->add($this->includeStage)
->build()
@ -27,13 +27,4 @@ class GenericParser extends AbstractParser
return $pipeline->process($content);
}
public function parseRaw(string $content): string
{
return str_replace(
['```', '``'],
'',
$content
);
}
}

View File

@ -4,13 +4,14 @@ namespace App\Service\SnipParser\Generic;
use InvalidArgumentException;
use League\Pipeline\StageInterface;
use Tempest\Highlight\Highlighter;
class ReplaceBlocksStage implements StageInterface
readonly class ReplaceBlocksStage implements StageInterface
{
public function __construct(
public readonly string $openTag = '<pre><code>',
public readonly string $closeTag = '</code></pre>',
public readonly string $delimiter = '```'
public string $openTag = '<pre><code>',
public string $closeTag = '</code></pre>',
public string $delimiter = '```'
) {}
public function __invoke(mixed $payload): string
@ -26,8 +27,9 @@ class ReplaceBlocksStage implements StageInterface
{
$pattern = sprintf('/%s(.+?)%s/s', preg_quote($this->delimiter), preg_quote($this->delimiter));
return preg_replace_callback($pattern, function ($matches) {
return $this->openTag . trim($matches[1]) . $this->closeTag;
$highlighter = new Highlighter()->withGutter();
return preg_replace_callback($pattern, function ($matches) use ($highlighter) {
return $this->openTag . $highlighter->parse(trim($matches[1]), 'php') . $this->closeTag;
}, $text);
}
}

View File

@ -3,11 +3,14 @@
namespace App\Service\SnipParser\Html;
use App\Service\SnipParser\AbstractParser;
use Tempest\Highlight\Highlighter;
class HtmlParser extends AbstractParser
{
public function safeParseView(string $content): string
{
return sprintf('<pre><code class="hljs">%s</code></pre>', htmlspecialchars($content));
$highlighter = new Highlighter()->withGutter();
return '<pre data-lang="html" class="notranslate">' . $highlighter->parse($content, 'html') . '</pre>';
}
}

View File

@ -6,11 +6,16 @@ use App\Repository\SnipRepository;
use App\Service\SnipParser\AbstractParser;
use League\CommonMark\Event\DocumentParsedEvent;
use League\CommonMark\Extension\CommonMark\Node\Inline\Link;
use League\CommonMark\Extension\DefaultAttributes\DefaultAttributesExtension;
use League\CommonMark\Extension\Footnote\FootnoteExtension;
use League\CommonMark\Extension\Table\Table;
use League\CommonMark\GithubFlavoredMarkdownConverter;
use League\CommonMark\Node\Inline\Text;
use League\CommonMark\Node\Query;
use Symfony\Component\DependencyInjection\Attribute\Autowire;
use Symfony\Component\Routing\RouterInterface;
use Tempest\Highlight\CommonMark\HighlightExtension;
use Tempest\Highlight\Highlighter;
class MarkdownParser extends AbstractParser
{
@ -21,8 +26,25 @@ class MarkdownParser extends AbstractParser
public function safeParseView(string $content): string
{
$converter = new GithubFlavoredMarkdownConverter();
$converter->getEnvironment()->addEventListener(DocumentParsedEvent::class, $this->documentParsed(...));
$config = [
'default_attributes' => [
Table::class => [
'class' => 'table table-hover',
],
Link::class => [
'class' => 'btn btn-sm btn-secondary',
],
],
];
$converter = new GithubFlavoredMarkdownConverter($config);
$converter
->getEnvironment()
->addExtension(new HighlightExtension(new Highlighter()->withGutter()))
->addExtension(new FootnoteExtension())
->addExtension(new DefaultAttributesExtension())
->addEventListener(DocumentParsedEvent::class, $this->documentParsed(...))
;
return $converter->convert($content);
}
@ -32,11 +54,17 @@ class MarkdownParser extends AbstractParser
$linkNodes = new Query()
->where(Query::type(Link::class))
->findAll($document);
->findAll($document)
;
/** @var Link $linkNode */
foreach ($linkNodes as $linkNode) {
$url = $linkNode->getUrl();
if (!is_numeric($url)) {
continue;
}
$snip = $this->snipRepo->find($url);
if ($snip === null) {
continue;

View File

@ -54,21 +54,3 @@
</div>
</div>
{% endblock %}
{% block css %}
{{ parent() }}
<link rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
{% endblock %}
{% block js %}
{{ parent() }}
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>
const codeBlocks = document.querySelectorAll('code.hljs');
codeBlocks.forEach((block) => {
hljs.highlightElement(block);
});
</script>
{% endblock %}

View File

@ -15,18 +15,5 @@
{% block css %}
{{ parent() }}
<link rel="stylesheet"
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
{% endblock %}
{% block js %}
{{ parent() }}
<script src="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
<script>
const codeBlocks = document.querySelectorAll('code.hljs');
codeBlocks.forEach((block) => {
hljs.highlightElement(block);
});
</script>
<link rel="stylesheet" href="{{ asset('github-light-default.css') }}">
{% endblock %}