Compare commits
87 Commits
v2.1.0
...
c6ac821c4a
| Author | SHA1 | Date | |
|---|---|---|---|
| c6ac821c4a | |||
| 9ad3f25d14 | |||
| eaa357a65f | |||
| 0f5630c3dc | |||
| c14c28b56b | |||
| 0dea8afba4 | |||
| e3896a6b3d | |||
| 6016f07cd1 | |||
| c0c9867cc8 | |||
| 5f2cac54e8 | |||
| 8c1012eb8c | |||
| cc28bfef96 | |||
| ef9b2daf6d | |||
| bae26fc452 | |||
| 5738f6b5ff | |||
| 6bb8e6ebaa | |||
| 8eb6882d60 | |||
| 61cf2984eb | |||
| 7f7dcd6ba8 | |||
| d3d88ddb49 | |||
| 9a8ac3886e | |||
| d4c477749c | |||
| 79c785176f | |||
| 82ad98085f | |||
| dd184c5fdd | |||
| c334c49592 | |||
| 41c28e2b24 | |||
| e1eb17fe96 | |||
| 823d22afee | |||
| 91f4bc5052 | |||
| 7126c0e090 | |||
| 6a3dd6ac2f | |||
| 1542e5e898 | |||
| 05c471b5d9 | |||
| 76f580ba29 | |||
| 119acbb092 | |||
| 7c4354fb2e | |||
| b93aea6698 | |||
| b3fb5779d3 | |||
| 7bdda5f5c7 | |||
| 7257cb467e | |||
| 40a563fb1e | |||
| 3e0d6ad265 | |||
| f65a068b4e | |||
| bc328642e7 | |||
| d2053be41e | |||
| eb48b1c79f | |||
| 8317fa738c | |||
| 3be12b0539 | |||
| c92d78fb20 | |||
| 30d75f159d | |||
| 52e4b917e5 | |||
| 48a68b2a79 | |||
| 8eb0172eb4 | |||
| 52e35fb610 | |||
| 84f809e57f | |||
| 351e8bd355 | |||
| 5babc862b4 | |||
| dc7139fbe7 | |||
| bbbeae6fe9 | |||
| 271c7c619a | |||
| 8cc6b2b263 | |||
| c7961ce94c | |||
| eae91fa428 | |||
| 6f18e8e4a2 | |||
| 48c1800065 | |||
| bad1cb74c6 | |||
| 4198504fd1 | |||
| 1bef00de14 | |||
| d707cf7a47 | |||
| c27cd5f210 | |||
| 41bbc3d03d | |||
| 1e651bebc6 | |||
| ea309f2323 | |||
| 3306d6ad5e | |||
| 0505ca30bc | |||
| c49ae26720 | |||
| c2618ead1d | |||
| 6e113fef6e | |||
| e5378e2300 | |||
| caf932b536 | |||
| fa106c8206 | |||
| 0ac9fd9aaa | |||
| 5bbfb4606d | |||
| 37703e9bab | |||
| 3b920f4336 | |||
| abe151fdb7 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
docs/ export-ignore
|
||||||
|
resources/*.png export-ignore
|
||||||
|
resources/*.py export-ignore
|
||||||
4
.gitignore
vendored
4
.gitignore
vendored
@ -1,3 +1 @@
|
|||||||
Thumbs.db
|
__pycache__
|
||||||
__pycache__/
|
|
||||||
cache.txt
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"keys": ["alt+m"],
|
|
||||||
"command": "new_markdown_live_preview",
|
|
||||||
"context": [
|
|
||||||
{
|
|
||||||
"key": "selector",
|
|
||||||
"operator": "equal",
|
|
||||||
"operand": "text.html.markdown"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,20 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "preferences",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "package-settings",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview",
|
|
||||||
"command": "open_file",
|
|
||||||
"args": {
|
|
||||||
"file": "$packages/User/MarkdownLivePreview.css",
|
|
||||||
"contents": "/* See http://www.sublimetext.com/docs/3/minihtml.html#css to know which property you're able to use */\n\n$0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,18 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview: Edit Current File",
|
|
||||||
"command": "new_markdown_live_preview"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview: Clear Cache",
|
|
||||||
"command": "markdown_live_preview_clear_cache"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"caption": "Preferences: MarkdownLivePreview Settings",
|
|
||||||
"command": "edit_settings",
|
|
||||||
"args": {
|
|
||||||
"base_file": "${packages}/MarkdownLivePreview/.sublime/MarkdownLivePreview.sublime-settings",
|
|
||||||
"default": "// Your settings for MarkdownLivePreview. See the default file to see the different options. \n{\n\t$0\n}\n"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,4 +0,0 @@
|
|||||||
{
|
|
||||||
"markdown_live_preview_on_open": false,
|
|
||||||
"load_from_internet_when_starts": ["http://", "https://"]
|
|
||||||
}
|
|
||||||
@ -1,14 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>name</key>
|
|
||||||
<string>MarkdownLivePreviewSyntax</string>
|
|
||||||
|
|
||||||
<key>patterns</key>
|
|
||||||
<array>
|
|
||||||
</array>
|
|
||||||
<key>scopeName</key>
|
|
||||||
<string>text.markdown-live-preview</string>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@ -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.
|
||||||
89
MLPApi.py
89
MLPApi.py
@ -1,89 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sublime
|
|
||||||
import sublime_plugin
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
|
|
||||||
from .lib import markdown2 as md2
|
|
||||||
from .escape_amp import *
|
|
||||||
from .functions import *
|
|
||||||
from .setting_names import *
|
|
||||||
from .image_manager import CACHE_FILE
|
|
||||||
|
|
||||||
__folder__ = os.path.dirname(__file__)
|
|
||||||
|
|
||||||
STYLE_FILE = os.path.join(os.path.dirname(__folder__), 'User',
|
|
||||||
'MarkdownLivePreview.css')
|
|
||||||
|
|
||||||
|
|
||||||
def plugin_loaded():
|
|
||||||
global DEFAULT_STYLE_FILE
|
|
||||||
DEFAULT_STYLE_FILE = sublime.load_resource('Packages/MarkdownLivePreview/'
|
|
||||||
'default.css')
|
|
||||||
|
|
||||||
def get_preview_name(md_view):
|
|
||||||
file_name = md_view.file_name()
|
|
||||||
name = md_view.name() \
|
|
||||||
or os.path.basename(file_name) if file_name else None \
|
|
||||||
or 'Untitled'
|
|
||||||
return name + ' - Preview'
|
|
||||||
|
|
||||||
def create_preview(window, file_name):
|
|
||||||
preview = window.new_file()
|
|
||||||
|
|
||||||
preview.set_name(get_preview_name(file_name))
|
|
||||||
preview.set_scratch(True)
|
|
||||||
preview.set_syntax_file('Packages/MarkdownLivePreview/.sublime/'
|
|
||||||
'MarkdownLivePreviewSyntax.hidden-tmLanguage')
|
|
||||||
|
|
||||||
return preview
|
|
||||||
|
|
||||||
def get_style():
|
|
||||||
content = ''.join([line.strip() + ' ' for line in DEFAULT_STYLE_FILE.splitlines()])
|
|
||||||
return content + "pre code .space {color: var(--light-bg)}"
|
|
||||||
|
|
||||||
def show_html(md_view, preview):
|
|
||||||
html = []
|
|
||||||
html.append('<style>\n{}\n</style>'.format(get_style()))
|
|
||||||
html.append(pre_with_br(md2.markdown(get_view_content(md_view),
|
|
||||||
extras=['fenced-code-blocks',
|
|
||||||
'no-code-highlighting'])))
|
|
||||||
# the option no-code-highlighting does not exists
|
|
||||||
# in the official version of markdown2 for now
|
|
||||||
# I personaly edited the file (markdown2.py:1743)
|
|
||||||
html = '\n'.join(html)
|
|
||||||
|
|
||||||
html = html.replace(' ', ' espace;') # save where are the spaces
|
|
||||||
|
|
||||||
html = HTMLParser().unescape(html)
|
|
||||||
|
|
||||||
html = escape_amp(html)
|
|
||||||
|
|
||||||
# exception, again, because <pre> aren't supported by the phantoms
|
|
||||||
html = html.replace(' espace;', '<i class="space">.</i>')
|
|
||||||
html = replace_img_src_base64(html, basepath=os.path.dirname(
|
|
||||||
md_view.file_name()))
|
|
||||||
preview.erase_phantoms('markdown_preview')
|
|
||||||
preview.add_phantom('markdown_preview',
|
|
||||||
sublime.Region(-1),
|
|
||||||
html,
|
|
||||||
sublime.LAYOUT_BLOCK,
|
|
||||||
lambda href: sublime.run_command('open_url',
|
|
||||||
{'url': href}))
|
|
||||||
|
|
||||||
return
|
|
||||||
# set viewport position
|
|
||||||
# 0 < y < 1
|
|
||||||
y = md_view.text_to_layout(md_view.sel()[0].begin())[1] / md_view.layout_extent()[1]
|
|
||||||
vector = [0, y * preview.layout_extent()[1]]
|
|
||||||
# remove half of the viewport_extent.y to center it on the screen (verticaly)
|
|
||||||
vector[1] -= preview.viewport_extent()[1] / 2
|
|
||||||
vector[1] = mini(vector[1], 0)
|
|
||||||
vector[1] += preview.line_height()
|
|
||||||
preview.set_viewport_position(vector, animate=False)
|
|
||||||
|
|
||||||
def clear_cache():
|
|
||||||
"""Removes the cache file"""
|
|
||||||
os.remove(CACHE_FILE)
|
|
||||||
@ -1,112 +1,223 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
import os.path
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
|
|
||||||
from .MLPApi import *
|
from functools import partial
|
||||||
from .setting_names import *
|
|
||||||
from .functions import *
|
|
||||||
|
|
||||||
class NewMarkdownLivePreviewCommand(sublime_plugin.ApplicationCommand):
|
from .markdown2html import markdown2html
|
||||||
|
from .utils import *
|
||||||
|
|
||||||
def run(self):
|
MARKDOWN_VIEW_INFOS = "markdown_view_infos"
|
||||||
|
PREVIEW_VIEW_INFOS = "preview_view_infos"
|
||||||
|
# FIXME: put this as a setting for the user to choose?
|
||||||
|
DELAY = 100 # ms
|
||||||
|
|
||||||
"""Inspired by the edit_settings command"""
|
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)
|
||||||
|
|
||||||
current_view = sublime.active_window().active_view()
|
resources = {}
|
||||||
file_name = current_view.file_name()
|
|
||||||
current_view.close()
|
def plugin_loaded():
|
||||||
if file_name is None:
|
resources["base64_loading_image"] = get_resource('loading.base64')
|
||||||
return sublime.error_message('MarkdownLivePreview: Not supporting '
|
resources["base64_404_image"] = get_resource('404.base64')
|
||||||
'unsaved file for now')
|
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
|
||||||
|
# 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):
|
||||||
|
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
|
||||||
|
original_window_id = original_view.window().id()
|
||||||
|
file_name = original_view.file_name()
|
||||||
|
|
||||||
|
syntax_file = original_view.settings().get('syntax')
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
sublime.run_command('new_window')
|
sublime.run_command('new_window')
|
||||||
self.window = sublime.active_window()
|
preview_window = sublime.active_window()
|
||||||
self.window.settings().set(PREVIEW_WINDOW, True)
|
|
||||||
self.window.run_command('set_layout', {
|
preview_window.run_command('set_layout', {
|
||||||
'cols': [0.0, 0.5, 1.0],
|
'cols': [0.0, 0.5, 1.0],
|
||||||
'rows': [0.0, 1.0],
|
'rows': [0.0, 1.0],
|
||||||
'cells': [[0, 0, 1, 1], [1, 0, 2, 1]]
|
'cells': [[0, 0, 1, 1], [1, 0, 2, 1]]
|
||||||
})
|
})
|
||||||
self.window.focus_group(1)
|
|
||||||
preview = create_preview(self.window, current_view)
|
|
||||||
|
|
||||||
self.window.focus_group(0)
|
preview_window.focus_group(1)
|
||||||
md_view = self.window.open_file(file_name)
|
preview_view = preview_window.new_file()
|
||||||
mdsettings = md_view.settings()
|
preview_view.set_scratch(True)
|
||||||
|
preview_view.settings().set(PREVIEW_VIEW_INFOS, {})
|
||||||
|
preview_view.set_name('Preview')
|
||||||
|
|
||||||
mdsettings.set(PREVIEW_ENABLED, True)
|
|
||||||
mdsettings.set(PREVIEW_ID, preview.id())
|
preview_window.focus_group(0)
|
||||||
|
if file_name:
|
||||||
|
markdown_view = preview_window.open_file(file_name)
|
||||||
|
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(MARKDOWN_VIEW_INFOS, {
|
||||||
|
"original_window_id": original_window_id
|
||||||
|
})
|
||||||
|
|
||||||
def is_enabled(self):
|
def is_enabled(self):
|
||||||
return is_markdown_view(sublime.active_window().active_view())
|
# 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):
|
class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
||||||
|
|
||||||
def update(self, view):
|
phantom_sets = {
|
||||||
vsettings = view.settings()
|
# markdown_view.id(): phantom set
|
||||||
if not vsettings.get(PREVIEW_ENABLED):
|
}
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
""" Close the view in the preview window, and store information for the on_close
|
||||||
|
listener (see doc there)
|
||||||
|
"""
|
||||||
|
if not markdown_view.settings().get(MARKDOWN_VIEW_INFOS):
|
||||||
return
|
return
|
||||||
id = vsettings.get(PREVIEW_ID)
|
|
||||||
if id is None:
|
|
||||||
raise ValueError('The preview id is None')
|
|
||||||
preview = get_view_from_id(view.window(), id)
|
|
||||||
if preview is None:
|
|
||||||
raise ValueError('The preview is None (id: {})'.format(id))
|
|
||||||
|
|
||||||
show_html(view, preview)
|
self.markdown_view = markdown_view
|
||||||
return view, preview
|
self.preview_window = markdown_view.window()
|
||||||
|
self.file_name = markdown_view.file_name()
|
||||||
|
|
||||||
def on_modified(self, view):
|
if self.file_name is None:
|
||||||
if not is_markdown_view(view): # faster than getting the settings
|
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_load_async(self, markdown_view):
|
||||||
|
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||||
|
if not infos:
|
||||||
return
|
return
|
||||||
self.update(view)
|
|
||||||
|
|
||||||
def on_window_command(self, window, command, args):
|
preview_view = markdown_view.window().active_view_in_group(1)
|
||||||
if command == 'close' and window.settings().get(PREVIEW_WINDOW):
|
|
||||||
return 'close_window', {}
|
|
||||||
|
|
||||||
def on_activated_async(self, view):
|
self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view)
|
||||||
vsettings = view.settings()
|
self._update_preview(markdown_view)
|
||||||
|
|
||||||
if (is_markdown_view(view)
|
def on_close(self, markdown_view):
|
||||||
and get_settings().get('markdown_live_preview_on_open')
|
""" Use the information saved to restore the markdown_view as an original_view
|
||||||
and not vsettings.get(PREVIEW_ENABLED)
|
"""
|
||||||
and vsettings.get('syntax') != 'Packages/MarkdownLivePreview/'
|
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||||
'.sublime/MarkdownLivePreviewSyntax'
|
if not infos:
|
||||||
'.hidden-tmLanguage'):
|
|
||||||
sublime.run_command('new_markdown_live_preview')
|
|
||||||
|
|
||||||
|
|
||||||
def on_load_async(self, view):
|
|
||||||
"""Check the settings to hide menu, minimap, etc"""
|
|
||||||
try:
|
|
||||||
md_view, preview = self.update(view)
|
|
||||||
except TypeError:
|
|
||||||
# the function update has returned None
|
|
||||||
return
|
return
|
||||||
window = preview.window()
|
|
||||||
psettings = preview.settings()
|
|
||||||
|
|
||||||
show_tabs = psettings.get('show_tabs')
|
assert markdown_view.id() == self.markdown_view.id(), \
|
||||||
show_minimap = psettings.get('show_minimap')
|
"pre_close view.id() != close view.id()"
|
||||||
show_status_bar = psettings.get('show_status_bar')
|
|
||||||
show_sidebar = psettings.get('show_sidebar')
|
|
||||||
show_menus = psettings.get('show_menus')
|
|
||||||
|
|
||||||
if show_tabs is not None:
|
del self.phantom_sets[markdown_view.id()]
|
||||||
window.set_tabs_visible(show_tabs)
|
|
||||||
if show_minimap is not None:
|
self.preview_window.run_command('close_window')
|
||||||
window.set_minimap_visible(show_minimap)
|
|
||||||
if show_status_bar is not None:
|
# find the window with the right id
|
||||||
window.set_status_bar_visible(show_status_bar)
|
original_window = next(window for window in sublime.windows() \
|
||||||
if show_sidebar is not None:
|
if window.id() == infos['original_window_id'])
|
||||||
window.set_sidebar_visible(show_sidebar)
|
if self.file_name:
|
||||||
if show_menus is not None:
|
original_window.open_file(self.file_name)
|
||||||
window.set_menu_visible(show_menus)
|
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'))
|
||||||
|
|
||||||
|
|
||||||
class MarkdownLivePreviewClearCacheCommand(sublime_plugin.ApplicationCommand):
|
# 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):
|
||||||
|
|
||||||
def run(self):
|
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||||
clear_cache()
|
if not infos:
|
||||||
|
return
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
basepath = os.path.dirname(markdown_view.file_name())
|
||||||
|
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,
|
||||||
|
lambda href: sublime.run_command('open_url', {'url': href}))
|
||||||
|
])
|
||||||
|
|
||||||
|
|
||||||
7
MarkdownLivePreview.sublime-commands
Normal file
7
MarkdownLivePreview.sublime-commands
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
|
||||||
|
{
|
||||||
|
"caption": "MarkdownLivePreview: Open Preview",
|
||||||
|
"command": "open_markdown_preview"
|
||||||
|
}
|
||||||
|
]
|
||||||
@ -1,36 +0,0 @@
|
|||||||
Fast:
|
|
||||||
|
|
||||||
☐ sync scroll @needsUpdate(because of images)
|
|
||||||
☐ cache image in object when used, so that it's faster @needsTest
|
|
||||||
☐ add clear cache command
|
|
||||||
☐ update README for settings in view
|
|
||||||
☐ add edit settings
|
|
||||||
|
|
||||||
Medium:
|
|
||||||
☐ auto refresh preview if loading images
|
|
||||||
☐ use alt attribute for 404 error
|
|
||||||
☐ use MarkdownLivePreview syntax, so we can use syntax's settings
|
|
||||||
☐ listen for settings to change
|
|
||||||
|
|
||||||
Long:
|
|
||||||
☐ fix #4 @high
|
|
||||||
☐ support hanchor (TOC) @big
|
|
||||||
|
|
||||||
Unknown:
|
|
||||||
☐ check how many times is the show_html function called
|
|
||||||
|
|
||||||
|
|
||||||
___________________
|
|
||||||
Archive:
|
|
||||||
✘ call settings listener on_new too - might be too heavy @cancelled Sun 08 Jan 2017 at 19:33 @project(Fast)
|
|
||||||
✔ fix relative source @done Sun 08 Jan 2017 at 19:22 @project(Medium)
|
|
||||||
✔ add settings for the preview @done Sun 08 Jan 2017 at 17:36 @project(Fast)
|
|
||||||
✔ regive focus to the right markdown view @done Mon 02 Jan 2017 at 18:34 @project(Fast)
|
|
||||||
✔ try/except for 404 @done Mon 02 Jan 2017 at 18:03 @project(Fast)
|
|
||||||
✔ fix bug when empty `src` @done Mon 02 Jan 2017 at 17:15 @project(Fast)
|
|
||||||
✔ preview.set_scratch(True) @done Mon 02 Jan 2017 at 16:58
|
|
||||||
✔ set the title of the preview @done Mon 02 Jan 2017 at 16:58
|
|
||||||
✔ preview.wordWrap => True @done Mon 02 Jan 2017 at 16:58
|
|
||||||
✔ clean the code (syntax) @done Mon 02 Jan 2017 at 16:58
|
|
||||||
✔ add 404 image @done Mon 02 Jan 2017 at 16:57
|
|
||||||
✔ load images from internet (`https:`) @done Mon 02 Jan 2017 at 16:57
|
|
||||||
53
README.md
53
README.md
@ -1,51 +1,18 @@
|
|||||||
# MarkdownLivePreview
|
# MarkdownLivePreview
|
||||||
|
|
||||||
This is a sublime text **3** plugin that allows you to preview your markdown instantly *in* it!
|
A simple plugin to preview your markdown as you type right in Sublime Text.
|
||||||
|
No dependencies!
|
||||||
|
|
||||||
### Dependencies
|
## How to install
|
||||||
|
|
||||||
**None**! There is no dependency! It uses [markdown2](https://github.com/trentm/python-markdown2) but it's a one file plugin, so it's included in the package.
|
It's available on package control!
|
||||||
|
|
||||||
## Installation
|
## How to contribute
|
||||||
|
|
||||||
MarkdownLivePreview is available on the default channel of [PackageControl](http://packagecontrol.io), which means you just have to
|
1. Fork this repo
|
||||||
|
2. Make your own branch (the name of the branch should be the feature you are
|
||||||
1. Open the command palette (`ctrl+shift+p`)
|
implementing eg. `improve-tables`, `fix-crash-on-multiple-preview`
|
||||||
2. Search for: `Package Control: Install Package`
|
3. All your code should be formated by black.
|
||||||
3. Search for: `MarkdownLivePreview`
|
4. Send a PR!
|
||||||
4. hit <kbd>enter</kbd>
|
|
||||||
|
|
||||||
to have MarkdownLivePreview working on your computer. Cool right? You can [thank package control](https://packagecontrol.io/say_thanks) for this.
|
|
||||||
|
|
||||||
### Usage
|
|
||||||
|
|
||||||
You can choose to enable MarkdownLivePreview by pressing <kbd>alt+m</kbd> or selecting in the command palette `MarkdownLivePreview: Edit Current File`. Note that you need to be editing (simply having the focus on) a markdown file. Because [Markdown Extended][markdown-extended] did a good job, it's compatible with this plugin.
|
|
||||||
|
|
||||||
It will open a new window, with only your markdown file, with the preview. Once your done, close whichever file and it'll close the entire window.
|
|
||||||
|
|
||||||
*Notice that it will close the entire window if you close **whichever** file. It means that if you open a random file in this window, and then close it, it'll close the entire window still*
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
- `markdown_live_preview_on_open`: if set to `true`, as soon as you open a markdown file, the preview window will popup (thanks to [@ooing](https://github.com/ooing) for it's [suggestion](https://github.com/math2001/MarkdownLivePreview/issues/7#issue-199464852)). Default to `false`
|
|
||||||
- `load_from_internet_when_starts`: every images that starts with any of the string specified in this list will be loaded from internet. Default to `["http://", "https://"]`
|
|
||||||
|
|
||||||
### In dev
|
|
||||||
|
|
||||||
This plugin is not finished, there's still some things to fix (custom css, focus, etc). So, don't run away if you have any trouble, just submit an issue [here](http://github.com/math2001/MarkdownLivePreview/issues).
|
|
||||||
|
|
||||||
### Demo
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Custom css
|
|
||||||
|
|
||||||
It is possible to set your own css. But, be carefull, you have to respect [those rules](http://www.sublimetext.com/docs/3/minihtml.html#css). Just go to `Preferences -> Package Settings -> MarkdownLivePreview`. It will open a css file, here: `$packages/User/MarkdownLivePreview.css`. Just save it and it will automatically use it instead of the default one.
|
|
||||||
|
|
||||||
### How to open the [README](http://github.com/math2001/MarkdownLivePreview/README.md)
|
|
||||||
|
|
||||||
Some of the package add a command in the menus, others in the command palette, or other nowhere. None of those options are really good, especially the last one on ST3 because the packages are compressed. But, fortunately, there is plugin that exists and **will solve this problem** for us (and he has a really cute name, don't you think?): [ReadmePlease](https://packagecontrol.io/packages/ReadmePlease).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
[markdown-extended]: https://packagecontrol.io/packages/Markdown%20Extended
|
|
||||||
|
|||||||
39
default.css
39
default.css
@ -1,39 +0,0 @@
|
|||||||
html {
|
|
||||||
--light-bg: color(var(--background) blend(#999 85%));
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
padding:10px;
|
|
||||||
padding-top: 0px;
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
font-style: italic;
|
|
||||||
display: block;
|
|
||||||
margin-left: 30px;
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding-left: 0.2rem;
|
|
||||||
padding-right: 0.2rem;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
display: block;
|
|
||||||
margin-top: 20px;
|
|
||||||
line-height: 1.7;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
padding-left: 10px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
7
dependencies.json
Normal file
7
dependencies.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"bs4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,20 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import sublime
|
|
||||||
import sublime_plugin
|
|
||||||
import os.path
|
|
||||||
|
|
||||||
class MLPDevListener(sublime_plugin.EventListener):
|
|
||||||
|
|
||||||
def on_post_save(self, view):
|
|
||||||
return
|
|
||||||
if not (os.path.dirname(__file__) in view.file_name() and
|
|
||||||
view.file_name().endswith('.py')):
|
|
||||||
return
|
|
||||||
sublime.run_command('reload_plugin', {
|
|
||||||
'main': os.path.join(sublime.packages_path(),
|
|
||||||
'MarkdownLivePreview', 'md_in_popup.py'),
|
|
||||||
'scripts': ['image_manager', 'functions', 'MLPApi',
|
|
||||||
'setting_names'],
|
|
||||||
'quiet': True
|
|
||||||
})
|
|
||||||
@ -1,39 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import re
|
|
||||||
|
|
||||||
__all__ = ['escape_amp']
|
|
||||||
|
|
||||||
RE_REPLACE_AMPERSAND = re.compile(r'&(\w*)(;)?')
|
|
||||||
|
|
||||||
def replace(matchobj):
|
|
||||||
if matchobj.group(2):
|
|
||||||
return matchobj.group(0)
|
|
||||||
else:
|
|
||||||
return matchobj.group(0).replace('&', '&')
|
|
||||||
|
|
||||||
def escape_amp(text):
|
|
||||||
return RE_REPLACE_AMPERSAND.sub(replace, text)
|
|
||||||
|
|
||||||
def run_tests():
|
|
||||||
tests = [
|
|
||||||
['&', '&'],
|
|
||||||
['&', '&amp'],
|
|
||||||
['&', '&'],
|
|
||||||
['& &hello &bonjour;', '& &hello &bonjour;']
|
|
||||||
]
|
|
||||||
fails = 0
|
|
||||||
for i, (subject, result) in enumerate(tests):
|
|
||||||
if RE_REPLACE_AMPERSAND.sub(replace, subject) != result:
|
|
||||||
# CSW: ignore
|
|
||||||
print('TEST FAIL ({i}): {subject!r} escaped did not match {result!r}'.format(**locals()))
|
|
||||||
fails += 1
|
|
||||||
if fails == 0:
|
|
||||||
# CSW: ignore
|
|
||||||
print("SUCCESS: every tests ({}) passed successfully!".format(len(tests)))
|
|
||||||
else:
|
|
||||||
# CSW: ignore
|
|
||||||
print("{} test{} failed".format(fails, 's' if fails > 1 else ''))
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
run_tests()
|
|
||||||
102
functions.py
102
functions.py
@ -1,102 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
import base64
|
|
||||||
import os.path
|
|
||||||
import sublime
|
|
||||||
import re
|
|
||||||
from .image_manager import ImageManager
|
|
||||||
|
|
||||||
def plugin_loaded():
|
|
||||||
global error404, loading
|
|
||||||
loading = sublime.load_resource('Packages/MarkdownLivePreview/loading.txt')
|
|
||||||
error404 = sublime.load_resource('Packages/MarkdownLivePreview/404.txt')
|
|
||||||
|
|
||||||
|
|
||||||
def replace_img_src_base64(html, basepath):
|
|
||||||
"""Really messy, but it works (should be updated)"""
|
|
||||||
index = -1
|
|
||||||
tag_start = '<img src="'
|
|
||||||
shtml, html = html, list(html)
|
|
||||||
while True:
|
|
||||||
index = shtml.find(tag_start, index + 1)
|
|
||||||
if index == -1:
|
|
||||||
break
|
|
||||||
path, end = get_content_till(html, '"', start=index + len(tag_start))
|
|
||||||
if ''.join(path).startswith('data:image/'):
|
|
||||||
continue
|
|
||||||
if ''.join(path).startswith(tuple(get_settings().get('load_from_internet'
|
|
||||||
'_when_starts'))):
|
|
||||||
image = ImageManager.get(''.join(path))
|
|
||||||
image = image or loading
|
|
||||||
|
|
||||||
else:
|
|
||||||
# local image
|
|
||||||
path = ''.join(path)
|
|
||||||
path = os.path.join(basepath, path)
|
|
||||||
image = to_base64(path)
|
|
||||||
html[index+len(tag_start):end] = image
|
|
||||||
shtml = ''.join(html)
|
|
||||||
return ''.join(html)
|
|
||||||
|
|
||||||
def is_markdown_view(view):
|
|
||||||
return 'markdown' in view.scope_name(0)
|
|
||||||
|
|
||||||
def to_base64(path=None, content=None):
|
|
||||||
if path is None and content is None:
|
|
||||||
return error404
|
|
||||||
elif content is None and path is not None:
|
|
||||||
try:
|
|
||||||
with open(path, 'rb') as fp:
|
|
||||||
content = fp.read()
|
|
||||||
except (FileNotFoundError, OSError):
|
|
||||||
return error404
|
|
||||||
|
|
||||||
return 'data:image/png;base64,' + ''.join([chr(el) for el in list(base64.standard_b64encode(content))])
|
|
||||||
|
|
||||||
def md(*t, **kwargs):
|
|
||||||
sublime.message_dialog(kwargs.get('sep', '\n').join([str(el) for el in t]))
|
|
||||||
|
|
||||||
def sm(*t, **kwargs):
|
|
||||||
sublime.status_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
|
||||||
|
|
||||||
def em(*t, **kwargs):
|
|
||||||
sublime.error_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
|
||||||
|
|
||||||
def mini(val, min):
|
|
||||||
if val < min:
|
|
||||||
return min
|
|
||||||
return val
|
|
||||||
|
|
||||||
def get_content_till(string, char_to_look_for, start=0):
|
|
||||||
i = start
|
|
||||||
while i < len(string):
|
|
||||||
if string[i] == char_to_look_for:
|
|
||||||
return string[start:i], i
|
|
||||||
i += 1
|
|
||||||
|
|
||||||
def get_view_content(view):
|
|
||||||
return view.substr(sublime.Region(0, view.size()))
|
|
||||||
|
|
||||||
def get_view_from_id(window, id):
|
|
||||||
if not isinstance(id, int):
|
|
||||||
return
|
|
||||||
for view in window.views():
|
|
||||||
if view.id() == id:
|
|
||||||
return view
|
|
||||||
|
|
||||||
def get_settings():
|
|
||||||
return sublime.load_settings('MarkdownLivePreview.sublime-settings')
|
|
||||||
|
|
||||||
def pre_with_br(html):
|
|
||||||
"""Because the phantoms of sublime text does not support <pre> blocks
|
|
||||||
this function replaces every \n with a <br> in a <pre>"""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
obj = re.search(r'<pre>(.*?)</pre>', html, re.DOTALL)
|
|
||||||
if not obj:
|
|
||||||
break
|
|
||||||
html = list(html)
|
|
||||||
html[obj.start(0):obj.end(0)] = '<pre >' + ''.join(html[obj.start(1):obj.end(1)]) \
|
|
||||||
.replace('\n', '<br>') \
|
|
||||||
.replace(' ', ' ') + '</pre>'
|
|
||||||
html = ''.join(html)
|
|
||||||
return html
|
|
||||||
@ -1,98 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import os.path
|
|
||||||
import tempfile
|
|
||||||
import sublime
|
|
||||||
from threading import Thread
|
|
||||||
import urllib.request, urllib.error
|
|
||||||
from .functions import *
|
|
||||||
|
|
||||||
CACHE_FILE = os.path.join(tempfile.gettempdir(),
|
|
||||||
'MarkdownLivePreviewCache.txt')
|
|
||||||
TIMEOUT = 20 # seconds
|
|
||||||
|
|
||||||
SEPARATOR = '---%cache%--'
|
|
||||||
|
|
||||||
def get_base64_saver(loading, url):
|
|
||||||
def callback(content):
|
|
||||||
if isinstance(content, urllib.error.HTTPError):
|
|
||||||
if content.getcode() == 404:
|
|
||||||
loading[url] = 404
|
|
||||||
return
|
|
||||||
elif isinstance(content, urllib.error.URLError):
|
|
||||||
if (content.reason.errno == 11001 and
|
|
||||||
content.reason.strerror == 'getaddrinfo failed'):
|
|
||||||
loading[url] = 404
|
|
||||||
return
|
|
||||||
return sublime.error_message('An unexpected error has occured: ' +
|
|
||||||
str(content))
|
|
||||||
loading[url] = to_base64(content=content)
|
|
||||||
|
|
||||||
return callback
|
|
||||||
|
|
||||||
def get_cache_for(imageurl):
|
|
||||||
if not os.path.exists(CACHE_FILE):
|
|
||||||
return
|
|
||||||
with open(CACHE_FILE) as fp:
|
|
||||||
for line in fp.read().splitlines():
|
|
||||||
url, base64 = line.split(SEPARATOR, 1)
|
|
||||||
if url == imageurl:
|
|
||||||
return base64
|
|
||||||
|
|
||||||
def cache(imageurl, base64):
|
|
||||||
with open(CACHE_FILE, 'a') as fp:
|
|
||||||
fp.write(imageurl + SEPARATOR + base64 + '\n')
|
|
||||||
|
|
||||||
class ImageLoader(Thread):
|
|
||||||
|
|
||||||
def __init__(self, url, callback):
|
|
||||||
Thread.__init__(self)
|
|
||||||
self.url = url
|
|
||||||
self.callback = callback
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
try:
|
|
||||||
page = urllib.request.urlopen(self.url, None, TIMEOUT)
|
|
||||||
except Exception as e:
|
|
||||||
self.callback(e)
|
|
||||||
else:
|
|
||||||
self.callback(page.read())
|
|
||||||
|
|
||||||
|
|
||||||
class ImageManager(object):
|
|
||||||
|
|
||||||
"""
|
|
||||||
Usage:
|
|
||||||
|
|
||||||
>>> image = ImageManager.get('http://domain.com/image.png')
|
|
||||||
>>> image = ImageManager.get('http://domain.com/image.png')
|
|
||||||
# still loading (this is a comment, no an outputed text), it doesn't
|
|
||||||
# run an other request
|
|
||||||
>>> image = ImageManager.get('http://domain.com/image.png')
|
|
||||||
'data:image/png;base64,....'
|
|
||||||
"""
|
|
||||||
loading = {}
|
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def get(imageurl, user_callback=None):
|
|
||||||
|
|
||||||
cached = get_cache_for(imageurl)
|
|
||||||
if cached:
|
|
||||||
return cached
|
|
||||||
elif imageurl in ImageManager.loading.keys():
|
|
||||||
# return None (the file is still loading, already made a request)
|
|
||||||
# return string the base64 of the url (which is going to be cached)
|
|
||||||
temp_cached = ImageManager.loading[imageurl]
|
|
||||||
if temp_cached == 404:
|
|
||||||
return to_base64('404.png')
|
|
||||||
if temp_cached:
|
|
||||||
cache(imageurl, temp_cached)
|
|
||||||
del ImageManager.loading[imageurl]
|
|
||||||
return temp_cached
|
|
||||||
else:
|
|
||||||
# load from internet
|
|
||||||
ImageManager.loading[imageurl] = None
|
|
||||||
callback = get_base64_saver(ImageManager.loading, imageurl)
|
|
||||||
loader = ImageLoader(imageurl, callback)
|
|
||||||
loader.start()
|
|
||||||
sublime.set_timeout_async(lambda: loader.join(), TIMEOUT * 1000)
|
|
||||||
@ -1 +0,0 @@
|
|||||||
There is images here, allthough they aren't of any use for the plugin. They're just the image that I used to generate the base64 (404.txt and loading.txt)
|
|
||||||
348
lib/markdown2.py
348
lib/markdown2.py
@ -1,13 +1,8 @@
|
|||||||
# CSW: ignore
|
|
||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
# Copyright (c) 2012 Trent Mick.
|
# Copyright (c) 2012 Trent Mick.
|
||||||
# Copyright (c) 2007-2008 ActiveState Corp.
|
# Copyright (c) 2007-2008 ActiveState Corp.
|
||||||
# License: MIT (http://www.opensource.org/licenses/mit-license.php)
|
# License: MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||||
|
|
||||||
from __future__ import generators
|
|
||||||
|
|
||||||
r"""A fast and complete Python implementation of Markdown.
|
r"""A fast and complete Python implementation of Markdown.
|
||||||
|
|
||||||
[from http://daringfireball.net/projects/markdown/]
|
[from http://daringfireball.net/projects/markdown/]
|
||||||
@ -55,6 +50,8 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details):
|
|||||||
implemented in other Markdown processors (tho not in Markdown.pl v1.0.1).
|
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
|
* header-ids: Adds "id" attributes to headers. The id value is a slug of
|
||||||
the header text.
|
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
|
* html-classes: Takes a dict mapping html tag names (lowercase) to a
|
||||||
string to use for a "class" tag attribute. Currently only supports "img",
|
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
|
"table", "pre" and "code" tags. Add an issue if you require this for other
|
||||||
@ -78,6 +75,7 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details):
|
|||||||
and ellipses.
|
and ellipses.
|
||||||
* spoiler: A special kind of blockquote commonly hidden behind a
|
* spoiler: A special kind of blockquote commonly hidden behind a
|
||||||
click on SO. Syntax per <http://meta.stackexchange.com/a/72878>.
|
click on SO. Syntax per <http://meta.stackexchange.com/a/72878>.
|
||||||
|
* strike: text inside of double tilde is ~~strikethrough~~
|
||||||
* tag-friendly: Requires atx style headers to have a space between the # and
|
* 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
|
the header text. Useful for applications that require twitter style tags to
|
||||||
pass through the parser.
|
pass through the parser.
|
||||||
@ -98,20 +96,18 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details):
|
|||||||
# not yet sure if there implications with this. Compare 'pydoc sre'
|
# not yet sure if there implications with this. Compare 'pydoc sre'
|
||||||
# and 'perldoc perlre'.
|
# and 'perldoc perlre'.
|
||||||
|
|
||||||
__version_info__ = (2, 3, 2)
|
__version_info__ = (2, 3, 9)
|
||||||
__version__ = '.'.join(map(str, __version_info__))
|
__version__ = '.'.join(map(str, __version_info__))
|
||||||
__author__ = "Trent Mick"
|
__author__ = "Trent Mick"
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import re
|
import re
|
||||||
import logging
|
import logging
|
||||||
try:
|
from hashlib import sha256
|
||||||
from hashlib import md5
|
|
||||||
except ImportError:
|
|
||||||
from md5 import md5
|
|
||||||
import optparse
|
import optparse
|
||||||
from random import random, randint
|
from random import random, randint
|
||||||
import codecs
|
import codecs
|
||||||
|
from collections import defaultdict
|
||||||
try:
|
try:
|
||||||
from urllib import quote_plus
|
from urllib import quote_plus
|
||||||
except ImportError:
|
except ImportError:
|
||||||
@ -120,11 +116,6 @@ except ImportError:
|
|||||||
|
|
||||||
# ---- Python version compat
|
# ---- Python version compat
|
||||||
|
|
||||||
if sys.version_info[:2] < (2, 4):
|
|
||||||
def reversed(sequence):
|
|
||||||
for i in sequence[::-1]:
|
|
||||||
yield i
|
|
||||||
|
|
||||||
# Use `bytes` for byte strings and `unicode` for unicode strings (str in Py3).
|
# Use `bytes` for byte strings and `unicode` for unicode strings (str in Py3).
|
||||||
if sys.version_info[0] <= 2:
|
if sys.version_info[0] <= 2:
|
||||||
py3 = False
|
py3 = False
|
||||||
@ -147,13 +138,19 @@ DEFAULT_TAB_WIDTH = 4
|
|||||||
|
|
||||||
|
|
||||||
SECRET_SALT = bytes(randint(0, 1000000))
|
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):
|
def _hash_text(s):
|
||||||
return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest()
|
return 'md5-' + sha256(SECRET_SALT + s.encode("utf-8")).hexdigest()[32:]
|
||||||
|
|
||||||
# Table of hash values for escaped characters:
|
# Table of hash values for escaped characters:
|
||||||
g_escape_table = dict([(ch, _hash_text(ch))
|
g_escape_table = dict([(ch, _hash_text(ch))
|
||||||
for ch in '\\`*_{}[]()>#+-.!'])
|
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
|
# ---- exceptions
|
||||||
class MarkdownError(Exception):
|
class MarkdownError(Exception):
|
||||||
@ -165,6 +162,7 @@ class MarkdownError(Exception):
|
|||||||
def markdown_path(path, encoding="utf-8",
|
def markdown_path(path, encoding="utf-8",
|
||||||
html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
|
html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
|
||||||
safe_mode=None, extras=None, link_patterns=None,
|
safe_mode=None, extras=None, link_patterns=None,
|
||||||
|
footnote_title=None, footnote_return_symbol=None,
|
||||||
use_file_vars=False):
|
use_file_vars=False):
|
||||||
fp = codecs.open(path, 'r', encoding)
|
fp = codecs.open(path, 'r', encoding)
|
||||||
text = fp.read()
|
text = fp.read()
|
||||||
@ -172,16 +170,21 @@ def markdown_path(path, encoding="utf-8",
|
|||||||
return Markdown(html4tags=html4tags, tab_width=tab_width,
|
return Markdown(html4tags=html4tags, tab_width=tab_width,
|
||||||
safe_mode=safe_mode, extras=extras,
|
safe_mode=safe_mode, extras=extras,
|
||||||
link_patterns=link_patterns,
|
link_patterns=link_patterns,
|
||||||
|
footnote_title=footnote_title,
|
||||||
|
footnote_return_symbol=footnote_return_symbol,
|
||||||
use_file_vars=use_file_vars).convert(text)
|
use_file_vars=use_file_vars).convert(text)
|
||||||
|
|
||||||
|
|
||||||
def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
|
def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
|
||||||
safe_mode=None, extras=None, link_patterns=None,
|
safe_mode=None, extras=None, link_patterns=None,
|
||||||
use_file_vars=False):
|
footnote_title=None, footnote_return_symbol=None,
|
||||||
|
use_file_vars=False, cli=False):
|
||||||
return Markdown(html4tags=html4tags, tab_width=tab_width,
|
return Markdown(html4tags=html4tags, tab_width=tab_width,
|
||||||
safe_mode=safe_mode, extras=extras,
|
safe_mode=safe_mode, extras=extras,
|
||||||
link_patterns=link_patterns,
|
link_patterns=link_patterns,
|
||||||
use_file_vars=use_file_vars).convert(text)
|
footnote_title=footnote_title,
|
||||||
|
footnote_return_symbol=footnote_return_symbol,
|
||||||
|
use_file_vars=use_file_vars, cli=cli).convert(text)
|
||||||
|
|
||||||
|
|
||||||
class Markdown(object):
|
class Markdown(object):
|
||||||
@ -206,7 +209,9 @@ class Markdown(object):
|
|||||||
_ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
|
_ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
|
||||||
|
|
||||||
def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
|
def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
|
||||||
extras=None, link_patterns=None, use_file_vars=False):
|
extras=None, link_patterns=None,
|
||||||
|
footnote_title=None, footnote_return_symbol=None,
|
||||||
|
use_file_vars=False, cli=False):
|
||||||
if html4tags:
|
if html4tags:
|
||||||
self.empty_element_suffix = ">"
|
self.empty_element_suffix = ">"
|
||||||
else:
|
else:
|
||||||
@ -231,13 +236,23 @@ class Markdown(object):
|
|||||||
extras = dict([(e, None) for e in extras])
|
extras = dict([(e, None) for e in extras])
|
||||||
self.extras.update(extras)
|
self.extras.update(extras)
|
||||||
assert isinstance(self.extras, dict)
|
assert isinstance(self.extras, dict)
|
||||||
if "toc" in self.extras and "header-ids" not in self.extras:
|
|
||||||
self.extras["header-ids"] = None # "toc" implies "header-ids"
|
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._instance_extras = self.extras.copy()
|
||||||
|
|
||||||
self.link_patterns = link_patterns
|
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.use_file_vars = use_file_vars
|
||||||
self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
|
self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
|
||||||
|
self.cli = cli
|
||||||
|
|
||||||
self._escape_table = g_escape_table.copy()
|
self._escape_table = g_escape_table.copy()
|
||||||
if "smarty-pants" in self.extras:
|
if "smarty-pants" in self.extras:
|
||||||
@ -255,16 +270,26 @@ class Markdown(object):
|
|||||||
self.footnotes = {}
|
self.footnotes = {}
|
||||||
self.footnote_ids = []
|
self.footnote_ids = []
|
||||||
if "header-ids" in self.extras:
|
if "header-ids" in self.extras:
|
||||||
self._count_from_header_id = {} # no `defaultdict` in Python 2.4
|
self._count_from_header_id = defaultdict(int)
|
||||||
if "metadata" in self.extras:
|
if "metadata" in self.extras:
|
||||||
self.metadata = {}
|
self.metadata = {}
|
||||||
|
|
||||||
# Per <https://developer.mozilla.org/en-US/docs/HTML/Element/a> "rel"
|
# Per <https://developer.mozilla.org/en-US/docs/HTML/Element/a> "rel"
|
||||||
# should only be used in <a> tags with an "href" attribute.
|
# should only be used in <a> tags with an "href" attribute.
|
||||||
_a_nofollow = re.compile(r"<(a)([^>]*href=)", re.IGNORECASE)
|
_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
|
# Opens the linked document in a new window or tab
|
||||||
# should only used in <a> tags with an "target" attribute.
|
# should only used in <a> tags with an "href" attribute.
|
||||||
# same with _a_nofollow
|
# same with _a_nofollow
|
||||||
_a_blank = _a_nofollow
|
_a_blank = _a_nofollow
|
||||||
|
|
||||||
@ -366,11 +391,21 @@ class Markdown(object):
|
|||||||
if "target-blank-links" in self.extras:
|
if "target-blank-links" in self.extras:
|
||||||
text = self._a_blank.sub(r'<\1 target="_blank"\2', text)
|
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"
|
text += "\n"
|
||||||
|
|
||||||
|
# Attach attrs to output
|
||||||
rv = UnicodeWithAttrs(text)
|
rv = UnicodeWithAttrs(text)
|
||||||
if "toc" in self.extras:
|
|
||||||
rv._toc = self._toc
|
if "toc" in self.extras and self._toc:
|
||||||
|
rv.toc_html = self._toc_html
|
||||||
|
|
||||||
if "metadata" in self.extras:
|
if "metadata" in self.extras:
|
||||||
rv.metadata = self.metadata
|
rv.metadata = self.metadata
|
||||||
return rv
|
return rv
|
||||||
@ -402,30 +437,33 @@ class Markdown(object):
|
|||||||
#
|
#
|
||||||
# # header
|
# # 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)
|
_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("[\S\w]+\s*:(?! >)[ \t]*.*\n?", re.MULTILINE)
|
_key_val_pat = re.compile(r"[\S\w]+\s*:(?! >)[ \t]*.*\n?", re.MULTILINE)
|
||||||
# this allows key: >
|
# this allows key: >
|
||||||
# value
|
# value
|
||||||
# conutiues over multiple lines
|
# conutiues over multiple lines
|
||||||
_key_val_block_pat = re.compile(
|
_key_val_block_pat = re.compile(
|
||||||
"(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)", re.MULTILINE)
|
"(.*:\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):
|
def _extract_metadata(self, text):
|
||||||
match = re.findall(self._meta_data_pattern, text)
|
|
||||||
|
|
||||||
if not match:
|
|
||||||
return text
|
|
||||||
|
|
||||||
last_item = list(filter(None, match[-1]))[0]
|
|
||||||
end_of_metadata = text.index(last_item)+len(last_item)
|
|
||||||
if text.startswith("---"):
|
if text.startswith("---"):
|
||||||
# add 8 charachters for opening and closing
|
fence_splits = re.split(self._meta_data_fence_pattern, text, maxsplit=2)
|
||||||
# and since indexing starts at 0 we add a step
|
metadata_content = fence_splits[1]
|
||||||
tail = text[end_of_metadata+4:]
|
match = re.findall(self._meta_data_pattern, metadata_content)
|
||||||
|
if not match:
|
||||||
|
return text
|
||||||
|
tail = fence_splits[2]
|
||||||
else:
|
else:
|
||||||
tail = text[end_of_metadata:]
|
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, text)
|
kv = re.findall(self._key_val_pat, metadata_content)
|
||||||
kvm = re.findall(self._key_val_block_pat, text)
|
kvm = re.findall(self._key_val_block_pat, metadata_content)
|
||||||
kvm = [item.replace(": >\n", ":", 1) for item in kvm]
|
kvm = [item.replace(": >\n", ":", 1) for item in kvm]
|
||||||
|
|
||||||
for item in kv + kvm:
|
for item in kv + kvm:
|
||||||
@ -957,12 +995,14 @@ class Markdown(object):
|
|||||||
|
|
||||||
def _table_sub(self, match):
|
def _table_sub(self, match):
|
||||||
trim_space_re = '^[ \t\n]+|[ \t\n]+$'
|
trim_space_re = '^[ \t\n]+|[ \t\n]+$'
|
||||||
trim_bar_re = '^\||\|$'
|
trim_bar_re = r'^\||\|$'
|
||||||
|
split_bar_re = r'^\||(?<!\\)\|'
|
||||||
|
escape_bar_re = r'\\\|'
|
||||||
|
|
||||||
head, underline, body = match.groups()
|
head, underline, body = match.groups()
|
||||||
|
|
||||||
# Determine aligns for columns.
|
# Determine aligns for columns.
|
||||||
cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", underline)).split('|')]
|
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, "", underline)))]
|
||||||
align_from_col_idx = {}
|
align_from_col_idx = {}
|
||||||
for col_idx, col in enumerate(cols):
|
for col_idx, col in enumerate(cols):
|
||||||
if col[0] == ':' and col[-1] == ':':
|
if col[0] == ':' and col[-1] == ':':
|
||||||
@ -974,7 +1014,7 @@ class Markdown(object):
|
|||||||
|
|
||||||
# thead
|
# thead
|
||||||
hlines = ['<table%s>' % self._html_class_str_from_tag('table'), '<thead>', '<tr>']
|
hlines = ['<table%s>' % self._html_class_str_from_tag('table'), '<thead>', '<tr>']
|
||||||
cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", head)).split('|')]
|
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):
|
for col_idx, col in enumerate(cols):
|
||||||
hlines.append(' <th%s>%s</th>' % (
|
hlines.append(' <th%s>%s</th>' % (
|
||||||
align_from_col_idx.get(col_idx, ''),
|
align_from_col_idx.get(col_idx, ''),
|
||||||
@ -987,7 +1027,7 @@ class Markdown(object):
|
|||||||
hlines.append('<tbody>')
|
hlines.append('<tbody>')
|
||||||
for line in body.strip('\n').split('\n'):
|
for line in body.strip('\n').split('\n'):
|
||||||
hlines.append('<tr>')
|
hlines.append('<tr>')
|
||||||
cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", line)).split('|')]
|
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):
|
for col_idx, col in enumerate(cols):
|
||||||
hlines.append(' <td%s>%s</td>' % (
|
hlines.append(' <td%s>%s</td>' % (
|
||||||
align_from_col_idx.get(col_idx, ''),
|
align_from_col_idx.get(col_idx, ''),
|
||||||
@ -1071,6 +1111,9 @@ class Markdown(object):
|
|||||||
text = self._escape_special_chars(text)
|
text = self._escape_special_chars(text)
|
||||||
|
|
||||||
# Process anchor and image tags.
|
# Process anchor and image tags.
|
||||||
|
if "link-patterns" in self.extras:
|
||||||
|
text = self._do_link_patterns(text)
|
||||||
|
|
||||||
text = self._do_links(text)
|
text = self._do_links(text)
|
||||||
|
|
||||||
# Make links out of things like `<http://example.com/>`
|
# Make links out of things like `<http://example.com/>`
|
||||||
@ -1078,9 +1121,6 @@ class Markdown(object):
|
|||||||
# delimiters in inline links like [this](<url>).
|
# delimiters in inline links like [this](<url>).
|
||||||
text = self._do_auto_links(text)
|
text = self._do_auto_links(text)
|
||||||
|
|
||||||
if "link-patterns" in self.extras:
|
|
||||||
text = self._do_link_patterns(text)
|
|
||||||
|
|
||||||
text = self._encode_amps_and_angles(text)
|
text = self._encode_amps_and_angles(text)
|
||||||
|
|
||||||
if "strike" in self.extras:
|
if "strike" in self.extras:
|
||||||
@ -1160,7 +1200,7 @@ class Markdown(object):
|
|||||||
self.html_spans[key] = sanitized
|
self.html_spans[key] = sanitized
|
||||||
tokens.append(key)
|
tokens.append(key)
|
||||||
else:
|
else:
|
||||||
tokens.append(token)
|
tokens.append(self._encode_incomplete_tags(token))
|
||||||
is_html_markup = not is_html_markup
|
is_html_markup = not is_html_markup
|
||||||
return ''.join(tokens)
|
return ''.join(tokens)
|
||||||
|
|
||||||
@ -1353,7 +1393,7 @@ class Markdown(object):
|
|||||||
if is_img:
|
if is_img:
|
||||||
img_class_str = self._html_class_str_from_tag("img")
|
img_class_str = self._html_class_str_from_tag("img")
|
||||||
result = '<img src="%s" alt="%s"%s%s%s' \
|
result = '<img src="%s" alt="%s"%s%s%s' \
|
||||||
% (_urlencode(url, safe_mode=self.safe_mode),
|
% (_html_escape_url(url, safe_mode=self.safe_mode),
|
||||||
_xml_escape_attr(link_text),
|
_xml_escape_attr(link_text),
|
||||||
title_str,
|
title_str,
|
||||||
img_class_str,
|
img_class_str,
|
||||||
@ -1363,11 +1403,12 @@ class Markdown(object):
|
|||||||
curr_pos = start_idx + len(result)
|
curr_pos = start_idx + len(result)
|
||||||
text = text[:start_idx] + result + text[url_end_idx:]
|
text = text[:start_idx] + result + text[url_end_idx:]
|
||||||
elif start_idx >= anchor_allowed_pos:
|
elif start_idx >= anchor_allowed_pos:
|
||||||
if self.safe_mode and not self._safe_protocols.match(url):
|
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)
|
result_head = '<a href="#"%s>' % (title_str)
|
||||||
else:
|
else:
|
||||||
result_head = '<a href="%s"%s>' % (_urlencode(url, safe_mode=self.safe_mode), title_str)
|
result_head = '<a href="%s"%s>' % (_html_escape_url(url, safe_mode=self.safe_mode), title_str)
|
||||||
result = '%s%s</a>' % (result_head, _xml_escape_attr(link_text))
|
result = '%s%s</a>' % (result_head, link_text)
|
||||||
if "smarty-pants" in self.extras:
|
if "smarty-pants" in self.extras:
|
||||||
result = result.replace('"', self._escape_table['"'])
|
result = result.replace('"', self._escape_table['"'])
|
||||||
# <img> allowed from curr_pos on, <a> from
|
# <img> allowed from curr_pos on, <a> from
|
||||||
@ -1408,7 +1449,7 @@ class Markdown(object):
|
|||||||
if is_img:
|
if is_img:
|
||||||
img_class_str = self._html_class_str_from_tag("img")
|
img_class_str = self._html_class_str_from_tag("img")
|
||||||
result = '<img src="%s" alt="%s"%s%s%s' \
|
result = '<img src="%s" alt="%s"%s%s%s' \
|
||||||
% (_urlencode(url, safe_mode=self.safe_mode),
|
% (_html_escape_url(url, safe_mode=self.safe_mode),
|
||||||
_xml_escape_attr(link_text),
|
_xml_escape_attr(link_text),
|
||||||
title_str,
|
title_str,
|
||||||
img_class_str,
|
img_class_str,
|
||||||
@ -1421,7 +1462,7 @@ class Markdown(object):
|
|||||||
if self.safe_mode and not self._safe_protocols.match(url):
|
if self.safe_mode and not self._safe_protocols.match(url):
|
||||||
result_head = '<a href="#"%s>' % (title_str)
|
result_head = '<a href="#"%s>' % (title_str)
|
||||||
else:
|
else:
|
||||||
result_head = '<a href="%s"%s>' % (_urlencode(url, safe_mode=self.safe_mode), title_str)
|
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)
|
result = '%s%s</a>' % (result_head, link_text)
|
||||||
if "smarty-pants" in self.extras:
|
if "smarty-pants" in self.extras:
|
||||||
result = result.replace('"', self._escape_table['"'])
|
result = result.replace('"', self._escape_table['"'])
|
||||||
@ -1461,15 +1502,17 @@ class Markdown(object):
|
|||||||
header_id = _slugify(text)
|
header_id = _slugify(text)
|
||||||
if prefix and isinstance(prefix, base_string_type):
|
if prefix and isinstance(prefix, base_string_type):
|
||||||
header_id = prefix + '-' + header_id
|
header_id = prefix + '-' + header_id
|
||||||
if header_id in self._count_from_header_id:
|
|
||||||
self._count_from_header_id[header_id] += 1
|
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]
|
header_id += '-%s' % self._count_from_header_id[header_id]
|
||||||
else:
|
|
||||||
self._count_from_header_id[header_id] = 1
|
|
||||||
return header_id
|
return header_id
|
||||||
|
|
||||||
_toc = None
|
_toc = None
|
||||||
def _toc_add_entry(self, level, id, name):
|
def _toc_add_entry(self, level, id, name):
|
||||||
|
if level > self._toc_depth:
|
||||||
|
return
|
||||||
if self._toc is None:
|
if self._toc is None:
|
||||||
self._toc = []
|
self._toc = []
|
||||||
self._toc.append((level, id, self._unescape_special_chars(name)))
|
self._toc.append((level, id, self._unescape_special_chars(name)))
|
||||||
@ -1491,7 +1534,9 @@ class Markdown(object):
|
|||||||
_h_re_tag_friendly = 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):
|
def _h_sub(self, match):
|
||||||
if match.group(1) is not None:
|
if match.group(1) is not None and match.group(3) == "-":
|
||||||
|
return match.group(1)
|
||||||
|
elif match.group(1) is not None:
|
||||||
# Setext header
|
# Setext header
|
||||||
n = {"=": 1, "-": 2}[match.group(3)[0]]
|
n = {"=": 1, "-": 2}[match.group(3)[0]]
|
||||||
header_group = match.group(2)
|
header_group = match.group(2)
|
||||||
@ -1610,16 +1655,16 @@ class Markdown(object):
|
|||||||
re.M | re.X | re.S)
|
re.M | re.X | re.S)
|
||||||
|
|
||||||
_task_list_item_re = re.compile(r'''
|
_task_list_item_re = re.compile(r'''
|
||||||
(\[[\ x]\])[ \t]+ # tasklist marker = \1
|
(\[[\ xX]\])[ \t]+ # tasklist marker = \1
|
||||||
(.*) # list item text = \2
|
(.*) # list item text = \2
|
||||||
''', re.M | re.X | re.S)
|
''', re.M | re.X | re.S)
|
||||||
|
|
||||||
_task_list_warpper_str = r'<p><input type="checkbox" class="task-list-item-checkbox" %sdisabled>%s</p>'
|
_task_list_warpper_str = r'<input type="checkbox" class="task-list-item-checkbox" %sdisabled> %s'
|
||||||
|
|
||||||
def _task_list_item_sub(self, match):
|
def _task_list_item_sub(self, match):
|
||||||
marker = match.group(1)
|
marker = match.group(1)
|
||||||
item_text = match.group(2)
|
item_text = match.group(2)
|
||||||
if marker == '[x]':
|
if marker in ['[x]','[X]']:
|
||||||
return self._task_list_warpper_str % ('checked ', item_text)
|
return self._task_list_warpper_str % ('checked ', item_text)
|
||||||
elif marker == '[ ]':
|
elif marker == '[ ]':
|
||||||
return self._task_list_warpper_str % ('', item_text)
|
return self._task_list_warpper_str % ('', item_text)
|
||||||
@ -1728,7 +1773,8 @@ class Markdown(object):
|
|||||||
codeblock = rest.lstrip("\n") # Remove lexer declaration line.
|
codeblock = rest.lstrip("\n") # Remove lexer declaration line.
|
||||||
formatter_opts = self.extras['code-color'] or {}
|
formatter_opts = self.extras['code-color'] or {}
|
||||||
|
|
||||||
if lexer_name:
|
# Use pygments only if not using the highlightjs-lang extra
|
||||||
|
if lexer_name and "highlightjs-lang" not in self.extras:
|
||||||
def unhash_code(codeblock):
|
def unhash_code(codeblock):
|
||||||
for key, sanitized in list(self.html_spans.items()):
|
for key, sanitized in list(self.html_spans.items()):
|
||||||
codeblock = codeblock.replace(key, sanitized)
|
codeblock = codeblock.replace(key, sanitized)
|
||||||
@ -1741,7 +1787,7 @@ class Markdown(object):
|
|||||||
codeblock = codeblock.replace(old, new)
|
codeblock = codeblock.replace(old, new)
|
||||||
return codeblock
|
return codeblock
|
||||||
lexer = self._get_pygments_lexer(lexer_name)
|
lexer = self._get_pygments_lexer(lexer_name)
|
||||||
if lexer and self.extras.get('no-code-highlighting', True) is True:
|
if lexer:
|
||||||
codeblock = unhash_code( codeblock )
|
codeblock = unhash_code( codeblock )
|
||||||
colored = self._color_with_pygments(codeblock, lexer,
|
colored = self._color_with_pygments(codeblock, lexer,
|
||||||
**formatter_opts)
|
**formatter_opts)
|
||||||
@ -1749,7 +1795,12 @@ class Markdown(object):
|
|||||||
|
|
||||||
codeblock = self._encode_code(codeblock)
|
codeblock = self._encode_code(codeblock)
|
||||||
pre_class_str = self._html_class_str_from_tag("pre")
|
pre_class_str = self._html_class_str_from_tag("pre")
|
||||||
code_class_str = self._html_class_str_from_tag("code")
|
|
||||||
|
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" % (
|
return "\n\n<pre%s><code%s>%s\n</code></pre>\n\n" % (
|
||||||
pre_class_str, code_class_str, codeblock)
|
pre_class_str, code_class_str, codeblock)
|
||||||
|
|
||||||
@ -1788,7 +1839,7 @@ class Markdown(object):
|
|||||||
|
|
||||||
_fenced_code_block_re = re.compile(r'''
|
_fenced_code_block_re = re.compile(r'''
|
||||||
(?:\n+|\A\n?)
|
(?:\n+|\A\n?)
|
||||||
^```([\w+-]+)?[ \t]*\n # opening fence, $1 = optional lang
|
^```\s*?([\w+-]+)?\s*?\n # opening fence, $1 = optional lang
|
||||||
(.*?) # $2 = code block content
|
(.*?) # $2 = code block content
|
||||||
^```[ \t]*\n # closing fence
|
^```[ \t]*\n # closing fence
|
||||||
''', re.M | re.X | re.S)
|
''', re.M | re.X | re.S)
|
||||||
@ -1930,6 +1981,13 @@ class Markdown(object):
|
|||||||
text = text.replace("...", "…")
|
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
|
return text
|
||||||
|
|
||||||
_block_quote_base = r'''
|
_block_quote_base = r'''
|
||||||
@ -2001,8 +2059,13 @@ class Markdown(object):
|
|||||||
# text (issue 33). Note the `[-1]` is a quick way to
|
# text (issue 33). Note the `[-1]` is a quick way to
|
||||||
# consider numeric bullets (e.g. "1." and "2.") to be
|
# consider numeric bullets (e.g. "1." and "2.") to be
|
||||||
# equal.
|
# equal.
|
||||||
if (li and len(li.group(2)) <= 3 and li.group("next_marker")
|
if (li and len(li.group(2)) <= 3
|
||||||
and li.group("marker")[-1] == li.group("next_marker")[-1]):
|
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()
|
start = li.start()
|
||||||
cuddled_list = self._do_lists(graf[start:]).rstrip("\n")
|
cuddled_list = self._do_lists(graf[start:]).rstrip("\n")
|
||||||
assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>")
|
assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>")
|
||||||
@ -2010,7 +2073,7 @@ class Markdown(object):
|
|||||||
|
|
||||||
# Wrap <p> tags.
|
# Wrap <p> tags.
|
||||||
graf = self._run_span_gamut(graf)
|
graf = self._run_span_gamut(graf)
|
||||||
grafs.append("<p>" + graf.lstrip(" \t") + "</p>")
|
grafs.append("<p%s>" % self._html_class_str_from_tag('p') + graf.lstrip(" \t") + "</p>")
|
||||||
|
|
||||||
if cuddled_list:
|
if cuddled_list:
|
||||||
grafs.append(cuddled_list)
|
grafs.append(cuddled_list)
|
||||||
@ -2024,15 +2087,31 @@ class Markdown(object):
|
|||||||
'<hr' + self.empty_element_suffix,
|
'<hr' + self.empty_element_suffix,
|
||||||
'<ol>',
|
'<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):
|
for i, id in enumerate(self.footnote_ids):
|
||||||
if i != 0:
|
if i != 0:
|
||||||
footer.append('')
|
footer.append('')
|
||||||
footer.append('<li id="fn-%s">' % id)
|
footer.append('<li id="fn-%s">' % id)
|
||||||
footer.append(self._run_block_gamut(self.footnotes[id]))
|
footer.append(self._run_block_gamut(self.footnotes[id]))
|
||||||
backlink = ('<a href="#fnref-%s" '
|
try:
|
||||||
'class="footnoteBackLink" '
|
backlink = ('<a href="#fnref-%s" ' +
|
||||||
'title="Jump back to footnote %d in the text.">'
|
'class="footnoteBackLink" ' +
|
||||||
'↩</a>' % (id, i+1))
|
'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>"):
|
if footer[-1].endswith("</p>"):
|
||||||
footer[-1] = footer[-1][:-len("</p>")] \
|
footer[-1] = footer[-1][:-len("</p>")] \
|
||||||
+ ' ' + backlink + "</p>"
|
+ ' ' + backlink + "</p>"
|
||||||
@ -2045,16 +2124,13 @@ class Markdown(object):
|
|||||||
else:
|
else:
|
||||||
return text
|
return text
|
||||||
|
|
||||||
# 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+);)')
|
|
||||||
_naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
|
_naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
|
||||||
_naked_gt_re = re.compile(r'''(?<![a-z0-9?!/'"-])>''', re.I)
|
_naked_gt_re = re.compile(r'''(?<![a-z0-9?!/'"-])>''', re.I)
|
||||||
|
|
||||||
def _encode_amps_and_angles(self, text):
|
def _encode_amps_and_angles(self, text):
|
||||||
# Smart processing for ampersands and angle brackets that need
|
# Smart processing for ampersands and angle brackets that need
|
||||||
# to be encoded.
|
# to be encoded.
|
||||||
text = self._ampersand_re.sub('&', text)
|
text = _AMPERSAND_RE.sub('&', text)
|
||||||
|
|
||||||
# Encode naked <'s
|
# Encode naked <'s
|
||||||
text = self._naked_lt_re.sub('<', text)
|
text = self._naked_lt_re.sub('<', text)
|
||||||
@ -2065,6 +2141,14 @@ class Markdown(object):
|
|||||||
text = self._naked_gt_re.sub('>', text)
|
text = self._naked_gt_re.sub('>', text)
|
||||||
return 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):
|
def _encode_backslash_escapes(self, text):
|
||||||
for ch, escape in list(self._escape_table.items()):
|
for ch, escape in list(self._escape_table.items()):
|
||||||
text = text.replace("\\"+ch, escape)
|
text = text.replace("\\"+ch, escape)
|
||||||
@ -2115,13 +2199,6 @@ class Markdown(object):
|
|||||||
return addr
|
return addr
|
||||||
|
|
||||||
def _do_link_patterns(self, text):
|
def _do_link_patterns(self, text):
|
||||||
"""Caveat emptor: there isn't much guarding against link
|
|
||||||
patterns being formed inside other standard Markdown links, e.g.
|
|
||||||
inside a [link def][like this].
|
|
||||||
|
|
||||||
Dev Notes: *Could* consider prefixing regexes with a negative
|
|
||||||
lookbehind assertion to attempt to guard against this.
|
|
||||||
"""
|
|
||||||
link_from_hash = {}
|
link_from_hash = {}
|
||||||
for regex, repl in self.link_patterns:
|
for regex, repl in self.link_patterns:
|
||||||
replacements = []
|
replacements = []
|
||||||
@ -2132,6 +2209,20 @@ class Markdown(object):
|
|||||||
href = match.expand(repl)
|
href = match.expand(repl)
|
||||||
replacements.append((match.span(), href))
|
replacements.append((match.span(), href))
|
||||||
for (start, end), href in reversed(replacements):
|
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 = (
|
escaped_href = (
|
||||||
href.replace('"', '"') # b/c of attr quote
|
href.replace('"', '"') # b/c of attr quote
|
||||||
# To avoid markdown <em> and <strong>:
|
# To avoid markdown <em> and <strong>:
|
||||||
@ -2173,46 +2264,48 @@ class MarkdownWithExtras(Markdown):
|
|||||||
|
|
||||||
# ---- internal support functions
|
# ---- 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):
|
class UnicodeWithAttrs(unicode):
|
||||||
"""A subclass of unicode used for the return value of conversion to
|
"""A subclass of unicode used for the return value of conversion to
|
||||||
possibly attach some attributes. E.g. the "toc_html" attribute when
|
possibly attach some attributes. E.g. the "toc_html" attribute when
|
||||||
the "toc" extra is used.
|
the "toc" extra is used.
|
||||||
"""
|
"""
|
||||||
metadata = None
|
metadata = None
|
||||||
_toc = None
|
toc_html = None
|
||||||
def toc_html(self):
|
|
||||||
"""Return the HTML for the current TOC.
|
|
||||||
|
|
||||||
This expects the `_toc` attribute to have been set on this instance.
|
|
||||||
"""
|
|
||||||
if self._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 self._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'
|
|
||||||
toc_html = property(toc_html)
|
|
||||||
|
|
||||||
## {{{ http://code.activestate.com/recipes/577257/ (r1)
|
## {{{ http://code.activestate.com/recipes/577257/ (r1)
|
||||||
_slugify_strip_re = re.compile(r'[^\w\s-]')
|
_slugify_strip_re = re.compile(r'[^\w\s-]')
|
||||||
@ -2433,8 +2526,9 @@ def _xml_escape_attr(attr, skip_single_quote=True):
|
|||||||
By default this doesn't bother with escaping `'` to `'`, presuming that
|
By default this doesn't bother with escaping `'` to `'`, presuming that
|
||||||
the tag attribute is surrounded by double quotes.
|
the tag attribute is surrounded by double quotes.
|
||||||
"""
|
"""
|
||||||
|
escaped = _AMPERSAND_RE.sub('&', attr)
|
||||||
|
|
||||||
escaped = (attr
|
escaped = (attr
|
||||||
.replace('&', '&')
|
|
||||||
.replace('"', '"')
|
.replace('"', '"')
|
||||||
.replace('<', '<')
|
.replace('<', '<')
|
||||||
.replace('>', '>'))
|
.replace('>', '>'))
|
||||||
@ -2457,12 +2551,15 @@ def _xml_encode_email_char_at_random(ch):
|
|||||||
return '&#%s;' % ord(ch)
|
return '&#%s;' % ord(ch)
|
||||||
|
|
||||||
|
|
||||||
def _urlencode(attr, safe_mode=False):
|
def _html_escape_url(attr, safe_mode=False):
|
||||||
"""Replace special characters in string using the %xx escape."""
|
"""Replace special characters that are potentially malicious in url string."""
|
||||||
|
escaped = (attr
|
||||||
|
.replace('"', '"')
|
||||||
|
.replace('<', '<')
|
||||||
|
.replace('>', '>'))
|
||||||
if safe_mode:
|
if safe_mode:
|
||||||
escaped = quote_plus(attr).replace('+', ' ')
|
escaped = escaped.replace('+', ' ')
|
||||||
else:
|
escaped = escaped.replace("'", "'")
|
||||||
escaped = attr.replace('"', '%22')
|
|
||||||
return escaped
|
return escaped
|
||||||
|
|
||||||
|
|
||||||
@ -2587,7 +2684,8 @@ def main(argv=None):
|
|||||||
html4tags=opts.html4tags,
|
html4tags=opts.html4tags,
|
||||||
safe_mode=opts.safe_mode,
|
safe_mode=opts.safe_mode,
|
||||||
extras=extras, link_patterns=link_patterns,
|
extras=extras, link_patterns=link_patterns,
|
||||||
use_file_vars=opts.use_file_vars)
|
use_file_vars=opts.use_file_vars,
|
||||||
|
cli=True)
|
||||||
if py3:
|
if py3:
|
||||||
sys.stdout.write(html)
|
sys.stdout.write(html)
|
||||||
else:
|
else:
|
||||||
|
|||||||
18
live-testing/images.md
Normal file
18
live-testing/images.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
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):
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This is the first image from the local file system, *relative* path!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
This is the first image from the internet!
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc
|
||||||
BIN
live-testing/sublime_merge.png
Normal file
BIN
live-testing/sublime_merge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
live-testing/sublime_text.png
Normal file
BIN
live-testing/sublime_text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
18
live-testing/test.md
Normal file
18
live-testing/test.md
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
# hello world
|
||||||
|
|
||||||
|
This is a *test*. Some inline `[2]code()`.
|
||||||
|
|
||||||
|
what the hell...
|
||||||
|
|
||||||
|
```python
|
||||||
|
import this
|
||||||
|
|
||||||
|
if input("answer yes") != 'yes':
|
||||||
|
print("Really?")
|
||||||
|
```
|
||||||
|
|
||||||
|
this flickering is really annoying...
|
||||||
|
|
||||||
|
It looks like it's gone... Oh wait nah, it's still here...
|
||||||
|
|
||||||
|
This should still be working, and it is!
|
||||||
@ -1 +0,0 @@
|
|||||||
data:image/png;base64,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=
|
|
||||||
119
markdown2html.py
Normal file
119
markdown2html.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import copy
|
||||||
|
import os.path
|
||||||
|
import concurrent.futures
|
||||||
|
import urllib.request
|
||||||
|
import base64
|
||||||
|
import bs4
|
||||||
|
|
||||||
|
from functools import lru_cache, partial
|
||||||
|
|
||||||
|
from .lib.markdown2 import Markdown
|
||||||
|
|
||||||
|
__all__ = ('markdown2html', )
|
||||||
|
|
||||||
|
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)
|
||||||
|
executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
|
||||||
|
|
||||||
|
images_cache = {}
|
||||||
|
|
||||||
|
class LoadingError(Exception):
|
||||||
|
pass
|
||||||
|
|
||||||
|
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
|
||||||
|
call when an image has finished loading to retrigger a render (see #90)
|
||||||
|
"""
|
||||||
|
html = markdowner.convert(markdown)
|
||||||
|
|
||||||
|
soup = bs4.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, re_render)
|
||||||
|
except FileNotFoundError as e:
|
||||||
|
base64 = resources['base64_404_image']
|
||||||
|
except LoadingError:
|
||||||
|
base64 = resources['base64_loading_image']
|
||||||
|
|
||||||
|
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?
|
||||||
|
|
||||||
|
# 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 method sucks, but can we do better?
|
||||||
|
fixed_pre = str(code_element) \
|
||||||
|
.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
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
|
||||||
|
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))
|
||||||
|
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')
|
||||||
|
|
||||||
|
# FIXME: wait what the hell? Why do I have two caches? (lru and images_cache)
|
||||||
|
# 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')
|
||||||
@ -1,5 +1,7 @@
|
|||||||
{
|
{
|
||||||
"install": "messages/install.txt",
|
"install": "messages/install.txt",
|
||||||
"1.1.2": "messages/1.1.2.txt",
|
"1.1.2": "messages/1.1.2.txt",
|
||||||
"2.0.1": "messages/2.0.1.txt"
|
"2.0.1": "messages/2.0.1.txt",
|
||||||
|
"2.2.1": "messages/2.2.0.txt",
|
||||||
|
"2.4.1": "messages/2.4.txt"
|
||||||
}
|
}
|
||||||
|
|||||||
8
messages/2.1.0.txt
Normal file
8
messages/2.1.0.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
A settings is now available: `markdown_live_preview_on_open`. If set to true,
|
||||||
|
it opens the window preview as soon as you open a markdown file. See the
|
||||||
|
README for more infos.
|
||||||
|
|
||||||
|
Tip of the day: `ctrl+w`: closes the current file
|
||||||
|
`ctrl+shift+w`: closes the current window
|
||||||
8
messages/2.2.0.txt
Normal file
8
messages/2.2.0.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
Something changed on MarkdownLivePreview: the preview is now scrolled to where
|
||||||
|
you are editing your markdown file! It doesn't scroll back up all the time any
|
||||||
|
more!
|
||||||
|
|
||||||
|
Tip of the day: You can center the screen on your cursor by pressing:
|
||||||
|
`ctrl+k, ctrl+v` (on OSX `cmd+k, cmd+v`)
|
||||||
10
messages/2.4.txt
Normal file
10
messages/2.4.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
Some stuff changed on MarkdownLivePreview. It now supports YAML/TOML front matters. You can hide it,
|
||||||
|
or show it in a pre block (edit your settings for this).
|
||||||
|
|
||||||
|
Hope you'll enjoy it!
|
||||||
|
|
||||||
|
Tip of the day: If you want a VIM-like search feature, then just press 'ctrl/cmd+i'
|
||||||
|
(Find → Incremental find). You can still go the next match by pressing 'f3', and to
|
||||||
|
the previous one by pressing 'shift+f3'
|
||||||
@ -10,10 +10,10 @@ Thanks for installing MarkdownLivePreview! I hope you'll enjoy using it!
|
|||||||
Quick Start:
|
Quick Start:
|
||||||
~~~~~~~~~~~~
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
To enable MarkdownLivePreview, you need to me on a markdown view (works with
|
To enable MarkdownLivePreview, you need to be on a markdown view (works with
|
||||||
Markdown Extended). Then just press `alt+m`, or search up in the command
|
Markdown Extended). Then just press `alt+m`, or search up in the command
|
||||||
palette: 'MarkdownLivePreview: Toggle'. Hit enter and you're done. As soon as
|
palette: 'MarkdownLivePreview: Toggle'. Hit enter and you're done. As soon as
|
||||||
you'll type anything in, it'll show up the preview in a new group.
|
you type anything in, it'll show up with the preview in a new group.
|
||||||
|
|
||||||
Say thanks:
|
Say thanks:
|
||||||
~~~~~~~~~~~
|
~~~~~~~~~~~
|
||||||
|
|||||||
1
resources/404.base64
Normal file
1
resources/404.base64
Normal file
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
9
resources/convertresources.py
Normal file
9
resources/convertresources.py
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
""" A small script to convert the images into base64 data """
|
||||||
|
|
||||||
|
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()))
|
||||||
1
resources/loading.base64
Normal file
1
resources/loading.base64
Normal file
@ -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=
|
||||||
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 953 B |
44
resources/stylesheet.css
Normal file
44
resources/stylesheet.css
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
html {
|
||||||
|
--light-bg: color(var(--background) blend(#fff 90%));
|
||||||
|
--very-light-bg: color(var(--background) blend(#fff 85%));
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
font-family: "Ubuntu", "DejaVu Sans", "Open Sans", sans-serif;
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
i.space {
|
||||||
|
color: var(--very-light-bg);
|
||||||
|
}
|
||||||
15
sample.md
15
sample.md
@ -1,15 +0,0 @@
|
|||||||
# DuckDuckGo - The Search engine you'll fall in love
|
|
||||||
|
|
||||||
Hope you'll enjoy using MarkdownLivePreview!
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
```python
|
|
||||||
print('Hello world')
|
|
||||||
if DEBUG:
|
|
||||||
print('DEBUG_MODE on')
|
|
||||||
```
|
|
||||||
|
|
||||||
> Only a fool knows everything. A wise man knows how little he knows
|
|
||||||
|
|
||||||
The only think I know right now is that Boxy Theme's just awesome.
|
|
||||||
@ -1,8 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
PREVIEW_ENABLED = 'markdown_live_preview_enabled'
|
|
||||||
PREVIEW_ID = 'markdown_live_preview_id'
|
|
||||||
IS_PREVIEW = 'is_markdown_live_preview'
|
|
||||||
IS_HIDDEN = 'is_hidden_markdown_live_preview'
|
|
||||||
MD_VIEW_ID = 'markdown_live_preview_md_id'
|
|
||||||
PREVIEW_WINDOW = 'markdown_live_preview_window'
|
|
||||||
23
utils.py
Normal file
23
utils.py
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
# import sublime
|
||||||
|
import time
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
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
|
||||||
Reference in New Issue
Block a user