Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| f48ef63956 | |||
| a4e670de43 | |||
| d466a29cd4 | |||
| 0f0e53ff34 | |||
| 30ac30082f | |||
| e4e7c44c3c | |||
| 75a8cf53f9 | |||
| 3dcaed0ede | |||
| c605ffb3db | |||
| 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
|
||||||
|
|||||||
@ -1,10 +1,7 @@
|
|||||||
[
|
[
|
||||||
{
|
{
|
||||||
"keys": ["alt+m"],
|
"keys": ["alt+m"],
|
||||||
"command": "toggle_setting",
|
"command": "new_markdown_live_preview",
|
||||||
"args": {
|
|
||||||
"setting": "markdown_preview_enabled"
|
|
||||||
},
|
|
||||||
"context": [
|
"context": [
|
||||||
{
|
{
|
||||||
"key": "selector",
|
"key": "selector",
|
||||||
6
.sublime/MarkdownLivePreview.sublime-commands
Normal file
6
.sublime/MarkdownLivePreview.sublime-commands
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"caption": "MarkdownLivePreview: Edit Current File",
|
||||||
|
"command": "new_markdown_live_preview"
|
||||||
|
}
|
||||||
|
]
|
||||||
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://"]
|
||||||
|
}
|
||||||
83
MLPApi.py
Normal file
83
MLPApi.py
Normal file
@ -0,0 +1,83 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
|
||||||
|
import os.path
|
||||||
|
from html.parser import HTMLParser
|
||||||
|
|
||||||
|
from .lib import markdown2 as md2
|
||||||
|
from .escape_amp import *
|
||||||
|
from .functions import *
|
||||||
|
from .setting_names import *
|
||||||
|
|
||||||
|
__folder__ = os.path.dirname(__file__)
|
||||||
|
|
||||||
|
STYLE_FILE = os.path.join(os.path.dirname(__folder__), 'User',
|
||||||
|
'MarkdownLivePreview.css')
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_loaded():
|
||||||
|
global DEFAULT_STYLE_FILE
|
||||||
|
DEFAULT_STYLE_FILE = sublime.load_resource('Packages/MarkdownLivePreview/'
|
||||||
|
'default.css')
|
||||||
|
|
||||||
|
def get_preview_name(md_view):
|
||||||
|
file_name = md_view.file_name()
|
||||||
|
name = md_view.name() \
|
||||||
|
or os.path.basename(file_name) if file_name else None \
|
||||||
|
or 'Untitled'
|
||||||
|
return name + ' - Preview'
|
||||||
|
|
||||||
|
def create_preview(window, file_name):
|
||||||
|
preview = window.new_file()
|
||||||
|
|
||||||
|
preview.set_name(get_preview_name(file_name))
|
||||||
|
preview.set_scratch(True)
|
||||||
|
|
||||||
|
return preview
|
||||||
|
|
||||||
|
def get_style():
|
||||||
|
content = ''.join([line.strip() + ' ' for line in DEFAULT_STYLE_FILE.splitlines()])
|
||||||
|
return content + "pre code .space {color: var(--light-bg)}"
|
||||||
|
|
||||||
|
def show_html(md_view, preview):
|
||||||
|
html = []
|
||||||
|
html.append('<style>\n{}\n</style>'.format(get_style()))
|
||||||
|
html.append(pre_with_br(md2.markdown(get_view_content(md_view),
|
||||||
|
extras=['fenced-code-blocks',
|
||||||
|
'no-code-highlighting'])))
|
||||||
|
|
||||||
|
html = '\n'.join(html)
|
||||||
|
|
||||||
|
# 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}))
|
||||||
|
|
||||||
|
return
|
||||||
|
# set viewport position
|
||||||
|
# 0 < y < 1
|
||||||
|
y = md_view.text_to_layout(md_view.sel()[0].begin())[1] / md_view.layout_extent()[1]
|
||||||
|
vector = [0, y * preview.layout_extent()[1]]
|
||||||
|
# remove half of the viewport_extent.y to center it on the screen (verticaly)
|
||||||
|
vector[1] -= preview.viewport_extent()[1] / 2
|
||||||
|
vector[1] = mini(vector[1], 0)
|
||||||
|
vector[1] += preview.line_height()
|
||||||
|
preview.set_viewport_position(vector, animate=False)
|
||||||
65
MarkdownLivePreview.py
Normal file
65
MarkdownLivePreview.py
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
|
||||||
|
from .MLPApi import *
|
||||||
|
from .setting_names import *
|
||||||
|
from .functions import *
|
||||||
|
|
||||||
|
class NewMarkdownLivePreviewCommand(sublime_plugin.ApplicationCommand):
|
||||||
|
|
||||||
|
def run(self):
|
||||||
|
|
||||||
|
"""Inspired by the edit_settings command"""
|
||||||
|
|
||||||
|
current_view = sublime.active_window().active_view()
|
||||||
|
file_name = current_view.file_name()
|
||||||
|
current_view.close()
|
||||||
|
if file_name is None:
|
||||||
|
return sublime.error_message('Not supporting unsaved file for now')
|
||||||
|
|
||||||
|
sublime.run_command('new_window')
|
||||||
|
self.window = sublime.active_window()
|
||||||
|
self.window.settings().set(PREVIEW_WINDOW, True)
|
||||||
|
self.window.run_command('set_layout', {
|
||||||
|
'cols': [0.0, 0.5, 1.0],
|
||||||
|
'rows': [0.0, 1.0],
|
||||||
|
'cells': [[0, 0, 1, 1], [1, 0, 2, 1]]
|
||||||
|
})
|
||||||
|
self.window.focus_group(1)
|
||||||
|
preview = create_preview(self.window, current_view)
|
||||||
|
|
||||||
|
self.window.focus_group(0)
|
||||||
|
md_view = self.window.open_file(file_name)
|
||||||
|
mdsettings = md_view.settings()
|
||||||
|
|
||||||
|
mdsettings.set(PREVIEW_ENABLED, True)
|
||||||
|
mdsettings.set(PREVIEW_ID, preview.id())
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
return is_markdown_view(sublime.active_window().active_view())
|
||||||
|
|
||||||
|
class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
||||||
|
|
||||||
|
def on_modified(self, view):
|
||||||
|
if not is_markdown_view(view): # faster than getting the settings
|
||||||
|
return
|
||||||
|
vsettings = view.settings()
|
||||||
|
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)
|
||||||
|
|
||||||
|
def on_window_command(self, window, command, args):
|
||||||
|
if command == 'close' and window.settings().get(PREVIEW_WINDOW):
|
||||||
|
return 'close_window', {}
|
||||||
|
|
||||||
|
def on_load_async(self, view):
|
||||||
|
self.on_modified(view)
|
||||||
@ -1,9 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview: Toggle",
|
|
||||||
"command": "toggle_setting",
|
|
||||||
"args": {
|
|
||||||
"setting": "markdown_preview_enabled"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
34
MarkdownLivePreview.tasks
Normal file
34
MarkdownLivePreview.tasks
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
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
|
||||||
|
☐ add settings for the preview
|
||||||
|
|
||||||
|
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
|
||||||
|
☐ fix relative source
|
||||||
|
|
||||||
|
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
|
||||||
10
README.md
10
README.md
@ -28,7 +28,11 @@ Done!
|
|||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
Sometimes, you just want to open a markdown file to edit it quickly, you don't care about the preview, and even worse, **you don't want it**. So, if you want to have the preview, press `alt+m`, edit your file, and you'll get a nice preview.
|
You can choose to enable MarkdownLivePreview by pressing <kbd>alt+m</kbd> or selecting in the command palette `MarkdownLivePreview: Edit Current File`. Note that you need to be editing (simply having the focus on) a markdown file. Because [Markdown Extended][markdown-extended] did a good job, it's compatible with this plugin.
|
||||||
|
|
||||||
|
It will open a new window, with only your markdown file, with the preview. Once your done, close whichever file and it'll close the entire window.
|
||||||
|
|
||||||
|
*Notice that it will close the entire window if you close **whichever** file. It means that if you open a random file in this window, and then close it, it'll close the entire window still*
|
||||||
|
|
||||||
### In dev
|
### In dev
|
||||||
|
|
||||||
@ -45,3 +49,7 @@ It is possible to set your own css. But, be carefull, you have to respect [those
|
|||||||
### How to open the [README](http://github.com/math2001/MarkdownLivePreview/README.md)
|
### How to open the [README](http://github.com/math2001/MarkdownLivePreview/README.md)
|
||||||
|
|
||||||
Some of the package add a command in the menus, others in the command palette, or other nowhere. None of those options are really good, especially the last one on ST3 because the packages are compressed. But, fortunately, there is plugin that exists and **will solve this problem** for us (and he has a really cute name, don't you think?): [ReadmePlease](https://packagecontrol.io/packages/ReadmePlease).
|
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
|
||||||
|
|||||||
39
default.css
Normal file
39
default.css
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
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;
|
||||||
|
}
|
||||||
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
|
||||||
|
})
|
||||||
39
escape_amp.py
Normal file
39
escape_amp.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# -*- 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()
|
||||||
100
functions.py
Normal file
100
functions.py
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
# -*- 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):
|
||||||
|
if not isinstance(id, int):
|
||||||
|
return
|
||||||
|
for view in window.views():
|
||||||
|
if view.id() == id:
|
||||||
|
return view
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
return sublime.load_settings('MarkdownLivePreview.sublime-settings')
|
||||||
|
|
||||||
|
def pre_with_br(html):
|
||||||
|
"""Because the phantoms of sublime text does not support <pre> blocks
|
||||||
|
this function replaces every \n with a <br> in a <pre>"""
|
||||||
|
|
||||||
|
while True:
|
||||||
|
obj = re.search(r'<pre>(.*?)</pre>', html, re.DOTALL)
|
||||||
|
if not obj:
|
||||||
|
break
|
||||||
|
html = list(html)
|
||||||
|
html[obj.start(0):obj.end(0)] = '<pre >' + ''.join(html[obj.start(1):obj.end(1)]) \
|
||||||
|
.replace('\n', '<br>') \
|
||||||
|
.replace(' ', ' ') + '</pre>'
|
||||||
|
html = ''.join(html)
|
||||||
|
return html
|
||||||
98
image_manager.py
Normal file
98
image_manager.py
Normal file
@ -0,0 +1,98 @@
|
|||||||
|
# -*- 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)
|
||||||
BIN
imgs/404-image.png
Normal file
BIN
imgs/404-image.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
1
imgs/README.md
Normal file
1
imgs/README.md
Normal file
@ -0,0 +1 @@
|
|||||||
|
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)
|
||||||
BIN
imgs/loading.png
Normal file
BIN
imgs/loading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 953 B |
@ -1,3 +1,4 @@
|
|||||||
|
# CSW: ignore
|
||||||
# -*- encoding: utf-8 -*-
|
# -*- encoding: utf-8 -*-
|
||||||
|
|
||||||
#!/usr/bin/env python
|
#!/usr/bin/env python
|
||||||
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)
|
|
||||||
4
messages.json
Normal file
4
messages.json
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"install": "messages/install.txt",
|
||||||
|
"1.1.2": "messages/1.1.2.txt"
|
||||||
|
}
|
||||||
12
messages/1.1.2.txt
Normal file
12
messages/1.1.2.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Sorry to interupt you... :(
|
||||||
|
|
||||||
|
Small changes occured on MarkdownLivePreview.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Main callback is now async (prevent ST from crashing)
|
||||||
|
|
||||||
|
|
||||||
|
Tip of the day: Delete all the content till the end of the line: Ctrl+k, Ctrl+k
|
||||||
|
You'll use it much more than you think ;)
|
||||||
42
messages/install.txt
Normal file
42
messages/install.txt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
__ __ _ _ _ _ _____ _
|
||||||
|
| \/ | | | | | | | (_) | __ \ (_)
|
||||||
|
| \ / | __ _ _ __| | ____| | _____ ___ __ | | ___ _____| |__) | __ _____ ___ _____ __
|
||||||
|
| |\/| |/ _` | '__| |/ / _` |/ _ \ \ /\ / / '_ \| | | \ \ / / _ \ ___/ '__/ _ \ \ / / |/ _ \ \ /\ / /
|
||||||
|
| | | | (_| | | | < (_| | (_) \ V V /| | | | |____| |\ V / __/ | | | | __/\ V /| | __/\ V V /
|
||||||
|
|_| |_|\__,_|_| |_|\_\__,_|\___/ \_/\_/ |_| |_|______|_| \_/ \___|_| |_| \___| \_/ |_|\___| \_/\_/
|
||||||
|
|
||||||
|
Thanks for installing MarkdownLivePreview! I hope you'll enjoy using it!
|
||||||
|
|
||||||
|
Quick Start:
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To enable MarkdownLivePreview, you need to me on a markdown view (works with
|
||||||
|
Markdown Extended). Then just press `alt+m`, or search up in the command
|
||||||
|
palette: 'MarkdownLivePreview: Toggle'. Hit enter and you're done. As soon as
|
||||||
|
you'll type anything in, it'll show up the preview in a new group.
|
||||||
|
|
||||||
|
Say thanks:
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Just letting me know you're enjoying this plugin is a great way to say thanks!
|
||||||
|
|
||||||
|
You can do so by staring the GitHub repo, or sending a tweet (@_math2001) for
|
||||||
|
example!
|
||||||
|
|
||||||
|
Troubles?
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
If you have any kind of trouble with it, just let me now by raising an issue on
|
||||||
|
the GitHub issue tracker here:
|
||||||
|
|
||||||
|
https://github.com/math2001/MarkdownLivePreview/issues
|
||||||
|
|
||||||
|
Tip of the day: Right click on a URL and choose 'Open <the url>' to open it in
|
||||||
|
your default browser.
|
||||||
|
|
||||||
|
Credits:
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
The ASCII MLP has been generated using the ASCII Decorator.
|
||||||
|
|
||||||
|
https://github.com/viisual/ASCII-Decorator
|
||||||
47
sample.md
47
sample.md
@ -1,46 +1,19 @@
|
|||||||
# Hello world!
|
# DuckDuckGo - The Search engine you'll fall in love
|
||||||
|
|
||||||
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 cool. The stylesheet's back!
|
||||||
|
|
||||||
### The Zen of Python, by Tim Peters
|
This is a test, and this is pretty cool!
|
||||||
|
|
||||||
> 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
|
```python
|
||||||
print('This is some pretty')
|
print(hello world)
|
||||||
print('cool stuff')
|
if DEBUG:
|
||||||
if test:
|
print('DEBUG_MODE on')
|
||||||
print('hello world')
|
|
||||||
```
|
```
|
||||||
|
|
||||||
This is some `code`
|
> Only a fool knows everything. A wise man knows how little he knows
|
||||||
|
|
||||||
- a
|
The only think I know right now is that Boxy Theme's just awesome.
|
||||||
- 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'
|
||||||
|
PREVIEW_WINDOW = 'markdown_live_preview_window'
|
||||||
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