Add Markdown export

This commit is contained in:
Nicolas Lœuillet 2024-11-12 15:21:23 +01:00
parent 2560826b41
commit 2a382b15c1
9 changed files with 170 additions and 22 deletions

View file

@ -114,6 +114,10 @@ parameters:
name: export_xml name: export_xml
value: 1 value: 1
section: export section: export
-
name: export_md
value: 1
section: export
- -
name: import_with_redis name: import_with_redis
value: 0 value: 0

View file

@ -87,6 +87,7 @@
"jms/serializer-bundle": "^5.4", "jms/serializer-bundle": "^5.4",
"laminas/laminas-code": "^4.7.1", "laminas/laminas-code": "^4.7.1",
"lcobucci/jwt": "^4.3", "lcobucci/jwt": "^4.3",
"league/html-to-markdown": "^5.1",
"mgargano/simplehtmldom": "^1.5", "mgargano/simplehtmldom": "^1.5",
"mnapoli/piwik-twig-extension": "^3.0", "mnapoli/piwik-twig-extension": "^3.0",
"monolog/monolog": "^2.9", "monolog/monolog": "^2.9",

93
composer.lock generated
View file

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically" "This file is @generated automatically"
], ],
"content-hash": "6407ed5fbd4b0973ed565c2a136d3a81", "content-hash": "a45ce1bad60f024c66e17b6aca7ad88d",
"packages": [ "packages": [
{ {
"name": "babdev/pagerfanta-bundle", "name": "babdev/pagerfanta-bundle",
@ -5303,6 +5303,95 @@
], ],
"time": "2023-01-02T13:28:00+00:00" "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", "name": "masterminds/html5",
"version": "2.9.0", "version": "2.9.0",
@ -20034,7 +20123,7 @@
"ext-tokenizer": "*", "ext-tokenizer": "*",
"ext-xml": "*" "ext-xml": "*"
}, },
"platform-dev": [], "platform-dev": {},
"platform-overrides": { "platform-overrides": {
"php": "7.4.29" "php": "7.4.29"
}, },

View file

@ -0,0 +1,24 @@
<?php
declare(strict_types=1);
namespace Application\Migrations;
use Doctrine\DBAL\Schema\Schema;
use Wallabag\Doctrine\WallabagMigration;
/**
* Added the internal setting to export articles in markdown.
*/
final class Version20241112193044 extends WallabagMigration
{
public function up(Schema $schema): void
{
$this->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';");
}
}

View file

@ -20,7 +20,7 @@ class ExportController extends AbstractController
* Gets one entry content. * Gets one entry content.
* *
* @Route("/export/{id}.{format}", name="export_entry", requirements={ * @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+" * "id": "\d+"
* }) * })
* *
@ -54,7 +54,7 @@ class ExportController extends AbstractController
* Export all entries for current user. * Export all entries for current user.
* *
* @Route("/export/{category}.{format}", name="export_entries", requirements={ * @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" * "category": "all|unread|starred|archive|tag_entries|untagged|search|annotated|same_domain"
* }) * })
* *

View file

