Compare commits
47 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 192f61bf0c | |||
| 2785df74ce | |||
| e13842ede4 | |||
| c6ac821c4a | |||
| 9ad3f25d14 | |||
| eaa357a65f | |||
| 0f5630c3dc | |||
| c14c28b56b | |||
| 0dea8afba4 | |||
| e3896a6b3d | |||
| 6016f07cd1 | |||
| c0c9867cc8 | |||
| 5f2cac54e8 | |||
| 8c1012eb8c | |||
| cc28bfef96 | |||
| ef9b2daf6d | |||
| bae26fc452 | |||
| 5738f6b5ff | |||
| 6bb8e6ebaa | |||
| 8eb6882d60 | |||
| 61cf2984eb | |||
| 7f7dcd6ba8 | |||
| d3d88ddb49 | |||
| 9a8ac3886e | |||
| d4c477749c | |||
| 79c785176f | |||
| 82ad98085f | |||
| dd184c5fdd | |||
| c334c49592 | |||
| 41c28e2b24 | |||
| e1eb17fe96 | |||
| 823d22afee | |||
| 91f4bc5052 | |||
| 7126c0e090 | |||
| 6a3dd6ac2f | |||
| 1542e5e898 | |||
| 05c471b5d9 | |||
| 76f580ba29 | |||
| 119acbb092 | |||
| 7c4354fb2e | |||
| b93aea6698 | |||
| b3fb5779d3 | |||
| 7bdda5f5c7 | |||
| 7257cb467e | |||
| 40a563fb1e | |||
| 3e0d6ad265 | |||
| f65a068b4e |
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__/
|
||||
cache.txt
|
||||
venv/
|
||||
site/
|
||||
__pycache__
|
||||
|
||||
@ -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,16 +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"
|
||||
}
|
||||
@ -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>
|
||||
122
MLPApi.py
@ -1,122 +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__)
|
||||
|
||||
USER_STYLE_FILE = os.path.join(os.path.dirname(__folder__), 'User', 'MarkdownLivePreview.css')
|
||||
|
||||
# used to store the phantom's set
|
||||
windows_phantom_set = {}
|
||||
|
||||
|
||||
def plugin_loaded():
|
||||
global DEFAULT_STYLE_FILE
|
||||
if os.path.exists(os.path.join(__folder__, 'default.css')):
|
||||
with open(os.path.join(__folder__, 'default.css')) as fp:
|
||||
DEFAULT_STYLE_FILE = fp.read()
|
||||
else:
|
||||
DEFAULT_STYLE_FILE = sublime.load_resource('Packages/MarkdownLivePreview/default.css')
|
||||
|
||||
def create_preview(window, file_name):
|
||||
preview = window.new_file()
|
||||
|
||||
preview.set_name(get_preview_name(file_name))
|
||||
preview.set_scratch(True)
|
||||
preview.set_syntax_file('Packages/MarkdownLivePreview/.sublime/' + \
|
||||
'MarkdownLivePreviewSyntax.hidden-tmLanguage')
|
||||
|
||||
return preview
|
||||
|
||||
def get_style():
|
||||
content = ''.join([line.strip() + ' ' for line in DEFAULT_STYLE_FILE.splitlines()])
|
||||
if os.path.exists(USER_STYLE_FILE):
|
||||
with open(USER_STYLE_FILE) as fp:
|
||||
content += '\n' + fp.read() + '\n'
|
||||
return content
|
||||
|
||||
def markdown2html(md, basepath):
|
||||
# removes/format the header.
|
||||
md = manage_header(md, get_settings().get('header_action'))
|
||||
|
||||
html = '<style>\n{}\n</style>\n'.format(get_style())
|
||||
|
||||
|
||||
# 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', 'no-code-highlighting', 'tables'])
|
||||
|
||||
# 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=os.path.dirname(basepath))
|
||||
|
||||
# BeautifulSoup uses the <br/> but the sublime phantoms do not support them...
|
||||
html = html.replace('<br/>', '<br />').replace('<hr/>', '<hr />')
|
||||
|
||||
sublime.set_clipboard(html) # print
|
||||
|
||||
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()))
|
||||
|
||||
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,113 +1,237 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import os.path
|
||||
import sublime
|
||||
import sublime_plugin
|
||||
|
||||
from .MLPApi import *
|
||||
from .setting_names import *
|
||||
from .functions import *
|
||||
from functools import partial
|
||||
|
||||
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"""
|
||||
|
||||
current_view = sublime.active_window().active_view()
|
||||
file_name = current_view.file_name()
|
||||
if get_settings().get('keep_open_when_opening_preview') is False:
|
||||
current_view.close()
|
||||
if file_name is None:
|
||||
return sublime.error_message('MarkdownLivePreview: Not supporting '
|
||||
'unsaved file for now')
|
||||
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)
|
||||
|
||||
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()
|
||||
resources = {}
|
||||
|
||||
mdsettings.set(PREVIEW_ENABLED, True)
|
||||
mdsettings.set(PREVIEW_ID, preview.id())
|
||||
|
||||
def plugin_loaded():
|
||||
resources["base64_404_image"] = get_resource("404.base64")
|
||||
resources["base64_loading_image"] = get_resource("loading.base64")
|
||||
resources["stylesheet"] = get_resource("stylesheet.css")
|
||||
|
||||
|
||||
# try to reload the resources if we save this file
|
||||
try:
|
||||
plugin_loaded()
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
# Terminology
|
||||
# original_view: the view in the regular editor, without it's own window
|
||||
# markdown_view: the markdown view, in the special window
|
||||
# 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")
|
||||
preview_window = sublime.active_window()
|
||||
|
||||
preview_window.run_command(
|
||||
"set_layout",
|
||||
{
|
||||
"cols": [0.0, 0.5, 1.0],
|
||||
"rows": [0.0, 1.0],
|
||||
"cells": [[0, 0, 1, 1], [1, 0, 2, 1]],
|
||||
},
|
||||
)
|
||||
|
||||
preview_window.focus_group(1)
|
||||
preview_view = preview_window.new_file()
|
||||
preview_view.set_scratch(True)
|
||||
preview_view.settings().set(PREVIEW_VIEW_INFOS, {})
|
||||
preview_view.set_name("Preview")
|
||||
|
||||
preview_window.focus_group(0)
|
||||
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):
|
||||
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):
|
||||
|
||||
def update(self, view):
|
||||
vsettings = view.settings()
|
||||
if not vsettings.get(PREVIEW_ENABLED):
|
||||
phantom_sets = {
|
||||
# markdown_view.id(): phantom set
|
||||
}
|
||||
|
||||
# we schedule an update for every key stroke, with a delay of DELAY
|
||||
# then, we update only if now() - last_update > DELAY
|
||||
last_update = 0
|
||||
|
||||
# FIXME: maybe we shouldn't restore the file in the original window...
|
||||
|
||||
def on_pre_close(self, markdown_view):
|
||||
""" 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
|
||||
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)
|
||||
return view, preview
|
||||
self.markdown_view = markdown_view
|
||||
self.preview_window = markdown_view.window()
|
||||
self.file_name = markdown_view.file_name()
|
||||
|
||||
def on_modified(self, view):
|
||||
if not is_markdown_view(view): # faster than getting the settings
|
||||
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:
|
||||
self.content = None
|
||||
|
||||
def on_load_async(self, markdown_view):
|
||||
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||
if not infos:
|
||||
return
|
||||
self.update(view)
|
||||
|
||||
def on_window_command(self, window, command, args):
|
||||
if command == 'close' and window.settings().get(PREVIEW_WINDOW):
|
||||
release_phantoms_set(window.id())
|
||||
return 'close_window', {}
|
||||
preview_view = markdown_view.window().active_view_in_group(1)
|
||||
|
||||
def on_activated_async(self, view):
|
||||
vsettings = view.settings()
|
||||
self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view)
|
||||
self._update_preview(markdown_view)
|
||||
|
||||
if (is_markdown_view(view)
|
||||
and get_settings().get('markdown_live_preview_on_open')
|
||||
and not vsettings.get(PREVIEW_ENABLED)
|
||||
and vsettings.get('syntax') != 'Packages/MarkdownLivePreview/' + \
|
||||
'.sublime/MarkdownLivePreviewSyntax' + \
|
||||
'.hidden-tmLanguage'):
|
||||
sublime.run_command('new_markdown_live_preview')
|
||||
|
||||
|
||||
def on_load_async(self, view):
|
||||
"""Check the settings to hide menu, minimap, etc"""
|
||||
try:
|
||||
md_view, preview = self.update(view)
|
||||
except TypeError:
|
||||
# the function update has returned None
|
||||
def on_close(self, markdown_view):
|
||||
""" Use the information saved to restore the markdown_view as an original_view
|
||||
"""
|
||||
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||
if not infos:
|
||||
return
|
||||
window = preview.window()
|
||||
psettings = preview.settings()
|
||||
|
||||
show_tabs = psettings.get('show_tabs')
|
||||
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')
|
||||
assert (
|
||||
markdown_view.id() == self.markdown_view.id()
|
||||
), "pre_close view.id() != close view.id()"
|
||||
|
||||
if show_tabs is not None:
|
||||
window.set_tabs_visible(show_tabs)
|
||||
if show_minimap is not None:
|
||||
window.set_minimap_visible(show_minimap)
|
||||
if show_status_bar is not None:
|
||||
window.set_status_bar_visible(show_status_bar)
|
||||
if show_sidebar is not None:
|
||||
window.set_sidebar_visible(show_sidebar)
|
||||
if show_menus is not None:
|
||||
window.set_menu_visible(show_menus)
|
||||
del self.phantom_sets[markdown_view.id()]
|
||||
|
||||
class MarkdownLivePreviewClearCacheCommand(sublime_plugin.ApplicationCommand):
|
||||
self.preview_window.run_command("close_window")
|
||||
|
||||
def run(self):
|
||||
clear_cache()
|
||||
# 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}),
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
7
MarkdownLivePreview.sublime-commands
Normal file
@ -0,0 +1,7 @@
|
||||
[
|
||||
|
||||
{
|
||||
"caption": "MarkdownLivePreview: Open Preview",
|
||||
"command": "open_markdown_preview"
|
||||
}
|
||||
]
|
||||
@ -1,37 +0,0 @@
|
||||
Fast:
|
||||
☐ cache image in object when used, so that it's faster @needsTest
|
||||
☐ add settings to keep md view open #13
|
||||
|
||||
Medium:
|
||||
☐ auto refresh preview if loading images
|
||||
☐ use alt attribute for 404 error
|
||||
|
||||
Long:
|
||||
☐ support anchor (TOC) @big
|
||||
|
||||
Unknown:
|
||||
|
||||
|
||||
___________________
|
||||
Archive:
|
||||
✔ 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
|
||||
65
README.md
@ -1,57 +1,26 @@
|
||||
# MarkdownLivePreview
|
||||
|
||||
This is a sublime text **3** plugin that allows you to preview your markdown instantly *in* it!
|
||||
A simple plugin to preview your markdown as you type right in Sublime Text.
|
||||
No dependencies!
|
||||
|
||||
### Dependencies
|
||||
## How to install
|
||||
|
||||
**None! There is no dependency!** It uses [markdown2](https://github.com/trentm/python-markdown2) but it's a one file plugin, so it's included in the package.
|
||||
It's available on package control!
|
||||
|
||||
## Installation
|
||||
## How to contribute
|
||||
|
||||
MarkdownLivePreview is available on the default channel of
|
||||
[PackageControl](http://packagecontrol.io), which means you just have to
|
||||
If you know what feature you want to implement, or what bug you wanna fix, then
|
||||
go ahead and hack! But if you wanna contribute just to say thanks, and don't
|
||||
really know what you could be working on, then there are a bunch of `FIXME`s
|
||||
in `MarkdownLivePreview.py` and `markdown2html.py` (GitHub only shows the top
|
||||
2 results if you try to search using their interface :slightly_frowning_face:).
|
||||
|
||||
1. Open the command palette (`ctrl+shift+p`)
|
||||
2. Search for: `Package Control: Install Package`
|
||||
3. Search for: `MarkdownLivePreview`
|
||||
4. hit <kbd>enter</kbd>
|
||||
### Hack it!
|
||||
|
||||
to have MarkdownLivePreview working on your computer. Cool right? You can
|
||||
[thank package control](https://packagecontrol.io/say_thanks) for this. :wink:
|
||||
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!
|
||||
|
||||
### 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
|
||||
FIXME: add a git hook to format using black (can the git hook be added on github?)
|
||||
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;
|
||||
}
|
||||
|
||||
pre {
|
||||
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,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()
|
||||
113
functions.py
@ -1,113 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
import base64
|
||||
import os.path
|
||||
import sublime
|
||||
import re
|
||||
from .image_manager import ImageManager
|
||||
from bs4 import BeautifulSoup, Comment as html_comment
|
||||
|
||||
def plugin_loaded():
|
||||
global error404, loading
|
||||
loading = sublime.load_resource('Packages/MarkdownLivePreview/loading.txt')
|
||||
error404 = sublime.load_resource('Packages/MarkdownLivePreview/404.txt')
|
||||
|
||||
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_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.replaceWith(BeautifulSoup(''.join(str(node) for node in pre.contents) \
|
||||
.replace('\n', '<br/>').replace(' ', '<i class="space">.</i>'), 'html.parser'))
|
||||
return str(soup)
|
||||
@ -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)
|
||||
1471
lib/markdown2.py
@ -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)
|
||||
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=
|
||||
138
markdown2html.py
Normal file
@ -0,0 +1,138 @@
|
||||
""" Notice how this file is completely independent of sublime text
|
||||
|
||||
I think it should be kept this way, just because it gives a bit more organisation,
|
||||
and makes it a lot easier to think about, and for anyone who would want to, test since
|
||||
markdown2html is just a pure function
|
||||
"""
|
||||
|
||||
import os.path
|
||||
import concurrent.futures
|
||||
import urllib.request
|
||||
import base64
|
||||
import bs4
|
||||
|
||||
from functools import 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):
|
||||
""" Gets the base64 for the image (local and remote images). re_render is a
|
||||
callback which is called when we finish loading an image from the internet
|
||||
to trigger an update of the preview (the image will then be loaded from the cache)
|
||||
"""
|
||||
|
||||
def callback(path, future):
|
||||
# altering image_cache 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[path] = 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 in images_cache:
|
||||
return images_cache[path]
|
||||
|
||||
if path.startswith("http://") or path.startswith("https://"):
|
||||
executor.submit(load_image, path).add_done_callback(partial(callback, path))
|
||||
raise LoadingError()
|
||||
|
||||
with open(path, "rb") as fp:
|
||||
image = "data:image/png;base64," + base64.b64encode(fp.read()).decode("utf-8")
|
||||
images_cache[path] = image
|
||||
return image
|
||||
|
||||
|
||||
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);
|
||||
}
|
||||
47
sample.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* and **bold** text.
|
||||
|
||||
```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)
|
||||
@ -1,8 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
PREVIEW_ENABLED = 'markdown_live_preview_enabled'
|
||||
PREVIEW_ID = 'markdown_live_preview_id'
|
||||
IS_PREVIEW = 'is_markdown_live_preview'
|
||||
IS_HIDDEN = 'is_hidden_markdown_live_preview'
|
||||
MD_VIEW_ID = 'markdown_live_preview_md_id'
|
||||
PREVIEW_WINDOW = 'markdown_live_preview_window'
|
||||
27
utils.py
Normal file
@ -0,0 +1,27 @@
|
||||
# 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
|
||||