diff --git a/.gitignore b/.gitignore index 3b171aba..966cdaa9 100644 --- a/.gitignore +++ b/.gitignore @@ -50,3 +50,6 @@ docs/_themes/jrnl/static/less/3L.less # export testing director exp/ + +_extras/ +*.sublime-* diff --git a/features/data/configs/markdown-headings-335.yaml b/features/data/configs/markdown-headings-335.yaml new file mode 100644 index 00000000..a32c3539 --- /dev/null +++ b/features/data/configs/markdown-headings-335.yaml @@ -0,0 +1,11 @@ +default_hour: 9 +default_minute: 0 +editor: '' +encrypt: false +highlight: true +journals: + default: features/journals/markdown-headings-335.journal +linewrap: 80 +password: '' +tagsymbols: '@' +timeformat: '%Y-%m-%d %H:%M' diff --git a/features/data/journals/markdown-headings-335.journal b/features/data/journals/markdown-headings-335.journal new file mode 100644 index 00000000..30f592ef --- /dev/null +++ b/features/data/journals/markdown-headings-335.journal @@ -0,0 +1,42 @@ +[2015-04-14 13:23] Heading Test + +H1-1 += + +H1-2 +=== + +H1-3 +============================ + +H2-1 +- + +H2-2 +--- + +H2-3 +---------------------------------- + +Horizontal Rules (ignore) + +--- + +=== + +# ATX H1 + +## ATX H2 + +### ATX H3 + +#### ATX H4 + +##### ATX H5 + +###### ATX H6 + +Stuff + +More stuff +more stuff again diff --git a/features/exporting.feature b/features/exporting.feature index 3d6c2607..31036674 100644 --- a/features/exporting.feature +++ b/features/exporting.feature @@ -26,3 +26,52 @@ Feature: Exporting a Journal Then we should get no error and the output should be parsable as json and the json output should contain entries.0.uuid = "4BB1F46946AD439996C9B59DE7C4DDC1" + + Scenario: Increasing Headings on Markdown export + Given we use the config "markdown-headings-335.yaml" + When we run "jrnl --export markdown" + Then the output should be + """ + 2015 + ==== + + April + ----- + + ### 2015-04-14 13:23 Heading Test + + #### H1-1 + + #### H1-2 + + #### H1-3 + + ##### H2-1 + + ##### H2-2 + + ##### H2-3 + + Horizontal Rules (ignore) + + --- + + === + + #### ATX H1 + + ##### ATX H2 + + ###### ATX H3 + + ####### ATX H4 + + ######## ATX H5 + + ######### ATX H6 + + Stuff + + More stuff + more stuff again + """ diff --git a/jrnl/cli.py b/jrnl/cli.py index 4ecd1399..57aa4f6d 100644 --- a/jrnl/cli.py +++ b/jrnl/cli.py @@ -271,6 +271,3 @@ def run(manual_args=None): journal.entries += other_entries journal.sort() journal.write() - -if __name__ == "__main__": - run() diff --git a/jrnl/plugins/__init__.py b/jrnl/plugins/__init__.py index 6f802342..93019af0 100644 --- a/jrnl/plugins/__init__.py +++ b/jrnl/plugins/__init__.py @@ -8,6 +8,7 @@ import importlib class PluginMeta(type): + def __init__(cls, name, bases, attrs): """Called when a Plugin derived class is imported""" if not hasattr(cls, 'PLUGINS'): diff --git a/jrnl/plugins/markdown_exporter.py b/jrnl/plugins/markdown_exporter.py index 2ad660fc..157e01b5 100644 --- a/jrnl/plugins/markdown_exporter.py +++ b/jrnl/plugins/markdown_exporter.py @@ -1,33 +1,67 @@ #!/usr/bin/env python # encoding: utf-8 -from __future__ import absolute_import, unicode_literals +from __future__ import absolute_import, unicode_literals, print_function from .text_exporter import TextExporter +import re +import sys class MarkdownExporter(TextExporter): - """This Exporter can convert entries and journals into json.""" + """This Exporter can convert entries and journals into Markdown.""" names = ["md", "markdown"] extension = "md" @classmethod - def export_entry(cls, entry): + def export_entry(cls, entry, to_multifile=True): """Returns a markdown representation of a single entry.""" date_str = entry.date.strftime(entry.journal.config['timeformat']) body_wrapper = "\n" if entry.body else "" body = body_wrapper + entry.body + if to_multifile is True: + heading = '#' + else: + heading = '###' + + '''Increase heading levels in body text''' + newbody = '' + previous_line = '' + warn_on_heading_level = False + for line in body.splitlines(True): + if re.match(r"#+ ", line): + """ATX style headings""" + newbody = newbody + previous_line + heading + line + if re.match(r"#######+ ", heading + line): + warn_on_heading_level = True + line = '' + elif re.match(r"=+$", line) and not re.match(r"^$", previous_line): + """Setext style H1""" + newbody = newbody + heading + "# " + previous_line + line = '' + elif re.match(r"-+$", line) and not re.match(r"^$", previous_line): + """Setext style H2""" + newbody = newbody + heading + "## " + previous_line + line = '' + else: + newbody = newbody + previous_line + previous_line = line + newbody = newbody + previous_line # add very last line + + if warn_on_heading_level is True: + print("{}WARNING{}: Headings increased past H6 on export - {} {}".format("\033[33m", "\033[0m", date_str, entry.title), file=sys.stderr) + return "{md} {date} {title} {body} {space}".format( - md="###", + md=heading, date=date_str, title=entry.title, - body=body, + body=newbody, space="" ) @classmethod def export_journal(cls, journal): - """Returns a json representation of an entire journal.""" + """Returns a Markdown representation of an entire journal.""" out = [] year, month = -1, -1 for e in journal.entries: @@ -39,6 +73,6 @@ class MarkdownExporter(TextExporter): month = e.date.month out.append(e.date.strftime("%B")) out.append('-' * len(e.date.strftime("%B")) + "\n") - out.append(cls.export_entry(e)) + out.append(cls.export_entry(e, False)) result = "\n".join(out) return result diff --git a/jrnl/plugins/tag_exporter.py b/jrnl/plugins/tag_exporter.py index 69029f61..439bac7c 100644 --- a/jrnl/plugins/tag_exporter.py +++ b/jrnl/plugins/tag_exporter.py @@ -7,18 +7,18 @@ from .util import get_tags_count class TagExporter(TextExporter): - """This Exporter can convert entries and journals into json.""" + """This Exporter can lists the tags for entries and journals, exported as a plain text file.""" names = ["tags"] extension = "tags" @classmethod def export_entry(cls, entry): - """Returns a markdown representation of a single entry.""" + """Returns a list of tags for a single entry.""" return ", ".join(entry.tags) @classmethod def export_journal(cls, journal): - """Returns a json representation of an entire journal.""" + """Returns a list of tags and their frequency for an entire journal.""" tag_counts = get_tags_count(journal) result = "" if not tag_counts: diff --git a/jrnl/plugins/text_exporter.py b/jrnl/plugins/text_exporter.py index ce474e8f..042c66fd 100644 --- a/jrnl/plugins/text_exporter.py +++ b/jrnl/plugins/text_exporter.py @@ -56,7 +56,7 @@ class TextExporter(BaseExporter): representation as unicode if output is None.""" if output and os.path.isdir(output): # multiple files return cls.write_files(journal, output) - elif output: + elif output: # single file return cls.write_file(journal, output) else: return cls.export_journal(journal) diff --git a/jrnl/plugins/yaml_exporter.py b/jrnl/plugins/yaml_exporter.py new file mode 100644 index 00000000..1e00cf2f --- /dev/null +++ b/jrnl/plugins/yaml_exporter.py @@ -0,0 +1,70 @@ +#!/usr/bin/env python +# encoding: utf-8 + +from __future__ import absolute_import, unicode_literals, print_function +from .text_exporter import TextExporter +import re +import sys +import yaml + + +class MarkdownExporter(TextExporter): + """This Exporter can convert entries and journals into Markdown with YAML front matter.""" + names = ["yaml"] + extension = "md" + + @classmethod + def export_entry(cls, entry, to_multifile=True): + """Returns a markdown representation of a single entry, with YAML front matter.""" + if to_multifile is False: + print("{}ERROR{}: YAML export must be to individual files. Please specify a directory to export to.".format("\033[31m", "\033[0m", file=sys.stderr)) + return + + date_str = entry.date.strftime(entry.journal.config['timeformat']) + body_wrapper = "\n" if entry.body else "" + body = body_wrapper + entry.body + + '''Increase heading levels in body text''' + newbody = '' + heading = '###' + previous_line = '' + warn_on_heading_level = False + for line in entry.body.splitlines(True): + if re.match(r"#+ ", line): + """ATX style headings""" + newbody = newbody + previous_line + heading + line + if re.match(r"#######+ ", heading + line): + warn_on_heading_level = True + line = '' + elif re.match(r"=+$", line) and not re.match(r"^$", previous_line): + """Setext style H1""" + newbody = newbody + heading + "# " + previous_line + line = '' + elif re.match(r"-+$", line) and not re.match(r"^$", previous_line): + """Setext style H2""" + newbody = newbody + heading + "## " + previous_line + line = '' + else: + newbody = newbody + previous_line + previous_line = line + newbody = newbody + previous_line # add very last line + + if warn_on_heading_level is True: + print("{}WARNING{}: Headings increased past H6 on export - {} {}".format("\033[33m", "\033[0m", date_str, entry.title), file=sys.stderr) + + # top = yaml.dump(entry) + + return "title: {title}\ndate: {date}\nstared: {stared}\ntags: {tags}\n{body} {space}".format( + date=date_str, + title=entry.title, + stared=entry.starred, + tags=', '.join([tag[1:] for tag in entry.tags]), + body=newbody, + space="" + ) + + @classmethod + 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("\033[31m", "\033[0m", file=sys.stderr)) + return diff --git a/jrnl/util.py b/jrnl/util.py index c6ea4659..5f0571cf 100644 --- a/jrnl/util.py +++ b/jrnl/util.py @@ -146,6 +146,8 @@ def slugify(string): """ string = u(string) ascii_string = str(unicodedata.normalize('NFKD', string).encode('ascii', 'ignore')) + if PY3: + ascii_string = ascii_string[1:] # removed the leading 'b' no_punctuation = re.sub(r'[^\w\s-]', '', ascii_string).strip().lower() slug = re.sub(r'[-\s]+', '-', no_punctuation) return u(slug) diff --git a/setup.py b/setup.py index 526dc7d6..39b45082 100644 --- a/setup.py +++ b/setup.py @@ -78,12 +78,12 @@ setup( name="jrnl", version=get_version(), description="A command line journal application that stores your journal in a plain text file", - packages=['jrnl'], + packages=['jrnl', 'jrnl.plugins'], install_requires=[ "parsedatetime>=1.2", "pytz>=2013b", "six>=1.7.4", - "cryptography==0.8.1", + "cryptography>=0.8.1", "tzlocal>=1.1", "pyyaml>=3.11", "keyring>=3.3",