reader 3.22__tar.gz → 3.23__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {reader-3.22 → reader-3.23}/CHANGES.rst +16 -0
- {reader-3.22/src/reader.egg-info → reader-3.23}/PKG-INFO +2 -1
- {reader-3.22 → reader-3.23}/README.rst +1 -0
- {reader-3.22 → reader-3.23}/docs/api.rst +21 -0
- {reader-3.22 → reader-3.23}/docs/app.rst +12 -0
- {reader-3.22 → reader-3.23}/docs/conf.py +0 -2
- {reader-3.22 → reader-3.23}/docs/dev.rst +0 -1
- {reader-3.22 → reader-3.23}/docs/guide.rst +52 -23
- reader-3.23/docs/screenshots/feeds.png +0 -0
- reader-3.23/docs/screenshots/import-result.png +0 -0
- reader-3.23/docs/screenshots/import-select.png +0 -0
- {reader-3.22 → reader-3.23}/examples/podcast.py +0 -1
- {reader-3.22 → reader-3.23}/pyproject.toml +0 -1
- {reader-3.22 → reader-3.23}/scripts/bench.py +0 -1
- {reader-3.22 → reader-3.23}/scripts/debug_storage_stats.py +0 -1
- {reader-3.22 → reader-3.23}/scripts/entry_dedupe_backtest.py +0 -1
- {reader-3.22 → reader-3.23}/scripts/jscontrols.py +0 -2
- {reader-3.22 → reader-3.23}/scripts/vendor_app_stuff.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/__init__.py +7 -3
- {reader-3.22 → reader-3.23}/src/reader/__main__.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_app/__init__.py +36 -1
- {reader-3.22 → reader-3.23}/src/reader/_app/ext.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/__init__.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/wsgi.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/add_feed.html +9 -1
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/feeds.html +8 -1
- reader-3.23/src/reader/_app/templates/import_feeds.html +121 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/layout_base.html +3 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/macros.html +22 -7
- {reader-3.22 → reader-3.23}/src/reader/_app/wsgi.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_cli.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_hash_utils.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_parser/__init__.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_parser/_http_utils.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_parser/_lazy.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_parser/feedparser.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_parser/jsonfeed.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_parser/requests/__init__.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_parser/requests/_lazy.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/enclosure_tags.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/preview_feed_list.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/share.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_plugins/sqlite_releases.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_storage/__init__.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_storage/_base.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_storage/_changes.py +2 -5
- {reader-3.22 → reader-3.23}/src/reader/_storage/_entries.py +7 -12
- {reader-3.22 → reader-3.23}/src/reader/_storage/_feeds.py +1 -2
- {reader-3.22 → reader-3.23}/src/reader/_storage/_html_utils.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_storage/_schema.py +4 -9
- {reader-3.22 → reader-3.23}/src/reader/_storage/_search.py +13 -22
- {reader-3.22 → reader-3.23}/src/reader/_storage/_sql_utils.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_storage/_sqlite_utils.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_storage/_tags.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_types.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_update.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/_utils.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/core.py +79 -1
- {reader-3.22 → reader-3.23}/src/reader/exceptions.py +8 -0
- reader-3.23/src/reader/opml.py +178 -0
- {reader-3.22 → reader-3.23}/src/reader/plugins/__init__.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/plugins/_loader.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/plugins/entry_dedupe.py +2 -5
- {reader-3.22 → reader-3.23}/src/reader/plugins/mark_as_read.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/plugins/readtime.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/plugins/ua_fallback.py +0 -1
- {reader-3.22 → reader-3.23}/src/reader/types.py +70 -1
- {reader-3.22 → reader-3.23}/src/reader/utils.py +16 -2
- {reader-3.22 → reader-3.23/src/reader.egg-info}/PKG-INFO +2 -1
- {reader-3.22 → reader-3.23}/src/reader.egg-info/SOURCES.txt +5 -0
- {reader-3.22 → reader-3.23}/tests/data/10.json.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/empty.atom.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/empty.json.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/empty.rss.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/full.atom.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/full.json.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/full.rss.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/invalid.json.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/relative.atom.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/relative.rss.py +0 -1
- {reader-3.22 → reader-3.23}/tests/data/unknown.json.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test__types.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test__utils.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_app_legacy.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_bench.py +0 -2
- {reader-3.22 → reader-3.23}/tests/test_changes.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_config_utils.py +2 -4
- {reader-3.22 → reader-3.23}/tests/test_html_utils.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_lazy_imports.py +2 -5
- reader-3.23/tests/test_opml.py +153 -0
- {reader-3.22 → reader-3.23}/tests/test_plugins_entry_dedupe.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_plugins_mark_as_read.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_plugins_preview_feed_list.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_plugins_readtime.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_reader_deprecations.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_reader_filter.py +25 -0
- {reader-3.22 → reader-3.23}/tests/test_reader_hooks.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_reader_lifecycle.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_reader_private.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_reader_search.py +2 -7
- {reader-3.22 → reader-3.23}/tests/test_reader_sort.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_reader_update.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_reader_utils.py +14 -2
- {reader-3.22 → reader-3.23}/tests/test_search.py +0 -1
- {reader-3.22 → reader-3.23}/tests/test_sql_utils.py +12 -22
- {reader-3.22 → reader-3.23}/tests/test_sqlite_utils.py +0 -1
- reader-3.22/docs/screenshots/feeds.png +0 -0
- {reader-3.22 → reader-3.23}/LICENSE +0 -0
- {reader-3.22 → reader-3.23}/MANIFEST.in +0 -0
- {reader-3.22 → reader-3.23}/docs/changelog.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/cli.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/config.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/contributing.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/index.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/install.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/internal.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/plugins.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/screenshots/entries-filters-light.png +0 -0
- {reader-3.22 → reader-3.23}/docs/screenshots/entries.png +0 -0
- {reader-3.22 → reader-3.23}/docs/screenshots/entry-image-light.png +0 -0
- {reader-3.22 → reader-3.23}/docs/screenshots/entry.png +0 -0
- {reader-3.22 → reader-3.23}/docs/screenshots/feed.png +0 -0
- {reader-3.22 → reader-3.23}/docs/tutorial.rst +0 -0
- {reader-3.22 → reader-3.23}/docs/why.rst +0 -0
- {reader-3.22 → reader-3.23}/examples/config.toml +0 -0
- {reader-3.22 → reader-3.23}/examples/custom_headers.py +0 -0
- {reader-3.22 → reader-3.23}/examples/feed_slugs.py +0 -0
- {reader-3.22 → reader-3.23}/examples/parser_only.py +0 -0
- {reader-3.22 → reader-3.23}/examples/terminal.py +0 -0
- {reader-3.22 → reader-3.23}/run.sh +0 -0
- {reader-3.22 → reader-3.23}/scripts/backup.sh +0 -0
- {reader-3.22 → reader-3.23}/scripts/jscontrols.html +0 -0
- {reader-3.22 → reader-3.23}/scripts/lines.sh +0 -0
- {reader-3.22 → reader-3.23}/scripts/release.py +0 -0
- {reader-3.22 → reader-3.23}/setup.cfg +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/cli.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/forms.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/api_thing.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/static/controls.js +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/static/style.css +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/add_entry.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/entries.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/entry.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/feeds.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/layout.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/macros.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/metadata.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/legacy/templates/tags.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/style.css +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/theme.js +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/README +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/bootstrap-icons.min.css +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap-icons@1.13.1/font/fonts/bootstrap-icons.woff2 +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/css/bootstrap.min.css +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/bootstrap@5.3.8/dist/js/bootstrap.bundle.min.js +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/htmx-ext-response-targets@2.0.4/dist/response-targets.min.js +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/static/vendor/htmx.org@2.0.8/dist/htmx.min.js +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/change_feed_title.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/delete_feed.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/entries.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/entry.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_app/templates/layout.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_config_utils.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_parser/_url_utils.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_parser/file.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_parser/http.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_plugins/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_plugins/cli_status.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_plugins/legacy/templates/preview_feed_list.html +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_plugins/timer.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/api.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/asctime.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/greek.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/hungarian.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/iso8601.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/korean.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/perforce.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/rfc822.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/datetimes/w3dtf.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/encodings.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/exceptions.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/html.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/http.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/mixin.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/_base.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/admin.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/cc.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/dc.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/georss.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/itunes.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/mediarss.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/namespaces/psc.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/json.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/loose.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/parsers/strict.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/py.typed +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/sanitizer.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/sgml.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/urls.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/_vendor/feedparser/util.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/plugins/enclosure_dedupe.py +0 -0
- {reader-3.22 → reader-3.23}/src/reader/py.typed +0 -0
- {reader-3.22 → reader-3.23}/src/reader.egg-info/dependency_links.txt +0 -0
- {reader-3.22 → reader-3.23}/src/reader.egg-info/requires.txt +0 -0
- {reader-3.22 → reader-3.23}/src/reader.egg-info/top_level.txt +0 -0
- {reader-3.22 → reader-3.23}/tests/_plugins/__init__.py +0 -0
- {reader-3.22 → reader-3.23}/tests/_plugins/builtin.py +0 -0
- {reader-3.22 → reader-3.23}/tests/_plugins/error_during_import.py +0 -0
- {reader-3.22 → reader-3.23}/tests/_plugins/error_import_error.py +0 -0
- {reader-3.22 → reader-3.23}/tests/conftest.py +0 -0
- {reader-3.22 → reader-3.23}/tests/data/10.json +0 -0
- {reader-3.22 → reader-3.23}/tests/data/custom +0 -0
- {reader-3.22 → reader-3.23}/tests/data/empty.atom +0 -0
- {reader-3.22 → reader-3.23}/tests/data/empty.json +0 -0
- {reader-3.22 → reader-3.23}/tests/data/empty.rss +0 -0
- {reader-3.22 → reader-3.23}/tests/data/full.atom +0 -0
- {reader-3.22 → reader-3.23}/tests/data/full.json +0 -0
- {reader-3.22 → reader-3.23}/tests/data/full.rss +0 -0
- {reader-3.22 → reader-3.23}/tests/data/invalid.json +0 -0
- {reader-3.22 → reader-3.23}/tests/data/relative.atom +0 -0
- {reader-3.22 → reader-3.23}/tests/data/relative.rss +0 -0
- {reader-3.22 → reader-3.23}/tests/data/sqlite_releases.html +0 -0
- {reader-3.22 → reader-3.23}/tests/data/unknown.json +0 -0
- {reader-3.22 → reader-3.23}/tests/fakeparser.py +0 -0
- {reader-3.22 → reader-3.23}/tests/reader_methods.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_app_wsgi.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_cli.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_exceptions.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_hash_utils.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_parser.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_plugins_cli_status.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_plugins_enclosure_dedupe.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_plugins_loader.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_plugins_sqlite_releases.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_plugins_ua_fallback.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_reader.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_reader_counts.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_reader_integration.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_storage.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_tags.py +0 -0
- {reader-3.22 → reader-3.23}/tests/test_types.py +0 -0
- {reader-3.22 → reader-3.23}/tests/utils.py +0 -0
- {reader-3.22 → reader-3.23}/tox.ini +0 -0
|
@@ -6,6 +6,22 @@ Changelog
|
|
|
6
6
|
:no-index:
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
Version 3.23
|
|
10
|
+
------------
|
|
11
|
+
|
|
12
|
+
Released 2026-04-24
|
|
13
|
+
|
|
14
|
+
* Add support for :ref:`opml`. (:issue:`165`)
|
|
15
|
+
|
|
16
|
+
* Add support for import / export in the web app; :ref:`screenshots <screenshots-import>`.
|
|
17
|
+
|
|
18
|
+
* Deprecate the :func:`~reader.utils.archive_entries` ``feed_user_title`` argument.
|
|
19
|
+
* Fix :meth:`get_feeds(feed='') <Reader.get_feeds>` and
|
|
20
|
+
:meth:`get_entries(entry=('', '')) <Reader.get_entries>`
|
|
21
|
+
returning wrong entries.
|
|
22
|
+
(:issue:`392`)
|
|
23
|
+
|
|
24
|
+
|
|
9
25
|
Version 3.22
|
|
10
26
|
------------
|
|
11
27
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: reader
|
|
3
|
-
Version: 3.
|
|
3
|
+
Version: 3.23
|
|
4
4
|
Summary: A Python feed reader library.
|
|
5
5
|
Author: lemon24
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -113,6 +113,7 @@ you'd probably call it a fat model.
|
|
|
113
113
|
* filter feeds and articles
|
|
114
114
|
* full-text search articles
|
|
115
115
|
* get statistics on feed and user activity
|
|
116
|
+
* import / export feeds as OPML
|
|
116
117
|
* write plugins to extend its functionality
|
|
117
118
|
* skip all the low level stuff and focus on what makes your feed reader different
|
|
118
119
|
|
|
@@ -49,6 +49,7 @@ you'd probably call it a fat model.
|
|
|
49
49
|
* filter feeds and articles
|
|
50
50
|
* full-text search articles
|
|
51
51
|
* get statistics on feed and user activity
|
|
52
|
+
* import / export feeds as OPML
|
|
52
53
|
* write plugins to extend its functionality
|
|
53
54
|
* skip all the low level stuff and focus on what makes your feed reader different
|
|
54
55
|
|
|
@@ -67,6 +67,17 @@ Data objects
|
|
|
67
67
|
.. autoclass:: EntryUpdateStatus
|
|
68
68
|
:members:
|
|
69
69
|
|
|
70
|
+
.. autoclass:: FeedToImport
|
|
71
|
+
:members:
|
|
72
|
+
:undoc-members:
|
|
73
|
+
|
|
74
|
+
.. autoclass:: FeedImportResult
|
|
75
|
+
:members:
|
|
76
|
+
|
|
77
|
+
.. autoclass:: FeedExport
|
|
78
|
+
:members:
|
|
79
|
+
|
|
80
|
+
|
|
70
81
|
|
|
71
82
|
Exceptions
|
|
72
83
|
----------
|
|
@@ -155,6 +166,9 @@ Exceptions
|
|
|
155
166
|
.. autoexception:: PluginInitError
|
|
156
167
|
:show-inheritance:
|
|
157
168
|
|
|
169
|
+
.. autoexception:: FeedImportError
|
|
170
|
+
:show-inheritance:
|
|
171
|
+
|
|
158
172
|
|
|
159
173
|
.. autoexception:: ReaderWarning
|
|
160
174
|
:show-inheritance:
|
|
@@ -214,3 +228,10 @@ Utilities
|
|
|
214
228
|
---------
|
|
215
229
|
|
|
216
230
|
.. autofunction:: reader.utils.archive_entries
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
``reader.opml``
|
|
235
|
+
---------------
|
|
236
|
+
|
|
237
|
+
.. automodule:: reader.opml
|
|
@@ -62,6 +62,18 @@ Screenshots
|
|
|
62
62
|
|
|
63
63
|
article view (light mode)
|
|
64
64
|
|
|
65
|
+
.. _screenshots-import:
|
|
66
|
+
|
|
67
|
+
.. figure:: screenshots/import-select.png
|
|
68
|
+
:width: 240px
|
|
69
|
+
|
|
70
|
+
import feeds (select)
|
|
71
|
+
|
|
72
|
+
.. figure:: screenshots/import-result.png
|
|
73
|
+
:width: 240px
|
|
74
|
+
|
|
75
|
+
import feeds (result)
|
|
76
|
+
|
|
65
77
|
|
|
66
78
|
|
|
67
79
|
Serving the web app
|
|
@@ -6,7 +6,6 @@ import unittest.mock
|
|
|
6
6
|
import packaging.version
|
|
7
7
|
from setuptools.config.pyprojecttoml import read_configuration
|
|
8
8
|
|
|
9
|
-
|
|
10
9
|
sys.path.insert(0, os.path.abspath('../src'))
|
|
11
10
|
|
|
12
11
|
# mock some things "by hand", so we can import reader below without any dependencies
|
|
@@ -25,7 +24,6 @@ for name in [
|
|
|
25
24
|
|
|
26
25
|
import reader
|
|
27
26
|
|
|
28
|
-
|
|
29
27
|
extensions = [
|
|
30
28
|
'sphinx_rtd_theme',
|
|
31
29
|
'sphinx.ext.autodoc',
|
|
@@ -32,7 +32,6 @@ in my spare time,
|
|
|
32
32
|
but I will prioritize supporting :doc:`contributors <contributing>`
|
|
33
33
|
(discussions, reviews and so on).
|
|
34
34
|
|
|
35
|
-
* OPML support, :issue:`165`
|
|
36
35
|
* :ref:`deleting entries <deleting entries>`
|
|
37
36
|
|
|
38
37
|
* archiving important entries of deleted feeds, :issue:`290`
|
|
@@ -705,35 +705,32 @@ you can delete entries using the low-level
|
|
|
705
705
|
|
|
706
706
|
|
|
707
707
|
|
|
708
|
-
..
|
|
708
|
+
.. _opml:
|
|
709
709
|
|
|
710
|
-
|
|
711
|
-
|
|
710
|
+
OPML import / export
|
|
711
|
+
--------------------
|
|
712
712
|
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
713
|
+
You can import and export feeds from and to an `OPML subscription list`_
|
|
714
|
+
by using the :meth:`~Reader.import_feeds`
|
|
715
|
+
and :meth:`~Reader.export_feeds` methods::
|
|
716
716
|
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
>>> for entry in reader.get_entries(limit=2):
|
|
724
|
-
... print(entry.title)
|
|
717
|
+
>>> export = reader.export_feeds()
|
|
718
|
+
>>> export.filename
|
|
719
|
+
'reader-feeds-2026-04-24-10-00-00.opml'
|
|
720
|
+
>>> with open(export.filename, 'wb') as f:
|
|
721
|
+
... f.write(export.content)
|
|
725
722
|
...
|
|
726
|
-
|
|
727
|
-
|
|
723
|
+
>>> # with another reader instance
|
|
724
|
+
>>> reader = make_reader(':memory:')
|
|
725
|
+
>>> with open(export.filename, 'rb') as f:
|
|
726
|
+
... reader.import_feeds(f)
|
|
728
727
|
|
|
729
|
-
|
|
730
|
-
|
|
728
|
+
For more detailed control over which feeds are imported,
|
|
729
|
+
see :meth:`~Reader.import_feeds_iter` and the :mod:`reader.opml` module.
|
|
731
730
|
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
# H.I. 134: Boxing Day
|
|
736
|
-
Star Wars: The Rise of Skywalker, Hello Internet Christmas Special
|
|
731
|
+
.. _OPML subscription list: https://opml.org/spec2.opml#subscriptionLists
|
|
732
|
+
|
|
733
|
+
.. versionadded:: 3.23
|
|
737
734
|
|
|
738
735
|
|
|
739
736
|
|
|
@@ -831,6 +828,38 @@ Some examples of how this is useful:
|
|
|
831
828
|
|
|
832
829
|
|
|
833
830
|
|
|
831
|
+
.. _pagination:
|
|
832
|
+
|
|
833
|
+
Pagination
|
|
834
|
+
----------
|
|
835
|
+
|
|
836
|
+
:meth:`~Reader.get_feeds`, :meth:`~Reader.get_entries`,
|
|
837
|
+
and :meth:`~Reader.search_entries`
|
|
838
|
+
can be used in a paginated fashion.
|
|
839
|
+
|
|
840
|
+
The ``limit`` argument allows limiting the number of results returned;
|
|
841
|
+
the ``starting_after`` argument allows skipping results until after
|
|
842
|
+
a specific one.
|
|
843
|
+
|
|
844
|
+
To get the first page, use only ``limit``::
|
|
845
|
+
|
|
846
|
+
>>> for entry in reader.get_entries(limit=2):
|
|
847
|
+
... print(entry.title)
|
|
848
|
+
...
|
|
849
|
+
H.I. #136: Dog Bingo
|
|
850
|
+
H.I. #135: Place Your Bets
|
|
851
|
+
|
|
852
|
+
To get the next page, use the last result from a call as
|
|
853
|
+
``starting_after`` in the next call::
|
|
854
|
+
|
|
855
|
+
>>> for entry in reader.get_entries(limit=2, starting_after=entry):
|
|
856
|
+
... print(entry.title)
|
|
857
|
+
...
|
|
858
|
+
# H.I. 134: Boxing Day
|
|
859
|
+
Star Wars: The Rise of Skywalker, Hello Internet Christmas Special
|
|
860
|
+
|
|
861
|
+
|
|
862
|
+
|
|
834
863
|
.. _reserved names:
|
|
835
864
|
|
|
836
865
|
Reserved names
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -8,14 +8,12 @@ from flask import jsonify
|
|
|
8
8
|
from flask import redirect
|
|
9
9
|
from flask import request
|
|
10
10
|
|
|
11
|
-
|
|
12
11
|
root_dir = os.path.dirname(__file__)
|
|
13
12
|
sys.path.insert(0, os.path.join(root_dir, '../src'))
|
|
14
13
|
|
|
15
14
|
from reader._app.api_thing import APIError
|
|
16
15
|
from reader._app.api_thing import APIThing
|
|
17
16
|
|
|
18
|
-
|
|
19
17
|
app = Flask(
|
|
20
18
|
__name__,
|
|
21
19
|
template_folder='../src/reader/_app/templates',
|
|
@@ -37,7 +37,7 @@ Print the titles of the unread entries::
|
|
|
37
37
|
|
|
38
38
|
"""
|
|
39
39
|
|
|
40
|
-
__version__ = '3.
|
|
40
|
+
__version__ = '3.23'
|
|
41
41
|
|
|
42
42
|
# isort: off
|
|
43
43
|
|
|
@@ -64,6 +64,9 @@ from .types import (
|
|
|
64
64
|
UpdateResult as UpdateResult,
|
|
65
65
|
UpdatedFeed as UpdatedFeed,
|
|
66
66
|
EntryUpdateStatus as EntryUpdateStatus,
|
|
67
|
+
FeedToImport as FeedToImport,
|
|
68
|
+
FeedImportResult as FeedImportResult,
|
|
69
|
+
FeedExport as FeedExport,
|
|
67
70
|
)
|
|
68
71
|
|
|
69
72
|
from .exceptions import (
|
|
@@ -91,12 +94,13 @@ from .exceptions import (
|
|
|
91
94
|
InvalidPluginError as InvalidPluginError,
|
|
92
95
|
PluginInitError as PluginInitError,
|
|
93
96
|
ReaderWarning as ReaderWarning,
|
|
97
|
+
FeedImportError as FeedImportError,
|
|
94
98
|
)
|
|
95
99
|
|
|
96
|
-
|
|
97
100
|
# Constants.
|
|
98
101
|
|
|
99
|
-
|
|
102
|
+
SOURCE_URL = 'https://github.com/lemon24/reader'
|
|
103
|
+
USER_AGENT = f'python-reader/{__version__} (+{SOURCE_URL})'
|
|
100
104
|
|
|
101
105
|
|
|
102
106
|
# Prevent any logging output by default. If no handler is set,
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import json
|
|
1
2
|
import pathlib
|
|
3
|
+
from dataclasses import asdict
|
|
2
4
|
from datetime import datetime
|
|
3
5
|
from datetime import timedelta
|
|
4
6
|
from datetime import timezone
|
|
@@ -28,7 +30,9 @@ from werkzeug.exceptions import NotFound
|
|
|
28
30
|
from reader import EntryNotFoundError
|
|
29
31
|
from reader import FeedExistsError
|
|
30
32
|
from reader import FeedNotFoundError
|
|
33
|
+
from reader import FeedToImport
|
|
31
34
|
from reader import InvalidFeedURLError
|
|
35
|
+
from reader import opml
|
|
32
36
|
from reader import UpdateError
|
|
33
37
|
|
|
34
38
|
from .ext import get_reader
|
|
@@ -38,7 +42,6 @@ from .forms import ChangeFeedTitle
|
|
|
38
42
|
from .forms import EntryFilter
|
|
39
43
|
from .forms import FeedFilter
|
|
40
44
|
|
|
41
|
-
|
|
42
45
|
# for a prototype with tags and search support, see
|
|
43
46
|
# https://github.com/lemon24/reader/tree/3.21/src/reader/_app/v2
|
|
44
47
|
|
|
@@ -164,6 +167,10 @@ def feeds():
|
|
|
164
167
|
if form.validate():
|
|
165
168
|
feeds = reader.get_feeds(**kwargs)
|
|
166
169
|
|
|
170
|
+
if request.args.get('format', '').lower() == 'opml':
|
|
171
|
+
export = reader.export_feeds(feeds)
|
|
172
|
+
return export.content, export.headers
|
|
173
|
+
|
|
167
174
|
return stream_template(
|
|
168
175
|
'feeds.html',
|
|
169
176
|
form=form,
|
|
@@ -270,6 +277,34 @@ def add_feed():
|
|
|
270
277
|
return render_template('add_feed.html', form=form)
|
|
271
278
|
|
|
272
279
|
|
|
280
|
+
@blueprint.route('/feeds/import', methods=['GET', 'POST'])
|
|
281
|
+
def import_feeds():
|
|
282
|
+
reader = get_reader()
|
|
283
|
+
parsed_feeds = None
|
|
284
|
+
error = None
|
|
285
|
+
imported_feeds = None
|
|
286
|
+
|
|
287
|
+
if request.method == 'POST':
|
|
288
|
+
if file := request.files.get('file'):
|
|
289
|
+
try:
|
|
290
|
+
parsed_feeds = [asdict(f) for f in opml.parse(file.stream)]
|
|
291
|
+
except opml.OPMLError as e:
|
|
292
|
+
error = str(e)
|
|
293
|
+
else:
|
|
294
|
+
feeds = (
|
|
295
|
+
FeedToImport(**json.loads(f)) for f in request.form.getlist('feed')
|
|
296
|
+
)
|
|
297
|
+
imported_feeds = list(reader.import_feeds_iter(feeds))
|
|
298
|
+
# TODO: out of band update (unlike add, there's too many to do here)
|
|
299
|
+
|
|
300
|
+
return render_template(
|
|
301
|
+
'import_feeds.html',
|
|
302
|
+
parsed_feeds=parsed_feeds,
|
|
303
|
+
error=error,
|
|
304
|
+
imported_feeds=imported_feeds,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
|
|
273
308
|
@blueprint.route('/entry')
|
|
274
309
|
def entry():
|
|
275
310
|
reader = get_reader()
|
|
@@ -12,7 +12,9 @@
|
|
|
12
12
|
|
|
13
13
|
<h1>Add feed</h1>
|
|
14
14
|
|
|
15
|
-
<
|
|
15
|
+
<p>Add a single feed:</p>
|
|
16
|
+
|
|
17
|
+
<form action="{{ url_for(request.endpoint) }}" method="post" class="my-2 mb-4">
|
|
16
18
|
{#- TODO: aria stuff #}
|
|
17
19
|
|
|
18
20
|
{{ form.csrf_token }}
|
|
@@ -43,4 +45,10 @@
|
|
|
43
45
|
|
|
44
46
|
</form>
|
|
45
47
|
|
|
48
|
+
<p>
|
|
49
|
+
...or
|
|
50
|
+
<a href="{{ url_for('.import_feeds') }}">import feeds</a>
|
|
51
|
+
from an OPML subscription list.
|
|
52
|
+
</p>
|
|
53
|
+
|
|
46
54
|
{% endblock %}
|
|
@@ -12,7 +12,14 @@
|
|
|
12
12
|
|
|
13
13
|
{{ macros.flashed_messages() }}
|
|
14
14
|
|
|
15
|
-
<h1>
|
|
15
|
+
<h1>
|
|
16
|
+
<span class="me-1">
|
|
17
|
+
Feeds
|
|
18
|
+
</span>
|
|
19
|
+
<a href="{{ url_for(request.endpoint, format='opml', **request.args) }}" class="small" title="export as OPML">
|
|
20
|
+
<i class="bi bi-filetype-xml text-secondary"></i>
|
|
21
|
+
</a>
|
|
22
|
+
</h1>
|
|
16
23
|
|
|
17
24
|
|
|
18
25
|
{% call macros.filter_form(form) %}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
{% extends "layout.html" %}
|
|
2
|
+
|
|
3
|
+
{% import "macros.html" as macros %}
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
{% block page_title %}
|
|
7
|
+
Import feeds - {{ super() }}
|
|
8
|
+
{% endblock %}
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
{% block body %}
|
|
12
|
+
|
|
13
|
+
<h1>Import feeds</h1>
|
|
14
|
+
|
|
15
|
+
<form
|
|
16
|
+
action="{{ url_for(request.endpoint) }}"
|
|
17
|
+
method="post"
|
|
18
|
+
enctype="multipart/form-data"
|
|
19
|
+
class="my-2 mb-4"
|
|
20
|
+
>
|
|
21
|
+
{#- TODO: aria stuff #}
|
|
22
|
+
|
|
23
|
+
{{ macros.csrf_token_field() }}
|
|
24
|
+
|
|
25
|
+
{% if parsed_feeds is none and imported_feeds is none %}
|
|
26
|
+
|
|
27
|
+
<p>Upload OPML subscription list:</p>
|
|
28
|
+
|
|
29
|
+
<div class="mb-3">
|
|
30
|
+
|
|
31
|
+
<!-- TODO accept -->
|
|
32
|
+
<input
|
|
33
|
+
class="form-control{% if error %} is-invalid {% endif %}"
|
|
34
|
+
type="file"
|
|
35
|
+
name="file"
|
|
36
|
+
>
|
|
37
|
+
{%- if error %}
|
|
38
|
+
<div class="invalid-feedback">
|
|
39
|
+
{{ error }}
|
|
40
|
+
</div>
|
|
41
|
+
{%- endif %}
|
|
42
|
+
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
<input type="submit" class="btn btn-primary" value="upload file">
|
|
46
|
+
|
|
47
|
+
<a href="javascript:history.back()" class="btn btn-outline-secondary" role="button" >
|
|
48
|
+
cancel
|
|
49
|
+
</a>
|
|
50
|
+
|
|
51
|
+
{% elif parsed_feeds is not none %}
|
|
52
|
+
|
|
53
|
+
<p>Select feeds to import:</p>
|
|
54
|
+
|
|
55
|
+
{% for feed in parsed_feeds %}
|
|
56
|
+
<div class="form-check mb-3">
|
|
57
|
+
<input
|
|
58
|
+
class="form-check-input"
|
|
59
|
+
id="feed-{{ loop.index }}"
|
|
60
|
+
type="checkbox"
|
|
61
|
+
name="feed"
|
|
62
|
+
value='{{ feed | tojson }}'
|
|
63
|
+
checked
|
|
64
|
+
>
|
|
65
|
+
|
|
66
|
+
<label class="form-check-label" for="feed-{{ loop.index }}">
|
|
67
|
+
{#- NOTE similarity to feeds page feeds #}
|
|
68
|
+
<h2 class="h5 mb-1" style="font-size: 1.125rem">
|
|
69
|
+
{{ macros.title_href(
|
|
70
|
+
feed.title or feed.url,
|
|
71
|
+
None,
|
|
72
|
+
external=feed.link,
|
|
73
|
+
feed_url=feed.url,
|
|
74
|
+
) }}
|
|
75
|
+
</h2>
|
|
76
|
+
{{ macros.subtitle(feed.subtitle) }}
|
|
77
|
+
</label>
|
|
78
|
+
|
|
79
|
+
</div>
|
|
80
|
+
{% endfor %}
|
|
81
|
+
|
|
82
|
+
<input type="submit" class="btn btn-primary" value="import selected">
|
|
83
|
+
|
|
84
|
+
<a href="javascript:history.back()" class="btn btn-outline-secondary" role="button" >
|
|
85
|
+
cancel
|
|
86
|
+
</a>
|
|
87
|
+
|
|
88
|
+
{% else %}
|
|
89
|
+
|
|
90
|
+
<p>Imported feeds:</p>
|
|
91
|
+
|
|
92
|
+
{% for result in imported_feeds %}
|
|
93
|
+
{%- set feed = result.feed %}
|
|
94
|
+
<div class="mb-3">
|
|
95
|
+
|
|
96
|
+
{#- NOTE similarity to feeds page feeds #}
|
|
97
|
+
<h2 class="h5 mb-1" style="font-size: 1.125rem">
|
|
98
|
+
{{ macros.title_href(
|
|
99
|
+
feed.title or feed.url,
|
|
100
|
+
url_for('.entries', feed=feed.url) if not result.error else None,
|
|
101
|
+
external=feed.link,
|
|
102
|
+
feed_url=feed.url,
|
|
103
|
+
) }}
|
|
104
|
+
</h2>
|
|
105
|
+
|
|
106
|
+
{% if result.added %}
|
|
107
|
+
{{ macros.subtitle("feed added", 'text-success-emphasis') }}
|
|
108
|
+
{% elif result.error %}
|
|
109
|
+
{{ macros.subtitle("invalid feed: {}".format(result.error), 'text-danger-emphasis') }}
|
|
110
|
+
{% else %}
|
|
111
|
+
{{ macros.subtitle("feed already exists", 'text-secondary') }}
|
|
112
|
+
{% endif %}
|
|
113
|
+
|
|
114
|
+
</div>
|
|
115
|
+
{% endfor %}
|
|
116
|
+
|
|
117
|
+
{% endif %}
|
|
118
|
+
|
|
119
|
+
</form>
|
|
120
|
+
|
|
121
|
+
{% endblock %}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
<!doctype html>
|
|
2
2
|
<html lang="en" data-bs-theme="auto">
|
|
3
3
|
<head>
|
|
4
|
+
{% block head %}
|
|
5
|
+
|
|
4
6
|
<meta charset="utf-8">
|
|
5
7
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
6
8
|
|
|
@@ -18,6 +20,7 @@
|
|
|
18
20
|
<script src="{{ find_static('htmx.min.js', 'reader') }}" defer></script>
|
|
19
21
|
<script src="{{ find_static('response-targets.min.js', 'reader') }}" defer></script>
|
|
20
22
|
|
|
23
|
+
{% endblock %}
|
|
21
24
|
</head>
|
|
22
25
|
<body hx-ext="response-targets">
|
|
23
26
|
|
|
@@ -245,7 +245,7 @@
|
|
|
245
245
|
{% endmacro %}
|
|
246
246
|
|
|
247
247
|
|
|
248
|
-
{% macro title_href(text, href=none, external=none) %}
|
|
248
|
+
{% macro title_href(text, href=none, external=none, feed_url=none) %}
|
|
249
249
|
{%- set text = text | truncate(95) -%}
|
|
250
250
|
|
|
251
251
|
{%- macro maybe_href(text, href) %}
|
|
@@ -254,7 +254,7 @@
|
|
|
254
254
|
{%- if href %}</a>{% endif -%}
|
|
255
255
|
{%- endmacro %}
|
|
256
256
|
|
|
257
|
-
{%- if not external %}
|
|
257
|
+
{%- if not (external or feed_url) %}
|
|
258
258
|
{{ maybe_href(text, href) }}
|
|
259
259
|
{%- else %}
|
|
260
260
|
{%- set first, _, last = text.strip().rpartition(' ') -%}
|
|
@@ -263,19 +263,34 @@
|
|
|
263
263
|
<span class="me-1">
|
|
264
264
|
{{ maybe_href(last, href) }}
|
|
265
265
|
</span>
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
{% if external %}
|
|
267
|
+
<a
|
|
268
|
+
href="{{ external }}"
|
|
269
|
+
target="_blank"
|
|
270
|
+
class="text-nowrap small{% if feed_url %} me-1{% endif %}"
|
|
271
|
+
title="external"
|
|
272
|
+
>
|
|
273
|
+
<i class="bi bi-box-arrow-up-right text-secondary"></i></a>
|
|
274
|
+
{% endif %}
|
|
275
|
+
{% if feed_url %}
|
|
276
|
+
<a
|
|
277
|
+
href="{{ feed_url }}"
|
|
278
|
+
target="_blank"
|
|
279
|
+
class="text-nowrap small"
|
|
280
|
+
title="feed source"
|
|
281
|
+
>
|
|
282
|
+
<i class="bi bi-rss text-secondary"></i></a>
|
|
283
|
+
{% endif %}
|
|
269
284
|
</span>
|
|
270
285
|
{%- endif %}
|
|
271
286
|
|
|
272
287
|
{%- endmacro %}
|
|
273
288
|
|
|
274
289
|
|
|
275
|
-
{% macro subtitle(text) %}
|
|
290
|
+
{% macro subtitle(text, class=none) %}
|
|
276
291
|
{%- set text = (text or '') | striptags | trim -%}
|
|
277
292
|
{%- if text %}
|
|
278
|
-
<p class="small mb-
|
|
293
|
+
<p class="small mb-1{% if class %} {{class}}{% endif %}">
|
|
279
294
|
{{ text | truncate(223) }}
|
|
280
295
|
</p>
|
|
281
296
|
{%- endif %}
|