From 9a8ac3886ef1e0204575be88c645bb2b6ffd874a Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL Date: Wed, 13 Nov 2019 13:55:16 +1100 Subject: [PATCH 01/20] Open current markdown file in a new window It works for saved and unsaved files. Maybe unsaved file's content should be written to a temporary file in case we crash, so that the user doesn't lose all it's content. --- MarkdownLivePreview.py | 64 ++++++++++++++++++++++++++++ MarkdownLivePreview.sublime-commands | 7 +++ utils.py | 4 ++ 3 files changed, 75 insertions(+) create mode 100644 MarkdownLivePreview.py create mode 100644 MarkdownLivePreview.sublime-commands create mode 100644 utils.py diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py new file mode 100644 index 0000000..1e28f1c --- /dev/null +++ b/MarkdownLivePreview.py @@ -0,0 +1,64 @@ +import sublime +import sublime_plugin + +from .utils import * + +def plugin_loaded(): + pass + +class MdlpInsertCommand(sublime_plugin.TextCommand): + + def run(self, edit, point, string): + self.view.insert(edit, point, string) + +class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): + + def run(self, edit): + + """ If the file is saved exists on disk, we close it, and reopen it in a new + window. Otherwise, we copy the content, erase it all (to close the file without + a dialog) and re-insert it into a new view into a new window """ + + original_view = self.view + file_name = original_view.file_name() + + syntax_file = original_view.settings().get('syntax') + + if file_name is None: + + # the file isn't saved, we need to restore the content manually + total_region = sublime.Region(0, original_view.size()) + content = original_view.substr(total_region) + original_view.erase(edit, total_region) + original_view.close() + + # FIXME: save the document to a temporary file, so that if we crash, + # the user doesn't lose what he wrote + + else: + original_view.close() + + sublime.run_command('new_window') + window = sublime.active_window() + + window.run_command('set_layout', { + 'cols': [0.0, 0.5, 1.0], + 'rows': [0.0, 1.0], + 'cells': [[0, 0, 1, 1], [1, 0, 2, 1]] + }) + + window.focus_group(0) + if file_name: + new_view = window.open_file(file_name) + else: + new_view = window.new_file() + new_view.run_command('mdlp_insert', {'point': 0, 'string': content}) + + new_view.set_syntax_file(syntax_file) + + def is_enabled(self): + # FIXME: is this the best way there is to check if the current syntax is markdown? + # should we only support default markdown? + # what about "md"? + return 'markdown' in self.view.settings().get('syntax').lower() + diff --git a/MarkdownLivePreview.sublime-commands b/MarkdownLivePreview.sublime-commands new file mode 100644 index 0000000..cdca348 --- /dev/null +++ b/MarkdownLivePreview.sublime-commands @@ -0,0 +1,7 @@ +[ + + { + "caption": "MarkdownLivePreview: Open Preview", + "command": "open_markdown_preview" + } +] \ No newline at end of file diff --git a/utils.py b/utils.py new file mode 100644 index 0000000..e4339f7 --- /dev/null +++ b/utils.py @@ -0,0 +1,4 @@ +import sublime + +def get_settings(): + return sublime.get_settings("MarkdownLivePreview.sublime-settings") \ No newline at end of file From d3d88ddb49191904569b6a97de31a4ac6ff16f2b Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL Date: Thu, 14 Nov 2019 11:16:00 +1100 Subject: [PATCH 02/20] Add basic readme.md --- README.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..089b543 --- /dev/null +++ b/README.md @@ -0,0 +1,4 @@ +# MarkdownLivePreview + +A simple plugin to preview your markdown as you type right in Sublime Text. +No dependencies! From 7f7dcd6ba89a77f09a4b1a18c6e84685b337a1bb Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL Date: Thu, 14 Nov 2019 11:18:18 +1100 Subject: [PATCH 03/20] Restore the markdown_view as an original_view See the top of MarkdownLivePreview.py for terminology Doesn't work well for unsaved files, as pre_close is triggered after the confirmation dialog --- MarkdownLivePreview.py | 75 ++++++++++++++++++++++++++++++++++++++---- 1 file changed, 68 insertions(+), 7 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 1e28f1c..1fac793 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -6,6 +6,14 @@ from .utils import * def plugin_loaded(): pass +SETTING_MDLP = "markdown_live_preview" + +# original_view: the view in the regular editor, without it's own window +# markdown_view: the markdown view, in the special window +# preview_view: the preview view, in the special window +# original_window: the regular window +# preview_window: the window with the markdown file and the preview + class MdlpInsertCommand(sublime_plugin.TextCommand): def run(self, edit, point, string): @@ -20,10 +28,12 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): a dialog) and re-insert it into a new view into a new window """ original_view = self.view + original_window_id = original_view.window().id() file_name = original_view.file_name() syntax_file = original_view.settings().get('syntax') + if file_name is None: # the file isn't saved, we need to restore the content manually @@ -39,22 +49,25 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): original_view.close() sublime.run_command('new_window') - window = sublime.active_window() + preview_window = sublime.active_window() - window.run_command('set_layout', { + preview_window.run_command('set_layout', { 'cols': [0.0, 0.5, 1.0], 'rows': [0.0, 1.0], 'cells': [[0, 0, 1, 1], [1, 0, 2, 1]] }) - window.focus_group(0) + preview_window.focus_group(0) if file_name: - new_view = window.open_file(file_name) + markdown_view = preview_window.open_file(file_name) else: - new_view = window.new_file() - new_view.run_command('mdlp_insert', {'point': 0, 'string': content}) + markdown_view = preview_window.new_file() + markdown_view.run_command('mdlp_insert', {'point': 0, 'string': content}) - new_view.set_syntax_file(syntax_file) + markdown_view.set_syntax_file(syntax_file) + markdown_view.settings().set(SETTING_MDLP, { + "original_window_id": original_window_id + }) def is_enabled(self): # FIXME: is this the best way there is to check if the current syntax is markdown? @@ -62,3 +75,51 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): # what about "md"? return 'markdown' in self.view.settings().get('syntax').lower() +class MarkdownLivePreviewListener(sublime_plugin.EventListener): + + def on_pre_close(self, markdown_view): + """ Close the view in the preview window, and store information for the on_close + listener (see doc there) + """ + print('pre close') + + if not markdown_view.settings().get(SETTING_MDLP): + return + + self.markdown_view = markdown_view + self.preview_window = markdown_view.window() + self.file_name = markdown_view.file_name() + + if self.file_name is None: + # FIXME: this is duplicated code. How should it be generalized? + total_region = sublime.Region(0, markdown_view.size()) + self.content = markdown_view.substr(total_region) + markdown_view.erase(edit, total_region) + else: + self.content = None + + def on_close(self, markdown_view): + """ Use the information saved to restore the markdown_view as an original_view + """ + infos = markdown_view.settings().get(SETTING_MDLP) + if not infos: + return + + assert markdown_view.id() == self.markdown_view.id(), \ + "pre_close view.id() != close view.id()" + + self.preview_window.run_command('close_window') + + # find the window with the right id + original_window = next(window for window in sublime.windows() \ + if window.id() == infos['original_window_id']) + print(original_window.id(), self.preview_window.id(), infos) + if self.file_name: + original_window.open_file(self.file_name) + else: + # note here that this is called original_view, because it's what semantically + # makes sense, but this original_view.id() will be different than the one + # that we closed first to reopen in the preview window + # shouldn't cause any trouble though + original_view = original_window.new_file() + original_view.run_command('mdlp_insert', {'point': 0, 'string': self.content}) From 61cf2984eba7d6430bf7d8b0adc2bcac5daea8b2 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL Date: Thu, 14 Nov 2019 11:24:31 +1100 Subject: [PATCH 04/20] Fix restoration of markdown_view as an original_view for unsaved files It use to ask for confirmation (to save) because on_pre_close is run after this dialog. Hence, we set the markdown_view as scratch for unsaved files. :^) --- MarkdownLivePreview.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 1fac793..6e0dba5 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -34,19 +34,17 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): syntax_file = original_view.settings().get('syntax') - if file_name is None: - + if file_name: + original_view.close() + else: # the file isn't saved, we need to restore the content manually total_region = sublime.Region(0, original_view.size()) content = original_view.substr(total_region) original_view.erase(edit, total_region) original_view.close() - # FIXME: save the document to a temporary file, so that if we crash, # the user doesn't lose what he wrote - else: - original_view.close() sublime.run_command('new_window') preview_window = sublime.active_window() @@ -63,6 +61,7 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): else: markdown_view = preview_window.new_file() markdown_view.run_command('mdlp_insert', {'point': 0, 'string': content}) + markdown_view.set_scratch(True) markdown_view.set_syntax_file(syntax_file) markdown_view.settings().set(SETTING_MDLP, { @@ -81,8 +80,6 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): """ Close the view in the preview window, and store information for the on_close listener (see doc there) """ - print('pre close') - if not markdown_view.settings().get(SETTING_MDLP): return @@ -113,13 +110,16 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): # find the window with the right id original_window = next(window for window in sublime.windows() \ if window.id() == infos['original_window_id']) - print(original_window.id(), self.preview_window.id(), infos) if self.file_name: original_window.open_file(self.file_name) else: + assert markdown_view.is_scratch(), "markdown view of an unsaved file should " \ + "be a scratch" # note here that this is called original_view, because it's what semantically # makes sense, but this original_view.id() will be different than the one # that we closed first to reopen in the preview window # shouldn't cause any trouble though original_view = original_window.new_file() original_view.run_command('mdlp_insert', {'point': 0, 'string': self.content}) + + original_view.set_syntax_file(markdown_view.settings().get('syntax')) \ No newline at end of file From 8eb6882d601b029fc99b71d49b08c1efb68c9ee7 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL Date: Thu, 14 Nov 2019 12:16:18 +1100 Subject: [PATCH 05/20] Update readme.md with contributing info Jokes, I haven't even set black on my own editor yet... --- README.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/README.md b/README.md index 089b543..5a8cef2 100644 --- a/README.md +++ b/README.md @@ -2,3 +2,17 @@ A simple plugin to preview your markdown as you type right in Sublime Text. No dependencies! + +## How to install + +It's available on package control! + +## How to contribute + +1. Fork this repo +2. Make your own branch (the name of the branch should be the feature you are + implementing eg. `improve-tables`, `fix-crash-on-multiple-preview` +3. All your code should be formated by black. +4. Send a PR! + + From 6bb8e6ebaaa3e6654c8510b3d5d6da49929ccc63 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL Date: Thu, 14 Nov 2019 15:47:42 +1100 Subject: [PATCH 06/20] update preview when the user types We don't have any delay in between updates (because i'm scared of threading), which has a few problem: 1. probably really sluggish on slow systems 2. probably slow for readmes with images (need to test) 3. flickers (the phantoms are updated too quickly, so sometimes it doesn't replace the old one smoothly) BUG: the preview doesn't load when we preview the markdown file --- MarkdownLivePreview.py | 55 +- lib/markdown2.py | 2711 ++++++++++++++++++++++++++++++++++++++++ test.md | 8 + utils.py | 27 +- 4 files changed, 2793 insertions(+), 8 deletions(-) create mode 100644 lib/markdown2.py create mode 100644 test.md diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 6e0dba5..b06a0ab 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -1,12 +1,16 @@ import sublime import sublime_plugin +from .lib.markdown2 import Markdown from .utils import * def plugin_loaded(): pass -SETTING_MDLP = "markdown_live_preview" +MARKDOWN_VIEW_INFOS = "markdown_view_infos" +PREVIEW_VIEW_INFOS = "preview_view_infos" +# FIXME: put this as a setting for the user to choose? +DELAY = 500 # ms # original_view: the view in the regular editor, without it's own window # markdown_view: the markdown view, in the special window @@ -33,7 +37,6 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): syntax_file = original_view.settings().get('syntax') - if file_name: original_view.close() else: @@ -55,6 +58,12 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): 'cells': [[0, 0, 1, 1], [1, 0, 2, 1]] }) + preview_window.focus_group(1) + preview_view = preview_window.new_file() + preview_view.set_scratch(True) + preview_view.settings().set(PREVIEW_VIEW_INFOS, {}) + + preview_window.focus_group(0) if file_name: markdown_view = preview_window.open_file(file_name) @@ -63,8 +72,11 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): markdown_view.run_command('mdlp_insert', {'point': 0, 'string': content}) markdown_view.set_scratch(True) + MarkdownLivePreviewListener.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view) + MarkdownLivePreviewListener._update_preview(MarkdownLivePreviewListener, markdown_view) + markdown_view.set_syntax_file(syntax_file) - markdown_view.settings().set(SETTING_MDLP, { + markdown_view.settings().set(MARKDOWN_VIEW_INFOS, { "original_window_id": original_window_id }) @@ -76,11 +88,17 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): class MarkdownLivePreviewListener(sublime_plugin.EventListener): + markdowner = Markdown() + + phantom_sets = { + # markdown_view.id(): phantom set + } + def on_pre_close(self, markdown_view): """ Close the view in the preview window, and store information for the on_close listener (see doc there) """ - if not markdown_view.settings().get(SETTING_MDLP): + if not markdown_view.settings().get(MARKDOWN_VIEW_INFOS): return self.markdown_view = markdown_view @@ -98,7 +116,7 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): def on_close(self, markdown_view): """ Use the information saved to restore the markdown_view as an original_view """ - infos = markdown_view.settings().get(SETTING_MDLP) + infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) if not infos: return @@ -122,4 +140,29 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): original_view = original_window.new_file() original_view.run_command('mdlp_insert', {'point': 0, 'string': self.content}) - original_view.set_syntax_file(markdown_view.settings().get('syntax')) \ No newline at end of file + original_view.set_syntax_file(markdown_view.settings().get('syntax')) + + # here, views are NOT treated independently, which is theoratically wrong + # but in practice, you can only edit one markdown file at a time, so it doesn't really + # matter. + # @min_time_between_call(.5) + def on_modified_async(self, markdown_view): + infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) + if not infos: + return + + self._update_preview(markdown_view) + + def _update_preview(self, markdown_view): + total_region = sublime.Region(0, markdown_view.size()) + markdown = markdown_view.substr(total_region) + + html = self.markdowner.convert(markdown) + + # FIXME: replace images + + self.phantom_sets[markdown_view.id()].update([ + sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, + lambda href: sublime.run_command('open_url', {'url': href})) + ]) + diff --git a/lib/markdown2.py b/lib/markdown2.py new file mode 100644 index 0000000..49bedd0 --- /dev/null +++ b/lib/markdown2.py @@ -0,0 +1,2711 @@ +#!/usr/bin/env python +# Copyright (c) 2012 Trent Mick. +# Copyright (c) 2007-2008 ActiveState Corp. +# License: MIT (http://www.opensource.org/licenses/mit-license.php) + +r"""A fast and complete Python implementation of Markdown. + +[from http://daringfireball.net/projects/markdown/] +> Markdown is a text-to-HTML filter; it translates an easy-to-read / +> easy-to-write structured text format into HTML. Markdown's text +> format is most similar to that of plain text email, and supports +> features such as headers, *emphasis*, code blocks, blockquotes, and +> links. +> +> Markdown's syntax is designed not as a generic markup language, but +> specifically to serve as a front-end to (X)HTML. You can use span-level +> HTML tags anywhere in a Markdown document, and you can use block level +> HTML tags (like
and as well). + +Module usage: + + >>> import markdown2 + >>> markdown2.markdown("*boo!*") # or use `html = markdown_path(PATH)` + u'

boo!

\n' + + >>> markdowner = Markdown() + >>> markdowner.convert("*boo!*") + u'

boo!

\n' + >>> markdowner.convert("**boom!**") + u'

boom!

\n' + +This implementation of Markdown implements the full "core" syntax plus a +number of extras (e.g., code syntax coloring, footnotes) as described on +. +""" + +cmdln_desc = """A fast and complete Python implementation of Markdown, a +text-to-HTML conversion tool for web writers. + +Supported extra syntax options (see -x|--extras option below and +see for details): + +* code-friendly: Disable _ and __ for em and strong. +* cuddled-lists: Allow lists to be cuddled to the preceding paragraph. +* fenced-code-blocks: Allows a code block to not have to be indented + by fencing it with '```' on a line before and after. Based on + with support for + syntax highlighting. +* footnotes: Support footnotes as in use on daringfireball.net and + implemented in other Markdown processors (tho not in Markdown.pl v1.0.1). +* header-ids: Adds "id" attributes to headers. The id value is a slug of + the header text. +* highlightjs-lang: Allows specifying the language which used for syntax + highlighting when using fenced-code-blocks and highlightjs. +* html-classes: Takes a dict mapping html tag names (lowercase) to a + string to use for a "class" tag attribute. Currently only supports "img", + "table", "pre" and "code" tags. Add an issue if you require this for other + tags. +* link-patterns: Auto-link given regex patterns in text (e.g. bug number + references, revision number references). +* markdown-in-html: Allow the use of `markdown="1"` in a block HTML tag to + have markdown processing be done on its contents. Similar to + but with + some limitations. +* metadata: Extract metadata from a leading '---'-fenced block. + See for details. +* nofollow: Add `rel="nofollow"` to add `` tags with an href. See + . +* numbering: Support of generic counters. Non standard extension to + allow sequential numbering of figures, tables, equations, exhibits etc. +* pyshell: Treats unindented Python interactive shell sessions as + blocks. +* smarty-pants: Replaces ' and " with curly quotation marks or curly + apostrophes. Replaces --, ---, ..., and . . . with en dashes, em dashes, + and ellipses. +* spoiler: A special kind of blockquote commonly hidden behind a + click on SO. Syntax per . +* strike: text inside of double tilde is ~~strikethrough~~ +* tag-friendly: Requires atx style headers to have a space between the # and + the header text. Useful for applications that require twitter style tags to + pass through the parser. +* tables: Tables using the same format as GFM + and + PHP-Markdown Extra . +* toc: The returned HTML string gets a new "toc_html" attribute which is + a Table of Contents for the document. (experimental) +* use-file-vars: Look for an Emacs-style markdown-extras file variable to turn + on Extras. +* wiki-tables: Google Code Wiki-style tables. See + . +* xml: Passes one-liner processing instructions and namespaced XML tags. +""" + +# Dev Notes: +# - Python's regex syntax doesn't have '\z', so I'm using '\Z'. I'm +# not yet sure if there implications with this. Compare 'pydoc sre' +# and 'perldoc perlre'. + +__version_info__ = (2, 3, 9) +__version__ = '.'.join(map(str, __version_info__)) +__author__ = "Trent Mick" + +import sys +import re +import logging +from hashlib import sha256 +import optparse +from random import random, randint +import codecs +from collections import defaultdict +try: + from urllib import quote_plus +except ImportError: + from urllib.parse import quote_plus + + +# ---- Python version compat + +# Use `bytes` for byte strings and `unicode` for unicode strings (str in Py3). +if sys.version_info[0] <= 2: + py3 = False + try: + bytes + except NameError: + bytes = str + base_string_type = basestring +elif sys.version_info[0] >= 3: + py3 = True + unicode = str + base_string_type = str + +# ---- globals + +DEBUG = False +log = logging.getLogger("markdown") + +DEFAULT_TAB_WIDTH = 4 + + +SECRET_SALT = bytes(randint(0, 1000000)) +# MD5 function was previously used for this; the "md5" prefix was kept for +# backwards compatibility. +def _hash_text(s): + return 'md5-' + sha256(SECRET_SALT + s.encode("utf-8")).hexdigest()[32:] + +# Table of hash values for escaped characters: +g_escape_table = dict([(ch, _hash_text(ch)) + for ch in '\\`*_{}[]()>#+-.!']) + +# Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin: +# http://bumppo.net/projects/amputator/ +_AMPERSAND_RE = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)') + + +# ---- exceptions +class MarkdownError(Exception): + pass + + +# ---- public api + +def markdown_path(path, encoding="utf-8", + html4tags=False, tab_width=DEFAULT_TAB_WIDTH, + safe_mode=None, extras=None, link_patterns=None, + footnote_title=None, footnote_return_symbol=None, + use_file_vars=False): + fp = codecs.open(path, 'r', encoding) + text = fp.read() + fp.close() + return Markdown(html4tags=html4tags, tab_width=tab_width, + safe_mode=safe_mode, extras=extras, + link_patterns=link_patterns, + footnote_title=footnote_title, + footnote_return_symbol=footnote_return_symbol, + use_file_vars=use_file_vars).convert(text) + + +def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH, + safe_mode=None, extras=None, link_patterns=None, + footnote_title=None, footnote_return_symbol=None, + use_file_vars=False, cli=False): + return Markdown(html4tags=html4tags, tab_width=tab_width, + safe_mode=safe_mode, extras=extras, + link_patterns=link_patterns, + footnote_title=footnote_title, + footnote_return_symbol=footnote_return_symbol, + use_file_vars=use_file_vars, cli=cli).convert(text) + + +class Markdown(object): + # The dict of "extras" to enable in processing -- a mapping of + # extra name to argument for the extra. Most extras do not have an + # argument, in which case the value is None. + # + # This can be set via (a) subclassing and (b) the constructor + # "extras" argument. + extras = None + + urls = None + titles = None + html_blocks = None + html_spans = None + html_removed_text = "[HTML_REMOVED]" # for compat with markdown.py + + # Used to track when we're inside an ordered or unordered list + # (see _ProcessListItems() for details): + list_level = 0 + + _ws_only_line_re = re.compile(r"^[ \t]+$", re.M) + + def __init__(self, html4tags=False, tab_width=4, safe_mode=None, + extras=None, link_patterns=None, + footnote_title=None, footnote_return_symbol=None, + use_file_vars=False, cli=False): + if html4tags: + self.empty_element_suffix = ">" + else: + self.empty_element_suffix = " />" + self.tab_width = tab_width + + # For compatibility with earlier markdown2.py and with + # markdown.py's safe_mode being a boolean, + # safe_mode == True -> "replace" + if safe_mode is True: + self.safe_mode = "replace" + else: + self.safe_mode = safe_mode + + # Massaging and building the "extras" info. + if self.extras is None: + self.extras = {} + elif not isinstance(self.extras, dict): + self.extras = dict([(e, None) for e in self.extras]) + if extras: + if not isinstance(extras, dict): + extras = dict([(e, None) for e in extras]) + self.extras.update(extras) + assert isinstance(self.extras, dict) + + if "toc" in self.extras: + if "header-ids" not in self.extras: + self.extras["header-ids"] = None # "toc" implies "header-ids" + + if self.extras["toc"] is None: + self._toc_depth = 6 + else: + self._toc_depth = self.extras["toc"].get("depth", 6) + self._instance_extras = self.extras.copy() + + self.link_patterns = link_patterns + self.footnote_title = footnote_title + self.footnote_return_symbol = footnote_return_symbol + self.use_file_vars = use_file_vars + self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M) + self.cli = cli + + self._escape_table = g_escape_table.copy() + if "smarty-pants" in self.extras: + self._escape_table['"'] = _hash_text('"') + self._escape_table["'"] = _hash_text("'") + + def reset(self): + self.urls = {} + self.titles = {} + self.html_blocks = {} + self.html_spans = {} + self.list_level = 0 + self.extras = self._instance_extras.copy() + if "footnotes" in self.extras: + self.footnotes = {} + self.footnote_ids = [] + if "header-ids" in self.extras: + self._count_from_header_id = defaultdict(int) + if "metadata" in self.extras: + self.metadata = {} + + # Per "rel" + # should only be used in tags with an "href" attribute. + _a_nofollow = re.compile(r""" + <(a) + ( + [^>]* + href= # href is required + ['"]? # HTML5 attribute values do not have to be quoted + [^#'"] # We don't want to match href values that start with # (like footnotes) + ) + """, + re.IGNORECASE | re.VERBOSE + ) + + # Opens the linked document in a new window or tab + # should only used in tags with an "href" attribute. + # same with _a_nofollow + _a_blank = _a_nofollow + + def convert(self, text): + """Convert the given text.""" + # Main function. The order in which other subs are called here is + # essential. Link and image substitutions need to happen before + # _EscapeSpecialChars(), so that any *'s or _'s in the + # and tags get encoded. + + # Clear the global hashes. If we don't clear these, you get conflicts + # from other articles when generating a page which contains more than + # one article (e.g. an index page that shows the N most recent + # articles): + self.reset() + + if not isinstance(text, unicode): + # TODO: perhaps shouldn't presume UTF-8 for string input? + text = unicode(text, 'utf-8') + + if self.use_file_vars: + # Look for emacs-style file variable hints. + emacs_vars = self._get_emacs_vars(text) + if "markdown-extras" in emacs_vars: + splitter = re.compile("[ ,]+") + for e in splitter.split(emacs_vars["markdown-extras"]): + if '=' in e: + ename, earg = e.split('=', 1) + try: + earg = int(earg) + except ValueError: + pass + else: + ename, earg = e, None + self.extras[ename] = earg + + # Standardize line endings: + text = text.replace("\r\n", "\n") + text = text.replace("\r", "\n") + + # Make sure $text ends with a couple of newlines: + text += "\n\n" + + # Convert all tabs to spaces. + text = self._detab(text) + + # Strip any lines consisting only of spaces and tabs. + # This makes subsequent regexen easier to write, because we can + # match consecutive blank lines with /\n+/ instead of something + # contorted like /[ \t]*\n+/ . + text = self._ws_only_line_re.sub("", text) + + # strip metadata from head and extract + if "metadata" in self.extras: + text = self._extract_metadata(text) + + text = self.preprocess(text) + + if "fenced-code-blocks" in self.extras and not self.safe_mode: + text = self._do_fenced_code_blocks(text) + + if self.safe_mode: + text = self._hash_html_spans(text) + + # Turn block-level HTML blocks into hash entries + text = self._hash_html_blocks(text, raw=True) + + if "fenced-code-blocks" in self.extras and self.safe_mode: + text = self._do_fenced_code_blocks(text) + + # Because numbering references aren't links (yet?) then we can do everything associated with counters + # before we get started + if "numbering" in self.extras: + text = self._do_numbering(text) + + # Strip link definitions, store in hashes. + if "footnotes" in self.extras: + # Must do footnotes first because an unlucky footnote defn + # looks like a link defn: + # [^4]: this "looks like a link defn" + text = self._strip_footnote_definitions(text) + text = self._strip_link_definitions(text) + + text = self._run_block_gamut(text) + + if "footnotes" in self.extras: + text = self._add_footnotes(text) + + text = self.postprocess(text) + + text = self._unescape_special_chars(text) + + if self.safe_mode: + text = self._unhash_html_spans(text) + + if "nofollow" in self.extras: + text = self._a_nofollow.sub(r'<\1 rel="nofollow"\2', text) + + if "target-blank-links" in self.extras: + text = self._a_blank.sub(r'<\1 target="_blank"\2', text) + + if "toc" in self.extras and self._toc: + self._toc_html = calculate_toc_html(self._toc) + + # Prepend toc html to output + if self.cli: + text = '{}\n{}'.format(self._toc_html, text) + + text += "\n" + + # Attach attrs to output + rv = UnicodeWithAttrs(text) + + if "toc" in self.extras and self._toc: + rv.toc_html = self._toc_html + + if "metadata" in self.extras: + rv.metadata = self.metadata + return rv + + def postprocess(self, text): + """A hook for subclasses to do some postprocessing of the html, if + desired. This is called before unescaping of special chars and + unhashing of raw HTML spans. + """ + return text + + def preprocess(self, text): + """A hook for subclasses to do some preprocessing of the Markdown, if + desired. This is called after basic formatting of the text, but prior + to any extras, safe mode, etc. processing. + """ + return text + + # Is metadata if the content starts with optional '---'-fenced `key: value` + # pairs. E.g. (indented for presentation): + # --- + # foo: bar + # another-var: blah blah + # --- + # # header + # or: + # foo: bar + # another-var: blah blah + # + # # header + _meta_data_pattern = re.compile(r'^(?:---[\ \t]*\n)?(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)|([\S\w]+\s*:(?! >)[ \t]*.*\n?)(?:---[\ \t]*\n)?', re.MULTILINE) + _key_val_pat = re.compile(r"[\S\w]+\s*:(?! >)[ \t]*.*\n?", re.MULTILINE) + # this allows key: > + # value + # conutiues over multiple lines + _key_val_block_pat = re.compile( + "(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)", re.MULTILINE) + _meta_data_fence_pattern = re.compile(r'^---[\ \t]*\n', re.MULTILINE) + _meta_data_newline = re.compile("^\n", re.MULTILINE) + + def _extract_metadata(self, text): + if text.startswith("---"): + fence_splits = re.split(self._meta_data_fence_pattern, text, maxsplit=2) + metadata_content = fence_splits[1] + match = re.findall(self._meta_data_pattern, metadata_content) + if not match: + return text + tail = fence_splits[2] + else: + metadata_split = re.split(self._meta_data_newline, text, maxsplit=1) + metadata_content = metadata_split[0] + match = re.findall(self._meta_data_pattern, metadata_content) + if not match: + return text + tail = metadata_split[1] + + kv = re.findall(self._key_val_pat, metadata_content) + kvm = re.findall(self._key_val_block_pat, metadata_content) + kvm = [item.replace(": >\n", ":", 1) for item in kvm] + + for item in kv + kvm: + k, v = item.split(":", 1) + self.metadata[k.strip()] = v.strip() + + return tail + + _emacs_oneliner_vars_pat = re.compile(r"-\*-\s*([^\r\n]*?)\s*-\*-", re.UNICODE) + # This regular expression is intended to match blocks like this: + # PREFIX Local Variables: SUFFIX + # PREFIX mode: Tcl SUFFIX + # PREFIX End: SUFFIX + # Some notes: + # - "[ \t]" is used instead of "\s" to specifically exclude newlines + # - "(\r\n|\n|\r)" is used instead of "$" because the sre engine does + # not like anything other than Unix-style line terminators. + _emacs_local_vars_pat = re.compile(r"""^ + (?P(?:[^\r\n|\n|\r])*?) + [\ \t]*Local\ Variables:[\ \t]* + (?P.*?)(?:\r\n|\n|\r) + (?P.*?\1End:) + """, re.IGNORECASE | re.MULTILINE | re.DOTALL | re.VERBOSE) + + def _get_emacs_vars(self, text): + """Return a dictionary of emacs-style local variables. + + Parsing is done loosely according to this spec (and according to + some in-practice deviations from this): + http://www.gnu.org/software/emacs/manual/html_node/emacs/Specifying-File-Variables.html#Specifying-File-Variables + """ + emacs_vars = {} + SIZE = pow(2, 13) # 8kB + + # Search near the start for a '-*-'-style one-liner of variables. + head = text[:SIZE] + if "-*-" in head: + match = self._emacs_oneliner_vars_pat.search(head) + if match: + emacs_vars_str = match.group(1) + assert '\n' not in emacs_vars_str + emacs_var_strs = [s.strip() for s in emacs_vars_str.split(';') + if s.strip()] + if len(emacs_var_strs) == 1 and ':' not in emacs_var_strs[0]: + # While not in the spec, this form is allowed by emacs: + # -*- Tcl -*- + # where the implied "variable" is "mode". This form + # is only allowed if there are no other variables. + emacs_vars["mode"] = emacs_var_strs[0].strip() + else: + for emacs_var_str in emacs_var_strs: + try: + variable, value = emacs_var_str.strip().split(':', 1) + except ValueError: + log.debug("emacs variables error: malformed -*- " + "line: %r", emacs_var_str) + continue + # Lowercase the variable name because Emacs allows "Mode" + # or "mode" or "MoDe", etc. + emacs_vars[variable.lower()] = value.strip() + + tail = text[-SIZE:] + if "Local Variables" in tail: + match = self._emacs_local_vars_pat.search(tail) + if match: + prefix = match.group("prefix") + suffix = match.group("suffix") + lines = match.group("content").splitlines(0) + # print "prefix=%r, suffix=%r, content=%r, lines: %s"\ + # % (prefix, suffix, match.group("content"), lines) + + # Validate the Local Variables block: proper prefix and suffix + # usage. + for i, line in enumerate(lines): + if not line.startswith(prefix): + log.debug("emacs variables error: line '%s' " + "does not use proper prefix '%s'" + % (line, prefix)) + return {} + # Don't validate suffix on last line. Emacs doesn't care, + # neither should we. + if i != len(lines)-1 and not line.endswith(suffix): + log.debug("emacs variables error: line '%s' " + "does not use proper suffix '%s'" + % (line, suffix)) + return {} + + # Parse out one emacs var per line. + continued_for = None + for line in lines[:-1]: # no var on the last line ("PREFIX End:") + if prefix: line = line[len(prefix):] # strip prefix + if suffix: line = line[:-len(suffix)] # strip suffix + line = line.strip() + if continued_for: + variable = continued_for + if line.endswith('\\'): + line = line[:-1].rstrip() + else: + continued_for = None + emacs_vars[variable] += ' ' + line + else: + try: + variable, value = line.split(':', 1) + except ValueError: + log.debug("local variables error: missing colon " + "in local variables entry: '%s'" % line) + continue + # Do NOT lowercase the variable name, because Emacs only + # allows "mode" (and not "Mode", "MoDe", etc.) in this block. + value = value.strip() + if value.endswith('\\'): + value = value[:-1].rstrip() + continued_for = variable + else: + continued_for = None + emacs_vars[variable] = value + + # Unquote values. + for var, val in list(emacs_vars.items()): + if len(val) > 1 and (val.startswith('"') and val.endswith('"') + or val.startswith('"') and val.endswith('"')): + emacs_vars[var] = val[1:-1] + + return emacs_vars + + def _detab_line(self, line): + r"""Recusively convert tabs to spaces in a single line. + + Called from _detab().""" + if '\t' not in line: + return line + chunk1, chunk2 = line.split('\t', 1) + chunk1 += (' ' * (self.tab_width - len(chunk1) % self.tab_width)) + output = chunk1 + chunk2 + return self._detab_line(output) + + def _detab(self, text): + r"""Iterate text line by line and convert tabs to spaces. + + >>> m = Markdown() + >>> m._detab("\tfoo") + ' foo' + >>> m._detab(" \tfoo") + ' foo' + >>> m._detab("\t foo") + ' foo' + >>> m._detab(" foo") + ' foo' + >>> m._detab(" foo\n\tbar\tblam") + ' foo\n bar blam' + """ + if '\t' not in text: + return text + output = [] + for line in text.splitlines(): + output.append(self._detab_line(line)) + return '\n'.join(output) + + # I broke out the html5 tags here and add them to _block_tags_a and + # _block_tags_b. This way html5 tags are easy to keep track of. + _html5tags = '|article|aside|header|hgroup|footer|nav|section|figure|figcaption' + + _block_tags_a = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math|ins|del' + _block_tags_a += _html5tags + + _strict_tag_block_re = re.compile(r""" + ( # save in \1 + ^ # start of line (with re.M) + <(%s) # start tag = \2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + ) + """ % _block_tags_a, + re.X | re.M) + + _block_tags_b = 'p|div|h[1-6]|blockquote|pre|table|dl|ol|ul|script|noscript|form|fieldset|iframe|math' + _block_tags_b += _html5tags + + _liberal_tag_block_re = re.compile(r""" + ( # save in \1 + ^ # start of line (with re.M) + <(%s) # start tag = \2 + \b # word break + (.*\n)*? # any number of lines, minimally matching + .* # the matching end tag + [ \t]* # trailing spaces/tabs + (?=\n+|\Z) # followed by a newline or end of document + ) + """ % _block_tags_b, + re.X | re.M) + + _html_markdown_attr_re = re.compile( + r'''\s+markdown=("1"|'1')''') + def _hash_html_block_sub(self, match, raw=False): + html = match.group(1) + if raw and self.safe_mode: + html = self._sanitize_html(html) + elif 'markdown-in-html' in self.extras and 'markdown=' in html: + first_line = html.split('\n', 1)[0] + m = self._html_markdown_attr_re.search(first_line) + if m: + lines = html.split('\n') + middle = '\n'.join(lines[1:-1]) + last_line = lines[-1] + first_line = first_line[:m.start()] + first_line[m.end():] + f_key = _hash_text(first_line) + self.html_blocks[f_key] = first_line + l_key = _hash_text(last_line) + self.html_blocks[l_key] = last_line + return ''.join(["\n\n", f_key, + "\n\n", middle, "\n\n", + l_key, "\n\n"]) + key = _hash_text(html) + self.html_blocks[key] = html + return "\n\n" + key + "\n\n" + + def _hash_html_blocks(self, text, raw=False): + """Hashify HTML blocks + + We only want to do this for block-level HTML tags, such as headers, + lists, and tables. That's because we still want to wrap

