diff --git a/composer.json b/composer.json index 482d341..075222d 100644 --- a/composer.json +++ b/composer.json @@ -12,6 +12,7 @@ "doctrine/doctrine-migrations-bundle": "^3.4", "doctrine/orm": "^3.3", "league/commonmark": "^2.7", + "league/html-to-markdown": "^5.1", "phpdocumentor/reflection-docblock": "^5.6", "phpstan/phpdoc-parser": "^2.1", "symfony/asset": "7.2.*", diff --git a/composer.lock b/composer.lock index eefe00f..5818740 100644 --- a/composer.lock +++ b/composer.lock @@ -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": "c3cbc37c53092d5fb23e7a7e21304c25", + "content-hash": "194a6d80a9896ce73994dc1b6c059f1d", "packages": [ { "name": "composer/semver", @@ -1635,6 +1635,95 @@ ], "time": "2022-12-11T20:36:23+00:00" }, + { + "name": "league/html-to-markdown", + "version": "5.1.1", + "source": { + "type": "git", + "url": "https://github.com/thephpleague/html-to-markdown.git", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/thephpleague/html-to-markdown/zipball/0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "reference": "0b4066eede55c48f38bcee4fb8f0aa85654390fd", + "shasum": "" + }, + "require": { + "ext-dom": "*", + "ext-xml": "*", + "php": "^7.2.5 || ^8.0" + }, + "require-dev": { + "mikehaertl/php-shellcommand": "^1.1.0", + "phpstan/phpstan": "^1.8.8", + "phpunit/phpunit": "^8.5 || ^9.2", + "scrutinizer/ocular": "^1.6", + "unleashedtech/php-coding-standard": "^2.7 || ^3.0", + "vimeo/psalm": "^4.22 || ^5.0" + }, + "bin": [ + "bin/html-to-markdown" + ], + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "5.2-dev" + } + }, + "autoload": { + "psr-4": { + "League\\HTMLToMarkdown\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Colin O'Dell", + "email": "colinodell@gmail.com", + "homepage": "https://www.colinodell.com", + "role": "Lead Developer" + }, + { + "name": "Nick Cernis", + "email": "nick@cern.is", + "homepage": "http://modernnerd.net", + "role": "Original Author" + } + ], + "description": "An HTML-to-markdown conversion helper for PHP", + "homepage": "https://github.com/thephpleague/html-to-markdown", + "keywords": [ + "html", + "markdown" + ], + "support": { + "issues": "https://github.com/thephpleague/html-to-markdown/issues", + "source": "https://github.com/thephpleague/html-to-markdown/tree/5.1.1" + }, + "funding": [ + { + "url": "https://www.colinodell.com/sponsor", + "type": "custom" + }, + { + "url": "https://www.paypal.me/colinpodell/10.00", + "type": "custom" + }, + { + "url": "https://github.com/colinodell", + "type": "github" + }, + { + "url": "https://tidelift.com/funding/github/packagist/league/html-to-markdown", + "type": "tidelift" + } + ], + "time": "2023-07-12T21:21:09+00:00" + }, { "name": "monolog/monolog", "version": "3.9.0", diff --git a/config/services.yaml b/config/services.yaml index 2d6a76f..dd6b5e3 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -18,6 +18,7 @@ services: exclude: - '../src/DependencyInjection/' - '../src/Entity/' + - '../src/Interface/' - '../src/Kernel.php' # add more service definitions when explicit configuration is needed diff --git a/migrations/Version20250522212300.php b/migrations/Version20250522212300.php index ec69856..b5116e5 100644 --- a/migrations/Version20250522212300.php +++ b/migrations/Version20250522212300.php @@ -4,6 +4,7 @@ declare(strict_types=1); namespace DoctrineMigrations; +use DateTime; use Doctrine\DBAL\Schema\Schema; use Doctrine\Migrations\AbstractMigration; use Symfony\Component\Yaml\Yaml; @@ -38,17 +39,21 @@ final class Version20250522212300 extends AbstractMigration $blogPostPath = 'scripts/blog-migrated/' . $slug . '.yaml'; $blogPost = Yaml::parseFile($blogPostPath); - $publishedDate = $blogPost['pubDate']; - $updatedDate = array_key_exists(array: $blogPost, key: 'updatedDate') - ? $blogPost['updatedDate'] + $publishedDateTime = DateTime::createFromFormat('U', strval($blogPost['pubDate'])); + $publishedDate = $publishedDateTime->format('Y-m-d'); + $updatedDateTime = array_key_exists(array: $blogPost, key: 'updatedDate') + ? DateTime::createFromFormat('U', strval($blogPost['updatedDate'])) : null; + $updatedDate = $updatedDateTime == null + ? 'NULL' + : "'" . $updatedDateTime->format('Y-m-d') . "'"; $title = str_replace("'", "''", $blogPost['title']); $description = str_replace("'", "''", $blogPost['description']); $content = str_replace("'", "''", $blogPost['content']); $this->addSql(<< 'January', + '02' => 'February', + '03' => 'March', + '04' => 'April', + '05' => 'May', + '06' => 'June', + '07' => 'July', + '08' => 'August', + '09' => 'September', + '10' => 'October', + '11' => 'November', + '12' => 'December', +]; + class GuiController extends AbstractController { + /** + * @return array + */ #[Route('/', name: 'index')] #[Template('/index.html.twig')] public function index(): array { @@ -23,7 +42,9 @@ class GuiController extends AbstractController { 'description' => 'Joe Carstairs\' personal website', ]; } - + /** + * @return array + */ #[Route('/notes', name: 'notes')] #[Template('/notes.html.twig')] public function notes( @@ -43,93 +64,115 @@ class GuiController extends AbstractController { return [ 'title' => 'Joe Carstairs\' notes', 'description' => 'Joe Carstairs\' notes', + 'isFeed' => True, 'notes' => $notesByYearAndMonth, 'years' => $years, 'months' => $monthsByYear, - 'monthNames' => [ - '01' => 'January', - '02' => 'February', - '03' => 'March', - '04' => 'April', - '05' => 'May', - '06' => 'June', - '07' => 'July', - '08' => 'August', - '09' => 'September', - '10' => 'October', - '11' => 'November', - '12' => 'December', - ], + 'monthNames' => MONTH_NAMES, + ]; + } + /** + * @return array + */ + #[Route('/blog', name: 'blog_posts')] + #[Template('/blog_posts.html.twig')] + public function blogPosts( + EntityManagerInterface $entityManager, + ): array { + $posts = $entityManager->getRepository(BlogPost::class)->findAllOrderedBySlugDesc(); + $years = $this->getYears($posts); + $postsByYear = $this->groupByYear($posts); + $months = array_map(array: $years, callback: fn($year) => $this->getMonths($postsByYear[$year])); + $monthsByYear = array_combine(keys: $years, values: $months); + $groupByMonth = fn($posts) => $this->groupByMonth($posts); + $postsByYearAndMonth = array_map( + array: $postsByYear, + callback: $groupByMonth, + ); + + return [ + 'title' => 'Joe Carstairs\' blog', + 'description' => 'Joe Carstairs\' blog', + 'isFeed' => True, + 'posts' => $postsByYearAndMonth, + 'years' => $years, + 'months' => $monthsByYear, + 'monthNames' => MONTH_NAMES, ]; } /** - * @param $notes Note[] - * @return Note[][] + * @@template T of \FeedEntry + * @param $entries T[] + * @return T[][] */ - function groupByYear(array $notes): array { - $years = $this->getYears($notes); + function groupByYear(array $entries): array { + $years = $this->getYears($entries); $filterByYear = fn(string $year) => - fn($note) => ($note->getPublishedDate()->format('Y') == $year); - $notesForYear = fn(string $year) => - array_filter(array: $notes, callback: $filterByYear($year)); - $notesByYear = array_map( + fn($entry) => ($entry->getPublishedDate()->format('Y') == $year); + $entriesForYear = fn(string $year) => + array_filter(array: $entries, callback: $filterByYear($year)); + $entriesByYear = array_map( array: $years, - callback: $notesForYear, + callback: $entriesForYear, ); - return array_combine(keys: $years, values: $notesByYear); + return array_combine(keys: $years, values: $entriesByYear); } /** - * @param $notes Note[] - * @return string[] - */ - function getYears(array $notes): array { - $getYear = fn(Note $note): string => $note->getPublishedDate()->format('Y'); - $years = array_map(array: $notes, callback: $getYear); + * @template T of \FeedEntry + * @param $entries T[] + * @return string[] + */ + function getYears(array $entries): array { + $getYear = fn($note): string => $note->getPublishedDate()->format('Y'); + $years = array_map(array: $entries, callback: $getYear); $years = array_unique($years); arsort($years); return $years; } /** - * @param $notes Note[] - * @return Note[] + * @template T of \FeedEntry + * @param $entries T[] + * @return T[] */ - function groupByMonth(array $notes): array { - $months = $this->getMonths($notes); + function groupByMonth(array $entries): array { + $months = $this->getMonths($entries); $filterByMonth = fn(string $month) => - fn($note) => ($note->getPublishedDate()->format('m') == $month); - $notesByMonth = array_map( + fn($entry) => ($entry->getPublishedDate()->format('m') == $month); + $entriesByMonth = array_map( array: $months, callback: fn(string $month) => - array_filter(array: $notes, callback: $filterByMonth($month)), + array_filter(array: $entries, callback: $filterByMonth($month)), ); - return array_combine(keys: $months, values: $notesByMonth); + return array_combine(keys: $months, values: $entriesByMonth); } /** - * @param $notes Note[] - * @return string[] + * @@template T of \FeedEntry + * @param $entries T[] + * @return string[] */ - function getMonths(array $notes): array { - $getMonth = fn(Note $note): string => $note->getPublishedDate()->format('m'); - $months = array_map(array: $notes, callback: $getMonth); + function getMonths(array $entries): array { + $getMonth = fn($entry) => $entry->getPublishedDate()->format('m'); + $months = array_map(array: $entries, callback: $getMonth); $months = array_unique($months); arsort($months); return $months; } /** - * @param $notes Note[] - * @return Note[] + * @template T of \FeedEntry + * @param $entries T[] + * @return T[] */ - function sortBySlug(array $notes): array { - $getSlug = fn(Note $note): string => $note->getSlug(); - $slugs = array_map(array: $notes, callback: $getSlug); - $notesBySlug = array_combine(keys: $slugs, values: $notes); - krsort($notesBySlug); - return array_values($notesBySlug); + function sortBySlug(array $entries): array { + $getSlug = fn($entry): string => $entry->getSlug(); + $slugs = array_map(array: $entries, callback: $getSlug); + $entriesBySlug = array_combine(keys: $slugs, values: $entries); + krsort($entriesBySlug); + return array_values($entriesBySlug); } #[IsGranted('ROLE_EDITOR')] @@ -185,8 +228,30 @@ class GuiController extends AbstractController { return [ 'title' => 'Joe Carstairs\' notes', 'description' => 'Joe Carstairs\' notes', + 'isFeedEntry' => True, 'note' => $note, 'slug' => $slug, ]; } + + /** + * @return array + */ + #[Route('/blog/{slug}', name: 'blog_post')] + #[Template('/blog_post.html.twig')] + public function blogPost( + EntityManagerInterface $entityManager, + string $slug, + ): array { + $repository = $entityManager->getRepository(BlogPost::class); + $post = $repository->findOneBy(['slug' => $slug]); + + return [ + 'title' => $post->getTitle(), + 'description' => $post->getDescription(), + 'isFeedEntry' => True, + 'post' => $post, + 'slug' => $slug, + ]; + } } diff --git a/src/Entity/BlogPost.php b/src/Entity/BlogPost.php index ee91382..582315a 100644 --- a/src/Entity/BlogPost.php +++ b/src/Entity/BlogPost.php @@ -2,12 +2,13 @@ namespace App\Entity; +use App\Interface\FeedEntry; use App\Repository\BlogPostRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: BlogPostRepository::class)] -class BlogPost +class BlogPost implements FeedEntry { #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Entity/Note.php b/src/Entity/Note.php index f894813..2674e75 100644 --- a/src/Entity/Note.php +++ b/src/Entity/Note.php @@ -2,12 +2,13 @@ namespace App\Entity; +use App\Interface\FeedEntry; use App\Repository\NoteRepository; use Doctrine\DBAL\Types\Types; use Doctrine\ORM\Mapping as ORM; #[ORM\Entity(repositoryClass: NoteRepository::class)] -class Note +class Note implements FeedEntry { #[ORM\Id] #[ORM\GeneratedValue] diff --git a/src/Interface/FeedEntry.php b/src/Interface/FeedEntry.php new file mode 100644 index 0000000..fa9db83 --- /dev/null +++ b/src/Interface/FeedEntry.php @@ -0,0 +1,8 @@ +createQueryBuilder('n') + ->orderBy('n.slug', 'DESC') + ->getQuery() + ->getResult(); + } } diff --git a/templates/base.html.twig b/templates/base.html.twig index cfac6e5..2e2ec7c 100644 --- a/templates/base.html.twig +++ b/templates/base.html.twig @@ -12,8 +12,10 @@ {{ include('components/_navbar.html.twig') }} -
- {% block content %}{% endblock %} -
+ {% block main %} +
+ {% block content %}{% endblock %} +
+ {% endblock %} diff --git a/templates/blog_post.html.twig b/templates/blog_post.html.twig new file mode 100644 index 0000000..e6d25f3 --- /dev/null +++ b/templates/blog_post.html.twig @@ -0,0 +1,47 @@ +{% extends 'base.html.twig' %} + +{% block content %} + {% if post %} +
+ + +
+

