Compare commits
23 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| aa5fd4faf8 | |||
| 613e3fb1b2 | |||
| 057f770859 | |||
| 76d56deff6 | |||
| d2863e4a43 | |||
| ed57d2813a | |||
| b4038c3575 | |||
| fd633e0bc0 | |||
| 4a3caf30bb | |||
| 28a7274b05 | |||
| 6ad9e79926 | |||
| ac09c523e7 | |||
| 3ad29ede37 | |||
| 576956a8d1 | |||
| 93a04733da | |||
| 6a0267fb3b | |||
| f52bf98470 | |||
| 5a3b1a7f81 | |||
| 182862ecce | |||
| 518f6f1ed4 | |||
| b44151ed69 | |||
| e4a5ea886a | |||
| e6ea13f0a2 |
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1,3 @@
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
__pycache__/
|
||||||
|
cache.txt
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
"keys": ["alt+m"],
|
"keys": ["alt+m"],
|
||||||
"command": "toggle_setting",
|
"command": "toggle_setting",
|
||||||
"args": {
|
"args": {
|
||||||
"setting": "markdown_preview_enabled"
|
"setting": "markdown_live_preview_enabled"
|
||||||
},
|
},
|
||||||
"context": [
|
"context": [
|
||||||
{
|
{
|
||||||
@ -3,7 +3,7 @@
|
|||||||
"caption": "MarkdownLivePreview: Toggle",
|
"caption": "MarkdownLivePreview: Toggle",
|
||||||
"command": "toggle_setting",
|
"command": "toggle_setting",
|
||||||
"args": {
|
"args": {
|
||||||
"setting": "markdown_preview_enabled"
|
"setting": "markdown_live_preview_enabled"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
3
.sublime/MarkdownLivePreview.sublime-settings
Normal file
3
.sublime/MarkdownLivePreview.sublime-settings
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"load_from_internet_when_starts": ["http://", "https://"]
|
||||||
|
}
|
||||||
BIN
404-image.png
Normal file
BIN
404-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
144
MLPApi.py
Normal file
144
MLPApi.py
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
from .lib import markdown2
|
||||||
|
from .escape_amp import *
|
||||||
|
from .functions import *
|
||||||
|
from .setting_names import *
|
||||||
|
|
||||||
|
__folder__ = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
STYLE_FILE = os.path.join(os.path.dirname(__folder__), 'User',
|
||||||
|
'MarkdownLivePreview.css')
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_loaded():
|
||||||
|
global DEFAULT_STYLE_FILE
|
||||||
|
DEFAULT_STYLE_FILE = sublime.load_resource('Packages/MarkdownLivePreview/'
|
||||||
|
'default.css')
|
||||||
|
|
||||||
|
def get_preview_name(md_view):
|
||||||
|
name = md_view.name() \
|
||||||
|
or os.path.basename(md_view.file_name()) \
|
||||||
|
or 'Untitled'
|
||||||
|
return name + ' - Preview'
|
||||||
|
|
||||||
|
def find_preview(window):
|
||||||
|
for view in window.views():
|
||||||
|
vsettings = view.settings()
|
||||||
|
if vsettings.get(IS_PREVIEW):
|
||||||
|
yield view, vsettings
|
||||||
|
|
||||||
|
def create_preview(md_view):
|
||||||
|
window = md_view.window()
|
||||||
|
md_view_settings = md_view.settings()
|
||||||
|
md_view_settings.set(JUST_CREATED, True)
|
||||||
|
|
||||||
|
preview = window.new_file()
|
||||||
|
|
||||||
|
psettings = preview.settings()
|
||||||
|
psettings.set(IS_PREVIEW, True)
|
||||||
|
psettings.set(MD_VIEW_ID, md_view.id())
|
||||||
|
preview.set_name(get_preview_name(md_view))
|
||||||
|
preview.set_scratch(True)
|
||||||
|
md_view_settings.set(PREVIEW_ID, preview.id())
|
||||||
|
def move_and_focus_md_view():
|
||||||
|
window.run_command('new_pane')
|
||||||
|
sublime.set_timeout_async(lambda: window.focus_view(md_view), 250)
|
||||||
|
sublime.set_timeout_async(move_and_focus_md_view, 250)
|
||||||
|
|
||||||
|
return preview
|
||||||
|
|
||||||
|
def hide_preview(view):
|
||||||
|
window = view.window()
|
||||||
|
vsettings = view.settings()
|
||||||
|
if vsettings.get(IS_PREVIEW):
|
||||||
|
preview = view
|
||||||
|
psettings = vsettings
|
||||||
|
md_view_id = vsettings.get(MD_VIEW_ID)
|
||||||
|
md_view = get_view_from_id(window, md_view_id)
|
||||||
|
if md_view is None:
|
||||||
|
raise ValueError('Tried to get md_view from id {} but got None'.format(md_view_id))
|
||||||
|
mdvsettings = md_view.settings()
|
||||||
|
|
||||||
|
elif vsettings.get(PREVIEW_ENABLED):
|
||||||
|
md_view = view
|
||||||
|
preview_id = vsettings.get(PREVIEW_ID)
|
||||||
|
preview = get_view_from_id(window, preview_id)
|
||||||
|
mdvsettings = vsettings
|
||||||
|
if preview is None:
|
||||||
|
raise ValueError('Tried to get preview from id {} but got None'.format(preview_id))
|
||||||
|
psettings = preview.settings()
|
||||||
|
else:
|
||||||
|
raise ValueError('Call hide_preview with a view which is not the preview or the md_view')
|
||||||
|
psettings.set(IS_HIDDEN, True)
|
||||||
|
mdvsettings.erase(PREVIEW_ID)
|
||||||
|
sublime.set_timeout(lambda: preview.close(), 250)
|
||||||
|
return
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
window = md_view.window()
|
||||||
|
if window is None:
|
||||||
|
return
|
||||||
|
mdvsettings = md_view.settings()
|
||||||
|
preview_id = mdvsettings.get(PREVIEW_ID)
|
||||||
|
if not preview_id:
|
||||||
|
return
|
||||||
|
mdvsettings.erase(PREVIEW_ID)
|
||||||
|
preview = get_view_from_id(window, preview_id)
|
||||||
|
if preview is None:
|
||||||
|
raise ValueError('Tried to get view from id {} but got None'.format(preview_id))
|
||||||
|
psettings = preview.settings()
|
||||||
|
psettings.set(IS_HIDDEN, True)
|
||||||
|
sublime.set_timeout(preview.close(), 250)
|
||||||
|
|
||||||
|
def get_style():
|
||||||
|
content = ''.join([line.strip() for line in DEFAULT_STYLE_FILE.splitlines()])
|
||||||
|
return content + "pre code .space {color: var(--light-bg)}"
|
||||||
|
|
||||||
|
def show_html(md_view, preview):
|
||||||
|
html = '<style>{}</style>\n{}'.format(get_style(),
|
||||||
|
pre_with_br(markdown2.markdown(get_view_content(md_view),
|
||||||
|
extras=['fenced-code-blocks',
|
||||||
|
'no-code-highlighting'])))
|
||||||
|
|
||||||
|
# the option no-code-highlighting does not exists
|
||||||
|
# in the official version of markdown2 for now
|
||||||
|
# I personaly edited the file (markdown2.py:1743)
|
||||||
|
|
||||||
|
html = html.replace(' ', ' espace;') # save where are the spaces
|
||||||
|
|
||||||
|
html = HTMLParser().unescape(html)
|
||||||
|
|
||||||
|
html = escape_amp(html)
|
||||||
|
|
||||||
|
# exception, again, because <pre> aren't supported by the phantoms
|
||||||
|
html = html.replace(' espace;', '<i class="space">.</i>')
|
||||||
|
html = replace_img_src_base64(html)
|
||||||
|
preview.erase_phantoms('markdown_preview')
|
||||||
|
preview.add_phantom('markdown_preview',
|
||||||
|
sublime.Region(-1),
|
||||||
|
html,
|
||||||
|
sublime.LAYOUT_BLOCK,
|
||||||
|
lambda href: sublime.run_command('open_url',
|
||||||
|
{'url': href}))
|
||||||
|
|
||||||
|
# set viewport position
|
||||||
|
|
||||||
|
# sublime.set_clipboard(html)
|
||||||
|
|
||||||
|
return
|
||||||
|
# 0 < y < 1
|
||||||
|
y = md_view.text_to_layout(md_view.sel()[0].begin())[1] / md_view.layout_extent()[1]
|
||||||
|
vector = [0, y * preview.layout_extent()[1]]
|
||||||
|
# remove half of the viewport_extent.y to center it on the screen (verticaly)
|
||||||
|
vector[1] -= preview.viewport_extent()[1] / 2
|
||||||
|
vector[1] = mini(vector[1], 0)
|
||||||
|
vector[1] += preview.line_height()
|
||||||
|
preview.set_viewport_position(vector, animate=False)
|
||||||
51
MarkdownLivePreview.py
Normal file
51
MarkdownLivePreview.py
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
import sys
|
||||||
|
import os.path
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
|
||||||
|
from .MLPApi import *
|
||||||
|
from .setting_names import *
|
||||||
|
from .functions import *
|
||||||
|
|
||||||
|
class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
||||||
|
|
||||||
|
def on_modified(self, view):
|
||||||
|
window = view.window()
|
||||||
|
vsettings = view.settings()
|
||||||
|
if vsettings.get(PREVIEW_ENABLED):
|
||||||
|
id = vsettings.get(PREVIEW_ID)
|
||||||
|
preview = get_view_from_id(window, id)
|
||||||
|
if id is None or preview is None:
|
||||||
|
preview = create_preview(view)
|
||||||
|
sublime.set_timeout_async(lambda: show_html(view, preview), 1000)
|
||||||
|
else:
|
||||||
|
show_html(view, preview)
|
||||||
|
|
||||||
|
return
|
||||||
|
|
||||||
|
def on_activated(self, view):
|
||||||
|
# if view is md_view and has no preview
|
||||||
|
# -> create preview
|
||||||
|
window = view.window()
|
||||||
|
vsettings = view.settings()
|
||||||
|
if vsettings.get(PREVIEW_ENABLED):
|
||||||
|
id = vsettings.get(PREVIEW_ID)
|
||||||
|
preview = get_view_from_id(window, id)
|
||||||
|
if id is None or preview is None:
|
||||||
|
preview = create_preview(view)
|
||||||
|
sublime.set_timeout(lambda: show_html(view, preview), 1000)
|
||||||
|
else:
|
||||||
|
show_html(view, preview)
|
||||||
|
return
|
||||||
|
|
||||||
|
# if view is preview
|
||||||
|
# -> do nothing
|
||||||
|
if vsettings.get(IS_PREVIEW):
|
||||||
|
return
|
||||||
|
# if view is not the md_view or the preview
|
||||||
|
# remove preview if any
|
||||||
|
for view, settings in find_preview(window):
|
||||||
|
settings.set(IS_HIDDEN, True)
|
||||||
|
view.close()
|
||||||
32
MarkdownLivePreview.tasks
Normal file
32
MarkdownLivePreview.tasks
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
Fast:
|
||||||
|
|
||||||
|
☐ sync scroll @needsUpdate(because of images)
|
||||||
|
☐ cache image in object when used, so that it's faster @needsTest
|
||||||
|
☐ call settings listener on_new too - might be too heavy
|
||||||
|
☐ add clear cache command
|
||||||
|
|
||||||
|
Medium:
|
||||||
|
☐ auto refresh preview if loading images
|
||||||
|
☐ use alt attribute for 404 error
|
||||||
|
☐ use MarkdownLivePreview syntax, so we can use syntax's settings
|
||||||
|
☐ listen for settings to change
|
||||||
|
|
||||||
|
Long:
|
||||||
|
☐ fix #4 @high
|
||||||
|
☐ support hanchor (TOC) @big
|
||||||
|
|
||||||
|
Unknown:
|
||||||
|
☐ check how many times is the show_html function called
|
||||||
|
|
||||||
|
|
||||||
|
___________________
|
||||||
|
Archive:
|
||||||
|
✔ 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
|
||||||
43
default.css
Normal file
43
default.css
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
:root, html, body {
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
html {
|
||||||
|
--light-bg: color(var(--background) blend(#999 85%))
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
body {
|
||||||
|
padding:10px;
|
||||||
|
padding-top: 0px;
|
||||||
|
font-family: "Open Sans", sans-serif;
|
||||||
|
background-color: var(--background);
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockquote {
|
||||||
|
font-style: italic;
|
||||||
|
display: block;
|
||||||
|
margin-left: 30px;
|
||||||
|
border: 1px solid red;
|
||||||
|
}
|
||||||
|
|
||||||
|
code {
|
||||||
|
padding-left: 0.2rem;
|
||||||
|
padding-right: 0.2rem;
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
margin: 0;
|
||||||
|
border-radius: 3px;
|
||||||
|
margin: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
pre {
|
||||||
|
display: block;
|
||||||
|
margin-top: 20px;
|
||||||
|
line-height: 1.7;
|
||||||
|
background-color: var(--light-bg);
|
||||||
|
padding-left: 10px;
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 3px;
|
||||||
|
}
|
||||||
|
pre code {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
20
devListener.py
Normal file
20
devListener.py
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
import os.path
|
||||||
|
|
||||||
|
class MLPDevListener(sublime_plugin.EventListener):
|
||||||
|
|
||||||
|
def on_post_save(self, view):
|
||||||
|
return
|
||||||
|
if not (os.path.dirname(__file__) in view.file_name() and
|
||||||
|
view.file_name().endswith('.py')):
|
||||||
|
return
|
||||||
|
sublime.run_command('reload_plugin', {
|
||||||
|
'main': os.path.join(sublime.packages_path(),
|
||||||
|
'MarkdownLivePreview', 'md_in_popup.py'),
|
||||||
|
'scripts': ['image_manager', 'functions', 'MLPApi',
|
||||||
|
'setting_names'],
|
||||||
|
'quiet': True
|
||||||
|
})
|
||||||
38
escape_amp.py
Normal file
38
escape_amp.py
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
# -*- 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:
|
||||||
|
print('TEST FAIL ({i}): {subject!r} escaped did not match {result!r}'.format(**locals()))
|
||||||
|
fails += 1
|
||||||
|
if fails == 0:
|
||||||
|
print("SUCCESS: every tests ({}) passed successfully!".format(len(tests)))
|
||||||
|
else:
|
||||||
|
print("{} test{} failed".format(fails, 's' if fails > 1 else ''))
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_tests()
|
||||||
98
functions.py
Normal file
98
functions.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
import base64
|
||||||
|
import os.path
|
||||||
|
import sublime
|
||||||
|
import re
|
||||||
|
from .image_manager import ImageManager
|
||||||
|
|
||||||
|
def plugin_loaded():
|
||||||
|
global error404, loading
|
||||||
|
loading = sublime.load_resource('Packages/MarkdownLivePreview/loading.txt')
|
||||||
|
error404 = sublime.load_resource('Packages/MarkdownLivePreview/404.txt')
|
||||||
|
|
||||||
|
|
||||||
|
def replace_img_src_base64(html):
|
||||||
|
"""Really messy, but it works (should be updated)"""
|
||||||
|
index = -1
|
||||||
|
tag_start = '<img src="'
|
||||||
|
shtml, html = html, list(html)
|
||||||
|
while True:
|
||||||
|
index = shtml.find(tag_start, index + 1)
|
||||||
|
if index == -1:
|
||||||
|
break
|
||||||
|
path, end = get_content_till(html, '"', start=index + len(tag_start))
|
||||||
|
if ''.join(path).startswith('data:image/'):
|
||||||
|
continue
|
||||||
|
if ''.join(path).startswith(tuple(get_settings().get('load_from_internet'
|
||||||
|
'_when_starts'))):
|
||||||
|
image = ImageManager.get(''.join(path))
|
||||||
|
image = image or loading
|
||||||
|
|
||||||
|
else:
|
||||||
|
# local image
|
||||||
|
image = to_base64(''.join(path))
|
||||||
|
html[index+len(tag_start):end] = image
|
||||||
|
shtml = ''.join(html)
|
||||||
|
return ''.join(html)
|
||||||
|
|
||||||
|
def is_markdown_view(view):
|
||||||
|
return 'markdown' in view.scope_name(0)
|
||||||
|
|
||||||
|
def to_base64(path=None, content=None):
|
||||||
|
if path is None and content is None:
|
||||||
|
return error404
|
||||||
|
elif content is None and path is not None:
|
||||||
|
try:
|
||||||
|
with open(path, 'rb') as fp:
|
||||||
|
content = fp.read()
|
||||||
|
except (FileNotFoundError, OSError):
|
||||||
|
return error404
|
||||||
|
|
||||||
|
return 'data:image/png;base64,' + ''.join([chr(el) for el in list(base64.standard_b64encode(content))])
|
||||||
|
|
||||||
|
def md(*t, **kwargs):
|
||||||
|
sublime.message_dialog(kwargs.get('sep', '\n').join([str(el) for el in t]))
|
||||||
|
|
||||||
|
def sm(*t, **kwargs):
|
||||||
|
sublime.status_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
||||||
|
|
||||||
|
def em(*t, **kwargs):
|
||||||
|
sublime.error_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
||||||
|
|
||||||
|
def mini(val, min):
|
||||||
|
if val < min:
|
||||||
|
return min
|
||||||
|
return val
|
||||||
|
|
||||||
|
def get_content_till(string, char_to_look_for, start=0):
|
||||||
|
i = start
|
||||||
|
while i < len(string):
|
||||||
|
if string[i] == char_to_look_for:
|
||||||
|
return string[start:i], i
|
||||||
|
i += 1
|
||||||
|
|
||||||
|
def get_view_content(view):
|
||||||
|
return view.substr(sublime.Region(0, view.size()))
|
||||||
|
|
||||||
|
def get_view_from_id(window, id):
|
||||||
|
for view in window.views():
|
||||||
|
if view.id() == id:
|
||||||
|
return view
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
return sublime.load_settings('MarkdownLivePreview.sublime-settings')
|
||||||
|
|
||||||
|
def pre_with_br(html):
|
||||||
|
"""Because the phantoms of sublime text does not support <pre> blocks
|
||||||
|
this function replaces every \n with a <br> in a <pre>"""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
obj = re.search(r'<pre>(.*?)</pre>', html, re.DOTALL)
|
||||||
|
if not obj:
|
||||||
|
break
|
||||||
|
html = list(html)
|
||||||
|
html[obj.start(0):obj.end(0)] = '<pre >' + ''.join(html[obj.start(1):obj.end(1)]) \
|
||||||
|
.replace('\n', '<br>') \
|
||||||
|
.replace(' ', ' ') + '</pre>'
|
||||||
|
html = ''.join(html)
|
||||||
|
return html
|
||||||
98
image_manager.py
Normal file
98
image_manager.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from threading import Thread
|
||||||
|
import urllib.request, urllib.error
|
||||||
|
import sublime
|
||||||
|
from .functions import *
|
||||||
|
import tempfile
|
||||||
|
|
||||||
|
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,3 +1,4 @@
|
|||||||
|
# CSW: ignore
|
||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
BIN
loading.png
Normal file
BIN
loading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 953 B |
1
loading.txt
Normal file
1
loading.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
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=
|
||||||
200
md_in_popup.py
200
md_in_popup.py
@ -1,200 +0,0 @@
|
|||||||
import sublime
|
|
||||||
import sublime_plugin
|
|
||||||
from . import markdown2
|
|
||||||
import os.path
|
|
||||||
import re
|
|
||||||
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
|
|
||||||
# Main sublime tools function
|
|
||||||
|
|
||||||
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
|
|
||||||
|
|
||||||
STYLE_FILE = os.path.join(sublime.packages_path(), 'User', 'MarkdownLivePreview.css')
|
|
||||||
def get_style():
|
|
||||||
content = None
|
|
||||||
if os.path.exists(STYLE_FILE):
|
|
||||||
with open(STYLE_FILE) as fp:
|
|
||||||
content = fp.read()
|
|
||||||
return content
|
|
||||||
if not content:
|
|
||||||
content = """
|
|
||||||
html {
|
|
||||||
--light-bg: color(var(--background) blend(#999 85%))
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
padding:10px;
|
|
||||||
padding-top: 0px;
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
font-style: italic;
|
|
||||||
display: block;
|
|
||||||
margin-left: 30px;
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding-left: 0.2rem;
|
|
||||||
padding-right: 0.2rem;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
display: block;
|
|
||||||
margin-top: 20px;
|
|
||||||
line-height: 1.7;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
padding-left: 10px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
return content + "pre code .space {color: var(--light-bg)}"
|
|
||||||
|
|
||||||
def pre_with_br(html):
|
|
||||||
"""Because the phantoms of sublime text does not support <pre> blocks
|
|
||||||
this function replaces every \n with a <br> in a <pre>"""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
obj = re.search(r'<pre>(.*?)</pre>', html, re.DOTALL)
|
|
||||||
if not obj:
|
|
||||||
break
|
|
||||||
html = list(html)
|
|
||||||
html[obj.start(0):obj.end(0)] = '<pre >' + ''.join(html[obj.start(1):obj.end(1)]) \
|
|
||||||
.replace('\n', '<br>') \
|
|
||||||
.replace(' ', ' ') + '</pre>'
|
|
||||||
html = ''.join(html)
|
|
||||||
return html
|
|
||||||
|
|
||||||
def close_preview(md_view_settings, preview):
|
|
||||||
preview.close()
|
|
||||||
md_view_settings.erase('markdown_preview_id')
|
|
||||||
md_view_settings.erase('markdown_preview_enabled')
|
|
||||||
|
|
||||||
def create_preview(window, md_view):
|
|
||||||
focus_group, focus_view = window.get_view_index(md_view)
|
|
||||||
preview = window.new_file()
|
|
||||||
window.run_command('new_pane') # move the preview to a new group
|
|
||||||
preview.set_name(os.path.basename(md_view.file_name()) + ' - Preview')
|
|
||||||
|
|
||||||
preview_settings = preview.settings()
|
|
||||||
preview_settings.set('gutter', False)
|
|
||||||
preview_settings.set('is_markdown_preview', True)
|
|
||||||
preview_settings.set('markdown_view_id', md_view.id())
|
|
||||||
|
|
||||||
md_view.settings().set('markdown_preview_id', preview.id())
|
|
||||||
window.focus_group(focus_group)
|
|
||||||
window.focus_view(md_view)
|
|
||||||
|
|
||||||
return preview
|
|
||||||
|
|
||||||
def show_html(md_view, preview):
|
|
||||||
html = ('<style>{}</style>'.format(get_style()) +
|
|
||||||
pre_with_br(markdown2.markdown(get_view_content(md_view),
|
|
||||||
extras=['fenced-code-blocks', 'no-code-highlighting'])))
|
|
||||||
|
|
||||||
# the option no-code-highlighting does not exists
|
|
||||||
# in the official version of markdown2 for now
|
|
||||||
# I personaly edited the file (markdown2.py:1743)
|
|
||||||
|
|
||||||
html = html.replace(' ', ' espace;') # save where are the spaces
|
|
||||||
|
|
||||||
html = HTMLParser().unescape(html)
|
|
||||||
|
|
||||||
# exception, again, because <pre> aren't supported by the phantoms
|
|
||||||
html = html.replace(' espace;', '<i class="space">.</i>')
|
|
||||||
preview.erase_phantoms('markdown_preview')
|
|
||||||
preview.add_phantom('markdown_preview',
|
|
||||||
sublime.Region(-1),
|
|
||||||
html,
|
|
||||||
sublime.LAYOUT_BLOCK,
|
|
||||||
lambda href: sublime.run_command('open_url', {'url': href}))
|
|
||||||
# 0 < y < 1
|
|
||||||
y = md_view.text_to_layout(md_view.sel()[0].begin())[1] / md_view.layout_extent()[1]
|
|
||||||
vector = [0, y * preview.layout_extent()[1]]
|
|
||||||
# remove half of the viewport_extent.y to center it on the screen (verticaly)
|
|
||||||
vector[1] -= preview.viewport_extent()[1] / 2
|
|
||||||
vector[1] = mini(vector[1], 0)
|
|
||||||
vector[1] += preview.line_height()
|
|
||||||
preview.set_viewport_position(vector, animate=False)
|
|
||||||
|
|
||||||
def get_view_content(view):
|
|
||||||
return view.substr(sublime.Region(0, view.size()))
|
|
||||||
|
|
||||||
def get_view_from_id(window, id):
|
|
||||||
for view in window.views():
|
|
||||||
if view.id() == id:
|
|
||||||
return view
|
|
||||||
|
|
||||||
class MarkdownInPopupCommand(sublime_plugin.EventListener):
|
|
||||||
|
|
||||||
def on_load(self, view):
|
|
||||||
settings = view.settings()
|
|
||||||
if not 'markdown' in settings.get('syntax').lower():
|
|
||||||
return
|
|
||||||
settings.add_on_change('markdown_preview_enabled', lambda: self.on_modified(view))
|
|
||||||
|
|
||||||
def on_modified(self, md_view):
|
|
||||||
window = md_view.window()
|
|
||||||
md_view_settings = md_view.settings()
|
|
||||||
|
|
||||||
if not 'markdown' in md_view_settings.get('syntax').lower():
|
|
||||||
return
|
|
||||||
|
|
||||||
markdown_preview_enabled = md_view_settings.get('markdown_preview_enabled') is True
|
|
||||||
preview_id = md_view_settings.get('markdown_preview_id', None)
|
|
||||||
|
|
||||||
if not markdown_preview_enabled:
|
|
||||||
if preview_id is not None:
|
|
||||||
preview = get_view_from_id(window, preview_id)
|
|
||||||
if preview:
|
|
||||||
close_preview(md_view_settings, preview)
|
|
||||||
return
|
|
||||||
|
|
||||||
if preview_id is None:
|
|
||||||
preview = create_preview(window, md_view)
|
|
||||||
else:
|
|
||||||
preview = get_view_from_id(window, preview_id)
|
|
||||||
if not preview:
|
|
||||||
md_view_settings.erase('markdown_preview_id')
|
|
||||||
md_view_settings.erase('markdown_preview_enabled')
|
|
||||||
return
|
|
||||||
|
|
||||||
show_html(md_view, preview)
|
|
||||||
|
|
||||||
def on_pre_close(self, view):
|
|
||||||
settings = view.settings()
|
|
||||||
if settings.get('markdown_preview_enabled') is True:
|
|
||||||
preview = get_view_from_id(view.window(), settings.get('markdown_preview_id'))
|
|
||||||
if preview:
|
|
||||||
sublime.set_timeout_async(lambda: preview.close(), 250)
|
|
||||||
elif settings.get('is_markdown_preview') is True:
|
|
||||||
md_view = get_view_from_id(view.window(), settings.get('markdown_view_id'))
|
|
||||||
if md_view:
|
|
||||||
def callback():
|
|
||||||
md_view_settings = md_view.settings()
|
|
||||||
md_view_settings.erase('markdown_preview_enabled')
|
|
||||||
md_view_settings.erase('markdown_preview_id')
|
|
||||||
sublime.set_timeout_async(callback, 250)
|
|
||||||
47
sample.md
47
sample.md
@ -1,46 +1,13 @@
|
|||||||
# Hello world!
|
# DuckDuckGo - The Search engine you'll fall in love with this is a test.
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
This is a test, and this is pretty cool!
|
||||||
|
|
||||||
### The Zen of Python, by Tim Peters
|

|
||||||
|
|
||||||
> Beautiful is better than ugly.
|
Hope you'll enjoy using MarkdownLivePreview!
|
||||||
> Explicit is better than implicit.
|
|
||||||
> Simple is better than complex.
|
|
||||||
> Complex is better than complicated.
|
|
||||||
> Flat is better than nested.
|
|
||||||
> Sparse is better than dense.
|
|
||||||
> Readability counts.
|
|
||||||
> Special cases aren't special enough to break the rules.
|
|
||||||
> Although practicality beats purity.
|
|
||||||
> Errors should never pass silently.
|
|
||||||
> Unless explicitly silenced.
|
|
||||||
> In the face of ambiguity, refuse the temptation to guess.
|
|
||||||
> There should be one-- and preferably only one --obvious way to do it.
|
|
||||||
> Although that way may not be obvious at first unless you're Dutch.
|
|
||||||
> Now is better than never.
|
|
||||||
> Although never is often better than *right* now.
|
|
||||||
> If the implementation is hard to explain, it's a bad idea.
|
|
||||||
> If the implementation is easy to explain, it may be a good idea.
|
|
||||||
> Namespaces are one honking great idea -- let's do more of those!
|
|
||||||
|
|
||||||
> Code tells you how, comments tells you why
|

|
||||||
|
|
||||||
print('hello world')
|

|
||||||
print('hi')
|
|
||||||
|
|
||||||
```python
|
this is a tets
|
||||||
print('This is some pretty')
|
|
||||||
print('cool stuff')
|
|
||||||
if test:
|
|
||||||
print('hello world')
|
|
||||||
```
|
|
||||||
|
|
||||||
This is some `code`
|
|
||||||
|
|
||||||
- a
|
|
||||||
- list
|
|
||||||
|
|
||||||
1. and
|
|
||||||
2. other
|
|
||||||
3. list
|
|
||||||
|
|||||||
8
setting_names.py
Normal file
8
setting_names.py
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# -*- 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'
|
||||||
|
JUST_CREATED = 'markdown_live_preview_just_created'
|
||||||
12
todo.md
12
todo.md
@ -1,12 +0,0 @@
|
|||||||
# todo
|
|
||||||
|
|
||||||
- add message in status bar @notGoodIdea
|
|
||||||
|
|
||||||
|
|
||||||
- add **custom css** feature @done
|
|
||||||
- sync scroll @done
|
|
||||||
- regive focus to the right markdown view @done
|
|
||||||
- set the title of the preview @done
|
|
||||||
- disable previewing when the preview is closed @done
|
|
||||||
- check when setting is activated and create panel and stuff @done
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user