diff --git a/jrnl/plugins/base.py b/jrnl/plugins/base.py new file mode 100644 index 00000000..e7a3dee0 --- /dev/null +++ b/jrnl/plugins/base.py @@ -0,0 +1,119 @@ +#!/usr/bin/env python +# encoding: utf-8 +# 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 + +from jrnl import __version__ +from jrnl.color import ERROR_COLOR +from jrnl.color import RESET_COLOR + + +class BaseImporter: + """Base Importer class (to sub-class)""" + + # 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.""" + raise NotImplementedError + + @classmethod + def export_journal(cls, journal): + """Returns a string representation of an entire journal.""" + return "\n".join(cls.export_entry(entry) for entry in journal) + + @classmethod + def write_file(cls, journal, path): + """Exports a journal into a single file.""" + try: + with open(path, "w", encoding="utf-8") as f: + f.write(cls.export_journal(journal)) + 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): + return entry.date.strftime("%Y-%m-%d") + "_{}.{}".format( + cls._slugify(str(entry.title)), cls.extension + ) + + @classmethod + def write_files(cls, journal, path): + """Exports a journal into individual files for each entry.""" + 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. + Based on public domain code from https://github.com/zacharyvoase/slugify + """ + normalized_string = str(unicodedata.normalize("NFKD", string)) + no_punctuation = re.sub(r"[^\w\s-]", "", normalized_string).strip().lower() + slug = re.sub(r"[-\s]+", "-", no_punctuation) + return slug + + @classmethod + def export(cls, journal, output=None): + """Exports to individual files if output is an existing path, or into + a single file if output is a file name, or returns the exporter's + representation as string if output is None.""" + if output and os.path.isdir(output): # multiple files + return cls.write_files(journal, output) + elif output: # single file + return cls.write_file(journal, output) + else: + return cls.export_journal(journal) diff --git a/jrnl/plugins/exporter/dates_exporter.py b/jrnl/plugins/exporter/dates_exporter.py index 768f68bf..68cb5d55 100644 --- a/jrnl/plugins/exporter/dates_exporter.py +++ b/jrnl/plugins/exporter/dates_exporter.py @@ -2,13 +2,15 @@ # encoding: utf-8 # Copyright (C) 2012-2021 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html + from collections import Counter -from jrnl import __version__ -from jrnl.plugins.exporter.text_exporter import Exporter as TextExporter +from jrnl.plugins.base import BaseExporter + +from ... import __version__ -class Exporter(TextExporter): +class Exporter(BaseExporter): """This Exporter lists dates and their respective counts, for heatingmapping etc.""" names = ["dates"] diff --git a/jrnl/plugins/exporter/fancy_exporter.py b/jrnl/plugins/exporter/fancy_exporter.py index f5d25ae0..9a8f23e5 100644 --- a/jrnl/plugins/exporter/fancy_exporter.py +++ b/jrnl/plugins/exporter/fancy_exporter.py @@ -5,14 +5,17 @@ from textwrap import TextWrapper -from jrnl.plugins.exporter.text_exporter import Exporter as TextExporter +from jrnl.plugins.base import BaseExporter + +from ... import __version__ -class Exporter(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__ border_a = "┎" border_b = "─" diff --git a/jrnl/plugins/exporter/json_exporter.py b/jrnl/plugins/exporter/json_exporter.py index d4971599..624a3c17 100644 --- a/jrnl/plugins/exporter/json_exporter.py +++ b/jrnl/plugins/exporter/json_exporter.py @@ -5,15 +5,18 @@ import json -from jrnl.plugins.exporter.text_exporter import Exporter as TextExporter +from jrnl.plugins.base import BaseExporter from jrnl.plugins.util import get_tags_count +from ... import __version__ -class Exporter(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): diff --git a/jrnl/plugins/exporter/markdown_exporter.py b/jrnl/plugins/exporter/markdown_exporter.py index 113c28e3..3638a127 100644 --- a/jrnl/plugins/exporter/markdown_exporter.py +++ b/jrnl/plugins/exporter/markdown_exporter.py @@ -9,14 +9,17 @@ import sys from jrnl.color import RESET_COLOR from jrnl.color import WARNING_COLOR -from jrnl.plugins.exporter.text_exporter import Exporter as TextExporter +from jrnl.plugins.base import BaseExporter + +from ... import __version__ -class Exporter(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): diff --git a/jrnl/plugins/exporter/tag_exporter.py b/jrnl/plugins/exporter/tag_exporter.py index ee360446..561df755 100644 --- a/jrnl/plugins/exporter/tag_exporter.py +++ b/jrnl/plugins/exporter/tag_exporter.py @@ -6,12 +6,15 @@ from jrnl.plugins.exporter.text_exporter import Exporter as TextExporter from jrnl.plugins.util import get_tags_count +from ... import __version__ + class Exporter(TextExporter): """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): diff --git a/jrnl/plugins/exporter/text_exporter.py b/jrnl/plugins/exporter/text_exporter.py index 2b0795ff..91580dcc 100644 --- a/jrnl/plugins/exporter/text_exporter.py +++ b/jrnl/plugins/exporter/text_exporter.py @@ -3,83 +3,20 @@ # Copyright (C) 2012-2021 jrnl contributors # License: https://www.gnu.org/licenses/gpl-3.0.html -import os -import re -import unicodedata - from jrnl import __version__ -from jrnl.color import ERROR_COLOR -from jrnl.color import RESET_COLOR +from jrnl.plugins.base import BaseExporter + +from ... import __version__ -class Exporter: +class Exporter(BaseExporter): """This Exporter can convert entries and journals into text files.""" 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) - - @classmethod - def export_journal(cls, journal): - """Returns a string representation of an entire journal.""" - return "\n".join(cls.export_entry(entry) for entry in journal) - - @classmethod - def write_file(cls, journal, path): - """Exports a journal into a single file.""" - try: - with open(path, "w", encoding="utf-8") as f: - f.write(cls.export_journal(journal)) - return f"[Journal exported to {path}]" - except IOError as e: - return f"[{ERROR_COLOR}ERROR{RESET_COLOR}: {e.filename} {e.strerror}]" - - @classmethod - def make_filename(cls, entry): - return entry.date.strftime("%Y-%m-%d") + "_{}.{}".format( - cls._slugify(str(entry.title)), cls.extension - ) - - @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) - - def _slugify(string): - """Slugifies a string. - Based on public domain code from https://github.com/zacharyvoase/slugify - """ - normalized_string = str(unicodedata.normalize("NFKD", string)) - no_punctuation = re.sub(r"[^\w\s-]", "", normalized_string).strip().lower() - slug = re.sub(r"[-\s]+", "-", no_punctuation) - return slug - - @classmethod - def export(cls, journal, output=None): - """Exports to individual files if output is an existing path, or into - a single file if output is a file name, or returns the exporter's - representation as string if output is None.""" - if output and os.path.isdir(output): # multiple files - return cls.write_files(journal, output) - elif output: # single file - return cls.write_file(journal, output) - else: - return cls.export_journal(journal) diff --git a/jrnl/plugins/exporter/xml_exporter.py b/jrnl/plugins/exporter/xml_exporter.py index ec28c337..7e34d8f9 100644 --- a/jrnl/plugins/exporter/xml_exporter.py +++ b/jrnl/plugins/exporter/xml_exporter.py @@ -5,30 +5,20 @@ from xml.dom import minidom -from jrnl.plugins.exporter.json_exporter import Exporter as JSONExporter +# from jrnl.plugins.exporter.json_exporter import Exporter as JSONExporter +from jrnl.plugins.base import BaseExporter from jrnl.plugins.util import get_tags_count +from ... import __version__ -class Exporter(JSONExporter): + +# class Exporter(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 +35,21 @@ class Exporter(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.""" diff --git a/jrnl/plugins/exporter/yaml_exporter.py b/jrnl/plugins/exporter/yaml_exporter.py index 8cce8dca..cbfb5b31 100644 --- a/jrnl/plugins/exporter/yaml_exporter.py +++ b/jrnl/plugins/exporter/yaml_exporter.py @@ -10,14 +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.exporter.text_exporter import Exporter as TextExporter +from jrnl.plugins.base import BaseExporter + +from ... import __version__ -class Exporter(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): @@ -132,9 +135,10 @@ class Exporter(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 diff --git a/jrnl/plugins/importer/jrnl_importer.py b/jrnl/plugins/importer/jrnl_importer.py index 751cabaf..b8da9df4 100644 --- a/jrnl/plugins/importer/jrnl_importer.py +++ b/jrnl/plugins/importer/jrnl_importer.py @@ -5,18 +5,17 @@ import sys -from jrnl import __version__ +from jrnl.plugins.base import BaseImporter -class Importer: +from ... import __version__ + + +class Importer(BaseImporter): """This plugin imports entries from other jrnl files.""" names = ["jrnl"] version = __version__ - @classmethod - def class_path(cls): - return cls.__module__ - @staticmethod def import_(journal, input=None): """Imports from an existing file if input is specified, and @@ -34,7 +33,9 @@ class Importer: 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()