From 2a382b15c1bc9a7ff346745262d314456c6ad09b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicolas=20L=C5=93uillet?= Date: Tue, 12 Nov 2024 15:21:23 +0100 Subject: [PATCH] Add Markdown export --- app/config/wallabag.yml | 4 + composer.json | 1 + composer.lock | 93 ++++++++++++++++++++++- migrations/Version20241112193044.php | 24 ++++++ src/Controller/ExportController.php | 4 +- src/Helper/EntriesExport.php | 48 +++++++----- templates/Entry/entries.html.twig | 1 + templates/Entry/entry.html.twig | 1 + tests/Controller/ExportControllerTest.php | 16 ++++ 9 files changed, 170 insertions(+), 22 deletions(-) create mode 100644 migrations/Version20241112193044.php diff --git a/app/config/wallabag.yml b/app/config/wallabag.yml index 4c26ec8a7..b3c1bd7c7 100644 --- a/app/config/wallabag.yml +++ b/app/config/wallabag.yml @@ -114,6 +114,10 @@ parameters: name: export_xml value: 1 section: export + - + name: export_md + value: 1 + section: export - name: import_with_redis value: 0 diff --git a/composer.json b/composer.json index c6ca1557b..a365bf705 100644 --- a/composer.json +++ b/composer.json @@ -87,6 +87,7 @@ "jms/serializer-bundle": "^5.4", "laminas/laminas-code": "^4.7.1", "lcobucci/jwt": "^4.3", + "league/html-to-markdown": "^5.1", "mgargano/simplehtmldom": "^1.5", "mnapoli/piwik-twig-extension": "^3.0", "monolog/monolog": "^2.9", diff --git a/composer.lock b/composer.lock index 91230d050..f1e058f6e 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": "6407ed5fbd4b0973ed565c2a136d3a81", + "content-hash": "a45ce1bad60f024c66e17b6aca7ad88d", "packages": [ { "name": "babdev/pagerfanta-bundle", @@ -5303,6 +5303,95 @@ ], "time": "2023-01-02T13:28:00+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": "masterminds/html5", "version": "2.9.0", @@ -20034,7 +20123,7 @@ "ext-tokenizer": "*", "ext-xml": "*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "7.4.29" }, diff --git a/migrations/Version20241112193044.php b/migrations/Version20241112193044.php new file mode 100644 index 000000000..7a3705bc5 --- /dev/null +++ b/migrations/Version20241112193044.php @@ -0,0 +1,24 @@ +addSql('INSERT INTO ' . $this->getTable('internal_setting') . " (name, value, section) VALUES ('export_md', '1', 'export');"); + } + + public function down(Schema $schema): void + { + $this->addSql('DELETE FROM' . $this->getTable('internal_setting') . " WHERE name = 'export_md';"); + } +} diff --git a/src/Controller/ExportController.php b/src/Controller/ExportController.php index e4ddbb1f5..c49a3d2c1 100644 --- a/src/Controller/ExportController.php +++ b/src/Controller/ExportController.php @@ -20,7 +20,7 @@ class ExportController extends AbstractController * Gets one entry content. * * @Route("/export/{id}.{format}", name="export_entry", requirements={ - * "format": "epub|pdf|json|xml|txt|csv", + * "format": "epub|pdf|json|xml|txt|csv|md", * "id": "\d+" * }) * @@ -54,7 +54,7 @@ class ExportController extends AbstractController * Export all entries for current user. * * @Route("/export/{category}.{format}", name="export_entries", requirements={ - * "format": "epub|pdf|json|xml|txt|csv", + * "format": "epub|pdf|json|xml|txt|csv|md", * "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain" * }) * diff --git a/src/Helper/EntriesExport.php b/src/Helper/EntriesExport.php index 9b4712632..e05f3af84 100644 --- a/src/Helper/EntriesExport.php +++ b/src/Helper/EntriesExport.php @@ -5,6 +5,7 @@ namespace Wallabag\Helper; use Html2Text\Html2Text; use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializerBuilder; +use League\HTMLToMarkdown\HtmlConverter; use PHPePub\Core\EPub; use PHPePub\Core\Structure\OPF\DublinCore; use Symfony\Component\HttpFoundation\Response; @@ -129,10 +130,8 @@ class EntriesExport /** * Use PHPePub to dump a .epub file. - * - * @return Response */ - private function produceEpub() + private function produceEpub(): Response { $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; \assert($user instanceof User); @@ -249,10 +248,8 @@ class EntriesExport /** * Use TCPDF to dump a .pdf file. - * - * @return Response */ - private function producePdf() + private function producePdf(): Response { $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null; \assert($user instanceof User); @@ -326,10 +323,8 @@ class EntriesExport /** * Inspired from CsvFileDumper. - * - * @return Response */ - private function produceCsv() + private function produceCsv(): Response { $delimiter = ';'; $enclosure = '"'; @@ -372,10 +367,8 @@ class EntriesExport /** * Dump a JSON file. - * - * @return Response */ - private function produceJson() + private function produceJson(): Response { return Response::create( $this->prepareSerializingContent('json'), @@ -390,10 +383,8 @@ class EntriesExport /** * Dump a XML file. - * - * @return Response */ - private function produceXml() + private function produceXml(): Response { return Response::create( $this->prepareSerializingContent('xml'), @@ -408,10 +399,8 @@ class EntriesExport /** * Dump a TXT file. - * - * @return Response */ - private function produceTxt() + private function produceTxt(): Response { $content = ''; $bar = str_repeat('=', 100); @@ -432,6 +421,29 @@ class EntriesExport ); } + /** + * Dump a Markdown file. + */ + private function produceMd(): Response + { + $content = ''; + $converter = new HtmlConverter(); + $converter->getConfig()->setOption('strip_tags', true); + foreach ($this->entries as $entry) { + $content .= $converter->convert('

' . $entry->getTitle() . '

' . $entry->getContent()); + } + + return Response::create( + $content, + 200, + [ + 'Content-type' => 'text/markdown', + 'Content-Disposition' => 'attachment; filename="' . $this->getSanitizedFilename() . '.md"', + 'Content-Transfer-Encoding' => 'UTF-8', + ] + ); + } + /** * Return a Serializer object for producing processes that need it (JSON & XML). * diff --git a/templates/Entry/entries.html.twig b/templates/Entry/entries.html.twig index 2e8b8ebce..1dc4f16fd 100644 --- a/templates/Entry/entries.html.twig +++ b/templates/Entry/entries.html.twig @@ -105,6 +105,7 @@ {% if craue_setting('export_csv') %}
  • CSV
  • {% endif %} {% if craue_setting('export_txt') %}
  • TXT
  • {% endif %} {% if craue_setting('export_xml') %}
  • XML
  • {% endif %} + {% if craue_setting('export_md') %}
  • Markdown
  • {% endif %} diff --git a/templates/Entry/entry.html.twig b/templates/Entry/entry.html.twig index e5b45a59f..d3714acf6 100644 --- a/templates/Entry/entry.html.twig +++ b/templates/Entry/entry.html.twig @@ -242,6 +242,7 @@ {% if craue_setting('export_json') %}
  • JSON
  • {% endif %} {% if craue_setting('export_txt') %}
  • TXT
  • {% endif %} {% if craue_setting('export_xml') %}
  • XML
  • {% endif %} + {% if craue_setting('export_md') %}
  • Markdown
  • {% endif %} diff --git a/tests/Controller/ExportControllerTest.php b/tests/Controller/ExportControllerTest.php index ffe2aa42a..bb8e6995f 100644 --- a/tests/Controller/ExportControllerTest.php +++ b/tests/Controller/ExportControllerTest.php @@ -307,6 +307,22 @@ class ExportControllerTest extends WallabagTestCase $this->assertNotEmpty('updated_at', (string) $content->entry[0]->updated_at); } + public function testMdExport() + { + $this->logInAs('admin'); + $client = $this->getTestClient(); + $client->request('GET', '/export/all.md'); + + $this->assertSame(200, $client->getResponse()->getStatusCode()); + + $headers = $client->getResponse()->headers; + $content = $client->getResponse()->getContent(); + $this->assertSame('text/markdown; charset=UTF-8', $headers->get('content-type')); + $this->assertSame('attachment; filename="All articles.md"', $headers->get('content-disposition')); + $this->assertSame('UTF-8', $headers->get('content-transfer-encoding')); + $this->assertStringContainsString('=================', $content); + } + public function testJsonExportFromSameDomain() { $this->logInAs('admin');