@ -5,6 +5,7 @@ namespace Wallabag\Helper;
use Html2Text\Html2Text; use Html2Text\Html2Text;
use JMS\Serializer\SerializationContext; use JMS\Serializer\SerializationContext;
use JMS\Serializer\SerializerBuilder; use JMS\Serializer\SerializerBuilder;
use League\HTMLToMarkdown\HtmlConverter;
use PHPePub\Core\EPub; use PHPePub\Core\EPub;
use PHPePub\Core\Structure\OPF\DublinCore; use PHPePub\Core\Structure\OPF\DublinCore;
use Symfony\Component\HttpFoundation\Response; use Symfony\Component\HttpFoundation\Response;
@ -129,10 +130,8 @@ class EntriesExport
/** /**
* Use PHPePub to dump a .epub file. * 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; $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
\assert($user instanceof User); \assert($user instanceof User);
@ -249,10 +248,8 @@ class EntriesExport
/** /**
* Use TCPDF to dump a .pdf file. * 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; $user = $this->tokenStorage->getToken() ? $this->tokenStorage->getToken()->getUser() : null;
\assert($user instanceof User); \assert($user instanceof User);
@ -326,10 +323,8 @@ class EntriesExport
/** /**
* Inspired from CsvFileDumper. * Inspired from CsvFileDumper.
*
* @return Response
*/ */
private function produceCsv() private function produceCsv(): Response
{ {
$delimiter = ';'; $delimiter = ';';
$enclosure = '"'; $enclosure = '"';
@ -372,10 +367,8 @@ class EntriesExport
/** /**
* Dump a JSON file. * Dump a JSON file.
*
* @return Response
*/ */
private function produceJson() private function produceJson(): Response
{ {
return Response::create( return Response::create(
$this->prepareSerializingContent('json'), $this->prepareSerializingContent('json'),
@ -390,10 +383,8 @@ class EntriesExport
/** /**
* Dump a XML file. * Dump a XML file.
*
* @return Response
*/ */
private function produceXml() private function produceXml(): Response
{ {
return Response::create( return Response::create(
$this->prepareSerializingContent('xml'), $this->prepareSerializingContent('xml'),
@ -408,10 +399,8 @@ class EntriesExport
/** /**
* Dump a TXT file. * Dump a TXT file.
*
* @return Response
*/ */
private function produceTxt() private function produceTxt(): Response
{ {
$content = ''; $content = '';
$bar = str_repeat('=', 100); $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('<h1>' . $entry->getTitle() . '</h1>' . $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). * Return a Serializer object for producing processes that need it (JSON & XML).
* *

View file

@ -105,6 +105,7 @@
{% if craue_setting('export_csv') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'csv', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">CSV</a></li>{% endif %} {% if craue_setting('export_csv') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'csv', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">CSV</a></li>{% endif %}
{% if craue_setting('export_txt') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'txt', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">TXT</a></li>{% endif %} {% if craue_setting('export_txt') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'txt', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">TXT</a></li>{% endif %}
{% if craue_setting('export_xml') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'xml', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">XML</a></li>{% endif %} {% if craue_setting('export_xml') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'xml', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">XML</a></li>{% endif %}
{% if craue_setting('export_md') %}<li class="bold"><a class="waves-effect" href="{{ path('export_entries', {'category': current_route, 'format': 'md', 'tag': current_tag, 'search_entry[term]': export_search_term, 'currentRoute': previous_route, 'entry': entry}) }}">Markdown</a></li>{% endif %}
</ul> </ul>
</div> </div>

View file

@ -242,6 +242,7 @@
{% if craue_setting('export_json') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'json'}) }}" title="Generate JSON file">JSON</a></li>{% endif %} {% if craue_setting('export_json') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'json'}) }}" title="Generate JSON file">JSON</a></li>{% endif %}
{% if craue_setting('export_txt') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'txt'}) }}" title="Generate TXT file">TXT</a></li>{% endif %} {% if craue_setting('export_txt') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'txt'}) }}" title="Generate TXT file">TXT</a></li>{% endif %}
{% if craue_setting('export_xml') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'xml'}) }}" title="Generate XML file">XML</a></li>{% endif %} {% if craue_setting('export_xml') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'xml'}) }}" title="Generate XML file">XML</a></li>{% endif %}
{% if craue_setting('export_md') %}<li><a href="{{ path('export_entry', {'id': entry.id, 'format': 'md'}) }}" title="Generate MD file">Markdown</a></li>{% endif %}
</ul> </ul>
</div> </div>
</li> </li>

View file

@ -307,6 +307,22 @@ class ExportControllerTest extends WallabagTestCase
$this->assertNotEmpty('updated_at', (string) $content->entry[0]->updated_at); $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() public function testJsonExportFromSameDomain()
{ {
$this->logInAs('admin'); $this->logInAs('admin');