s around + "paragraphs" that are wrapped in non-block-level tags, such as anchors, + phrase emphasis, and spans. The list of tags we're looking for is + hard-coded. + + @param raw {boolean} indicates if these are raw HTML blocks in + the original source. It makes a difference in "safe" mode. + """ + if '<' not in text: + return text + + # Pass `raw` value into our calls to self._hash_html_block_sub. + hash_html_block_sub = _curry(self._hash_html_block_sub, raw=raw) + + # First, look for nested blocks, e.g.: + #

+ #
+ # tags for inner block must be indented. + #
+ #
+ # + # The outermost tags must start at the left margin for this to match, and + # the inner nested divs must be indented. + # We need to do this before the next, more liberal match, because the next + # match will start at the first `
` and stop at the first `
`. + text = self._strict_tag_block_re.sub(hash_html_block_sub, text) + + # Now match more liberally, simply from `\n` to `\n` + text = self._liberal_tag_block_re.sub(hash_html_block_sub, text) + + # Special case just for
. It was easier to make a special + # case than to make the other regex more complicated. + if "", start_idx) + 3 + except ValueError: + break + + # Start position for next comment block search. + start = end_idx + + # Validate whitespace before comment. + if start_idx: + # - Up to `tab_width - 1` spaces before start_idx. + for i in range(self.tab_width - 1): + if text[start_idx - 1] != ' ': + break + start_idx -= 1 + if start_idx == 0: + break + # - Must be preceded by 2 newlines or hit the start of + # the document. + if start_idx == 0: + pass + elif start_idx == 1 and text[0] == '\n': + start_idx = 0 # to match minute detail of Markdown.pl regex + elif text[start_idx-2:start_idx] == '\n\n': + pass + else: + break + + # Validate whitespace after comment. + # - Any number of spaces and tabs. + while end_idx < len(text): + if text[end_idx] not in ' \t': + break + end_idx += 1 + # - Must be following by 2 newlines or hit end of text. + if text[end_idx:end_idx+2] not in ('', '\n', '\n\n'): + continue + + # Escape and hash (must match `_hash_html_block_sub`). + html = text[start_idx:end_idx] + if raw and self.safe_mode: + html = self._sanitize_html(html) + key = _hash_text(html) + self.html_blocks[key] = html + text = text[:start_idx] + "\n\n" + key + "\n\n" + text[end_idx:] + + if "xml" in self.extras: + # Treat XML processing instructions and namespaced one-liner + # tags as if they were block HTML tags. E.g., if standalone + # (i.e. are their own paragraph), the following do not get + # wrapped in a

tag: + # + # + # + _xml_oneliner_re = _xml_oneliner_re_from_tab_width(self.tab_width) + text = _xml_oneliner_re.sub(hash_html_block_sub, text) + + return text + + def _strip_link_definitions(self, text): + # Strips link definitions from text, stores the URLs and titles in + # hash references. + less_than_tab = self.tab_width - 1 + + # Link defs are in the form: + # [id]: url "optional title" + _link_def_re = re.compile(r""" + ^[ ]{0,%d}\[(.+)\]: # id = \1 + [ \t]* + \n? # maybe *one* newline + [ \t]* + ? # url = \2 + [ \t]* + (?: + \n? # maybe one newline + [ \t]* + (?<=\s) # lookbehind for whitespace + ['"(] + ([^\n]*) # title = \3 + ['")] + [ \t]* + )? # title is optional + (?:\n+|\Z) + """ % less_than_tab, re.X | re.M | re.U) + return _link_def_re.sub(self._extract_link_def_sub, text) + + def _extract_link_def_sub(self, match): + id, url, title = match.groups() + key = id.lower() # Link IDs are case-insensitive + self.urls[key] = self._encode_amps_and_angles(url) + if title: + self.titles[key] = title + return "" + + def _do_numbering(self, text): + ''' We handle the special extension for generic numbering for + tables, figures etc. + ''' + # First pass to define all the references + self.regex_defns = re.compile(r''' + \[\#(\w+)\s* # the counter. Open square plus hash plus a word \1 + ([^@]*)\s* # Some optional characters, that aren't an @. \2 + @(\w+) # the id. Should this be normed? \3 + ([^\]]*)\] # The rest of the text up to the terminating ] \4 + ''', re.VERBOSE) + self.regex_subs = re.compile(r"\[@(\w+)\s*\]") # [@ref_id] + counters = {} + references = {} + replacements = [] + definition_html = '

{}{}{}
' + reference_html = '
{}' + for match in self.regex_defns.finditer(text): + # We must have four match groups otherwise this isn't a numbering reference + if len(match.groups()) != 4: + continue + counter = match.group(1) + text_before = match.group(2) + ref_id = match.group(3) + text_after = match.group(4) + number = counters.get(counter, 1) + references[ref_id] = (number, counter) + replacements.append((match.start(0), + definition_html.format(counter, + ref_id, + text_before, + number, + text_after), + match.end(0))) + counters[counter] = number + 1 + for repl in reversed(replacements): + text = text[:repl[0]] + repl[1] + text[repl[2]:] + + # Second pass to replace the references with the right + # value of the counter + # Fwiw, it's vaguely annoying to have to turn the iterator into + # a list and then reverse it but I can't think of a better thing to do. + for match in reversed(list(self.regex_subs.finditer(text))): + number, counter = references.get(match.group(1), (None, None)) + if number is not None: + repl = reference_html.format(counter, + match.group(1), + number) + else: + repl = reference_html.format(match.group(1), + 'countererror', + '?' + match.group(1) + '?') + if "smarty-pants" in self.extras: + repl = repl.replace('"', self._escape_table['"']) + + text = text[:match.start()] + repl + text[match.end():] + return text + + def _extract_footnote_def_sub(self, match): + id, text = match.groups() + text = _dedent(text, skip_first_line=not text.startswith('\n')).strip() + normed_id = re.sub(r'\W', '-', id) + # Ensure footnote text ends with a couple newlines (for some + # block gamut matches). + self.footnotes[normed_id] = text + "\n\n" + return "" + + def _strip_footnote_definitions(self, text): + """A footnote definition looks like this: + + [^note-id]: Text of the note. + + May include one or more indented paragraphs. + + Where, + - The 'note-id' can be pretty much anything, though typically it + is the number of the footnote. + - The first paragraph may start on the next line, like so: + + [^note-id]: + Text of the note. + """ + less_than_tab = self.tab_width - 1 + footnote_def_re = re.compile(r''' + ^[ ]{0,%d}\[\^(.+)\]: # id = \1 + [ \t]* + ( # footnote text = \2 + # First line need not start with the spaces. + (?:\s*.*\n+) + (?: + (?:[ ]{%d} | \t) # Subsequent lines must be indented. + .*\n+ + )* + ) + # Lookahead for non-space at line-start, or end of doc. + (?:(?=^[ ]{0,%d}\S)|\Z) + ''' % (less_than_tab, self.tab_width, self.tab_width), + re.X | re.M) + return footnote_def_re.sub(self._extract_footnote_def_sub, text) + + _hr_re = re.compile(r'^[ ]{0,3}([-_*][ ]{0,2}){3,}$', re.M) + + def _run_block_gamut(self, text): + # These are all the transformations that form block-level + # tags like paragraphs, headers, and list items. + + if "fenced-code-blocks" in self.extras: + text = self._do_fenced_code_blocks(text) + + text = self._do_headers(text) + + # Do Horizontal Rules: + # On the number of spaces in horizontal rules: The spec is fuzzy: "If + # you wish, you may use spaces between the hyphens or asterisks." + # Markdown.pl 1.0.1's hr regexes limit the number of spaces between the + # hr chars to one or two. We'll reproduce that limit here. + hr = "\n tags around block-level tags. + text = self._hash_html_blocks(text) + + text = self._form_paragraphs(text) + + return text + + def _pyshell_block_sub(self, match): + lines = match.group(0).splitlines(0) + _dedentlines(lines) + indent = ' ' * self.tab_width + s = ('\n' # separate from possible cuddled paragraph + + indent + ('\n'+indent).join(lines) + + '\n\n') + return s + + def _prepare_pyshell_blocks(self, text): + """Ensure that Python interactive shell sessions are put in + code blocks -- even if not properly indented. + """ + if ">>>" not in text: + return text + + less_than_tab = self.tab_width - 1 + _pyshell_block_re = re.compile(r""" + ^([ ]{0,%d})>>>[ ].*\n # first line + ^(\1.*\S+.*\n)* # any number of subsequent lines + ^\n # ends with a blank line + """ % less_than_tab, re.M | re.X) + + return _pyshell_block_re.sub(self._pyshell_block_sub, text) + + def _table_sub(self, match): + trim_space_re = '^[ \t\n]+|[ \t\n]+$' + trim_bar_re = r'^\||\|$' + split_bar_re = r'^\||(?' % self._html_class_str_from_tag('table'), '
', ''] + cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", head)))] + for col_idx, col in enumerate(cols): + hlines.append(' %s' % ( + align_from_col_idx.get(col_idx, ''), + self._run_span_gamut(col) + )) + hlines.append('') + hlines.append('') + + # tbody + hlines.append('') + for line in body.strip('\n').split('\n'): + hlines.append('') + cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", line)))] + for col_idx, col in enumerate(cols): + hlines.append(' %s' % ( + align_from_col_idx.get(col_idx, ''), + self._run_span_gamut(col) + )) + hlines.append('') + hlines.append('') + hlines.append('
') + + return '\n'.join(hlines) + '\n' + + def _do_tables(self, text): + """Copying PHP-Markdown and GFM table syntax. Some regex borrowed from + https://github.com/michelf/php-markdown/blob/lib/Michelf/Markdown.php#L2538 + """ + less_than_tab = self.tab_width - 1 + table_re = re.compile(r''' + (?:(?<=\n\n)|\A\n?) # leading blank line + + ^[ ]{0,%d} # allowed whitespace + (.*[|].*) \n # $1: header row (at least one pipe) + + ^[ ]{0,%d} # allowed whitespace + ( # $2: underline row + # underline row with leading bar + (?: \|\ *:?-+:?\ * )+ \|? \n + | + # or, underline row without leading bar + (?: \ *:?-+:?\ *\| )+ (?: \ *:?-+:?\ * )? \n + ) + + ( # $3: data rows + (?: + ^[ ]{0,%d}(?!\ ) # ensure line begins with 0 to less_than_tab spaces + .*\|.* \n + )+ + ) + ''' % (less_than_tab, less_than_tab, less_than_tab), re.M | re.X) + return table_re.sub(self._table_sub, text) + + def _wiki_table_sub(self, match): + ttext = match.group(0).strip() + # print 'wiki table: %r' % match.group(0) + rows = [] + for line in ttext.splitlines(0): + line = line.strip()[2:-2].strip() + row = [c.strip() for c in re.split(r'(?' % self._html_class_str_from_tag('table'), ''] + for row in rows: + hrow = [''] + for cell in row: + hrow.append('') + hrow.append(self._run_span_gamut(cell)) + hrow.append('') + hrow.append('') + hlines.append(''.join(hrow)) + hlines += ['', ''] + return '\n'.join(hlines) + '\n' + + def _do_wiki_tables(self, text): + # Optimization. + if "||" not in text: + return text + + less_than_tab = self.tab_width - 1 + wiki_table_re = re.compile(r''' + (?:(?<=\n\n)|\A\n?) # leading blank line + ^([ ]{0,%d})\|\|.+?\|\|[ ]*\n # first line + (^\1\|\|.+?\|\|\n)* # any number of subsequent lines + ''' % less_than_tab, re.M | re.X) + return wiki_table_re.sub(self._wiki_table_sub, text) + + def _run_span_gamut(self, text): + # These are all the transformations that occur *within* block-level + # tags like paragraphs, headers, and list items. + + text = self._do_code_spans(text) + + text = self._escape_special_chars(text) + + # Process anchor and image tags. + if "link-patterns" in self.extras: + text = self._do_link_patterns(text) + + text = self._do_links(text) + + # Make links out of things like `` + # Must come after _do_links(), because you can use < and > + # delimiters in inline links like [this](). + text = self._do_auto_links(text) + + text = self._encode_amps_and_angles(text) + + if "strike" in self.extras: + text = self._do_strike(text) + + text = self._do_italics_and_bold(text) + + if "smarty-pants" in self.extras: + text = self._do_smart_punctuation(text) + + # Do hard breaks: + if "break-on-newline" in self.extras: + text = re.sub(r" *\n", " + | + # auto-link (e.g., ) + <\w+[^>]*> + | + # comment + | + <\?.*?\?> # processing instruction + ) + """, re.X) + + def _escape_special_chars(self, text): + # Python markdown note: the HTML tokenization here differs from + # that in Markdown.pl, hence the behaviour for subtle cases can + # differ (I believe the tokenizer here does a better job because + # it isn't susceptible to unmatched '<' and '>' in HTML tags). + # Note, however, that '>' is not allowed in an auto-link URL + # here. + escaped = [] + is_html_markup = False + for token in self._sorta_html_tokenize_re.split(text): + if is_html_markup: + # Within tags/HTML-comments/auto-links, encode * and _ + # so they don't conflict with their use in Markdown for + # italics and strong. We're replacing each such + # character with its corresponding MD5 checksum value; + # this is likely overkill, but it should prevent us from + # colliding with the escape values by accident. + escaped.append(token.replace('*', self._escape_table['*']) + .replace('_', self._escape_table['_'])) + else: + escaped.append(self._encode_backslash_escapes(token)) + is_html_markup = not is_html_markup + return ''.join(escaped) + + def _hash_html_spans(self, text): + # Used for safe_mode. + + def _is_auto_link(s): + if ':' in s and self._auto_link_re.match(s): + return True + elif '@' in s and self._auto_email_link_re.match(s): + return True + return False + + tokens = [] + is_html_markup = False + for token in self._sorta_html_tokenize_re.split(text): + if is_html_markup and not _is_auto_link(token): + sanitized = self._sanitize_html(token) + key = _hash_text(sanitized) + self.html_spans[key] = sanitized + tokens.append(key) + else: + tokens.append(self._encode_incomplete_tags(token)) + is_html_markup = not is_html_markup + return ''.join(tokens) + + def _unhash_html_spans(self, text): + for key, sanitized in list(self.html_spans.items()): + text = text.replace(key, sanitized) + return text + + def _sanitize_html(self, s): + if self.safe_mode == "replace": + return self.html_removed_text + elif self.safe_mode == "escape": + replacements = [ + ('&', '&'), + ('<', '<'), + ('>', '>'), + ] + for before, after in replacements: + s = s.replace(before, after) + return s + else: + raise MarkdownError("invalid value for 'safe_mode': %r (must be " + "'escape' or 'replace')" % self.safe_mode) + + _inline_link_title = re.compile(r''' + ( # \1 + [ \t]+ + (['"]) # quote char = \2 + (?P.*?) + \2 + )? # title is optional + \)$ + ''', re.X | re.S) + _tail_of_reference_link_re = re.compile(r''' + # Match tail of: [text][id] + [ ]? # one optional space + (?:\n[ ]*)? # one optional newline followed by spaces + \[ + (?P<id>.*?) + \] + ''', re.X | re.S) + + _whitespace = re.compile(r'\s*') + + _strip_anglebrackets = re.compile(r'<(.*)>.*') + + def _find_non_whitespace(self, text, start): + """Returns the index of the first non-whitespace character in text + after (and including) start + """ + match = self._whitespace.match(text, start) + return match.end() + + def _find_balanced(self, text, start, open_c, close_c): + """Returns the index where the open_c and close_c characters balance + out - the same number of open_c and close_c are encountered - or the + end of string if it's reached before the balance point is found. + """ + i = start + l = len(text) + count = 1 + while count > 0 and i < l: + if text[i] == open_c: + count += 1 + elif text[i] == close_c: + count -= 1 + i += 1 + return i + + def _extract_url_and_title(self, text, start): + """Extracts the url and (optional) title from the tail of a link""" + # text[start] equals the opening parenthesis + idx = self._find_non_whitespace(text, start+1) + if idx == len(text): + return None, None, None + end_idx = idx + has_anglebrackets = text[idx] == "<" + if has_anglebrackets: + end_idx = self._find_balanced(text, end_idx+1, "<", ">") + end_idx = self._find_balanced(text, end_idx, "(", ")") + match = self._inline_link_title.search(text, idx, end_idx) + if not match: + return None, None, None + url, title = text[idx:match.start()], match.group("title") + if has_anglebrackets: + url = self._strip_anglebrackets.sub(r'\1', url) + return url, title, end_idx + + _safe_protocols = re.compile(r'(https?|ftp):', re.I) + def _do_links(self, text): + """Turn Markdown link shortcuts into XHTML <a> and <img> tags. + + This is a combination of Markdown.pl's _DoAnchors() and + _DoImages(). They are done together because that simplified the + approach. It was necessary to use a different approach than + Markdown.pl because of the lack of atomic matching support in + Python's regex engine used in $g_nested_brackets. + """ + MAX_LINK_TEXT_SENTINEL = 3000 # markdown2 issue 24 + + # `anchor_allowed_pos` is used to support img links inside + # anchors, but not anchors inside anchors. An anchor's start + # pos must be `>= anchor_allowed_pos`. + anchor_allowed_pos = 0 + + curr_pos = 0 + while True: # Handle the next link. + # The next '[' is the start of: + # - an inline anchor: [text](url "title") + # - a reference anchor: [text][id] + # - an inline img: ![text](url "title") + # - a reference img: ![text][id] + # - a footnote ref: [^id] + # (Only if 'footnotes' extra enabled) + # - a footnote defn: [^id]: ... + # (Only if 'footnotes' extra enabled) These have already + # been stripped in _strip_footnote_definitions() so no + # need to watch for them. + # - a link definition: [id]: url "title" + # These have already been stripped in + # _strip_link_definitions() so no need to watch for them. + # - not markup: [...anything else... + try: + start_idx = text.index('[', curr_pos) + except ValueError: + break + text_length = len(text) + + # Find the matching closing ']'. + # Markdown.pl allows *matching* brackets in link text so we + # will here too. Markdown.pl *doesn't* currently allow + # matching brackets in img alt text -- we'll differ in that + # regard. + bracket_depth = 0 + for p in range(start_idx+1, min(start_idx+MAX_LINK_TEXT_SENTINEL, + text_length)): + ch = text[p] + if ch == ']': + bracket_depth -= 1 + if bracket_depth < 0: + break + elif ch == '[': + bracket_depth += 1 + else: + # Closing bracket not found within sentinel length. + # This isn't markup. + curr_pos = start_idx + 1 + continue + link_text = text[start_idx+1:p] + + # Possibly a footnote ref? + if "footnotes" in self.extras and link_text.startswith("^"): + normed_id = re.sub(r'\W', '-', link_text[1:]) + if normed_id in self.footnotes: + self.footnote_ids.append(normed_id) + result = '<sup class="footnote-ref" id="fnref-%s">' \ + '<a href="#fn-%s">%s</a></sup>' \ + % (normed_id, normed_id, len(self.footnote_ids)) + text = text[:start_idx] + result + text[p+1:] + else: + # This id isn't defined, leave the markup alone. + curr_pos = p+1 + continue + + # Now determine what this is by the remainder. + p += 1 + if p == text_length: + return text + + # Inline anchor or img? + if text[p] == '(': # attempt at perf improvement + url, title, url_end_idx = self._extract_url_and_title(text, p) + if url is not None: + # Handle an inline anchor or img. + is_img = start_idx > 0 and text[start_idx-1] == "!" + if is_img: + start_idx -= 1 + + # We've got to encode these to avoid conflicting + # with italics/bold. + url = url.replace('*', self._escape_table['*']) \ + .replace('_', self._escape_table['_']) + if title: + title_str = ' title="%s"' % ( + _xml_escape_attr(title) + .replace('*', self._escape_table['*']) + .replace('_', self._escape_table['_'])) + else: + title_str = '' + if is_img: + img_class_str = self._html_class_str_from_tag("img") + result = '<img src="%s" alt="%s"%s%s%s' \ + % (_html_escape_url(url, safe_mode=self.safe_mode), + _xml_escape_attr(link_text), + title_str, + img_class_str, + self.empty_element_suffix) + if "smarty-pants" in self.extras: + result = result.replace('"', self._escape_table['"']) + curr_pos = start_idx + len(result) + text = text[:start_idx] + result + text[url_end_idx:] + elif start_idx >= anchor_allowed_pos: + safe_link = self._safe_protocols.match(url) or url.startswith('#') + if self.safe_mode and not safe_link: + result_head = '<a href="#"%s>' % (title_str) + else: + result_head = '<a href="%s"%s>' % (_html_escape_url(url, safe_mode=self.safe_mode), title_str) + result = '%s%s</a>' % (result_head, link_text) + if "smarty-pants" in self.extras: + result = result.replace('"', self._escape_table['"']) + # <img> allowed from curr_pos on, <a> from + # anchor_allowed_pos on. + curr_pos = start_idx + len(result_head) + anchor_allowed_pos = start_idx + len(result) + text = text[:start_idx] + result + text[url_end_idx:] + else: + # Anchor not allowed here. + curr_pos = start_idx + 1 + continue + + # Reference anchor or img? + else: + match = self._tail_of_reference_link_re.match(text, p) + if match: + # Handle a reference-style anchor or img. + is_img = start_idx > 0 and text[start_idx-1] == "!" + if is_img: + start_idx -= 1 + link_id = match.group("id").lower() + if not link_id: + link_id = link_text.lower() # for links like [this][] + if link_id in self.urls: + url = self.urls[link_id] + # We've got to encode these to avoid conflicting + # with italics/bold. + url = url.replace('*', self._escape_table['*']) \ + .replace('_', self._escape_table['_']) + title = self.titles.get(link_id) + if title: + title = _xml_escape_attr(title) \ + .replace('*', self._escape_table['*']) \ + .replace('_', self._escape_table['_']) + title_str = ' title="%s"' % title + else: + title_str = '' + if is_img: + img_class_str = self._html_class_str_from_tag("img") + result = '<img src="%s" alt="%s"%s%s%s' \ + % (_html_escape_url(url, safe_mode=self.safe_mode), + _xml_escape_attr(link_text), + title_str, + img_class_str, + self.empty_element_suffix) + if "smarty-pants" in self.extras: + result = result.replace('"', self._escape_table['"']) + curr_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + elif start_idx >= anchor_allowed_pos: + if self.safe_mode and not self._safe_protocols.match(url): + result_head = '<a href="#"%s>' % (title_str) + else: + result_head = '<a href="%s"%s>' % (_html_escape_url(url, safe_mode=self.safe_mode), title_str) + result = '%s%s</a>' % (result_head, link_text) + if "smarty-pants" in self.extras: + result = result.replace('"', self._escape_table['"']) + # <img> allowed from curr_pos on, <a> from + # anchor_allowed_pos on. + curr_pos = start_idx + len(result_head) + anchor_allowed_pos = start_idx + len(result) + text = text[:start_idx] + result + text[match.end():] + else: + # Anchor not allowed here. + curr_pos = start_idx + 1 + else: + # This id isn't defined, leave the markup alone. + curr_pos = match.end() + continue + + # Otherwise, it isn't markup. + curr_pos = start_idx + 1 + + return text + + def header_id_from_text(self, text, prefix, n): + """Generate a header id attribute value from the given header + HTML content. + + This is only called if the "header-ids" extra is enabled. + Subclasses may override this for different header ids. + + @param text {str} The text of the header tag + @param prefix {str} The requested prefix for header ids. This is the + value of the "header-ids" extra key, if any. Otherwise, None. + @param n {int} The <hN> tag number, i.e. `1` for an <h1> tag. + @returns {str} The value for the header tag's "id" attribute. Return + None to not have an id attribute and to exclude this header from + the TOC (if the "toc" extra is specified). + """ + header_id = _slugify(text) + if prefix and isinstance(prefix, base_string_type): + header_id = prefix + '-' + header_id + + self._count_from_header_id[header_id] += 1 + if 0 == len(header_id) or self._count_from_header_id[header_id] > 1: + header_id += '-%s' % self._count_from_header_id[header_id] + + return header_id + + _toc = None + def _toc_add_entry(self, level, id, name): + if level > self._toc_depth: + return + if self._toc is None: + self._toc = [] + self._toc.append((level, id, self._unescape_special_chars(name))) + + _h_re_base = r''' + (^(.+)[ \t]*\n(=+|-+)[ \t]*\n+) + | + (^(\#{1,6}) # \1 = string of #'s + [ \t]%s + (.+?) # \2 = Header text + [ \t]* + (?<!\\) # ensure not an escaped trailing '#' + \#* # optional closing #'s (not counted) + \n+ + ) + ''' + + _h_re = re.compile(_h_re_base % '*', re.X | re.M) + _h_re_tag_friendly = re.compile(_h_re_base % '+', re.X | re.M) + + def _h_sub(self, match): + if match.group(1) is not None and match.group(3) == "-": + return match.group(1) + elif match.group(1) is not None: + # Setext header + n = {"=": 1, "-": 2}[match.group(3)[0]] + header_group = match.group(2) + else: + # atx header + n = len(match.group(5)) + header_group = match.group(6) + + demote_headers = self.extras.get("demote-headers") + if demote_headers: + n = min(n + demote_headers, 6) + header_id_attr = "" + if "header-ids" in self.extras: + header_id = self.header_id_from_text(header_group, + self.extras["header-ids"], n) + if header_id: + header_id_attr = ' id="%s"' % header_id + html = self._run_span_gamut(header_group) + if "toc" in self.extras and header_id: + self._toc_add_entry(n, header_id, html) + return "<h%d%s>%s</h%d>\n\n" % (n, header_id_attr, html, n) + + def _do_headers(self, text): + # Setext-style headers: + # Header 1 + # ======== + # + # Header 2 + # -------- + + # atx-style headers: + # # Header 1 + # ## Header 2 + # ## Header 2 with closing hashes ## + # ... + # ###### Header 6 + + if 'tag-friendly' in self.extras: + return self._h_re_tag_friendly.sub(self._h_sub, text) + return self._h_re.sub(self._h_sub, text) + + _marker_ul_chars = '*+-' + _marker_any = r'(?:[%s]|\d+\.)' % _marker_ul_chars + _marker_ul = '(?:[%s])' % _marker_ul_chars + _marker_ol = r'(?:\d+\.)' + + def _list_sub(self, match): + lst = match.group(1) + lst_type = match.group(3) in self._marker_ul_chars and "ul" or "ol" + result = self._process_list_items(lst) + if self.list_level: + return "<%s>\n%s</%s>\n" % (lst_type, result, lst_type) + else: + return "<%s>\n%s</%s>\n\n" % (lst_type, result, lst_type) + + def _do_lists(self, text): + # Form HTML ordered (numbered) and unordered (bulleted) lists. + + # Iterate over each *non-overlapping* list match. + pos = 0 + while True: + # Find the *first* hit for either list style (ul or ol). We + # match ul and ol separately to avoid adjacent lists of different + # types running into each other (see issue #16). + hits = [] + for marker_pat in (self._marker_ul, self._marker_ol): + less_than_tab = self.tab_width - 1 + whole_list = r''' + ( # \1 = whole list + ( # \2 + [ ]{0,%d} + (%s) # \3 = first list item marker + [ \t]+ + (?!\ *\3\ ) # '- - - ...' isn't a list. See 'not_quite_a_list' test case. + ) + (?:.+?) + ( # \4 + \Z + | + \n{2,} + (?=\S) + (?! # Negative lookahead for another list item marker + [ \t]* + %s[ \t]+ + ) + ) + ) + ''' % (less_than_tab, marker_pat, marker_pat) + if self.list_level: # sub-list + list_re = re.compile("^"+whole_list, re.X | re.M | re.S) + else: + list_re = re.compile(r"(?:(?<=\n\n)|\A\n?)"+whole_list, + re.X | re.M | re.S) + match = list_re.search(text, pos) + if match: + hits.append((match.start(), match)) + if not hits: + break + hits.sort() + match = hits[0][1] + start, end = match.span() + middle = self._list_sub(match) + text = text[:start] + middle + text[end:] + pos = start + len(middle) # start pos for next attempted match + + return text + + _list_item_re = re.compile(r''' + (\n)? # leading line = \1 + (^[ \t]*) # leading whitespace = \2 + (?P<marker>%s) [ \t]+ # list marker = \3 + ((?:.+?) # list item text = \4 + (\n{1,2})) # eols = \5 + (?= \n* (\Z | \2 (?P<next_marker>%s) [ \t]+)) + ''' % (_marker_any, _marker_any), + re.M | re.X | re.S) + + _task_list_item_re = re.compile(r''' + (\[[\ xX]\])[ \t]+ # tasklist marker = \1 + (.*) # list item text = \2 + ''', re.M | re.X | re.S) + + _task_list_warpper_str = r'<input type="checkbox" class="task-list-item-checkbox" %sdisabled> %s' + + def _task_list_item_sub(self, match): + marker = match.group(1) + item_text = match.group(2) + if marker in ['[x]','[X]']: + return self._task_list_warpper_str % ('checked ', item_text) + elif marker == '[ ]': + return self._task_list_warpper_str % ('', item_text) + + _last_li_endswith_two_eols = False + def _list_item_sub(self, match): + item = match.group(4) + leading_line = match.group(1) + if leading_line or "\n\n" in item or self._last_li_endswith_two_eols: + item = self._run_block_gamut(self._outdent(item)) + else: + # Recursion for sub-lists: + item = self._do_lists(self._outdent(item)) + if item.endswith('\n'): + item = item[:-1] + item = self._run_span_gamut(item) + self._last_li_endswith_two_eols = (len(match.group(5)) == 2) + + if "task_list" in self.extras: + item = self._task_list_item_re.sub(self._task_list_item_sub, item) + + return "<li>%s</li>\n" % item + + def _process_list_items(self, list_str): + # Process the contents of a single ordered or unordered list, + # splitting it into individual list items. + + # The $g_list_level global keeps track of when we're inside a list. + # Each time we enter a list, we increment it; when we leave a list, + # we decrement. If it's zero, we're not in a list anymore. + # + # We do this because when we're not inside a list, we want to treat + # something like this: + # + # I recommend upgrading to version + # 8. Oops, now this line is treated + # as a sub-list. + # + # As a single paragraph, despite the fact that the second line starts + # with a digit-period-space sequence. + # + # Whereas when we're inside a list (or sub-list), that line will be + # treated as the start of a sub-list. What a kludge, huh? This is + # an aspect of Markdown's syntax that's hard to parse perfectly + # without resorting to mind-reading. Perhaps the solution is to + # change the syntax rules such that sub-lists must start with a + # starting cardinal number; e.g. "1." or "a.". + self.list_level += 1 + self._last_li_endswith_two_eols = False + list_str = list_str.rstrip('\n') + '\n' + list_str = self._list_item_re.sub(self._list_item_sub, list_str) + self.list_level -= 1 + return list_str + + def _get_pygments_lexer(self, lexer_name): + try: + from pygments import lexers, util + except ImportError: + return None + try: + return lexers.get_lexer_by_name(lexer_name) + except util.ClassNotFound: + return None + + def _color_with_pygments(self, codeblock, lexer, **formatter_opts): + import pygments + import pygments.formatters + + class HtmlCodeFormatter(pygments.formatters.HtmlFormatter): + def _wrap_code(self, inner): + """A function for use in a Pygments Formatter which + wraps in <code> tags. + """ + yield 0, "<code>" + for tup in inner: + yield tup + yield 0, "</code>" + + def wrap(self, source, outfile): + """Return the source with a code, pre, and div.""" + return self._wrap_div(self._wrap_pre(self._wrap_code(source))) + + formatter_opts.setdefault("cssclass", "codehilite") + formatter = HtmlCodeFormatter(**formatter_opts) + return pygments.highlight(codeblock, lexer, formatter) + + def _code_block_sub(self, match, is_fenced_code_block=False): + lexer_name = None + if is_fenced_code_block: + lexer_name = match.group(1) + if lexer_name: + formatter_opts = self.extras['fenced-code-blocks'] or {} + codeblock = match.group(2) + codeblock = codeblock[:-1] # drop one trailing newline + else: + codeblock = match.group(1) + codeblock = self._outdent(codeblock) + codeblock = self._detab(codeblock) + codeblock = codeblock.lstrip('\n') # trim leading newlines + codeblock = codeblock.rstrip() # trim trailing whitespace + + # Note: "code-color" extra is DEPRECATED. + if "code-color" in self.extras and codeblock.startswith(":::"): + lexer_name, rest = codeblock.split('\n', 1) + lexer_name = lexer_name[3:].strip() + codeblock = rest.lstrip("\n") # Remove lexer declaration line. + formatter_opts = self.extras['code-color'] or {} + + # Use pygments only if not using the highlightjs-lang extra + if lexer_name and "highlightjs-lang" not in self.extras: + def unhash_code(codeblock): + for key, sanitized in list(self.html_spans.items()): + codeblock = codeblock.replace(key, sanitized) + replacements = [ + ("&", "&"), + ("<", "<"), + (">", ">") + ] + for old, new in replacements: + codeblock = codeblock.replace(old, new) + return codeblock + lexer = self._get_pygments_lexer(lexer_name) + if lexer: + codeblock = unhash_code( codeblock ) + colored = self._color_with_pygments(codeblock, lexer, + **formatter_opts) + return "\n\n%s\n\n" % colored + + codeblock = self._encode_code(codeblock) + pre_class_str = self._html_class_str_from_tag("pre") + + if "highlightjs-lang" in self.extras and lexer_name: + code_class_str = ' class="%s"' % lexer_name + else: + code_class_str = self._html_class_str_from_tag("code") + + return "\n\n<pre%s><code%s>%s\n</code></pre>\n\n" % ( + pre_class_str, code_class_str, codeblock) + + def _html_class_str_from_tag(self, tag): + """Get the appropriate ' class="..."' string (note the leading + space), if any, for the given tag. + """ + if "html-classes" not in self.extras: + return "" + try: + html_classes_from_tag = self.extras["html-classes"] + except TypeError: + return "" + else: + if tag in html_classes_from_tag: + return ' class="%s"' % html_classes_from_tag[tag] + return "" + + def _do_code_blocks(self, text): + """Process Markdown `<pre><code>` blocks.""" + code_block_re = re.compile(r''' + (?:\n\n|\A\n?) + ( # $1 = the code block -- one or more lines, starting with a space/tab + (?: + (?:[ ]{%d} | \t) # Lines must start with a tab or a tab-width of spaces + .*\n+ + )+ + ) + ((?=^[ ]{0,%d}\S)|\Z) # Lookahead for non-space at line-start, or end of doc + # Lookahead to make sure this block isn't already in a code block. + # Needed when syntax highlighting is being used. + (?![^<]*\</code\>) + ''' % (self.tab_width, self.tab_width), + re.M | re.X) + return code_block_re.sub(self._code_block_sub, text) + + _fenced_code_block_re = re.compile(r''' + (?:\n+|\A\n?) + ^```\s*?([\w+-]+)?\s*?\n # opening fence, $1 = optional lang + (.*?) # $2 = code block content + ^```[ \t]*\n # closing fence + ''', re.M | re.X | re.S) + + def _fenced_code_block_sub(self, match): + return self._code_block_sub(match, is_fenced_code_block=True) + + def _do_fenced_code_blocks(self, text): + """Process ```-fenced unindented code blocks ('fenced-code-blocks' extra).""" + return self._fenced_code_block_re.sub(self._fenced_code_block_sub, text) + + # Rules for a code span: + # - backslash escapes are not interpreted in a code span + # - to include one or or a run of more backticks the delimiters must + # be a longer run of backticks + # - cannot start or end a code span with a backtick; pad with a + # space and that space will be removed in the emitted HTML + # See `test/tm-cases/escapes.text` for a number of edge-case + # examples. + _code_span_re = re.compile(r''' + (?<!\\) + (`+) # \1 = Opening run of ` + (?!`) # See Note A test/tm-cases/escapes.text + (.+?) # \2 = The code block + (?<!`) + \1 # Matching closer + (?!`) + ''', re.X | re.S) + + def _code_span_sub(self, match): + c = match.group(2).strip(" \t") + c = self._encode_code(c) + return "<code>%s</code>" % c + + def _do_code_spans(self, text): + # * Backtick quotes are used for <code></code> spans. + # + # * You can use multiple backticks as the delimiters if you want to + # include literal backticks in the code span. So, this input: + # + # Just type ``foo `bar` baz`` at the prompt. + # + # Will translate to: + # + # <p>Just type <code>foo `bar` baz</code> at the prompt.</p> + # + # There's no arbitrary limit to the number of backticks you + # can use as delimters. If you need three consecutive backticks + # in your code, use four for delimiters, etc. + # + # * You can use spaces to get literal backticks at the edges: + # + # ... type `` `bar` `` ... + # + # Turns to: + # + # ... type <code>`bar`</code> ... + return self._code_span_re.sub(self._code_span_sub, text) + + def _encode_code(self, text): + """Encode/escape certain characters inside Markdown code runs. + The point is that in code, these characters are literals, + and lose their special Markdown meanings. + """ + replacements = [ + # Encode all ampersands; HTML entities are not + # entities within a Markdown code span. + ('&', '&'), + # Do the angle bracket song and dance: + ('<', '<'), + ('>', '>'), + ] + for before, after in replacements: + text = text.replace(before, after) + hashed = _hash_text(text) + self._escape_table[text] = hashed + return hashed + + _strike_re = re.compile(r"~~(?=\S)(.+?)(?<=\S)~~", re.S) + def _do_strike(self, text): + text = self._strike_re.sub(r"<strike>\1</strike>", text) + return text + + _strong_re = re.compile(r"(\*\*|__)(?=\S)(.+?[*_]*)(?<=\S)\1", re.S) + _em_re = re.compile(r"(\*|_)(?=\S)(.+?)(?<=\S)\1", re.S) + _code_friendly_strong_re = re.compile(r"\*\*(?=\S)(.+?[*_]*)(?<=\S)\*\*", re.S) + _code_friendly_em_re = re.compile(r"\*(?=\S)(.+?)(?<=\S)\*", re.S) + def _do_italics_and_bold(self, text): + # <strong> must go first: + if "code-friendly" in self.extras: + text = self._code_friendly_strong_re.sub(r"<strong>\1</strong>", text) + text = self._code_friendly_em_re.sub(r"<em>\1</em>", text) + else: + text = self._strong_re.sub(r"<strong>\2</strong>", text) + text = self._em_re.sub(r"<em>\2</em>", text) + return text + + # "smarty-pants" extra: Very liberal in interpreting a single prime as an + # apostrophe; e.g. ignores the fact that "round", "bout", "twer", and + # "twixt" can be written without an initial apostrophe. This is fine because + # using scare quotes (single quotation marks) is rare. + _apostrophe_year_re = re.compile(r"'(\d\d)(?=(\s|,|;|\.|\?|!|$))") + _contractions = ["tis", "twas", "twer", "neath", "o", "n", + "round", "bout", "twixt", "nuff", "fraid", "sup"] + def _do_smart_contractions(self, text): + text = self._apostrophe_year_re.sub(r"’\1", text) + for c in self._contractions: + text = text.replace("'%s" % c, "’%s" % c) + text = text.replace("'%s" % c.capitalize(), + "’%s" % c.capitalize()) + return text + + # Substitute double-quotes before single-quotes. + _opening_single_quote_re = re.compile(r"(?<!\S)'(?=\S)") + _opening_double_quote_re = re.compile(r'(?<!\S)"(?=\S)') + _closing_single_quote_re = re.compile(r"(?<=\S)'") + _closing_double_quote_re = re.compile(r'(?<=\S)"(?=(\s|,|;|\.|\?|!|$))') + def _do_smart_punctuation(self, text): + """Fancifies 'single quotes', "double quotes", and apostrophes. + Converts --, ---, and ... into en dashes, em dashes, and ellipses. + + Inspiration is: <http://daringfireball.net/projects/smartypants/> + See "test/tm-cases/smarty_pants.text" for a full discussion of the + support here and + <http://code.google.com/p/python-markdown2/issues/detail?id=42> for a + discussion of some diversion from the original SmartyPants. + """ + if "'" in text: # guard for perf + text = self._do_smart_contractions(text) + text = self._opening_single_quote_re.sub("‘", text) + text = self._closing_single_quote_re.sub("’", text) + + if '"' in text: # guard for perf + text = self._opening_double_quote_re.sub("“", text) + text = self._closing_double_quote_re.sub("”", text) + + text = text.replace("---", "—") + text = text.replace("--", "–") + text = text.replace("...", "…") + text = text.replace(" . . . ", "…") + text = text.replace(". . .", "…") + + # TODO: Temporary hack to fix https://github.com/trentm/python-markdown2/issues/150 + if "footnotes" in self.extras and "footnote-ref" in text: + # Quotes in the footnote back ref get converted to "smart" quotes + # Change them back here to ensure they work. + text = text.replace('class="footnote-ref”', 'class="footnote-ref"') + + return text + + _block_quote_base = r''' + ( # Wrap whole match in \1 + ( + ^[ \t]*>%s[ \t]? # '>' at the start of a line + .+\n # rest of the first line + (.+\n)* # subsequent consecutive lines + \n* # blanks + )+ + ) + ''' + _block_quote_re = re.compile(_block_quote_base % '', re.M | re.X) + _block_quote_re_spoiler = re.compile(_block_quote_base % '[ \t]*?!?', re.M | re.X) + _bq_one_level_re = re.compile('^[ \t]*>[ \t]?', re.M) + _bq_one_level_re_spoiler = re.compile('^[ \t]*>[ \t]*?![ \t]?', re.M) + _bq_all_lines_spoilers = re.compile(r'\A(?:^[ \t]*>[ \t]*?!.*[\n\r]*)+\Z', re.M) + _html_pre_block_re = re.compile(r'(\s*<pre>.+?</pre>)', re.S) + def _dedent_two_spaces_sub(self, match): + return re.sub(r'(?m)^ ', '', match.group(1)) + + def _block_quote_sub(self, match): + bq = match.group(1) + is_spoiler = 'spoiler' in self.extras and self._bq_all_lines_spoilers.match(bq) + # trim one level of quoting + if is_spoiler: + bq = self._bq_one_level_re_spoiler.sub('', bq) + else: + bq = self._bq_one_level_re.sub('', bq) + # trim whitespace-only lines + bq = self._ws_only_line_re.sub('', bq) + bq = self._run_block_gamut(bq) # recurse + + bq = re.sub('(?m)^', ' ', bq) + # These leading spaces screw with <pre> content, so we need to fix that: + bq = self._html_pre_block_re.sub(self._dedent_two_spaces_sub, bq) + + if is_spoiler: + return '<blockquote class="spoiler">\n%s\n</blockquote>\n\n' % bq + else: + return '<blockquote>\n%s\n</blockquote>\n\n' % bq + + def _do_block_quotes(self, text): + if '>' not in text: + return text + if 'spoiler' in self.extras: + return self._block_quote_re_spoiler.sub(self._block_quote_sub, text) + else: + return self._block_quote_re.sub(self._block_quote_sub, text) + + def _form_paragraphs(self, text): + # Strip leading and trailing lines: + text = text.strip('\n') + + # Wrap <p> tags. + grafs = [] + for i, graf in enumerate(re.split(r"\n{2,}", text)): + if graf in self.html_blocks: + # Unhashify HTML blocks + grafs.append(self.html_blocks[graf]) + else: + cuddled_list = None + if "cuddled-lists" in self.extras: + # Need to put back trailing '\n' for `_list_item_re` + # match at the end of the paragraph. + li = self._list_item_re.search(graf + '\n') + # Two of the same list marker in this paragraph: a likely + # candidate for a list cuddled to preceding paragraph + # text (issue 33). Note the `[-1]` is a quick way to + # consider numeric bullets (e.g. "1." and "2.") to be + # equal. + if (li and len(li.group(2)) <= 3 + and ( + (li.group("next_marker") and li.group("marker")[-1] == li.group("next_marker")[-1]) + or + li.group("next_marker") is None + ) + ): + start = li.start() + cuddled_list = self._do_lists(graf[start:]).rstrip("\n") + assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>") + graf = graf[:start] + + # Wrap <p> tags. + graf = self._run_span_gamut(graf) + grafs.append("<p%s>" % self._html_class_str_from_tag('p') + graf.lstrip(" \t") + "</p>") + + if cuddled_list: + grafs.append(cuddled_list) + + return "\n\n".join(grafs) + + def _add_footnotes(self, text): + if self.footnotes: + footer = [ + '<div class="footnotes">', + '<hr' + self.empty_element_suffix, + '<ol>', + ] + + if not self.footnote_title: + self.footnote_title = "Jump back to footnote %d in the text." + if not self.footnote_return_symbol: + self.footnote_return_symbol = "↩" + + for i, id in enumerate(self.footnote_ids): + if i != 0: + footer.append('') + footer.append('<li id="fn-%s">' % id) + footer.append(self._run_block_gamut(self.footnotes[id])) + try: + backlink = ('<a href="#fnref-%s" ' + + 'class="footnoteBackLink" ' + + 'title="' + self.footnote_title + '">' + + self.footnote_return_symbol + + '</a>') % (id, i+1) + except TypeError: + log.debug("Footnote error. `footnote_title` " + "must include parameter. Using defaults.") + backlink = ('<a href="#fnref-%s" ' + 'class="footnoteBackLink" ' + 'title="Jump back to footnote %d in the text.">' + '↩</a>' % (id, i+1)) + + if footer[-1].endswith("</p>"): + footer[-1] = footer[-1][:-len("</p>")] \ + + ' ' + backlink + "</p>" + else: + footer.append("\n<p>%s</p>" % backlink) + footer.append('</li>') + footer.append('</ol>') + footer.append('</div>') + return text + '\n\n' + '\n'.join(footer) + else: + return text + + _naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I) + _naked_gt_re = re.compile(r'''(?<![a-z0-9?!/'"-])>''', re.I) + + def _encode_amps_and_angles(self, text): + # Smart processing for ampersands and angle brackets that need + # to be encoded. + text = _AMPERSAND_RE.sub('&', text) + + # Encode naked <'s + text = self._naked_lt_re.sub('<', text) + + # Encode naked >'s + # Note: Other markdown implementations (e.g. Markdown.pl, PHP + # Markdown) don't do this. + text = self._naked_gt_re.sub('>', text) + return text + + _incomplete_tags_re = re.compile("<(/?\w+[\s/]+?)") + + def _encode_incomplete_tags(self, text): + if self.safe_mode not in ("replace", "escape"): + return text + + return self._incomplete_tags_re.sub("<\\1", text) + + def _encode_backslash_escapes(self, text): + for ch, escape in list(self._escape_table.items()): + text = text.replace("\\"+ch, escape) + return text + + _auto_link_re = re.compile(r'<((https?|ftp):[^\'">\s]+)>', re.I) + def _auto_link_sub(self, match): + g1 = match.group(1) + return '<a href="%s">%s</a>' % (g1, g1) + + _auto_email_link_re = re.compile(r""" + < + (?:mailto:)? + ( + [-.\w]+ + \@ + [-\w]+(\.[-\w]+)*\.[a-z]+ + ) + > + """, re.I | re.X | re.U) + def _auto_email_link_sub(self, match): + return self._encode_email_address( + self._unescape_special_chars(match.group(1))) + + def _do_auto_links(self, text): + text = self._auto_link_re.sub(self._auto_link_sub, text) + text = self._auto_email_link_re.sub(self._auto_email_link_sub, text) + return text + + def _encode_email_address(self, addr): + # Input: an email address, e.g. "foo@example.com" + # + # Output: the email address as a mailto link, with each character + # of the address encoded as either a decimal or hex entity, in + # the hopes of foiling most address harvesting spam bots. E.g.: + # + # <a href="mailto:foo@e + # xample.com">foo + # @example.com</a> + # + # Based on a filter by Matthew Wickline, posted to the BBEdit-Talk + # mailing list: <http://tinyurl.com/yu7ue> + chars = [_xml_encode_email_char_at_random(ch) + for ch in "mailto:" + addr] + # Strip the mailto: from the visible part. + addr = '<a href="%s">%s</a>' \ + % (''.join(chars), ''.join(chars[7:])) + return addr + + def _do_link_patterns(self, text): + link_from_hash = {} + for regex, repl in self.link_patterns: + replacements = [] + for match in regex.finditer(text): + if hasattr(repl, "__call__"): + href = repl(match) + else: + href = match.expand(repl) + replacements.append((match.span(), href)) + for (start, end), href in reversed(replacements): + + # Do not match against links inside brackets. + if text[start - 1:start] == '[' and text[end:end + 1] == ']': + continue + + # Do not match against links in the standard markdown syntax. + if text[start - 2:start] == '](' or text[end:end + 2] == '")': + continue + + # Do not match against links which are escaped. + if text[start - 3:start] == '"""' and text[end:end + 3] == '"""': + text = text[:start - 3] + text[start:end] + text[end + 3:] + continue + + escaped_href = ( + href.replace('"', '"') # b/c of attr quote + # To avoid markdown <em> and <strong>: + .replace('*', self._escape_table['*']) + .replace('_', self._escape_table['_'])) + link = '<a href="%s">%s</a>' % (escaped_href, text[start:end]) + hash = _hash_text(link) + link_from_hash[hash] = link + text = text[:start] + hash + text[end:] + for hash, link in list(link_from_hash.items()): + text = text.replace(hash, link) + return text + + def _unescape_special_chars(self, text): + # Swap back in all the special characters we've hidden. + for ch, hash in list(self._escape_table.items()): + text = text.replace(hash, ch) + return text + + def _outdent(self, text): + # Remove one level of line-leading tabs or spaces + return self._outdent_re.sub('', text) + + +class MarkdownWithExtras(Markdown): + """A markdowner class that enables most extras: + + - footnotes + - code-color (only has effect if 'pygments' Python module on path) + + These are not included: + - pyshell (specific to Python-related documenting) + - code-friendly (because it *disables* part of the syntax) + - link-patterns (because you need to specify some actual + link-patterns anyway) + """ + extras = ["footnotes", "code-color"] + + +# ---- internal support functions + + +def calculate_toc_html(toc): + """Return the HTML for the current TOC. + + This expects the `_toc` attribute to have been set on this instance. + """ + if toc is None: + return None + + def indent(): + return ' ' * (len(h_stack) - 1) + lines = [] + h_stack = [0] # stack of header-level numbers + for level, id, name in toc: + if level > h_stack[-1]: + lines.append("%s<ul>" % indent()) + h_stack.append(level) + elif level == h_stack[-1]: + lines[-1] += "</li>" + else: + while level < h_stack[-1]: + h_stack.pop() + if not lines[-1].endswith("</li>"): + lines[-1] += "</li>" + lines.append("%s</ul></li>" % indent()) + lines.append('%s<li><a href="#%s">%s</a>' % ( + indent(), id, name)) + while len(h_stack) > 1: + h_stack.pop() + if not lines[-1].endswith("</li>"): + lines[-1] += "</li>" + lines.append("%s</ul>" % indent()) + return '\n'.join(lines) + '\n' + + +class UnicodeWithAttrs(unicode): + """A subclass of unicode used for the return value of conversion to + possibly attach some attributes. E.g. the "toc_html" attribute when + the "toc" extra is used. + """ + metadata = None + toc_html = None + +## {{{ http://code.activestate.com/recipes/577257/ (r1) +_slugify_strip_re = re.compile(r'[^\w\s-]') +_slugify_hyphenate_re = re.compile(r'[-\s]+') +def _slugify(value): + """ + Normalizes string, converts to lowercase, removes non-alpha characters, + and converts spaces to hyphens. + + From Django's "django/template/defaultfilters.py". + """ + import unicodedata + value = unicodedata.normalize('NFKD', value).encode('ascii', 'ignore').decode() + value = _slugify_strip_re.sub('', value).strip().lower() + return _slugify_hyphenate_re.sub('-', value) +## end of http://code.activestate.com/recipes/577257/ }}} + + +# From http://aspn.activestate.com/ASPN/Cookbook/Python/Recipe/52549 +def _curry(*args, **kwargs): + function, args = args[0], args[1:] + def result(*rest, **kwrest): + combined = kwargs.copy() + combined.update(kwrest) + return function(*args + rest, **combined) + return result + + +# Recipe: regex_from_encoded_pattern (1.0) +def _regex_from_encoded_pattern(s): + """'foo' -> re.compile(re.escape('foo')) + '/foo/' -> re.compile('foo') + '/foo/i' -> re.compile('foo', re.I) + """ + if s.startswith('/') and s.rfind('/') != 0: + # Parse it: /PATTERN/FLAGS + idx = s.rfind('/') + pattern, flags_str = s[1:idx], s[idx+1:] + flag_from_char = { + "i": re.IGNORECASE, + "l": re.LOCALE, + "s": re.DOTALL, + "m": re.MULTILINE, + "u": re.UNICODE, + } + flags = 0 + for char in flags_str: + try: + flags |= flag_from_char[char] + except KeyError: + raise ValueError("unsupported regex flag: '%s' in '%s' " + "(must be one of '%s')" + % (char, s, ''.join(list(flag_from_char.keys())))) + return re.compile(s[1:idx], flags) + else: # not an encoded regex + return re.compile(re.escape(s)) + + +# Recipe: dedent (0.1.2) +def _dedentlines(lines, tabsize=8, skip_first_line=False): + """_dedentlines(lines, tabsize=8, skip_first_line=False) -> dedented lines + + "lines" is a list of lines to dedent. + "tabsize" is the tab width to use for indent width calculations. + "skip_first_line" is a boolean indicating if the first line should + be skipped for calculating the indent width and for dedenting. + This is sometimes useful for docstrings and similar. + + Same as dedent() except operates on a sequence of lines. Note: the + lines list is modified **in-place**. + """ + DEBUG = False + if DEBUG: + print("dedent: dedent(..., tabsize=%d, skip_first_line=%r)"\ + % (tabsize, skip_first_line)) + margin = None + for i, line in enumerate(lines): + if i == 0 and skip_first_line: continue + indent = 0 + for ch in line: + if ch == ' ': + indent += 1 + elif ch == '\t': + indent += tabsize - (indent % tabsize) + elif ch in '\r\n': + continue # skip all-whitespace lines + else: + break + else: + continue # skip all-whitespace lines + if DEBUG: print("dedent: indent=%d: %r" % (indent, line)) + if margin is None: + margin = indent + else: + margin = min(margin, indent) + if DEBUG: print("dedent: margin=%r" % margin) + + if margin is not None and margin > 0: + for i, line in enumerate(lines): + if i == 0 and skip_first_line: continue + removed = 0 + for j, ch in enumerate(line): + if ch == ' ': + removed += 1 + elif ch == '\t': + removed += tabsize - (removed % tabsize) + elif ch in '\r\n': + if DEBUG: print("dedent: %r: EOL -> strip up to EOL" % line) + lines[i] = lines[i][j:] + break + else: + raise ValueError("unexpected non-whitespace char %r in " + "line %r while removing %d-space margin" + % (ch, line, margin)) + if DEBUG: + print("dedent: %r: %r -> removed %d/%d"\ + % (line, ch, removed, margin)) + if removed == margin: + lines[i] = lines[i][j+1:] + break + elif removed > margin: + lines[i] = ' '*(removed-margin) + lines[i][j+1:] + break + else: + if removed: + lines[i] = lines[i][removed:] + return lines + + +def _dedent(text, tabsize=8, skip_first_line=False): + """_dedent(text, tabsize=8, skip_first_line=False) -> dedented text + + "text" is the text to dedent. + "tabsize" is the tab width to use for indent width calculations. + "skip_first_line" is a boolean indicating if the first line should + be skipped for calculating the indent width and for dedenting. + This is sometimes useful for docstrings and similar. + + textwrap.dedent(s), but don't expand tabs to spaces + """ + lines = text.splitlines(1) + _dedentlines(lines, tabsize=tabsize, skip_first_line=skip_first_line) + return ''.join(lines) + + +class _memoized(object): + """Decorator that caches a function's return value each time it is called. + If called later with the same arguments, the cached value is returned, and + not re-evaluated. + + http://wiki.python.org/moin/PythonDecoratorLibrary + """ + def __init__(self, func): + self.func = func + self.cache = {} + + def __call__(self, *args): + try: + return self.cache[args] + except KeyError: + self.cache[args] = value = self.func(*args) + return value + except TypeError: + # uncachable -- for instance, passing a list as an argument. + # Better to not cache than to blow up entirely. + return self.func(*args) + + def __repr__(self): + """Return the function's docstring.""" + return self.func.__doc__ + + +def _xml_oneliner_re_from_tab_width(tab_width): + """Standalone XML processing instruction regex.""" + return re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in $1 + [ ]{0,%d} + (?: + <\?\w+\b\s+.*?\?> # XML processing instruction + | + <\w+:\w+\b\s+.*?/> # namespaced single tag + ) + [ \t]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + ) + """ % (tab_width - 1), re.X) +_xml_oneliner_re_from_tab_width = _memoized(_xml_oneliner_re_from_tab_width) + + +def _hr_tag_re_from_tab_width(tab_width): + return re.compile(r""" + (?: + (?<=\n\n) # Starting after a blank line + | # or + \A\n? # the beginning of the doc + ) + ( # save in \1 + [ ]{0,%d} + <(hr) # start tag = \2 + \b # word break + ([^<>])*? # + /?> # the matching end tag + [ \t]* + (?=\n{2,}|\Z) # followed by a blank line or end of document + ) + """ % (tab_width - 1), re.X) +_hr_tag_re_from_tab_width = _memoized(_hr_tag_re_from_tab_width) + + +def _xml_escape_attr(attr, skip_single_quote=True): + """Escape the given string for use in an HTML/XML tag attribute. + + By default this doesn't bother with escaping `'` to `'`, presuming that + the tag attribute is surrounded by double quotes. + """ + escaped = _AMPERSAND_RE.sub('&', attr) + + escaped = (attr + .replace('"', '"') + .replace('<', '<') + .replace('>', '>')) + if not skip_single_quote: + escaped = escaped.replace("'", "'") + return escaped + + +def _xml_encode_email_char_at_random(ch): + r = random() + # Roughly 10% raw, 45% hex, 45% dec. + # '@' *must* be encoded. I [John Gruber] insist. + # Issue 26: '_' must be encoded. + if r > 0.9 and ch not in "@_": + return ch + elif r < 0.45: + # The [1:] is to drop leading '0': 0x63 -> x63 + return '&#%s;' % hex(ord(ch))[1:] + else: + return '&#%s;' % ord(ch) + + +def _html_escape_url(attr, safe_mode=False): + """Replace special characters that are potentially malicious in url string.""" + escaped = (attr + .replace('"', '"') + .replace('<', '<') + .replace('>', '>')) + if safe_mode: + escaped = escaped.replace('+', ' ') + escaped = escaped.replace("'", "'") + return escaped + + +# ---- mainline + +class _NoReflowFormatter(optparse.IndentedHelpFormatter): + """An optparse formatter that does NOT reflow the description.""" + def format_description(self, description): + return description or "" + + +def _test(): + import doctest + doctest.testmod() + + +def main(argv=None): + if argv is None: + argv = sys.argv + if not logging.root.handlers: + logging.basicConfig() + + usage = "usage: %prog [PATHS...]" + version = "%prog "+__version__ + parser = optparse.OptionParser(prog="markdown2", usage=usage, + version=version, description=cmdln_desc, + formatter=_NoReflowFormatter()) + parser.add_option("-v", "--verbose", dest="log_level", + action="store_const", const=logging.DEBUG, + help="more verbose output") + parser.add_option("--encoding", + help="specify encoding of text content") + parser.add_option("--html4tags", action="store_true", default=False, + help="use HTML 4 style for empty element tags") + parser.add_option("-s", "--safe", metavar="MODE", dest="safe_mode", + help="sanitize literal HTML: 'escape' escapes " + "HTML meta chars, 'replace' replaces with an " + "[HTML_REMOVED] note") + parser.add_option("-x", "--extras", action="append", + help="Turn on specific extra features (not part of " + "the core Markdown spec). See above.") + parser.add_option("--use-file-vars", + help="Look for and use Emacs-style 'markdown-extras' " + "file var to turn on extras. See " + "<https://github.com/trentm/python-markdown2/wiki/Extras>") + parser.add_option("--link-patterns-file", + help="path to a link pattern file") + parser.add_option("--self-test", action="store_true", + help="run internal self-tests (some doctests)") + parser.add_option("--compare", action="store_true", + help="run against Markdown.pl as well (for testing)") + parser.set_defaults(log_level=logging.INFO, compare=False, + encoding="utf-8", safe_mode=None, use_file_vars=False) + opts, paths = parser.parse_args() + log.setLevel(opts.log_level) + + if opts.self_test: + return _test() + + if opts.extras: + extras = {} + for s in opts.extras: + splitter = re.compile("[,;: ]+") + for e in splitter.split(s): + if '=' in e: + ename, earg = e.split('=', 1) + try: + earg = int(earg) + except ValueError: + pass + else: + ename, earg = e, None + extras[ename] = earg + else: + extras = None + + if opts.link_patterns_file: + link_patterns = [] + f = open(opts.link_patterns_file) + try: + for i, line in enumerate(f.readlines()): + if not line.strip(): continue + if line.lstrip().startswith("#"): continue + try: + pat, href = line.rstrip().rsplit(None, 1) + except ValueError: + raise MarkdownError("%s:%d: invalid link pattern line: %r" + % (opts.link_patterns_file, i+1, line)) + link_patterns.append( + (_regex_from_encoded_pattern(pat), href)) + finally: + f.close() + else: + link_patterns = None + + from os.path import join, dirname, abspath, exists + markdown_pl = join(dirname(dirname(abspath(__file__))), "test", + "Markdown.pl") + if not paths: + paths = ['-'] + for path in paths: + if path == '-': + text = sys.stdin.read() + else: + fp = codecs.open(path, 'r', opts.encoding) + text = fp.read() + fp.close() + if opts.compare: + from subprocess import Popen, PIPE + print("==== Markdown.pl ====") + p = Popen('perl %s' % markdown_pl, shell=True, stdin=PIPE, stdout=PIPE, close_fds=True) + p.stdin.write(text.encode('utf-8')) + p.stdin.close() + perl_html = p.stdout.read().decode('utf-8') + if py3: + sys.stdout.write(perl_html) + else: + sys.stdout.write(perl_html.encode( + sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) + print("==== markdown2.py ====") + html = markdown(text, + html4tags=opts.html4tags, + safe_mode=opts.safe_mode, + extras=extras, link_patterns=link_patterns, + use_file_vars=opts.use_file_vars, + cli=True) + if py3: + sys.stdout.write(html) + else: + sys.stdout.write(html.encode( + sys.stdout.encoding or "utf-8", 'xmlcharrefreplace')) + if extras and "toc" in extras: + log.debug("toc_html: " + + str(html.toc_html.encode(sys.stdout.encoding or "utf-8", 'xmlcharrefreplace'))) + if opts.compare: + test_dir = join(dirname(dirname(abspath(__file__))), "test") + if exists(join(test_dir, "test_markdown2.py")): + sys.path.insert(0, test_dir) + from test_markdown2 import norm_html_from_html + norm_html = norm_html_from_html(html) + norm_perl_html = norm_html_from_html(perl_html) + else: + norm_html = html + norm_perl_html = perl_html + print("==== match? %r ====" % (norm_perl_html == norm_html)) + + +if __name__ == "__main__": + sys.exit(main(sys.argv)) diff --git a/test.md b/test.md new file mode 100644 index 0000000..59ff640 --- /dev/null +++ b/test.md @@ -0,0 +1,8 @@ +# hello world + +This is a *test*. + +I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev] + + +[prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc \ No newline at end of file diff --git a/utils.py b/utils.py index e4339f7..6504b05 100644 --- a/utils.py +++ b/utils.py @@ -1,4 +1,27 @@ -import sublime +# import sublime +import time def get_settings(): - return sublime.get_settings("MarkdownLivePreview.sublime-settings") \ No newline at end of file + return sublime.get_settings("MarkdownLivePreview.sublime-settings") + +def min_time_between_call(timeout, on_block=lambda *args, **kwargs: None): + """ Enforces a timeout between each call to the function + timeout is in seconds + """ + last_call = 0 + + def outer(func): + def wrapper(*args, **kwargs): + nonlocal last_call + + if time.time() - last_call < timeout: + time.sleep(timeout - (time.time() - last_call)) + + last_call = time.time() + return func(*args, **kwargs) + return wrapper + return outer + +@min_time_between_call(1) +def hello(): + print(time.time()) \ No newline at end of file From 5738f6b5ff8db9d57dabe87e8be18f8a698b5062 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Thu, 14 Nov 2019 15:56:55 +1100 Subject: [PATCH 07/20] render the preview as soon as the markdown file loads --- MarkdownLivePreview.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index b06a0ab..6b10aa2 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -72,9 +72,6 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): markdown_view.run_command('mdlp_insert', {'point': 0, 'string': content}) markdown_view.set_scratch(True) - MarkdownLivePreviewListener.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view) - MarkdownLivePreviewListener._update_preview(MarkdownLivePreviewListener, markdown_view) - markdown_view.set_syntax_file(syntax_file) markdown_view.settings().set(MARKDOWN_VIEW_INFOS, { "original_window_id": original_window_id @@ -113,6 +110,16 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): else: self.content = None + def on_load_async(self, markdown_view): + infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) + if not infos: + return + + preview_view = markdown_view.window().active_view_in_group(1) + + self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view) + self._update_preview(markdown_view) + def on_close(self, markdown_view): """ Use the information saved to restore the markdown_view as an original_view """ @@ -154,10 +161,12 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): self._update_preview(markdown_view) def _update_preview(self, markdown_view): + print('update markdown view', markdown_view.is_loading()) total_region = sublime.Region(0, markdown_view.size()) markdown = markdown_view.substr(total_region) html = self.markdowner.convert(markdown) + print(html) # FIXME: replace images From bae26fc45277f9bbdb363b0e17bcaf6836896c11 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Thu, 14 Nov 2019 17:28:02 +1100 Subject: [PATCH 08/20] Viewing images works for local files We have to make sure that everything is converted to base64 (and replaced in the src attribute) because otherwise ST3 messes up the height of the images --- MarkdownLivePreview.py | 23 ++++++++---- dependencies.json | 7 ++++ live-testing/sublime_text.png | Bin 0 -> 38950 bytes live-testing/test.md | 20 ++++++++++ markdown2html.py | 69 ++++++++++++++++++++++++++++++++++ test.md | 8 ---- 6 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 dependencies.json create mode 100644 live-testing/sublime_text.png create mode 100644 live-testing/test.md create mode 100644 markdown2html.py delete mode 100644 test.md diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 6b10aa2..3f5ae64 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -1,7 +1,10 @@ +import os.path import sublime import sublime_plugin -from .lib.markdown2 import Markdown +from functools import partial + +from .markdown2html import markdown2html from .utils import * def plugin_loaded(): @@ -85,8 +88,6 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): class MarkdownLivePreviewListener(sublime_plugin.EventListener): - markdowner = Markdown() - phantom_sets = { # markdown_view.id(): phantom set } @@ -161,17 +162,23 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): self._update_preview(markdown_view) def _update_preview(self, markdown_view): - print('update markdown view', markdown_view.is_loading()) + # if the buffer id is 0, that means that the markdown_view has been closed + # This check is needed since a this function is used as a callback for when images + # are loaded from the internet (ie. it could finish loading *after* the user + # closes the markdown_view) + if markdown_view.buffer_id() == 0: + return + total_region = sublime.Region(0, markdown_view.size()) markdown = markdown_view.substr(total_region) - html = self.markdowner.convert(markdown) - print(html) - - # FIXME: replace images + basepath = os.path.dirname(markdown_view.file_name()) + html = markdown2html(markdown, basepath, partial(self._update_preview, + markdown_view)) self.phantom_sets[markdown_view.id()].update([ sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, lambda href: sublime.run_command('open_url', {'url': href})) ]) + \ No newline at end of file diff --git a/dependencies.json b/dependencies.json new file mode 100644 index 0000000..d0b3031 --- /dev/null +++ b/dependencies.json @@ -0,0 +1,7 @@ +{ + "*": { + "*": [ + "bs4" + ] + } +} diff --git a/live-testing/sublime_text.png b/live-testing/sublime_text.png new file mode 100644 index 0000000000000000000000000000000000000000..8be1c1f54bb1cc00a66d363c395576862de35851 GIT binary patch literal 38950 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 literal 0 HcmV?d00001 diff --git a/live-testing/test.md b/live-testing/test.md new file mode 100644 index 0000000..cfff733 --- /dev/null +++ b/live-testing/test.md @@ -0,0 +1,20 @@ +# hello world + +This is a *test*. + +I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev] + +This is the first image from the local file system (absolute path, sorry): + +![The sublime text logo!](file:///home/math2001/.config/sublime-text-3/Packages/MarkdownLivePreview2/live-testing/sublime_text.png) + +This is the first image from the local file system, *relative* path! + +![The sublime text logo!](sublime_text.png) + +This is the first image from the internet! + + +![The sublime text logo!](https://www.sublimehq.com/images/sublime_text.png) + +[prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc \ No newline at end of file diff --git a/markdown2html.py b/markdown2html.py new file mode 100644 index 0000000..29de445 --- /dev/null +++ b/markdown2html.py @@ -0,0 +1,69 @@ +import base64 +import os.path +from functools import lru_cache +from .lib.markdown2 import Markdown +from bs4 import BeautifulSoup + +__all__ = ('markdown2html', ) + +markdowner = Markdown() + +# FIXME: put a nice picture please :^) +BASE64_LOADING_IMAGE = 'loading image!' +BASE64_404_IMAGE = '404 not found :-(' + +class LoadingError(Exception): + pass + +def markdown2html(markdown, basepath, re_render): + """ converts the markdown to html, loads the images and puts in base64 for sublime + to understand them correctly. That means that we are responsible for loading the + images from the internet. Hence, we take in re_render, which is just a function we + call when an image has finished loading to retrigger a render (see #90) + """ + html = markdowner.convert(markdown) + + soup = BeautifulSoup(html, "html.parser") + for img_element in soup.find_all('img'): + src = img_element['src'] + # already in base64, or something of the like + # FIXME: what other types are possible? Are they handled by ST? If not, could we + # convert it into base64? is it worth the effort? + if src.startswith('data:image/'): + continue + + if src.startswith('http://') or src.startswith('https://'): + path = src + elif src.startswith('file://'): + path = src[len('file://'):] + else: + # expanduser: ~ -> /home/math2001 + # realpath: simplify that paths so that we don't have duplicated caches + path = os.path.realpath(os.path.expanduser(os.path.join(basepath, src))) + + try: + base64 = get_base64_image(path) + except FileNotFoundError as e: + print("{!r} not found {!r}".format(path, e)) + base64 = BASE64_404_IMAGE + except LoadingError: + # the image is loading + base64 = BASE64_LOADING_IMAGE + + img_element['src'] = base64 + + # FIXME: how do tables look? should we use ascii tables? + + return str(soup) + +# FIXME: This is an in memory cache. 20 seems like a fair bit of images... Should it be +# bigger? Should the user be allowed to chose? There definitely should be a limit +# because we don't wanna use to much memory, we're a simple markdown preview plugin +@lru_cache(maxsize=20) +def get_base64_image(path): + if path.startswith('http://') or path.startswith('https://'): + return 'loading of the internet!' + + with open(path, 'rb') as fp: + return 'data:image/png;base64,' + base64.b64encode(fp.read()).decode('utf-8') + diff --git a/test.md b/test.md deleted file mode 100644 index 59ff640..0000000 --- a/test.md +++ /dev/null @@ -1,8 +0,0 @@ -# hello world - -This is a *test*. - -I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev] - - -[prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc \ No newline at end of file From ef9b2daf6d2bc433fc62c61ddfe885f09226159f Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Thu, 14 Nov 2019 19:30:26 +1100 Subject: [PATCH 09/20] Load images from the internet using an in memory cache As soon as the plugin is reloaded, or the editor restarted, the images must be reloaded. Maybe we could use a file cache... --- MarkdownLivePreview.py | 2 +- live-testing/test.md | 3 +-- markdown2html.py | 48 +++++++++++++++++++++++++++++++++++------- 3 files changed, 42 insertions(+), 11 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 3f5ae64..f330712 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -150,7 +150,7 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): original_view.set_syntax_file(markdown_view.settings().get('syntax')) - # here, views are NOT treated independently, which is theoratically wrong + # here, views are NOT treated independently, which is theoretically wrong # but in practice, you can only edit one markdown file at a time, so it doesn't really # matter. # @min_time_between_call(.5) diff --git a/live-testing/test.md b/live-testing/test.md index cfff733..cc20e53 100644 --- a/live-testing/test.md +++ b/live-testing/test.md @@ -14,7 +14,6 @@ This is the first image from the local file system, *relative* path! This is the first image from the internet! - -![The sublime text logo!](https://www.sublimehq.com/images/sublime_text.png) +![some paysage](https://4.bp.blogspot.com/-RHTlwmd9EBw/Tn84-JEW8NI/AAAAAAAAAD0/6ugjklUMbtY/s1600/acapulco-8-704125.jpg) [prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc \ No newline at end of file diff --git a/markdown2html.py b/markdown2html.py index 29de445..b90c211 100644 --- a/markdown2html.py +++ b/markdown2html.py @@ -1,17 +1,26 @@ +import concurrent.futures +import urllib.request import base64 import os.path -from functools import lru_cache -from .lib.markdown2 import Markdown + +from functools import lru_cache, partial from bs4 import BeautifulSoup +from .lib.markdown2 import Markdown __all__ = ('markdown2html', ) markdowner = Markdown() +# FIXME: how do I choose how many workers I want? Does thread pool reuse threads or +# does it stupidly throw them out? (we could implement something of our own) +executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) + # FIXME: put a nice picture please :^) BASE64_LOADING_IMAGE = 'loading image!' BASE64_404_IMAGE = '404 not found :-(' +images_cache = {} + class LoadingError(Exception): pass @@ -26,6 +35,7 @@ def markdown2html(markdown, basepath, re_render): soup = BeautifulSoup(html, "html.parser") for img_element in soup.find_all('img'): src = img_element['src'] + # already in base64, or something of the like # FIXME: what other types are possible? Are they handled by ST? If not, could we # convert it into base64? is it worth the effort? @@ -42,7 +52,7 @@ def markdown2html(markdown, basepath, re_render): path = os.path.realpath(os.path.expanduser(os.path.join(basepath, src))) try: - base64 = get_base64_image(path) + base64 = get_base64_image(path, re_render) except FileNotFoundError as e: print("{!r} not found {!r}".format(path, e)) base64 = BASE64_404_IMAGE @@ -56,14 +66,36 @@ def markdown2html(markdown, basepath, re_render): return str(soup) -# FIXME: This is an in memory cache. 20 seems like a fair bit of images... Should it be -# bigger? Should the user be allowed to chose? There definitely should be a limit -# because we don't wanna use to much memory, we're a simple markdown preview plugin -@lru_cache(maxsize=20) -def get_base64_image(path): +def get_base64_image(path, re_render): + + def callback(url, future): + # this is "safe" to do because callback is called in the same thread as + # add_done_callback: + # > Added callables are called in the order that they were added and are always + # > called in a thread belonging to the process that added them + # > --- Python docs + images_cache[url] = future.result() + # we render, which means this function will be called again, but this time, we + # will read from the cache + re_render() + if path.startswith('http://') or path.startswith('https://'): + if path in images_cache: + return images_cache[path] + executor.submit(load_image, path).add_done_callback(partial(callback, path)) return 'loading of the internet!' with open(path, 'rb') as fp: return 'data:image/png;base64,' + base64.b64encode(fp.read()).decode('utf-8') +# FIXME: This is an in memory cache. 20 seems like a fair bit of images... Should it be +# bigger? Should the user be allowed to chose? There definitely should be a limit +# because we don't wanna use to much memory, we're a simple markdown preview plugin +# NOTE: > The LRU feature performs best when maxsize is a power-of-two. --- python docs +@lru_cache(maxsize=2 ** 4) +def load_image(url): + with urllib.request.urlopen(url, timeout=60) as conn: + content_type = conn.info().get_content_type() + if 'image' not in content_type: + raise ValueError("{!r} doesn't point to an image, but to a {!r}".format(url, content_type)) + return 'data:image/png;base64,' + base64.b64encode(conn.read()).decode('utf-8') \ No newline at end of file From cc28bfef96de4d577112f40cda41b39ea757190b Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Thu, 14 Nov 2019 21:13:16 +1100 Subject: [PATCH 10/20] clean up code; support code block; add few FIXMEs --- .gitignore | 1 + live-testing/sublime_merge.png | Bin 0 -> 38423 bytes live-testing/test.md | 12 +++++++++--- markdown2html.py | 20 +++++++++++++------- utils.py | 4 ---- 5 files changed, 23 insertions(+), 14 deletions(-) create mode 100644 .gitignore create mode 100644 live-testing/sublime_merge.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bee8a64 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +__pycache__ diff --git a/live-testing/sublime_merge.png b/live-testing/sublime_merge.png new file mode 100644 index 0000000000000000000000000000000000000000..d94427ea817cbcb77e320e42ef4ed3d8577f3748 GIT binary patch literal 38423 zcmb??1D9s8((c=~ZDZQDZQHhO+wPvW?P(j+wr$&(=I!&Hd%s_Bv-VD^YE_=9O0u%| z3L+Kc#NnW^p#cB@oTP+^5&!`FLjnUJL4JzCQqs|%0@PekRuBMah==(w1pDEEoR!3d z0JYP&XFm*-gM_9t004vXpBD&_nT`3QfMcnm;i4fc!)0u5OJ`_eZ)8g6Ve9at4FK@) zdpH;xTbsHN8kw3~+VK+KboLSxTAJ_@tFy{7$T|p{T3AYWIhiVZ$*CB7SsQbj5cBgv z^LTLm0N9$k7!rEe+SoaBdGHed4=&da{U0$sG2#EHxLETNYse}P3fnuG60*{<(lHS8 zK@;*gnV4}YiHQB*%0C=0v4x9^0~bBLySqD`J2RcVlQ}&jCnqO80~0+H6YY-%t+S_{ zi=hXtoioXQApVOXV(M({Wa;2yX>UjPA523ddsi1;Vq*IL7@_~aR({;2|6hN9nE#E~ z)b1xtr=NJRrK-+<nrTQuMnzQIK}PI<lDN3ItgNhvh{#X=BM=Y}_^<pYmynS7U;0P> zqaZ0M`9u6S{jmQ_{Luep0mmm1YwOa|(lRnKf`WoVLPAnfQo_Q*KNWvW`~d!#`@s|y z6&1?(Et;4Fbac#DTW7Yl9WpmBtE>#z+zMY=4ICR+)Yi7i$N-Fuv)S1A_V)4CH}KTe z0}hYE*49-`O?g#S5jwlV7MC>b?09r_xeW|Fn_B=2i)PW$R57sv%E~H+hB#H#{_7hC zK0Z>ivN8$^j9%XUe}{fN5d3i<IYl@*1u!#9oRbS!TPG|nb1E+v78MPgn&vb&=a-QI z>>r>tw@{|0vAVi>b#!VuJ4;GQS;WVaW@d>?N&;3^AqW4`hlV+plwchmnS_T6N=eZ~ zM$!BFNl8oF=H|+4Xc%s7Q|%w9ZtsYRiwn!j3X6!iH~tY47S{9h<lNi0+1wJ}-NUM_ z6OfaG?df%?u2HwLa@*K63=A|13>3Gu{b@L|rDd0zTEOm}>&7N$dk4R$s9k=(=uZa~ z75_A@V0ndNbxq|SPUX+w#&J=SQwB&7K(4r)04+xwa!vdy?K`c54S9eGIjFOHNWo~S z1^@^Fk|KgC9_yD{6LGlW@Q=nOhNK!nt_*}nM8K;E>)+p5uIfoK25J|JtizAQoCyr^ z=an+#Sg_z1>H00U8~;C^@4V-emlb+_eZLsnn*qNrpU?ZZ&l&%-TYf)3-+w0s-&=nE z@^0L5=_@k?J0Gw9-?t;r&+pCrd>>yeYu}ax-`)s;^7awHy*)M%z5d?+o@(a5Z>}~^ z>U(!UL3=)%dEaTCS6VM-zFXnGXH>xbIzK+x-@WPRJRCuNHvWCvU^-Z$y|!I^KimaS z|K2_Ro>_&_S^c)R=PS}Jh49$~-Q~3GQx)A^cs_DoFN;3r?*#E#*1OFdn+?AjGjB#n zX9$1;%)kJt*9XjO#Lrmg(L)BTTn(QzufPBsyadc_Lh0RpojmU|ZyeZ1`4I%0bpATf z_nqkX_V4m_It07Z{fK=U`PgzCyDR>uzqNjC`BIN|*Cs`vW#bp|xKXbbaDZKp?y_xF zTcc*9y)aOI1h;{)=I^><(R$d}>DPI@{mheey!H0{4u%?lyZCFNy90k@^KNhD5n~<w zLSc+mj|LZWgY&&cS1KM4q3)U(Y%Ias3hU6xpDwTImBQc6kMzU7{3vI+XE-En+L`=U z=(PEC3;pOjtq(p0kp7osJclapei`FsgW|-I#yr;eeHWf3PK2&T0)sNkoQc@zCfmF> zuj?w|AYSJ6@!LlKQ!=z;9BZq$LH$<POPp_iHLPw+TglL`89y_9UimSO^@m;_T!6lB zJb23D-Qr$D-8P#$be1Qh+?lVCp;zH1_f+0-$x>*EMeIA@@$3;#C=wWkn(#PTitHkZ zR%2sXuQ97Ht6SeO|Esh5?oiH&iH~o0A=EDNh_fRMB2?tHB1p?04m8D$e>uLe2#nu^ zYtt9-3XR066ri<jgl7({{s{hu7Vieq_j*LOoRsd>ifp|GWMPlzP5XXTwpU8=!ZXwB zQwO$v@!m@*)=ZDe0TC<h#b#D?I%ULBt($W-mWb6{yF`)TW#dysZ1W6aGXy%P7AAU0 z19CL5PnPmH)WH+kmmV5E!L%tfaoH~pn<scKw`40Q_JU3y*CT^Kvjd259$N&o{hq}9 zO)3~B_!x!09m#Op_S9aI8pNmg-qxgW)cPjW1+l@mhtD#0x!8F^{w*Veau?gaHz}Uj zGZvu1h5RmNg#rUtbqbZYL4e)gS~0)f8kFmi3c<^3%A6;Uc-b22F_A25Z{>0$1ogr{ z&~wIXe1ffu#<(n6GG1tGb@RyVhpB0&*GoY-_PFo;!Vh#R>YX&v)i?y``XxNdJ{<rC zP4{kfAe8dj@*vIAmm2KRe}7R{cM+XSc!HXH>5PTA09Fk*wYoz42HVtg0jnN?coh@q zjz}E#`@ba)tW_c&7IEgOHE9vWN+gh?9f@D!{)Eb=1w7q(E)4=1a6xmU88#d7?Bx#d zxkC-Y#SOt}R>nUxH6BaS%r_t#aUC6ZH!f6hWK%d)V6o)%Q7Z-gZf)r{V;7n||MR>w zeiCH_nDHH1pI&|%wP3nGq*YJ37?BRhytze?X&XFEAh!@kSi<kt+o9fQD%&JH?EQ0k zNU<C`2nE5+QGhRN-8Y|}BbmOW<CaqgX_bAZ!*K~vkwAn7Yl99-0NtO1Yz)O4-6op* zZf(+1QW!rz07CS@YFjhKQejW%pKa8B%#0&3Nf_gTxxwnFRy+%AI7gz8+EH}O#vb8f z@?u_)9Il*j@r>sk$SVIm5+)gmO_R^=xi_mlE-Yf9$iUrSYX`2A;)E@lYNAVqg+V9~ zhLO1|HKtkdCB|xS;%PMe0Fj%iSz_@`y6h>K+{b_;7PCagrK7S%mXpMw34K0yD?)8= zl+Zn{d=-5-3g%1Vv)4?F4a(AE{5yF=EHQ!<r;s3#bFE0|<a(;sYucDr-32IhaTK2S zXcKB)qaA(;=5M3!Q*6)_z%%jmjga<8meVo`W{HxLDZZqyISf74h>WuF@@b0L>#y$| zN#@hLA@u6i2*`E+5VZd3)b$=`dwFC?B`}qaTQPYMdXiaaF*_4fe0*|WiDQjY@OVxX zTi&!x9r4RKbi#E69gsZJsH@H>pO>{WVGEF;u&GhW%$TsSe+J@yUo>40x|#^1-4e(t zuj&3q7e#gMo|_}j>uX?b;cSUB0+N^~dqZmdFnJq|mhxWVa+?T5eVecnD?y3D@BODV z9G02|92-W1!or?B{tU!6SpEA02c=EZ<VPZrj5M{#LUSlZ22}!8i7<Pj>9(0x(oLMY zU?ri+DIBAy16D`h$4a6!5b}0gr6-A*z4CC@4j9f;Xm|q;R3sS!Q_Vm2`|$G(sZ#e) znOwkwwQEpFe}uvhmvO=rqojV}$L1)}nuO1glCbsvk^+++QM<;$0>RN+nmpEU`{a-> zn1w6HE`%Wkldt0lTgaM5Zyp1FPz?iy$`k)x>+{|r1YwcA<C?V$LiLyVCf12lbIG>6 z{uhmW_*?RT{bJb$t7cgHB)mkG3TM*_@GKeH*nD?`AianjO(vo}4OnJO|A=|MCEN1Y zzr~b(Ic_F}p(5JVfN9~@!XS0R0K}n6`(HR`<v0LF%&`z+7I0%|@JpT}=Db$?knxaN zjy9>%%S;?$weReI;XWH9#FQn216~oMxE+Cwh)<|m_5FOCt2yj<)dt*%0LmWFq6Uth zY&KfW(1EI`ioPtbGT_~TFco@x_qG5KkF7CVk7;=GfQ6#936;^TZx*Qv{DQ?qNLo|^ z=^@fpET+Ucy}cyNI+E!9B$g(^UeW$r*n8sIQW|cl#zsPgjYqo)?^+bTZW7fp>`%*k zKLOu55$deZ437v47eXdB8<})1t-EW-Sa`%sggY6p=3hXh^7F45KpPqj1Po-q<V#eL zL)YtHNI00xVas$wdFN>d2}_z2E#a}vGo`PmH1bzIP)^H`JT+FWh{dUd1Ev|AF&J1B z21Cx{<7~u+%ut?fI}2TJ@o}!U5^?#aA%9DE6~l~*49_|W%1XYb>#Q@Q$ZKN#zK3os z&qtc%d4nPaZ!Bk`dRz+Vf5eZuT%@q9)CrWjq#R}d;$uzqyt{%?GFjEg9unx#pK4P~ zHilwuZ$7P@eSTJuut2HJzlFA$8Q0G(&l23{RElTQ{8ift0BWN2w<y>P@s-dF&e5w7 zwP6*8BvRni>7O%fa_+VAG3dqxj}-!ixJ$npXpnFR2Q`<6u|h{XK{%xJr*p7NaLu4> zA{1c~B?~`r86qXtLECPa-j_o^nRp#=ebHcDpU@aRFXTM@*n<Y8^uq2C6SRE8_negY zQjQuw<b3)KP0-j3Dw+{tYE%lZd&ba?Emk#f+%~t!p>n3N&BltAL`DQWI~OsgiN`Pn zBw>^vlpVUB`mZkxTIR}c@&0eB5v?MLAazWA7_x-8ayD6gorsLy9iPB*CX`HQ#T6WM zA~F}A5cSzv%e)_{2av@pAJ=uH?TDfp6dur~4ZV_BZ1z8_-;;rv;_{QoY0`93RzXU= zaG?sW>%^??Fe@Gzs|?U+XbGB^0*ZK`R>#)>sII{<7;taSB5HV?DKR-ox=gTQWdpeC zO(u{59t8i1Dk-~W`T0u2DV9QZns#O27mMGFhyCaCDjsk=4{a4uR0AP~piRSclV^-! zq$0_6mme%n(6ABPgmU@lKxXqsQm)E^ahy_umUf&3QBt9or7GtqvqZ)en+_q)$PGaK zqiFV^6;<({CblZc;h^6|C3m$pkUA75riY-k;?p5kL!!)+>7lN*X$7NZw%fzbU~g__ zlz=~9G*R!cB^IW$LA7e!R8_xzGw=ChZdC--0vTjKm*X2)?CphmVK25XFM{zMPSVs% z*^dPWY3U3A0mqVSM>EurQ!YussVkm~SP91H!X>^+K*#9Si8DEHRmdBa68=TJCqgz8 zK*Bg^zYLG5DQ?kuGV2R4i}Mw^pqdtsr8aB(>$?#HPyT^{j`!wc&TWu~(r?HtX~n7K z<oTHV5LSOY8or26sF+fh0j}1a99I~r(HNgC3qD5Y6|B)!v4r$g1EPWpORkb%43{BO zd=K2Mp>v93p7#-pZ^CIIp)nxAGs?Gd8&JtGpUcqr(+7O4v;9>4qbZTK6%I4nqU9E* z8It!>A~-ZqbNb)oN-?W$OL#GaY)yOX2S5S+(8kW{u~I1e=G2%TVL?$gb_)Br_c!O@ z3Zv|eLdA7gYj?cZT_@Z`l+Q)`Mts5LPnY-B`w)?hNbFj&s+twZ%lo2m@n|L+am8TH zRo{vSAg|HNS6F8>B0GonrBKUC(N#Qu#&!~*XAuvA01;|x;krj_YzDuW6XofJ%p`>m zpDA_d;hCA=Z_&O+I+_n9l8)MfyTAwp9)Yk=WeE(HgWE^5i{iJ=$nT7sX{lAr#6rS5 zvv?&@CC7-`i}RNstiAzWM4i8I{uLF8dXoAH!6mRwM&&$^Fq<vfm`%GZK(sK1vRQ=x zO+#EEYAy^0RbDo<(Jiw06S?whoyv;6oz*h7-hb15ULM+q5e89WZgIV(qhXw@@1OrT z)K*Zih`~}FT0zPUAKC=R&8npniVZh$Aw->gDm`T%t`EXHITE?v>SoaScZ9$BEM#5G z#P<rJz$YTvjoXMk)fVg#Dx_%OC9SRyO#MVdx^Cq2Vn;=h4yxdZmW6u9KNKvBZ>c-+ z-*&`xZ^9mZW+^H6FG{sI7Zt)Om8zEgz!oTgy>mw7_zf^$jxI6JvO-v#=Bajxb<T<V zepXx2*@==*rw|yTzrSd0ZhT+pP}43=sX}+EK3b8zRRQ*d6ULxpkgm(Enl-KO15ke- z8z}~VCyZU`&C<px82A6Ry-s=tLlOx7LNcD8E3ojg(&xEp3pgRR1jfY=mtsftbXJ?P zoZ`)fEmv@kUKbYa4f^EBVk6?I&%Bk9-~l##xI)pJ!Pz1TKHE?tN-AG_z46VUQZ7<s z^=m}IWVRh{^51s!Zros4(E_sh^0MF($$G$^l+fO-WE>UY23adXipD{tBInF?nZF}| zA(^;>8>gq0vXWNe(Pz$x5}kLc@bS2#X=NQ`7G*SazNRXwB@4{@T2cTe8u{ees^|%p zSx_Yi%w*Z14r;+vt~T@@hfrgvmBDrXBtZwg)0=@LJy2OYL6jQT>wt=jK<)4Jqs6ZO zL}KNin!FC9gG2*uo6Fi0j{{{YvHY~`Nq(xiD9Fb=o2PTz;tTplP$>d8o;X#7WCa$_ z6nJxY8s-dm%`WS^*K*$HeaUZyZ76>WM9WszE9p31+NNR$GQ`m4x|ngqyy9uk*f4KZ zgknyWKn)NleLo+ti9#ymaL&Ojmc%x^I@{|WiNRcsLdQ8wM&fAA)ybmrL3m)3B+|Bb z34X{n<Z*+^%qG@Oxp29fx*q8F6JMkuVoUQ*tNl)nCG<Ew#|<@fJAWT?<012Cp3qO` z+g^@nj(Ij_KhP~e*DK5ftb-=N-h~_ylIheJUF647g@ohmwnBPZa5{|_`N7>kpit<B zaRej$accy&sz_1FzJ46x#E(ATHrjeg!$FI)VLe>Y4Lp}5=92^zU<YoSa^@o8v5VJj z!H!^A<$4q}%}m9Ns_biYa*l6D^!$$OmCP8S8W9#798;cvh;!61)CSW4@i5rL?l-p4 z$jNUs@C)=L_5NtK+fJDQ9Tra1pUKY&^}PvR`GP||Y}HJA&g<ZiYQlZRzy&maqlC3v zew?6l^gTX;RFZygVuC@<2qHRqY<(c@D0_9$e#_d(%MiY!lkuY|73%!G!dE#-qOb-i z<{1snJ%y$rLuS~z#Qd$5(`A9+m6I*pN9jiQT(osdcl&iWb$^qT815K8#AmnWBl&J{ zFJ60dV|X+#L_pfdhUWq46Oie>F08BIke(A#O7EARCxdVBySwo?ZAj5!ywG(eU-6%O zbi)JJfDeNuC&|uId^$o_35_`gi_|{fr$Uk+<;nxxj4JLK`mM>lGUAkAYcg@J#~+O| zP&I-j)q+i{muWNsrp<trYSY=*;2AKFON40SKYb3{D01;$T^SgY8$t<4QHCG^7m-Z( z#4~~a(&K{0A3p^o37Ht23}H30S3_8e@I}@ZHVJq7RN1`*Y_&98(rU5;D<OiwB(}e} zTn|hgXZ%G{ip+`qP+~c)`jBa8!dTJa;?u3v?DnJ49ln*2XH>;+3J9}Wb#mJI3Zn)* zQ#^`HX2O=(ayC;U2~O%gv@lOaDz4Yj$1A|kGwMFqQDT|{K3=n|Vy-48D5Y4szrp-N z&pwBC1%ddYZoRS;KE5Xdb?V5?8h1ym+)#UQ1mCCJ;7#|pU$H#zHnrRv?85{F)-dA! zJ<xYwePMV_Q?XFv+56oANfHO}k%Ge)pC`1H#UY<vLqpi}BCFBwvV2`YPxIuJ=<53# zTm9>*=Fpz5>TA2YqAFQh9+02yT7eNBNrrP*bG=F*(E+(|-Oj&S{1`I=jZZ~y%6#Ct zxS;?zXDGKRLz@KNyWXF#Ll$dh%Z7I!Y>jFB>Qn(LwT3UZ4dUQ__XdxlK&D?tA7<nL z+s_y|U4Nm3Xfd+f(eSBfL8<2JmB1;mdQG~BU~jaX8J1WlV$03F?5)!squm4dGyz09 zdL8HccUM~FcE31aX?=!58;BrA<TQy?%C_Z^H(jPGlQ4|<&e}h`aEh0vvO7l(K6VOQ z7vUIX$wgWnkblJ6PAH^d{Jpi(Ad7qKRL}~T;*ezh>eO_S)gY5-wtcv7s^3xl0|BL? zczu32@EZ_L>H8PEA{A8($0nO^0pTm(O75a3tYToZHxGcCDSO$9)8Vh2)`fl@1LIvP zZ?phuX&f_mk~J#hiTN-}X%b?*e4b5DzT_vzc|f(o_XittWFwPkx$7@b)B`Bwc!P^; z>o;rMYrJ@rU6dYvB2Kv3%#~aX?p~@c@oHw}-;=dBx>(%<*^;w5$dm1!guq^HO{;^% zza|}R3#faMCTgo)E}{>3#|KFkD`n$bv9E0F+>^mQ+Pd%*r)jurP1%%{QDGM+3|G~} zHxY{uvs*?99!OG}z}6<$p?GDjWqbC;zvM>`?<298NIJaWScN@;Je)Ng1B+Ah$}E-c z-_Mz;HNvIJbHo1HO24L_OA~16c>?3A_S#Jpf4$+NsEh<wq?y3l*&YgHY{}V@Lg8Ua z#(6L3)p;#91u}UnZ9hGbp3BOlr6>xIHJF!v<W%(6ZB&%9u-F=+5HC>td>Ys?r*T=7 z`E-V(3fD)(9|d7v@#aQxYS#~yl%vdp$vT`dQ1L!&kz^<|;z5<sW_H1vqHqm-wqx$T zb0u;P47}3CDfCbcX$6P^W?a>$l1i-JO+}c@!X0IRPJ)9(e%s?UwB!8RD`Uau7rtr) zEoNP0tYzfw&Oox#?^g#>A70AnMzZm~(iunTkk{(l&cx*r8mqZ}Bh+-nz`Em8a%8cy z{X9<wgW&^O{tchw`^djC5}ah;M=;%w-Pvs$p?`|G4eraOy}kDC4}STY{&kzt_&^hO zwIIKZMK<=fcgQy(GB;QAOF%xs(*I`cRC>{Sgpx<VlIX9z7vOsFe(-Rc9m*iOxLTXf zv_GtYWqS)=dulR$LEaR#g=gBVFW6pPnr<R5!sdPpqqal!o>zKJy=cW5#;kT<MpBBF z<raM|eupqf?m{fgLWa&k>5=6ep$v|c&;9-*L~GX&Z9)$ZsgAVix?sonk>kAV_l`;X z!bBCwj6MNJAS=;EE}o4Fs;_Nvh?C@oB$)5t0S#9S8m!NWdV#wlWZql`pc2x}?BgK= zj*(1zG3a3l*5)2+XPW~W!AA~<Gh9}LwlW)-S}4m{8S%Ll-wv3X*k<)+8Iw3dP-gRv zHqPL1BhvbK;8OR|@0^DB(>MggfY4dT><X9JSv$-2Lv%y;)APsYP*n2w&Xmu+xHIsg z4f4D5iRhbj%BNB5!Iq}t{za)o7?%0BAZqFu4*CU=N4JO?)|mWkomW#Om~`@dl?nw| z-uDnR`C$`Dsuc(dXrNuCl>1qceTK1jNtm_}pZac5!3t-C8OMV8brD1D7IwmNkja2@ zG9@@^8z`&lG^8S)Ut!}zwmSK~w?Qn#^~dziS~|H*sn+63vO>8KdTEQ~kG}0ml-pcB z>bRpJyKUNVG6@ZWH(KVTs{EF<-XC3ZFidX<ib>Q^23xLt^+DRWbAOGwb+T^fERGj# z>aLzafG_S)kbmz7>~}sui|{r!DT~!6sGNu&gckV^azg9uYckMvl0t1dx2cfRjnp-I zCaX1R7wR`7-@giLj9If~MSRv~w^tE;#Y{OeC*n0<4l*8eHNz|LoY_+V<jbGm+BSs$ zkvB}7SYit};)#yh=+~s~UpIAON+;mMbuijgj?fn}$9un`YS!(++CQ|ohFMVJW4=Co z@}CA5TfYvC_$<<}ua1dKjDm5Y@9Vf%L_X*&o`d5vFO&e^gS9CXTkCKN)2dOCd#~ow z%3)oK4PXrBFI%xlK}8%ll3O%${NZG1&l&qY3|gto++i$xLN~X-Tk!epn|Q!^fx~Q? zSw6d~l;3IaIB6*?ZS|qk_bi6A<{)?@M9g-ga-PiI+x#a$b%xyyM7rjZeYi@Ne+6{j zp`pkz?P_@H$pOE^-Nyh_(+sK{w!No9sWnZ9XNiX$MvG~d6sG?*@{0faf&crh*Z=$b z`+l#di4fWC+_IW0W91z}^~p~}*WzIrpI2n!8c9J1Xpp{Q$>{l$rN@wx<7ijWMv2@6 zPeaTZhs6DFH;!H625P^*W@{lDcO2d;5zGRx;_DJoq@n(Y0Yg|o-Oki!%=`Byb4_K^ z@EzBM$8i5e_Lg&LnKqSbYLi*1%^;@PpVso+&kvB)5gXEJY}IjZaG^#xgAPL`AZJ`( z=i(&R$=F82!{t`i6NH#j<hPWUZ{Fvp=VUp~)ZDdAy&OKLvIjbO?&-`S90?Htr#A3T zpX+3NpXMRIT0wc+)2jDU)Te}r+G`gMdVql1JVf?XXd|OPgit}aSDsSLMaKkLz4A4q zys?Q=`B9_(rz`w!@hyugvu*W+p7~65T^jFP^|X{H+#j~7YO5=PZTt?tFH4IhJ$M}d zN3YZc#<nHYywcY~N);$JTIZAx-3lCdY}?l;nn0j%MgynV#-bf*DF{u@P@=-(%G05+ zwP1x3fl!?`2Ffg*lP*(;Y^ZBE6u~ve)?J%$Noj29SU|xI?+_?Ohfufryb_q*D%i~o z-!}xLPpwTbixWKl^0H(LNUN7IgH*eaj1ZA682N8a@kU=(<=z-ay~?j5L|TnX#-^|l z=FR|INeNn&_1+KIhE122Nb0PxD%c5mmpcNxr?2OQf3NPK0a4>k#xh_h`~=?tiF43q zEA#!xG*9^cfm2O}G<-ciJaj87JIhDIQ$%FT(Gcl*QQ#y<qEma6d0N!9iHpWjB5OAq zQx+4<&Jt}SC^%3i_g2uN|E>cO1(AS812CM8jLiFOa$x@~eG1~Y7mi*XV;+h%liffD z>_xOXue{4ALt5~qH?l3@<QOcftvyAGwL(98^Wp92`1>Hb<9z6tSpJMiuvT-*XLg$p zt{Qxejt(ARh_7PaOOqp|DCmLC;OwGjQ9UKUW)ocAI-_t`57l>bHO=c$6xukHv|;RW znKP{#F1qYJc(mP33C)1_51~XXfA`w(&t$sJDHpgR8cW|d1O<=p_q*1Mt@N(0&Z(`g zdx~pe%2X{xb*>Xt@d@q?BKY<mYZ+VdnCY_SG9&+JR2A$1R}kxz-5O3DGUtvVDu|j@ z69R@dgICU>SWd9TQL2mqOjnvI#(Jik45;Eu_rN69V_j3%s}a6ALJMcgXcb{&W^tT> z0xBY%?|UDg6ED9R8G+6>Z=iDZbCf9kKhPwd4rOO4B0U;b9k{J6#7!_GlX&7F*<xm$ zPj2}p-%7&`CS~awav%s0&()^dVUVGjyMq#V9({i#s;Y5Kl|-X%dW*0`tPNvcc-<h0 zb1Du+V6Zlp*KAsH)i(va2zh~;@(#D|wk}@eQTfkmH%LacI&-$GdHzTZL>L0*EN9Lj zC}0%#-d&EH?@`C8!pzd8rF5<eEgXW&vYaf^e*dPShXrqvM+;879<ePGMz7)+JqTB* zBQnO!@agQH)duc6_C2<!hFzBWZF6I<aOX-Skt5}#<#syC`_G}vg%`BI&~J#HY-o`- zc=}+B<9UUa@bJd4yxMT7^lfF3b)6$My#PaTpod<no;&x%BIq!%(wpCF+BTuE&_BX7 zH`Ae&dBf#6n2ggzQi!slgBp(ZGYpwQq~<%}{k})mf5BRcY26;27z;z!Q5)9$>v<#^ zNFiv}#R&3&Wjz$K!TeH9Z&N~<CsZ*`Eq}N2$oGw+(3@|q1|BU4e}A+kgAH*Ak?h#> ze)!pax8>ono|hQWnhl$>e&i<5y-Y?BJjyjG*Bh8nd<(~DjqQ?2nh(kHKEJ<{%p7dK z<qW5x7oWcaKM3(2h{@Rn;OvD|g;O@|5JNIsYhDsJfU>5YV+0pFZs2Fi4r)Y^iF)#E zVys(zQm&w%bB}dx!CIjQlF|oOhxA)_e{j<Tc-tyCe>+g2ep3M$dL+%cunpmvIP0;I z{15S<gXLQdq@ZVt9ZZSr6Ixz8fj)wug%mjV6{y&YJ!8Bb0hCU9<>7q!KmbB?I#GEN zCOofUKVEj#J<7_Z@fcWnxwGh+G&~-@sN;H~9&nfrFZobvJllj3oJQ@}QzS#z5hXLX z{IvoQOZz-sN?@{OywXc-4qUut3#~&mIpq3yR6;b^Yxw)d#s51C@OiO?v6aydlY=>| z`MA@NS)bRcrfEBz-T3arF*#t69j@UnWVqNvV8rS1vm!?#;X1T^Z<HeJ2$7ZC7qFD( z$QcR_O_G2SCdhik=UE9OvEFH)^zsP2JMG{8O_3ZlT56fz`DZuV<o+!4*+UEzHDuEQ z72@0D<KgK;T@mqNvM<w|@oiFWf>=a~UrG{TkFpv<kl{e@Y`IA0rqJ?!9lBcdL8+N+ zc_|vXaHT>A2b??CLvNirBj_?%%<XRnIawahdZ87$mmMGf9Xu$dVJxl?{nqVCSjaDI zXwm~ApJat}nk;0)zoCB$eYo_Tc|nPs+)6Fun`X+MCC7!dujMO8hntO)W&6?sFAkY7 zTe@;NMHvupNrXB+o9qk6;ddJ^6NO4kl1s;5w;PbWRls3kz6fxOh8SMB-0+3CGY7|u z_mWy&K)_=0N=nzWZi;er!A#q-2c$kWkuhS8R0z*JKVEoYZbbz-5?kHd*?vK;{Vr80 z_B@Gpc&b5^k&n>lvN`9W2m@}ht7uYLp3f0{zOjpV6HqD)*uj<qN99)IyH^9WxKlSJ zsJ&pSQAJb<&QOWt1e|${2t;&;u7yAPEEHaKL>9mbwf_QK02_nk#Hb`@;h6IA64Z6$ z2Qg4JwKWs%`41Pu!^EsK9MxlWaZ)$fa9}WOtQncfC3(!9K{ZY$B0A(55pd7*w`<_I z5rtQ!Z77CDxp=A{sBr?lLB9`5N&yfA!w8aeM23g|VX~(H7X2blG<n%CWR{@ZX<+UZ ziduE}t5=yaC+FpT-9P>|bBF9zpv>SciU(`j0j9G3T&llJrwX0lYfc`P{*{kVJS7yJ zoLQ>L?T@0O#iI78^feF!M#WbP55lXNMPDWfk^e(cYn3@G&-xp{m(2saPqa3#YB~1W zR)9CzOdYC5afjJvj1?XAdGyqIQ1qHYNl2Wuv^#jGydsQfR>{%=s^uA2j*<qU4>N(a zIvv!4Kd0RTGvr6eQKI~XJ?gd21YIcZ(y4@87U#CYW#(UlYzxyYH?}1V6t7Td)XNZJ zqZE~Sqgak;<L2C4Ddu|$CtT2BTI>^B{J3h0z?q%Hi(DTC@Lg~Y<bzm1uhRHT1bYo4 z^qDWHVRM<~1z&|lybS)(dME1%L9bLw{Z)T45LioA;|PJWb%G1pxyWeSWS)kB?2uE7 zAEmQQhs>jcN%zzn3qn)p6_bOg4vsMD?E~NlF6eyGYqLH}3tK0eGH-;~nbM##YWVj0 z_+B|Et2^oK_&2zeXbYfwB0cX%;edL8=Wk<S*5ZMwr;`8+<?nZ8_7_3<F!8~}X`+x- zYDm2HDN`S{*wAc#C}+_YUrD+&Cpw>1eiTF%fG%vBEk`OJzN3c#40DD>RF{CeTxTF8 zJclTshE%pKN?Gmw>G0$G8gg}mEQ?nImkq~rc5FQdA_z;Ne1yd$=GS&p=M!uTz|2LD z2`pGogza?tsN4;3RDWNlBf%qQ^kteJ-`O3Y0sse_Hdp0RNepABTBc$O^vXCa*h9cn zNeUF02u>)E*UuqcwtqIo{d&N)evAG&>eWjR`E5=w$8fVHoM?;v%Qio`R-}DNhhJlx zNM2}sxYG2g4p;S-Wt0{Tc5!*KzybsXnndwhBNU|Ln!`(oxHw0pegnW6UUi0HouxsV z5(c22p%t4YZi}qxC>BO;HK&yqxk<9osDa=)NQxTyyYKTpa-gqg&#%t3f}YT5*ng(B zX2By6wc4CFxnBJ?2`)FMiQPP!+rG+D^8to6N7=o{7Uxxf8okwnl(l-X)vA+W3wZ7< zh+?`T;F`DnM<We8)oAbxhRWk94poku_=lQ)$}r1;4EHb~7-`6AB>1$UQs<<Kwl5vD zAg*~lCabWpxEWUY!gn?X>{iAj^U2B=rF&`YS_=(^Ju8B({)mTGTu~nJ3M;Yeh9$Uz zllK*(s4aw4MVG||DwgGhK^S{iOJ#9*(*U#rj!3=ikoq7EJFhHYEl>7jz(HYNEA^~H z&laeM5K>p&NQsGJDV1P7H-wPDv@}Y=bMg0Px=2uq13#1#ruU%Da6;GaD+VR#vcq4A zbqrM`c1>gCN|dL_S_R>>j5PN-W-ZUqHev9f*5UfVv&MNJYL&+U)8seOFdm>Pb0O6| zbDBO3&^`anLZ3m-Os?(qwmp9IPx`AwUbRLYf=s{uaFX>|Sz@AA7<nU?v+YBsMo0qP z>gLL2$AB^|4IF_zfTEOP4d2s;8iRv{`eL>~0T{0GdzO0&pr!#`z11a^>@E=i&b!wg zH)2Pr9fD-0DV6FzQl~d_6AV<MfWpubnMt=c^!E}a+w3Vzi~1>ssHQ-kmXlZf53pu4 zPcc_$Ng0Gn$T>w5n!-zleu1C9B8VT5Jp!%f7yWlEZV^xlHBU~MQMGoa;D-exXJgT# z+erY(oOfCG2eaMWt;TaDKW?wz&mWPES~XCSj#5V}JG65gKEM-Ks`V=`gW4kYax(C{ z*z^^VRs6&#V?qdgK%j5T_qsilm&O~0^Vq*^r>ruGONPMhbKNgt_pWK9?n$zwGHDyK z+ihav5i`OeDXtaE^PYEJ#9orN1@&@Wk1;(o-NUC8(k%{-iE)fpN9}JpCHxX-Hz!Ll zO&pIYnM@wJ;zDQ72}!klp=y+%+xH!Vf>dZkMsC2z4f<{lRDn`noJ@(ORJcO)FWL1j za-CMc%Cn#SLceJXQ0g?8G5MVRKh=WRP?D)MJ0aWdaXrP_yI5M2Jk3KzqhZ^!vO*u7 zP5KF`T%W5Tb^6)6AQG^qXkMF}LH`gAaw=ZS1^;=kO=N#X+NV3$o~&T@=u9L*x2F`K z5+D#cC7qN~Q8^ScXp4!ZcWD{ICRY&m^d`>z7PSB6>2>v1a^o7R<-KV$F9E>q#ZTHH zrD-yCdZhBJ?|^$c<p~C@+{`gpxe4^@#7DdhA7k-T>rl9&`jF4uvm2dQnXp{B;ZN{3 z+bma1E$Jv;Yd&M;SAQkhBu%8OUCmolMKo+2%bDkq1T|veCKjLQXuBg%nit;NK3mDE za^e8DI-^WVju+x(SV_UZh_N)tbaL`#bDk_tda2PtSk7QVf4d}Bs*w8GUjyzUfn4$t zX>8=YJ6-7(&c0u?oqP%mOVMdLkgTFlc6Il~Yvg@njRLetOM;5&i7`tFE5rL!D#xaf zu?+)V+e95E3kZ>vK?i6aZ6E<sqpgR>(&|TmQ+v-`5Ivn<o!d|<vj3eqoj=`QWZKq4 zP(q5wi?u&!jE6v=Gls<8tJWv1*~!uCGf-Bvf9(cV;gtH3nr@dSN1O0>2@)!*(paN% zlhU7?Co=Ahi?tXK$?pNq)foA;pmvInO(R5tv_e#il1?;vHe_gaOn|nt5Y3)-*gM=K zQ5$6kM>@ef(2CAO3!6)uG6BeG)=k~c8VE(-Wl%$RWXtCdGQt#O^TXpN@pmFrRk>@s zp5Jq8S1Hj#IgIQ&CkX;Y-lC2OB6C`NWQYfdi!u-ecu%g}_T)n&1UqIx+c}8DD~FdW z4tRm8&WB^3JagTXlhwSKXIprZ)qyP|BLN*TJoxp|FQ<_PAf9vE%#sgDU5<8S2^sZi z4aYG|F@=Aa#bZke#PTaS3^5-U$-Po<k*0Fs(R<d4786RP;%TOc8wC$N_zoYAfL72$ z+9ShREXARB7mzs>;HpNzmpYyHK}3*QkZNq$*N_+AT-*mGdC8h%5UfYZ5b9TC33Pb} z2*XqA?>WsMB;5B*icV44j3b@3`kuPuBizbOz9*WYUD$QG_hi$~+{~zp%hP*x@SS*j z$6chZe2!NWM{^5~u(zO%aRkV~xT}oR@F!+5@7KW#uV!paR}O$e2r0VQ3wH^_c<>gQ z@NRVmiN-m<OP3R>bbk93=e<a;V10|ofWbKh{QD-9{^eZmK>q|0te=|T1hU?^QT}pt zUw*NHP=$xW55%!LkE@dZzG?-fur(bFyR3_!mw<4qeya^C0GCw;dxqD}-uN~f@K;Wm zb{w}z4>obcrZwXb>(gauR(n75zFWj-I~PEdrW1_{KD+H63Y+hOr$EO@jQN&{dqN6x zdmZA?yqkutgDV#UL@v1;MCNh9hraFbC^IYKxsHqzd4$qCd{mLq>F(!zNkD23*}@>0 z)|%ok!_dK9GECb18br1w`*qp|L-=FG!F<wCBMr@@LWK(vPVM#(km=pDkap2(pDwQF zZR%1a$Jw%X?EN=}<Qu3Y6*7gXr284@B{H>AK=mZneMU!oT5}SyNRAB>z#fgv<g~az zBPkX(PUGyQ<$At_JdZc}2qtRwx=ev7m_#aLi;L>usbe)=(ov5<8=P|o*Ag$Ar$-;( z&G&8HVb$|;yc+^?A;iKe&d9IEj(LsuynBwO^i&z{XLh9bjbDQ?tB2{vdkdFXg9-|H z8uH+yEQ>~F?%EYrh}f>gm>F*4|1aCoVR;sH@s2bXxpd1hPM*Cr6BNHtt^CWi7VLHF zVx(?_@#-L?Dw=D@kDxj5R!MpFl1v`*b~ZzwU%@nvb;0_?J(0+B64INC7eg_+ScOW7 zMo?Cq73a5Cd7&b?Q(1$B@1EtFjTHK2kW4Aw6TLR6ck3PUHY`m6-QB(m~?;QL|W z=x#+7k(iek1zVs2I#4l~x<fr9z!91`LRKU9V#_@<F{HK2T5e_>^xfmxZ};3tj3S(b zmnV=>b3;Z23f<RQTu+4f?E}I17?cDOtr6_xu(77_{^eh3=A0kxBNf4b-`&cDx6Vvm z&}|gFj)~my>WCT&FD$!pSl+Gr7By*iSf@gUJW~A9Qh@;g6=bQPj$J4bk-I+#N7pZ8 z8a}xOAmyt{6zaEb{Kr{9N|9wRJExDe1rf1^6F_0FAdR>4fLp56ye>jXLgk2W@Spid zs@F-#7;&OhgNcc1R^jwHN65{F!Eu+i>@lWpO9%@8`5&<i-7MEPm$-!9JZGYkVLUXH z^^7|b?tsy~ptEM4Xq|!jUCzBjIYIZDK9G#FhqGP2QQeA7NA@$M2E>Z|*Tl($x9&(t zf<-JiZHe{teiTrj%<b&XReN+8wjau}pKH>pPl1uLo6P-%5~>A(s8}vIFE*8mfFOGZ zJVONE6Q0!=jaP6*vvLA5XKR7nKH#iGr4Sn{QDUrb0&_l+S3fV#mQA1T#zPm(a9aYi zvCZwOOf7-jq>jzgMsA32g(rJ72A7+$f;~W#UxyHyVuPg%M&nmSiec1winz!TaDtn9 zw3)x%v_dbMIZcSf)AO{YG8WB|Feu4jmWexPa=uvlh`fJ$*TvI6^Hjxo^|?8r1-FM2 z#HkrWw^WKOx1=ji*J4h?N4hv~LMZ3`WgN&bI3~@vP0uQ$Zwr!wQ{6UayseT$ayP&J z-%u$go4yCIpc)J{Iz2m}l5E6-I+joMCWFGJ$4@1JZx(kFFt_hPw>wtni;V{Sm6XG8 z;dv0~`MR%>#!lfKU)uAI3N5E04y7S)vDM~n<rt$-`NrX$r9Kr@j8f?hz1fUv03|P& zM)-vs_naU=76}wGMH$jT-EgQ)+7{dE_@0xl{-*)VZRZNX_Yp(caUkaV6cUXP9k;Ls z?(o4ugvk{MC;8hV8biBux5+qXO<zFSw&em9qTW&Cr}mgoMQmUFqKb8;2$K!LR6XCc zB@h`SwYj^H)jRtNWilliqAnV8=&E3~lSj8t-_r+$A*Qr(H%NAy@E%qb&m@syk!Xkt z<~nF$fqG&sG(p_%K7_gFHsVwBJpm%|QQYIIZV>iwJ@XxUz{6!G2BP>(Al|9eBy*Tx z&&zg_P^YlP0kQRYMdF)<!$8o8c7QwRUR@UvjT4&^FBCbWL)6+EvTe2EKJ{ZLT>fW} z9W?s{?kDNgV`13!MlTU0w)TFYM=b?f7X({1aWxc4J9Z?)2iL$R)F2_gsfpZC0gduE z!sa25;u#^S)c1w|jS<i`MhNygfOT7x(cC0d?+RthQB{`ax#Boo8lF;R5wU9tXBL@& zh2T$5dqR$>Nz|ZI5JWHxc*OpPPnN9?IeE%0N~9%jEs&feFoY>lR3ppzj07yjyI4jg z%B^3I9^bXsKhYe>BGDrQ0OE+I0+AujLB#3h<F8J9H5*QmfrN<DFxl~K5Juv;%HaZ` zyOudcF4npMcm}%=jbtrr`SRnIV9~q>SOS65c&VCm=FYI7GqIxBMntP9aMimL_dkFe zD53irWLZHb_|YDmK~x-w;xhrR&v${2r)DuFP3L8MGDwQQ$gss~N(tRm5jNOmL`T;= zi*NHOB8dCLNz5022?KeCAm<sE7az$9o4gyQ9-Ek^o@c$&?&8pQc?H|%DFp5|lWUtx ziOX=r#j_4KM+H7epwoy?IA4I&I)|6vJ9$YqRDs*US^o|I61;vjid%i`U3~K~zU3jy znOSQEVz)&{xK8s0XuwcvwD_8~Qafj=K?;ky*T4DDr^oly2GyrEMzqbsZMy_Fdws!~ z!ag%nbuOf}c`7!=1|N;nd4p#Z=b=rpWMA|qSTFyHR)du837XCr=$-5%#A`j>rJivF zW;6y>%-Tjv64Nsh-ySGr<GQ||I6A01;dicPf9A1<OP6kz0$@TZNdp}<JJL<;;|W3F zl3|fJ8{8lNwNANW8tq(r{3!lnn-v1%f+OZ7lYH9&7kG<1uR>7)B>H^>*<B$1TZEhA zvIHvf)adC1^KciUWHE&+bU_X4<Zpr0ZK0%?L0OAy{GHBt5Fi0~J|R^3tMK6*x2t=2 zcY$s~6jY&{gffqaiQGSxcR4$J3l$$Gc$M@8F^gjTrBuBaAXr*PP|DGBhlIZS0=B2` z$s8yO;;OVk5$4@#GgIHtFh$>_S6)4G8JZCSe=t@&XE!S?mXS=RD3ba{ijH|B5S?W9 zmqM1&DmHffK87OrtMu4Pi?%cB6d|z-mQAz<20YacCxr5EBqED!e)zPJG<Z#LIZ2r% z3nY-@NB8z0>l)Rk6#SzEzMFyf4uX^1KUG#sT2;@!-NK^K$rL6&!P(c%nec0<YxHkZ zgQj-$eAd7TQJcmP<~xzB>N{E<{1QD^FtwS|PoXJ2%rz7FWWej<xiH1Q`@`E!9ytZK za@Q3vr}N0y#}HT%tt;Q|NwF*l6kgrsTN3Ex<U3Vfw)s+?L3Z-MUg-oejX8?X5RVKa zb8zqF+%}Y6YL5}#H#6DiZ(BnZbPAA2(q*3*gHD6e9ugiSp|jEBn+^#Zc#Fm1tn;Pd znQy<coy~xJd1gx*orS1JiPLEZSJf?^zi9g0AqNoDqisb+&|}}MeYKc39|nSX^%$11 zZ+i~FFdQ_7M*<V5SH2-T&@YPv-sx-tDPm*EX-G6p`Id3yu#C6Bj#jb0sWCtI)^YQ( za#qAR-31*9=vM3S>D;(M3(#^o7Cwdgw`4m<Gp1{g(BiwE#Vf9FXECP)|0<CPj|;9E zkrzxjVvgoXyq!;k*Tv{qDV)jDXsCr90*^`L2mqUgDWs{G5e;-s4~MK)Et56)d-kI5 za)TNgB{&CaKgk2ztmBhgDzpT9ZH@KY0XNc1ce5Y^)G}OVD6$@ok?wzC54MZ+dt+vq zmD05PRI`N(=$L;5L(;)4ZMEc6-qd8MmtM`GZY0L~Wlvt@@#@?@ADdb?TpOKna&KNb zeFU?VfuA!G=_z0#xLxBWYC_T`cNM^<FDUkDdZ(9QCmOy+&KYmmKllG)FR|kO1Z6OH z2*BMQy=Q_#wl?ecq2&pvtM~i5|5=(nu}^1HE9S`d?l9?+$YS_xOBY3|D(e`0<H6Sx zRxgzh<%Y6(SIM$=OD2g-GSi}lLGQeFYg3m?M8}*=;#<U8=rjGUQUi+RI^$$v6+ni@ z$-CCq=hu0B-iOeky9E|VxA&YI>fC&Ej~r$hV!_5b(BST)1<w|}LKYLa(l7nv!ZtPB zFiXU(z%5@qAo2G)I+v)kyb-AkQxNR!We|Zd8oYuvt<=$hX8LoUzt;+Q6U-34&W)nB z<;6iJ*&(EQ%o{OH#>?Itkzlnx16rj<_XZmqqPCJ2SDco3`~~9QcIC8^jZdUr9a$M; zI!r<a*|o=bTD=|tsJX&!ZWyXa`(D9FfwRl)M<bMj3?x)f{zuuc99_wY*a(+!X8?Tu zj#|LLw?&6OqX0BIZ-;5rp@VfQ8n_JV5L-@d8c^%D6itZsxC0`m(DpBJChzf)!Q_vU z1#WNb?j491?Z$t~r6$QI7sP<pm^h+zu&np%T9x|d691V!60)>d14$?hp>DCKhD~YX zYKrzjba$-yU86j3F_PJOw5nM2a@{Ljyx>S4VQ?k%L>$9AXtEp~@a6JftqDIj=*Iu! zFHUDC(GCs)x~$*TrHFlB$H~X$ntK=SyU`!bKP2h4FP*xh^<;DjGvT)fiK=Ue3oO)) z=1m(0YRtfRBZbsEy5QH++IB8dexzA~Oknk$BKE+W=1tM2zHbg!8(Ha%-%f)*sXSYJ z%cX3tobXFM@tM+z&X@R_q@&#MjSxVNyCza{m?mzro?$K0mmC}&8HJ_MCSQw@V}vQs z>mSYTbtdh&6fwcdnHiHk2^2}(O0x#X)S>JUK_bsXL(IGXw6D(O#+YsGOUC6`?8KQv zD!pESitqfH!4ck2V%+!K65+<a*@7?{hs2fOR!w=iXyxH#LYNQ&PRANrnXg0oe(?d8 zu}8sZnDyWJ#zI6}XqTR!F8uX)K=AcGHa4)v*8;jRnQ0~Jxc8;JQ+Hh67WTJ?GqhaL zkYDMV<fq_tWs*1O<sz9%p3Y`>1VxXDlJFgJ&er|&_CO*4D#j{WB;M~-7WRT7fsG1T z*9TQQ{GA@)n_xuE_%5>JP7Ey!qrY3maM-LoC==F_lXDMm%e0QKS0R7T=<F}r@ppjx z=zC1l39tSce#9Ct;NkGh-3K*``v_r1`K6{JM_y@7=cgLXB}Y;fdQ8vP>jCa_FV4g{ za~5I_W9f`147_wW-DBWJ0}ef`(@W(e#ZsS|rHFBEV=zlahof?sCT@^!;Q<sulb;48 z5q22k=+J#4r_J&Zs7SFz#YxI8Bjl=N_iut<w;S%`X+|;+M1(8FNf~0aHhA+2JIbL( zE{W%wuv&^>oZCx`NjU`Utqy9I!75+EgQ0m#eJG%{aRUo*+m{qtvB+W0--isgxya(8 zblOGWhiS<eBc-Y`g8X^iiZK)Li6wO9Ekt``WX!x0dZ-;SUsmG5(WXVvoUo`E-DX^^ zlSQ-k#PdN`hK6KJ%}0n|)j0Yuw~6Xnp4U{K5F{g1&nF%6bBM>gc5YuPzxm=bFwe)= z7185CN!?u8RQ=LSt7+$a-%8NJV-$+}ItX|uiOnsfQ2IcGj|m^>?M^SE_SZ09)cLPk z&flTwJ0!NC1;FM$Y_mzK@R*S>@ACb8LGArw&{ZIwgd@6j-_Hlp=6w&Xg)Z8~&8VY9 zgGmG%^DWW1U-JEol_)45z)lz5%up=_YCFy0kU@`3sx99G0{4-kOafk}OMMubHY#-2 z?{bVmyNv$-0)B2-Uq&}#ewlv3;@%gHSbaQ3|A7C&(7%GF>f=^Hw>fT9ErXqHfnzo& zQQ@yss$C8B%vKM7Qj!JB1w*~5W`AJ<7S=(wXmZ}vGF4r*&K2-~PC*BVz%T8XL>SE4 z1!yavDA!vrvHra%BZMq~86v#9?uZiARevUA8)ovfDBM0%g>ind{Y>aXi=*L&9mK~T zd?p;y_wjEsB_$0KJ-sI1!2?1(IP6Tr!PdkSM>!ld4$stegu)~$JAi^5P%Usp8~Acx z;{OXjOvmWNpj(IC3=TSAS{A=AwID|<1ntET796IWBjtF5QlXp$<gVH1z6%RFd(^Qh zDh;XIG@ZzcK^`ykU8^~yH#U<SL}=4hMh?(+IqkIZ8b6So1o~H)#9s+1@oPXvF0DiN zxe$H=M{(7{y^AOxC~w+IoLM^F{1S3g-*ucOzsoG(FwPHPCnZ%JUUEuaP*x~e{5ZG- zw)f|NIYjIK08Bu$zul0=N7|A}dqyqYD%?PZdk31X)o<Z}AwrEoUMd5~z(WLpBv2x6 zdOtf1IjEzE<mkockig^ZZWx`O5eDR5xIvP$63Wb>Ducn-JPkp(CpXwe;4VQ9^N?dS zGyd@GHYObu%d1d=?!pLb$TO+7z7OgF6XFrEiixGE!=mYTNytKs!kklf1qU;{Tr6G* zn}QS5Q!k9O@`nYS&#SKD%fEkeaS(X^R#$*x)l~RFm6lZIECb8`Ne{C2$qXsvnD1yY z=+=#ml}Wkx$bU+cQfTy_9gYpr?Yj{dPT&9)PvXBPl|n_#4*<1$GA!~kG0?kU>yOI| z!iOyg<$0yx41j4r2x_Ar^yS+f>h4D_FjPAv+>$9q-3OQnhKIm5UJ5AKNAxlQ5p$0< zcgiqGt~cilOopy?wK;7J5%XhNjF|_-itc}Wd3izj#iuRtG+}|mUU*$?9sXDeShr?p zWJYBa-ePvpbl<P;VsRnX(C^6(5Rjq-@496JRVy@(ZccFm(YbCQU>fjjp?+Y9*cUUz zV|Hq%*uedPCm;x4SL_h>0MWZX=*xL8gxey-0B=<TfNSF5QdIK}F@jp6!Unkrg`DJ3 z+l_|k{wVnd0H_yam*^QjKsbd-11b+OG>@P`gyq(DHCyCvQ1TB78lQ$B<b}ap9X4SE zZV1%}(HTPlKUP4*&0(KtlCECP8qqN57BNOk$6;?qUNr5xOEOUa(Z4tc0fx-j>3)ND zelXpXiPM_CK_a0t21NZ@OX1`3+Y^`&rh0DKu>+rvJcq3qUSwy-Y<?mE5zXYHfxK|_ zhOjS8YwdpbO)i>{^(GAgkLn3H)(+ZskOCPFs{jBX07*naRHs<MF?@$=24=cRkD{<H z)i1;i#GhCt7;_6f`SODB+3^`d5BQEXzOn1EB+!xOfQ06cnD{PF7V5zRT<FVkTHHa^ zyH_iES{MQfLHw;riDr=ZZ?`ZfNfOpGQ~)==)JQZ&B53N=jecab+NA~>O`!P4tsc9) zAbfcKmJufqu#=~7{??u^MwO%+BfMh-6+601^i^Rk=mcoYr|5v41@b3!v5k>+Sukja z@QegNHV8&~!x72tO%tDto<a}r3cXQ<j7YVxuk2WjYY4($fBU0%-v0CIhsW)ezxl`f z`<3te|Jy6SU_a}d*G9@XI*EwJBhe9V^rb+Z8%MLaHQC0bV^E=yn4a<3z}bQ1a}N_* zTM+s9jSz|S2#6yTr^Xg9_R4VqLHP5({NaP|_4e^UzPT674K^16(#Nz_r2ZP4RFw#% zs)QJT97Q#pI^az+s1p)^vfdVDA<c1ew*l@vham_LY!Ln3r<6Tdw!FnAwf7(ISrC5v zJ%|5;4?g<n<9}_27m=>t4VE`rO;U0c)JS^@YQG*4$(%}8UTA8Wr*HtCjXc5sa(4wg zl3YdbOZr%P87c2bz9Rd8I9akda6oWC7Mv_3E=YKmi{S(ZBU$jpbiIs-e6#c#ucvl; zx=Y&G-kzT7>i04uBO<eAg#aEvt5$Fr-9!E1(Ce|aIN+5-wQ=&_s|v!$(^Zb2g@`nJ zi_x~8edSneM0Jq%#*CViBn$yalnvgddDKuS2@8s2$cNarb~iQ>&biAN<~c+vRN_N} zbX&B$8FaAE|Gt_a{PLUAm5zr^WC_uuTyr&Gso66L1^em*i98OgD^W&TLKQNH%ma|k zQu@aZTNAnlWIhWP0F^S*zXh0dOGH>zL%zT?y{;q(|MKQ^wd29cf^7r2gC!MV3qcIu zSvSQ8NFU&8P~Jpi^C1HvA*)aRz9}Bm5|z;xDgo#qER+YeB(Gg3YzB0t4&j&Ip00Sj zd8m>f+QTV~CV*mzjk!oz@IKm_SH3MBU=qX{%Q|Kr0Bi5dPS39E9GAAr!3%&#mX-#J z1)@=6fwwCN!oPiXy6*8ne+(Y=Jnz5uTJ7Z3iZK%mm;z|~<w9Fjjza<fumbJ5h$t&Z z+;nFpB7}2xBw9H3G?N{YHbFRHMa>v5J2LwD-%nRR9+Wz0I4lD_x>m|D+1O^0*MkD( z=4Ti^DLAbH9M%iif@o!M-_#GSZVyCC%O0B71ZhOIju3VLWqodwz|!7$Q9=0f;_p2X zu*vKxDm@9{ER(T9Jo?e7np(xyR*(fl^qL>y1a?SaxEWk-4`>K%jq5;b!f1LpFP49# z6J;SavpK)PiwVNV)9W}MtmO%-o6+10%I>io!A)IwaM7@@>oy&ua$u;v4w(s*s-JJ- zc>v9%SkupMYxgxh<Bt_+8(F<%1%m<$tQjvN2(Ksp{x<?*T{KGSC=l>^N=s?YxZ?Bx zW)@C#Mkz|nMcodNfqQs?JoSm$Boa4N^kHL)*TbA11}KR4h26zM1)&!ozkndTrue%Y z0VRy1Q)2@$6LbPl{pX+yz|5IZQ?Hq?hy3I^$MXY#JG4{5DNrmrg9cP#J)FUn*aZwb z6wP3?h>@|x`cQ4pdDY+$_4x#EKe8aauK2qU0bqV9(JI*?jKosmX}FAyOJ6a884J+7 z3i-m=j%Wt)J>>pZsuO@FRE1+Nr23%O^V%k!RgLh?aENxfCtX~q&c_vmR~LU5F(B*# zalW$hfkmJSy-XK3RKXQq)1MntGVKT-py?-Vih7_c+d3Q~Tnk*NN(i80!hQ$JS!BE; z7Kf=$_wjzI%xe&TmoT7kR#kp1otO4^5It)JuH+ED5(db%NvIBS0?FRXpkV24GQu~l zE>`goln_K+X0-?CUF<~!ZDxh|qvVX^C_a)P{Kub8ukm=`8gHE^#LsTN+3?$+h4QkB zF9WDnd5}!thyW<0!BDDJGE)<vOO!5$$-cUjRR6=G&}z_12isz2SCRQRBcqR}S9(0G z#;<xR5Qk<+h)vZN)E<l~0a#L1)%X}EaGsaM4lBu<S3vuExd8Si$xbS4p!#p4+Q>1p zAJrfLHgz;6dK@kYUy=B`;0M%}_G-@vphhmeC##JWUe=2;v+g|@rV~fZ12D&j5CDzR zU<0t+>$>XmqX|E{&#jdogA(pw5=4ekgux#Yw&$0A`mg&He-|U5$7&?{hKlR(@Ryao z@~Y?qHRFJY;2{XWs$F|5aEHAd(SN2oMqr^K`yh}c9>pT)QBOOYs4?oFLNtHa^P7DA z&!69|_`ARaOUp3@mg-p;3OE8LH5drzml#@+aOp+FVLm{#1q}0#-5r=1uq}?XnL&}i zx>7<^RW<MXFqVM&rra%l(77Im3BuPW{w`sF=Z2rGrY#i>*q9;&VyXK@rUCPYaYz7= zZAf=S2V;mJpfy}$6(<x2>(tgCW`N8g$`Am-V{~g<1Fkq`Wb{>vzY7u21#uF<VuNV! zckTd&Xn&yj*fw?GqQP;*M9_XxNB~H6RJ$QYPL?7bIIIH6RZs+h-hr}v*zJ-;7*B>7 zSZ~@0J)%SSO2yx$D&PpvY7VA|&7!%_P^J$qIVjLT_AqhxfD*7K_fQNHY1a0h5h}(~ zzT_Zajn%=*Kv7WzMs-d<0CmUCkDe_EA5XV(JcMpIW9jT^AFo?Y1%SRBlaOGmBEct~ z-wT-KJBL`N^u?_FpoD7+QdFM|btz3E{#^pBFSaO-AScVS#I_0IdB0`!HH*Is5x|;> z(QXUaCa}MQZJ83VZu0hfX8eEv7(#Cbq!JzUcRR(qFR4g0bb+SefSj$`><XJiH@1Rm z0}F!eKl2Ph_`1d4MGWXwtArDT4pIQm4(zK}_K=dXN;K4~Ie-JyIce!aPC)TeMc$PE zd{|MD4vh)!!?u--3fD3j!V?BiPNF`KIu85SpC|}lz4*J-7trh!AhAE>`h`g~Mz`Dw zb7=~fWURnAMg~fXN{5KQ+Z|Tjsfd(^$vhtH6v5sF5u<_x$sPtZW>tc&IO2r(NrLbO z#NWm4@G=+xuim<v=jG#o7O>0#lrL8^<NFUB5CF26#N$RYJ;;WVYk3Yk@xuz+nrbP+ zFd)jp3-z@_)&Q%C*soj-^~63{5Z;FPyXXnZbP*W{8GbZP(S+IkcyJ<LuQ)~pv?Wc8 zvNo61JcgwzxgV)Qk9*Chkv-B>S<ZWio|p<Vtda9&S>GWFmdf}*L3k_T?|t(D0zgu& zeGSd@1^c=grvSQiLTW(dfL=g`3%Do4NbmM{QW;tmp;8<AwFPQw`bA^ecJ6LPgioZH z5Y1Fc{N=yzQv6-60{fO1ezqrA`QYBHnRfYTJD;9`V*mXJBZ%=N;YV$b&iU>PPk6}g zS7SjYJ*zk}Q;p~_XNa>ym^u+?i{u9g!kZF*7ps5@Ki2+#FVr-CCI)2ls(M-K@e#iy zs`#3lbR!AN&fs=>JV014&K3~x<|DCuG=Dg6baZnOkZiOSdOm)nAiOd0_ns;s4HlJ# z%all^gT4X-vGM`FGLKLJSm!z>ytw;2+@wmD@Y?JY&)Y@5OrdUQS=Z_ThDV}&*02PG zfn+}Z&yNs<pMQJ0_2WSfaH>z<K)dFfpK{i(^8A<punw3AuM8g|Y9fVFZz<ra(v-#p zQM$dhrGjC9dGW?<_TbO~IZk49m*D>j!dn!77kEHYEZJ|@zqph+Iwnl8mvlQM0OlLn z&>Mnw@V13aL3g?$Nafcf1qo6iV_vy*6IMA;@idGg%PLfC#oHeggf}YwE@1%7oX~7* z#l*SLPdRnAaz7{ll8`d98BOgxEt(>QN`92Po1-$>29PEihfFTTZ1QhTl<RYjswTR* z0G78uBnWR-{9V9+;ag|w^QZV%?S$!ch>0=dNT7}|f-)N1Id4FqSK+~vK(#)N9&TBK zSa@M>+23^YP=zfO=4paJErR#|Qkloo{TvU1c4WA+W5O;aS)LbZ&#<hR$oX`-{vPpP z#<cb19@WfkqtTMI`+HZuc6hKmu`qor0Ax!SbOl?wi513f6fvwT3N8x5+ZKNpt3Vn1 z24UjsT$L<+F#lGcpi(w#7ULKvaPqobHjWm4bj-E(gH3}Xct&$?vi4bVn#B$%1t>eR zEuo>F2%0tEJ%aGo#ot8?K(|#Bjj=LJQt2|cLvWm>*pBH1^fin{ycOLTg7A!t1>-68 z(^CY>T(}t^%@=}@JtWe)F)asq1?hWISUKLlBM9$6{9T$6>Mk0LzR4b%B7y6drp-7C z0cxMqN>DhSp>~c$N-nWdqM-1Bu%5AQXL_0>iE2U;-d(uT0-lzoWvMz(JA`*3{@y*_ zd_trwrk#6=?4LcS%WeXY8Lhu0phg}s55NMHP4>0^+}v#J$Kn?8i8Ry`q*O3eQo2#R zQ@wIAOM1@J<U%v8Zy9T`{<jnNBL3bzzWTJxz$Eac0U0(~_ouU>BV^cDnms=NGSR%N zIOxc!|A`72i&3W}?=cn??WAB7NENe55gf|_MU3Mmg(ErIXHb<$3&Q&mfA1dOS4ySF zq>A~a?+GT{4&_Zrz%ldWn07F_mBW3To0QgXgKj_;1581`F=7U2EV0={kf`9E;G_XO z+tBxDM$r(jXc}}G{=9bxAK%>T{-3^+3{1c&zNb?5S8jw8CMzb6=mpev6j$dCCp@;* z(zI_;hrnTJX2~1Muu+79Etn`YdEsvRaeeS~<C6t1StHJZ@SD^99-n<}??pS_ASt{o z?U$Kln%2smiP!Vz0hW@iQau{81KBp^a8H6knseJSyp;t7Lk4k{*`w&z6iAZ-ei4P3 zV{SD_mh^|_cmD93(_IHZ6RcVGX60Lr)C7i;CEbJDzk2`&m~R=~^<nU3UsJbp=aV`2 zJlmeE%gnm?XjNmQKU%g(4U{CDDp3)y&%~e?5yW4Ad%El6J1s2ry>c<qL`l$UUo}M( z4zPj=zt!#&Gt=@@fFwymZE%X@ajI+gD^oJgjf(mtyQa}el9eR8?M$4{Ck3KM$o5P7 z-LLO^|4-kGEv2=SRkK_|OYrUo&ec4C1P*CT!b?u>o;<(hhis@yeL;lV#nZ2<_F*%A z!FnjmOCyx;kXQ+Sl0{CVGn4+|`Ke$1{<AyZ|F6D4Fgw&hvGnGXrc0U2aKKE^L*@aJ zZ=1XcK{2PX&D4I0>o-K?YVW0xeBpjcZp+=zc8^%-Ql~ZDS#SWq_~Yr`k5ATeTiNhm za;~1$wyX&LlAA5+t!|mbL~#NIK@bx$6B98L0}aQYFvuVb_<udSyQ+GfN#710Z(sDz zz4v_X)!o%q)qP&>QBPqzsLS&mD(`3kIqEVQm(ECAQyrp7(==ZhNuYPQvpK>HxTz2_ z^U#Gvip+hcR{I@HiPtUP{o{wPKK{RQagh%n@;Tc!(J#yZapyYoJEnmG+=F1!nP6=f z?9<&<qhLxf%xU4G@MiIXeZ;ir&3M_=HQQHPuvZq;CglA7uOH8df99vO$;OILxg{Gr zrnYjt!b*o*W&t$r)4Pcs7`=^<?#%Hk@=)}pPW{-nu%>HFL&#$!fM9z|35PVM=s{n% z`0KB4DV#<K8#6=4I^Xt;Y`DgI$1o_5HlJ5$NZEH9l*SWeBJ}MX8XvyRfef4Y?lH2~ z8XDTb))CT^Io?rS{MRo2y8a6uKc)BKXandVWCP?7BaMXfCKZq!Y6k0y3S{lL_oM{e z@mFF4ZGkszy0WOQ#P^J!2ussRtBqGe>^C0y)r-Hz{|-0XaMCp6CV0+t)!uN*AFk8H zEh}Mm@dVI?0l710{+ssuYz6gOar@4A1N`PFJ>tmOaErG@jv+TxAs`9<`o$mPKYcPy z1kL%kU2@wh$G%M0?B^qZ<DLMuSdK0kY06-}Vq}SUm3qdT(Z9V~u*cNSlMSS_V<$K& zl~VE#^-dlj{;vN2Iu~rV*i>CnY9*_Vsj(R+62T#AHza^dSv;?$o^jh)-_S$Km)u`{ zy&2fsNsC7r!xR859vkM=8lZ%E-4Wg~zox<u5P#MGZGcnPC6P7B#SW@H`oBezWO&?? z0Cah;n1YOdIzSP2WX#7wI;<UbetIdH5c{~I^lxJu_N%3mN$Uf-C42TW#NQQvpF^Dt z^4N^fj7)4W<Dg^~Re6^Yl+H1-47xL!!g&+?F63<vr#pp5%5^F17-9-rDHb*mG||{} zI<}`wPY{1y|B>UM=2U-ce{IvAj-1+oTJk*~42jceA_I{cbQXFuq?yO;CDcVA56^n% zJ5!B=ET*z;ce`hzed^z?B6x)OYyP)+l$g-Ij&i^eM=u5JryB(Nmpk?Wa^`=~$Mz20 z>Eb@qfqu36e6v>!GQB8e{~<0&Fk4IufX%K!IZG$$5#q1)KhLnL=a5jZ>V`#<t#YW2 zszXDYBQNoM%O`_6##Mv*<jGDE{8SHUT3`V{)N(RZD(7%{a(iomNTf&nueZMC_|(N* zED+BSf9?PF0lV>5g=|`I>D9SBHYU|2cAst=#Cekp+@j_$u;B|?v~?jDIzh}WjqsZ7 zA`jtc0|~+B8xs+I$!alL{mQ3^KlwkNn?RB&8!Xv*;jka;fcdsZo44!(v<es^=vJNM zN2k!;n~HI|d<oiAhibVpsj~w_f&eC?<}#Gu-#tV8$^R#=LST9n6?-i>RAXRdvTyy| zD(tfp04zUqwY?Ai7AH!{t5Oqp_fp@okdTudZVN!<vD?a-aXnLILq0_OmH)%;e=09m z9NWEy;xb+sA<l>U*J>Mg%mS_`9os+i^0KB}=vP{%NP+I^2?e>{h4QBYxD-t#`%b$Y zz9t&r`4I8f{-1_E*Euy2z@`G=c_5Ds5N+U9gGGq2^%>vN0wnqspZYJZ*Fq|))wR*Q zRLQ~euF#t&#-@1`Tm@*P*xnY}$}=*7*WLg5=Z^=~KP2$P<hu2fN|gevd0L8n#Vl+b zPHNu60hodvYJ}1<nUH}qDW)-8Rn7_q<w2Xf#TG8*x&<G6A}$L05|EwfuiKv@{<!~9 zING}Dbp*RAEupg5GY;O-h-(lQcLqQWeakEWSzI0G6oBmsMG+xOKg0uxUFt-clC(@D z>@osQvY6f*=?8$&5|0so<o{<tQR)wNcv?(#@$Koq9v0C_=V@N&c*jBrW9`PmPxPr- zdvmk#iK#-K--rb<8RjoL2a)Z%Up)l6&lU@Q-Simo$N8T<9(pGpxs@OkKSYHWu<+t9 z&_)2I)*T7J=M9VYuHB0SR7So)3Ox$$Kfu(K9iZGkPlofL_86)w`A3Ec^u0bkM*Io? zaWMYtaC`j%64zS+*yPw6m<O%1eU1$Crhg>L@GM^5eW_3(Tfuo)A^7QlZcBxJj-49t zLvYr)1Qe}QUZ7L?IpR<Czo&Kx>qsyApsNtba}aa^<(3%CF^B3nZ*l^srbXK!|Jm!v zTXPF`0jdTF&e=(t2y7-X2KoD*1hqDW{`U80#lHg$RWU{xWAebRFSKeWPzLei*c-T^ z1)y6vLT<in{PrfHsXf{T=$NMT0_$7N;}~j^>2*E;;ylcU>s}8MfAW98mJXkdxRIML z5!4knhaEcOXe_;{1q4V&`P4B&(AlQ%1>B^5;EYIvf;^0YV9aY6FoYov96&xu{MG(P z*`aUilz<C5$f=nPEIY$OOf9uR&>_NY!=SEV^?hb4xqkXX3f6#Ow<481IF`cfY{>Hy zeYGBZ(ByCYe{vxxw#axhc+~X`4a7^gfXH(MTL+IDjG!34=XWWh$DzK`t=}`!P{s_o zpaC_WhWnRDe?0)l5V5@CCyBo+{+M+bIljm|cvQG(?7Yc_O;N=P3@Fc{RrnnUpcixh zMzJ^|enCSe?%5BN6W0wy_Xf1hXFI$BOCNa$`0+{Nuj)T^5U!AJ?IcpL7sE)aur@_m z+iSeX{NpkBd<1}JI+P()XjT5rJgEJ<%8{;mUK8|=riaCZzaft)K`ZG|;*b1aJl`FC z!WxoBah$G6A`HJR3}C8QXMD#jAQzywax{J9PrlIh0BK{cW~Ug|UtNT@0bw70o|YTe z`+k)8`{?~|M|GgHD_SNAzv;~A%^Np0698`yP3&$+0I<OMHla5q>~<-)tIK9E_E1Pg zLRN-&0i4l&1@-+|;;;E10dupN3K8v_h&}Vj(7X&|yIQ0p>?U>5-txHsNl*0FXNOoq zzW4yfnQGAI)DTPs8Ngx^bb2uqK=XrViN6o-e^MAXU2>OaTVP?mJLcvLLWZL0!i=Du zj<{zUC^({J3lDmfavDsGaTRhX2yd(Y=gbslphwJ$o$F!ZkNqDMa`e(V_E<-4Xv7<F z%uqFzSEJx4r5_kH2X9#kI}E*cb6)Q*R<<j>)V_`6!De7>2qi5|LMV?Jck#b?nE0#u zXT7g3FN1wrOs9o_I3I>#r?nrVs2bL4+lJlt9uAO$0U_-6^u}yI$nW*~tZF{G?IenY zUNLBQ>3YI??|*ri_$&Q?fGMdJ3NOjAET_!&5W3SbWz+Cf-YQumaK}<8hWbf`77Gyt zx!}M`#8R}Fg6=~HIm~ITCNLi<dz$!j{OhO|LyAV$n2m|95|!D~g3W>x2Brz*S4hna zZx{xtJ*x$Paux6pinCl!D`QlMauv(kfg=JTT5=y8j}w12|A#(JLDiRd^G{TK!<TcB zDL~L%$s@wplu(|^6|Y--fa^745Gm9EQu}9SqMY=guP5XgP0tV@Oyc%sK2H37e0=j? zhD)ci?C(IERY53a_!kfQ)ibXWT5Ytr0M(OUk6M(AcYHRmv41JGK+2x(I@;!dGgECS zoE`GEy78}34JZ=%IPv$2|NrQbiaZ&$E%^{uf@2790JQiiAs3W-uy_Rbuy5c1(tmU! z7KVu&13Z(25O@*y2Od()L>bK=;(6ll<D36{PX$l=hN5Yiz)2Yn+S~W3yDzU=j{zUe z4Q3{E$1I>C*-VAOLw}NILAq6}{?IUjiD^`#rLdKt!POTp{=WJ4XE%R+|0kFr1y?JZ zT33yEK?>)LsO6K74I%)54fPP;Qscg37?g_I6!V)o&5CY_x?X)TzDxWhk1n<jc2i$p z{%8Jv_si4!8htjQqDf2d7F_^Ge{Xlbg474_qYcv_M_#5@gXe1+eA*@E0b2m@kn7LE z|0C?$-NFIZ#Q(6L)!z#WJSgxDV+C2b7DXgw6-)i{#or(P9f#>+j^_Bn<J-AQyLe0F zC?(W-u!cd?m^dyC`A&-ON1}`S+@WJSbmO>M!mzI40(V%!^r<hopT|D>z)<Vf)fW-+ zdKWxJtQ(aLN%ydB{^G^o&;Mwhg%~6}Jnvw=R}hyByf)4jJ-%={V5T8Nks!U-#xHd< z{ejm{SB0(Vo5d5-X+*Zg4_GX1?gVA|XD2`zi`P4FPE~J-6Yq=&*UxL|-8NUKv|P;o z=<=ZXjW7QneEH3<b<~`JH(Xy(4x+I{HxY4l;6jzX)FQ6*csr^0Sua9AMXZr_NVU-_ zpB1^@&0t_u*s|ghAgw!`K&A4b=5ttGcf#`gNI<*xMFy5y6lrd|$MsZSy!iX!U*GlB z^;O1N!-VK{UNyHG_!eWZ4(P^o1$6)%0%=u#thU9MCVM<IVMc_>^HMe{UHEe23NodZ zES6hT0POxmYkbYBE@+RR<a1xog4R+FPNZ}%a+6=a`1{3g+xGCqDf&yDy;EJ08*aQt z2tyyOTr3a_3$UMy6gW`F5m#S{u$EM}D{isuma87e6E^6syEuRrRsT=T)$7c09Km;K z6dJe&oG6hq2PO;xN(3oTKtO~eIWwFRPAt8+zMZbB?sLiocpn9kB6)YaJJVf%Jtyv3 z(_C)6v*qk{0x`k)G4D+BUkLa5=I^(kn)smy!+H9VaYgB)xoq!1D>CoKk>%CwY?051 z2Q}sNoaixfaY6vD{plWhFL>fZ?Kf8aVVt?80B}ObuJwoO^}rOjJ8e9kYMgJQbUAf_ zmXe>|f%#kfoIL8r;!bVt_^`7%WkJ|KKg{HMqxuFg+*3z*vU_Bdk4w@}B)^5{^w86S zES1qC9owgF^^RlV6YI3Vc>|>f@A3$n2Z;UsCxHE-G)%Tf!2sBMFn<TMAolJ6UVYt4 z%uZMuo!dbkr;YCv{b5pA$_<o!52}G)Y3F6vQa;*zCAkKpI0uW34t-7|JwLhw0UqmV zTk!OaCdJ(fr5X)f(0o9sC*)x9PRt+D=V7m%z@z&do^V{!4^Ys9<?m(LK!kK8)S*f% zw}&+$XMv8~SSrt%d6JGK2NfUD7e1y44qM@<N_>3~1P*Xunb8%%?nYmZ$j9j*(A9~R z39Tad_d76uvNr_omSSA|2O+ID{VWI`lgn`*4naO7$Eb=@WO)4qL4HXs(>m4ec``c| zY=DsqbPo+7IYvaSmUk7vz}8BrU`gYR1{{jeqOe3RAUk}9_hA0MNWw7Lx!UPLcqhn_ z+|O<(qHC*91BgMuE(d*c=j<p(6;SFmP+-mhrjJUWWi2BGZ{pMt8c|^II5YtZk2?xr zMgikRpmjK^_^NUY2F@>HQ1Za_-k<*Y`PI$e2Zj92xqL2rIKsh9M$eh3gS^)uo}3T$ z!YD~hqcH31<%5|60O~2qolvC|F*BG1NXgl(ga@w&@6z?3cN74|Wj5leJ907qL{ij( zy8RZALcsT6{v>3(@2*YhLk`@COB~9dgK2;k1Xj!-!MF1%kM6)w-GjUd2fA^BY|~~w z=`TEaldhoC<et;2IczZ*-4O&i2qAA(Ed<+x!IM8>^kS|UpiALhm_NnwtCh4%Ag4iS ze~N(lu)iF4df$|9HnAKJQ~&@V07*naRCzGRCx_SMf?Z_vhU9+9i)F2PkEH_vVXI;H zQCR17TZP<E0FH5(@-WC8=n^h<Hz*8Th1)9#ya)5=HH8qb3~1@LcTj&e1<Jpe7(Ldm zSkQw<IJCtN)Qayn6UauvpG#QXDw5trvJC5X1_OXwf}qMd33lpKkS9gtMweD*#NB#? z;X&dZn7@*470Em7g*p)9Z5V~kKFT$~!~utPxx!%!fTb*o7;ybnl&|SIpf1iJeiS#9 zwSi@%aOnKFV-~O^rYdyNymj#kf|(nm%%qv`!TiApF6!1CLuF6lU1NCK1z_SnZjGmA zNP{(?SmelvN3KIPN8!UDJ^?CZ?n+5e88uQb@dlj_E-A6P<%=OG{a*l<R$=DN>5hCd z8;I505ZVj)4$R-DtUc^Sf1?fJw)Cl2s0*;OJDY|?>{)#wpHB=37OKXzmt6F@@)V(g z!U)k)bEb+$T^%;Tr6Vl@ZqWcJO+YGhDUUV<HImb_F|=-QuKDRbm_N`{@e~J7w?R^z z23yGxWq0R54Escq?hy_9piCtHPyTB_fZ5k|qIM3R2}4Sk^@ug>6iU_vqT#&dL=X*5 zE7j(=(E?|sfJuP*Nno-(clPPk;s2PwDeQcXX}ZcO?wjl|)ecScu4M>5^AZbK@ExRx zTuq(+VjzJbip$sgN=&V!xNLau#Gp8kga2{O(!<_xA_x<T%?f$^!$*vA-D6S$8RK8x z!{kr)>?xlCpA|gYb15_voow2*Wg`{5M(mgp>!)2DIeqG_*DQLCA1t&&?ihW|{8=_v z!L7{r3@U6V_^vyGph@;9@6C7gqfs0QaQ9K`Mk4FOtDC>yxo<Eim-O1waT;=L9`5+| zjdk4>X)#Ylac%6hsr6j4kqH=2AHdeR!gKXC=A;cBcIf<(m?e07^z69jdjUGf0>#eU z;gwm;D0npwFyYi|1HgAb=5BZ*cQBf{ejoC0nuH4bFoD6qKFVO>6TW_RQD(d~dm!!Y z_~)3wn2WfVGZ)V5HKgZ|{!4#>7#AJgkOM%<gXbVHMILFl%DnRJngj>d2hL3Lr47Jk z{yJ4J3Rk$(#n!drIS}~C^atcb2n)}FWrRl;lO@ROVc*c7n#}@lf8NX6B{H@v<S5Di z;W^=i$dwC#@}49RKGz^|*H6IPE=&eQ$1rO2dWL_z?)HCvGk+9LAe<xT<{^cPQ9y64 z6}@}iXce*WqwM?nF|p1`Fg6koC<zLwJkt0~ibgB?M931VqfIX}P}r#fpWy}($j@^C z=3q>h(0N%Sf>%I{^tuS}{fDR-{nkjb0b2~=oigc_e%rJfXJ(9Muf&|t#AN2K1->?F zPXf=CB1a=!AfRzn(RMPa)PajtC|-LK!}Z>}Z5U)_wDx>vUpf<a!Yr&x6@k6#2;eWj z{ma=^v{<_yBvuX^E(TUJC6*IgaLwJ5BYu5wV4Q)`<Uxwg@z<Il^T;85NmpVnA9kwg zWIp`+xc<>G^CqXbWfpMC(7d8VLO0+IZw0`$AUjDH{`Hk7e`Wi0x>|%C7PVy<_W2i2 zU!f5JHYh66Kbsnc@>F0lR?X2KYs<mk7TJpqki$$d!X&ybSxMFv*Asnx5Y(DvU!4G0 zQ3A?RTa+^Z%7sgy;?wspI{E8mTtR&u{pAIuwhLSNQ3S1lj%cIci%vr%5seR|`y(5w zW}`>hd-u$pr>H`q+7#g+aCk!Zqz}eSu*h!E0L-_F;&iN^Po{$C7&9U6+7H&vht~^& zzxxrZR|CVT+S5r@py_*OgOTei0u%XMwH^znrRFQg=1J^JccyW{wX`x+Zfiu2vQELT z6eBYM^h03XZ`~pSlcykhIg~Xp5{RgtJg6?24zHncFXG1+G=FkkmySQnXKfn;*+`Rk zOv?Ty{1xpIkc%zQ$j}lIP;G&DjQyz$4b~Bmz~s0mEGTcL&xK%(s5|{hd46e2dlB4m zBrH7jQ{vydUmI$sn$wPEK~v(>A1`M9zRcmZrwP5+n?EXu!tB9PI5V;^j^sJf?b_H0 zN&;>XG{Z!&TDkyZtQCf7i`@inbKhBXB?W;Z4)c`Yitp_3ph4j45y0g~cdC+8(W%%X zV!lX`)o4g@RruR?uVnuI&R&YJ?~4QNQc`E*iksNH9hT%2!2y~C|2x%Via4Jb4e=Og zoD7fR0P7R%4E%%6)3zXT3C8B$@@`CqS+@j1n9_X`vg462Z4>6svozwUIYauN7czf8 z)inih??UQ{oldzOE|Q&9?|knn*=^RsvRH%-`wH9>Egn%jdq>m+?H99luCila|y zQK3WFFen@+5LRh4hHr1bfdbeBakaQU{d7;cKox0w6~&R027LM7%a}hc81bwpu3;w4 z17?9)X@c|x^uet74b`Z*+^Ieqpob3ff8}_+h~%Y^yHL)nW3dd<z!0aWFzYEjUaH2> zG-f7p*OSF9B5)~Nor4v45h4$nY4sYY&^OId@hRlrUn3Cy;nx$GP)%)aJwv@<3{Tmn zU4yj$SZt4rI@AlHVtC|-r}+|MWl7fg7}r@;+YEOiv0vYOGk@a^Sz4?W#wjJI2Vai> zq1l9LZWQgT29fQkrr|+4!{)%3h=VTkw{Dx64;q&32$wKtF@y)idK!7K)3~Ahpg}T0 z2w~idIUO*xUtB|hjMCm<X{lhCD#z&XrroYKy#z#?u!JE^e0>49v|@rkrKFSwnA#&+ z^}4{G=bRb%<rT~yPcR7dN4QbbmGYGSy>dK3EfznF>?yS<yQRlNTdYHjk*oii4rGn~ z7oVO=Xk<l)yb*-mPzhRd4MudS#^}}?hC$cWiwGp`7tF+qV*1p6q^dFlwX(|w)rg<J zaUl5l<9`icOihrpFCn&Tbt4k(SzGfz;kU(BG}h9ig_L*~PYkmJ(S>Vst`jYjCcX)Y ztdIy{1whcl0u7Cl1uJ@x8CCLzx<GD70$pIyC4Q>0+n}GHGP*FLH$-35qy4*Y8wh^? zVFEfSe{X!K#M`7>Z^aUI#CF-dAld2ZQlipMEV&Y7gsU@R2qZ+M!uzwneNv=Lq}_=2 zoxhT02sEsnUB|lVeuP`T8%U4LT8$hd!viy{>^f*Q8F`E|?ALz$mges#^%1;peZNiJ z6YR~5POlD9yyhbGIc^QT5!>C98zLl(&xS=zNN^!Y_(*Mkplx5zC1guR4juqWrOr`w z8)>D;6HH{GTXFyz4;?(2DP&L@haWjP0pKtoMYm@i@a2zhX#QemuTeUo8)}%4uXp3B zy`GOgnC&UknH6$f6eOIbVK^dPM6q#DQ*3$cWDIOJ&UaHADX2z#QYhy5ahI!=rZoqN zCvM0AOp|H?4?PT$;o?qt0r^5fhgk7j1%f~PO04AYG}fGzdt~<vqrp5YQv$25^}=b) z-siPiX6c#Jf%bq30vIjYJn<(nSFbB+l>=Xd#3jo?OB+MBNV`Hv6B10Y0k6QOt4Uz9 zm3Zgv@3^Y$o^bAWFCf8%+drp|?Q&JwJ=7z-%CV!6{@$?Or9?+-OlBQVGrU_?!q_iK zl0qHuQSt%O9J&v+ZtAgs@jB7J|040n<b*&R>Qfy5KFAAqTQ65&+kyIIz*6?Gh#(0< zDv%{gC)2E~64)=3x#wo?K7w?@9o|63@c{er0~o|9b>%L5DUkExZ$|*=&65V7ggwMW zJr)oWC|j8RO5^ML{|ZC!`U78Nq|~06f|Gi#RIq@h1f^%?ZJs^`J1(u#FqR!MXMu>< z^*Z~(JWPoL`3ItTkVeq}0jbranlGJcAkbK$BtjXlfjC)xhYYl^Y2fRnDHBdi(k~Z4 z4h1`2mNfOp8-joS_MZjEYZjB^XRhHM-0K{XoN2+4Kc$$~vO>G^dJE#>7Q-!}rpSsY zUlSKhN@Xq(S7?!@`fHR}u;yzB!tTsh;N{%a9S9)A>Lyhbf`P@758snt>W_g1g6UrY zA8ZJo$)76aVaOaGRutd=39ROB*#~hij|XezxhxvnUmyc|(L#!E`#Rv*Y%Bl_k=RQo z>bag9inl()&_S`XAp#@K8Q;zUFxCQKdPzmfe!6fVSKhkC>g=a6eEv}J_lx>xtx+D~ zQddW_7a1CFfRziFT-oasTN{(Rhu=9Aa2a^zH-w#<>x~jhwLV`fyN5XkCN=dk^sH<1 z@xq!O=*>thZb|^Yw?W=(ak@p*VER{disZH}Oz0H<Ktu4SAAHoYM>t*!5y3Y11nns! z-;6<VI_Eds>eHROk<1ymR#R6fE4K$QbU#OKLf<&hmqW~3QMq;sC|7j3W}+AjE2Yg1 z9Dtw1nC{-B)a?UBYS1m5sO?F?JmUG=4>JUx;*Wglfl+5n2$u~oR2=lZ!Y$Px(32cU zgf%P{t-Hk+ZG**Dk>>>hFRK(14c5|S2`9`Dpq35hSufCaYAIo7A4FDem<C?Q4?z0> z=BW*ljCV%x0cOLtE@c&tHvIXc#GgEfQ>4O9{#-<)2IS2JM#e!2pb%p*#Kq3|*YgfS zO5Yp(*o!g}cq1}A@^6;-P?kbdt1YV6{bF9>@QsEcNT@W;xJ3reA~MtRI(^idLxB^r z5e4?!HN2{t>qC9WhTv2D&B71-wVsYU?}y_JnwQ8m&vV2po#%pmS+Q3mp-Z#0Aw<GT z-Q%R*^U4qn@z0cF*;+L-r+8X;-rF+=&#d)>j?#R>nOJ1v8b;w36)>sVNd`~PNkd1p zvQZ$SPaZ>O6#wh_@&1P3ufP3ILS4FRVnOaq>>a1G<g}A>FFn)G>I>G!Q(|+A2Hi^o zYM}$U^~JhIO#zUj5MP9XM+DRhP|sI_29pau>GUySK)=`>Tfy?H(q|x1<?2b=L_5vE zPG;0V7lu4u|M$HO!SDY$dQM$_sXuR7p@zOr&$q*&ZwZXZIYF+m=8|CtyX3_03RIko z2yg~0I#1UBc-f$IYsZBYr2I|k$p1pCSDsyIE*(jO5G#Ya2i!mc7d9FkO$T@}Abo~> zOKv>_-4$oD+70(#zrVBibK+qZs#eYEm}A75aSIXHSE1(xHUit~+j|}_2{03#D-f}V z<mFf84pOYOFX{ULq_43^g9^QxZ3NVTnH9jy3~>hn@PkxXrYP3pp2#KtjY2uJKm>AI zF7Igw{`AAku$-sjq$f182ZzCmA*L&@7{3htJ^!Wf8(Vj?vs9x{@cProNtj4;yM_2O zEN1qE<%R%$SSJA)e2vK2RI)}*zQ{dJ;36zV+XvP!MUcYKfAfXPoekFwgi+)D48c!- zsK6ZCngo=m4{<Agm-W(qz%n;HbW^{+E*sS4qXudoFUl818Me4Pw?T#u^-DxUWOlfq zBlNT495w8x4GYf|>VpF=d`#QDK1tj$3|f^`t&mGC6SLvcU#$>n!Rd;^Cz8jquXX+7 z^ZSXvCkMU$?l_Q-=NeCWuw%HY*=bY($=Lwb>~%J9Ah1Tx1G$~q@GN3@DiLd9WU&aj zz^8{F5y_;$>L*|EP^DwRs~0vA)cQFNy59Pxt>CWUbMPi9R7E8?92&*cb@jz@Kv2US zdzA1#hTxBXKT(buvNSU#Ae3Y`YydHa6&1{jh*@jpS0NZiQ!oQC9WOxZ@PX+C9pzYd zB1|T8I6JH%WD;|xvwIQW_vE%)ytTmyuuiBM4&8wO6`p0)qEgq5rKn%WU8oc$FG4lC zFpDeKI~an0UTdFSCFDox5Ud^MLFiyH9hFXxgHjyobnec=?$D5c@U0!m>p!C@9JQvd zhQ%JvN2O4xY|jtp*^=}74zDW03tOT1kCh)egPJ8P?@<Amn@~TD0R0gHW?J>{Y_}44 z?WreSZZ`zK|Hr{5f@lyY(bn!9-L^}vJ=|*i<?UY1W&^W7Va_USuu!W(J5m^<HXSd{ zU_Ve<kgMhQ6^2Bj-dxbBhALxtfzO({)!xr<cxD*l9v|QY)^Y?yA$7lV=Dxkogfg4) zM8h}VAjbxhU;cZ$_(LB=IbcRwA|`6Sft)Hg?{cIkI!cn0#|vlE3AT`}RQ2=d&JQ%( z)9(C1o*0bcbuA|$C&-vbtr=nvQ5*$zB9(t1?-m)T0_GJCvtwDr8Lf?DO6vvufU`2F zA9Rto8iLo_XHEe<b28NE4+A_lER$2Nc_(VQHtkgwM!{{1r|li3`^zY2(2@P6)|`o~ zElSMMe6ooA<|58Mna>TO4N>5;$)o0k!j4f9*W1NS3nBRegXS{AJ8RuO$v8vMnXmJn z{fKaipAUMYA$U#xeEY8ix(Eocw_}mS3-nBn&FO09VKLceNAp7m6TbR%AWkXR4TqMX z<b+_^BSZI|4?F^3QTXPez+9{uiJC{lm~J?A_c4PeKJm>75Qac002)wzsal$9jEfZb zHYN;wE($17aXs|+`^2At(&Q~%N0fIV{&rYDv0I!ll6y`);9x9K3HR?v3&*IK#SOE( zerVt&=(G33K$0_<6@(o<Q$q~3*6afE23%6a12T~p%jE>)1nGtZkN`P%#`HWz&=ZMW z0bK<rv?{-T&Q($ByS03(n+(CH_$%x3N&f@C5xyA*<RoZJd;&7Ao7^smo@^8s^RWOD z^C_%Con~$4L&!0c1mV38^gKtXD5`T|9-ubldAx8x^kUuEJa@uN>$-mV_7)J*(PBRl z9HE43#ACjpNG_{%`nb1Qhav7T1b_Yci^5jTx+143Mu$(mwBbA46(AqA;hgo!?>U~> z2h6v?Cr7TWpaQ+W+*I9WIT`5^j?A)b7!CE+fZl3?snL+qhwK}AoL<}>dxYcE8F<Gy zI6}lHm5WNbQ`t3D6bJ!*5ZtJQy3q<4!czS64dU<2P1wj!eEKelt3^aol@?Y*gBf8u zeIt!l{cmBzC~oO~W=+qM!lp;tNxeCtk|#jKtWQvwxGW;zLdOzbaWGf+=ssMLtH>Jm z-5;E9HzYt<$hF!FHAv$m#)N|P&6OJ>2I4LPf1dmfL-22(e<AQgTrJ!a8Im?1`x1pu z<p76x_DLOnyjpD1<9@me3cl1>ziD(aGw%KIDE_W2kf9nLMPyFPS<rZ%b)6)_YSz{y z;b&|OT`Xcr+nyx$FYfqaNK6g)I7h8EUT8lVRet^9yf1`Ba@re@HyeW2pY}O^{7L<D z@uBxoSxE;>)yInoXO_)ZyFySQcBn`abg2<kvmurjMunK~`Z63D(|0&djd2j?GWQ7D zT7kcd+{%CSL1RRCXbqe8cQ}C{fA(kXstyvC;PtT7`y?=WVRNq&9F4=9t-j3={PYL( z!B;R-G3#GxtEPq1Vyq!BGbLe`+tpol*@kSW!=y|>CrQ)sQ|**Pk+0&<on|>nF%zI` ziWTb{9@bCLur<OdmhNk`pL6c``YTDtZ!m(~4wa^1J;=-pN{&--ygA>?Y7gw7L3rTQ z?8}eeF8%~PG1wQex{{A#rlz2WW59CnEsUHTk8wD5hgNAt8mx0#_w~4NlhZ-U13;!_ zGyl*!ZAeE;OEL`%x9jyF`FgUg)crFoW@`+KI}%_evhpq1>@<7Om7Cjvy-<93Ux?CM zCJnyX5PXWiXd2p;6*l)+p;LkF0LHfaG8V6jvEq`q8B_Rud{&<sugQM!jt;^Y<>`=B zEFe4?@|`q9_{Qe-U%!RWxfJ1P+gdM?;jld68Oo8=Jb!$<4-m4M(j+t8{!Hg0C}naH zE28s>Qa7rGuL`)SeX}9>6o2lJLwe~_457-vbOcP(?Vh~)cKXu=LD``UFN#2n>-nIX zeF61B;$!Fywf$u)WmxjqCf`{lloKLAiOBXgv>Ca_@ywBa(^%%AVd(}Rpv7JYR-~ze zNZL(|#vdD`ea-Kz_?EL84u{OePv5fki4C*s^}$YL<sU=0g4m=GvsI=!<4A|7uXuIj zmYDu9886x(CMRYXoDxBq|7Y#mb?rEg;Ez<&gTc+5FrhLVkRd2kBs#HvL1yaAP^O5V z?7P$T=)I<B5?To)?&F-@*_rO@s_H({L_yP!5?)q<L$>LvJ<dGMK@r?i&MWUq`s4ia z5e`t54J=i%ojWO;d9H_pRLNM;rY*C;fbL6&(Jw!IbLa0<Wh<Ea!WezlBjBXDkD-~} z-PP`?>A`(G?CfM0H=uZ}4tZUNicFBb4JI_pLnBJ7R16xftDN`z*JYQLY>V5a(dZ^a zh$e;O4iQ?<Z~zrWdL3KJh%A2i-}2j7Kt-@D8<Vps&u0(5`&RL1g2~f(k@%}J2~fh< z@X7Bs{i<+s7dS!akp8s>^g8ouSV4C-Sqd@p<&GUihu+GUxZdazO|gcEsL_^;oG8{Y z?QmICS{y#W0S13_3#e|auybsy?v-(^wNyXP+&uDY$+zn<K73o}kNt>bz^%q_HbVw6 z6u*`{TFSeZ9f}s5DA}KastVVu=``ppS$C)&oQC8Caq{29zhubh>ed)RV1e1QMZv%+ zjfiPs8cv~$)M;7uL;#!xA*<{bggth`u9-n4`9dG7>Q2SnBWObQ4Tj*mkAhZ@ozn6z zJ$q@~wzOu5Gn?gKi#dcmP(mw^{*m7+_)lU(V=W~}9K!=T{vCBjfn0@^lULuD2gs-! zs7gnt97EC}OG2|o<Kg-}go0@uK4Apu`TcmCogJZI8c<z^c1^y09sz+3mMDW)|MS&` zV2eMjmp*FBaT}sL4rNWYZN_i%XMf*lwdl3DfoQa5JMaeJJ1wPt58}kQQ>RzHUKrbT zyaSCs(j<M9Q#DK%PA4eiNEu2a<$ZG`Io*KmV|m00tl{$8b~<UbO6o)D(7OkE(^aFr zQmgJ*z4bKLD}Ve-@%Qh<T&d_trL4?e#s~~U;udKjOC~Xm_d&9{9l!(Ge#9*aH|!x@ zJ&mDC@WW3>$Y4>B1AwzE4C`R1g3%DL=cLdz99@3{?0<B{wUjwOcwio&bcBP9h^#s~ zTz9AvWv98HK^zMQ(6z*lzseAN|NWI3P@7h}UKUU^VYOr1wg0*iLb^|ez!qY;gEz#( zX4aS4w#eUBD8sUmwQ#GORD%m2=yw3zS=4;PC^>uiIHkE^s44pGVk1{inr1A@h<pLV zBSw%ptCA;jCF79hMS{P;@OAu9hKi*--T2YQuQCKr=dZKr^)g_o!Lm^0IgoK|*;5)7 zA!7-KFA;|AHe2uG$x3S)OP=%;brNAX%k<-VH-<L~<5OC;#kg2P_~L_Twu&-<>`(4e z_@e`<2KP}h8~x!Y5FkQ+PcpE)o{B@Fm*~%y9+|x`9ao)a=5<g%{a^7XBH24;W` z*j`zz`oTNoxo~yfI(34lwQ(?G)$GuP5-NeAzKqJE#PL+gxzTwQss@day`W3JY=-zg zIZx!Mi$WMYKsISqL7C-rGs4r?k4yydLA9KAe6Cx;iC3CXvlF2FX&A&ZZC-<~3rzc$ zUp55)^UI>3HV{sRDn!{5-8v}E2i9<3RjW{QP{5%++AB30v6XTs`6~do?DIKuz6+^) z2d$cDVkh$1J}es!8o-I<bdYAQaiFl}t&m8bT&-tHK&>~-x@=49hgI^uoI$s^wvIYE zPs}Csl_B9vhTyxORQL8Cc%mfREj8j#`?MH~Q-R8nv9af}#q6wQ+;s?NkW77Vad9*q z_UMiw?>haEU_sB8qj-I5b$u?B(kTliaP!hxSR-LdZK4o}u6;IM>WPV<Q`BBB1!Nb4 ztFG=ka}5ggBFcuhi`)!p{!|40yT5$lfA!!*^T^CiqMp0MT^aYpbL1tQl561Rx!VEP z@8F>)OepuRug}$%F}+h7BDt31j{v(sbA!MrK&W+|uLho2&9boujFsHJ93-+#BObAW zvy`j&U_e5}I-3$T=ZlG~O^0cFK)9u&yDswKkHp{a7wPL6T6rrs>SS7$;B@AJtyz}= z)6kk0-r*r)^XKeB?&si#U74~+&N&Fk`0K;Cjw;NgZ;f^98tCxQJ8=oR?Ipc(=VV7J z=!}sXOmrED%DnQK5`b_SmtgALNV*z!=9)yR7YG-H80^6dapC&({tr5T_L7E;-*m_% zWOH??LWnSl2RA6gK!uZ6<22U{{O#CsF#E^q4)IVh65ZD?JHq61{iU6CTN94g#z!-{ z937Gd(j_H1x|9JjYIJvr#OMx1Qo2)GV)W=1B&FjE2%|%CKi<M~J?H#$UctGp6ZieG z>EVbv{S2G1b`3Wpp8Gx6?t`s;_$)H)-Dgk66+!9lPkex#1cX%eyf9!}z~-T`H#U&n zanhQ%J(*Q-@9O^Gt_vOa{NKjR^DO9}Hv?>(zRS?h@y5Asfdy=`LWaM4#aOyNJa;-s zbeY>$i2`ZRFArdl=?{}}eEKxeL`heMSoaOEVEe2;i$#Uha21_{YxU&TmeIIy+dA4v z!2yaSEUU-yqrtZ-ufK{p0a<IcWGDNv42R3-kAv>x{w3eQwwgr=8H8Xs<+NE7+(IM^ zh0g!L#BY(*7OnB2URc~;C*=ACJl~Zsr%LdKh?7TZVg7Q?5WqwQqh6U0PVaPG7!gbt zGK+etsdbUa7UhIHNW5YK0+NiPdw=}Sb#>D>S-|RlE-^6pHLHqTaRjeX&aexO3hG<a zfNZ-CN5jal-yxs$3Ul8D`s%aLN#OQA8e~0j&51Z1aC(!#$UvUs2IJ}-$jfyAC_?Ma z$Sd0%$^%KjD<(S?S{K1k@&GRbBXBV*ig&Qq=n2mO7<)tGzSG5<D!^E+b3-s9jpr*; z8!ERSx{=V*Kk|BosK<wycqG8@Z%hg;XZ24zUj3z;a>@$zp^n?UE%TE`{yt5J#`ovy z9*Oc_6nu(b+uUnN6-9W463Mr?$dv_q=5wx75k@s9x(smU;zJJ)*AdL87)vXX?To*E z1z0o}E!My1eX{W0)BHsuMb>Hz{B1pKVZ3%PNPhCi^v$W}>eKv8dO=R_prJRN<b@)% zZ=;yP%`{ih?n#9^b7?OD02Ib53bMMPIA@+|Tm@&rAjg@WiRp#4icOt^Y=(Mwk20Y_ z6JBpZ6;ApQvBKkd+lts*e2p5%upydFKj|r=wlfms@+@bwmSMK+l)%!YEk2)8_^xs< zgslhWgilb{g$~_Ezj}<4LVorM#g)_(FNysD-Q~}7G!bAI5bCzVvhoTc!8XW*-<!{B z^s*sMxB$UA-*1VVOEjiUDD#p$b-q;{scdYv+8mmz>FK3myQvM-C1D^vnMnXB?u-R3 ze@TDd&4&Wwd=2Y%=UOUeuNp``>M(?;s*MBi5=Yga*6$F~6?l(|%w0nt%n$J_HfM`S zO6nBPioE5kKTtjZ-=9xY+TeY`R(>HmOI2r*n~+vVKs2KiL1^&mN;lL8QASf`$=%^f zpZiAxc(R(;YO8?#CstG*qjg+wTLik-R@N^5>s|ri=};-ZVlt0EoCz6Zrse-Qu-A-e zs{7Vmay=%&&DvmN`@%J;9(Rp-EKBFSje0kE-^(Val&VKGj&l^bXDBmJuXq>pZsB!k zpZXeD$>XRia+a3G6<J0U6px`jdyJwu7-$L6vAu(RV3z-?0vzMXVF~`NZtzQqTdC-{ z8I#i;&-Nv<n5hx104T{~E-Of8;NU96rWMx%+LbP0OVq~FahyI`oyd7TJ5_nbq=O53 z7+u2M5^$8$kt)lS;CJaLG@c1yjlDQ}vjP(RzU&~85R(M3m$0EJ567R$fBWCfY%U4F zcZdfl7L$;kp-kYz@Wbu3f&@Rp8VE&-=;0KW%oR6Eyfy4>aVFLYMv(jD{WnFsFm$p0 zwA|Fc`l30^%mY=Rw`ixnVhLLeCMs^9X>0Foa@<z~@dpk<qg^QSQGJT%p#n5_OlP`3 zlR$xH?2h;)u!p`Dy8ilWJiEHW_0qnZ;s9;Kb+{P+lK2J|=@U&p#QCLYjP!K*ddJ;1 zV*>6DTmR>wszl2$T~TrO`EVA}IVa5g3$@|clJ$Zd5K9r@E#)sQWVZZws4k|b6l*}r zzhpiEZYM|><~Cx_?*IeaKssMFvBx4ww7hK@*GqQqJzN&8<{MIvE*Vu63lmhfSf8_< zgMqY|T9ww#X@MF!YQm@~c8#OOBTFp2YMzR-!pC~kKAZwps<vCXzA=xOgm|5%^M2=x z3GJCI?-S6hEesCVlmA;~rEKd?FiI_{gPB!huG_Mh5bX(3uia>kvp3o7aLFf#(ur65 zEhXcUT;9*hlAo*$&mh&^bE1msRTU<fD8e%ODEse<{{qt7IYq|RH{IvXckfF-L!0>u zd$*NG+{WO%R!LK~w7QI&U=0=_!%;D)W=2;|*K&f7XAAm9N4VM;L1DIWBscKQMned6 zG1(@Dm7<$`{Hv+C`%{-;gIL<Cl6lx!LkTl^<>2q{MpOgjCkh25eagal6rzzfS|aeQ z%TIxHnRukSlHJAR+n1#S3YqqM)oJs;)?*fTI+>XgRE4t;SU&ABR)g9u!sO1`w{QaG z`q}YA&!lXd1R+_j^4YKLO9acOQ}d2B>EdFfTZ~%@V{!w`*&%*6r?>$9?S3^lE857c z%dJMyrW+qWL6`VRs2`24_i#eEvx_>mNAP!cU$Gv9UkCG2+@sgh$M-2zAX;ZhGbEBx z*uK2GV;7$KPc+}z^p76*+MNJQ;nWfNLuR$Jud{i{NDE<Q<`=<HO5e)jFuu>j^dBEu zM-??^Bf|2=YeAZ(>)36UisOo6SAvD}MspDNQHL$?MkMoUIlD`SBJ0@fOOMA$*LiS_ z23=Gz^MNKhd;sfusztXgSf&LqnO@DKdD(^QCIS(wA4`xW+Xnp8PP8=l(*4`DRaCly zjCI1v)H0C;SoYwHm`p1(TC{S^6NPq%gxAy;)2;lLd;<4cAQ^Ut!qWaq>(O!SNm%PY zA;rba;w=$;H+!dfpu#u!9t66E12~^n1cVpFZJmQ3A}qn$CQ<+^{Y(}1!_a3AQv2pk z3fc~y>=!)^M3ptVO0oal-D=nKQKWGI6!9CgE&o*p%X)(Z!X{I?@lj>tRzpAxirZ=O z#dS7f!wVluT_@OOUfO1D{j1#;8HUXIuv;NxY+dt;?>!Va!<?`q>yHbBt}$OZ-7uE& zHI*7O*nxww^Y)C!oK?$zsh%4g_S{$%C#d&tF_~&%20Ka39f7X$!X7rx%^9Iu<NZ$$ z;IJC$W?Tb<j?epwdtXL~w!4elIc&+ZpHa6$l|P{2X_6ePJywgV<z>A>dEGVBnH8q3 zRnx8??iV`!ra9c^2@m?=BFx2*S}DyX!PJCb<v+#Vn-n%7@1*f(Mr};<FqLi0rf5mL zf6EzII%aritAX8fCSS9v4M<pjYY#UI{fzZqq0AE-eHQ38hTo0J(S<dva-#dS%7D>{ zB^@5&eJU65N<zXHj<kO`$+brTGS)_4aTIkrvU23N>2E}vIgrmoqAwe*V6u^wuhe+o zl9E9xR3NdLnD(n+Rbpnqg8{bDXJiV15%6I@XC(aeB>b0!jqnV6zW6PFFE^*JeQ`GP zAAz^_G&ng({zN(9C}l2+_iau2Rz!ZSwX>yj9>rnb{gOgeD}ke;wO1c#C_~hSwXIO2 zuX(gFtcCKQ-tqdvv-&GK_}MykdZMm$Oy)#xzH^f`Qn!T^JzCuLY=(UcE(VhFb4Gt; zq+amJVH{S(C&~t4rW=I4WAs-w1x9i(F6oFYrr7Z;-uiOOygPCmZ~j1^NkXKYNm53M z_|kUnRrm=9GhMXVZz~Z_%3hJkyc<z>SkAgl7E`a`3V;3KO*Tbr6|*(PjrR3;8n143 zpQaJ@#t3_?%`RlWz?oBa6rZpeCK7h5<5ophTCTv|o15xvy~jE$2;b2y_&R@2mN$;W zl2csz!qDO4*xb)DU8i{*pmsaX;LFeoXSPV|)rvS4!H^61r3Hv_C@MgjQsnQ>dCi2* z51aQk@<qQRW^{4EFJzD)tNob8FK+c`vP*Q>)GkV_;db}DtgYp}u_<L#YBI%n5QV62 z_l6yk?lQKwCA51@l-hqy1fH~GarFxakNZ3Ir`0;qQbHeJst4%j3UX0a;gtp4*}Z?Z z(FXE3%<`?3W{w{ghX+%x&Bc#^iN=lT1lgK)_*6&2sU<SOstavBdw)7rQmoANA`Y?? z1jn}1yJ_F<VjNKN=wRTh%uS1OOM8Y`MFo3@<n>8u(zk^zV5YJnN-&p#n4|{Yq@~zj zB)2e|$$_Q|We_Y_M?b%&_P(J-3>pE0+P#KP_reO|nPJkOATk@bXXQ=en`&hV_B+nY z&$1p_e@bghE9-qS*~0eYIL%2F%g;2m`dE@6${QB+T(z}kAllw|15Dv~h<q#0J@;jk zvY07)dM?*@NYz_Df45C^qUz&&&-j!f_v5kJ(K)Z;FIkw4g;}bZ;*JL^j1hQKOI9n# zuc)Tg**)GpRIw3c*C+!dQo@#w<ZR}^rwNwDH%4BUA*@#)vBOL1pS#BA5NtZQz*LO| z2kk+GLzS^1SiZIgl=l!~E|nHY@!wHd_ypg1wb=_NhH=fk!l>HS7c4SSRd&7*pH%Pk zf~Du@A{|@ZsdA$Qtd_GTC)5hMb;8#iqbZ~w!4#S}F)K)gI++*|k;EJC`T_L5i}I8Y zZnzu4fX)_$OH)C=@<DkXm(`i72$fpRR&JPv3eC<DMQ&=DhCJ<hxMWA;jG}dXFer)j zwcB)?c9JA*mqEH`G<lz;jdS&%Q{yw~@lTT!5gdt({xFf(U*@B8ny5Nv3<Z`X2ff>J za;13_14f`znj_JgrY2T6yQkA@Npt%A=VMVjZ>OS2=zszg#-r}trW6h=0uJ)PucY1_ zLb@M|Th6v)cq8DE{6k-tj6x5Y$lMDh+q^_O70*VeyTps8O=@;tQYvUk{zxV{<L}RN zREhM+E4LIf)K=18nVn|<+VX$}!u6b5SU^_JQDs#>iyIb0>_=8j|CVpS7=zRrFWkf{ zZU;G#c_S5JAnf<!!&+E(2&TRO0w|7GTpq83CoA1$F#Y))f{>~>`HSJZH-QCqWCj!^ z@%~V7<RQTn;Idr>KdVwdRPzEXK6Cb7J!M|4p^x~z9paA;#--F`^zW}OL+-<MuH(G` zXqchV!6l5(C}ndSm~)UK-R4k7(`sj{kXzFvXui6Ilb+}65ZwTA|D4K4maG@to)YeI zK<a2P`m&2Lrs<qRe9%VoBGrNON0#$&l}IUXJO9WQ$hbmqB<&yIJCk!ZeU7^#@NyeA z%C$jMX9hHW^St2ttz8bEuO*Jtibw`#IZ8t(UMOatcJ7{dEvRw#1(Ip;x1aI5_*?^X zD=ho4>&ix_YSQ3kh6Ig{5RoPOpXvwU-HP~ApYSA~?cYB6z(xccJ>A@^x2kRaGkwNl zL1nybKr?@A@MQwWfs~@Nrj|nh7gyv<xBQRd!Z-0w;T~Rp{-aZ!-p;EUdjClvUM-6T z%I#FGPI6a%gv6+s%Q(+?-ARw{<$PBrB$J3*z}LL1EFM#y=Y12Z(t}#Dql8?E@2Yof z_mRihSP4I?Q?u>0qdWLDE4VHs)qV+OYU1)C9kY$Qt|%x^9VbKaj%*?s5qe8HbV{$X z<10AY;n?7U*|C=>vvEg=D)m}z5xHYF9oE<jH*?3=l^8GjuseR$YG;L~)Er~FC*w_> z3Lrw(@RoV7GcREE;L_?+FM9>IgS^1q;K%PL0$Zq}4NQB<DkXoFLWhK~VnI;?Ye>c8 zzjUnZE#1IUf|{Gi=Oj}n;gh-GUT-TIx`fVU+BaB>o=&Jqs2*cyH1(kni1XG5qe5bs zbLF~#?~<W|!MC;t;pEIm&DmuIoAuq^S=0CD%_CtfBUP=X4NXK{Wu0d1&m87dn~M6q zpstPUrBwY)?$qJyAP3_WWjsKQJY<bq21BLgDu}J)vI-&YVt%yy7m_YQT!!`$JJSQ* zOLx9Q)Apw^$GG6apn}fo<k*5*sY_t2gKb{(%$D|K^se25yN%_z-x6%ChLZjuR)q8w zr=57?N|DPy9&<$Yx4kidF+Y@aSTl>ksPKRd@%|R5m|GwW{8sjMOm5ae7fy@%wVc2> zA=J}oIPpdU3iM-C^1)AaGcEG}VC_o1_S72l6%b4d(Q)!V3Tww;<4|Z*Ph$W%k|$X+ zkl9-I?=H-D#W-4nTcQJEm3?G;tmmddSbIMBIr<9Ek^N=W&surdJN>EE06oXL>>M{d z>>PFpAxB#>9&xRXfKV2t%n5rUH-Fxrh(AqJsi4x$PwUcIZBfJ{bh!NyrgJ_u>9``| z?Z~2Bd-43V#MM7S_z2Ko`PNN8>(8q%xaoC1i{~cY<HOlG#|xe%r?z7alQe~$ICk$g zdy+qp4plQdqRQjd5U5UIC@SIFh=QY44wtSiJBC&R0#lpNJi0}Giply*($VVjBQAKf z(~A7TnCoH|+TeDAFBIzhL@K{u#(!=||5j(xS6i6AYO5y%W#!6Q366#)d#c^PdRBYa zAyM-e;M3a_2|=eH^{7<qJH7)i{rJNn%=9r3ZM}azv1mCQnjLozZC3IbrA9b94wn0a z7XjAC$#q_#E2^S63}US%`YbZ@PZn3FOwge3Zq%e_>?dM_h)|l{apDoLyMR6(wymPU zt#6<@%AdY0o8HxPw#ya|b$`Iw*9r*rLvaze$3>u!-=YQd@L-^aa*{3?@PA7*f9E)z zw1@v^FU<B_MgRYN(SNc3iO2pMJH4C*o>+lSGqy%6Dc6zQn}UOp`dr<Jk9{@?J~f)~ z$%d#&hFljmEHE|^kcQx7ZSY}+d+U6ll#p!w@vdsCVc__hkiyKf8{L5WPrv;^`t?oL z=>v)b=z1rn_Eer`K6I@p0i{cE+fBa#nnfGZCuv-q)B2ekGAF5njGEESccu+Flc1m# zUUm4NcM-Uf^cernz3uKryr(=q%5<&pB&^|L<&;12uA#dS-6!zfeRR-jpxm+&p(G)? zYb{W-JwxP#=mh=W)y(Fj+kco{zW!+6u7@$-JLm208uiDuj*f@JmY`d{?DhG+N7CG* zX0(QX;PuAn{PlTtohf@~2l{pgiI{AT{(nV~GZm2*08siPLcFp)ryuYXDvFv4PjdE< F{{gs<&bj~q literal 0 HcmV?d00001 diff --git a/live-testing/test.md b/live-testing/test.md index cc20e53..50be7cf 100644 --- a/live-testing/test.md +++ b/live-testing/test.md @@ -2,18 +2,24 @@ This is a *test*. +``` +import this +# you should try this +``` + I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev] -This is the first image from the local file system (absolute path, sorry): +This is the first image from the local file system (absolute path, sorry, it's not going +to work on your system unless your username is math2001): ![The sublime text logo!](file:///home/math2001/.config/sublime-text-3/Packages/MarkdownLivePreview2/live-testing/sublime_text.png) This is the first image from the local file system, *relative* path! -![The sublime text logo!](sublime_text.png) +![The sublime text logo!](sublime_merge.png) This is the first image from the internet! -![some paysage](https://4.bp.blogspot.com/-RHTlwmd9EBw/Tn84-JEW8NI/AAAAAAAAAD0/6ugjklUMbtY/s1600/acapulco-8-704125.jpg) +![math2001's logo](https://avatars1.githubusercontent.com/u/15224242?s=400&u=53324cf4e303d15032ba53aa41673a2046b3284b&v=4) [prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc \ No newline at end of file diff --git a/markdown2html.py b/markdown2html.py index b90c211..ad16814 100644 --- a/markdown2html.py +++ b/markdown2html.py @@ -1,15 +1,16 @@ +import os.path import concurrent.futures import urllib.request import base64 -import os.path +import bs4 from functools import lru_cache, partial -from bs4 import BeautifulSoup + from .lib.markdown2 import Markdown __all__ = ('markdown2html', ) -markdowner = Markdown() +markdowner = Markdown(extras=['fenced-code-blocks']) # FIXME: how do I choose how many workers I want? Does thread pool reuse threads or # does it stupidly throw them out? (we could implement something of our own) @@ -32,7 +33,7 @@ def markdown2html(markdown, basepath, re_render): """ html = markdowner.convert(markdown) - soup = BeautifulSoup(html, "html.parser") + soup = bs4.BeautifulSoup(html, "html.parser") for img_element in soup.find_all('img'): src = img_element['src'] @@ -54,7 +55,6 @@ def markdown2html(markdown, basepath, re_render): try: base64 = get_base64_image(path, re_render) except FileNotFoundError as e: - print("{!r} not found {!r}".format(path, e)) base64 = BASE64_404_IMAGE except LoadingError: # the image is loading @@ -64,6 +64,12 @@ def markdown2html(markdown, basepath, re_render): # FIXME: how do tables look? should we use ascii tables? + # FIXME: pre aren't handled by ST3. The require manual adjustment + + # FIXME: include a stylesheet + + # FIXME: remove the comments, because they pollute the console with error messages + return str(soup) def get_base64_image(path, re_render): @@ -83,7 +89,7 @@ def get_base64_image(path, re_render): if path in images_cache: return images_cache[path] executor.submit(load_image, path).add_done_callback(partial(callback, path)) - return 'loading of the internet!' + raise LoadingError() with open(path, 'rb') as fp: return 'data:image/png;base64,' + base64.b64encode(fp.read()).decode('utf-8') @@ -98,4 +104,4 @@ def load_image(url): content_type = conn.info().get_content_type() if 'image' not in content_type: raise ValueError("{!r} doesn't point to an image, but to a {!r}".format(url, content_type)) - return 'data:image/png;base64,' + base64.b64encode(conn.read()).decode('utf-8') \ No newline at end of file + return 'data:image/png;base64,' + base64.b64encode(conn.read()).decode('utf-8') diff --git a/utils.py b/utils.py index 6504b05..e0661d1 100644 --- a/utils.py +++ b/utils.py @@ -21,7 +21,3 @@ def min_time_between_call(timeout, on_block=lambda *args, **kwargs: None): return func(*args, **kwargs) return wrapper return outer - -@min_time_between_call(1) -def hello(): - print(time.time()) \ No newline at end of file From 8c1012eb8c057bb4952cd99a334b5789fa176702 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Thu, 14 Nov 2019 21:17:22 +1100 Subject: [PATCH 11/20] Remove entry in phantom_sets when markdown_view is closed Otherwise the object would keep on growing forever. It probably would never ever be a problem for anyone, but it just makes me feel better --- MarkdownLivePreview.py | 10 ++++++++++ live-testing/images.md | 16 ++++++++++++++++ live-testing/test.md | 19 +++---------------- markdown2html.py | 6 ++++-- 4 files changed, 33 insertions(+), 18 deletions(-) create mode 100644 live-testing/images.md diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index f330712..5bc40fb 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -15,6 +15,7 @@ PREVIEW_VIEW_INFOS = "preview_view_infos" # FIXME: put this as a setting for the user to choose? DELAY = 500 # ms +# Terminology # original_view: the view in the regular editor, without it's own window # markdown_view: the markdown view, in the special window # preview_view: the preview view, in the special window @@ -92,6 +93,8 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): # markdown_view.id(): phantom set } + # FIXME: maybe we shouldn't restore the file in the original window... + def on_pre_close(self, markdown_view): """ Close the view in the preview window, and store information for the on_close listener (see doc there) @@ -118,6 +121,7 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): preview_view = markdown_view.window().active_view_in_group(1) + # FIXME: set the preview title self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view) self._update_preview(markdown_view) @@ -131,6 +135,8 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): assert markdown_view.id() == self.markdown_view.id(), \ "pre_close view.id() != close view.id()" + del self.phantom_sets[markdown_view.id()] + self.preview_window.run_command('close_window') # find the window with the right id @@ -150,11 +156,15 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): original_view.set_syntax_file(markdown_view.settings().get('syntax')) + # here, views are NOT treated independently, which is theoretically wrong # but in practice, you can only edit one markdown file at a time, so it doesn't really # matter. # @min_time_between_call(.5) def on_modified_async(self, markdown_view): + + # FIXME: it keeps on flickering, it's really annoying + infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) if not infos: return diff --git a/live-testing/images.md b/live-testing/images.md new file mode 100644 index 0000000..268e112 --- /dev/null +++ b/live-testing/images.md @@ -0,0 +1,16 @@ +I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev] + +This is the first image from the local file system (absolute path, sorry, it's not going +to work on your system unless your username is math2001): + +![The sublime text logo!](file:///home/math2001/.config/sublime-text-3/Packages/MarkdownLivePreview2/live-testing/sublime_text.png) + +This is the first image from the local file system, *relative* path! + +![The sublime text logo!](sublime_merge.png) + +This is the first image from the internet! + +![math2001's logo](https://avatars1.githubusercontent.com/u/15224242?s=400&u=53324cf4e303d15032ba53aa41673a2046b3284b&v=4) + +[prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc \ No newline at end of file diff --git a/live-testing/test.md b/live-testing/test.md index 50be7cf..24d3981 100644 --- a/live-testing/test.md +++ b/live-testing/test.md @@ -1,25 +1,12 @@ # hello world -This is a *test*. +This is a *test*. asdfas ``` import this # you should try this ``` -I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev] +<!-- some comment --> -This is the first image from the local file system (absolute path, sorry, it's not going -to work on your system unless your username is math2001): - -![The sublime text logo!](file:///home/math2001/.config/sublime-text-3/Packages/MarkdownLivePreview2/live-testing/sublime_text.png) - -This is the first image from the local file system, *relative* path! - -![The sublime text logo!](sublime_merge.png) - -This is the first image from the internet! - -![math2001's logo](https://avatars1.githubusercontent.com/u/15224242?s=400&u=53324cf4e303d15032ba53aa41673a2046b3284b&v=4) - -[prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc \ No newline at end of file +this flickering is really annoying... \ No newline at end of file diff --git a/markdown2html.py b/markdown2html.py index ad16814..3d4792c 100644 --- a/markdown2html.py +++ b/markdown2html.py @@ -62,14 +62,16 @@ def markdown2html(markdown, basepath, re_render): img_element['src'] = base64 + # remove comments, because they pollute the console with error messages + for comment_element in soup.find_all(text=lambda text: isinstance(text, bs4.Comment)): + comment_element.extract() + # FIXME: how do tables look? should we use ascii tables? # FIXME: pre aren't handled by ST3. The require manual adjustment # FIXME: include a stylesheet - # FIXME: remove the comments, because they pollute the console with error messages - return str(soup) def get_base64_image(path, re_render): From 5f2cac54e8688b563c83936bdedf9a931abc8dd0 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Fri, 15 Nov 2019 07:21:41 +1100 Subject: [PATCH 12/20] use a resource system to load images and stylesheet --- MarkdownLivePreview.py | 10 +++++++--- live-testing/images.md | 2 +- markdown2html.py | 15 ++++++--------- resources.py | 17 +++++++++++++++++ resources/404.base64 | 1 + resources/404.png | Bin 0 -> 5938 bytes resources/convertresources.py | 12 ++++++++++++ resources/loading.base64 | 1 + resources/loading.png | Bin 0 -> 953 bytes resources/stylesheet.css | 3 +++ 10 files changed, 48 insertions(+), 13 deletions(-) create mode 100644 resources.py create mode 100644 resources/404.base64 create mode 100644 resources/404.png create mode 100644 resources/convertresources.py create mode 100644 resources/loading.base64 create mode 100644 resources/loading.png create mode 100644 resources/stylesheet.css diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 5bc40fb..5f99e63 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -6,6 +6,7 @@ from functools import partial from .markdown2html import markdown2html from .utils import * +from .resources import resources def plugin_loaded(): pass @@ -52,7 +53,6 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): # FIXME: save the document to a temporary file, so that if we crash, # the user doesn't lose what he wrote - sublime.run_command('new_window') preview_window = sublime.active_window() @@ -183,8 +183,12 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): markdown = markdown_view.substr(total_region) basepath = os.path.dirname(markdown_view.file_name()) - html = markdown2html(markdown, basepath, partial(self._update_preview, - markdown_view)) + html = markdown2html( + markdown, + basepath, + partial(self._update_preview, markdown_view), + resources + ) self.phantom_sets[markdown_view.id()].update([ sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, diff --git a/live-testing/images.md b/live-testing/images.md index 268e112..734f94e 100644 --- a/live-testing/images.md +++ b/live-testing/images.md @@ -3,7 +3,7 @@ I'm not sure that it **actually** going to work, but it seems nicer than the [pr This is the first image from the local file system (absolute path, sorry, it's not going to work on your system unless your username is math2001): -![The sublime text logo!](file:///home/math2001/.config/sublime-text-3/Packages/MarkdownLivePreview2/live-testing/sublime_text.png) +![The sublime text logo!](file:///home/math2001/.config/sublime-text-3/Packages/MarkdownLivePreview/live-testing/sublime_text.png) This is the first image from the local file system, *relative* path! diff --git a/markdown2html.py b/markdown2html.py index 3d4792c..04430ca 100644 --- a/markdown2html.py +++ b/markdown2html.py @@ -16,16 +16,12 @@ markdowner = Markdown(extras=['fenced-code-blocks']) # does it stupidly throw them out? (we could implement something of our own) executor = concurrent.futures.ThreadPoolExecutor(max_workers=5) -# FIXME: put a nice picture please :^) -BASE64_LOADING_IMAGE = 'loading image!' -BASE64_404_IMAGE = '404 not found :-(' - images_cache = {} class LoadingError(Exception): pass -def markdown2html(markdown, basepath, re_render): +def markdown2html(markdown, basepath, re_render, resources): """ converts the markdown to html, loads the images and puts in base64 for sublime to understand them correctly. That means that we are responsible for loading the images from the internet. Hence, we take in re_render, which is just a function we @@ -55,10 +51,9 @@ def markdown2html(markdown, basepath, re_render): try: base64 = get_base64_image(path, re_render) except FileNotFoundError as e: - base64 = BASE64_404_IMAGE + base64 = resources['base64_404_image'] except LoadingError: - # the image is loading - base64 = BASE64_LOADING_IMAGE + base64 = resources['base64_loading_image'] img_element['src'] = base64 @@ -72,7 +67,7 @@ def markdown2html(markdown, basepath, re_render): # FIXME: include a stylesheet - return str(soup) + return "<style>\n{}\n</style>\n\n{}".format(resources['stylesheet'], soup) def get_base64_image(path, re_render): @@ -93,6 +88,8 @@ def get_base64_image(path, re_render): executor.submit(load_image, path).add_done_callback(partial(callback, path)) raise LoadingError() + # FIXME: use some kind of cache for this as well, because it decodes on every + # keystroke here... with open(path, 'rb') as fp: return 'data:image/png;base64,' + base64.b64encode(fp.read()).decode('utf-8') diff --git a/resources.py b/resources.py new file mode 100644 index 0000000..cecc707 --- /dev/null +++ b/resources.py @@ -0,0 +1,17 @@ +import os.path +import sublime + +def get_resource(resource): + path = 'Packages/MarkdownLivePreview/resources/' + resource + abs_path = os.path.join(sublime.packages_path(), '..', path) + if os.path.isfile(abs_path): + with open(abs_path, 'r') as fp: + return fp.read() + return sublime.load_resource(path) + +resources = {} + +def plugin_loaded(): + resources["base64_loading_image"] = get_resource('loading.base64') + resources["base64_404_image"] = get_resource('404.base64') + resources["stylesheet"] = get_resource('stylesheet.css') diff --git a/resources/404.base64 b/resources/404.base64 new file mode 100644 index 0000000..d82c610 --- /dev/null +++ b/resources/404.base64 @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/resources/404.png b/resources/404.png new file mode 100644 index 0000000000000000000000000000000000000000..af1ef6a23a4769907969e3fa8b9d89bbb069281a GIT binary patch literal 5938 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 UhYuZJ0ssI207*qoM6N<$f*Xud2mk;8 literal 0 HcmV?d00001 diff --git a/resources/convertresources.py b/resources/convertresources.py new file mode 100644 index 0000000..eb78792 --- /dev/null +++ b/resources/convertresources.py @@ -0,0 +1,12 @@ +""" A small script to convert the images into base64 data """ + +# FIXME: ignore this script and the original images in .gitignore so that it pushes to +# the GitHub repository but not package control + +from base64 import b64encode + +with open('404.png', 'rb') as png, open('404.base64', 'wb') as base64: + base64.write(b64encode(png.read())) + +with open('loading.png', 'rb') as png, open('loading.base64', 'wb') as base64: + base64.write(b64encode(png.read())) diff --git a/resources/loading.base64 b/resources/loading.base64 new file mode 100644 index 0000000..3fcbe3d --- /dev/null +++ b/resources/loading.base64 @@ -0,0 +1 @@ +R0lGODlhZABkAPU+AAAAAAwNDRweHyYpKzg8Pzo+QUBFSERJTEdMT05UV1NYXFVbX1hfY1lfZGFobWJpbmhvdGxzeHF5fnJ6gHV9g3Z+hHqDiXuEin+IjoCIjoKLkYKMkoSNk4eQl4iSmIqTmouUm42XnY+ZoJKco5OdpJOepZSeppahqJeiqZmjqpumrZ6psJ+qsqOutqSvt6SwuKezu6i0vKm1vay4wK66wq+7w6+8xLK+xrK/x7TAybXCy7bDy7jEzbjFzrzJ0gAAACH5BAUAAD4AIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAZABkAAAG/kCfcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiW8IAAAUilUpjQABkEsmMUchkwBIOTOQBQICGUabk0ctFhYdiSajAgOZRKeNRjkYqxaghyuwAgxFtZ1FJBe6NokHvya0nEUzuhYgijG/B86oRCDSOZAPv6VCw0SquiiWNwOwAzfjz0I8uasYPIMvDQ0kRhm/Ee/afKiQ1sIIDBAgkuUxQKDhA29ERMHK9GJSJR85pLUiwkOELgx6Goo0sG/IK1gVhCig9MjHimOreAmBMU+XngciRTrAMSQB/qxmR6KtEjGko7Shey7kbGgA6A0GBz4oOUjCno8YNXWp6NOCwVICD6UYPQqiBiANDHNOkILiqIYVg2Y0yPlAikddICASQtuwJJQY9OAimqFCZpRPei0pPnKjg4fHkB936JBYyg4VmDNrVlH5zYMFoEOLZgDBSoejqDfQEc1atBXUsOl8bi26bpUNsKWpnlPjg+PIj32brZJjs/HOi5PjiMFzCo4ZyAWpqCBhwgspMFa9NZRjg4TvEjZCEQFzWvQ9KiiA/+73SVtpGAT7mcFh/XcPVqH0uCsNhDs+J9gnAQXX+cADCSDMggRVVtGE2lZ6fCAgfkPcdYFhRAhlAVHxxfCnC4d42EdghtII1hYGLgjxki6GOSiNHtR990F+QpymizcZ0SNEjquI1+FHetDHQYFEuCANhBpaMMRAuqRYxEEJDSLPR1YlWVRN9Vjy3ioFCWHlEC6Uh44iOcB0gQck2kSEB90o4sEFx1yY5irQ9JdIDdIANcSXRBiDzGAfVcbnELiwmEgHx3Q5p5JGmOPjIdAF9eIRnyRnhA1AWvqEn4pq6umnoIYq6qiklmrqqaimquqqrLbq6quwxirrrLTWauuttwYBADs= \ No newline at end of file diff --git a/resources/loading.png b/resources/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..183ae2d99189f8eedec016fc9fda9d772a909048 GIT binary patch literal 953 zcmV;q14jHuNk%w1VPpVg0QEiq0000C4GkO~A0{a)I6OZ(K0!c5NJL3YM@&ynR9912 zTvc0NSYKmVUu0ouZDMI|Xm50Eb9ixiesX$%b$x?&euR31iF<^Ke~6BNh>n7bk%Ekp zgpHGjke7&(n2M8{i<Fy<mz|H9ppu-UlbxiKo~4wYrk0_om!heeqpF*xt)8i{pQ^H> zuC}DFx1_MRr?b1Lw7jXcy{x#vuDZgnyTh-%#InA|vcJc)z{$13%eKSIxWvu4#m>CR z(f|MeA^8La002G!A^!_bMO0HmK~P09E-(WD0000i00000WB_CU00#a*pKwSl8jr}N za>;BupU|juO08P2*sONT?Rvl9uy{-^o6qR9dd+UT-|)D6POsbV_`H74@B9CNfr5jC zg@%WSiEju102GQ<DUARDkV_^pM<J5{NI5f*1p)#YMw^pIEfy9Xi6)~01DQmpjYc^b zs}`V#E3g6#MYWwpBp13ii3h(Xw46mVx)vacF~0}Ss6-&rIgk&(r9#6*s=6qaHv_N( zH{;JjJh`hFJcBO{4J1YxzY*`+e5jDt!UzlyAd=-VK%n6RZ$w1F%JgDVB_BDYwIaep zgf0wv8j3WqZ^$cH6@)0D^vK7GW2<-xfiX{)o(CdDI>0d`0sgFJN209~F{Gokp?fam zY-j-J4F(53C^<;No{tzcb*bpng27S(52YAA3Ze!e4GeQmkb>f<h82TmG|2ftibq`_ zAQD2`uq2ci^xz_zp+aVqPkJpWK60ajhsTf~cj%B<$_^EnGiz2+`OSj`phJsh0KrO! zqo_B~5zSh(71Xfld~PkeZj}wNrKz4%<AdYKkA0i1l4G;+&Wn@dh{1CTjv2@Wsi+`f z!V4)dR=qWpV}pe65;j5+0drdPJt`=G|L*rmTWJ^s`<cOE{dW&mq4c;b4TL*BC)g(e z1=sn&0|_9^f&^7o(S+JodVC-tenZ@Kfnh`lWdTv~#qg&Khj`dWV208N))odX2=S6G zhB+vW9@KsJL4HD<ri(Wj(IZ5TtBBX(M|#l5L4icLAPs~XS}?>wx};ddK?w~a&qr1z zSyWB+Sn}Q~1qos05H6I5jv_h0bb$vX+C+p0-6-P01;<>N<|@$imq-oL05!yyL>Pn2 zU>{Y+=McECm`Df5bUCMzMwsK{A<zZ&;t`)DXM_zvTKa^aifZcVr=W%^>ZqiaYU-(| brmE_ythVavtFXo@>#VfaYU{1H1_1y&aJ8`j literal 0 HcmV?d00001 diff --git a/resources/stylesheet.css b/resources/stylesheet.css new file mode 100644 index 0000000..12780e3 --- /dev/null +++ b/resources/stylesheet.css @@ -0,0 +1,3 @@ +body { + font-family: "Open Sans", sans-serif, serif; +} \ No newline at end of file From c0c9867cc89efb2d39cc1d843c98b6e03d375174 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Fri, 15 Nov 2019 13:33:58 +1100 Subject: [PATCH 13/20] move resources.py to MarkdownLivePreview.py Having an extra file is just a pain because you have to save it manually for ST to reload, and it was small and created way more trouble than it needed to... It fits really nicely in there anyway And add some nice CSS --- MarkdownLivePreview.py | 27 ++++++++++++++++++++++---- live-testing/test.md | 9 ++++++--- resources.py | 17 ----------------- resources/stylesheet.css | 41 ++++++++++++++++++++++++++++++++++++++-- 4 files changed, 68 insertions(+), 26 deletions(-) delete mode 100644 resources.py diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 5f99e63..f1f3064 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -6,16 +6,33 @@ from functools import partial from .markdown2html import markdown2html from .utils import * -from .resources import resources - -def plugin_loaded(): - pass MARKDOWN_VIEW_INFOS = "markdown_view_infos" PREVIEW_VIEW_INFOS = "preview_view_infos" # FIXME: put this as a setting for the user to choose? DELAY = 500 # ms +def get_resource(resource): + path = 'Packages/MarkdownLivePreview/resources/' + resource + abs_path = os.path.join(sublime.packages_path(), '..', path) + if os.path.isfile(abs_path): + with open(abs_path, 'r') as fp: + return fp.read() + return sublime.load_resource(path) + +resources = {} + +def plugin_loaded(): + resources["base64_loading_image"] = get_resource('loading.base64') + resources["base64_404_image"] = get_resource('404.base64') + resources["stylesheet"] = get_resource('stylesheet.css') + +# try to reload the resources if we save this file +try: + plugin_loaded() +except OSError: + pass + # Terminology # original_view: the view in the regular editor, without it's own window # markdown_view: the markdown view, in the special window @@ -190,6 +207,8 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): resources ) + print(html) + self.phantom_sets[markdown_view.id()].update([ sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, lambda href: sublime.run_command('open_url', {'url': href})) diff --git a/live-testing/test.md b/live-testing/test.md index 24d3981..cc7fdde 100644 --- a/live-testing/test.md +++ b/live-testing/test.md @@ -1,12 +1,15 @@ # hello world -This is a *test*. asdfas +This is a *test*. Some inline `[2]code()`. -``` +```python import this # you should try this ``` <!-- some comment --> -this flickering is really annoying... \ No newline at end of file +this flickering is really annoying... + + +It looks like it's gone... Oh wait nah, it's still here... \ No newline at end of file diff --git a/resources.py b/resources.py deleted file mode 100644 index cecc707..0000000 --- a/resources.py +++ /dev/null @@ -1,17 +0,0 @@ -import os.path -import sublime - -def get_resource(resource): - path = 'Packages/MarkdownLivePreview/resources/' + resource - abs_path = os.path.join(sublime.packages_path(), '..', path) - if os.path.isfile(abs_path): - with open(abs_path, 'r') as fp: - return fp.read() - return sublime.load_resource(path) - -resources = {} - -def plugin_loaded(): - resources["base64_loading_image"] = get_resource('loading.base64') - resources["base64_404_image"] = get_resource('404.base64') - resources["stylesheet"] = get_resource('stylesheet.css') diff --git a/resources/stylesheet.css b/resources/stylesheet.css index 12780e3..ac04e54 100644 --- a/resources/stylesheet.css +++ b/resources/stylesheet.css @@ -1,3 +1,40 @@ +html { + --light-bg: color(var(--background) blend(#fff 90%)); + --very-light-bg: color(var(--background) blend(#fff 85%)); +} + body { - font-family: "Open Sans", sans-serif, serif; -} \ No newline at end of file + font-family: "Ubuntu", "DejaVu Sans", "Open Sans", sans-serif; +} + +blockquote { + font-style: italic; + display: block; + margin-left: 30px; +} + +pre { + display: block; + background-color: var(--very-light-bg); +} + +code { + padding-left: 0.2rem; + padding-right: 0.2rem; + margin: 0; + border-radius: 3px; + background-color: red; +} + +code { + padding-left: 0.2rem; + padding-right: 0.2rem; + background-color: var(--very-light-bg); + margin: 0; + border-radius: 3px; +} + +pre code { + padding: 0; +} + From 6016f07cd16f2a15b95ab595bde45bce43f0b253 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Fri, 15 Nov 2019 14:52:20 +1100 Subject: [PATCH 14/20] add line breaks for pre --- MarkdownLivePreview.py | 2 +- live-testing/test.md | 5 +++-- markdown2html.py | 11 ++++++++++- 3 files changed, 14 insertions(+), 4 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index f1f3064..980a064 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -207,7 +207,7 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): resources ) - print(html) + # print(html) self.phantom_sets[markdown_view.id()].update([ sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, diff --git a/live-testing/test.md b/live-testing/test.md index cc7fdde..d829260 100644 --- a/live-testing/test.md +++ b/live-testing/test.md @@ -2,14 +2,15 @@ This is a *test*. Some inline `[2]code()`. +what the hell... + ```python import this # you should try this ``` -<!-- some comment --> - this flickering is really annoying... +this It looks like it's gone... Oh wait nah, it's still here... \ No newline at end of file diff --git a/markdown2html.py b/markdown2html.py index 04430ca..9b33dfb 100644 --- a/markdown2html.py +++ b/markdown2html.py @@ -1,3 +1,4 @@ +import copy import os.path import concurrent.futures import urllib.request @@ -65,7 +66,15 @@ def markdown2html(markdown, basepath, re_render, resources): # FIXME: pre aren't handled by ST3. The require manual adjustment - # FIXME: include a stylesheet + br = soup.new_tag('br') + for pre_element in soup.find_all('pre'): + # select the first child, <code> + code_element = next(pre_element.children) + # FIXME: this line sucks, but can we do better? + code_element.replace_with( + bs4.BeautifulSoup(str(code_element).replace('\n', '<br>'), "html.parser")) + + # FIXME: highlight the code using Sublime's syntax return "<style>\n{}\n</style>\n\n{}".format(resources['stylesheet'], soup) From e3896a6b3d93c06dff28e042bd09c095fddb9a3d Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Fri, 15 Nov 2019 15:02:31 +1100 Subject: [PATCH 15/20] fix indentation in pre in preview Again, this is a dodgy hack: replace spaces with dot with the color exactly like the background, because otherwise ST ignores it, even in pre --- MarkdownLivePreview.py | 2 +- live-testing/test.md | 4 +++- markdown2html.py | 14 ++++++++------ resources/stylesheet.css | 3 +++ 4 files changed, 15 insertions(+), 8 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 980a064..f7d5f78 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -207,7 +207,7 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): resources ) - # print(html) + print(repr(html)) self.phantom_sets[markdown_view.id()].update([ sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, diff --git a/live-testing/test.md b/live-testing/test.md index d829260..609f165 100644 --- a/live-testing/test.md +++ b/live-testing/test.md @@ -6,7 +6,9 @@ what the hell... ```python import this -# you should try this + +if input("answer yes") != 'yes': + print("Really?") ``` this flickering is really annoying... diff --git a/markdown2html.py b/markdown2html.py index 9b33dfb..2468b1f 100644 --- a/markdown2html.py +++ b/markdown2html.py @@ -64,15 +64,17 @@ def markdown2html(markdown, basepath, re_render, resources): # FIXME: how do tables look? should we use ascii tables? - # FIXME: pre aren't handled by ST3. The require manual adjustment - - br = soup.new_tag('br') + # pre aren't handled by ST3. The require manual adjustment for pre_element in soup.find_all('pre'): # select the first child, <code> code_element = next(pre_element.children) - # FIXME: this line sucks, but can we do better? - code_element.replace_with( - bs4.BeautifulSoup(str(code_element).replace('\n', '<br>'), "html.parser")) + + # FIXME: this method sucks, but can we do better? + fixed_pre = str(code_element) \ + .replace('\n', '<br>')\ + .replace(' ', '<i class="space">.</i>') + + code_element.replace_with(bs4.BeautifulSoup(fixed_pre, "html.parser")) # FIXME: highlight the code using Sublime's syntax diff --git a/resources/stylesheet.css b/resources/stylesheet.css index ac04e54..f8c119d 100644 --- a/resources/stylesheet.css +++ b/resources/stylesheet.css @@ -38,3 +38,6 @@ pre code { padding: 0; } +i.space { + color: var(--very-light-bg); +} \ No newline at end of file From 0dea8afba40719442d866925a33a2cc75f014ee2 Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Fri, 15 Nov 2019 15:19:15 +1100 Subject: [PATCH 16/20] fix clipping of files after pre The aim is to replace every \n in a <pre> with a <br /> because st doesn't support pre. However, ST doesn't support <br/> for some reason, only <br> or <br />. We use to add some <br>s, but BeautifulSoup automatically adds a <br/> when it sees a <br> (close the tag), which causes the clipping of the rest of the file by ST. But if we replace every \n with a <br />, BeautifulSoup automatically replaces it with <br/> (= ST bug) So, we do exactly that, except that at the very end, when markdown2html returns, we replace every <br/> with a <br /> --- MarkdownLivePreview.py | 2 -- markdown2html.py | 7 ++++--- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index f7d5f78..ab7fa10 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -207,8 +207,6 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): resources ) - print(repr(html)) - self.phantom_sets[markdown_view.id()].update([ sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK, lambda href: sublime.run_command('open_url', {'url': href})) diff --git a/markdown2html.py b/markdown2html.py index 2468b1f..d053da6 100644 --- a/markdown2html.py +++ b/markdown2html.py @@ -71,14 +71,15 @@ def markdown2html(markdown, basepath, re_render, resources): # FIXME: this method sucks, but can we do better? fixed_pre = str(code_element) \ - .replace('\n', '<br>')\ - .replace(' ', '<i class="space">.</i>') + .replace(' ', '<i class="space">.</i>') \ + .replace('\n', '<br />') code_element.replace_with(bs4.BeautifulSoup(fixed_pre, "html.parser")) # FIXME: highlight the code using Sublime's syntax - return "<style>\n{}\n</style>\n\n{}".format(resources['stylesheet'], soup) + # FIXME: report that ST doesn't support <br/> but does work with <br />... WTF? + return "<style>\n{}\n</style>\n\n{}".format(resources['stylesheet'], soup).replace('<br/>', '<br />') def get_base64_image(path, re_render): From c14c28b56bfef3b8b4310cf1d50302563a3ea69f Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Fri, 15 Nov 2019 16:10:50 +1100 Subject: [PATCH 17/20] fix the flickering by delaying and throwing Explanation coming on my blog, math2001.github.io, something like "lessons learned from re writing MarkdownLivePreview" --- MarkdownLivePreview.py | 18 +++++++++++++----- live-testing/images.md | 2 ++ live-testing/test.md | 6 ++---- resources/stylesheet.css | 3 ++- 4 files changed, 19 insertions(+), 10 deletions(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index ab7fa10..320a327 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -10,7 +10,7 @@ from .utils import * MARKDOWN_VIEW_INFOS = "markdown_view_infos" PREVIEW_VIEW_INFOS = "preview_view_infos" # FIXME: put this as a setting for the user to choose? -DELAY = 500 # ms +DELAY = 100 # ms def get_resource(resource): path = 'Packages/MarkdownLivePreview/resources/' + resource @@ -102,6 +102,7 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): # FIXME: is this the best way there is to check if the current syntax is markdown? # should we only support default markdown? # what about "md"? + # FIXME: what about other languages, where markdown preview roughly works? return 'markdown' in self.view.settings().get('syntax').lower() class MarkdownLivePreviewListener(sublime_plugin.EventListener): @@ -110,6 +111,10 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): # markdown_view.id(): phantom set } + # we schedule an update for every key stroke, with a delay of DELAY + # then, we update only if now() - last_update > DELAY + last_update = 0 + # FIXME: maybe we shouldn't restore the file in the original window... def on_pre_close(self, markdown_view): @@ -124,7 +129,6 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): self.file_name = markdown_view.file_name() if self.file_name is None: - # FIXME: this is duplicated code. How should it be generalized? total_region = sublime.Region(0, markdown_view.size()) self.content = markdown_view.substr(total_region) markdown_view.erase(edit, total_region) @@ -180,22 +184,26 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): # @min_time_between_call(.5) def on_modified_async(self, markdown_view): - # FIXME: it keeps on flickering, it's really annoying - infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS) if not infos: return - self._update_preview(markdown_view) + # we schedule an update, which won't run if an + sublime.set_timeout(partial(self._update_preview, markdown_view), DELAY) def _update_preview(self, markdown_view): # if the buffer id is 0, that means that the markdown_view has been closed # This check is needed since a this function is used as a callback for when images # are loaded from the internet (ie. it could finish loading *after* the user # closes the markdown_view) + if time.time() - self.last_update < DELAY / 1000: + return + if markdown_view.buffer_id() == 0: return + self.last_update = time.time() + total_region = sublime.Region(0, markdown_view.size()) markdown = markdown_view.substr(total_region) diff --git a/live-testing/images.md b/live-testing/images.md index 734f94e..05b7958 100644 --- a/live-testing/images.md +++ b/live-testing/images.md @@ -1,5 +1,7 @@ I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev] +this is a test, hello world + This is the first image from the local file system (absolute path, sorry, it's not going to work on your system unless your username is math2001): diff --git a/live-testing/test.md b/live-testing/test.md index 609f165..773c03f 100644 --- a/live-testing/test.md +++ b/live-testing/test.md @@ -1,4 +1,4 @@ -# hello world +# hello world This is a *test*. Some inline `[2]code()`. @@ -13,6 +13,4 @@ if input("answer yes") != 'yes': this flickering is really annoying... -this - -It looks like it's gone... Oh wait nah, it's still here... \ No newline at end of file +It looks like it's gone... Oh wait nah, it's still here... diff --git a/resources/stylesheet.css b/resources/stylesheet.css index f8c119d..fb94801 100644 --- a/resources/stylesheet.css +++ b/resources/stylesheet.css @@ -4,7 +4,8 @@ html { } body { - font-family: "Ubuntu", "DejaVu Sans", "Open Sans", sans-serif; + font-family: "Ubuntu", "DejaVu Sans", "Open Sans", sans-serif; + font-size: 15px; } blockquote { From 0f5630c3dcf492f20d61cc4c2a1316b48089bcbd Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Sat, 16 Nov 2019 07:53:30 +1100 Subject: [PATCH 18/20] Set the preview's name (in the tab) just a one liner --- MarkdownLivePreview.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MarkdownLivePreview.py b/MarkdownLivePreview.py index 320a327..3b09249 100644 --- a/MarkdownLivePreview.py +++ b/MarkdownLivePreview.py @@ -83,6 +83,7 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand): preview_view = preview_window.new_file() preview_view.set_scratch(True) preview_view.settings().set(PREVIEW_VIEW_INFOS, {}) + preview_view.set_name('Preview') preview_window.focus_group(0) @@ -142,7 +143,6 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener): preview_view = markdown_view.window().active_view_in_group(1) - # FIXME: set the preview title self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view) self._update_preview(markdown_view) From eaa357a65f01f94aa3b05e67da80ad3f6b64728a Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Sat, 16 Nov 2019 08:28:08 +1100 Subject: [PATCH 19/20] don't export png's and compilers to the final users Though they are still available to the devs, as they are just `export-ignore`d in the .gitattributes. I've done a little bit of research, and this attribute prevents the files from being archived by git. So, I'm assuming that package control fetches archives of the packages, and not the actual ones. But I haven't found any documentation on packagecontrol.io stating that. I quickly browsed its source code on GitHub, and couldn't really find anything. So, I'm going to contact @jfcherng, who showed me this trick on FileManager. --- .gitattributes | 3 +++ resources/convertresources.py | 3 --- 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 .gitattributes diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..dcc40d8 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +docs/ export-ignore +resources/*.png export-ignore +resources/*.py export-ignore \ No newline at end of file diff --git a/resources/convertresources.py b/resources/convertresources.py index eb78792..fdcd878 100644 --- a/resources/convertresources.py +++ b/resources/convertresources.py @@ -1,8 +1,5 @@ """ A small script to convert the images into base64 data """ -# FIXME: ignore this script and the original images in .gitignore so that it pushes to -# the GitHub repository but not package control - from base64 import b64encode with open('404.png', 'rb') as png, open('404.base64', 'wb') as base64: From 9ad3f25d14103cd1e809c7538baa96c15ba9566d Mon Sep 17 00:00:00 2001 From: Mathieu PATUREL <australie.p@gmail.com> Date: Sat, 16 Nov 2019 09:04:14 +1100 Subject: [PATCH 20/20] add license --- LICENSE | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 LICENSE diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..9620bc3 --- /dev/null +++ b/LICENSE @@ -0,0 +1,7 @@ +Copyright 2017 Mathieu PATUREL + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.