{% apply markdown_to_html %}{{ post.title }}{% endapply %}

+

+ {% apply markdown_to_html %}{{ post.description }}{% endapply %} +

+
+ + + {% apply markdown_to_html %} +
+ {{ post.content }} +
+ {% endapply %} +
+ {% else %} +
+

Post not found

+

I don't have a blog post '{{ slug }}'.

+

Go back to Blog.

+
+ {% endif %} +{% endblock %} diff --git a/templates/blog_posts.html.twig b/templates/blog_posts.html.twig new file mode 100644 index 0000000..3fe182b --- /dev/null +++ b/templates/blog_posts.html.twig @@ -0,0 +1,81 @@ +{% extends 'base.html.twig' %} + +{% block content %} +
+

Joe Carstairs' blog

+ + + + + {% if years %} + + {% endif %} + + {% for year in years %} +

{{ year }}

+ + + + {% for month in months[year] %} +

{{ monthNames[month] }}

+ + {% for post in posts[year][month] %} +
+ +

+ {{ post.title }} +

+ +

+ Added: + +

+ + {% if post.updatedDate %} +

+ Updated: + +

+ {% endif %} + +
+ {% apply markdown_to_html %} + {{ post.description|markdown_to_html|striptags('

')|html_to_markdown }} + {% endapply %} +

+
+
+ {% endfor %} + {% endfor %} + {% else %} +

I have no blog posts.

+ {% endfor %} +
+{% endblock %} diff --git a/templates/components/_navbar.html.twig b/templates/components/_navbar.html.twig index 5b88080..6d9c7e0 100644 --- a/templates/components/_navbar.html.twig +++ b/templates/components/_navbar.html.twig @@ -4,7 +4,7 @@ Home
  • - Blog + Blog
  • Notes diff --git a/templates/components/head/_stylesheets.html.twig b/templates/components/head/_stylesheets.html.twig index d6e4762..f3f3f4a 100644 --- a/templates/components/head/_stylesheets.html.twig +++ b/templates/components/head/_stylesheets.html.twig @@ -1,4 +1,9 @@ - +{% if isFeed|default(false) %} + +{% endif %} +{% if isFeedEntry|default(false) %} + +{% endif %} diff --git a/templates/note.html.twig b/templates/note.html.twig index 005e78e..cbf84bf 100644 --- a/templates/note.html.twig +++ b/templates/note.html.twig @@ -1,23 +1,28 @@ {% extends 'base.html.twig' %} -{% block content %} +{% block main %} {% if note %} -
    -

    Note {{ note.slug }}

    - - -
    +
    +
    +

    Note {{ note.slug }}

    + + +

    Added:

    +
    + +
    + {{ note.content|markdown_to_html }} +
    +
    {% else %} -
    -

    Note not found

    -

    I don't have a note '{{ slug }}'.

    -

    Go back to Notes.

    -
    +
    +
    +

    Note not found

    +
    +
    +

    I don't have a note '{{ slug }}'.

    +

    Go back to Notes.

    +
    +
    {% endif %} {% endblock %} diff --git a/templates/notes.html.twig b/templates/notes.html.twig index 46cc58a..ed9d43d 100644 --- a/templates/notes.html.twig +++ b/templates/notes.html.twig @@ -14,14 +14,16 @@ Permalink

    - + {% if years %} + + {% endif %} {% for year in years %}

    {{ year }}

    @@ -42,18 +44,23 @@ {% for note in notes[year][month] %}
    -

    - Note {{ note.slug }} -

    - + +

    + Note {{ note.slug }} +

    +

    + Added: + +

    -
    - {% apply markdown_to_html %} - {{ note.content }} - {% endapply %} -
    +
    + {% apply markdown_to_html %} + {{ note.content|markdown_to_html|striptags('

    ')|html_to_markdown }} + {% endapply %} +

    +
    {% endfor %} {% endfor %} diff --git a/todo.txt b/todo.txt index ce514a5..87a6948 100644 --- a/todo.txt +++ b/todo.txt @@ -1,5 +1,3 @@ -Add blog post page -Add blog index page Add a most recent blog post to my home page Add a most recent note to my home page Write a script to take a database dump