Merge remote-tracking branch 'update/master'
This is a weird merge. I had re-written everything in a separate repository, and basically wanted that new repo (update) to be the new master of this repository, whilst preserving the history of the update repo. Here's what I did: $ git remote add update <path to update> $ git fetch update $ git merge -X theirs --allow-unrelated-histories update/master $ # remove extra files $ git commit --amend
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
docs/ export-ignore
|
||||||
|
resources/*.png export-ignore
|
||||||
|
resources/*.py export-ignore
|
||||||
6
.gitignore
vendored
@ -1,5 +1 @@
|
|||||||
Thumbs.db
|
__pycache__
|
||||||
__pycache__/
|
|
||||||
cache.txt
|
|
||||||
venv/
|
|
||||||
site/
|
|
||||||
|
|||||||
@ -1,13 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"keys": ["alt+m"],
|
|
||||||
"command": "new_markdown_live_preview",
|
|
||||||
"context": [
|
|
||||||
{
|
|
||||||
"key": "selector",
|
|
||||||
"operator": "equal",
|
|
||||||
"operand": "text.html.markdown"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
@ -1,33 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "preferences",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "package-settings",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"caption": "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\n}\n"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"caption": "Style - CSS",
|
|
||||||
"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,25 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview: Edit Current File",
|
|
||||||
"command": "new_markdown_live_preview"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview: Clear the cache",
|
|
||||||
"command": "markdown_live_preview_clear_cache"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview: Edit Custom CSS File",
|
|
||||||
"command": "open_file",
|
|
||||||
"args": {
|
|
||||||
"file": "$packages/User/MarkdownLivePreview.css"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"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,19 +0,0 @@
|
|||||||
{
|
|
||||||
// As soon as you open a markdown file, it opens the window preview
|
|
||||||
"markdown_live_preview_on_open": false,
|
|
||||||
|
|
||||||
// If an image starts with one of those strings, then it will be loaded from internet
|
|
||||||
"load_from_internet_when_starts": ["http://", "https://"],
|
|
||||||
|
|
||||||
// When the preview is opened, the markdown file is closed in the origin window and reopend in
|
|
||||||
// the preview window. If this option is set to 'true', then the markdown file will NOT be
|
|
||||||
// closed in the origin window
|
|
||||||
"keep_open_when_opening_preview": false,
|
|
||||||
|
|
||||||
// Choose what to do with YAML/TOML (---/+++ respectively) headers
|
|
||||||
// Valid values: "wrap_in_pre", "remove".
|
|
||||||
"header_action": "wrap_in_pre",
|
|
||||||
|
|
||||||
// Wait at least the specified *seconds* before updating the preview.
|
|
||||||
"update_preview_every": 0
|
|
||||||
}
|
|
||||||
@ -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>
|
|
||||||
106
MLPApi.py
@ -1,106 +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 .lib.pre_tables import pre_tables
|
|
||||||
|
|
||||||
from .escape_amp import *
|
|
||||||
from .functions import *
|
|
||||||
from .setting_names import *
|
|
||||||
from .image_manager import CACHE_FILE
|
|
||||||
from random import randint as rnd
|
|
||||||
|
|
||||||
__folder__ = os.path.dirname(__file__)
|
|
||||||
|
|
||||||
|
|
||||||
# used to store the phantom's set
|
|
||||||
windows_phantom_set = {}
|
|
||||||
|
|
||||||
|
|
||||||
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 markdown2html(md, basepath, color_scheme):
|
|
||||||
|
|
||||||
# removes/format the YAML/TOML header.
|
|
||||||
md = manage_header(md, get_settings().get('header_action'))
|
|
||||||
|
|
||||||
html = '<style>\n{}\n</style>\n'.format(get_style(color_scheme))
|
|
||||||
|
|
||||||
|
|
||||||
# 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 += md2.markdown(md, extras=['fenced-code-blocks', 'tables', 'strike'])
|
|
||||||
|
|
||||||
# tables aren't supported by the Phantoms
|
|
||||||
# This function transforms them into aligned ASCII tables and displays them in a <pre> block
|
|
||||||
# (the ironic thing is that they aren't supported either :D)
|
|
||||||
html = pre_tables(html)
|
|
||||||
|
|
||||||
# pre block are not supported by the Phantoms.
|
|
||||||
# This functions replaces the \n in them with <br> so that it does (1/2)
|
|
||||||
html = pre_with_br(html)
|
|
||||||
|
|
||||||
# comments aren't supported by the Phantoms
|
|
||||||
# Simply removes them using bs4, so you can be sadic and type `<!-- hey hey! -->`, these one
|
|
||||||
# won't be stripped!
|
|
||||||
html = strip_html_comments(html)
|
|
||||||
|
|
||||||
# exception, again, because <pre> aren't supported by the phantoms
|
|
||||||
# so, because this is monosaped font, I just replace it with a '.' and make transparent ;)
|
|
||||||
html = html.replace(' ', '<i class="space">.</i>')
|
|
||||||
|
|
||||||
# Phantoms have problem with images size when they're loaded from an url/path
|
|
||||||
# So, the solution is to convert them to base64
|
|
||||||
html = replace_img_src_base64(html, basepath=basepath)
|
|
||||||
|
|
||||||
# BeautifulSoup uses the <br/> but the sublime phantoms do not support them...
|
|
||||||
html = html.replace('<br/>', '<br />').replace('<hr/>', '<hr />')
|
|
||||||
|
|
||||||
return html
|
|
||||||
|
|
||||||
def show_html(md_view, preview):
|
|
||||||
global windows_phantom_set
|
|
||||||
html = markdown2html(get_view_content(md_view), os.path.dirname(md_view.file_name()), md_view.settings().get('color_scheme'))
|
|
||||||
|
|
||||||
phantom_set = windows_phantom_set.setdefault(preview.window().id(),
|
|
||||||
sublime.PhantomSet(preview, 'markdown_live_preview'))
|
|
||||||
phantom_set.update([sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK,
|
|
||||||
lambda href: sublime.run_command('open_url', {'url': href}))])
|
|
||||||
|
|
||||||
# lambda href: sublime.run_command('open_url', {'url': href})
|
|
||||||
# get the "ratio" of the markdown view's position.
|
|
||||||
# 0 < y < 1
|
|
||||||
y = md_view.text_to_layout(md_view.sel()[0].begin())[1] / md_view.layout_extent()[1]
|
|
||||||
# set the vector (position) for the preview
|
|
||||||
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
|
|
||||||
# make sure the minimum is 0
|
|
||||||
vector[1] = 0 if vector[1] < 0 else vector[1]
|
|
||||||
# the hide the first line
|
|
||||||
vector[1] += preview.line_height()
|
|
||||||
preview.set_viewport_position(vector, animate=False)
|
|
||||||
|
|
||||||
def clear_cache():
|
|
||||||
"""Removes the cache file"""
|
|
||||||
os.remove(CACHE_FILE)
|
|
||||||
|
|
||||||
def release_phantoms_set(view_id=None):
|
|
||||||
global windows_phantom_set
|
|
||||||
if view_id is None:
|
|
||||||
windows_phantom_set = {}
|
|
||||||
else:
|
|
||||||
del windows_phantom_set[view_id]
|
|
||||||
@ -1,124 +1,223 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
import os.path
|
||||||
|
|
||||||
import sublime
|
import sublime
|
||||||
import sublime_plugin
|
import sublime_plugin
|
||||||
import time
|
|
||||||
|
|
||||||
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()
|
|
||||||
if get_settings().get('keep_open_when_opening_preview') is False:
|
def plugin_loaded():
|
||||||
current_view.close()
|
resources["base64_loading_image"] = get_resource('loading.base64')
|
||||||
if file_name is None:
|
resources["base64_404_image"] = get_resource('404.base64')
|
||||||
return sublime.error_message('MarkdownLivePreview: Not supporting '
|
resources["stylesheet"] = get_resource('stylesheet.css')
|
||||||
'unsaved file for now')
|
|
||||||
|
# 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
|
||||||
now = time.time()
|
}
|
||||||
|
|
||||||
if now - vsettings.get(LAST_UPDATE, 0) < get_settings().get('update_preview_every'):
|
# we schedule an update for every key stroke, with a delay of DELAY
|
||||||
return
|
# then, we update only if now() - last_update > DELAY
|
||||||
vsettings.set(LAST_UPDATE, now)
|
last_update = 0
|
||||||
if not vsettings.get(PREVIEW_ENABLED):
|
|
||||||
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)
|
# FIXME: maybe we shouldn't restore the file in the original window...
|
||||||
return view, preview
|
|
||||||
|
|
||||||
def on_modified_async(self, view):
|
def on_pre_close(self, markdown_view):
|
||||||
if not is_markdown_view(view): # faster than getting the settings
|
""" 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
|
||||||
delay = get_settings().get('update_preview_every')
|
|
||||||
if not delay:
|
self.markdown_view = markdown_view
|
||||||
self.update(view)
|
self.preview_window = markdown_view.window()
|
||||||
|
self.file_name = markdown_view.file_name()
|
||||||
|
|
||||||
|
if self.file_name is None:
|
||||||
|
total_region = sublime.Region(0, markdown_view.size())
|
||||||
|
self.content = markdown_view.substr(total_region)
|
||||||
|
markdown_view.erase(edit, total_region)
|
||||||
else:
|
else:
|
||||||
sublime.set_timeout(lambda: self.update(view), delay * 1000)
|
self.content = None
|
||||||
|
|
||||||
def on_window_command(self, window, command, args):
|
def on_load_async(self, markdown_view):
|
||||||
if command == 'close' and window.settings().get(PREVIEW_WINDOW):
|
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||||
release_phantoms_set(window.id())
|
if not infos:
|
||||||
return 'close_window', {}
|
|
||||||
|
|
||||||
def on_activated_async(self, view):
|
|
||||||
vsettings = view.settings()
|
|
||||||
|
|
||||||
if (is_markdown_view(view) and get_settings().get(ON_OPEN)
|
|
||||||
and not vsettings.get(PREVIEW_ENABLED)
|
|
||||||
and vsettings.get('syntax') != 'Packages/MarkdownLivePreview/' + \
|
|
||||||
'.sublime/MarkdownLivePreviewSyntax' + \
|
|
||||||
'.hidden-tmLanguage'
|
|
||||||
and not any(filter(lambda window: window.settings().get(PREVIEW_WINDOW) is True,
|
|
||||||
sublime.windows()))):
|
|
||||||
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')
|
preview_view = markdown_view.window().active_view_in_group(1)
|
||||||
show_minimap = psettings.get('show_minimap')
|
|
||||||
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:
|
self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view)
|
||||||
window.set_tabs_visible(show_tabs)
|
self._update_preview(markdown_view)
|
||||||
if show_minimap is not None:
|
|
||||||
window.set_minimap_visible(show_minimap)
|
def on_close(self, markdown_view):
|
||||||
if show_status_bar is not None:
|
""" Use the information saved to restore the markdown_view as an original_view
|
||||||
window.set_status_bar_visible(show_status_bar)
|
"""
|
||||||
if show_sidebar is not None:
|
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||||
window.set_sidebar_visible(show_sidebar)
|
if not infos:
|
||||||
if show_menus is not None:
|
return
|
||||||
window.set_menu_visible(show_menus)
|
|
||||||
|
assert markdown_view.id() == self.markdown_view.id(), \
|
||||||
|
"pre_close view.id() != close view.id()"
|
||||||
|
|
||||||
|
del self.phantom_sets[markdown_view.id()]
|
||||||
|
|
||||||
|
self.preview_window.run_command('close_window')
|
||||||
|
|
||||||
|
# find the window with the right id
|
||||||
|
original_window = next(window for window in sublime.windows() \
|
||||||
|
if window.id() == infos['original_window_id'])
|
||||||
|
if self.file_name:
|
||||||
|
original_window.open_file(self.file_name)
|
||||||
|
else:
|
||||||
|
assert markdown_view.is_scratch(), "markdown view of an unsaved file should " \
|
||||||
|
"be a scratch"
|
||||||
|
# note here that this is called original_view, because it's what semantically
|
||||||
|
# makes sense, but this original_view.id() will be different than the one
|
||||||
|
# that we closed first to reopen in the preview window
|
||||||
|
# shouldn't cause any trouble though
|
||||||
|
original_view = original_window.new_file()
|
||||||
|
original_view.run_command('mdlp_insert', {'point': 0, 'string': self.content})
|
||||||
|
|
||||||
|
original_view.set_syntax_file(markdown_view.settings().get('syntax'))
|
||||||
|
|
||||||
|
|
||||||
|
# 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):
|
||||||
|
|
||||||
|
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||||
|
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}))
|
||||||
|
])
|
||||||
|
|
||||||
class MarkdownLivePreviewClearCacheCommand(sublime_plugin.ApplicationCommand):
|
|
||||||
|
|
||||||
def run(self):
|
|
||||||
clear_cache()
|
|
||||||
|
|||||||
7
MarkdownLivePreview.sublime-commands
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
[
|
||||||
|
|
||||||
|
{
|
||||||
|
"caption": "MarkdownLivePreview: Open Preview",
|
||||||
|
"command": "open_markdown_preview"
|
||||||
|
}
|
||||||
|
]
|
||||||
65
README.md
@ -1,63 +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!
|
||||||
|
|
||||||
## Unmaintained
|
## How to install
|
||||||
|
|
||||||
I am now using vim. I don't have the energy or the time to maintain this plugin anymore.
|
It's available on package control!
|
||||||
|
|
||||||
If anyone is interested in maintaining it, fork it, and submit a PR to package control to make it point to your fork.
|
## How to contribute
|
||||||
|
|
||||||
### Dependencies
|
1. Fork this repo
|
||||||
|
2. Make your own branch (the name of the branch should be the feature you are
|
||||||
|
implementing eg. `improve-tables`, `fix-crash-on-multiple-preview`
|
||||||
|
3. All your code should be formated by black.
|
||||||
|
4. Send a PR!
|
||||||
|
|
||||||
**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.
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
MarkdownLivePreview is available on the default channel of
|
|
||||||
[PackageControl](http://packagecontrol.io), which means you just have to
|
|
||||||
|
|
||||||
1. Open the command palette (`ctrl+shift+p`)
|
|
||||||
2. Search for: `Package Control: Install Package`
|
|
||||||
3. Search for: `MarkdownLivePreview`
|
|
||||||
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. :wink:
|
|
||||||
|
|
||||||
### 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.
|
|
||||||
|
|
||||||
So, once you've run it, it will open a new window, with only your markdown file, with the preview.
|
|
||||||
Once you're 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*
|
|
||||||
|
|
||||||
For further infos, please [read the docs](https://math2001.github.io/MarkdownLivePreview/)!
|
|
||||||
|
|
||||||
### Demo
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
### Somethings wrong!!
|
|
||||||
|
|
||||||
If you find that something's wrong with this package, you can let me know by raising an issue on the
|
|
||||||
[GitHub issue tracker][github-issue-tracker]
|
|
||||||
|
|
||||||
### How to open the [README][]
|
|
||||||
|
|
||||||
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
|
|
||||||
[github-issue-tracker]: https://github.com/math2001/MarkdownLivePreview/issues
|
|
||||||
[st-css-rules]: http://www.sublimetext.com/docs/3/minihtml.html#css
|
|
||||||
[README]: http://github.com/math2001/MarkdownLivePreview/README.md
|
|
||||||
|
|||||||
63
default.css
@ -1,63 +0,0 @@
|
|||||||
html {
|
|
||||||
--light-bg: color(var(--background) blend(#999 85%));
|
|
||||||
--very-light-bg: color(var(--background) blend(#999 92%));
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding-left: 0.2rem;
|
|
||||||
padding-right: 0.2rem;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
|
|
||||||
div.codehilite {
|
|
||||||
display: block;
|
|
||||||
margin-top: 20px;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
padding-left: 10px;
|
|
||||||
padding-top: 5px;
|
|
||||||
padding-bottom: 5px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre code .space {
|
|
||||||
color: var(--light-bg);
|
|
||||||
}
|
|
||||||
|
|
||||||
pre.table {
|
|
||||||
background-color: var(--background);
|
|
||||||
}
|
|
||||||
pre.table code {
|
|
||||||
background-color: var(--background);
|
|
||||||
padding: 0;
|
|
||||||
margin: 0;
|
|
||||||
}
|
|
||||||
pre.table code .space {
|
|
||||||
color: var(--background);
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
kbd {
|
|
||||||
padding: 0 5px;
|
|
||||||
background-color: var(--very-light-bg);
|
|
||||||
font-family: "Roboto Mono","Courier New",Courier,monospace;
|
|
||||||
}
|
|
||||||
@ -1,8 +1,7 @@
|
|||||||
{
|
{
|
||||||
"*": {
|
"*": {
|
||||||
"*": [
|
"*": [
|
||||||
"bs4",
|
"bs4"
|
||||||
"pygments"
|
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -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):
|
|
||||||
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',
|
|
||||||
'MarkdownLivePreview.py'),
|
|
||||||
'scripts': ['image_manager', 'functions', 'MLPApi',
|
|
||||||
'setting_names'],
|
|
||||||
'folders': ['lib'],
|
|
||||||
'quiet': True
|
|
||||||
})
|
|
||||||
@ -1,92 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
viewBox="0 0 99.999996 100"
|
|
||||||
id="svg2"
|
|
||||||
version="1.1"
|
|
||||||
inkscape:version="0.91 r13725"
|
|
||||||
sodipodi:docname="MarkdownLivePreview.svg">
|
|
||||||
<defs
|
|
||||||
id="defs4" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#2196f3"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="2.8"
|
|
||||||
inkscape:cx="36.574985"
|
|
||||||
inkscape:cy="6.9424003"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
showborder="true"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1018"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
fit-margin-top="0"
|
|
||||||
fit-margin-left="0"
|
|
||||||
fit-margin-right="0"
|
|
||||||
fit-margin-bottom="0"
|
|
||||||
inkscape:showpageshadow="false"
|
|
||||||
units="px"
|
|
||||||
showguides="true"
|
|
||||||
inkscape:guide-bbox="true"
|
|
||||||
inkscape:snap-global="false" />
|
|
||||||
<metadata
|
|
||||||
id="metadata7">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-349.87364,-434.14672)">
|
|
||||||
<g
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:59.90521622px;font-family:Candal;-inkscape-font-specification:Candal;text-align:start;text-anchor:start;opacity:1;fill:#e1ecff;fill-opacity:1;stroke:#ef524f;stroke-width:3.75156426;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="text7027"
|
|
||||||
transform="translate(-0.13506381,-12.064395)">
|
|
||||||
<path
|
|
||||||
d="m 352.04084,461.61747 12.16825,0 10.2962,16.08783 10.29621,-16.08783 12.46076,0 0,41.36034 -13.39678,0 0,-22.46445 -9.65269,16.35108 -2.83731,0 -9.97445,-16.35108 0,22.46445 -9.36019,0 0,-41.36034 z"
|
|
||||||
style="fill:#e1ecff;fill-opacity:1;stroke:none"
|
|
||||||
id="path7039"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="matrix(-1,0,0,1,-0.13506381,-12.064395)"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:59.90521622px;font-family:Candal;-inkscape-font-specification:Candal;text-align:start;text-anchor:start;opacity:1;fill:#b9d5ff;fill-opacity:1;stroke:#ef524f;stroke-width:3.75156426;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="text7031">
|
|
||||||
<path
|
|
||||||
d="m -447.97656,461.61747 12.16825,0 10.29621,16.08783 10.29621,-16.08783 12.46075,0 0,41.36034 -13.39677,0 0,-22.46445 -9.6527,16.35108 -2.8373,0 -9.97446,-16.35108 0,22.46445 -9.36019,0 0,-41.36034 z"
|
|
||||||
style="fill:#b9d5ff;fill-opacity:1;stroke:none"
|
|
||||||
id="path7036"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
style="opacity:0.75;fill:#cde0ff;fill-opacity:1;stroke:none;stroke-width:1.00199997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 375.63144,496.36803 24.28571,14.02137 24.28572,-14.02137 14.46428,0 -38.75,22.37233 -38.75,-22.37233 z"
|
|
||||||
id="path7081"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.8 KiB |
@ -1,92 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" standalone="no"?>
|
|
||||||
<!-- Created with Inkscape (http://www.inkscape.org/) -->
|
|
||||||
|
|
||||||
<svg
|
|
||||||
xmlns:dc="http://purl.org/dc/elements/1.1/"
|
|
||||||
xmlns:cc="http://creativecommons.org/ns#"
|
|
||||||
xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
|
||||||
xmlns:svg="http://www.w3.org/2000/svg"
|
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
|
||||||
xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
|
|
||||||
xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
|
|
||||||
width="100"
|
|
||||||
height="100"
|
|
||||||
viewBox="0 0 99.999996 100"
|
|
||||||
id="svg2"
|
|
||||||
version="1.1"
|
|
||||||
inkscape:version="0.91 r13725"
|
|
||||||
sodipodi:docname="MarkdownLivePreview.svg">
|
|
||||||
<defs
|
|
||||||
id="defs4" />
|
|
||||||
<sodipodi:namedview
|
|
||||||
id="base"
|
|
||||||
pagecolor="#ffffff"
|
|
||||||
bordercolor="#666666"
|
|
||||||
borderopacity="1.0"
|
|
||||||
inkscape:pageopacity="0.0"
|
|
||||||
inkscape:pageshadow="2"
|
|
||||||
inkscape:zoom="2.8"
|
|
||||||
inkscape:cx="36.574985"
|
|
||||||
inkscape:cy="6.9424003"
|
|
||||||
inkscape:document-units="px"
|
|
||||||
inkscape:current-layer="layer1"
|
|
||||||
showgrid="false"
|
|
||||||
showborder="true"
|
|
||||||
inkscape:window-width="1920"
|
|
||||||
inkscape:window-height="1018"
|
|
||||||
inkscape:window-x="-8"
|
|
||||||
inkscape:window-y="-8"
|
|
||||||
inkscape:window-maximized="1"
|
|
||||||
fit-margin-top="0"
|
|
||||||
fit-margin-left="0"
|
|
||||||
fit-margin-right="0"
|
|
||||||
fit-margin-bottom="0"
|
|
||||||
inkscape:showpageshadow="false"
|
|
||||||
units="px"
|
|
||||||
showguides="true"
|
|
||||||
inkscape:guide-bbox="true"
|
|
||||||
inkscape:snap-global="false" />
|
|
||||||
<metadata
|
|
||||||
id="metadata7">
|
|
||||||
<rdf:RDF>
|
|
||||||
<cc:Work
|
|
||||||
rdf:about="">
|
|
||||||
<dc:format>image/svg+xml</dc:format>
|
|
||||||
<dc:type
|
|
||||||
rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
|
|
||||||
<dc:title></dc:title>
|
|
||||||
</cc:Work>
|
|
||||||
</rdf:RDF>
|
|
||||||
</metadata>
|
|
||||||
<g
|
|
||||||
inkscape:label="Layer 1"
|
|
||||||
inkscape:groupmode="layer"
|
|
||||||
id="layer1"
|
|
||||||
transform="translate(-349.87364,-434.14672)">
|
|
||||||
<g
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:59.90521622px;font-family:Candal;-inkscape-font-specification:Candal;text-align:start;text-anchor:start;opacity:1;fill:#5599ff;fill-opacity:1;stroke:#ef524f;stroke-width:3.75156426;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="text7027"
|
|
||||||
transform="translate(-0.13506381,-12.064395)">
|
|
||||||
<path
|
|
||||||
d="m 352.04084,461.61747 12.16825,0 10.2962,16.08783 10.29621,-16.08783 12.46076,0 0,41.36034 -13.39678,0 0,-22.46445 -9.65269,16.35108 -2.83731,0 -9.97445,-16.35108 0,22.46445 -9.36019,0 0,-41.36034 z"
|
|
||||||
style="fill:#5599ff;fill-opacity:1;stroke:none"
|
|
||||||
id="path7039"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
<g
|
|
||||||
transform="matrix(-1,0,0,1,-0.13506381,-12.064395)"
|
|
||||||
style="font-style:normal;font-variant:normal;font-weight:normal;font-stretch:normal;font-size:59.90521622px;font-family:Candal;-inkscape-font-specification:Candal;text-align:start;text-anchor:start;opacity:0.75;fill:#5599ff;fill-opacity:1;stroke:#ef524f;stroke-width:3.75156426;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
id="text7031">
|
|
||||||
<path
|
|
||||||
d="m -447.97656,461.61747 12.16825,0 10.29621,16.08783 10.29621,-16.08783 12.46075,0 0,41.36034 -13.39677,0 0,-22.46445 -9.6527,16.35108 -2.8373,0 -9.97446,-16.35108 0,22.46445 -9.36019,0 0,-41.36034 z"
|
|
||||||
style="fill:#5599ff;fill-opacity:1;stroke:none"
|
|
||||||
id="path7036"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
<path
|
|
||||||
style="opacity:0.75;fill:#aaccff;fill-opacity:1;stroke:none;stroke-width:1.00199997;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1"
|
|
||||||
d="m 375.63144,496.36803 24.28571,14.02137 24.28572,-14.02137 14.46428,0 -38.75,22.37233 -38.75,-22.37233 z"
|
|
||||||
id="path7081"
|
|
||||||
inkscape:connector-curvature="0" />
|
|
||||||
</g>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 3.9 KiB |
|
Before Width: | Height: | Size: 70 KiB |
184
docs/index.md
@ -1,184 +0,0 @@
|
|||||||
# Welcome to MarkdownLivePreview's documentation!
|
|
||||||
|
|
||||||
<img src="imgs/MarkdownLivePreview.svg" alt="MarkdownLivePreview's logo"
|
|
||||||
style="width: 400px; margin: auto; display: block;">
|
|
||||||
|
|
||||||
MarkdownLivePreview is a [Sublime Text 3][st] plugin to preview your markdown as you type,
|
|
||||||
*right in Sublime Text itself*, without *any* dependency!
|
|
||||||
|
|
||||||
It's very easy to use, but there's a few things that you might want to be aware of... So, let's
|
|
||||||
get started
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
### Using Package Control
|
|
||||||
|
|
||||||
You can really easily install MarkdownLivePreview by using [Package Control][pck-con].
|
|
||||||
|
|
||||||
If it's not already, you need to [install it][install-pck-con] first.
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
If you're using the latest build of Sublime Text 3, you can just do
|
|
||||||
*Tools → Install Package Control…*
|
|
||||||
|
|
||||||
- Open up the command palette (<kbd>ctrl+shift+p</kbd>)
|
|
||||||
- Search up `Package Control: Install Package` (might take a few seconds)
|
|
||||||
- In the panel that just showed up, search for `MarkdownLivePreview`
|
|
||||||
|
|
||||||
Done! You have now access to every single features of MarkdownLivePreview! :wink:
|
|
||||||
|
|
||||||
### Using `git`
|
|
||||||
|
|
||||||
```sh
|
|
||||||
$ cd "%APPDATA%\Sublime Text 3\Packages" # on Windows
|
|
||||||
$ cd ~/Library/Application\ Support/Sublime\ Text\ 3 # on Mac
|
|
||||||
$ cd ~/.config/sublime-text-3 # on Linux
|
|
||||||
|
|
||||||
$ git clone "https://github.com/math2001/MarkdownLivePreview"
|
|
||||||
```
|
|
||||||
|
|
||||||
> So, which one do I pick?!
|
|
||||||
|
|
||||||
I depends of what you want to do. If you want to just use MarkdownLivePreview, pick the first
|
|
||||||
solution, you'll get every update automatically. But if you want to contribute, then choose the
|
|
||||||
second solution.
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
### Previewing
|
|
||||||
|
|
||||||
As told in the introduction, MarkdownLivePreview is very easy to use:
|
|
||||||
|
|
||||||
- open a markdown file
|
|
||||||
- press <kbd>alt+m</kbd>
|
|
||||||
- or select in the command palette `MarkdownLivePreview: Edit Current File`
|
|
||||||
|
|
||||||
!!! note
|
|
||||||
The preview of unsaved markdown files is currently not supported. It should be fixed soon.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
[Markdown Extended][] is supported too!
|
|
||||||
|
|
||||||
That's it. That's all you need to do to preview your markdown!
|
|
||||||
|
|
||||||
### Settings
|
|
||||||
|
|
||||||
To edit MarkdownLivePreview's settings, you just need to search in the command palette
|
|
||||||
`Preferences: MarkdownLivePreview Settings`, or from the menus:
|
|
||||||
*Preferences → Package Settings → MarkdownLivePreview → Settings*
|
|
||||||
|
|
||||||
Do not edit the left file (by default, you cannot), but the right one. This right file will
|
|
||||||
override the default one (on the left), and will be saved in your `User` folder, which makes it easy
|
|
||||||
to back up.
|
|
||||||
|
|
||||||
- `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][] for its [suggestion][@ooing suggestion]). 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://"]`
|
|
||||||
- `header_action`: If you're writing a blog with some markdown and a static website generator, you
|
|
||||||
probably have a YAML header. By default, this header will be displayed in a `pre` block. If you want
|
|
||||||
to hide it, then just change the value to `remove`. Thanks to [@tanhanjay][] for
|
|
||||||
[letting me know][front-matter-issue]!
|
|
||||||
- `keep_open_when_opening_preview`: Each time the preview window is opened, the original markdown
|
|
||||||
view is closed. If you want to keep it opened, just set this setting to `true`
|
|
||||||
|
|
||||||
### Custom CSS
|
|
||||||
|
|
||||||
If you want to, you can add custom `CSS` to the MarkdownLivePreview's default stylesheet.
|
|
||||||
|
|
||||||
Just search for `MarkdownLivePreview: Edit Custom CSS File` in the command palette
|
|
||||||
(<kbd>ctrl+shift+p</kbd>). It will open a file in which you can add some CSS that will be *added* to
|
|
||||||
the normal CSS.
|
|
||||||
|
|
||||||
!!! bug
|
|
||||||
Comments in the CSS is interpreted weirdly by Sublime Text's phantoms. After a few tests, I
|
|
||||||
think that everything that is bellow a comment is ignored.
|
|
||||||
|
|
||||||
If you want to be sure that your CSS works, don't put any comments in it
|
|
||||||
|
|
||||||
#### Share your tweaks!
|
|
||||||
|
|
||||||
If you think that other users would enjoy your added CSS, then raise an issue, or PR the
|
|
||||||
[GitHub repo][] to share your tweaks!
|
|
||||||
|
|
||||||
### Clearing the cache
|
|
||||||
|
|
||||||
MarkdownLivePreview has a cache system to store images you load from internet. You can clear this
|
|
||||||
cache by searching up in the command palette `MarkdownLivePreview: Clear the cache`.
|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
The cache is one simple file called `MarkdownLivePreviewCache`, which is located in your temp
|
|
||||||
folder. To know where it is, you can open the Sublime Text console (<kbd>ctrl+`</kbd> or
|
|
||||||
*View → Show Console*), and paste this in:
|
|
||||||
|
|
||||||
```python
|
|
||||||
import tempfile; print(tempfile.gettempdir())
|
|
||||||
```
|
|
||||||
|
|
||||||
### Custom settings for the preview
|
|
||||||
|
|
||||||
Sublime Text makes it easy to set custom settings for a specific *type* of view. For example,
|
|
||||||
`markdown`, `python`, etc. MarkdownLivePreview takes advantage of that: the preview view (the view
|
|
||||||
on the right) is a specific syntax (called — sorry for the originality —
|
|
||||||
`MarkdownLivePreviewSyntax`). So, to change this, you can focus the right view, open up the command
|
|
||||||
palette (<kbd>ctrl+shift+p</kbd>), and search up `Preferences: Settings — Syntax Specific`. In here,
|
|
||||||
you can specify any settings that is going to be applied only to this view.
|
|
||||||
|
|
||||||
### The hacky part
|
|
||||||
|
|
||||||
In fact, MarkdownLivePreview parses those settings, and looks for specific ones:
|
|
||||||
|
|
||||||
- `show_tabs`
|
|
||||||
- `show_minimap`
|
|
||||||
- `show_status_bar`
|
|
||||||
- `show_sidebar`
|
|
||||||
- `show_menus`
|
|
||||||
|
|
||||||
Those settings aren't supported by default because they affect the entire *window* instead of just
|
|
||||||
the view. But MarkdownLivePreview will look for them in your *preview*'s settings, and hide/show the
|
|
||||||
tabs, the minimap, etc...
|
|
||||||
|
|
||||||
As you probably guessed those settings takes a bool for value (`true` or `false`).
|
|
||||||
|
|
||||||
### Recommendation
|
|
||||||
|
|
||||||
Here's what I'd recommend for your MarkdownLivePreviewSyntax's settings (and what I use):
|
|
||||||
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"show_menus": false,
|
|
||||||
"show_tabs": false,
|
|
||||||
"show_minimap": false,
|
|
||||||
"gutter": false,
|
|
||||||
"rulers": [],
|
|
||||||
"word_wrap": true
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
And here's what you'll get (With the awesome [Boxy Theme][] and its [Monokai Color Scheme][]):
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
!!! tip
|
|
||||||
On Windows at least, you can press <kbd>alt</kbd> to focus (so show) the menus, even if they're
|
|
||||||
originally hidden.
|
|
||||||
|
|
||||||
That's it! I hope you'll enjoy using this package! If it's the case, please let your friends know
|
|
||||||
about it, and even myself by sending me a [tweet][] or staring the repo!
|
|
||||||
<iframe
|
|
||||||
src="https://ghbtns.com/github-btn.html?user=math2001&repo=MarkdownLivePreview&type=star&count=true&size=large"
|
|
||||||
frameborder="0" scrolling="0" style="width: 120px; height: 30px; vertical-align: bottom"></iframe>
|
|
||||||
|
|
||||||
[st]: https://sublimetext.com
|
|
||||||
[Markdown Extended]: https://packagecontrol.io/packages/Markdown%20Extended
|
|
||||||
[pck-con]: https://packagecontrol.io
|
|
||||||
[install-pck-con]: https://packagecontrol.io/installation
|
|
||||||
[tweet]: https://twitter.com/_math2001
|
|
||||||
[GitHub repo]: https://github.com/math2001/MarkdownLivePreview/issues
|
|
||||||
[@ooing]: https://github.com/ooing
|
|
||||||
[@ooing suggestion]: https://github.com/math2001/MarkdownLivePreview/issues/7#issue-199464852
|
|
||||||
[@tanhanjay]: https://github.com/tanhanjay
|
|
||||||
[front-matter-issue]: https://github.com/math2001/MarkdownLivePreview/issues/17
|
|
||||||
[Boxy Theme]: https://packagecontrol.io/packages/Boxy%20Theme
|
|
||||||
[Monokai Color Scheme]: https://github.com/ihodev/sublime-boxy#boxy-monokai--predawn
|
|
||||||
@ -1,24 +0,0 @@
|
|||||||
This project is published under MIT license.
|
|
||||||
|
|
||||||
> The MIT License is a permissive license that is short and to the point. It lets people do anything
|
|
||||||
> they want with your code as long as they provide attribution back to you and don’t hold you
|
|
||||||
> liable.
|
|
||||||
>
|
|
||||||
> — *from [choosealicense.com](http://choosealicense.com), by [GitHub](https://github.com)*
|
|
||||||
|
|
||||||
Copyright 2017 Mathieu PATUREL
|
|
||||||
|
|
||||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
|
||||||
andassociated 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.
|
|
||||||
@ -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()
|
|
||||||
47
example.md
@ -1,47 +0,0 @@
|
|||||||
---
|
|
||||||
title: Demo
|
|
||||||
description: Preview your markdown right in Sublime Text!
|
|
||||||
hope: You'll enjoy using it!
|
|
||||||
---
|
|
||||||
|
|
||||||
# Hello world
|
|
||||||
|
|
||||||
<!-- supports comments -->
|
|
||||||
|
|
||||||
And `<!-- vicious ones ;) -->`
|
|
||||||
|
|
||||||
Some `inline code` with *italic*, **bold** text, and ~~strike through~~.
|
|
||||||
|
|
||||||
```python
|
|
||||||
import this
|
|
||||||
if you is moods.curious:
|
|
||||||
print('then do it!')
|
|
||||||
```
|
|
||||||
|
|
||||||
<kbd>ctrl+\`</kbd> or *View → Show Console* and paste `import this`!
|
|
||||||
|
|
||||||
> Perfect programmers do NOT need comments.
|
|
||||||
|
|
||||||
- to be efficient
|
|
||||||
- you need
|
|
||||||
- todos
|
|
||||||
|
|
||||||
|
|
||||||
| ID | Name |
|
|
||||||
|-----------|-------|
|
|
||||||
| 56 | Matt |
|
|
||||||
| 42 | Colin |
|
|
||||||
| 23 | Lisa |
|
|
||||||
| 45 | John |
|
|
||||||
| `<table>` | `><` |
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
Some plugin I just *need*:
|
|
||||||
|
|
||||||
- [PackageResourceReviewer](https://packagecontrol.io/packages/PackageResourceViewer)
|
|
||||||
- [Boxy Theme](https://packagecontrol.io/packages/Boxy%20Theme)
|
|
||||||
- [Markdown Preview](https://packagecontrol.io/packages/Markdown%20Preview)
|
|
||||||
- [FileManager](https://packagecontrol.io/packages/FileManager)
|
|
||||||
- [PlainTasks](https://packagecontrol.io/packages/PlainTasks)
|
|
||||||
- [JSONComma](https://packagecontrol.io/packages/JSONComma)
|
|
||||||
139
functions.py
@ -1,139 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
import base64
|
|
||||||
import os.path
|
|
||||||
import sublime
|
|
||||||
import re
|
|
||||||
from .image_manager import ImageManager
|
|
||||||
from .lib.pygments_from_theme import pygments_from_theme
|
|
||||||
from bs4 import BeautifulSoup, Comment as html_comment
|
|
||||||
|
|
||||||
def plugin_loaded():
|
|
||||||
global error404, loading, DEFAULT_STYLE, USER_STYLE_FILE
|
|
||||||
loading = sublime.load_resource('Packages/MarkdownLivePreview/loading.txt')
|
|
||||||
error404 = sublime.load_resource('Packages/MarkdownLivePreview/404.txt')
|
|
||||||
|
|
||||||
DEFAULT_STYLE = sublime.load_resource('Packages/MarkdownLivePreview/default.css')
|
|
||||||
USER_STYLE_FILE = os.path.join(sublime.packages_path(), 'User', "MarkdownLivePreview.css")
|
|
||||||
|
|
||||||
MATCH_YAML_HEADER = re.compile(r'^([\-\+])\1{2}\n(?P<content>.+)\n\1{3}\n', re.DOTALL)
|
|
||||||
|
|
||||||
def strip_html_comments(html):
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
for element in soup.find_all(text=lambda text: isinstance(text, html_comment)):
|
|
||||||
element.extract()
|
|
||||||
return str(soup)
|
|
||||||
|
|
||||||
def manage_header(md, action):
|
|
||||||
matchobj = MATCH_YAML_HEADER.match(md)
|
|
||||||
if not matchobj:
|
|
||||||
return md
|
|
||||||
if action == 'remove':
|
|
||||||
return md[len(matchobj.group(0)):]
|
|
||||||
elif action == 'wrap_in_pre':
|
|
||||||
return '<pre><code>' + matchobj.group('content') + '</code></pre>' \
|
|
||||||
+ md[len(matchobj.group(0)):]
|
|
||||||
|
|
||||||
raise ValueError('Got an unknown action: "{}"'.format(action))
|
|
||||||
|
|
||||||
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 replace_img_src_base64(html, basepath):
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
load_from_internet_starters = get_settings().get('load_from_internet_when_starts')
|
|
||||||
for img in soup.find_all('img'):
|
|
||||||
if img['src'].startswith('data:image/'):
|
|
||||||
continue
|
|
||||||
elif img['src'].startswith(tuple(load_from_internet_starters)):
|
|
||||||
image = ImageManager.get(img['src']) or loading
|
|
||||||
else: # this is a local image
|
|
||||||
image = to_base64(os.path.join(basepath, img['src']))
|
|
||||||
|
|
||||||
img['src'] = image
|
|
||||||
|
|
||||||
return str(soup)
|
|
||||||
|
|
||||||
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_spaces(code):
|
|
||||||
for tag in code.find_all(text=True):
|
|
||||||
tag.replace_with(BeautifulSoup(str(tag).replace('\t', ' ' * 4).replace(' ', '<i class="space">.</i>').replace('\n', '<br />'), 'html.parser'))
|
|
||||||
return code
|
|
||||||
|
|
||||||
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>"""
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
for pre in soup.find_all('pre'):
|
|
||||||
code = pre.find('code')
|
|
||||||
code.replace_with(_pre_with_spaces(code))
|
|
||||||
return str(soup)
|
|
||||||
|
|
||||||
|
|
||||||
def get_style(color_scheme):
|
|
||||||
css = DEFAULT_STYLE
|
|
||||||
if os.path.exists(USER_STYLE_FILE):
|
|
||||||
with open(USER_STYLE_FILE) as fp:
|
|
||||||
css += '\n' + fp.read() + '\n'
|
|
||||||
if color_scheme and color_scheme.endswith('.tmTheme'):
|
|
||||||
css += pygments_from_theme(get_resource(color_scheme))
|
|
||||||
return ''.join([line.strip() + ' ' for line in css.splitlines()])
|
|
||||||
|
|
||||||
def get_resource(resource):
|
|
||||||
if os.path.exists(os.path.join(sublime.packages_path(), '..', resource)):
|
|
||||||
with open(os.path.join(sublime.packages_path(), '..', resource), encoding='utf-8') as fp:
|
|
||||||
return fp.read()
|
|
||||||
else:
|
|
||||||
return sublime.load_resource(resource)
|
|
||||||
@ -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
@ -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:
|
||||||
|
|||||||
@ -1,52 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
"""
|
|
||||||
'pre_tables' transform *html* tables into markdown tables, and put them in some <pre><code> tags
|
|
||||||
"""
|
|
||||||
|
|
||||||
from bs4 import BeautifulSoup
|
|
||||||
|
|
||||||
def python_table(s_table):
|
|
||||||
"""Transform BeautifulSoup table into list of list"""
|
|
||||||
rows = []
|
|
||||||
for row in s_table.find_all('tr'):
|
|
||||||
# rows.append(list(map( lambda td: td.text, row.find_all(['th', 'td']) )))
|
|
||||||
rows.append(row.find_all(['th', 'td']))
|
|
||||||
return rows
|
|
||||||
|
|
||||||
def pre_table(s_table):
|
|
||||||
rows = python_table(s_table)
|
|
||||||
cols_width = [len(cell) for cell in rows[0]]
|
|
||||||
for j, row in enumerate(rows):
|
|
||||||
for i, cell in enumerate(row):
|
|
||||||
if cols_width[i] < len(cell.text):
|
|
||||||
cols_width[i] = len(cell.text)
|
|
||||||
text = '<pre class="table"><code>'
|
|
||||||
for i, row in enumerate(rows):
|
|
||||||
if i == 1:
|
|
||||||
for j, cell in enumerate(row):
|
|
||||||
text += '|' + '-' * (cols_width[j] + 2)
|
|
||||||
text += '|\n'
|
|
||||||
|
|
||||||
for j, cell in enumerate(row):
|
|
||||||
text += '| '
|
|
||||||
if cell.name == 'th':
|
|
||||||
title = ' ' * ((cols_width[j] - len(cell.text)) // 2) \
|
|
||||||
+ ''.join(str(node) for node in cell.contents) \
|
|
||||||
+ ' ' * int(round((cols_width[j] - len(cell.text)) / 2 ) + 1)
|
|
||||||
# + 1 because of the added space before the closing | of each cell
|
|
||||||
if cols_width[j] + 1 != len(title):
|
|
||||||
title += ' '
|
|
||||||
text += title
|
|
||||||
else:
|
|
||||||
text += ''.join(str(node) for node in cell.contents) \
|
|
||||||
+ ' ' * (cols_width[j] - len(cell.text) + 1)
|
|
||||||
text += '|\n'
|
|
||||||
text += '</pre></code>'
|
|
||||||
return text
|
|
||||||
|
|
||||||
def pre_tables(html):
|
|
||||||
soup = BeautifulSoup(html, 'html.parser')
|
|
||||||
for table in soup.find_all('table'):
|
|
||||||
table.replace_with(BeautifulSoup(pre_table(table), 'html.parser'))
|
|
||||||
return str(soup)
|
|
||||||
@ -1,167 +0,0 @@
|
|||||||
# -*- encoding: utf-8 -*-
|
|
||||||
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
from xml.dom.minidom import parseString
|
|
||||||
from collections import defaultdict
|
|
||||||
|
|
||||||
class Style:
|
|
||||||
# .highlight is the wrapper class for highlighting therefore
|
|
||||||
# all css rules are prefixed with .highlight
|
|
||||||
PREFIX = '.codehilite'
|
|
||||||
|
|
||||||
# -----------------------------------------
|
|
||||||
# Params
|
|
||||||
# name: the name of the class
|
|
||||||
# args: each argument is an array.
|
|
||||||
# Each array consists of css properties
|
|
||||||
# that is either a color or font style
|
|
||||||
# ----------------------------------------
|
|
||||||
|
|
||||||
def __init__(self, name, *args):
|
|
||||||
self.name = name # Name of the class
|
|
||||||
self.rules = {} # The css rules
|
|
||||||
for arr in args:
|
|
||||||
for value in arr:
|
|
||||||
# Only define properties if they are already not defined
|
|
||||||
# This allows "cascading" if rules to be applied
|
|
||||||
if value.startswith('#') and 'color' not in self.rules:
|
|
||||||
self.rules['color'] = value
|
|
||||||
else:
|
|
||||||
if 'italic' in value and 'font-style' not in self.rules:
|
|
||||||
self.rules['font-style'] = 'italic'
|
|
||||||
if 'underline' in value and 'text-decoration' not in self.rules:
|
|
||||||
self.rules['text-decoration'] = 'underline'
|
|
||||||
if 'bold' in value and 'font-weight' not in self.rules:
|
|
||||||
self.rules['font-weight'] = 'bold'
|
|
||||||
|
|
||||||
# Helper method for creating the css rule
|
|
||||||
def _join_attr(self):
|
|
||||||
temp = []
|
|
||||||
if(len(self.rules) == 0):
|
|
||||||
return ''
|
|
||||||
for key in self.rules:
|
|
||||||
temp.append(key + ': ' + self.rules[key])
|
|
||||||
return '; '.join(temp) + ';'
|
|
||||||
|
|
||||||
def toString(self):
|
|
||||||
joined = self._join_attr()
|
|
||||||
if joined:
|
|
||||||
return "%s .%s { %s }" % (Style.PREFIX, self.name, joined)
|
|
||||||
return ''
|
|
||||||
|
|
||||||
|
|
||||||
# Crappy xml parsing function for getting the
|
|
||||||
# colors and font styles from colortheme file
|
|
||||||
|
|
||||||
|
|
||||||
def get_settings(color_scheme_content):
|
|
||||||
settings = defaultdict(lambda: [])
|
|
||||||
dom = parseString(color_scheme_content)
|
|
||||||
arr = dom.getElementsByTagName('array')[0]
|
|
||||||
editor_cfg = arr.getElementsByTagName('dict')[0].getElementsByTagName('dict')[0]
|
|
||||||
editor_vals = editor_cfg.getElementsByTagName('string')
|
|
||||||
background = editor_vals[0].firstChild.nodeValue
|
|
||||||
text_color = editor_vals[2].firstChild.nodeValue
|
|
||||||
settings['editor_bg'] = background
|
|
||||||
settings['text_color'] = text_color
|
|
||||||
for node in arr.childNodes:
|
|
||||||
if node.nodeName == "dict":
|
|
||||||
try:
|
|
||||||
setting = node.getElementsByTagName('string')[1].firstChild.nodeValue
|
|
||||||
attrs = []
|
|
||||||
values = node.getElementsByTagName('dict')[0].getElementsByTagName('string')
|
|
||||||
for v in values:
|
|
||||||
if v.firstChild:
|
|
||||||
a = str(v.firstChild.nodeValue).strip()
|
|
||||||
attrs.append(a)
|
|
||||||
for s in setting.split(', '):
|
|
||||||
settings[s] = attrs
|
|
||||||
except:
|
|
||||||
continue
|
|
||||||
return settings
|
|
||||||
|
|
||||||
|
|
||||||
def pygments_from_theme(color_scheme_content):
|
|
||||||
settings = get_settings(color_scheme_content)
|
|
||||||
styles = []
|
|
||||||
|
|
||||||
#Generic
|
|
||||||
styles.append(Style('ge', ['italic']))
|
|
||||||
styles.append(Style('gs', ['bold']))
|
|
||||||
|
|
||||||
# Comments
|
|
||||||
styles.append(Style('c', settings['comment']))
|
|
||||||
styles.append(Style('cp', settings['comment']))
|
|
||||||
styles.append(Style('c1', settings['comment']))
|
|
||||||
styles.append(Style('cs', settings['comment']))
|
|
||||||
styles.append(Style('cm', settings['comment.block'], settings['comment']))
|
|
||||||
|
|
||||||
# Constants
|
|
||||||
styles.append(Style('m', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
|
||||||
styles.append(Style('mf', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
|
||||||
styles.append(Style('mi', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
|
||||||
styles.append(Style('mo', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
|
||||||
styles.append(Style('se', settings['constant.language'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
|
||||||
styles.append(Style('kc', settings['constant.language'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
|
||||||
|
|
||||||
#Keywords
|
|
||||||
styles.append(Style('k', settings['entity.name.type'], settings['support.type'], settings['keyword']))
|
|
||||||
styles.append(Style('kd', settings['storage.type'], settings['storage']))
|
|
||||||
styles.append(Style('kn', settings['support.function.construct'], settings['keyword.control'], settings['keyword']))
|
|
||||||
styles.append(Style('kt', settings['entity.name.type'], settings['support.type'], settings['support.constant']))
|
|
||||||
|
|
||||||
#String
|
|
||||||
styles.append(Style('settings', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
|
||||||
styles.append(Style('sb', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
|
||||||
styles.append(Style('sc', settings['string.quoted.single'], settings['string.quoted'], settings['string']))
|
|
||||||
styles.append(Style('sd', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
|
||||||
styles.append(Style('s2', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
|
||||||
styles.append(Style('sh', settings['string']))
|
|
||||||
styles.append(Style('si', settings['string.interpolated'], settings['string']))
|
|
||||||
styles.append(Style('sx', settings['string.other'], settings['string']))
|
|
||||||
styles.append(Style('sr', settings['string.regexp'], settings['string']))
|
|
||||||
styles.append(Style('s1', settings['string.quoted.single'], settings['string']))
|
|
||||||
styles.append(Style('ss', settings['string']))
|
|
||||||
|
|
||||||
#Name
|
|
||||||
styles.append(Style('na', settings['entity.other.attribute-name'], settings['entity.other']))
|
|
||||||
styles.append(Style('bp', settings['variable.language'], settings['variable']))
|
|
||||||
styles.append(Style('nc', settings['entity.name.class'], settings['entity.other.inherited-class'], settings['support.class']))
|
|
||||||
styles.append(Style('no', settings['constant.language'], settings['constant']))
|
|
||||||
styles.append(Style('nd', settings['entity.name.class']))
|
|
||||||
styles.append(Style('ne', settings['entity.name.class']))
|
|
||||||
styles.append(Style('nf', settings['entity.name.function'], settings['support.function']))
|
|
||||||
styles.append(Style('nt', settings['entity.name.tag'], settings['keyword']))
|
|
||||||
styles.append(Style('nv', settings['variable'], [settings['text_color']]))
|
|
||||||
styles.append(Style('vc', settings['variable.language']))
|
|
||||||
styles.append(Style('vg', settings['variable.language']))
|
|
||||||
styles.append(Style('vi', settings['variable.language']))
|
|
||||||
|
|
||||||
#Operator
|
|
||||||
styles.append(Style('ow', settings['keyword.operator'], settings['keyword.operator'], settings['keyword']))
|
|
||||||
styles.append(Style('o', settings['keyword.operator'], settings['keyword.operator'], settings['keyword']))
|
|
||||||
|
|
||||||
# Text
|
|
||||||
styles.append(Style('n', [settings['text_color']]))
|
|
||||||
styles.append(Style('nl', [settings['text_color']]))
|
|
||||||
styles.append(Style('nn', [settings['text_color']]))
|
|
||||||
styles.append(Style('nx', [settings['text_color']]))
|
|
||||||
styles.append(Style('bp', settings['variable.language'], settings['variable'], [settings['text_color']]))
|
|
||||||
styles.append(Style('p', [settings['text_color']]))
|
|
||||||
|
|
||||||
css = '{} {{ background-color: {}; color: {}; }}\n'.format(Style.PREFIX, settings['editor_bg'], settings['text_color'])
|
|
||||||
for st in styles:
|
|
||||||
css_style = st.toString()
|
|
||||||
if css_style:
|
|
||||||
css += css_style + '\n'
|
|
||||||
|
|
||||||
return css
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
args = sys.argv[1:]
|
|
||||||
if len(args) < 1:
|
|
||||||
print("Please provide the .tmTheme file!", file=sys.stderr)
|
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(pygments_from_theme(args[0]))
|
|
||||||
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
|
After Width: | Height: | Size: 38 KiB |
BIN
live-testing/sublime_text.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
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
@ -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')
|
||||||
35
mkdocs.yml
@ -1,35 +0,0 @@
|
|||||||
site_name: MarkdownLivePreview
|
|
||||||
theme: material
|
|
||||||
repo_name: math2001/MarkdownLivePreview
|
|
||||||
repo_url: https://github.com/math2001/MarkdownLivePreview
|
|
||||||
site_description: Sublime Text 3 Plugin MarkdownLivePreview's documentation
|
|
||||||
site_author: math2001
|
|
||||||
|
|
||||||
markdown_extensions:
|
|
||||||
- toc(permalink=true)
|
|
||||||
- pymdownx.arithmatex
|
|
||||||
- pymdownx.betterem(smart_enable=all)
|
|
||||||
- pymdownx.caret
|
|
||||||
- pymdownx.critic
|
|
||||||
- pymdownx.emoji:
|
|
||||||
emoji_generator: !!python/name:pymdownx.emoji.to_svg
|
|
||||||
- pymdownx.inlinehilite
|
|
||||||
- pymdownx.magiclink
|
|
||||||
- pymdownx.mark
|
|
||||||
- pymdownx.smartsymbols
|
|
||||||
- pymdownx.superfences
|
|
||||||
- pymdownx.tasklist(custom_checkbox=true)
|
|
||||||
- pymdownx.tilde
|
|
||||||
- admonition
|
|
||||||
- codehilite
|
|
||||||
|
|
||||||
extra:
|
|
||||||
logo: imgs/MarkdownLivePreview-opposite.svg
|
|
||||||
palette:
|
|
||||||
primary: Blue
|
|
||||||
accent: Indigo
|
|
||||||
social:
|
|
||||||
- type: github
|
|
||||||
link: https://github.com/math2001
|
|
||||||
- type: twitter
|
|
||||||
link: https://twitter.com/_math2001
|
|
||||||
1
resources/404.base64
Normal file
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
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
@ -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
@ -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);
|
||||||
|
}
|
||||||
@ -1,10 +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'
|
|
||||||
ON_OPEN = 'markdown_live_preview_on_open'
|
|
||||||
LAST_UPDATE = 'markdonw_live_preview_last_run'
|
|
||||||
16
test.md
@ -1,16 +0,0 @@
|
|||||||
```python
|
|
||||||
import this
|
|
||||||
|
|
||||||
if you.are('new'):
|
|
||||||
print('Welcome!')
|
|
||||||
if you.are('brand new'):
|
|
||||||
print("You'll see, python's just awesome")
|
|
||||||
else:
|
|
||||||
print('Hello!')
|
|
||||||
```
|
|
||||||
|
|
||||||

|
|
||||||
|
|
||||||
<ol>
|
|
||||||
<li>test</li>
|
|
||||||
</ol>
|
|
||||||
38
todo
@ -1,38 +0,0 @@
|
|||||||
Fast:
|
|
||||||
☐ cache image in object when used, so that it's faster @needsTest
|
|
||||||
|
|
||||||
Medium:
|
|
||||||
☐ auto refresh preview if loading images
|
|
||||||
☐ use alt attribute for 404 error
|
|
||||||
☐ optimize usage of BeautifulSoup
|
|
||||||
|
|
||||||
Long:
|
|
||||||
☐ support anchor (TOC) @big
|
|
||||||
|
|
||||||
Unknown:
|
|
||||||
☐ properly convert tmtheme to css
|
|
||||||
|
|
||||||
___________________
|
|
||||||
Archive:
|
|
||||||
✔ add settings to keep md view open #13 @done Sat 11 Feb 2017 at 09:10 @project(Fast)
|
|
||||||
✔ fix custom css @bug @done Sun 22 Jan 2017 at 18:40 @project(Medium)
|
|
||||||
✘ check how many times is the show_html function called @cancelled Sun 22 Jan 2017 at 18:40 @project(Unknown)
|
|
||||||
✔ sync scroll @needsUpdate(because of images) @done Sun 22 Jan 2017 at 18:39 @project(Fast)
|
|
||||||
✔ fix #4 @high @done Mon 09 Jan 2017 at 18:42 @project(Long)
|
|
||||||
✔ use MarkdownLivePreview syntax, so we can use syntax's settings @done Mon 09 Jan 2017 at 18:41 @project(Medium)
|
|
||||||
✔ add clear cache command @done Mon 09 Jan 2017 at 18:41 @project(Fast)
|
|
||||||
✔ update README for settings in view @done Mon 09 Jan 2017 at 18:41 @project(Fast)
|
|
||||||
✔ add edit settings @done Mon 09 Jan 2017 at 18:41 @project(Fast)
|
|
||||||
✘ listen for settings to change @cancelled Mon 09 Jan 2017 at 18:41 @project(Medium)
|
|
||||||
✘ 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
|
|
||||||
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
|
||||||