├── .gitignore ├── .travis.yml ├── AUTHORS.txt ├── CONTRIBUTING.md ├── HISTORY.txt ├── LICENSE.txt ├── MANIFEST.in ├── README.md ├── dayone_export ├── __init__.py ├── cli.py ├── compat.py ├── filters.py ├── mdx_autobold.py ├── mdx_hashtag.py ├── mdx_urlize.py ├── templates │ ├── default.html │ ├── default.md │ ├── default.org │ ├── default.tex │ ├── default.txt │ └── imgbase64.html └── version.py ├── docs ├── Makefile ├── cli.rst ├── conf.py ├── index.rst ├── make.bat ├── module.rst ├── templates.rst └── timezones.rst ├── setup.cfg ├── setup.py ├── tests ├── fake_journal │ ├── entries │ │ ├── 00-first.doentry │ │ ├── for_exclude.doentry │ │ ├── full.doentry │ │ └── zz-last.doentry │ └── photos │ │ ├── 00F9FA96F29043D09638DF0866EC73B2.jpg │ │ └── ignore.jpg ├── regression │ └── entries │ │ └── bad-date.doentry └── test_dayone_export.py └── tox.ini /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | build/ 3 | dist/ 4 | *.egg-info 5 | _build/ 6 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.5" 5 | - "3.6" 6 | install: 7 | - pip install . 8 | script: cd tests && nosetests 9 | 10 | notifications: 11 | email: 12 | on_success: never 13 | on_failure: always 14 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Written by Nathan Grigg 2 | 3 | Contributions by Alexander Artemenko, Evan Lovely, Philip Lundrigan, Quentin 4 | Stafford-Fraser, Stephan Heuel, and steve ulrich. For more information, 5 | clone the git repository and run `git log --format='%aN <%aE>' | sort -u` 6 | 7 | This project also makes use of mdx_urlize, which is part of the marky project 8 | by Byron Ruth (https://github.com/bruth/marky) 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | This project is retired; no further contributions accepted. 2 | 3 | # Testing 4 | 5 | To run the tests, you have a couple of options. 6 | 7 | Option 1: [tox][1]. Install tox, locate yourself in the main project 8 | directory, and run `tox`. All dependencies will be installed in 9 | virtual environments, and the code will be tested against Python 2.6 10 | and 2.7, if they are installed on your system. 11 | 12 | Option 2: Install [nose][2] and [mock][4]. Locate yourself in the main 13 | project directory, and run `nosetests`. 14 | 15 | # Building documentation 16 | 17 | Install [sphinx][3], locate yourself in the `docs` directory and 18 | run `make html`. Then open the `_build/index.html` page. 19 | 20 | [1]: http://tox.testrun.org/latest/ 21 | [2]: https://nose.readthedocs.org/en/latest/ 22 | [3]: http://sphinx.pocoo.org/ 23 | [4]: http://pypi.python.org/pypi/mock 24 | -------------------------------------------------------------------------------- /HISTORY.txt: -------------------------------------------------------------------------------- 1 | 1.0.0 2 | - Final release using old Day One journal format 3 | 4 | 0.8.3 5 | - Fix unicode error caused by certain locale settings in Python 2. 6 | 7 | 0.8.2 8 | - Fix markdown extension bug caused by new version of python-markdown. 9 | 10 | 0.8.1 11 | - Add --before filter to command line interface. 12 | - Pass current date to Jinja templates. 13 | 14 | 0.8.0 15 | - Begin using user's locale for strftime 16 | - Make Windows compatible 17 | 18 | 0.7.1 19 | - Exclude entries with specific tags (Anton Parkhomenko) 20 | 21 | 0.7.0 22 | - Native Python 3 support 23 | 24 | 0.6.3 25 | - Support for newer versions of Day One Export (specifically 1.12 for iOS). 26 | 27 | 0.6.2 28 | - Fix bug when printing to stdout 29 | - Fix bug and improve message when no template found 30 | - New template for plain text 31 | 32 | 0.6.1 33 | - Fix github url 34 | 35 | 0.6.0 36 | - Allow strftime syntax in output filename to create multiple output files. 37 | Note this means you must now escape literal percent signs in output 38 | filename. (Quentin Stafford-Fraser) 39 | - Add org mode template (Steve Ulrich) 40 | 41 | 0.5.0 42 | - Adjust default html template to allow block quote styling 43 | - Adjust default markdown template to include images 44 | 45 | 0.4.1 46 | - Entry.weather() helper function 47 | - Latex template support 48 | - Bug fix in base 64 encode filter 49 | 50 | 0.4.0 51 | - Major bug fix: time zone calculation error 52 | 53 | 0.3.0 54 | - Markdown extensions for footnotes and tables 55 | - New option to autobold first line 56 | - New option to convert newlines to
s. 57 | - Hashtags at the beginnings of lines are no longer treated as headings 58 | - Autolink urls 59 | 60 | 0.2.0 61 | - Supports Day One's native tag format 62 | - Supports Day One's native Timezone format 63 | - Fixed compatibility with Python 2.6 64 | - Simplify Entry.place() arguments 65 | 66 | 0.1.1 67 | - Bug fixes 68 | - Better documentation 69 | - Improve setuptools experience 70 | - Remove stray comma from default template 71 | 72 | 0.1.0 73 | - Arrange source into package format and upload to PyPI 74 | - Ability to filter entries by hash tags 75 | - Ability to filter by time using --after 76 | - Option base64 encoding for images in html 77 | 78 | (unnumbered) 79 | - First published to Github 80 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012, Nathan Grigg 2 | All rights reserved 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright 8 | notice, this list of conditions and the following disclaimer. 9 | * Redistributions in binary form must reproduce the above copyright 10 | notice, this list of conditions and the following disclaimer in the 11 | documentation and/or other materials provided with the distribution. 12 | * Neither the name of this package nor the 13 | names of its contributors may be used to endorse or promote products 14 | derived from this software without specific prior written permission. 15 | 16 | This software is provided by the copyright holders and contributors "as 17 | is" and any express or implied warranties, including, but not limited 18 | to, the implied warranties of merchantability and fitness for a 19 | particular purpose are disclaimed. In no event shall Nathan Grigg be 20 | liable for any direct, indirect, incidental, special, exemplary, or 21 | consequential damages (including, but not limited to, procurement of 22 | substitute goods or services; loss of use, data, or profits; or business 23 | interruption) however caused and on any theory of liability, whether in 24 | contract, strict liability, or tort (including negligence or otherwise) 25 | arising in any way out of the use of this software, even if advised of 26 | the possibility of such damage. 27 | 28 | (also known as the New BSD License) 29 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include dayone_export/templates/* 2 | include AUTHORS.txt 3 | include LICENSE.txt 4 | include HISTORY.txt 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Notice 2 | 3 | **This tool is not compatible with the current Day One journal formats.** 4 | 5 | It is not really useful at this point, but I'm leaving it here in case 6 | someone wants to use it as a jump-off point for updating the tool. 7 | 8 | # Introduction 9 | 10 | Export [Day One][0] journal entries into html, text, or another format 11 | using a Jinja template. 12 | 13 | by Nathan Grigg 14 | 15 | [![Build status][statusimage]][statuslink] 16 | 17 | [statusimage]: https://api.travis-ci.org/nathangrigg/dayone_export.png?branch=master 18 | [statuslink]: https://travis-ci.org/nathangrigg/dayone_export 19 | 20 | # Installation 21 | 22 | Use [pip][4]: 23 | 24 | pip install dayone_export 25 | 26 | You can also use easy install 27 | (`easy_install dayone_export`) 28 | or download the source and install 29 | (`python setup.py install`). 30 | 31 | 32 | Depending on how your Python installation is configured, you may 33 | need extra permissions to install packages. If so, prefix the 34 | installation command by `sudo` and a space. 35 | 36 | Any of these methods will also install the dependencies 37 | [Jinja2][1], [pytz][2], [python-dateutil][6], and [Markdown][3]. 38 | 39 | # Quick start 40 | 41 | Export your entire journal with 42 | 43 | dayone_export --output journal.html /path/to/Journal.dayone 44 | 45 | To see available options, run 46 | 47 | dayone_export --help 48 | 49 | # Custom templates and advanced options 50 | 51 | You can specify a custom template using the command line option `--template`. 52 | 53 | You can permanently override the default template by creating a new template named `default.html` and saving it in the folder `~/.dayone_export`. 54 | 55 | [0]: http://dayoneapp.com 56 | [1]: http://jinja.pocoo.org 57 | [2]: http://pytz.sourceforge.net 58 | [3]: http://freewisdom.org/projects/python-markdown/ 59 | [4]: http://www.pip-installer.org/en/latest/index.html 60 | [6]: http://labix.org/python-dateutil 61 | -------------------------------------------------------------------------------- /dayone_export/__init__.py: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env python 2 | # 3 | # Copyright (c) 2012, Nathan Grigg 4 | # All rights reserved. 5 | # BSD License 6 | 7 | """Export Day One journal entries using a Jinja template.""" 8 | 9 | from operator import itemgetter 10 | from functools import partial 11 | from . import compat 12 | from . import filters 13 | from .version import VERSION 14 | import jinja2 15 | import plistlib 16 | import os 17 | import pytz 18 | from collections import defaultdict 19 | from datetime import datetime 20 | 21 | class PlistError(Exception): 22 | pass 23 | 24 | class Entry(object): 25 | """Parse a single journal entry. 26 | 27 | :raises: IOError, KeyError 28 | 29 | Acts like a read-only dictionary. 30 | The keys are as defined in the plist file by the Day One App, with 31 | minor exceptions: 32 | 33 | - What Day One calls "Entry Text", we call "Text". 34 | - The "Location", "Weather", "Creator", and "Music" dictionaries are 35 | flattened, so that their subkeys are accessible as keys of the main 36 | dictionary. 37 | - The "Photo" key is added and contains the path to attached photo. 38 | - The "Date" key is added and contains the localized date. 39 | 40 | Note that the "Creation Date" contains a naive date (that is, with no 41 | attached time zone) corresponding to the UTC time. 42 | """ 43 | 44 | def __init__(self, filename): 45 | try: 46 | self.data = plistlib.readPlist(filename) 47 | except AttributeError as err: # See #25. 48 | if str(err) == "'NoneType' object has no attribute 'groupdict'": 49 | raise PlistError( 50 | 'Unable to parse {} due to invalid ISO 8601 date.' 51 | .format(filename)) 52 | raise 53 | except IOError as err: 54 | raise PlistError('Unable to read {}: {}'.format(filename, repr(err))) 55 | 56 | # Required fields 57 | if "Creation Date" not in self.data: 58 | raise KeyError("Creation Date") 59 | 60 | # aliases and flattening 61 | self.data['Text'] = self.data.pop('Entry Text', "") 62 | for key in ['Location', 'Weather', 'Music', 'Creator']: 63 | if key in self.data: 64 | new_keys = ((k, v) for k, v in self.data[key].items() 65 | if k not in self.data) # prevent overwrite 66 | self.data.update(new_keys) 67 | 68 | def set_photo(self, filename): 69 | """Set the filename of the photo""" 70 | self.data['Photo'] = filename 71 | 72 | def set_localized_date(self, timezone): 73 | """Set the localized date (the "Date" key)""" 74 | try: 75 | tz = pytz.timezone(timezone) 76 | except pytz.UnknownTimeZoneError: 77 | tz = pytz.utc 78 | 79 | localized_utc = pytz.utc.localize(self["Creation Date"]) 80 | self.data["Date"] = localized_utc.astimezone(tz) 81 | 82 | def set_time_zone(self, timezone): 83 | """Set the time zone""" 84 | self.data["Time Zone"] = timezone 85 | 86 | def place(self, levels=4, ignore=None): 87 | """Format entry's location as string, with places separated by commas. 88 | 89 | :param levels: levels of specificity to include 90 | :type levels: list of int 91 | :keyword ignore: locations to ignore 92 | :type ignore: string or list of strings 93 | 94 | The *levels* parameter should be a list of integers corresponding to 95 | the following levels of specificity defined by Day One. 96 | 97 | - 0: Place Name 98 | - 1: Locality (e.g. city) 99 | - 2: Administrative Area (e.g. state) 100 | - 3: Country 101 | 102 | Alternately, *levels* can be an integer *n* to specify the *n* 103 | smallest levels. 104 | 105 | The keyword argument *ignore* directs the method to ignore one 106 | or more place names. For example, you may want to ignore 107 | your home country so that only foreign countries are shown. 108 | """ 109 | 110 | # deal with the arguments 111 | if isinstance(levels, int): 112 | levels = list(range(levels)) 113 | if ignore is None: 114 | ignore = [] 115 | if isinstance(ignore, compat.string_types): 116 | ignore = [ignore] 117 | 118 | # make sure there is a location set 119 | if not 'Location' in self: 120 | return "" # fail silently 121 | 122 | # mix up the order 123 | order = ['Place Name', 'Locality', 'Administrative Area', 'Country'] 124 | try: 125 | order_keys = [order[n] for n in levels] 126 | except TypeError: 127 | raise TypeError("'levels' argument must be an integer or list") 128 | 129 | # extract keys 130 | names = (self[key] for key in order_keys if key in self) 131 | 132 | # filter 133 | try: 134 | names = [name for name in names if len(name) and name not in ignore] 135 | except TypeError: 136 | raise TypeError("'ignore' argument must be a string or list") 137 | 138 | return ", ".join(names) 139 | 140 | def weather(self, temperature_type): 141 | if not 'Weather' in self: 142 | return "" # fail silently 143 | 144 | if temperature_type.lower() == 'celsius' or temperature_type.lower() == 'c': 145 | temperature = self.data['Celsius'] 146 | else: 147 | temperature = self.data['Fahrenheit'] 148 | 149 | weather = "{0}° {1}".format(temperature, self.data['Description']) 150 | return weather 151 | 152 | def __getitem__(self, key): 153 | return self.data[key] 154 | 155 | def __contains__(self, key): 156 | return key in self.data 157 | 158 | def keys(self): 159 | """List all keys.""" 160 | return list(self.data.keys()) 161 | 162 | def __repr__(self): 163 | return "".format(self['Creation Date']) 164 | 165 | 166 | def parse_journal(foldername): 167 | """Return a list of Entry objects, sorted by date""" 168 | 169 | journal = dict() 170 | for filename in os.listdir(os.path.join(foldername, 'entries')): 171 | if os.path.splitext(filename)[1] == '.doentry': 172 | try: 173 | entry = Entry(os.path.join(foldername, 'entries', filename)) 174 | except KeyError as err: 175 | pass 176 | 177 | journal[entry['UUID']] = entry 178 | 179 | if len(journal) == 0: 180 | raise Exception("No journal entries found in " + foldername) 181 | 182 | try: 183 | photos = os.listdir(os.path.join(foldername, 'photos')) 184 | except OSError: 185 | pass 186 | else: 187 | for filename in photos: 188 | base = os.path.splitext(filename)[0] 189 | try: 190 | journal[base].set_photo(os.path.join('photos', filename)) 191 | except KeyError: 192 | # ignore items in the photos folder with no corresponding entry 193 | pass 194 | 195 | # make it a list and sort 196 | journal = list(journal.values()) 197 | journal.sort(key=itemgetter('Creation Date')) 198 | 199 | # add timezone info 200 | newest_tz = 'utc' 201 | for entry in reversed(journal): 202 | if "Time Zone" in entry: 203 | newest_tz = entry["Time Zone"] 204 | break 205 | 206 | tz = newest_tz 207 | for entry in reversed(journal): 208 | if "Time Zone" in entry: 209 | tz = entry["Time Zone"] 210 | else: 211 | entry.set_time_zone(tz) 212 | 213 | entry.set_localized_date(tz) 214 | 215 | return journal 216 | 217 | 218 | def _determine_inheritance(template, template_dir, format): 219 | """Determines where to look for template based on user options""" 220 | 221 | # explicit path to template => only load that template 222 | if template is not None: 223 | path, base = os.path.split(template) 224 | if path: 225 | return jinja2.FileSystemLoader(path), base 226 | 227 | # template directory given => look there only 228 | if template_dir is not None: 229 | loader = jinja2.FileSystemLoader(template_dir) 230 | 231 | else: 232 | template_dir = os.path.expanduser('~/.dayone_export') 233 | # template is given => look in current directory, then defaults 234 | if template is not None: 235 | template_search_path = ['.', template_dir] 236 | # no template is given => don't look in current directory 237 | else: 238 | template_search_path = [template_dir] 239 | 240 | loader = jinja2.ChoiceLoader([ 241 | jinja2.FileSystemLoader(template_search_path), 242 | jinja2.PackageLoader('dayone_export') 243 | ]) 244 | 245 | # determine template if none is given 246 | if template is None: 247 | template = ("default." + format) if format else "default.html" 248 | 249 | return loader, template 250 | 251 | def _filter_by_tag(journal, tags): 252 | """filter by list of tags. tags='any' allows any entry with some tag""" 253 | if tags == 'any': 254 | tag_filter = lambda item: 'Tags' in item 255 | else: 256 | tag_filter = lambda item: 'Tags' in item and set(item['Tags']).intersection(set(tags)) 257 | 258 | return filter(tag_filter, journal) 259 | 260 | def _exclude_tags(journal, tags): 261 | """remain only entries without specified tags""" 262 | 263 | remain_filter = lambda item: 'Tags' not in item or not set(item['Tags']).intersection(set(tags)) 264 | 265 | return filter(remain_filter, journal) 266 | 267 | def _filter_by_date(journal, after, before): 268 | """return a list of entries after date 269 | 270 | :param before: A naive datetime representing a UTC time. 271 | :param after: A naive datetime representing a UTC time 272 | """ 273 | if after is None and before is None: 274 | return journal 275 | return [item for item in journal 276 | if (after is None or item['Creation Date'] >= after) and 277 | (before is None or item['Creation Date'] < before)] 278 | 279 | def _convert_to_utc(date, default_tz): 280 | """Convert date to UTC, using default_tz if no time zone is set.""" 281 | if date is None: 282 | return date 283 | if date.tzinfo is None: 284 | date = default_tz.localize(date) 285 | date.astimezone(pytz.utc) 286 | # strip timezone info 287 | return date.replace(tzinfo=None) 288 | 289 | 290 | def dayone_export(dayone_folder, template=None, reverse=False, tags=None, 291 | exclude=None, before=None, after=None, format=None, template_dir=None, autobold=False, 292 | nl2br=False, filename_template=""): 293 | """Render a template using entries from a Day One journal. 294 | 295 | :param dayone_folder: Name of Day One folder; generally ends in ``.dayone``. 296 | :type dayone_folder: string 297 | :param reverse: If true, the entries are formatted in reverse chronological 298 | order. 299 | :type reverse: bool 300 | :param tags: Only include entries with the given tags. 301 | This paramater can also be the literal string ``any``, 302 | in which case only entries with tags are included. 303 | Tags are interpreted as words at the end of an entry 304 | beginning with ``#``. 305 | :type tags: list of strings 306 | :param exclude: Exclude all entries with given tags. 307 | :type exclude: list of strings 308 | :param before: Only include entries on before the given date. 309 | :type before: naive datetime 310 | :param after: Only include entries on or after the given date. 311 | :type after: naive datetime 312 | :param format: The file extension of the default template to use. 313 | :type format: string 314 | :param template: Template file name. 315 | The program looks for the template first 316 | in the current directory, then the template directory. 317 | :type template: string 318 | :param template_dir: Directory containing templates. 319 | If not given, the program looks in 320 | ``~/.dayone_export`` followed by the 321 | dayone_export package. 322 | :type template_dir: string 323 | :param autobold: Specifies that the first line of each post should be 324 | a heading 325 | :type autobold: bool 326 | :param nl2br: Specifies that new lines should be translated in to
s 327 | :type nl2br: bool 328 | :type filename_template: string 329 | :param filename_template: An eventual filename, which can include strftime formatting codes. 330 | Each time the result of formatting an entry's timestamp with this changes, 331 | a new result will be returned. 332 | :returns: Iterator yielding (filename, filled_in_template) as strings on each iteration. 333 | """ 334 | 335 | # figure out which template to use 336 | loader, template = _determine_inheritance(template, template_dir, format) 337 | 338 | # custom latex template syntax 339 | custom_syntax = {} 340 | if os.path.splitext(template)[1] == ".tex": 341 | custom_syntax = {'block_start_string': r'\CMD{', 342 | 'block_end_string': '}', 343 | 'variable_start_string': r'\VAR{', 344 | 'variable_end_string': '}', 345 | } 346 | # define jinja environment 347 | env = jinja2.Environment(loader=loader, trim_blocks=True, **custom_syntax) 348 | 349 | # filters 350 | env.filters['markdown'] = filters.markdown_filter(autobold=autobold) 351 | env.filters['format'] = filters.format 352 | env.filters['escape_tex'] = filters.escape_tex 353 | env.filters['imgbase64'] = partial(filters.imgbase64, 354 | dayone_folder=dayone_folder) 355 | 356 | 357 | # load template 358 | template = env.get_template(template) 359 | 360 | # parse journal 361 | j = parse_journal(dayone_folder) 362 | 363 | # filter and manipulate based on options 364 | default_tz = j[-1]["Date"].tzinfo 365 | after = _convert_to_utc(after, default_tz) 366 | before = _convert_to_utc(before, default_tz) 367 | j = _filter_by_date(j, after=after, before=before) 368 | if tags is not None: 369 | j = _filter_by_tag(j, tags) 370 | if exclude is not None: 371 | j = _exclude_tags(j, exclude) 372 | if reverse: 373 | j.reverse() 374 | 375 | 376 | # Split into groups, possibly of length one 377 | # Generate a new output for each time the 'filename_template' changes. 378 | # Yield the resulting filename_template plus the output. 379 | # If the filename_template is an empty string (the default), we'll get 380 | # an empty grouper plus the rendering of the full list of entries. 381 | # This may throw an exception if the template is malformed. 382 | # The traceback is helpful, so I'm letting it through 383 | # it might be nice to clean up the error message, someday 384 | 385 | output_groups = defaultdict(list) 386 | for e in j: 387 | output_groups[e['Date'].strftime(filename_template)].append(e) 388 | 389 | today = datetime.today() 390 | for k in output_groups: 391 | yield k, template.render(journal=output_groups[k], today=today) 392 | -------------------------------------------------------------------------------- /dayone_export/cli.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # 3 | # Command line interface to dayone_export 4 | # 5 | # For help, run `dayone_export --help` 6 | 7 | from . import dayone_export, VERSION, compat, PlistError 8 | import dateutil.parser 9 | import jinja2 10 | import argparse 11 | import codecs 12 | import locale 13 | import os 14 | import sys 15 | 16 | 17 | def template_not_found_message(template): 18 | message = ["Template not found: {0}".format(template), 19 | "Use the `--template` option to specify a template."] 20 | try: 21 | from pkg_resources import resource_listdir 22 | message.extend(["The following templates are built-in:"] + 23 | resource_listdir('dayone_export', 'templates')) 24 | except ImportError: 25 | pass 26 | return '\n'.join(message) 27 | 28 | 29 | def parse_args(args=None): 30 | """Parse command line arguments""" 31 | parser = argparse.ArgumentParser( 32 | description="Export Day One entries using a Jinja template", 33 | usage="%(prog)s [--output FILE] [opts] journal", 34 | epilog="""If the Day One package has photos, you may need to copy 35 | the "photos" folder from the package into the same directory 36 | as the output file.""") 37 | parser.add_argument('journal', help="path to Day One journal package") 38 | parser.add_argument('--output', metavar="FILE", default="", 39 | help="file to write (default print to stdout). " 40 | "Using strftime syntax will produce multiple " 41 | "output files with entries grouped by date.") 42 | parser.add_argument('--format', metavar="FMT", 43 | help="output format (default guess from output file extension)") 44 | parser.add_argument('--template', metavar="NAME", 45 | help="name or file of template to use") 46 | parser.add_argument('--template-dir', metavar="DIR", 47 | help='location of templates (default ~/.dayone_export)') 48 | parser.add_argument('--tags', 49 | help='export entries with these comma-separated tags. Tag \'any\' has a special meaning.') 50 | parser.add_argument('--exclude', 51 | help='exclude entries with these comma-separated tags') 52 | parser.add_argument('--after', metavar='DATE', 53 | help='export entries published on or after this date') 54 | parser.add_argument('--before', metavar='DATE', 55 | help='export entries published before this date') 56 | parser.add_argument('--reverse', action="store_true", 57 | help="display in reverse chronological order") 58 | parser.add_argument('--autobold', action="store_true", 59 | help="autobold first lines (titles) of posts") 60 | parser.add_argument('--nl2br', action="store_true", 61 | help="convert each new line to a
") 62 | parser.add_argument('--locale', help=argparse.SUPPRESS, default="") 63 | 64 | parser.add_argument('--version', action='version', version=VERSION) 65 | return parser.parse_args(args) 66 | 67 | # command line interface 68 | def run(args=None): 69 | args = parse_args(args) 70 | locale.setlocale(locale.LC_ALL, args.locale) 71 | 72 | # determine output format 73 | if args.format is None: 74 | args.format = os.path.splitext(args.output)[1][1:] if args.output \ 75 | else 'html' 76 | if args.format.lower() in ['md', 'markdown', 'mdown', 'mkdn']: 77 | args.format = 'md' 78 | 79 | # Check journal files exist 80 | args.journal = os.path.expanduser(args.journal) 81 | if not os.path.exists(args.journal): 82 | return "File not found: " + args.journal 83 | if not os.path.exists(os.path.join(args.journal, 'entries')): 84 | return "Not a valid Day One package: " + args.journal 85 | 86 | # tags 87 | tags = args.tags 88 | if tags is not None: 89 | if tags != 'any': 90 | tags = [tag.strip() for tag in tags.split(',')] 91 | 92 | # excluded tags 93 | excluded_tags = args.exclude 94 | if excluded_tags is not None: 95 | excluded_tags = [tag.strip() for tag in excluded_tags.split(',')] 96 | 97 | # parse before and after date 98 | dates = [args.before, args.after] 99 | for i, date in enumerate(dates): 100 | if date: 101 | try: 102 | dates[i] = dateutil.parser.parse(date) 103 | except (ValueError, OverflowError): 104 | return "Unable to parse date '{0}'".format(date) 105 | before, after = dates 106 | 107 | generator = dayone_export( 108 | args.journal, 109 | template=args.template, 110 | reverse=args.reverse, 111 | tags=tags, 112 | exclude=excluded_tags, 113 | before=before, 114 | after=after, 115 | format=args.format, 116 | template_dir=args.template_dir, 117 | autobold=args.autobold, 118 | nl2br=args.nl2br, 119 | filename_template=args.output) 120 | 121 | try: 122 | 123 | # Output is a generator returning each file's name and contents one at a time 124 | for filename, output in generator: 125 | if args.output: 126 | with codecs.open(filename, 'w', encoding='utf-8') as f: 127 | f.write(output) 128 | else: 129 | compat.print_bytes(output.encode('utf-8')) 130 | compat.print_bytes("\n".encode('utf-8')) 131 | 132 | except jinja2.TemplateNotFound as err: 133 | return template_not_found_message(err) 134 | except PlistError as err: 135 | return str(err) 136 | 137 | 138 | if __name__ == "__main__": 139 | sys.exit(run()) 140 | -------------------------------------------------------------------------------- /dayone_export/compat.py: -------------------------------------------------------------------------------- 1 | """Python 2 vs 3 compatibility.""" 2 | import sys 3 | 4 | PY2 = sys.version_info[0] == 2 5 | 6 | if PY2: 7 | string_types = (str, unicode) 8 | print_bytes = lambda s: sys.stdout.write(s) 9 | else: 10 | string_types = (str,) 11 | print_bytes = lambda s: sys.stdout.buffer.write(s) 12 | -------------------------------------------------------------------------------- /dayone_export/filters.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2012, Nathan Grigg 2 | # All rights reserved. 3 | # BSD License 4 | 5 | import base64 6 | from io import BytesIO 7 | import locale 8 | import markdown 9 | import os 10 | import pytz 11 | import re 12 | import sys 13 | 14 | MARKER = 'zpoqjd_marker_zpoqjd' 15 | RE_PERCENT_MINUS = re.compile(r'(?>> warn_once = WarnOnce({'foo': 'bar'}) 23 | >>> warn_once('foo') 24 | (print to stderr) bar 25 | 26 | >>> warn_once('foo') 27 | (nothing happens) 28 | """ 29 | def __init__(self, warnings): 30 | self.warnings = warnings 31 | self.issued = dict((k, False) for k in warnings) 32 | 33 | def __call__(self, warning): 34 | if not self.issued[warning]: 35 | self.issued[warning] = True 36 | sys.stderr.write(self.warnings[warning] + '\n') 37 | 38 | warn_once = WarnOnce({ 39 | 'imgbase64': 'Warning: Cannot load Python Imaging Library. Encoding full-size images.' 40 | }) 41 | 42 | ############################# 43 | # Markdown 44 | ############################# 45 | 46 | def markdown_filter(autobold=False, nl2br=False): 47 | """Returns a markdown filter""" 48 | extensions = ['footnotes', 49 | 'tables', 50 | 'smart_strong', 51 | 'fenced_code', 52 | 'attr_list', 53 | 'def_list', 54 | 'abbr', 55 | 'dayone_export.mdx_hashtag', 56 | 'dayone_export.mdx_urlize', 57 | ] 58 | 59 | if autobold: 60 | extensions.append('dayone_export.mdx_autobold') 61 | 62 | if nl2br: 63 | extensions.append('nl2br') 64 | 65 | md = markdown.Markdown(extensions=extensions, 66 | extension_configs={'footnotes': [('UNIQUE_IDS', True)]}, 67 | output_format='html5') 68 | 69 | def markup(text, *args, **kwargs): 70 | md.reset() 71 | return md.convert(text) 72 | 73 | return markup 74 | 75 | 76 | ############################# 77 | # Date formatting 78 | ############################# 79 | def format(value, fmt='%A, %b %-d, %Y', tz=None): 80 | """Format a date or time.""" 81 | 82 | if tz: 83 | value = value.astimezone(pytz.timezone(tz)) 84 | try: 85 | formatted = value.strftime(fmt) 86 | except ValueError: 87 | formatted = _strftime_portable(value, fmt) 88 | 89 | # Workaround for python 2.7, which returns bytes from strftime. 90 | if not isinstance(formatted, UNICODE_TYPE): 91 | try: 92 | formatted = formatted.decode("ascii") 93 | except UnicodeDecodeError: 94 | _, encoding = locale.getlocale(locale.LC_ALL) 95 | formatted = formatted.decode(encoding) 96 | 97 | return formatted 98 | 99 | 100 | def _strftime_portable(value, fmt='%A, %b %-d, %Y'): 101 | marked = value.strftime(RE_PERCENT_MINUS.sub(MARKER + "%", fmt)) 102 | return RE_REMOVE_MARKER.sub("", marked) 103 | 104 | 105 | ############################# 106 | # Escape Latex (http://flask.pocoo.org/snippets/55/) 107 | ############################# 108 | LATEX_SUBS = ( 109 | (re.compile(r'\\'), r'\\textbackslashzzz'), 110 | (re.compile(r'([{}_#%&$])'), r'\\\1'), 111 | (re.compile(r'~'), r'\\textasciitilde{}'), 112 | (re.compile(r'\^'), r'\\textasciicircum{}'), 113 | (re.compile(r'"'), r"''"), 114 | (re.compile(r'\.\.\.+'), r'\\ldots'), 115 | (re.compile(r'\\textbackslashzzz'), r'\\textbackslash{}'), 116 | ) 117 | 118 | def escape_tex(value): 119 | newval = value 120 | for pattern, replacement in LATEX_SUBS: 121 | newval = pattern.sub(replacement, newval) 122 | return newval 123 | 124 | 125 | ############################# 126 | # Base64 encode images 127 | ############################# 128 | try: 129 | from PIL import Image 130 | except ImportError: 131 | # if we don't have PIL available, include the image in its 132 | # original size 133 | def imgbase64(infile, max_size=None, dayone_folder=None): 134 | warn_once('imgbase64') 135 | filename, ext = os.path.splitext(infile) 136 | with open(dayone_folder + "/" + infile, "rb") as image_file: 137 | base64data = base64.b64encode(image_file.read()) 138 | return "data:image/%s;base64,%s" % (ext[1:], base64data) 139 | else: 140 | # if we have PIL, resize the image 141 | def imgbase64(infile, max_size=400, dayone_folder=None): 142 | size = max_size, max_size 143 | filename, ext = os.path.splitext(infile) 144 | im = Image.open(dayone_folder + "/" + infile) 145 | im.thumbnail(size, Image.ANTIALIAS) 146 | output = BytesIO() 147 | im.save(output, "jpeg") # we assume that we get best compressions with jpeg 148 | base64data = output.getvalue().encode("base64") 149 | return "data:image/jpeg;base64,%s" % (base64data) 150 | -------------------------------------------------------------------------------- /dayone_export/mdx_autobold.py: -------------------------------------------------------------------------------- 1 | """Autobold preprocessor for Markdown. 2 | 3 | Makes the first line of text into a heading. 4 | """ 5 | 6 | import markdown 7 | 8 | MAX_LEN = 99 9 | 10 | class AutoboldPreprocessor(markdown.preprocessors.Preprocessor): 11 | def run(self, lines): 12 | """Makes the first line a heading""" 13 | line = lines[0] 14 | if line.startswith('# ') or len(line) > MAX_LEN: 15 | return lines 16 | else: 17 | return ["# " + line] + lines[1:] 18 | 19 | class AutoboldExtension(markdown.Extension): 20 | """The extension to be installed""" 21 | def extendMarkdown(self, md, md_globals): 22 | md.preprocessors['autobold'] = AutoboldPreprocessor(md) 23 | 24 | def makeExtension(**kwargs) : 25 | return AutoboldExtension(**kwargs) 26 | -------------------------------------------------------------------------------- /dayone_export/mdx_hashtag.py: -------------------------------------------------------------------------------- 1 | """Hashtag preprocessor for Markdown. 2 | 3 | Changes lines beginning with #tag to \#tag to prevent #tag from 4 | becoming

tag

. 5 | """ 6 | 7 | import markdown 8 | import re 9 | 10 | # Global Vars 11 | HASHTAG_RE = re.compile('#\w') 12 | 13 | class HashtagPreprocessor(markdown.preprocessors.Preprocessor): 14 | def run(self, lines): 15 | """Add a backslash before #\w at the beginning of each line""" 16 | transformed = [] 17 | for line in lines: 18 | if HASHTAG_RE.match(line): # matches beginning of lines only 19 | line = '\\' + line 20 | transformed.append(line) 21 | 22 | return transformed 23 | 24 | class HashtagExtension(markdown.Extension): 25 | """The extension to be installed""" 26 | def extendMarkdown(self, md, md_globals): 27 | md.preprocessors.add('hashtag', HashtagPreprocessor(md), '>reference') 28 | 29 | def makeExtension(**kwargs) : 30 | return HashtagExtension(**kwargs) 31 | -------------------------------------------------------------------------------- /dayone_export/mdx_urlize.py: -------------------------------------------------------------------------------- 1 | # encoding: utf-8 2 | # Adapted from https://github.com/bruth/marky, by Byron Ruth 3 | 4 | import re 5 | import markdown 6 | import logging 7 | import time 8 | 9 | 10 | PROTOCOL_MATCH = re.compile(r'^(news|telnet|nttp|file|http|ftp|https)') 11 | # from John Gruber 12 | URLIZE_RE = '(?!%s)' % markdown.util.INLINE_PLACEHOLDER_PREFIX[1:] + \ 13 | r'''(?i)\b((?:[a-z][\w-]+:(?:/{1,3}|[a-z0-9%])|www\d{0,3}[.]|[a-z0-9.\-]+[.][a-z]{2,4}/)(?:[^\s()<>]+|\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\))+(?:\((?:[^\s()<>]+|(?:\([^\s()<>]+\)))*\)|[^\s`!()\[\]{};:'".,<>?''' + u"«»“”‘’]))" 14 | 15 | class UrlizePattern(markdown.inlinepatterns.Pattern): 16 | def handleMatch(self, m): 17 | url = text = m.group(2) 18 | 19 | if not PROTOCOL_MATCH.match(url): 20 | url = 'http://' + url 21 | 22 | el = markdown.util.etree.Element("a") 23 | el.set('href', url) 24 | el.text = markdown.util.AtomicString(text) 25 | return el 26 | 27 | class UrlizeExtension(markdown.Extension): 28 | "Urlize Extension for Python-Markdown." 29 | 30 | def extendMarkdown(self, md, md_globals): 31 | md.inlinePatterns['urlize'] = UrlizePattern(URLIZE_RE, md) 32 | 33 | def makeExtension(**kwargs): 34 | return UrlizeExtension(**kwargs) 35 | -------------------------------------------------------------------------------- /dayone_export/templates/default.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Journal Entries 5 | 6 | 7 | 46 | 47 | 48 |

Journal Entries

49 | {% for entry in journal %} 50 |
51 |

{{ entry['Date'] | format }}

52 |

53 | {% if entry.place() %} 54 | {{ entry.place() }}, 55 | {% endif %} 56 | {{ entry['Date'] | format('%-I:%M %p %Z') }} 57 |

58 |
59 | {% if 'Photo' in entry %} 60 | 61 | {% endif %} 62 | {{ entry['Text'] | markdown }} 63 |
64 |
65 | {% endfor %} 66 | 67 | 68 | -------------------------------------------------------------------------------- /dayone_export/templates/default.md: -------------------------------------------------------------------------------- 1 | {% for entry in journal %} 2 | {{ entry['Date']|format }} 3 | -------------------------- 4 | 5 | {{ entry['Text'] }} 6 | 7 | {% if 'Photo' in entry %} 8 | ![Photo for {{entry['Date']|format}}]({{ entry['Photo'] }}) 9 | {% endif %} 10 | 11 | 12 | {% endfor %} 13 | -------------------------------------------------------------------------------- /dayone_export/templates/default.org: -------------------------------------------------------------------------------- 1 | {% for entry in journal %} 2 | * {{ entry['Date'] | format('%Y%m%d-%H%M00%z') }} - dayone journal export 3 | ** journal entry metadata 4 | UUID: {{ entry['UUID'] }} 5 | {% if entry['Tags'] %} 6 | tags: {{ entry['Tags'] }} 7 | {% endif %} 8 | {% if entry.place() %} 9 | place: {{ entry.place() }} 10 | {% endif %} 11 | {% if entry['Weather'] %} 12 | weather: {{ entry.weather('F') }} 13 | {% endif %} 14 | {% if entry['Latitude'] %} 15 | coords: {{ entry['Latitude'] }} / {{ entry['Longitude'] }} (lat/lon) 16 | {% endif %} 17 | ** entry 18 | {% if entry['Photo'] %}[[file:{{ entry['Photo'] }}]]{% endif %} 19 | 20 | #+BEGIN_SRC markdown 21 | {{ entry['Text'] }} 22 | #+END_SRC 23 | 24 | {% endfor %} 25 | -------------------------------------------------------------------------------- /dayone_export/templates/default.tex: -------------------------------------------------------------------------------- 1 | \documentclass[11pt,letterpaper]{scrartcl} 2 | \usepackage{graphicx} 3 | \setcounter{secnumdepth}{0} 4 | \title{Journal Entries} 5 | \author{} 6 | \date{} 7 | \begin{document} 8 | \maketitle 9 | 10 | \CMD{for entry in journal} 11 | \section{\VAR{entry['Date'] | format('%A, %b %-d, %Y at %-I:%M %p %Z')}} 12 | \VAR{entry['Text'] | escape_tex} 13 | \CMD{if 'Photo' in entry} 14 | \begin{figure}[h!] 15 | \centering 16 | \includegraphics[width=0.5\textwidth]{\VAR{entry['Photo']}} 17 | \end{figure} 18 | \CMD{endif} 19 | \CMD{endfor} 20 | \end{document} 21 | -------------------------------------------------------------------------------- /dayone_export/templates/default.txt: -------------------------------------------------------------------------------- 1 | {% for entry in journal %} 2 | Date: {{ entry['Date']|format('%x %X') }} 3 | 4 | {{ entry['Text'] }} 5 | 6 | {% endfor %} 7 | -------------------------------------------------------------------------------- /dayone_export/templates/imgbase64.html: -------------------------------------------------------------------------------- 1 | {# 2 | This template encodes images into base64 and includes them in the 3 | output. Optionally uses the Python Imaging Library to resize images. 4 | To download the imaging library, see 5 | https://developers.google.com/appengine/docs/python/images/installingPIL 6 | #} 7 | 8 | 9 | 10 | 11 | Journal Entries 12 | 13 | 14 | 47 | 48 | 49 |

Journal Entries

50 | {% for entry in journal %} 51 |
52 |

{{ entry['Date']|format }}

53 |

54 | {% if entry.place() %} 55 | {{ entry.place() }}, 56 | {% endif %} 57 | {{ entry['Date']|format('%-I:%M %p %Z') }} 58 |

59 |
60 | {% if 'Photo' in entry %} 61 | 62 | {% endif %} 63 | {{ entry['Text'] | markdown }} 64 |
65 |
66 | {% endfor %} 67 | 68 | 69 | -------------------------------------------------------------------------------- /dayone_export/version.py: -------------------------------------------------------------------------------- 1 | VERSION = "1.0.0" 2 | -------------------------------------------------------------------------------- /docs/Makefile: -------------------------------------------------------------------------------- 1 | # Makefile for Sphinx documentation 2 | # 3 | 4 | # You can set these variables from the command line. 5 | SPHINXOPTS = 6 | SPHINXBUILD = sphinx-build 7 | PAPER = 8 | BUILDDIR = _build 9 | 10 | # Internal variables. 11 | PAPEROPT_a4 = -D latex_paper_size=a4 12 | PAPEROPT_letter = -D latex_paper_size=letter 13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 14 | # the i18n builder cannot share the environment and doctrees with the others 15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . 16 | 17 | .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext 18 | 19 | help: 20 | @echo "Please use \`make ' where is one of" 21 | @echo " html to make standalone HTML files" 22 | @echo " dirhtml to make HTML files named index.html in directories" 23 | @echo " singlehtml to make a single large HTML file" 24 | @echo " pickle to make pickle files" 25 | @echo " json to make JSON files" 26 | @echo " htmlhelp to make HTML files and a HTML help project" 27 | @echo " qthelp to make HTML files and a qthelp project" 28 | @echo " devhelp to make HTML files and a Devhelp project" 29 | @echo " epub to make an epub" 30 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" 31 | @echo " latexpdf to make LaTeX files and run them through pdflatex" 32 | @echo " text to make text files" 33 | @echo " man to make manual pages" 34 | @echo " texinfo to make Texinfo files" 35 | @echo " info to make Texinfo files and run them through makeinfo" 36 | @echo " gettext to make PO message catalogs" 37 | @echo " changes to make an overview of all changed/added/deprecated items" 38 | @echo " linkcheck to check all external links for integrity" 39 | @echo " doctest to run all doctests embedded in the documentation (if enabled)" 40 | 41 | clean: 42 | -rm -rf $(BUILDDIR)/* 43 | 44 | html: 45 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html 46 | @echo 47 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." 48 | 49 | dirhtml: 50 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml 51 | @echo 52 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." 53 | 54 | singlehtml: 55 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml 56 | @echo 57 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." 58 | 59 | pickle: 60 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle 61 | @echo 62 | @echo "Build finished; now you can process the pickle files." 63 | 64 | json: 65 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json 66 | @echo 67 | @echo "Build finished; now you can process the JSON files." 68 | 69 | htmlhelp: 70 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp 71 | @echo 72 | @echo "Build finished; now you can run HTML Help Workshop with the" \ 73 | ".hhp project file in $(BUILDDIR)/htmlhelp." 74 | 75 | qthelp: 76 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp 77 | @echo 78 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \ 79 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:" 80 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/DayOneExport.qhcp" 81 | @echo "To view the help file:" 82 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/DayOneExport.qhc" 83 | 84 | devhelp: 85 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp 86 | @echo 87 | @echo "Build finished." 88 | @echo "To view the help file:" 89 | @echo "# mkdir -p $$HOME/.local/share/devhelp/DayOneExport" 90 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/DayOneExport" 91 | @echo "# devhelp" 92 | 93 | epub: 94 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub 95 | @echo 96 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub." 97 | 98 | latex: 99 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 100 | @echo 101 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." 102 | @echo "Run \`make' in that directory to run these through (pdf)latex" \ 103 | "(use \`make latexpdf' here to do that automatically)." 104 | 105 | latexpdf: 106 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex 107 | @echo "Running LaTeX files through pdflatex..." 108 | $(MAKE) -C $(BUILDDIR)/latex all-pdf 109 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." 110 | 111 | text: 112 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text 113 | @echo 114 | @echo "Build finished. The text files are in $(BUILDDIR)/text." 115 | 116 | man: 117 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man 118 | @echo 119 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man." 120 | 121 | texinfo: 122 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 123 | @echo 124 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." 125 | @echo "Run \`make' in that directory to run these through makeinfo" \ 126 | "(use \`make info' here to do that automatically)." 127 | 128 | info: 129 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo 130 | @echo "Running Texinfo files through makeinfo..." 131 | make -C $(BUILDDIR)/texinfo info 132 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." 133 | 134 | gettext: 135 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale 136 | @echo 137 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." 138 | 139 | changes: 140 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes 141 | @echo 142 | @echo "The overview file is in $(BUILDDIR)/changes." 143 | 144 | linkcheck: 145 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck 146 | @echo 147 | @echo "Link check complete; look for any errors in the above output " \ 148 | "or in $(BUILDDIR)/linkcheck/output.txt." 149 | 150 | doctest: 151 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest 152 | @echo "Testing of doctests in the sources finished, look at the " \ 153 | "results in $(BUILDDIR)/doctest/output.txt." 154 | -------------------------------------------------------------------------------- /docs/cli.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: none 2 | 3 | Use the command line tool 4 | ========================= 5 | 6 | 7 | Basic Usage 8 | ----------- 9 | 10 | :: 11 | 12 | usage: dayone_export [--output FILE] [opts] journal 13 | 14 | Export Day One entries using a Jinja template 15 | 16 | positional arguments: 17 | journal path to Day One journal package 18 | 19 | optional arguments: 20 | -h, --help show this help message and exit 21 | --output FILE file to write (default print to stdout). Using strftime 22 | syntax will produce multiple output files with entries 23 | grouped by date. 24 | --format FMT output format (default guess from output file extension) 25 | --template NAME name or file of template to use 26 | --template-dir DIR location of templates (default ~/.dayone_export) 27 | --tags TAGS export entries with these comma-separated tags. Tag 28 | 'any' has a special meaning. 29 | --exclude TAGS exclude entries with these comma-separated tags 30 | --after DATE export entries published on or after this date 31 | --before DATE export entries published before this date 32 | --reverse display in reverse chronological order 33 | --autobold autobold first lines (titles) of posts 34 | --nl2br convert each new line to a
35 | --version show program's version number and exit 36 | 37 | If the Day One package has photos, you may need to copy the "photos" folder 38 | from the package into the same directory as the output file. 39 | 40 | Use a custom template 41 | --------------------- 42 | 43 | Use the ``--template`` option to specify a custom template. 44 | 45 | For information on how to create templates, see :ref:`templates`. 46 | 47 | 48 | Change the default template 49 | --------------------------- 50 | 51 | You can override the default template by creating a ``default.html`` file 52 | and placing it in the folder ``~/.dayone_export``. 53 | 54 | You can also create default templates of other types in a similar manner. 55 | For example, ``default.tex`` would be a default LaTeX template. 56 | The default markdown template should be called `default.md`. 57 | 58 | The program uses the extension of the output file to determine which 59 | default template to use. If there is no output file, use the 60 | ``--format`` option to specify the format. 61 | 62 | If you wish to use a directory other than ``~/.dayone_export``, as the 63 | location for default templates, you can use the ``--template-dir`` option. 64 | 65 | Filter by tag 66 | ------------- 67 | 68 | Use the ``--tags`` option with a comma-separated list of tags to include. 69 | 70 | If you use the option ``--tags any``, then any entry with at least one tag 71 | will be included. 72 | 73 | Also, you can exclude entries with specified tags, by using the ``--exclude`` 74 | option. Note that ``--exclude`` has a priority over ``--tags``. 75 | 76 | Limit export to recent entries 77 | ------------------------------ 78 | 79 | Use the ``--after`` option to only export entries after a certain date. 80 | 81 | For best results, use some kind of 82 | standard form for the date (e.g. ``2012-03-04``). 83 | 84 | Markdown options 85 | ---------------- 86 | 87 | The ``--autobold`` option will convert the first line of each post into a heading, 88 | as long as it is relatively short (similar to the way Day One optionally can) 89 | 90 | The ``--nl2br`` option will insert a ``
`` tag after each new line. 91 | 92 | 93 | Link to or embed photos 94 | ----------------------- 95 | 96 | The default html template refers to photos by their relative names. 97 | To show the photos in the output file, you will need to copy the ``photos`` 98 | directory from inside the `Journal.dayone` package into the same directory 99 | as the output html file. 100 | 101 | There is an alternate template which embeds photos directly into the html 102 | file as base64-encoded images. To use this template, use the option 103 | ``--template imgbase64.html``. 104 | 105 | Template filenames and grouping 106 | ------------------------------- 107 | 108 | The ``--output`` option specifies the output filename if you 109 | want something other than stdout. 110 | 111 | It also has another feature: you can include strftime-style_ formatting codes, 112 | in which case multiple files will be produced, each containing the journal 113 | entries with timestamps that result in the same filename. 114 | 115 | Examples: 116 | 117 | ``--output journal_%Y_%m.md`` will produces monthly files named 118 | journal_2013_02.md etc. 119 | 120 | ``--output diary_%a.html`` will produce a separate file for each weekday. 121 | 122 | Note that if you want a literal ``%`` in your output filename, you will need 123 | to escape it as ``%%``. 124 | 125 | .. _strftime-style: http://docs.python.org/2/library/datetime.html#strftime-strptime-behavior 126 | -------------------------------------------------------------------------------- /docs/conf.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | # 3 | # Day One Export documentation build configuration file, created by 4 | # sphinx-quickstart on Wed Sep 26 19:30:45 2012. 5 | # 6 | # This file is execfile()d with the current directory set to its containing dir. 7 | # 8 | # Note that not all possible configuration values are present in this 9 | # autogenerated file. 10 | # 11 | # All configuration values have a default; values that are commented out 12 | # serve to show the default. 13 | 14 | import sys, os 15 | on_rtd = os.environ.get('READTHEDOCS', None) == 'True' 16 | if on_rtd: 17 | html_theme = 'default' 18 | else: 19 | html_theme = 'nature' 20 | 21 | # If extensions (or modules to document with autodoc) are in another directory, 22 | # add these directories to sys.path here. If the directory is relative to the 23 | # documentation root, use os.path.abspath to make it absolute, like shown here. 24 | sys.path.insert(0, os.path.abspath('..')) 25 | # -- General configuration ----------------------------------------------------- 26 | 27 | # If your documentation needs a minimal Sphinx version, state it here. 28 | #needs_sphinx = '1.0' 29 | 30 | # Add any Sphinx extension module names here, as strings. They can be extensions 31 | # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. 32 | extensions = ['sphinx.ext.autodoc'] 33 | 34 | # Add any paths that contain templates here, relative to this directory. 35 | templates_path = ['_templates'] 36 | 37 | # The suffix of source filenames. 38 | source_suffix = '.rst' 39 | 40 | # The encoding of source files. 41 | #source_encoding = 'utf-8-sig' 42 | 43 | # The master toctree document. 44 | master_doc = 'index' 45 | 46 | # General information about the project. 47 | project = u'Day One Export' 48 | copyright = u'2012, Nathan Grigg' 49 | 50 | # The version info for the project you're documenting, acts as replacement for 51 | # |version| and |release|, also used in various other places throughout the 52 | # built documents. 53 | # 54 | # The default replacements for |version| and |release|, also used in various 55 | # other places throughout the built documents. 56 | # 57 | import pkg_resources 58 | try: 59 | release = pkg_resources.get_distribution('dayone_export').version 60 | except ImportError: 61 | print 'To build the documentation, The distribution information of' 62 | print 'dayone_export has to be available. Either install the package' 63 | print 'into your development environment or run "setup.py develop" to' 64 | print 'setup the metadata. A virtualenv is recommended!' 65 | sys.exit(1) 66 | version = '.'.join(release.split('.')[:2]) 67 | 68 | # The language for content autogenerated by Sphinx. Refer to documentation 69 | # for a list of supported languages. 70 | #language = None 71 | 72 | # There are two options for replacing |today|: either, you set today to some 73 | # non-false value, then it is used: 74 | #today = '' 75 | # Else, today_fmt is used as the format for a strftime call. 76 | #today_fmt = '%B %d, %Y' 77 | 78 | # List of patterns, relative to source directory, that match files and 79 | # directories to ignore when looking for source files. 80 | exclude_patterns = ['_build'] 81 | 82 | # The reST default role (used for this markup: `text`) to use for all documents. 83 | #default_role = None 84 | 85 | # If true, '()' will be appended to :func: etc. cross-reference text. 86 | #add_function_parentheses = True 87 | 88 | # If true, the current module name will be prepended to all description 89 | # unit titles (such as .. function::). 90 | #add_module_names = True 91 | 92 | # If true, sectionauthor and moduleauthor directives will be shown in the 93 | # output. They are ignored by default. 94 | #show_authors = False 95 | 96 | # The name of the Pygments (syntax highlighting) style to use. 97 | pygments_style = 'sphinx' 98 | 99 | # A list of ignored prefixes for module index sorting. 100 | #modindex_common_prefix = [] 101 | 102 | 103 | # -- Options for HTML output --------------------------------------------------- 104 | 105 | # The theme to use for HTML and HTML Help pages. See the documentation for 106 | # a list of builtin themes. 107 | # html_theme = 'nature' 108 | 109 | # Theme options are theme-specific and customize the look and feel of a theme 110 | # further. For a list of options available for each theme, see the 111 | # documentation. 112 | #html_theme_options = {} 113 | 114 | # Add any paths that contain custom themes here, relative to this directory. 115 | #html_theme_path = [] 116 | 117 | # The name for this set of Sphinx documents. If None, it defaults to 118 | # " v documentation". 119 | html_title = 'Day One Export documentation' 120 | 121 | # A shorter title for the navigation bar. Default is the same as html_title. 122 | html_short_title = '{0} {1} documentation'.format(project, version) 123 | 124 | # The name of an image file (relative to this directory) to place at the top 125 | # of the sidebar. 126 | #html_logo = None 127 | 128 | # The name of an image file (within the static path) to use as favicon of the 129 | # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 130 | # pixels large. 131 | #html_favicon = None 132 | 133 | # Add any paths that contain custom static files (such as style sheets) here, 134 | # relative to this directory. They are copied after the builtin static files, 135 | # so a file named "default.css" will overwrite the builtin "default.css". 136 | html_static_path = ['_static'] 137 | 138 | # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, 139 | # using the given strftime format. 140 | html_last_updated_fmt = '%b %d, %Y' 141 | 142 | # If true, SmartyPants will be used to convert quotes and dashes to 143 | # typographically correct entities. 144 | #html_use_smartypants = True 145 | 146 | # Custom sidebar templates, maps document names to template names. 147 | #html_sidebars = {} 148 | 149 | # Additional templates that should be rendered to pages, maps page names to 150 | # template names. 151 | #html_additional_pages = {} 152 | 153 | # If false, no module index is generated. 154 | #html_domain_indices = True 155 | 156 | # If false, no index is generated. 157 | html_use_index = False 158 | 159 | # If true, the index is split into individual pages for each letter. 160 | #html_split_index = False 161 | 162 | # If true, links to the reST sources are added to the pages. 163 | #html_show_sourcelink = True 164 | 165 | # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. 166 | #html_show_sphinx = True 167 | 168 | # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. 169 | html_show_copyright = False 170 | 171 | # If true, an OpenSearch description file will be output, and all pages will 172 | # contain a tag referring to it. The value of this option must be the 173 | # base URL from which the finished HTML is served. 174 | #html_use_opensearch = '' 175 | 176 | # This is the file name suffix for HTML files (e.g. ".xhtml"). 177 | #html_file_suffix = None 178 | 179 | # Output file base name for HTML help builder. 180 | htmlhelp_basename = 'DayOneExportdoc' 181 | 182 | 183 | # -- Options for LaTeX output -------------------------------------------------- 184 | 185 | latex_elements = { 186 | # The paper size ('letterpaper' or 'a4paper'). 187 | #'papersize': 'letterpaper', 188 | 189 | # The font size ('10pt', '11pt' or '12pt'). 190 | #'pointsize': '10pt', 191 | 192 | # Additional stuff for the LaTeX preamble. 193 | #'preamble': '', 194 | } 195 | 196 | # Grouping the document tree into LaTeX files. List of tuples 197 | # (source start file, target name, title, author, documentclass [howto/manual]). 198 | latex_documents = [ 199 | ('index', 'DayOneExport.tex', u'Day One Export Documentation', 200 | u'Nathan Grigg', 'manual'), 201 | ] 202 | 203 | # The name of an image file (relative to this directory) to place at the top of 204 | # the title page. 205 | #latex_logo = None 206 | 207 | # For "manual" documents, if this is true, then toplevel headings are parts, 208 | # not chapters. 209 | #latex_use_parts = False 210 | 211 | # If true, show page references after internal links. 212 | #latex_show_pagerefs = False 213 | 214 | # If true, show URL addresses after external links. 215 | #latex_show_urls = False 216 | 217 | # Documents to append as an appendix to all manuals. 218 | #latex_appendices = [] 219 | 220 | # If false, no module index is generated. 221 | #latex_domain_indices = True 222 | 223 | 224 | # -- Options for manual page output -------------------------------------------- 225 | 226 | # One entry per manual page. List of tuples 227 | # (source start file, name, description, authors, manual section). 228 | man_pages = [ 229 | ('index', 'dayoneexport', u'Day One Export Documentation', 230 | [u'Nathan Grigg'], 1) 231 | ] 232 | 233 | # If true, show URL addresses after external links. 234 | #man_show_urls = False 235 | 236 | 237 | # -- Options for Texinfo output ------------------------------------------------ 238 | 239 | # Grouping the document tree into Texinfo files. List of tuples 240 | # (source start file, target name, title, author, 241 | # dir menu entry, description, category) 242 | texinfo_documents = [ 243 | ('index', 'DayOneExport', u'Day One Export Documentation', 244 | u'Nathan Grigg', 'DayOneExport', 'One line description of project.', 245 | 'Miscellaneous'), 246 | ] 247 | 248 | # Documents to append as an appendix to all manuals. 249 | #texinfo_appendices = [] 250 | 251 | # If false, no module index is generated. 252 | #texinfo_domain_indices = True 253 | 254 | # How to display URL addresses: 'footnote', 'no', or 'inline'. 255 | #texinfo_show_urls = 'footnote' 256 | -------------------------------------------------------------------------------- /docs/index.rst: -------------------------------------------------------------------------------- 1 | Table of Contents 2 | ============================ 3 | 4 | .. note:: 5 | 6 | This tool is not compatible with current versions of the Day One journal 7 | format, and is probably not useful, except possibly as a jump-off point 8 | for someone to make a newer, compatible tool. 9 | 10 | .. note:: 11 | 12 | The Day One Export project is hosted at 13 | http://github.com/nathangrigg/dayone_export. 14 | 15 | Go there for installation instructions, 16 | to report issues, 17 | or if you are interested in contributing. 18 | 19 | .. toctree:: 20 | :maxdepth: 2 21 | 22 | cli 23 | templates 24 | module 25 | timezones 26 | -------------------------------------------------------------------------------- /docs/make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | 3 | REM Command file for Sphinx documentation 4 | 5 | if "%SPHINXBUILD%" == "" ( 6 | set SPHINXBUILD=sphinx-build 7 | ) 8 | set BUILDDIR=_build 9 | set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . 10 | set I18NSPHINXOPTS=%SPHINXOPTS% . 11 | if NOT "%PAPER%" == "" ( 12 | set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% 13 | set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% 14 | ) 15 | 16 | if "%1" == "" goto help 17 | 18 | if "%1" == "help" ( 19 | :help 20 | echo.Please use `make ^` where ^ is one of 21 | echo. html to make standalone HTML files 22 | echo. dirhtml to make HTML files named index.html in directories 23 | echo. singlehtml to make a single large HTML file 24 | echo. pickle to make pickle files 25 | echo. json to make JSON files 26 | echo. htmlhelp to make HTML files and a HTML help project 27 | echo. qthelp to make HTML files and a qthelp project 28 | echo. devhelp to make HTML files and a Devhelp project 29 | echo. epub to make an epub 30 | echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter 31 | echo. text to make text files 32 | echo. man to make manual pages 33 | echo. texinfo to make Texinfo files 34 | echo. gettext to make PO message catalogs 35 | echo. changes to make an overview over all changed/added/deprecated items 36 | echo. linkcheck to check all external links for integrity 37 | echo. doctest to run all doctests embedded in the documentation if enabled 38 | goto end 39 | ) 40 | 41 | if "%1" == "clean" ( 42 | for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i 43 | del /q /s %BUILDDIR%\* 44 | goto end 45 | ) 46 | 47 | if "%1" == "html" ( 48 | %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html 49 | if errorlevel 1 exit /b 1 50 | echo. 51 | echo.Build finished. The HTML pages are in %BUILDDIR%/html. 52 | goto end 53 | ) 54 | 55 | if "%1" == "dirhtml" ( 56 | %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml 57 | if errorlevel 1 exit /b 1 58 | echo. 59 | echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. 60 | goto end 61 | ) 62 | 63 | if "%1" == "singlehtml" ( 64 | %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml 65 | if errorlevel 1 exit /b 1 66 | echo. 67 | echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. 68 | goto end 69 | ) 70 | 71 | if "%1" == "pickle" ( 72 | %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle 73 | if errorlevel 1 exit /b 1 74 | echo. 75 | echo.Build finished; now you can process the pickle files. 76 | goto end 77 | ) 78 | 79 | if "%1" == "json" ( 80 | %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json 81 | if errorlevel 1 exit /b 1 82 | echo. 83 | echo.Build finished; now you can process the JSON files. 84 | goto end 85 | ) 86 | 87 | if "%1" == "htmlhelp" ( 88 | %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp 89 | if errorlevel 1 exit /b 1 90 | echo. 91 | echo.Build finished; now you can run HTML Help Workshop with the ^ 92 | .hhp project file in %BUILDDIR%/htmlhelp. 93 | goto end 94 | ) 95 | 96 | if "%1" == "qthelp" ( 97 | %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp 98 | if errorlevel 1 exit /b 1 99 | echo. 100 | echo.Build finished; now you can run "qcollectiongenerator" with the ^ 101 | .qhcp project file in %BUILDDIR%/qthelp, like this: 102 | echo.^> qcollectiongenerator %BUILDDIR%\qthelp\DayOneExport.qhcp 103 | echo.To view the help file: 104 | echo.^> assistant -collectionFile %BUILDDIR%\qthelp\DayOneExport.ghc 105 | goto end 106 | ) 107 | 108 | if "%1" == "devhelp" ( 109 | %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp 110 | if errorlevel 1 exit /b 1 111 | echo. 112 | echo.Build finished. 113 | goto end 114 | ) 115 | 116 | if "%1" == "epub" ( 117 | %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub 118 | if errorlevel 1 exit /b 1 119 | echo. 120 | echo.Build finished. The epub file is in %BUILDDIR%/epub. 121 | goto end 122 | ) 123 | 124 | if "%1" == "latex" ( 125 | %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex 126 | if errorlevel 1 exit /b 1 127 | echo. 128 | echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. 129 | goto end 130 | ) 131 | 132 | if "%1" == "text" ( 133 | %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text 134 | if errorlevel 1 exit /b 1 135 | echo. 136 | echo.Build finished. The text files are in %BUILDDIR%/text. 137 | goto end 138 | ) 139 | 140 | if "%1" == "man" ( 141 | %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man 142 | if errorlevel 1 exit /b 1 143 | echo. 144 | echo.Build finished. The manual pages are in %BUILDDIR%/man. 145 | goto end 146 | ) 147 | 148 | if "%1" == "texinfo" ( 149 | %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo 150 | if errorlevel 1 exit /b 1 151 | echo. 152 | echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. 153 | goto end 154 | ) 155 | 156 | if "%1" == "gettext" ( 157 | %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale 158 | if errorlevel 1 exit /b 1 159 | echo. 160 | echo.Build finished. The message catalogs are in %BUILDDIR%/locale. 161 | goto end 162 | ) 163 | 164 | if "%1" == "changes" ( 165 | %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes 166 | if errorlevel 1 exit /b 1 167 | echo. 168 | echo.The overview file is in %BUILDDIR%/changes. 169 | goto end 170 | ) 171 | 172 | if "%1" == "linkcheck" ( 173 | %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck 174 | if errorlevel 1 exit /b 1 175 | echo. 176 | echo.Link check complete; look for any errors in the above output ^ 177 | or in %BUILDDIR%/linkcheck/output.txt. 178 | goto end 179 | ) 180 | 181 | if "%1" == "doctest" ( 182 | %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest 183 | if errorlevel 1 exit /b 1 184 | echo. 185 | echo.Testing of doctests in the sources finished, look at the ^ 186 | results in %BUILDDIR%/doctest/output.txt. 187 | goto end 188 | ) 189 | 190 | :end 191 | -------------------------------------------------------------------------------- /docs/module.rst: -------------------------------------------------------------------------------- 1 | Module documentation 2 | ==================== 3 | 4 | Here is information about the module itself. 5 | 6 | You can use this in a Python script by using ``import dayone_export``. 7 | 8 | .. _Entry: 9 | 10 | The Entry class 11 | --------------- 12 | 13 | .. autoclass:: dayone_export.Entry 14 | 15 | .. automethod:: dayone_export.Entry.place([levels, ignore=None]) 16 | 17 | .. automethod:: dayone_export.Entry.keys 18 | 19 | 20 | Journal parsing and exporting 21 | ----------------------------- 22 | 23 | .. autofunction:: dayone_export.parse_journal(foldername[, reverse=False]) 24 | 25 | .. autofunction:: dayone_export.dayone_export(dayone_folder[, **kwargs]) 26 | 27 | -------------------------------------------------------------------------------- /docs/templates.rst: -------------------------------------------------------------------------------- 1 | .. highlight:: none 2 | 3 | .. _templates: 4 | 5 | Create your own template 6 | ======================== 7 | 8 | Templates are written using Jinja2 syntax. 9 | You can learn a lot from their excellent 10 | `Template Designer Documentation`__ 11 | 12 | __ http://jinja.pocoo.org/docs/templates/ 13 | 14 | When making your own template, a great place to start from is one of 15 | Day One Export's `built-in templates`__. 16 | 17 | __ https://github.com/nathangrigg/dayone_export/tree/master/dayone_export/templates 18 | 19 | The journal variable 20 | -------------------- 21 | 22 | The most important variable that the program passes to the template is named 23 | ``journal``. This is a list of Entry_ objects, each of which 24 | represents a single journal entry. 25 | 26 | Generally, a template will loop over elements of the journal variable, 27 | like this:: 28 | 29 | ... Document header, title, etc ... 30 | 31 | {% for entry in journal %} 32 | 33 | ... Code for a single entry ... 34 | 35 | {% endfor %} 36 | 37 | ... End of document stuff, etc ... 38 | 39 | 40 | Other variables 41 | --------------- 42 | 43 | - ``today``: The current date. 44 | 45 | 46 | .. _Entry: 47 | 48 | The Entry object 49 | ---------------- 50 | 51 | An Entry object behaves a lot like a Python dictionary, 52 | which means you can access the data fields by name. 53 | For example, you use ``entry['Text']`` to get the text of 54 | an entry. 55 | 56 | Jinja uses double braces to insert a variable into the document, 57 | so to insert the entry's text at a certain point in the document, you 58 | would include the following line in your template:: 59 | 60 | {{ entry['Text'] }} 61 | 62 | 63 | Here are some keys that an entry may have: 64 | 65 | - Basic information: 66 | - ``Date`` 67 | - ``Text`` 68 | - ``Starred`` (boolean) 69 | - ``UUID`` 70 | - ``Activity``: Description of motion activity, e.g. "Stationary" 71 | - ``Step Count``: Number of steps from the motion sensor 72 | - ``Photo`` (the relative path of the corresponding photo, if it exists) 73 | - Information about the location: 74 | - ``Place Name`` (e.g. Boom Noodle) 75 | - ``Locality`` (e.g. Seattle) 76 | - ``Administrative Area`` (e.g. Washington) 77 | - ``Country`` (e.g. United States) 78 | - ``Longitude`` 79 | - ``Latitude`` 80 | - Information about the currently-playing music: 81 | - ``Album`` 82 | - ``Artist`` 83 | - ``Track`` 84 | - Information about weather: 85 | - ``Fahrenheit`` 86 | - ``Celsius`` 87 | - ``Description`` 88 | - ``IconName`` 89 | - ``Sunrise Date``: The date and time of sunrise 90 | - ``Sunset Date``: The date and time of sunset 91 | - ``Visibility KM`` 92 | - ``Relative Humidity`` 93 | - ``Pressure MB`` 94 | - ``Wind Bearing`` 95 | - ``Wind Chill Celsius`` 96 | - ``Wind Speed KPH`` 97 | - Information about the creation device: 98 | - ``Device Agent`` 99 | - ``Host Name`` 100 | - ``OS Agent`` 101 | - ``Software Agent`` 102 | 103 | Jinja will just leave a blank space if you try to access a nonexistent key. 104 | So if an entry has no location information, ``{{ entry['Latitude'] }}`` 105 | will have no effect. 106 | 107 | For more information, see the documentation for :ref:`Entry`. 108 | 109 | 110 | Places 111 | ------ 112 | 113 | You may want to combine the place information into a single string. 114 | You can do this with the ``place`` method. 115 | 116 | With no arguments, ``entry.place()`` inserts the place names in order from 117 | smallest to largest, separated by commas. 118 | 119 | With a single integer argument, ``entry.place(n)`` inserts the place names 120 | from smallest to largest, but only uses the *n* smallest places. For example, 121 | ``entry.place(3)`` will always leave off the country. 122 | 123 | If you want to get more specific, you can use a list as an argument. 124 | So ``entry.place([1, 3])`` will put the *Locality* and *Country*, but leave 125 | off the *Place Name* and *Administrative Area*. 126 | 127 | Finally, you can use an ``ignore`` keyword argument to ignore a specific 128 | string. For example, ``entry.place(ignore="United States")`` will print 129 | the full location information, but leave off the country if it is 130 | "United States". 131 | 132 | Don't forget that to insert any of this into the document, you need to put it 133 | inside double braces. 134 | 135 | More information is available in the documentation for :ref:`Entry`. 136 | 137 | 138 | Weather 139 | ------ 140 | 141 | You may want to combine the weather into a single string. 142 | You can do this with the ``weather`` method. 143 | 144 | The ``weather`` method takes one parameter to display the temperature as celcius 145 | or fahrenheit. For example, ``entry.weather('F')`` will display the temperature 146 | in fahrenheit. The same can be done for celsius but with ``entry.weather('C')``. 147 | 148 | Don't forget that to insert any of this into the document, you need to put it 149 | inside double braces. 150 | 151 | More information is available in the documentation for :ref:`Entry`. 152 | 153 | 154 | Jinja Filters 155 | ------------- 156 | 157 | Jinja allows you to transform a variable before inserting it into the document, 158 | using a filter which is denoted by a ``|``. 159 | 160 | For example, ``{{ entry['Country'] | default("Unknown") }}`` 161 | pass the Country through the ``default`` filter, which in turn changes 162 | it to the string ``Unknown`` if the country does not exist. 163 | 164 | Since the ``default`` filter can be particularly useful, I will point out 165 | that it may happen that Day One has defined the country to be the 166 | empty string, in which case, the ``default`` filter will let it remain 167 | empty. If you want the filter to be more aggressive (you probably do), 168 | you can use ``{{ entry['Country'] | default("Unknown", true) }}`` 169 | 170 | There are several `built-in Jinja filters`__ available. 171 | 172 | __ http://jinja.pocoo.org/docs/templates/#builtin-filters 173 | 174 | 175 | Format dates 176 | ------------ 177 | 178 | This program defines a custom filter called ``format`` which formats 179 | dates. 180 | 181 | For example:: 182 | 183 | {{ entry['Date'] | format('%Y-%m-%d %H:%M:%S %z') }} 184 | 185 | The ``format`` filter also accepts an optional timezone argument, which 186 | overrides the native timezone of every entry. For example:: 187 | 188 | {{ entry['Date'] | format('%-I:%M %p %Z', tz='America/Los_Angeles') }} 189 | 190 | displays the date in US Pacific time, regardless of the timezone where 191 | the entry was recorded. 192 | 193 | Convert to Markdown 194 | ------------------- 195 | 196 | This program defines a custom filter called ``markdown`` which converts 197 | markdown text to html:: 198 | 199 | {{ entry['Text'] | markdown }} 200 | 201 | Latex Templates 202 | --------------- 203 | 204 | The standard Jinja template syntax clashes with many Latex control characters. 205 | If you create a Latex template, you will need to use different syntax. 206 | 207 | In a Latex template, you use ``\CMD{...}`` instead of ``{% ... %}`` for 208 | block statements and ``\VAR{...}`` instead of ``{{ ... }}`` to insert 209 | variables. For example:: 210 | 211 | \CMD{for entry in journal} 212 | \section{\VAR{entry['Date'] | format}} 213 | \CMD{endfor} 214 | 215 | You will also find the ``escape_tex`` filter useful, which escapes 216 | Latex control characters:: 217 | 218 | \VAR{entry['Text'] | escape_tex} 219 | 220 | Note that the ``markdown`` filter outputs HTML so should not be used. 221 | There is currently no support for converting markdown input 222 | to formatted Latex output. 223 | 224 | Latex templates must end with the ``.tex`` extension. 225 | 226 | 227 | Inline images with base64 encoding 228 | ---------------------------------- 229 | 230 | You can include the images inline with base64 encoding using a custom filter:: 231 | 232 | {{ entry['Photo'] | imgbase64 }} 233 | 234 | The resulting entry looks like:: 235 | 236 | 237 | 238 | The base64 data can become quite large in size. If you have the 239 | `Python imaging library`__ 240 | installed, you can resize the images so that the resulting output 241 | remains sufficiently small (default maximum size is 400 pixels):: 242 | 243 | {{ entry['Photo'] | imgbase64(800) }} 244 | 245 | This includes the image inline with a maxium size of 800 pixels. 246 | 247 | __ http://www.pythonware.com/products/pil/ 248 | 249 | More templating information 250 | --------------------------- 251 | 252 | For more details on Jinja templates, see the 253 | `Jinja template designer documentation`__. 254 | 255 | __ http://jinja.pocoo.org/docs/templates/ 256 | -------------------------------------------------------------------------------- /docs/timezones.rst: -------------------------------------------------------------------------------- 1 | Time Zone Information 2 | ===================== 3 | 4 | The Day One apps store all dates in UTC. Newer versions of Day One 5 | also include the time zone where each journal entry was entered. 6 | 7 | Entries without time zone information 8 | ------------------------------------- 9 | 10 | Older versions of Day One did not record the current time zone in 11 | the journal entry. For these entries, ``dayone_export`` makes a guess 12 | based on the time zone in other entries. 13 | 14 | If you would like to manually set the time zone in a journal entry which was 15 | recorded with an older version of Day One, insert the following section 16 | directly into the entry's plist file:: 17 | 18 | Time Zone 19 | America/Los_Angeles 20 | 21 | This should be a key in the top-level dictionary. For more guidance 22 | on where to place it, look at an entry created by a current version 23 | of Day One. 24 | 25 | Time Zone Names 26 | --------------- 27 | 28 | A list of time zone names can be found at 29 | http://en.wikipedia.org/wiki/List_of_tz_database_time_zones. 30 | 31 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | """ 2 | Command line script to transform your journal to html or some other format. 3 | 4 | Basic usage:: 5 | 6 | dayone_export [--output FILE] [opts] JOURNAL 7 | 8 | For more information:: 9 | 10 | dayone_export --help 11 | """ 12 | 13 | import sys 14 | try: 15 | from setuptools import setup 16 | except ImportError: 17 | sys.exit("""Error: Setuptools is required for installation. 18 | -> http://pypi.python.org/pypi/setuptools""") 19 | 20 | if sys.version_info < (2, 7) or ( 21 | sys.version_info[0] == 3 and sys.version_info < (3, 3)): 22 | sys.exit("Requires Python 2.7 or Python 3.3 or higher.") 23 | 24 | setup( 25 | name = "dayone_export", 26 | version = '1.0.0', 27 | description = "Export Day One journal using Jinja2 templates", 28 | author = "Nathan Grigg", 29 | author_email = "nathan@nathangrigg.net", 30 | packages = ["dayone_export"], 31 | package_data={'dayone_export': ['templates/*']}, 32 | include_package_data = True, 33 | url = 'https://github.com/nathangrigg/dayone_export/', 34 | entry_points = { 35 | 'console_scripts': ['dayone_export = dayone_export.cli:run'] 36 | }, 37 | license = "BSD", 38 | zip_safe = False, 39 | long_description = __doc__, 40 | install_requires = ['Jinja2>=2.6', 41 | 'pytz', 42 | 'python-dateutil>=2.1', 43 | 'Markdown>=2.5.1'], 44 | classifiers = [ 45 | "Programming Language :: Python :: 2", 46 | "Programming Language :: Python :: 2.7", 47 | "Programming Language :: Python :: 3", 48 | "Development Status :: 4 - Beta", 49 | "License :: OSI Approved :: BSD License", 50 | "Environment :: Console", 51 | "Intended Audience :: End Users/Desktop", 52 | "Operating System :: MacOS :: MacOS X", 53 | "Operating System :: POSIX", 54 | "Topic :: Office/Business :: News/Diary", 55 | "Topic :: Sociology :: History", 56 | "Topic :: Text Processing :: Markup :: HTML", 57 | "Topic :: Utilities", 58 | "Topic :: Text Processing :: General" 59 | ], 60 | ) 61 | -------------------------------------------------------------------------------- /tests/fake_journal/entries/00-first.doentry: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Creation Date 6 | 2012-01-01T00:48:00Z 7 | Entry Text 8 | 1: Basic example with no time zone 9 | Starred 10 | 11 | UUID 12 | 00F9FA96F29043D09638DF0866EC73B2 13 | 14 | 15 | -------------------------------------------------------------------------------- /tests/fake_journal/entries/for_exclude.doentry: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Creation Date 6 | 2013-12-07T12:48:01Z 7 | Entry Text 8 | 4: Basic example with uniq tag and no time zone 9 | Starred 10 | 11 | UUID 12 | 00F9FA91D39096A09638DC0866EC74B2 13 | Tags 14 | 15 | absolutelyuniqtag22 16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /tests/fake_journal/entries/full.doentry: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Creation Date 6 | 2012-01-02T00:00:00Z 7 | Entry Text 8 | 2: Full entry with time zone, location, weather and a tag 9 | Location 10 | 11 | Administrative Area 12 | Washington 13 | Country 14 | United States 15 | Latitude 16 | 47.831128653640743 17 | Locality 18 | Seattle 19 | Longitude 20 | -122.14663693802373 21 | Place Name 22 | Zoo 23 | 24 | Starred 25 | 26 | Time Zone 27 | America/Los_Angeles 28 | Weather 29 | 30 | Celsius 31 | 16 32 | Description 33 | Mostly Sunny 34 | Fahrenheit 35 | 61 36 | IconName 37 | pcloudy.png 38 | 39 | UUID 40 | 00F9FA96F29043D09638DF0866000002 41 | Tags 42 | 43 | tag 44 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /tests/fake_journal/entries/zz-last.doentry: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Activity 6 | Stationary 7 | Creation Date 8 | 2013-11-14T01:31:03Z 9 | Creator 10 | 11 | Device Agent 12 | iPhone/iPhone6,1 13 | Generation Date 14 | 2013-11-14T01:31:03Z 15 | Host Name 16 | Nathan's iPhone 17 | OS Agent 18 | iOS/7.0.3 19 | Software Agent 20 | Day One iOS/1.12 21 | 22 | Entry Text 23 | Testing again. 24 | Location 25 | 26 | Administrative Area 27 | CA 28 | Country 29 | United States 30 | Latitude 31 | 34.1234 32 | Locality 33 | Santa Barbara 34 | Longitude 35 | -118.1234 36 | Place Name 37 | Psych HQ 38 | 39 | Music 40 | 41 | Album 42 | Along With Me 43 | Artist 44 | Bill Charlap 45 | Track 46 | On Green Dolphin Street 47 | 48 | Starred 49 | 50 | Step Count 51 | 2652 52 | Time Zone 53 | America/Los_Angeles 54 | UUID 55 | AE742F904B8146DA8A0882D97953AFFA 56 | Weather 57 | 58 | Celsius 59 | 23 60 | Description 61 | Mostly Clear 62 | Fahrenheit 63 | 73 64 | IconName 65 | cloudyn.png 66 | Pressure MB 67 | 1012 68 | Relative Humidity 69 | 44 70 | Service 71 | HAMweather 72 | Sunrise Date 73 | 2013-11-13T14:25:15Z 74 | Sunset Date 75 | 2013-11-14T00:50:47Z 76 | Visibility KM 77 | 16.093440000000001 78 | Wind Bearing 79 | 250 80 | Wind Chill Celsius 81 | 23 82 | Wind Speed KPH 83 | 11 84 | 85 | 86 | 87 | -------------------------------------------------------------------------------- /tests/fake_journal/photos/00F9FA96F29043D09638DF0866EC73B2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathangrigg/dayone_export/d8ac1634e9096976aac1d1b69d022e7ecb64997a/tests/fake_journal/photos/00F9FA96F29043D09638DF0866EC73B2.jpg -------------------------------------------------------------------------------- /tests/fake_journal/photos/ignore.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nathangrigg/dayone_export/d8ac1634e9096976aac1d1b69d022e7ecb64997a/tests/fake_journal/photos/ignore.jpg -------------------------------------------------------------------------------- /tests/regression/entries/bad-date.doentry: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Creation Date 6 | Sun Jan 25 08:09:32 2015 7 | Creator 8 | 9 | Software Agent 10 | Slogger/2.1.14 11 | 12 | Entry Text 13 | blahblahblah 14 | 15 | 16 | Starred 17 | 18 | Tags 19 | 20 | 21 | UUID 22 | 132935E7B36C40C5ADC7EBF28D19C624 23 | 24 | 25 | -------------------------------------------------------------------------------- /tests/test_dayone_export.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import dayone_export as doe 3 | import dayone_export.cli 4 | from mock import patch 5 | import os 6 | import jinja2 7 | from datetime import datetime 8 | import pytz 9 | import locale 10 | 11 | THIS_PATH = os.path.split(os.path.abspath(__file__))[0] 12 | FAKE_JOURNAL = os.path.join(THIS_PATH, 'fake_journal') 13 | REGRESSION_JOURNAL = os.path.join(THIS_PATH, 'regression') 14 | 15 | def reset_locale(): 16 | locale.setlocale(locale.LC_ALL, "C") 17 | 18 | def has_locale(loc): 19 | try: 20 | locale.setlocale(locale.LC_ALL, loc) 21 | except locale.Error: 22 | return False 23 | reset_locale() 24 | return True 25 | 26 | 27 | LOCALE = {"en": "en_US.UTF-8", "fr": "fr_CH.UTF-8"} 28 | MISSING_LOCALES = {x for x in LOCALE.values() if not has_locale(x)} 29 | SkipIfMissingLocale = lambda loc: unittest.skipIf( 30 | loc in MISSING_LOCALES, "System lacks locale {}".format(loc)) 31 | 32 | 33 | class TestEntryObject(unittest.TestCase): 34 | def setUp(self): 35 | self.entry = doe.Entry(FAKE_JOURNAL + '/entries/full.doentry') 36 | self.entry.set_photo('foo') 37 | self.no_location = doe.Entry(FAKE_JOURNAL + '/entries/00-first.doentry') 38 | self.entry.set_time_zone('America/Los_Angeles') 39 | self.entry.set_localized_date('America/Los_Angeles') 40 | self.last_entry = doe.Entry(FAKE_JOURNAL + '/entries/zz-last.doentry') 41 | 42 | def test_tags(self): 43 | self.assertEqual(self.entry.data['Tags'], ['tag']) 44 | 45 | def test_set_photo(self): 46 | self.assertEqual(self.entry.data['Photo'], 'foo') 47 | 48 | def test_place_no_arguments(self): 49 | expected = 'Zoo, Seattle, Washington, United States' 50 | actual = self.entry.place() 51 | self.assertEqual(expected, actual) 52 | 53 | def test_place_int_argument(self): 54 | expected = 'Zoo, Seattle, Washington' 55 | actual = self.entry.place(3) 56 | self.assertEqual(expected, actual) 57 | 58 | def test_old_invalid_place_range_argument(self): 59 | self.assertRaises(TypeError, self.entry.place, 1, 3) 60 | 61 | def test_place_list_argument(self): 62 | expected = 'Seattle, United States' 63 | actual = self.entry.place([1, 3]) 64 | self.assertEqual(expected, actual) 65 | 66 | def test_place_no_location(self): 67 | self.assertEqual(self.no_location.place(), "") 68 | 69 | def test_place_ignore_argument(self): 70 | expected = 'Washington' 71 | actual = self.entry.place([2, 3], ignore='United States') 72 | self.assertEqual(expected, actual) 73 | 74 | def test_getitem_data_key(self): 75 | self.assertEqual(self.entry['Photo'], 'foo') 76 | 77 | def test_getitem_text(self): 78 | expected = '2: Full entry with time zone, location, weather and a tag' 79 | self.assertEqual(self.entry['Text'], expected) 80 | 81 | def test_getitem_date(self): 82 | date = self.entry['Date'] 83 | naive_date = date.replace(tzinfo = None) 84 | expected_date = datetime(2012, 1, 1, 16, 0) 85 | expected_zone = 'America/Los_Angeles' 86 | self.assertEqual(naive_date, expected_date) 87 | self.assertEqual(date.tzinfo.zone, expected_zone) 88 | 89 | def test_getitem_raises_keyerror(self): 90 | self.assertRaises(KeyError, lambda:self.entry['foo']) 91 | 92 | def test_getitem_flattened_dict(self): 93 | self.assertEqual( 94 | self.entry['Country'], self.entry['Location']['Country']) 95 | self.assertEqual( 96 | self.last_entry['Album'], self.last_entry['Music']['Album']) 97 | self.assertEqual( 98 | self.last_entry['Host Name'], 99 | self.last_entry['Creator']['Host Name']) 100 | self.assertEqual( 101 | self.last_entry['Relative Humidity'], 102 | self.last_entry['Weather']['Relative Humidity']) 103 | 104 | def test_get_keys_are_actually_keys(self): 105 | for key in self.entry.keys(): 106 | self.assertTrue(key in self.entry, key) 107 | 108 | class TestJournalParser(unittest.TestCase): 109 | def setUp(self): 110 | self.j = doe.parse_journal(FAKE_JOURNAL) 111 | 112 | def test_automatically_set_photos(self): 113 | expected = 'photos/00F9FA96F29043D09638DF0866EC73B2.jpg' 114 | actual = self.j[0]['Photo'] 115 | self.assertEqual(expected, actual) 116 | 117 | def test_sort_order(self): 118 | j = self.j 119 | k = 'Creation Date' 120 | result = j[0][k] <= j[1][k] <= j[2][k] 121 | self.assertTrue(result) 122 | 123 | @patch('jinja2.Template.render') 124 | def test_dayone_export_run(self, mock_render): 125 | list(doe.dayone_export(FAKE_JOURNAL)) 126 | mock_render.assert_called() 127 | 128 | @patch('jinja2.Template.render') 129 | def test_dayone_export_run_with_naive_after(self, mock_render): 130 | list(doe.dayone_export(FAKE_JOURNAL, after=datetime(2012, 9, 1))) 131 | mock_render.assert_called() 132 | 133 | @patch('jinja2.Template.render') 134 | def test_dayone_export_run_with_localized_after(self, mock_render): 135 | after = pytz.timezone('America/New_York').localize(datetime(2012, 9, 1)) 136 | list(doe.dayone_export(FAKE_JOURNAL, after=after)) 137 | mock_render.assert_called() 138 | 139 | def test_after_filter(self): 140 | filtered = doe._filter_by_date(self.j, datetime(2013, 11, 14), None) 141 | self.assertEqual(len(filtered), 2) 142 | filtered = doe._filter_by_date(self.j, datetime(2013, 11, 15), None) 143 | self.assertEqual(len(filtered), 1) 144 | 145 | def test_before_filter(self): 146 | filtered = doe._filter_by_date(self.j, None, datetime(2013, 11, 14)) 147 | self.assertEqual(len(filtered), 2) 148 | filtered = doe._filter_by_date(self.j, None, datetime(2013, 11, 15)) 149 | self.assertEqual(len(filtered), 3) 150 | 151 | def test_two_sided_date_filter(self): 152 | filtered = doe._filter_by_date( 153 | self.j, datetime(2013, 11, 14), datetime(2013, 11, 15)) 154 | self.assertEqual(len(filtered), 1) 155 | 156 | def test_tags_any_tag(self): 157 | filtered = doe._filter_by_tag(self.j, 'any') 158 | self.assertEqual(len(list(filtered)), 2) 159 | 160 | def test_tags_one_tag(self): 161 | filtered = doe._filter_by_tag(self.j, ['tag']) 162 | self.assertEqual(len(list(filtered)), 1) 163 | 164 | def test_tags_no_matches(self): 165 | filtered = doe._filter_by_tag(self.j, ['porcupine']) 166 | self.assertEqual(len(list(filtered)), 0) 167 | 168 | def test_exclude_nonexistent_tag(self): 169 | actual_size = len(self.j) 170 | after_exlusion = doe._exclude_tags(self.j, ['porcupine']) 171 | self.assertEqual(actual_size, len(list(after_exlusion))) 172 | 173 | def test_exclude_multiple_nonexistent_tags(self): 174 | actual_size = len(self.j) 175 | after_exlusion = doe._exclude_tags(self.j, ['porcupine', 'nosuchtag']) 176 | self.assertEqual(actual_size, len(list(after_exlusion))) 177 | 178 | def test_exclude_tag(self): 179 | actual_size = len(self.j) 180 | after_exlusion = doe._exclude_tags(self.j, ['absolutelyuniqtag22']) 181 | self.assertEqual(len(list(after_exlusion)), actual_size-1) 182 | 183 | def test_tags_and_exclude_combined(self): 184 | actual_size = len(self.j) 185 | filtered = doe._filter_by_tag(self.j, 'any') 186 | after_exlusion = doe._exclude_tags(filtered, ['absolutelyuniqtag22']) 187 | self.assertEqual(len(list(after_exlusion)), 1) 188 | 189 | @patch('jinja2.Template.render') 190 | def test_file_splitter(self, mock_render): 191 | gen = doe.dayone_export(FAKE_JOURNAL) 192 | self.assertEqual(len(list(gen)), 1) 193 | # If doing careful date comparisons, beware of timezones 194 | gen = doe.dayone_export(FAKE_JOURNAL, filename_template="%Y") 195 | fnames = sorted(fn for fn, _ in gen) 196 | self.assertEqual(fnames, ["2011", "2012", "2013"]) 197 | gen = doe.dayone_export(FAKE_JOURNAL, filename_template="%Y%m%d") 198 | fnames = sorted(fn for fn, _ in gen) 199 | self.assertEqual(fnames, ["20111231", "20120101", "20131113", "20131207"]) 200 | 201 | 202 | 203 | class TestTemplateInheritance(unittest.TestCase): 204 | def setUp(self): 205 | self.patcher1 = patch('jinja2.ChoiceLoader', side_effect=lambda x:x) 206 | self.patcher2 = patch('jinja2.FileSystemLoader', side_effect=lambda x:x) 207 | self.patcher3 = patch('jinja2.PackageLoader', side_effect=lambda x:x) 208 | self.patcher1.start() 209 | self.patcher2.start() 210 | self.patcher3.start() 211 | self.dir = os.path.expanduser('~/.dayone_export') 212 | 213 | def tearDown(self): 214 | self.patcher1.stop() 215 | self.patcher2.stop() 216 | self.patcher3.stop() 217 | 218 | def test_explicit_template(self): 219 | actual = doe._determine_inheritance('a/b', 'ccc', 'ddd') 220 | expected = 'a', 'b' 221 | self.assertEqual(actual, expected) 222 | 223 | def test_no_template_no_dir_no_format(self): 224 | actual = doe._determine_inheritance(None, None, None) 225 | expected = [[self.dir], 'dayone_export'], 'default.html' 226 | self.assertEqual(actual, expected) 227 | 228 | def test_yes_template_no_dir_no_format(self): 229 | actual = doe._determine_inheritance('foo', None, None) 230 | expected = [['.', self.dir], 'dayone_export'], 'foo' 231 | self.assertEqual(actual, expected) 232 | 233 | def test_no_template_yes_dir_no_format(self): 234 | actual = doe._determine_inheritance(None, 'bar', None) 235 | expected = 'bar', 'default.html' 236 | self.assertEqual(actual, expected) 237 | 238 | def test_yes_template_yes_dir_no_format(self): 239 | actual = doe._determine_inheritance('foo', 'bar', None) 240 | expected = 'bar', 'foo' 241 | self.assertEqual(actual, expected) 242 | 243 | def test_no_template_no_dir_yes_format(self): 244 | actual = doe._determine_inheritance(None, None, 'text') 245 | expected = [[self.dir], 'dayone_export'], 'default.text' 246 | self.assertEqual(actual, expected) 247 | 248 | def test_yes_template_no_dir_yes_format(self): 249 | actual = doe._determine_inheritance('foo', None , 'text') 250 | expected = [['.', self.dir], 'dayone_export'], 'foo' 251 | self.assertEqual(actual, expected) 252 | 253 | def test_no_template_yes_dir_yes_format(self): 254 | actual = doe._determine_inheritance(None, 'bar', 'text') 255 | expected = 'bar', 'default.text' 256 | self.assertEqual(actual, expected) 257 | 258 | def test_yes_template_yes_dir_yes_format(self): 259 | actual = doe._determine_inheritance('foo', 'bar', 'text') 260 | expected = 'bar', 'foo' 261 | self.assertEqual(actual, expected) 262 | 263 | class TestCLI(unittest.TestCase): 264 | def setUp(self): 265 | self.silencer = patch('sys.stdout') 266 | self.silencer.start() 267 | 268 | def tearDown(self): 269 | self.silencer.stop() 270 | 271 | @patch('dayone_export.cli.dayone_export', return_value="") 272 | def test_tag_splitter_protects_any(self, mock_doe): 273 | dayone_export.cli.run(['--tags', 'any', FAKE_JOURNAL]) 274 | expected = 'any' 275 | actual = mock_doe.call_args[1]['tags'] 276 | self.assertEqual(expected, actual) 277 | 278 | @patch('dayone_export.cli.dayone_export', return_value="") 279 | def test_tag_splitter(self, mock_doe): 280 | dayone_export.cli.run(['--tags', 'a, b', FAKE_JOURNAL]) 281 | expected = ['a', 'b'] 282 | actual = mock_doe.call_args[1]['tags'] 283 | self.assertEqual(expected, actual) 284 | 285 | def test_invalid_package(self): 286 | actual = dayone_export.cli.run(['.']) 287 | expected = 'Not a valid Day One package' 288 | self.assertTrue(actual.startswith(expected), actual) 289 | 290 | @patch('dayone_export.jinja2.Template.render', side_effect=jinja2.TemplateNotFound('msg')) 291 | def test_template_not_found(self, mock_doe): 292 | actual = dayone_export.cli.run([FAKE_JOURNAL]) 293 | expected = "Template not found" 294 | self.assertTrue(actual.startswith(expected), actual) 295 | 296 | 297 | class TestMarkdown(unittest.TestCase): 298 | """Test the markdown formatter""" 299 | def setUp(self): 300 | self.md = doe.filters.markdown_filter() 301 | self.autobold = doe.filters.markdown_filter(autobold=True) 302 | self.nl2br = doe.filters.markdown_filter(nl2br=True) 303 | 304 | def test_basic_markdown(self): 305 | expected = '

This is a test.

' 306 | actual = self.md('This *is* a **test**.') 307 | self.assertEqual(expected, actual) 308 | 309 | def test_urlize_http(self): 310 | expected = '

xx (http://url.com) xx

' 311 | actual = self.md('xx (http://url.com) xx') 312 | self.assertEqual(expected, actual) 313 | 314 | def test_urlize_www(self): 315 | expected = '

xx www.google.com xx

' 316 | actual = self.md('xx www.google.com xx') 317 | self.assertEqual(expected, actual) 318 | 319 | def test_urlize_no_www(self): 320 | expected = '

xx bit.ly/blah xx

' 321 | actual = self.md('xx bit.ly/blah xx') 322 | self.assertEqual(expected, actual) 323 | 324 | def test_urlize_quotes(self): 325 | expected = '

"www.url.com"

' 326 | actual = self.md('"www.url.com"') 327 | self.assertEqual(expected, actual) 328 | 329 | def test_urlize_period(self): 330 | expected = '

See http://url.com.

' 331 | actual = self.md('See http://url.com.') 332 | self.assertEqual(expected, actual) 333 | 334 | def test_two_footnotes(self): 335 | """Make sure the footnote counter is working""" 336 | text = "Footnote[^1]\n\n[^1]: Footnote text" 337 | self.assertNotEqual(self.md(text), self.md(text)) 338 | 339 | def test_hashtag_does_not_become_h1(self): 340 | expected = '

#tag and #tag

' 341 | actual = self.md('#tag and #tag') 342 | self.assertEqual(expected, actual) 343 | 344 | def test_h1_becomes_h1(self): 345 | expected = '

tag and #tag

' 346 | actual = self.md('# tag and #tag') 347 | self.assertEqual(expected, actual) 348 | 349 | def test_autobold(self): 350 | expected = '

This is a title

\n

This is the next line

' 351 | actual = self.autobold('This is a title\nThis is the next line') 352 | self.assertEqual(expected, actual) 353 | 354 | def test_autobold_doesnt_happen_on_long_line(self): 355 | expected = '

AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA

' 356 | actual = self.autobold('AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA') 357 | self.assertEqual(expected, actual) 358 | 359 | def test_nl2br(self): 360 | expected = '

a
\nb

' 361 | actual = self.nl2br('a\nb') 362 | self.assertEqual(expected, actual) 363 | 364 | class TestLatex(unittest.TestCase): 365 | def setUp(self): 366 | reset_locale() 367 | 368 | def test_latex_escape_backslash(self): 369 | actual = doe.filters.escape_tex(r'bl\ah') 370 | expected = r'bl\textbackslash{}ah' 371 | self.assertEqual(expected, actual) 372 | 373 | def test_latex_escape_dollar(self): 374 | actual = doe.filters.escape_tex(r'bl$ah') 375 | expected = r'bl\$ah' 376 | self.assertEqual(expected, actual) 377 | 378 | def test_latex_escape_symbols(self): 379 | actual = doe.filters.escape_tex(r'${}#^&~') 380 | expected = r'\$\{\}\#\textasciicircum{}\&\textasciitilde{}' 381 | self.assertEqual(expected, actual) 382 | 383 | def test_latex_sanity(self): 384 | _, actual = next(doe.dayone_export(FAKE_JOURNAL, format='tex')) 385 | expected = r'\documentclass' 386 | self.assertEqual(actual[:14], expected) 387 | 388 | 389 | class TestDateFormat(unittest.TestCase): 390 | def setUp(self): 391 | reset_locale() 392 | self.date = datetime(2014, 2, 3) 393 | 394 | def test_default_format(self): 395 | expected = 'Monday, Feb 3, 2014' 396 | self.assertEqual(expected, doe.filters.format(self.date)) 397 | self.assertEqual(expected, doe.filters._strftime_portable(self.date)) 398 | 399 | def test_format_leave_zero(self): 400 | expected = '2014-02-03' 401 | self.assertEqual(expected, doe.filters.format(self.date, '%Y-%m-%d')) 402 | self.assertEqual( 403 | expected, doe.filters._strftime_portable(self.date, '%Y-%m-%d')) 404 | 405 | def test_format_remove_zero(self): 406 | expected = '2/3/2014' 407 | self.assertEqual( 408 | expected, doe.filters.format(self.date, '%-m/%-d/%Y')) 409 | self.assertEqual( 410 | expected, doe.filters._strftime_portable(self.date, '%-m/%-d/%Y')) 411 | 412 | class TestDefaultTemplates(unittest.TestCase): 413 | def setUp(self): 414 | self.silencer = patch('sys.stdout') 415 | self.silencer.start() 416 | 417 | def tearDown(self): 418 | self.silencer.stop() 419 | 420 | @SkipIfMissingLocale(LOCALE["en"]) 421 | def test_default_html_template_english(self): 422 | code = dayone_export.cli.run(["--locale", LOCALE["en"], FAKE_JOURNAL]) 423 | self.assertFalse(code) 424 | 425 | @SkipIfMissingLocale(LOCALE["fr"]) 426 | def test_default_html_template_french(self): 427 | code = dayone_export.cli.run(["--locale", LOCALE["fr"], FAKE_JOURNAL]) 428 | self.assertFalse(code) 429 | 430 | class TestRegression(unittest.TestCase): 431 | def setUp(self): 432 | self.silencer = patch('sys.stdout') 433 | self.silencer.start() 434 | 435 | def tearDown(self): 436 | self.silencer.stop() 437 | 438 | def test_regression(self): 439 | with self.assertRaisesRegexp(doe.PlistError, "ISO 8601"): 440 | doe.Entry(os.path.join(REGRESSION_JOURNAL, "entries/bad-date.doentry")) 441 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = py27,py33,py34 3 | 4 | [testenv] 5 | deps= 6 | nose 7 | mock 8 | changedir=tests 9 | commands=nosetests [] 10 | --------------------------------------------------------------------------------