mirror of
https://github.com/jrnl-org/jrnl.git
synced 2025-07-04 15:36:14 +02:00
Use implicit namespace plugins for import and export (#1216)
* behavior outline
* FIrst pass at allow external plugins
* remove template exporter
* Add listing of active plugins to '--version' output
* Documentation for plugins
* [Docs] add custom imports and exporters to site TOC
* [Docs] better linewrapping
* enforce positive initial linewrap
Check column widths
update gitignore
throw error when linewrap too small
simply check for large enough linewrap value
* delete unused error message
* PR feedback
make exception more informative
update check_linewrap signature in src and test
make check_linewrap a free function
* delete unused function
* delete else..pass block
* newline for make format
* Include dates_exporter
* Use Base classes for importer and exporters.
* [Docs] improve documentation of custom Importers and Exporters
* [Testing] separate run with external plugin!
* basic behavior test
* prototype unittest for JSON Exporter
test for unimplemented method
* make format
delete unused imports
* Remove 'importer' or 'exporter' from filenames where not needed
* [Test] run different tests with or without the external plugins installed
* [Test] move test rot13 plugin into git tree
from 0dc912af82
* consolidate demo plugins to common package
* [Docs] name page for plugins
* [Docs] include the sample plug in code files directly
* style fixes
* [test] determine whether to run external plug in tests based on installed packages
* improved code documentation
* style fixes for GitHub actions
* Convert "short" and "pretty" (and "default") formaters to plugins
further to https://github.com/jrnl-org/jrnl/pull/1177
* more code clean up
tests pass locally...now for GitHub...
* [tests] dynamically determine jrnl version for plugin tests
* [GitHub Actions] direct install of testing plugins
* Remove template code
* [plugins] meta --> collector
* [Docs] create scripted entries using an custom importer
* (closer to) being able to run behave tests outside project root directory
* We already know when exporter to use
Don't re-calculate it!
* [Tests] don't name test plugin 'testing"
If so named, pip won't install it.
* [Test] run behave tests with test plugins outside project root
* [Test] behave tests pass locally
* [Docs] fix typo
* [GitHub Actions] run test commands from poetry's shell
* black-ify code
* [GitHub Actions] move downstream (rather than up) to run tests
* [GitHub Actions] set shell to poetry
* [GitHub Workflows] Manually activate virtual environment
* [GitHub Actions] Skip Windows & Python 3.8
Can't seem to find Python exe?
* [GiotHub Actions] explicitly use virtual env
* [GitHub Actions] create virutal env directly
* [GitHub Actions] better activate of Windows virtual env
* [GitHub Actions] create virtual env on Mac
* [Github Actions] install wheel and upgrade pip
* [GitHub Actions] skip virtual environments altogether
* [GitHub Actions] change directory for behave test
* Remove Windows exclusions from CI as per note -- they should be working now
Co-authored-by: Suhas <sugas182@gmail.com>
Co-authored-by: Micah Jerome Ellison <micah.jerome.ellison@gmail.com>
This commit is contained in:
parent
104fa2caca
commit
1a7e8d8ea2
48 changed files with 1025 additions and 409 deletions
|
@ -12,9 +12,9 @@ from .commands import postconfig_list
|
|||
from .commands import preconfig_diagnostic
|
||||
from .commands import preconfig_version
|
||||
from .output import deprecated_cmd
|
||||
from .plugins import EXPORT_FORMATS
|
||||
from .plugins import IMPORT_FORMATS
|
||||
from .plugins import util
|
||||
from .plugins.collector import EXPORT_FORMATS
|
||||
from .plugins.collector import IMPORT_FORMATS
|
||||
|
||||
|
||||
class WrappingFormatter(argparse.RawTextHelpFormatter):
|
||||
|
|
47
jrnl/behave_testing.py
Normal file
47
jrnl/behave_testing.py
Normal file
|
@ -0,0 +1,47 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
"""
|
||||
Certain functions to support the *behave* test suite.
|
||||
|
||||
They are placed here so they are importable in multiple places, as otherwise
|
||||
imports fail when running the suite outside of the project's root folder.
|
||||
|
||||
"""
|
||||
import jrnl.time
|
||||
|
||||
|
||||
def _mock_getpass(inputs):
|
||||
def prompt_return(prompt=""):
|
||||
if type(inputs) == str:
|
||||
return inputs
|
||||
try:
|
||||
return next(inputs)
|
||||
except StopIteration:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
def _mock_input(inputs):
|
||||
def prompt_return(prompt=""):
|
||||
try:
|
||||
val = next(inputs)
|
||||
print(prompt, val)
|
||||
return val
|
||||
except StopIteration:
|
||||
raise KeyboardInterrupt
|
||||
|
||||
return prompt_return
|
||||
|
||||
|
||||
def _mock_time_parse(context):
|
||||
original_parse = jrnl.time.parse
|
||||
if "now" not in context:
|
||||
return original_parse
|
||||
|
||||
def wrapper(input, *args, **kwargs):
|
||||
input = context.now if input == "now" else input
|
||||
return original_parse(input, *args, **kwargs)
|
||||
|
||||
return wrapper
|
|
@ -28,6 +28,12 @@ def preconfig_diagnostic(_):
|
|||
def preconfig_version(_):
|
||||
from jrnl import __title__
|
||||
from jrnl import __version__
|
||||
from jrnl.plugins.collector import (
|
||||
IMPORT_FORMATS,
|
||||
EXPORT_FORMATS,
|
||||
get_exporter,
|
||||
get_importer,
|
||||
)
|
||||
|
||||
version_str = f"""{__title__} version {__version__}
|
||||
|
||||
|
@ -37,6 +43,22 @@ This is free software, and you are welcome to redistribute it under certain
|
|||
conditions; for details, see: https://www.gnu.org/licenses/gpl-3.0.html"""
|
||||
|
||||
print(version_str)
|
||||
print()
|
||||
print("Active Plugins:")
|
||||
print(" Importers:")
|
||||
for importer in IMPORT_FORMATS:
|
||||
importer_class = get_importer(importer)
|
||||
print(
|
||||
f" {importer} : {importer_class.version} from",
|
||||
f"{importer_class().class_path()}",
|
||||
)
|
||||
print(" Exporters:")
|
||||
for exporter in EXPORT_FORMATS:
|
||||
exporter_class = get_exporter(exporter)
|
||||
# print(f" {exporter} : {exporter_class.version} from {exporter_class().class_path()}")
|
||||
print(f" {exporter} : ", end="")
|
||||
print(f"{exporter_class.version} from ", end="")
|
||||
print(f"{exporter_class().class_path()}")
|
||||
|
||||
|
||||
def postconfig_list(config, **kwargs):
|
||||
|
@ -47,7 +69,7 @@ def postconfig_list(config, **kwargs):
|
|||
|
||||
def postconfig_import(args, config, **kwargs):
|
||||
from .Journal import open_journal
|
||||
from .plugins import get_importer
|
||||
from .plugins.collector import get_importer
|
||||
|
||||
# Requires opening the journal
|
||||
journal = open_journal(args.journal_name, config)
|
||||
|
|
0
jrnl/contrib/exporter/.gitkeep
Normal file
0
jrnl/contrib/exporter/.gitkeep
Normal file
0
jrnl/contrib/importer/.gitkeep
Normal file
0
jrnl/contrib/importer/.gitkeep
Normal file
14
jrnl/jrnl.py
14
jrnl/jrnl.py
|
@ -324,19 +324,17 @@ def _delete_search_results(journal, old_entries, **kwargs):
|
|||
|
||||
def _display_search_results(args, journal, **kwargs):
|
||||
if args.short or args.export == "short":
|
||||
print(journal.pprint(short=True))
|
||||
|
||||
elif args.export == "pretty":
|
||||
print(journal.pprint())
|
||||
print(plugins.collector.get_exporter("short").export(journal))
|
||||
|
||||
elif args.tags:
|
||||
print(plugins.get_exporter("tags").export(journal))
|
||||
print(plugins.collector.get_exporter("tags").export(journal))
|
||||
|
||||
elif args.export:
|
||||
exporter = plugins.get_exporter(args.export)
|
||||
exporter = plugins.collector.get_exporter(args.export)
|
||||
print(exporter.export(journal, args.filename))
|
||||
elif kwargs["config"].get("display_format"):
|
||||
exporter = plugins.get_exporter(kwargs["config"]["display_format"])
|
||||
exporter = plugins.collector.get_exporter(kwargs["config"]["display_format"])
|
||||
print(exporter.export(journal, args.filename))
|
||||
else:
|
||||
print(journal.pprint())
|
||||
# print(journal.pprint())
|
||||
print(plugins.collector.get_exporter("default").export(journal))
|
||||
|
|
|
@ -1,49 +1,5 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from .fancy_exporter import FancyExporter
|
||||
from .jrnl_importer import JRNLImporter
|
||||
from .json_exporter import JSONExporter
|
||||
from .markdown_exporter import MarkdownExporter
|
||||
from .tag_exporter import TagExporter
|
||||
from .dates_exporter import DatesExporter
|
||||
from .template_exporter import __all__ as template_exporters
|
||||
from .text_exporter import TextExporter
|
||||
from .xml_exporter import XMLExporter
|
||||
from .yaml_exporter import YAMLExporter
|
||||
|
||||
__exporters = [
|
||||
JSONExporter,
|
||||
MarkdownExporter,
|
||||
TagExporter,
|
||||
DatesExporter,
|
||||
TextExporter,
|
||||
XMLExporter,
|
||||
YAMLExporter,
|
||||
FancyExporter,
|
||||
] + template_exporters
|
||||
__importers = [JRNLImporter]
|
||||
|
||||
__exporter_types = {name: plugin for plugin in __exporters for name in plugin.names}
|
||||
__exporter_types["pretty"] = None
|
||||
__exporter_types["short"] = None
|
||||
__importer_types = {name: plugin for plugin in __importers for name in plugin.names}
|
||||
|
||||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||
|
||||
|
||||
def get_exporter(format):
|
||||
for exporter in __exporters:
|
||||
if hasattr(exporter, "names") and format in exporter.names:
|
||||
return exporter
|
||||
return None
|
||||
|
||||
|
||||
def get_importer(format):
|
||||
for importer in __importers:
|
||||
if hasattr(importer, "names") and format in importer.names:
|
||||
return importer
|
||||
return None
|
||||
|
|
|
@ -3,6 +3,11 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
"""
|
||||
Base class for Importers and Exporters.
|
||||
"""
|
||||
|
||||
|
||||
import os
|
||||
import re
|
||||
import unicodedata
|
||||
|
@ -11,16 +16,36 @@ from jrnl.color import ERROR_COLOR
|
|||
from jrnl.color import RESET_COLOR
|
||||
|
||||
|
||||
class TextExporter:
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
class BaseImporter:
|
||||
"""Base Importer class (to sub-class)"""
|
||||
|
||||
names = ["text", "txt"]
|
||||
extension = "txt"
|
||||
# names = ["jrnl"]
|
||||
# version = __version__
|
||||
|
||||
@classmethod
|
||||
def class_path(cls):
|
||||
return cls.__module__
|
||||
|
||||
@staticmethod
|
||||
def import_(journal, input=None):
|
||||
raise NotImplementedError
|
||||
|
||||
|
||||
class BaseExporter:
|
||||
"""Base Exporter class (to sub-class)"""
|
||||
|
||||
# names = ["text", "txt"]
|
||||
# extension = "txt"
|
||||
# version = __version__
|
||||
|
||||
@classmethod
|
||||
def class_path(cls):
|
||||
return cls.__module__
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a string representation of a single entry."""
|
||||
return str(entry)
|
||||
raise NotImplementedError
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
|
@ -33,9 +58,16 @@ class TextExporter:
|
|||
try:
|
||||
with open(path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_journal(journal))
|
||||
return f"[Journal exported to {path}]"
|
||||
return (
|
||||
f"[Journal '{journal.name}' exported (as a single file) to {path}]"
|
||||
)
|
||||
except IOError as e:
|
||||
return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]"
|
||||
except NotImplementedError:
|
||||
return (
|
||||
f"[{ERROR_COLOR}ERROR{RESET_COLOR}: This exporter doesn't support "
|
||||
"exporting as a single file.]"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def make_filename(cls, entry):
|
||||
|
@ -46,16 +78,23 @@ class TextExporter:
|
|||
@classmethod
|
||||
def write_files(cls, journal, path):
|
||||
"""Exports a journal into individual files for each entry."""
|
||||
for entry in journal.entries:
|
||||
try:
|
||||
full_path = os.path.join(path, cls.make_filename(entry))
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_entry(entry))
|
||||
except IOError as e:
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(
|
||||
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
||||
)
|
||||
return "[Journal exported to {}]".format(path)
|
||||
try:
|
||||
for entry in journal.entries:
|
||||
try:
|
||||
full_path = os.path.join(path, cls.make_filename(entry))
|
||||
with open(full_path, "w", encoding="utf-8") as f:
|
||||
f.write(cls.export_entry(entry))
|
||||
except IOError as e:
|
||||
return "[{2}ERROR{3}: {0} {1}]".format(
|
||||
e.filename, e.strerror, ERROR_COLOR, RESET_COLOR
|
||||
)
|
||||
except NotImplementedError:
|
||||
return (
|
||||
f"[{ERROR_COLOR}ERROR{RESET_COLOR}: This exporter doesn't support "
|
||||
"exporting as individual files.]"
|
||||
)
|
||||
else:
|
||||
return f"[Journal '{journal.name}' exported (as multiple files) to {path}]"
|
||||
|
||||
def _slugify(string):
|
||||
"""Slugifies a string.
|
106
jrnl/plugins/collector.py
Normal file
106
jrnl/plugins/collector.py
Normal file
|
@ -0,0 +1,106 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
"""
|
||||
Code relating to the collecting of plugins and distributing calls to them.
|
||||
|
||||
In particular, the code here collects the list of imports and exporters, both
|
||||
internal and external, and tells the main program which plugins are available.
|
||||
Actual calling of the plugins is done directly and works because given plugin
|
||||
functions are importable/callable at predetermined (code) locations.
|
||||
|
||||
Internal plugins are located in the `jrnl.plugins` namespace, and external
|
||||
plugins are located in the `jrnl.contrib` namespace.
|
||||
|
||||
This file was originally called "meta", using that title in the reflexive sense;
|
||||
i.e. it is the collection of code that allows plugins to deal with themselves.
|
||||
"""
|
||||
|
||||
import importlib
|
||||
import pkgutil
|
||||
|
||||
import jrnl.contrib.exporter
|
||||
import jrnl.contrib.importer
|
||||
import jrnl.plugins.exporter
|
||||
import jrnl.plugins.importer
|
||||
|
||||
__exporters_builtin = list(
|
||||
pkgutil.iter_modules(
|
||||
jrnl.plugins.exporter.__path__, jrnl.plugins.exporter.__name__ + "."
|
||||
)
|
||||
)
|
||||
__exporters_contrib = list(
|
||||
pkgutil.iter_modules(
|
||||
jrnl.contrib.exporter.__path__, jrnl.contrib.exporter.__name__ + "."
|
||||
)
|
||||
)
|
||||
|
||||
__importers_builtin = list(
|
||||
pkgutil.iter_modules(
|
||||
jrnl.plugins.importer.__path__, jrnl.plugins.importer.__name__ + "."
|
||||
)
|
||||
)
|
||||
__importers_contrib = list(
|
||||
pkgutil.iter_modules(
|
||||
jrnl.contrib.importer.__path__, jrnl.contrib.importer.__name__ + "."
|
||||
)
|
||||
)
|
||||
|
||||
__exporter_types_builtin = {
|
||||
name: importlib.import_module(plugin.name)
|
||||
for plugin in __exporters_builtin
|
||||
for name in importlib.import_module(plugin.name).Exporter.names
|
||||
}
|
||||
__exporter_types_contrib = {
|
||||
name: importlib.import_module(plugin.name)
|
||||
for plugin in __exporters_contrib
|
||||
for name in importlib.import_module(plugin.name).Exporter.names
|
||||
}
|
||||
|
||||
|
||||
__importer_types_builtin = {
|
||||
name: importlib.import_module(plugin.name)
|
||||
for plugin in __importers_builtin
|
||||
for name in importlib.import_module(plugin.name).Importer.names
|
||||
}
|
||||
__importer_types_contrib = {
|
||||
name: importlib.import_module(plugin.name)
|
||||
for plugin in __importers_contrib
|
||||
for name in importlib.import_module(plugin.name).Importer.names
|
||||
}
|
||||
|
||||
__exporter_types = {
|
||||
**__exporter_types_builtin,
|
||||
**__exporter_types_contrib,
|
||||
}
|
||||
__importer_types = {
|
||||
**__importer_types_builtin,
|
||||
**__importer_types_contrib,
|
||||
}
|
||||
|
||||
EXPORT_FORMATS = sorted(__exporter_types.keys())
|
||||
"""list of stings: all available export formats."""
|
||||
IMPORT_FORMATS = sorted(__importer_types.keys())
|
||||
"""list of stings: all available import formats."""
|
||||
|
||||
|
||||
def get_exporter(format):
|
||||
"""
|
||||
Given an export format, returns the (callable) class of the corresponding exporter.
|
||||
"""
|
||||
try:
|
||||
return __exporter_types[format].Exporter
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
||||
|
||||
|
||||
def get_importer(format):
|
||||
"""
|
||||
Given an import format, returns the (callable) class of the corresponding importer.
|
||||
"""
|
||||
try:
|
||||
return __importer_types[format].Importer
|
||||
except (AttributeError, KeyError):
|
||||
return None
|
|
@ -2,16 +2,20 @@
|
|||
# encoding: utf-8
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from collections import Counter
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class DatesExporter(TextExporter):
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter lists dates and their respective counts, for heatingmapping etc."""
|
||||
|
||||
names = ["dates"]
|
||||
extension = "dates"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
|
@ -3,17 +3,21 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from jrnl.exception import JrnlError
|
||||
|
||||
from textwrap import TextWrapper
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
from jrnl.plugins.util import check_provided_linewrap_viability
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class FancyExporter(TextExporter):
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter can convert entries and journals into text with unicode box drawing characters."""
|
||||
|
||||
names = ["fancy", "boxed"]
|
||||
extension = "txt"
|
||||
version = __version__
|
||||
|
||||
# Top border of the card
|
||||
border_a = "┎"
|
||||
|
@ -80,14 +84,3 @@ class FancyExporter(TextExporter):
|
|||
def export_journal(cls, journal):
|
||||
"""Returns a unicode representation of an entire journal."""
|
||||
return "\n".join(cls.export_entry(entry) for entry in journal)
|
||||
|
||||
|
||||
def check_provided_linewrap_viability(linewrap, card, journal):
|
||||
if len(card[0]) > linewrap:
|
||||
width_violation = len(card[0]) - linewrap
|
||||
raise JrnlError(
|
||||
"LineWrapTooSmallForDateFormat",
|
||||
config_linewrap=linewrap,
|
||||
columns=width_violation,
|
||||
journal=journal,
|
||||
)
|
|
@ -5,15 +5,18 @@
|
|||
|
||||
import json
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from .util import get_tags_count
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
from jrnl.plugins.util import get_tags_count
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class JSONExporter(TextExporter):
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter can convert entries and journals into json."""
|
||||
|
||||
names = ["json"]
|
||||
extension = "json"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def entry_to_dict(cls, entry):
|
|
@ -9,15 +9,17 @@ import sys
|
|||
|
||||
from jrnl.color import RESET_COLOR
|
||||
from jrnl.color import WARNING_COLOR
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class MarkdownExporter(TextExporter):
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown."""
|
||||
|
||||
names = ["md", "markdown"]
|
||||
extension = "md"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, to_multifile=True):
|
20
jrnl/plugins/exporter/pretty.py
Normal file
20
jrnl/plugins/exporter/pretty.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class Exporter(BaseExporter):
|
||||
"""Pretty print journal"""
|
||||
|
||||
names = ["pretty", "default"]
|
||||
extension = "txt"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
return journal.pprint()
|
20
jrnl/plugins/exporter/short.py
Normal file
20
jrnl/plugins/exporter/short.py
Normal file
|
@ -0,0 +1,20 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class Exporter(BaseExporter):
|
||||
"""Short export -- i.e. single line date and title"""
|
||||
|
||||
names = ["short"]
|
||||
extension = "txt"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
return journal.pprint(short=True)
|
|
@ -3,15 +3,19 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from .util import get_tags_count
|
||||
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
from jrnl.plugins.util import get_tags_count
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class TagExporter(TextExporter):
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter can lists the tags for entries and journals, exported as a plain text file."""
|
||||
|
||||
names = ["tags"]
|
||||
extension = "tags"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
21
jrnl/plugins/exporter/text.py
Normal file
21
jrnl/plugins/exporter/text.py
Normal file
|
@ -0,0 +1,21 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
|
||||
names = ["text", "txt"]
|
||||
extension = "txt"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a string representation of a single entry."""
|
||||
return str(entry)
|
|
@ -5,30 +5,18 @@
|
|||
|
||||
from xml.dom import minidom
|
||||
|
||||
from .json_exporter import JSONExporter
|
||||
from .util import get_tags_count
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
from jrnl.plugins.util import get_tags_count
|
||||
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class XMLExporter(JSONExporter):
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter can convert entries and journals into XML."""
|
||||
|
||||
names = ["xml"]
|
||||
extension = "xml"
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, doc=None):
|
||||
"""Returns an XML representation of a single entry."""
|
||||
doc_el = doc or minidom.Document()
|
||||
entry_el = doc_el.createElement("entry")
|
||||
for key, value in cls.entry_to_dict(entry).items():
|
||||
elem = doc_el.createElement(key)
|
||||
elem.appendChild(doc_el.createTextNode(value))
|
||||
entry_el.appendChild(elem)
|
||||
if not doc:
|
||||
doc_el.appendChild(entry_el)
|
||||
return doc_el.toprettyxml()
|
||||
else:
|
||||
return entry_el
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def entry_to_xml(cls, entry, doc):
|
||||
|
@ -45,6 +33,21 @@ class XMLExporter(JSONExporter):
|
|||
entry_el.appendChild(doc.createTextNode(entry.fulltext))
|
||||
return entry_el
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, doc=None):
|
||||
"""Returns an XML representation of a single entry."""
|
||||
doc_el = doc or minidom.Document()
|
||||
entry_el = doc_el.createElement("entry")
|
||||
for key, value in cls.entry_to_dict(entry).items():
|
||||
elem = doc_el.createElement(key)
|
||||
elem.appendChild(doc_el.createTextNode(value))
|
||||
entry_el.appendChild(elem)
|
||||
if not doc:
|
||||
doc_el.appendChild(entry_el)
|
||||
return doc_el.toprettyxml()
|
||||
else:
|
||||
return entry_el
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns an XML representation of an entire journal."""
|
|
@ -10,15 +10,17 @@ import sys
|
|||
from jrnl.color import ERROR_COLOR
|
||||
from jrnl.color import RESET_COLOR
|
||||
from jrnl.color import WARNING_COLOR
|
||||
from jrnl.plugins.base import BaseExporter
|
||||
|
||||
from .text_exporter import TextExporter
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class YAMLExporter(TextExporter):
|
||||
class Exporter(BaseExporter):
|
||||
"""This Exporter can convert entries and journals into Markdown formatted text with YAML front matter."""
|
||||
|
||||
names = ["yaml"]
|
||||
extension = "md"
|
||||
version = __version__
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry, to_multifile=True):
|
||||
|
@ -133,9 +135,10 @@ class YAMLExporter(TextExporter):
|
|||
def export_journal(cls, journal):
|
||||
"""Returns an error, as YAML export requires a directory as a target."""
|
||||
print(
|
||||
"{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format(
|
||||
ERROR_COLOR, RESET_COLOR
|
||||
(
|
||||
f"[{ERROR_COLOR}ERROR{RESET_COLOR}: YAML export must be to "
|
||||
"individual files. Please specify a directory to export to.]"
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
return
|
||||
raise NotImplementedError
|
|
@ -5,11 +5,16 @@
|
|||
|
||||
import sys
|
||||
|
||||
from jrnl.plugins.base import BaseImporter
|
||||
|
||||
class JRNLImporter:
|
||||
from ... import __version__
|
||||
|
||||
|
||||
class Importer(BaseImporter):
|
||||
"""This plugin imports entries from other jrnl files."""
|
||||
|
||||
names = ["jrnl"]
|
||||
version = __version__
|
||||
|
||||
@staticmethod
|
||||
def import_(journal, input=None):
|
||||
|
@ -28,7 +33,9 @@ class JRNLImporter:
|
|||
journal.import_(other_journal_txt)
|
||||
new_cnt = len(journal.entries)
|
||||
print(
|
||||
"[{} imported to {} journal]".format(new_cnt - old_cnt, journal.name),
|
||||
"[{} entries imported to '{}' journal]".format(
|
||||
new_cnt - old_cnt, journal.name
|
||||
),
|
||||
file=sys.stderr,
|
||||
)
|
||||
journal.write()
|
|
@ -1,142 +0,0 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
import re
|
||||
|
||||
import yaml
|
||||
|
||||
VAR_RE = r"[_a-zA-Z][a-zA-Z0-9_]*"
|
||||
EXPRESSION_RE = r"[\[\]():.a-zA-Z0-9_]*"
|
||||
PRINT_RE = r"{{ *(.+?) *}}"
|
||||
START_BLOCK_RE = r"{% *(if|for) +(.+?) *%}"
|
||||
END_BLOCK_RE = r"{% *end(for|if) *%}"
|
||||
FOR_RE = r"{{% *for +({varname}) +in +([^%]+) *%}}".format(varname=VAR_RE)
|
||||
IF_RE = r"{% *if +(.+?) *%}"
|
||||
BLOCK_RE = r"{% *block +(.+?) *%}((?:.|\n)+?){% *endblock *%}"
|
||||
INCLUDE_RE = r"{% *include +(.+?) *%}"
|
||||
|
||||
|
||||
class Template:
|
||||
def __init__(self, template):
|
||||
self.template = template
|
||||
self.clean_template = None
|
||||
self.blocks = {}
|
||||
|
||||
@classmethod
|
||||
def from_file(cls, filename):
|
||||
with open(filename) as f:
|
||||
front_matter, body = f.read().strip("-\n").split("---", 2)
|
||||
front_matter = yaml.load(front_matter, Loader=yaml.FullLoader)
|
||||
template = cls(body)
|
||||
template.__dict__.update(front_matter)
|
||||
return template
|
||||
|
||||
def render(self, **vars):
|
||||
if self.clean_template is None:
|
||||
self._get_blocks()
|
||||
return self._expand(self.clean_template, **vars)
|
||||
|
||||
def render_block(self, block, **vars):
|
||||
if self.clean_template is None:
|
||||
self._get_blocks()
|
||||
return self._expand(self.blocks[block], **vars)
|
||||
|
||||
def _eval_context(self, vars):
|
||||
import asteval
|
||||
|
||||
e = asteval.Interpreter(use_numpy=False, writer=None)
|
||||
e.symtable.update(vars)
|
||||
e.symtable["__last_iteration"] = vars.get("__last_iteration", False)
|
||||
return e
|
||||
|
||||
def _get_blocks(self):
|
||||
def s(match):
|
||||
name, contents = match.groups()
|
||||
self.blocks[name] = self._strip_single_nl(contents)
|
||||
return ""
|
||||
|
||||
self.clean_template = re.sub(BLOCK_RE, s, self.template, flags=re.MULTILINE)
|
||||
|
||||
def _expand(self, template, **vars):
|
||||
stack = sorted(
|
||||
[
|
||||
(m.start(), 1, m.groups()[0])
|
||||
for m in re.finditer(START_BLOCK_RE, template)
|
||||
]
|
||||
+ [
|
||||
(m.end(), -1, m.groups()[0])
|
||||
for m in re.finditer(END_BLOCK_RE, template)
|
||||
]
|
||||
)
|
||||
|
||||
last_nesting, nesting = 0, 0
|
||||
start = 0
|
||||
result = ""
|
||||
block_type = None
|
||||
if not stack:
|
||||
return self._expand_vars(template, **vars)
|
||||
|
||||
for pos, indent, typ in stack:
|
||||
nesting += indent
|
||||
if nesting == 1 and last_nesting == 0:
|
||||
block_type = typ
|
||||
result += self._expand_vars(template[start:pos], **vars)
|
||||
start = pos
|
||||
elif nesting == 0 and last_nesting == 1:
|
||||
if block_type == "if":
|
||||
result += self._expand_cond(template[start:pos], **vars)
|
||||
elif block_type == "for":
|
||||
result += self._expand_loops(template[start:pos], **vars)
|
||||
elif block_type == "block":
|
||||
result += self._save_block(template[start:pos], **vars)
|
||||
start = pos
|
||||
last_nesting = nesting
|
||||
|
||||
result += self._expand_vars(template[stack[-1][0] :], **vars)
|
||||
return result
|
||||
|
||||
def _expand_vars(self, template, **vars):
|
||||
safe_eval = self._eval_context(vars)
|
||||
expanded = re.sub(
|
||||
INCLUDE_RE, lambda m: self.render_block(m.groups()[0], **vars), template
|
||||
)
|
||||
return re.sub(PRINT_RE, lambda m: str(safe_eval(m.groups()[0])), expanded)
|
||||
|
||||
def _expand_cond(self, template, **vars):
|
||||
start_block = re.search(IF_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
expression = start_block.groups()[0]
|
||||
sub_template = self._strip_single_nl(
|
||||
template[start_block.end() : end_block.start()]
|
||||
)
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
if safe_eval(expression):
|
||||
return self._expand(sub_template)
|
||||
return ""
|
||||
|
||||
def _strip_single_nl(self, template, strip_r=True):
|
||||
if template[0] == "\n":
|
||||
template = template[1:]
|
||||
if strip_r and template[-1] == "\n":
|
||||
template = template[:-1]
|
||||
return template
|
||||
|
||||
def _expand_loops(self, template, **vars):
|
||||
start_block = re.search(FOR_RE, template, re.M)
|
||||
end_block = list(re.finditer(END_BLOCK_RE, template, re.M))[-1]
|
||||
var_name, iterator = start_block.groups()
|
||||
sub_template = self._strip_single_nl(
|
||||
template[start_block.end() : end_block.start()], strip_r=False
|
||||
)
|
||||
|
||||
safe_eval = self._eval_context(vars)
|
||||
|
||||
result = ""
|
||||
items = safe_eval(iterator)
|
||||
for idx, var in enumerate(items):
|
||||
vars[var_name] = var
|
||||
vars["__last_iteration"] = idx == len(items) - 1
|
||||
result += self._expand(sub_template, **vars)
|
||||
del vars[var_name]
|
||||
return self._strip_single_nl(result)
|
|
@ -1,44 +0,0 @@
|
|||
#!/usr/bin/env python
|
||||
# encoding: utf-8
|
||||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from glob import glob
|
||||
import os
|
||||
|
||||
from .template import Template
|
||||
from .text_exporter import TextExporter
|
||||
|
||||
|
||||
class GenericTemplateExporter(TextExporter):
|
||||
"""This Exporter can convert entries and journals into text files."""
|
||||
|
||||
@classmethod
|
||||
def export_entry(cls, entry):
|
||||
"""Returns a string representation of a single entry."""
|
||||
vars = {"entry": entry, "tags": entry.tags}
|
||||
return cls.template.render_block("entry", **vars)
|
||||
|
||||
@classmethod
|
||||
def export_journal(cls, journal):
|
||||
"""Returns a string representation of an entire journal."""
|
||||
vars = {"journal": journal, "entries": journal.entries, "tags": journal.tags}
|
||||
return cls.template.render_block("journal", **vars)
|
||||
|
||||
|
||||
def __exporter_from_file(template_file):
|
||||
"""Create a template class from a file"""
|
||||
name = os.path.basename(template_file).replace(".template", "")
|
||||
template = Template.from_file(template_file)
|
||||
return type(
|
||||
str(f"{name.title()}Exporter"),
|
||||
(GenericTemplateExporter,),
|
||||
{"names": [name], "extension": template.extension, "template": template},
|
||||
)
|
||||
|
||||
|
||||
__all__ = []
|
||||
|
||||
# Factory pattern to create Exporter classes for all available templates
|
||||
for template_file in glob("jrnl/templates/*.template"):
|
||||
__all__.append(__exporter_from_file(template_file))
|
|
@ -3,11 +3,13 @@
|
|||
# Copyright (C) 2012-2021 jrnl contributors
|
||||
# License: https://www.gnu.org/licenses/gpl-3.0.html
|
||||
|
||||
from jrnl.exception import JrnlError
|
||||
|
||||
|
||||
def get_tags_count(journal):
|
||||
"""Returns a set of tuples (count, tag) for all tags present in the journal."""
|
||||
# Astute reader: should the following line leave you as puzzled as me the first time
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightment.
|
||||
# I came across this construction, worry not and embrace the ensuing moment of enlightenment.
|
||||
tags = [tag for entry in journal.entries for tag in set(entry.tags)]
|
||||
# To be read: [for entry in journal.entries: for tag in set(entry.tags): tag]
|
||||
tag_counts = {(tags.count(tag), tag) for tag in tags}
|
||||
|
@ -25,3 +27,14 @@ def oxford_list(lst):
|
|||
return lst[0] + " or " + lst[1]
|
||||
else:
|
||||
return ", ".join(lst[:-1]) + ", or " + lst[-1]
|
||||
|
||||
|
||||
def check_provided_linewrap_viability(linewrap, card, journal):
|
||||
if len(card[0]) > linewrap:
|
||||
width_violation = len(card[0]) - linewrap
|
||||
raise JrnlError(
|
||||
"LineWrapTooSmallForDateFormat",
|
||||
config_linewrap=linewrap,
|
||||
columns=width_violation,
|
||||
journal=journal,
|
||||
)
|
||||
|
|
|
@ -1,18 +0,0 @@
|
|||
---
|
||||
extension: txt
|
||||
---
|
||||
|
||||
{% block journal %}
|
||||
{% for entry in entries %}
|
||||
{% include entry %}
|
||||
{% endfor %}
|
||||
|
||||
{% endblock %}
|
||||
|
||||
{% block entry %}
|
||||
{{ entry.title }}
|
||||
{{ "-" * len(entry.title) }}
|
||||
|
||||
{{ entry.body }}
|
||||
|
||||
{% endblock %}
|
Loading…
Add table
Add a link
Reference in a new issue