Merge remote-tracking branch 'update/master'
This is a weird merge. I had re-written everything in a separate repository, and basically wanted that new repo (update) to be the new master of this repository, whilst preserving the history of the update repo. Here's what I did: $ git remote add update <path to update> $ git fetch update $ git merge -X theirs --allow-unrelated-histories update/master $ # remove extra files $ git commit --amend
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
||||
docs/ export-ignore
|
||||
resources/*.png export-ignore
|
||||
resources/*.py export-ignore
|
||||
6
.gitignore
vendored
@ -1,5 +1 @@
|
||||
Thumbs.db
|
||||
__pycache__/
|
||||
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,19 +0,0 @@
|
||||
{
|
||||
// As soon as you open a markdown file, it opens the window preview
|
||||
"markdown_live_preview_on_open": false,
|
||||
|
||||
// If an image starts with one of those strings, then it will be loaded from internet
|
||||
"load_from_internet_when_starts": ["http://", "https://"],
|
||||
|
||||
// When the preview is opened, the markdown file is closed in the origin window and reopend in
|
||||
// the preview window. If this option is set to 'true', then the markdown file will NOT be
|
||||
// closed in the origin window
|
||||
"keep_open_when_opening_preview": false,
|
||||
|
||||
// Choose what to do with YAML/TOML (---/+++ respectively) headers
|
||||
// Valid values: "wrap_in_pre", "remove".
|
||||
"header_action": "wrap_in_pre",
|
||||
|
||||
// Wait at least the specified *seconds* before updating the preview.
|
||||
"update_preview_every": 0
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>name</key>
|
||||
<string>MarkdownLivePreviewSyntax</string>
|
||||
|
||||
<key>patterns</key>
|
||||
<array>
|
||||
</array>
|
||||
<key>scopeName</key>
|
||||
<string>text.markdown-live-preview</string>
|
||||
</dict>
|
||||
</plist>
|
||||
106
MLPApi.py
@ -1,106 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import sublime
|
||||
import sublime_plugin
|
||||
|
||||
import os.path
|
||||
from html.parser import HTMLParser
|
||||
|
||||
from .lib import markdown2 as md2
|
||||
from .lib.pre_tables import pre_tables
|
||||
|
||||
from .escape_amp import *
|
||||
from .functions import *
|
||||
from .setting_names import *
|
||||
from .image_manager import CACHE_FILE
|
||||
from random import randint as rnd
|
||||
|
||||
__folder__ = os.path.dirname(__file__)
|
||||
|
||||
|
||||
# used to store the phantom's set
|
||||
windows_phantom_set = {}
|
||||
|
||||
|
||||
def create_preview(window, file_name):
|
||||
preview = window.new_file()
|
||||
|
||||
preview.set_name(get_preview_name(file_name))
|
||||
preview.set_scratch(True)
|
||||
preview.set_syntax_file('Packages/MarkdownLivePreview/.sublime/' + \
|
||||
'MarkdownLivePreviewSyntax.hidden-tmLanguage')
|
||||
|
||||
return preview
|
||||
|
||||
def markdown2html(md, basepath, color_scheme):
|
||||
|
||||
# removes/format the YAML/TOML header.
|
||||
md = manage_header(md, get_settings().get('header_action'))
|
||||
|
||||
html = '<style>\n{}\n</style>\n'.format(get_style(color_scheme))
|
||||
|
||||
|
||||
# the option no-code-highlighting does not exists in the official version of markdown2 for now
|
||||
# I personaly edited the file (markdown2.py:1743)
|
||||
html += md2.markdown(md, extras=['fenced-code-blocks', 'tables', 'strike'])
|
||||
|
||||
# tables aren't supported by the Phantoms
|
||||
# This function transforms them into aligned ASCII tables and displays them in a <pre> block
|
||||
# (the ironic thing is that they aren't supported either :D)
|
||||
html = pre_tables(html)
|
||||
|
||||
# pre block are not supported by the Phantoms.
|
||||
# This functions replaces the \n in them with <br> so that it does (1/2)
|
||||
html = pre_with_br(html)
|
||||
|
||||
# comments aren't supported by the Phantoms
|
||||
# Simply removes them using bs4, so you can be sadic and type `<!-- hey hey! -->`, these one
|
||||
# won't be stripped!
|
||||
html = strip_html_comments(html)
|
||||
|
||||
# exception, again, because <pre> aren't supported by the phantoms
|
||||
# so, because this is monosaped font, I just replace it with a '.' and make transparent ;)
|
||||
html = html.replace(' ', '<i class="space">.</i>')
|
||||
|
||||
# Phantoms have problem with images size when they're loaded from an url/path
|
||||
# So, the solution is to convert them to base64
|
||||
html = replace_img_src_base64(html, basepath=basepath)
|
||||
|
||||
# BeautifulSoup uses the <br/> but the sublime phantoms do not support them...
|
||||
html = html.replace('<br/>', '<br />').replace('<hr/>', '<hr />')
|
||||
|
||||
return html
|
||||
|
||||
def show_html(md_view, preview):
|
||||
global windows_phantom_set
|
||||
html = markdown2html(get_view_content(md_view), os.path.dirname(md_view.file_name()), md_view.settings().get('color_scheme'))
|
||||
|
||||
phantom_set = windows_phantom_set.setdefault(preview.window().id(),
|
||||
sublime.PhantomSet(preview, 'markdown_live_preview'))
|
||||
phantom_set.update([sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK,
|
||||
lambda href: sublime.run_command('open_url', {'url': href}))])
|
||||
|
||||
# lambda href: sublime.run_command('open_url', {'url': href})
|
||||
# get the "ratio" of the markdown view's position.
|
||||
# 0 < y < 1
|
||||
y = md_view.text_to_layout(md_view.sel()[0].begin())[1] / md_view.layout_extent()[1]
|
||||
# set the vector (position) for the preview
|
||||
vector = [0, y * preview.layout_extent()[1]]
|
||||
# remove half of the viewport_extent.y to center it on the screen (verticaly)
|
||||
vector[1] -= preview.viewport_extent()[1] / 2
|
||||
# make sure the minimum is 0
|
||||
vector[1] = 0 if vector[1] < 0 else vector[1]
|
||||
# the hide the first line
|
||||
vector[1] += preview.line_height()
|
||||
preview.set_viewport_position(vector, animate=False)
|
||||
|
||||
def clear_cache():
|
||||
"""Removes the cache file"""
|
||||
os.remove(CACHE_FILE)
|
||||
|
||||
def release_phantoms_set(view_id=None):
|
||||
global windows_phantom_set
|
||||
if view_id is None:
|
||||
windows_phantom_set = {}
|
||||
else:
|
||||
del windows_phantom_set[view_id]
|
||||
@ -1,124 +1,223 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import os.path
|
||||
import sublime
|
||||
import sublime_plugin
|
||||
import time
|
||||
|
||||
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"""
|
||||
def get_resource(resource):
|
||||
path = 'Packages/MarkdownLivePreview/resources/' + resource
|
||||
abs_path = os.path.join(sublime.packages_path(), '..', path)
|
||||
if os.path.isfile(abs_path):
|
||||
with open(abs_path, 'r') as fp:
|
||||
return fp.read()
|
||||
return sublime.load_resource(path)
|
||||
|
||||
current_view = sublime.active_window().active_view()
|
||||
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')
|
||||
resources = {}
|
||||
|
||||
def plugin_loaded():
|
||||
resources["base64_loading_image"] = get_resource('loading.base64')
|
||||
resources["base64_404_image"] = get_resource('404.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')
|
||||
self.window = sublime.active_window()
|
||||
self.window.settings().set(PREVIEW_WINDOW, True)
|
||||
self.window.run_command('set_layout', {
|
||||
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]]
|
||||
})
|
||||
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()
|
||||
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')
|
||||
|
||||
mdsettings.set(PREVIEW_ENABLED, True)
|
||||
mdsettings.set(PREVIEW_ID, preview.id())
|
||||
|
||||
preview_window.focus_group(0)
|
||||
if file_name:
|
||||
markdown_view = preview_window.open_file(file_name)
|
||||
else:
|
||||
markdown_view = preview_window.new_file()
|
||||
markdown_view.run_command('mdlp_insert', {'point': 0, 'string': content})
|
||||
markdown_view.set_scratch(True)
|
||||
|
||||
markdown_view.set_syntax_file(syntax_file)
|
||||
markdown_view.settings().set(MARKDOWN_VIEW_INFOS, {
|
||||
"original_window_id": original_window_id
|
||||
})
|
||||
|
||||
def is_enabled(self):
|
||||
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()
|
||||
now = time.time()
|
||||
phantom_sets = {
|
||||
# markdown_view.id(): phantom set
|
||||
}
|
||||
|
||||
if now - vsettings.get(LAST_UPDATE, 0) < get_settings().get('update_preview_every'):
|
||||
return
|
||||
vsettings.set(LAST_UPDATE, now)
|
||||
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))
|
||||
# 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
|
||||
|
||||
show_html(view, preview)
|
||||
return view, preview
|
||||
# FIXME: maybe we shouldn't restore the file in the original window...
|
||||
|
||||
def on_modified_async(self, view):
|
||||
if not is_markdown_view(view): # faster than getting the settings
|
||||
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
|
||||
delay = get_settings().get('update_preview_every')
|
||||
if not delay:
|
||||
self.update(view)
|
||||
|
||||
self.markdown_view = markdown_view
|
||||
self.preview_window = markdown_view.window()
|
||||
self.file_name = markdown_view.file_name()
|
||||
|
||||
if self.file_name is None:
|
||||
total_region = sublime.Region(0, markdown_view.size())
|
||||
self.content = markdown_view.substr(total_region)
|
||||
markdown_view.erase(edit, total_region)
|
||||
else:
|
||||
sublime.set_timeout(lambda: self.update(view), delay * 1000)
|
||||
self.content = None
|
||||
|
||||
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', {}
|
||||
|
||||
def on_activated_async(self, view):
|
||||
vsettings = view.settings()
|
||||
|
||||
if (is_markdown_view(view) and get_settings().get(ON_OPEN)
|
||||
and not vsettings.get(PREVIEW_ENABLED)
|
||||
and vsettings.get('syntax') != 'Packages/MarkdownLivePreview/' + \
|
||||
'.sublime/MarkdownLivePreviewSyntax' + \
|
||||
'.hidden-tmLanguage'
|
||||
and not any(filter(lambda window: window.settings().get(PREVIEW_WINDOW) is True,
|
||||
sublime.windows()))):
|
||||
sublime.run_command('new_markdown_live_preview')
|
||||
|
||||
|
||||
def on_load_async(self, view):
|
||||
"""Check the settings to hide menu, minimap, etc"""
|
||||
try:
|
||||
md_view, preview = self.update(view)
|
||||
except TypeError:
|
||||
# the function update has returned None
|
||||
def on_load_async(self, markdown_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')
|
||||
preview_view = markdown_view.window().active_view_in_group(1)
|
||||
|
||||
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)
|
||||
self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view)
|
||||
self._update_preview(markdown_view)
|
||||
|
||||
class MarkdownLivePreviewClearCacheCommand(sublime_plugin.ApplicationCommand):
|
||||
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
|
||||
|
||||
def run(self):
|
||||
clear_cache()
|
||||
assert markdown_view.id() == self.markdown_view.id(), \
|
||||
"pre_close view.id() != close view.id()"
|
||||
|
||||
del self.phantom_sets[markdown_view.id()]
|
||||
|
||||
self.preview_window.run_command('close_window')
|
||||
|
||||
# find the window with the right id
|
||||
original_window = next(window for window in sublime.windows() \
|
||||
if window.id() == infos['original_window_id'])
|
||||
if self.file_name:
|
||||
original_window.open_file(self.file_name)
|
||||
else:
|
||||
assert markdown_view.is_scratch(), "markdown view of an unsaved file should " \
|
||||
"be a scratch"
|
||||
# note here that this is called original_view, because it's what semantically
|
||||
# makes sense, but this original_view.id() will be different than the one
|
||||
# that we closed first to reopen in the preview window
|
||||
# shouldn't cause any trouble though
|
||||
original_view = original_window.new_file()
|
||||
original_view.run_command('mdlp_insert', {'point': 0, 'string': self.content})
|
||||
|
||||
original_view.set_syntax_file(markdown_view.settings().get('syntax'))
|
||||
|
||||
|
||||
# here, views are NOT treated independently, which is theoretically wrong
|
||||
# but in practice, you can only edit one markdown file at a time, so it doesn't really
|
||||
# matter.
|
||||
# @min_time_between_call(.5)
|
||||
def on_modified_async(self, markdown_view):
|
||||
|
||||
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||
if not infos:
|
||||
return
|
||||
|
||||
# we schedule an update, which won't run if an
|
||||
sublime.set_timeout(partial(self._update_preview, markdown_view), DELAY)
|
||||
|
||||
def _update_preview(self, markdown_view):
|
||||
# if the buffer id is 0, that means that the markdown_view has been closed
|
||||
# This check is needed since a this function is used as a callback for when images
|
||||
# are loaded from the internet (ie. it could finish loading *after* the user
|
||||
# closes the markdown_view)
|
||||
if time.time() - self.last_update < DELAY / 1000:
|
||||
return
|
||||
|
||||
if markdown_view.buffer_id() == 0:
|
||||
return
|
||||
|
||||
self.last_update = time.time()
|
||||
|
||||
total_region = sublime.Region(0, markdown_view.size())
|
||||
markdown = markdown_view.substr(total_region)
|
||||
|
||||
basepath = os.path.dirname(markdown_view.file_name())
|
||||
html = markdown2html(
|
||||
markdown,
|
||||
basepath,
|
||||
partial(self._update_preview, markdown_view),
|
||||
resources
|
||||
)
|
||||
|
||||
self.phantom_sets[markdown_view.id()].update([
|
||||
sublime.Phantom(sublime.Region(0), html, sublime.LAYOUT_BLOCK,
|
||||
lambda href: sublime.run_command('open_url', {'url': href}))
|
||||
])
|
||||
|
||||
|
||||
7
MarkdownLivePreview.sublime-commands
Normal file
@ -0,0 +1,7 @@
|
||||
[
|
||||
|
||||
{
|
||||
"caption": "MarkdownLivePreview: Open Preview",
|
||||
"command": "open_markdown_preview"
|
||||
}
|
||||
]
|
||||
65
README.md
@ -1,63 +1,18 @@
|
||||
# MarkdownLivePreview
|
||||
|
||||
This is a sublime text **3** plugin that allows you to preview your markdown instantly *in* it!
|
||||
A simple plugin to preview your markdown as you type right in Sublime Text.
|
||||
No dependencies!
|
||||
|
||||
## Unmaintained
|
||||
## How to install
|
||||
|
||||
I am now using vim. I don't have the energy or the time to maintain this plugin anymore.
|
||||
It's available on package control!
|
||||
|
||||
If anyone is interested in maintaining it, fork it, and submit a PR to package control to make it point to your fork.
|
||||
## How to contribute
|
||||
|
||||
### Dependencies
|
||||
1. Fork this repo
|
||||
2. Make your own branch (the name of the branch should be the feature you are
|
||||
implementing eg. `improve-tables`, `fix-crash-on-multiple-preview`
|
||||
3. All your code should be formated by black.
|
||||
4. Send a PR!
|
||||
|
||||
**None! There is no dependency!** It uses [markdown2](https://github.com/trentm/python-markdown2) but it's a one file plugin, so it's included in the package.
|
||||
|
||||
## Installation
|
||||
|
||||
MarkdownLivePreview is available on the default channel of
|
||||
[PackageControl](http://packagecontrol.io), which means you just have to
|
||||
|
||||
1. Open the command palette (`ctrl+shift+p`)
|
||||
2. Search for: `Package Control: Install Package`
|
||||
3. Search for: `MarkdownLivePreview`
|
||||
4. hit <kbd>enter</kbd>
|
||||
|
||||
to have MarkdownLivePreview working on your computer. Cool right? You can
|
||||
[thank package control](https://packagecontrol.io/say_thanks) for this. :wink:
|
||||
|
||||
### Usage
|
||||
|
||||
You can choose to enable MarkdownLivePreview by pressing <kbd>alt+m</kbd> or selecting in the
|
||||
command palette `MarkdownLivePreview: Edit Current File`. Note that you need to be editing (simply
|
||||
having the focus on) a markdown file. Because [Markdown Extended][markdown-extended] did a good job,
|
||||
it's compatible with this plugin.
|
||||
|
||||
So, once you've run it, it will open a new window, with only your markdown file, with the preview.
|
||||
Once you're done, close whichever file and it'll close the entire window.
|
||||
|
||||
*Notice that it will close the entire window if you close __whichever__ file. It means that if you*
|
||||
*open a random file in this window, and then close it, it'll close the entire window still*
|
||||
|
||||
For further infos, please [read the docs](https://math2001.github.io/MarkdownLivePreview/)!
|
||||
|
||||
### Demo
|
||||
|
||||

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

|
||||
|
||||
!!! tip
|
||||
On Windows at least, you can press <kbd>alt</kbd> to focus (so show) the menus, even if they're
|
||||
originally hidden.
|
||||
|
||||
That's it! I hope you'll enjoy using this package! If it's the case, please let your friends know
|
||||
about it, and even myself by sending me a [tweet][] or staring the repo!
|
||||
<iframe
|
||||
src="https://ghbtns.com/github-btn.html?user=math2001&repo=MarkdownLivePreview&type=star&count=true&size=large"
|
||||
frameborder="0" scrolling="0" style="width: 120px; height: 30px; vertical-align: bottom"></iframe>
|
||||
|
||||
[st]: https://sublimetext.com
|
||||
[Markdown Extended]: https://packagecontrol.io/packages/Markdown%20Extended
|
||||
[pck-con]: https://packagecontrol.io
|
||||
[install-pck-con]: https://packagecontrol.io/installation
|
||||
[tweet]: https://twitter.com/_math2001
|
||||
[GitHub repo]: https://github.com/math2001/MarkdownLivePreview/issues
|
||||
[@ooing]: https://github.com/ooing
|
||||
[@ooing suggestion]: https://github.com/math2001/MarkdownLivePreview/issues/7#issue-199464852
|
||||
[@tanhanjay]: https://github.com/tanhanjay
|
||||
[front-matter-issue]: https://github.com/math2001/MarkdownLivePreview/issues/17
|
||||
[Boxy Theme]: https://packagecontrol.io/packages/Boxy%20Theme
|
||||
[Monokai Color Scheme]: https://github.com/ihodev/sublime-boxy#boxy-monokai--predawn
|
||||
@ -1,24 +0,0 @@
|
||||
This project is published under MIT license.
|
||||
|
||||
> The MIT License is a permissive license that is short and to the point. It lets people do anything
|
||||
> they want with your code as long as they provide attribution back to you and don’t hold you
|
||||
> liable.
|
||||
>
|
||||
> — *from [choosealicense.com](http://choosealicense.com), by [GitHub](https://github.com)*
|
||||
|
||||
Copyright 2017 Mathieu PATUREL
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
|
||||
andassociated documentation files (the "Software"), to deal in the Software without restriction,
|
||||
including without limitation the rights to use, copy, modify, merge, publish, distribute,
|
||||
sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial
|
||||
portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT
|
||||
NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES
|
||||
OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
|
||||
CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
@ -1,39 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import re
|
||||
|
||||
__all__ = ['escape_amp']
|
||||
|
||||
RE_REPLACE_AMPERSAND = re.compile(r'&(\w*)(;)?')
|
||||
|
||||
def replace(matchobj):
|
||||
if matchobj.group(2):
|
||||
return matchobj.group(0)
|
||||
else:
|
||||
return matchobj.group(0).replace('&', '&')
|
||||
|
||||
def escape_amp(text):
|
||||
return RE_REPLACE_AMPERSAND.sub(replace, text)
|
||||
|
||||
def run_tests():
|
||||
tests = [
|
||||
['&', '&'],
|
||||
['&', '&amp'],
|
||||
['&', '&'],
|
||||
['& &hello &bonjour;', '& &hello &bonjour;']
|
||||
]
|
||||
fails = 0
|
||||
for i, (subject, result) in enumerate(tests):
|
||||
if RE_REPLACE_AMPERSAND.sub(replace, subject) != result:
|
||||
# CSW: ignore
|
||||
print('TEST FAIL ({i}): {subject!r} escaped did not match {result!r}'.format(**locals()))
|
||||
fails += 1
|
||||
if fails == 0:
|
||||
# CSW: ignore
|
||||
print("SUCCESS: every tests ({}) passed successfully!".format(len(tests)))
|
||||
else:
|
||||
# CSW: ignore
|
||||
print("{} test{} failed".format(fails, 's' if fails > 1 else ''))
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_tests()
|
||||
47
example.md
@ -1,47 +0,0 @@
|
||||
---
|
||||
title: Demo
|
||||
description: Preview your markdown right in Sublime Text!
|
||||
hope: You'll enjoy using it!
|
||||
---
|
||||
|
||||
# Hello world
|
||||
|
||||
<!-- supports comments -->
|
||||
|
||||
And `<!-- vicious ones ;) -->`
|
||||
|
||||
Some `inline code` with *italic*, **bold** text, and ~~strike through~~.
|
||||
|
||||
```python
|
||||
import this
|
||||
if you is moods.curious:
|
||||
print('then do it!')
|
||||
```
|
||||
|
||||
<kbd>ctrl+\`</kbd> or *View → Show Console* and paste `import this`!
|
||||
|
||||
> Perfect programmers do NOT need comments.
|
||||
|
||||
- to be efficient
|
||||
- you need
|
||||
- todos
|
||||
|
||||
|
||||
| ID | Name |
|
||||
|-----------|-------|
|
||||
| 56 | Matt |
|
||||
| 42 | Colin |
|
||||
| 23 | Lisa |
|
||||
| 45 | John |
|
||||
| `<table>` | `><` |
|
||||
|
||||

|
||||
|
||||
Some plugin I just *need*:
|
||||
|
||||
- [PackageResourceReviewer](https://packagecontrol.io/packages/PackageResourceViewer)
|
||||
- [Boxy Theme](https://packagecontrol.io/packages/Boxy%20Theme)
|
||||
- [Markdown Preview](https://packagecontrol.io/packages/Markdown%20Preview)
|
||||
- [FileManager](https://packagecontrol.io/packages/FileManager)
|
||||
- [PlainTasks](https://packagecontrol.io/packages/PlainTasks)
|
||||
- [JSONComma](https://packagecontrol.io/packages/JSONComma)
|
||||
139
functions.py
@ -1,139 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
import base64
|
||||
import os.path
|
||||
import sublime
|
||||
import re
|
||||
from .image_manager import ImageManager
|
||||
from .lib.pygments_from_theme import pygments_from_theme
|
||||
from bs4 import BeautifulSoup, Comment as html_comment
|
||||
|
||||
def plugin_loaded():
|
||||
global error404, loading, DEFAULT_STYLE, USER_STYLE_FILE
|
||||
loading = sublime.load_resource('Packages/MarkdownLivePreview/loading.txt')
|
||||
error404 = sublime.load_resource('Packages/MarkdownLivePreview/404.txt')
|
||||
|
||||
DEFAULT_STYLE = sublime.load_resource('Packages/MarkdownLivePreview/default.css')
|
||||
USER_STYLE_FILE = os.path.join(sublime.packages_path(), 'User', "MarkdownLivePreview.css")
|
||||
|
||||
MATCH_YAML_HEADER = re.compile(r'^([\-\+])\1{2}\n(?P<content>.+)\n\1{3}\n', re.DOTALL)
|
||||
|
||||
def strip_html_comments(html):
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
for element in soup.find_all(text=lambda text: isinstance(text, html_comment)):
|
||||
element.extract()
|
||||
return str(soup)
|
||||
|
||||
def manage_header(md, action):
|
||||
matchobj = MATCH_YAML_HEADER.match(md)
|
||||
if not matchobj:
|
||||
return md
|
||||
if action == 'remove':
|
||||
return md[len(matchobj.group(0)):]
|
||||
elif action == 'wrap_in_pre':
|
||||
return '<pre><code>' + matchobj.group('content') + '</code></pre>' \
|
||||
+ md[len(matchobj.group(0)):]
|
||||
|
||||
raise ValueError('Got an unknown action: "{}"'.format(action))
|
||||
|
||||
def get_preview_name(md_view):
|
||||
file_name = md_view.file_name()
|
||||
name = md_view.name() \
|
||||
or os.path.basename(file_name) if file_name else None \
|
||||
or 'Untitled'
|
||||
return name + ' - Preview'
|
||||
|
||||
def replace_img_src_base64(html, basepath):
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
load_from_internet_starters = get_settings().get('load_from_internet_when_starts')
|
||||
for img in soup.find_all('img'):
|
||||
if img['src'].startswith('data:image/'):
|
||||
continue
|
||||
elif img['src'].startswith(tuple(load_from_internet_starters)):
|
||||
image = ImageManager.get(img['src']) or loading
|
||||
else: # this is a local image
|
||||
image = to_base64(os.path.join(basepath, img['src']))
|
||||
|
||||
img['src'] = image
|
||||
|
||||
return str(soup)
|
||||
|
||||
def is_markdown_view(view):
|
||||
return 'markdown' in view.scope_name(0)
|
||||
|
||||
def to_base64(path=None, content=None):
|
||||
if path is None and content is None:
|
||||
return error404
|
||||
elif content is None and path is not None:
|
||||
try:
|
||||
with open(path, 'rb') as fp:
|
||||
content = fp.read()
|
||||
except (FileNotFoundError, OSError):
|
||||
return error404
|
||||
|
||||
return 'data:image/png;base64,' + ''.join([chr(el) for el in list(base64.standard_b64encode(content))])
|
||||
|
||||
def md(*t, **kwargs):
|
||||
sublime.message_dialog(kwargs.get('sep', '\n').join([str(el) for el in t]))
|
||||
|
||||
def sm(*t, **kwargs):
|
||||
sublime.status_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
||||
|
||||
def em(*t, **kwargs):
|
||||
sublime.error_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
||||
|
||||
def mini(val, min):
|
||||
if val < min:
|
||||
return min
|
||||
return val
|
||||
|
||||
def get_content_till(string, char_to_look_for, start=0):
|
||||
i = start
|
||||
while i < len(string):
|
||||
if string[i] == char_to_look_for:
|
||||
return string[start:i], i
|
||||
i += 1
|
||||
|
||||
def get_view_content(view):
|
||||
return view.substr(sublime.Region(0, view.size()))
|
||||
|
||||
def get_view_from_id(window, id):
|
||||
if not isinstance(id, int):
|
||||
return
|
||||
for view in window.views():
|
||||
if view.id() == id:
|
||||
return view
|
||||
|
||||
def get_settings():
|
||||
return sublime.load_settings('MarkdownLivePreview.sublime-settings')
|
||||
|
||||
|
||||
def _pre_with_spaces(code):
|
||||
for tag in code.find_all(text=True):
|
||||
tag.replace_with(BeautifulSoup(str(tag).replace('\t', ' ' * 4).replace(' ', '<i class="space">.</i>').replace('\n', '<br />'), 'html.parser'))
|
||||
return code
|
||||
|
||||
def pre_with_br(html):
|
||||
"""Because the phantoms of sublime text does not support <pre> blocks
|
||||
this function replaces every \n with a <br> in a <pre>"""
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
for pre in soup.find_all('pre'):
|
||||
code = pre.find('code')
|
||||
code.replace_with(_pre_with_spaces(code))
|
||||
return str(soup)
|
||||
|
||||
|
||||
def get_style(color_scheme):
|
||||
css = DEFAULT_STYLE
|
||||
if os.path.exists(USER_STYLE_FILE):
|
||||
with open(USER_STYLE_FILE) as fp:
|
||||
css += '\n' + fp.read() + '\n'
|
||||
if color_scheme and color_scheme.endswith('.tmTheme'):
|
||||
css += pygments_from_theme(get_resource(color_scheme))
|
||||
return ''.join([line.strip() + ' ' for line in css.splitlines()])
|
||||
|
||||
def get_resource(resource):
|
||||
if os.path.exists(os.path.join(sublime.packages_path(), '..', resource)):
|
||||
with open(os.path.join(sublime.packages_path(), '..', resource), encoding='utf-8') as fp:
|
||||
return fp.read()
|
||||
else:
|
||||
return sublime.load_resource(resource)
|
||||
@ -1,98 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import os.path
|
||||
import tempfile
|
||||
import sublime
|
||||
from threading import Thread
|
||||
import urllib.request, urllib.error
|
||||
from .functions import *
|
||||
|
||||
CACHE_FILE = os.path.join(tempfile.gettempdir(),
|
||||
'MarkdownLivePreviewCache.txt')
|
||||
TIMEOUT = 20 # seconds
|
||||
|
||||
SEPARATOR = '---%cache%--'
|
||||
|
||||
def get_base64_saver(loading, url):
|
||||
def callback(content):
|
||||
if isinstance(content, urllib.error.HTTPError):
|
||||
if content.getcode() == 404:
|
||||
loading[url] = 404
|
||||
return
|
||||
elif isinstance(content, urllib.error.URLError):
|
||||
if (content.reason.errno == 11001 and
|
||||
content.reason.strerror == 'getaddrinfo failed'):
|
||||
loading[url] = 404
|
||||
return
|
||||
return sublime.error_message('An unexpected error has occured: ' +
|
||||
str(content))
|
||||
loading[url] = to_base64(content=content)
|
||||
|
||||
return callback
|
||||
|
||||
def get_cache_for(imageurl):
|
||||
if not os.path.exists(CACHE_FILE):
|
||||
return
|
||||
with open(CACHE_FILE) as fp:
|
||||
for line in fp.read().splitlines():
|
||||
url, base64 = line.split(SEPARATOR, 1)
|
||||
if url == imageurl:
|
||||
return base64
|
||||
|
||||
def cache(imageurl, base64):
|
||||
with open(CACHE_FILE, 'a') as fp:
|
||||
fp.write(imageurl + SEPARATOR + base64 + '\n')
|
||||
|
||||
class ImageLoader(Thread):
|
||||
|
||||
def __init__(self, url, callback):
|
||||
Thread.__init__(self)
|
||||
self.url = url
|
||||
self.callback = callback
|
||||
|
||||
def run(self):
|
||||
try:
|
||||
page = urllib.request.urlopen(self.url, None, TIMEOUT)
|
||||
except Exception as e:
|
||||
self.callback(e)
|
||||
else:
|
||||
self.callback(page.read())
|
||||
|
||||
|
||||
class ImageManager(object):
|
||||
|
||||
"""
|
||||
Usage:
|
||||
|
||||
>>> image = ImageManager.get('http://domain.com/image.png')
|
||||
>>> image = ImageManager.get('http://domain.com/image.png')
|
||||
# still loading (this is a comment, no an outputed text), it doesn't
|
||||
# run an other request
|
||||
>>> image = ImageManager.get('http://domain.com/image.png')
|
||||
'data:image/png;base64,....'
|
||||
"""
|
||||
loading = {}
|
||||
|
||||
@staticmethod
|
||||
def get(imageurl, user_callback=None):
|
||||
|
||||
cached = get_cache_for(imageurl)
|
||||
if cached:
|
||||
return cached
|
||||
elif imageurl in ImageManager.loading.keys():
|
||||
# return None (the file is still loading, already made a request)
|
||||
# return string the base64 of the url (which is going to be cached)
|
||||
temp_cached = ImageManager.loading[imageurl]
|
||||
if temp_cached == 404:
|
||||
return to_base64('404.png')
|
||||
if temp_cached:
|
||||
cache(imageurl, temp_cached)
|
||||
del ImageManager.loading[imageurl]
|
||||
return temp_cached
|
||||
else:
|
||||
# load from internet
|
||||
ImageManager.loading[imageurl] = None
|
||||
callback = get_base64_saver(ImageManager.loading, imageurl)
|
||||
loader = ImageLoader(imageurl, callback)
|
||||
loader.start()
|
||||
sublime.set_timeout_async(lambda: loader.join(), TIMEOUT * 1000)
|
||||
@ -1 +0,0 @@
|
||||
There is images here, allthough they aren't of any use for the plugin. They're just the image that I used to generate the base64 (404.txt and loading.txt)
|
||||
348
lib/markdown2.py
@ -1,13 +1,8 @@
|
||||
# CSW: ignore
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
#!/usr/bin/env python
|
||||
# Copyright (c) 2012 Trent Mick.
|
||||
# Copyright (c) 2007-2008 ActiveState Corp.
|
||||
# License: MIT (http://www.opensource.org/licenses/mit-license.php)
|
||||
|
||||
from __future__ import generators
|
||||
|
||||
r"""A fast and complete Python implementation of Markdown.
|
||||
|
||||
[from http://daringfireball.net/projects/markdown/]
|
||||
@ -55,6 +50,8 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details):
|
||||
implemented in other Markdown processors (tho not in Markdown.pl v1.0.1).
|
||||
* header-ids: Adds "id" attributes to headers. The id value is a slug of
|
||||
the header text.
|
||||
* highlightjs-lang: Allows specifying the language which used for syntax
|
||||
highlighting when using fenced-code-blocks and highlightjs.
|
||||
* html-classes: Takes a dict mapping html tag names (lowercase) to a
|
||||
string to use for a "class" tag attribute. Currently only supports "img",
|
||||
"table", "pre" and "code" tags. Add an issue if you require this for other
|
||||
@ -78,6 +75,7 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details):
|
||||
and ellipses.
|
||||
* spoiler: A special kind of blockquote commonly hidden behind a
|
||||
click on SO. Syntax per <http://meta.stackexchange.com/a/72878>.
|
||||
* strike: text inside of double tilde is ~~strikethrough~~
|
||||
* tag-friendly: Requires atx style headers to have a space between the # and
|
||||
the header text. Useful for applications that require twitter style tags to
|
||||
pass through the parser.
|
||||
@ -98,20 +96,18 @@ see <https://github.com/trentm/python-markdown2/wiki/Extras> for details):
|
||||
# not yet sure if there implications with this. Compare 'pydoc sre'
|
||||
# and 'perldoc perlre'.
|
||||
|
||||
__version_info__ = (2, 3, 2)
|
||||
__version_info__ = (2, 3, 9)
|
||||
__version__ = '.'.join(map(str, __version_info__))
|
||||
__author__ = "Trent Mick"
|
||||
|
||||
import sys
|
||||
import re
|
||||
import logging
|
||||
try:
|
||||
from hashlib import md5
|
||||
except ImportError:
|
||||
from md5 import md5
|
||||
from hashlib import sha256
|
||||
import optparse
|
||||
from random import random, randint
|
||||
import codecs
|
||||
from collections import defaultdict
|
||||
try:
|
||||
from urllib import quote_plus
|
||||
except ImportError:
|
||||
@ -120,11 +116,6 @@ except ImportError:
|
||||
|
||||
# ---- Python version compat
|
||||
|
||||
if sys.version_info[:2] < (2, 4):
|
||||
def reversed(sequence):
|
||||
for i in sequence[::-1]:
|
||||
yield i
|
||||
|
||||
# Use `bytes` for byte strings and `unicode` for unicode strings (str in Py3).
|
||||
if sys.version_info[0] <= 2:
|
||||
py3 = False
|
||||
@ -147,13 +138,19 @@ DEFAULT_TAB_WIDTH = 4
|
||||
|
||||
|
||||
SECRET_SALT = bytes(randint(0, 1000000))
|
||||
# MD5 function was previously used for this; the "md5" prefix was kept for
|
||||
# backwards compatibility.
|
||||
def _hash_text(s):
|
||||
return 'md5-' + md5(SECRET_SALT + s.encode("utf-8")).hexdigest()
|
||||
return 'md5-' + sha256(SECRET_SALT + s.encode("utf-8")).hexdigest()[32:]
|
||||
|
||||
# Table of hash values for escaped characters:
|
||||
g_escape_table = dict([(ch, _hash_text(ch))
|
||||
for ch in '\\`*_{}[]()>#+-.!'])
|
||||
|
||||
# Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
|
||||
# http://bumppo.net/projects/amputator/
|
||||
_AMPERSAND_RE = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
|
||||
|
||||
|
||||
# ---- exceptions
|
||||
class MarkdownError(Exception):
|
||||
@ -165,6 +162,7 @@ class MarkdownError(Exception):
|
||||
def markdown_path(path, encoding="utf-8",
|
||||
html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
|
||||
safe_mode=None, extras=None, link_patterns=None,
|
||||
footnote_title=None, footnote_return_symbol=None,
|
||||
use_file_vars=False):
|
||||
fp = codecs.open(path, 'r', encoding)
|
||||
text = fp.read()
|
||||
@ -172,16 +170,21 @@ def markdown_path(path, encoding="utf-8",
|
||||
return Markdown(html4tags=html4tags, tab_width=tab_width,
|
||||
safe_mode=safe_mode, extras=extras,
|
||||
link_patterns=link_patterns,
|
||||
footnote_title=footnote_title,
|
||||
footnote_return_symbol=footnote_return_symbol,
|
||||
use_file_vars=use_file_vars).convert(text)
|
||||
|
||||
|
||||
def markdown(text, html4tags=False, tab_width=DEFAULT_TAB_WIDTH,
|
||||
safe_mode=None, extras=None, link_patterns=None,
|
||||
use_file_vars=False):
|
||||
footnote_title=None, footnote_return_symbol=None,
|
||||
use_file_vars=False, cli=False):
|
||||
return Markdown(html4tags=html4tags, tab_width=tab_width,
|
||||
safe_mode=safe_mode, extras=extras,
|
||||
link_patterns=link_patterns,
|
||||
use_file_vars=use_file_vars).convert(text)
|
||||
footnote_title=footnote_title,
|
||||
footnote_return_symbol=footnote_return_symbol,
|
||||
use_file_vars=use_file_vars, cli=cli).convert(text)
|
||||
|
||||
|
||||
class Markdown(object):
|
||||
@ -206,7 +209,9 @@ class Markdown(object):
|
||||
_ws_only_line_re = re.compile(r"^[ \t]+$", re.M)
|
||||
|
||||
def __init__(self, html4tags=False, tab_width=4, safe_mode=None,
|
||||
extras=None, link_patterns=None, use_file_vars=False):
|
||||
extras=None, link_patterns=None,
|
||||
footnote_title=None, footnote_return_symbol=None,
|
||||
use_file_vars=False, cli=False):
|
||||
if html4tags:
|
||||
self.empty_element_suffix = ">"
|
||||
else:
|
||||
@ -231,13 +236,23 @@ class Markdown(object):
|
||||
extras = dict([(e, None) for e in extras])
|
||||
self.extras.update(extras)
|
||||
assert isinstance(self.extras, dict)
|
||||
if "toc" in self.extras and "header-ids" not in self.extras:
|
||||
self.extras["header-ids"] = None # "toc" implies "header-ids"
|
||||
|
||||
if "toc" in self.extras:
|
||||
if "header-ids" not in self.extras:
|
||||
self.extras["header-ids"] = None # "toc" implies "header-ids"
|
||||
|
||||
if self.extras["toc"] is None:
|
||||
self._toc_depth = 6
|
||||
else:
|
||||
self._toc_depth = self.extras["toc"].get("depth", 6)
|
||||
self._instance_extras = self.extras.copy()
|
||||
|
||||
self.link_patterns = link_patterns
|
||||
self.footnote_title = footnote_title
|
||||
self.footnote_return_symbol = footnote_return_symbol
|
||||
self.use_file_vars = use_file_vars
|
||||
self._outdent_re = re.compile(r'^(\t|[ ]{1,%d})' % tab_width, re.M)
|
||||
self.cli = cli
|
||||
|
||||
self._escape_table = g_escape_table.copy()
|
||||
if "smarty-pants" in self.extras:
|
||||
@ -255,16 +270,26 @@ class Markdown(object):
|
||||
self.footnotes = {}
|
||||
self.footnote_ids = []
|
||||
if "header-ids" in self.extras:
|
||||
self._count_from_header_id = {} # no `defaultdict` in Python 2.4
|
||||
self._count_from_header_id = defaultdict(int)
|
||||
if "metadata" in self.extras:
|
||||
self.metadata = {}
|
||||
|
||||
# Per <https://developer.mozilla.org/en-US/docs/HTML/Element/a> "rel"
|
||||
# should only be used in <a> tags with an "href" attribute.
|
||||
_a_nofollow = re.compile(r"<(a)([^>]*href=)", re.IGNORECASE)
|
||||
_a_nofollow = re.compile(r"""
|
||||
<(a)
|
||||
(
|
||||
[^>]*
|
||||
href= # href is required
|
||||
['"]? # HTML5 attribute values do not have to be quoted
|
||||
[^#'"] # We don't want to match href values that start with # (like footnotes)
|
||||
)
|
||||
""",
|
||||
re.IGNORECASE | re.VERBOSE
|
||||
)
|
||||
|
||||
# Opens the linked document in a new window or tab
|
||||
# should only used in <a> tags with an "target" attribute.
|
||||
# should only used in <a> tags with an "href" attribute.
|
||||
# same with _a_nofollow
|
||||
_a_blank = _a_nofollow
|
||||
|
||||
@ -366,11 +391,21 @@ class Markdown(object):
|
||||
if "target-blank-links" in self.extras:
|
||||
text = self._a_blank.sub(r'<\1 target="_blank"\2', text)
|
||||
|
||||
if "toc" in self.extras and self._toc:
|
||||
self._toc_html = calculate_toc_html(self._toc)
|
||||
|
||||
# Prepend toc html to output
|
||||
if self.cli:
|
||||
text = '{}\n{}'.format(self._toc_html, text)
|
||||
|
||||
text += "\n"
|
||||
|
||||
# Attach attrs to output
|
||||
rv = UnicodeWithAttrs(text)
|
||||
if "toc" in self.extras:
|
||||
rv._toc = self._toc
|
||||
|
||||
if "toc" in self.extras and self._toc:
|
||||
rv.toc_html = self._toc_html
|
||||
|
||||
if "metadata" in self.extras:
|
||||
rv.metadata = self.metadata
|
||||
return rv
|
||||
@ -402,30 +437,33 @@ class Markdown(object):
|
||||
#
|
||||
# # header
|
||||
_meta_data_pattern = re.compile(r'^(?:---[\ \t]*\n)?(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)|([\S\w]+\s*:(?! >)[ \t]*.*\n?)(?:---[\ \t]*\n)?', re.MULTILINE)
|
||||
_key_val_pat = re.compile("[\S\w]+\s*:(?! >)[ \t]*.*\n?", re.MULTILINE)
|
||||
_key_val_pat = re.compile(r"[\S\w]+\s*:(?! >)[ \t]*.*\n?", re.MULTILINE)
|
||||
# this allows key: >
|
||||
# value
|
||||
# conutiues over multiple lines
|
||||
_key_val_block_pat = re.compile(
|
||||
"(.*:\s+>\n\s+[\S\s]+?)(?=\n\w+\s*:\s*\w+\n|\Z)", re.MULTILINE)
|
||||
_meta_data_fence_pattern = re.compile(r'^---[\ \t]*\n', re.MULTILINE)
|
||||
_meta_data_newline = re.compile("^\n", re.MULTILINE)
|
||||
|
||||
def _extract_metadata(self, text):
|
||||
match = re.findall(self._meta_data_pattern, text)
|
||||
|
||||
if not match:
|
||||
return text
|
||||
|
||||
last_item = list(filter(None, match[-1]))[0]
|
||||
end_of_metadata = text.index(last_item)+len(last_item)
|
||||
if text.startswith("---"):
|
||||
# add 8 charachters for opening and closing
|
||||
# and since indexing starts at 0 we add a step
|
||||
tail = text[end_of_metadata+4:]
|
||||
fence_splits = re.split(self._meta_data_fence_pattern, text, maxsplit=2)
|
||||
metadata_content = fence_splits[1]
|
||||
match = re.findall(self._meta_data_pattern, metadata_content)
|
||||
if not match:
|
||||
return text
|
||||
tail = fence_splits[2]
|
||||
else:
|
||||
tail = text[end_of_metadata:]
|
||||
metadata_split = re.split(self._meta_data_newline, text, maxsplit=1)
|
||||
metadata_content = metadata_split[0]
|
||||
match = re.findall(self._meta_data_pattern, metadata_content)
|
||||
if not match:
|
||||
return text
|
||||
tail = metadata_split[1]
|
||||
|
||||
kv = re.findall(self._key_val_pat, text)
|
||||
kvm = re.findall(self._key_val_block_pat, text)
|
||||
kv = re.findall(self._key_val_pat, metadata_content)
|
||||
kvm = re.findall(self._key_val_block_pat, metadata_content)
|
||||
kvm = [item.replace(": >\n", ":", 1) for item in kvm]
|
||||
|
||||
for item in kv + kvm:
|
||||
@ -957,12 +995,14 @@ class Markdown(object):
|
||||
|
||||
def _table_sub(self, match):
|
||||
trim_space_re = '^[ \t\n]+|[ \t\n]+$'
|
||||
trim_bar_re = '^\||\|$'
|
||||
trim_bar_re = r'^\||\|$'
|
||||
split_bar_re = r'^\||(?<!\\)\|'
|
||||
escape_bar_re = r'\\\|'
|
||||
|
||||
head, underline, body = match.groups()
|
||||
|
||||
# Determine aligns for columns.
|
||||
cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", underline)).split('|')]
|
||||
cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", underline)))]
|
||||
align_from_col_idx = {}
|
||||
for col_idx, col in enumerate(cols):
|
||||
if col[0] == ':' and col[-1] == ':':
|
||||
@ -974,7 +1014,7 @@ class Markdown(object):
|
||||
|
||||
# thead
|
||||
hlines = ['<table%s>' % self._html_class_str_from_tag('table'), '<thead>', '<tr>']
|
||||
cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", head)).split('|')]
|
||||
cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", head)))]
|
||||
for col_idx, col in enumerate(cols):
|
||||
hlines.append(' <th%s>%s</th>' % (
|
||||
align_from_col_idx.get(col_idx, ''),
|
||||
@ -987,7 +1027,7 @@ class Markdown(object):
|
||||
hlines.append('<tbody>')
|
||||
for line in body.strip('\n').split('\n'):
|
||||
hlines.append('<tr>')
|
||||
cols = [cell.strip() for cell in re.sub(trim_bar_re, "", re.sub(trim_space_re, "", line)).split('|')]
|
||||
cols = [re.sub(escape_bar_re, '|', cell.strip()) for cell in re.split(split_bar_re, re.sub(trim_bar_re, "", re.sub(trim_space_re, "", line)))]
|
||||
for col_idx, col in enumerate(cols):
|
||||
hlines.append(' <td%s>%s</td>' % (
|
||||
align_from_col_idx.get(col_idx, ''),
|
||||
@ -1071,6 +1111,9 @@ class Markdown(object):
|
||||
text = self._escape_special_chars(text)
|
||||
|
||||
# Process anchor and image tags.
|
||||
if "link-patterns" in self.extras:
|
||||
text = self._do_link_patterns(text)
|
||||
|
||||
text = self._do_links(text)
|
||||
|
||||
# Make links out of things like `<http://example.com/>`
|
||||
@ -1078,9 +1121,6 @@ class Markdown(object):
|
||||
# delimiters in inline links like [this](<url>).
|
||||
text = self._do_auto_links(text)
|
||||
|
||||
if "link-patterns" in self.extras:
|
||||
text = self._do_link_patterns(text)
|
||||
|
||||
text = self._encode_amps_and_angles(text)
|
||||
|
||||
if "strike" in self.extras:
|
||||
@ -1160,7 +1200,7 @@ class Markdown(object):
|
||||
self.html_spans[key] = sanitized
|
||||
tokens.append(key)
|
||||
else:
|
||||
tokens.append(token)
|
||||
tokens.append(self._encode_incomplete_tags(token))
|
||||
is_html_markup = not is_html_markup
|
||||
return ''.join(tokens)
|
||||
|
||||
@ -1353,7 +1393,7 @@ class Markdown(object):
|
||||
if is_img:
|
||||
img_class_str = self._html_class_str_from_tag("img")
|
||||
result = '<img src="%s" alt="%s"%s%s%s' \
|
||||
% (_urlencode(url, safe_mode=self.safe_mode),
|
||||
% (_html_escape_url(url, safe_mode=self.safe_mode),
|
||||
_xml_escape_attr(link_text),
|
||||
title_str,
|
||||
img_class_str,
|
||||
@ -1363,11 +1403,12 @@ class Markdown(object):
|
||||
curr_pos = start_idx + len(result)
|
||||
text = text[:start_idx] + result + text[url_end_idx:]
|
||||
elif start_idx >= anchor_allowed_pos:
|
||||
if self.safe_mode and not self._safe_protocols.match(url):
|
||||
safe_link = self._safe_protocols.match(url) or url.startswith('#')
|
||||
if self.safe_mode and not safe_link:
|
||||
result_head = '<a href="#"%s>' % (title_str)
|
||||
else:
|
||||
result_head = '<a href="%s"%s>' % (_urlencode(url, safe_mode=self.safe_mode), title_str)
|
||||
result = '%s%s</a>' % (result_head, _xml_escape_attr(link_text))
|
||||
result_head = '<a href="%s"%s>' % (_html_escape_url(url, safe_mode=self.safe_mode), title_str)
|
||||
result = '%s%s</a>' % (result_head, link_text)
|
||||
if "smarty-pants" in self.extras:
|
||||
result = result.replace('"', self._escape_table['"'])
|
||||
# <img> allowed from curr_pos on, <a> from
|
||||
@ -1408,7 +1449,7 @@ class Markdown(object):
|
||||
if is_img:
|
||||
img_class_str = self._html_class_str_from_tag("img")
|
||||
result = '<img src="%s" alt="%s"%s%s%s' \
|
||||
% (_urlencode(url, safe_mode=self.safe_mode),
|
||||
% (_html_escape_url(url, safe_mode=self.safe_mode),
|
||||
_xml_escape_attr(link_text),
|
||||
title_str,
|
||||
img_class_str,
|
||||
@ -1421,7 +1462,7 @@ class Markdown(object):
|
||||
if self.safe_mode and not self._safe_protocols.match(url):
|
||||
result_head = '<a href="#"%s>' % (title_str)
|
||||
else:
|
||||
result_head = '<a href="%s"%s>' % (_urlencode(url, safe_mode=self.safe_mode), title_str)
|
||||
result_head = '<a href="%s"%s>' % (_html_escape_url(url, safe_mode=self.safe_mode), title_str)
|
||||
result = '%s%s</a>' % (result_head, link_text)
|
||||
if "smarty-pants" in self.extras:
|
||||
result = result.replace('"', self._escape_table['"'])
|
||||
@ -1461,15 +1502,17 @@ class Markdown(object):
|
||||
header_id = _slugify(text)
|
||||
if prefix and isinstance(prefix, base_string_type):
|
||||
header_id = prefix + '-' + header_id
|
||||
if header_id in self._count_from_header_id:
|
||||
self._count_from_header_id[header_id] += 1
|
||||
|
||||
self._count_from_header_id[header_id] += 1
|
||||
if 0 == len(header_id) or self._count_from_header_id[header_id] > 1:
|
||||
header_id += '-%s' % self._count_from_header_id[header_id]
|
||||
else:
|
||||
self._count_from_header_id[header_id] = 1
|
||||
|
||||
return header_id
|
||||
|
||||
_toc = None
|
||||
def _toc_add_entry(self, level, id, name):
|
||||
if level > self._toc_depth:
|
||||
return
|
||||
if self._toc is None:
|
||||
self._toc = []
|
||||
self._toc.append((level, id, self._unescape_special_chars(name)))
|
||||
@ -1491,7 +1534,9 @@ class Markdown(object):
|
||||
_h_re_tag_friendly = re.compile(_h_re_base % '+', re.X | re.M)
|
||||
|
||||
def _h_sub(self, match):
|
||||
if match.group(1) is not None:
|
||||
if match.group(1) is not None and match.group(3) == "-":
|
||||
return match.group(1)
|
||||
elif match.group(1) is not None:
|
||||
# Setext header
|
||||
n = {"=": 1, "-": 2}[match.group(3)[0]]
|
||||
header_group = match.group(2)
|
||||
@ -1610,16 +1655,16 @@ class Markdown(object):
|
||||
re.M | re.X | re.S)
|
||||
|
||||
_task_list_item_re = re.compile(r'''
|
||||
(\[[\ x]\])[ \t]+ # tasklist marker = \1
|
||||
(\[[\ xX]\])[ \t]+ # tasklist marker = \1
|
||||
(.*) # list item text = \2
|
||||
''', re.M | re.X | re.S)
|
||||
|
||||
_task_list_warpper_str = r'<p><input type="checkbox" class="task-list-item-checkbox" %sdisabled>%s</p>'
|
||||
_task_list_warpper_str = r'<input type="checkbox" class="task-list-item-checkbox" %sdisabled> %s'
|
||||
|
||||
def _task_list_item_sub(self, match):
|
||||
marker = match.group(1)
|
||||
item_text = match.group(2)
|
||||
if marker == '[x]':
|
||||
if marker in ['[x]','[X]']:
|
||||
return self._task_list_warpper_str % ('checked ', item_text)
|
||||
elif marker == '[ ]':
|
||||
return self._task_list_warpper_str % ('', item_text)
|
||||
@ -1728,7 +1773,8 @@ class Markdown(object):
|
||||
codeblock = rest.lstrip("\n") # Remove lexer declaration line.
|
||||
formatter_opts = self.extras['code-color'] or {}
|
||||
|
||||
if lexer_name:
|
||||
# Use pygments only if not using the highlightjs-lang extra
|
||||
if lexer_name and "highlightjs-lang" not in self.extras:
|
||||
def unhash_code(codeblock):
|
||||
for key, sanitized in list(self.html_spans.items()):
|
||||
codeblock = codeblock.replace(key, sanitized)
|
||||
@ -1741,7 +1787,7 @@ class Markdown(object):
|
||||
codeblock = codeblock.replace(old, new)
|
||||
return codeblock
|
||||
lexer = self._get_pygments_lexer(lexer_name)
|
||||
if lexer and self.extras.get('no-code-highlighting', True) is True:
|
||||
if lexer:
|
||||
codeblock = unhash_code( codeblock )
|
||||
colored = self._color_with_pygments(codeblock, lexer,
|
||||
**formatter_opts)
|
||||
@ -1749,7 +1795,12 @@ class Markdown(object):
|
||||
|
||||
codeblock = self._encode_code(codeblock)
|
||||
pre_class_str = self._html_class_str_from_tag("pre")
|
||||
code_class_str = self._html_class_str_from_tag("code")
|
||||
|
||||
if "highlightjs-lang" in self.extras and lexer_name:
|
||||
code_class_str = ' class="%s"' % lexer_name
|
||||
else:
|
||||
code_class_str = self._html_class_str_from_tag("code")
|
||||
|
||||
return "\n\n<pre%s><code%s>%s\n</code></pre>\n\n" % (
|
||||
pre_class_str, code_class_str, codeblock)
|
||||
|
||||
@ -1788,7 +1839,7 @@ class Markdown(object):
|
||||
|
||||
_fenced_code_block_re = re.compile(r'''
|
||||
(?:\n+|\A\n?)
|
||||
^```([\w+-]+)?[ \t]*\n # opening fence, $1 = optional lang
|
||||
^```\s*?([\w+-]+)?\s*?\n # opening fence, $1 = optional lang
|
||||
(.*?) # $2 = code block content
|
||||
^```[ \t]*\n # closing fence
|
||||
''', re.M | re.X | re.S)
|
||||
@ -1930,6 +1981,13 @@ class Markdown(object):
|
||||
text = text.replace("...", "…")
|
||||
text = text.replace(" . . . ", "…")
|
||||
text = text.replace(". . .", "…")
|
||||
|
||||
# TODO: Temporary hack to fix https://github.com/trentm/python-markdown2/issues/150
|
||||
if "footnotes" in self.extras and "footnote-ref" in text:
|
||||
# Quotes in the footnote back ref get converted to "smart" quotes
|
||||
# Change them back here to ensure they work.
|
||||
text = text.replace('class="footnote-ref”', 'class="footnote-ref"')
|
||||
|
||||
return text
|
||||
|
||||
_block_quote_base = r'''
|
||||
@ -2001,8 +2059,13 @@ class Markdown(object):
|
||||
# text (issue 33). Note the `[-1]` is a quick way to
|
||||
# consider numeric bullets (e.g. "1." and "2.") to be
|
||||
# equal.
|
||||
if (li and len(li.group(2)) <= 3 and li.group("next_marker")
|
||||
and li.group("marker")[-1] == li.group("next_marker")[-1]):
|
||||
if (li and len(li.group(2)) <= 3
|
||||
and (
|
||||
(li.group("next_marker") and li.group("marker")[-1] == li.group("next_marker")[-1])
|
||||
or
|
||||
li.group("next_marker") is None
|
||||
)
|
||||
):
|
||||
start = li.start()
|
||||
cuddled_list = self._do_lists(graf[start:]).rstrip("\n")
|
||||
assert cuddled_list.startswith("<ul>") or cuddled_list.startswith("<ol>")
|
||||
@ -2010,7 +2073,7 @@ class Markdown(object):
|
||||
|
||||
# Wrap <p> tags.
|
||||
graf = self._run_span_gamut(graf)
|
||||
grafs.append("<p>" + graf.lstrip(" \t") + "</p>")
|
||||
grafs.append("<p%s>" % self._html_class_str_from_tag('p') + graf.lstrip(" \t") + "</p>")
|
||||
|
||||
if cuddled_list:
|
||||
grafs.append(cuddled_list)
|
||||
@ -2024,15 +2087,31 @@ class Markdown(object):
|
||||
'<hr' + self.empty_element_suffix,
|
||||
'<ol>',
|
||||
]
|
||||
|
||||
if not self.footnote_title:
|
||||
self.footnote_title = "Jump back to footnote %d in the text."
|
||||
if not self.footnote_return_symbol:
|
||||
self.footnote_return_symbol = "↩"
|
||||
|
||||
for i, id in enumerate(self.footnote_ids):
|
||||
if i != 0:
|
||||
footer.append('')
|
||||
footer.append('<li id="fn-%s">' % id)
|
||||
footer.append(self._run_block_gamut(self.footnotes[id]))
|
||||
backlink = ('<a href="#fnref-%s" '
|
||||
'class="footnoteBackLink" '
|
||||
'title="Jump back to footnote %d in the text.">'
|
||||
'↩</a>' % (id, i+1))
|
||||
try:
|
||||
backlink = ('<a href="#fnref-%s" ' +
|
||||
'class="footnoteBackLink" ' +
|
||||
'title="' + self.footnote_title + '">' +
|
||||
self.footnote_return_symbol +
|
||||
'</a>') % (id, i+1)
|
||||
except TypeError:
|
||||
log.debug("Footnote error. `footnote_title` "
|
||||
"must include parameter. Using defaults.")
|
||||
backlink = ('<a href="#fnref-%s" '
|
||||
'class="footnoteBackLink" '
|
||||
'title="Jump back to footnote %d in the text.">'
|
||||
'↩</a>' % (id, i+1))
|
||||
|
||||
if footer[-1].endswith("</p>"):
|
||||
footer[-1] = footer[-1][:-len("</p>")] \
|
||||
+ ' ' + backlink + "</p>"
|
||||
@ -2045,16 +2124,13 @@ class Markdown(object):
|
||||
else:
|
||||
return text
|
||||
|
||||
# Ampersand-encoding based entirely on Nat Irons's Amputator MT plugin:
|
||||
# http://bumppo.net/projects/amputator/
|
||||
_ampersand_re = re.compile(r'&(?!#?[xX]?(?:[0-9a-fA-F]+|\w+);)')
|
||||
_naked_lt_re = re.compile(r'<(?![a-z/?\$!])', re.I)
|
||||
_naked_gt_re = re.compile(r'''(?<![a-z0-9?!/'"-])>''', re.I)
|
||||
|
||||
def _encode_amps_and_angles(self, text):
|
||||
# Smart processing for ampersands and angle brackets that need
|
||||
# to be encoded.
|
||||
text = self._ampersand_re.sub('&', text)
|
||||
text = _AMPERSAND_RE.sub('&', text)
|
||||
|
||||
# Encode naked <'s
|
||||
text = self._naked_lt_re.sub('<', text)
|
||||
@ -2065,6 +2141,14 @@ class Markdown(object):
|
||||
text = self._naked_gt_re.sub('>', text)
|
||||
return text
|
||||
|
||||
_incomplete_tags_re = re.compile("<(/?\w+[\s/]+?)")
|
||||
|
||||
def _encode_incomplete_tags(self, text):
|
||||
if self.safe_mode not in ("replace", "escape"):
|
||||
return text
|
||||
|
||||
return self._incomplete_tags_re.sub("<\\1", text)
|
||||
|
||||
def _encode_backslash_escapes(self, text):
|
||||
for ch, escape in list(self._escape_table.items()):
|
||||
text = text.replace("\\"+ch, escape)
|
||||
@ -2115,13 +2199,6 @@ class Markdown(object):
|
||||
return addr
|
||||
|
||||
def _do_link_patterns(self, text):
|
||||
"""Caveat emptor: there isn't much guarding against link
|
||||
patterns being formed inside other standard Markdown links, e.g.
|
||||
inside a [link def][like this].
|
||||
|
||||
Dev Notes: *Could* consider prefixing regexes with a negative
|
||||
lookbehind assertion to attempt to guard against this.
|
||||
"""
|
||||
link_from_hash = {}
|
||||
for regex, repl in self.link_patterns:
|
||||
replacements = []
|
||||
@ -2132,6 +2209,20 @@ class Markdown(object):
|
||||
href = match.expand(repl)
|
||||
replacements.append((match.span(), href))
|
||||
for (start, end), href in reversed(replacements):
|
||||
|
||||
# Do not match against links inside brackets.
|
||||
if text[start - 1:start] == '[' and text[end:end + 1] == ']':
|
||||
continue
|
||||
|
||||
# Do not match against links in the standard markdown syntax.
|
||||
if text[start - 2:start] == '](' or text[end:end + 2] == '")':
|
||||
continue
|
||||
|
||||
# Do not match against links which are escaped.
|
||||
if text[start - 3:start] == '"""' and text[end:end + 3] == '"""':
|
||||
text = text[:start - 3] + text[start:end] + text[end + 3:]
|
||||
continue
|
||||
|
||||
escaped_href = (
|
||||
href.replace('"', '"') # b/c of attr quote
|
||||
# To avoid markdown <em> and <strong>:
|
||||
@ -2173,46 +2264,48 @@ class MarkdownWithExtras(Markdown):
|
||||
|
||||
# ---- internal support functions
|
||||
|
||||
|
||||
def calculate_toc_html(toc):
|
||||
"""Return the HTML for the current TOC.
|
||||
|
||||
This expects the `_toc` attribute to have been set on this instance.
|
||||
"""
|
||||
if toc is None:
|
||||
return None
|
||||
|
||||
def indent():
|
||||
return ' ' * (len(h_stack) - 1)
|
||||
lines = []
|
||||
h_stack = [0] # stack of header-level numbers
|
||||
for level, id, name in toc:
|
||||
if level > h_stack[-1]:
|
||||
lines.append("%s<ul>" % indent())
|
||||
h_stack.append(level)
|
||||
elif level == h_stack[-1]:
|
||||
lines[-1] += "</li>"
|
||||
else:
|
||||
while level < h_stack[-1]:
|
||||
h_stack.pop()
|
||||
if not lines[-1].endswith("</li>"):
|
||||
lines[-1] += "</li>"
|
||||
lines.append("%s</ul></li>" % indent())
|
||||
lines.append('%s<li><a href="#%s">%s</a>' % (
|
||||
indent(), id, name))
|
||||
while len(h_stack) > 1:
|
||||
h_stack.pop()
|
||||
if not lines[-1].endswith("</li>"):
|
||||
lines[-1] += "</li>"
|
||||
lines.append("%s</ul>" % indent())
|
||||
return '\n'.join(lines) + '\n'
|
||||
|
||||
|
||||
class UnicodeWithAttrs(unicode):
|
||||
"""A subclass of unicode used for the return value of conversion to
|
||||
possibly attach some attributes. E.g. the "toc_html" attribute when
|
||||
the "toc" extra is used.
|
||||
"""
|
||||
metadata = None
|
||||
_toc = None
|
||||
def toc_html(self):
|
||||
"""Return the HTML for the current TOC.
|
||||
|
||||
This expects the `_toc` attribute to have been set on this instance.
|
||||
"""
|
||||
if self._toc is None:
|
||||
return None
|
||||
|
||||
def indent():
|
||||
return ' ' * (len(h_stack) - 1)
|
||||
lines = []
|
||||
h_stack = [0] # stack of header-level numbers
|
||||
for level, id, name in self._toc:
|
||||
if level > h_stack[-1]:
|
||||
lines.append("%s<ul>" % indent())
|
||||
h_stack.append(level)
|
||||
elif level == h_stack[-1]:
|
||||
lines[-1] += "</li>"
|
||||
else:
|
||||
while level < h_stack[-1]:
|
||||
h_stack.pop()
|
||||
if not lines[-1].endswith("</li>"):
|
||||
lines[-1] += "</li>"
|
||||
lines.append("%s</ul></li>" % indent())
|
||||
lines.append('%s<li><a href="#%s">%s</a>' % (
|
||||
indent(), id, name))
|
||||
while len(h_stack) > 1:
|
||||
h_stack.pop()
|
||||
if not lines[-1].endswith("</li>"):
|
||||
lines[-1] += "</li>"
|
||||
lines.append("%s</ul>" % indent())
|
||||
return '\n'.join(lines) + '\n'
|
||||
toc_html = property(toc_html)
|
||||
toc_html = None
|
||||
|
||||
## {{{ http://code.activestate.com/recipes/577257/ (r1)
|
||||
_slugify_strip_re = re.compile(r'[^\w\s-]')
|
||||
@ -2433,8 +2526,9 @@ def _xml_escape_attr(attr, skip_single_quote=True):
|
||||
By default this doesn't bother with escaping `'` to `'`, presuming that
|
||||
the tag attribute is surrounded by double quotes.
|
||||
"""
|
||||
escaped = _AMPERSAND_RE.sub('&', attr)
|
||||
|
||||
escaped = (attr
|
||||
.replace('&', '&')
|
||||
.replace('"', '"')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>'))
|
||||
@ -2457,12 +2551,15 @@ def _xml_encode_email_char_at_random(ch):
|
||||
return '&#%s;' % ord(ch)
|
||||
|
||||
|
||||
def _urlencode(attr, safe_mode=False):
|
||||
"""Replace special characters in string using the %xx escape."""
|
||||
def _html_escape_url(attr, safe_mode=False):
|
||||
"""Replace special characters that are potentially malicious in url string."""
|
||||
escaped = (attr
|
||||
.replace('"', '"')
|
||||
.replace('<', '<')
|
||||
.replace('>', '>'))
|
||||
if safe_mode:
|
||||
escaped = quote_plus(attr).replace('+', ' ')
|
||||
else:
|
||||
escaped = attr.replace('"', '%22')
|
||||
escaped = escaped.replace('+', ' ')
|
||||
escaped = escaped.replace("'", "'")
|
||||
return escaped
|
||||
|
||||
|
||||
@ -2587,7 +2684,8 @@ def main(argv=None):
|
||||
html4tags=opts.html4tags,
|
||||
safe_mode=opts.safe_mode,
|
||||
extras=extras, link_patterns=link_patterns,
|
||||
use_file_vars=opts.use_file_vars)
|
||||
use_file_vars=opts.use_file_vars,
|
||||
cli=True)
|
||||
if py3:
|
||||
sys.stdout.write(html)
|
||||
else:
|
||||
|
||||
@ -1,52 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
"""
|
||||
'pre_tables' transform *html* tables into markdown tables, and put them in some <pre><code> tags
|
||||
"""
|
||||
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
def python_table(s_table):
|
||||
"""Transform BeautifulSoup table into list of list"""
|
||||
rows = []
|
||||
for row in s_table.find_all('tr'):
|
||||
# rows.append(list(map( lambda td: td.text, row.find_all(['th', 'td']) )))
|
||||
rows.append(row.find_all(['th', 'td']))
|
||||
return rows
|
||||
|
||||
def pre_table(s_table):
|
||||
rows = python_table(s_table)
|
||||
cols_width = [len(cell) for cell in rows[0]]
|
||||
for j, row in enumerate(rows):
|
||||
for i, cell in enumerate(row):
|
||||
if cols_width[i] < len(cell.text):
|
||||
cols_width[i] = len(cell.text)
|
||||
text = '<pre class="table"><code>'
|
||||
for i, row in enumerate(rows):
|
||||
if i == 1:
|
||||
for j, cell in enumerate(row):
|
||||
text += '|' + '-' * (cols_width[j] + 2)
|
||||
text += '|\n'
|
||||
|
||||
for j, cell in enumerate(row):
|
||||
text += '| '
|
||||
if cell.name == 'th':
|
||||
title = ' ' * ((cols_width[j] - len(cell.text)) // 2) \
|
||||
+ ''.join(str(node) for node in cell.contents) \
|
||||
+ ' ' * int(round((cols_width[j] - len(cell.text)) / 2 ) + 1)
|
||||
# + 1 because of the added space before the closing | of each cell
|
||||
if cols_width[j] + 1 != len(title):
|
||||
title += ' '
|
||||
text += title
|
||||
else:
|
||||
text += ''.join(str(node) for node in cell.contents) \
|
||||
+ ' ' * (cols_width[j] - len(cell.text) + 1)
|
||||
text += '|\n'
|
||||
text += '</pre></code>'
|
||||
return text
|
||||
|
||||
def pre_tables(html):
|
||||
soup = BeautifulSoup(html, 'html.parser')
|
||||
for table in soup.find_all('table'):
|
||||
table.replace_with(BeautifulSoup(pre_table(table), 'html.parser'))
|
||||
return str(soup)
|
||||
@ -1,167 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
import os
|
||||
import sys
|
||||
from xml.dom.minidom import parseString
|
||||
from collections import defaultdict
|
||||
|
||||
class Style:
|
||||
# .highlight is the wrapper class for highlighting therefore
|
||||
# all css rules are prefixed with .highlight
|
||||
PREFIX = '.codehilite'
|
||||
|
||||
# -----------------------------------------
|
||||
# Params
|
||||
# name: the name of the class
|
||||
# args: each argument is an array.
|
||||
# Each array consists of css properties
|
||||
# that is either a color or font style
|
||||
# ----------------------------------------
|
||||
|
||||
def __init__(self, name, *args):
|
||||
self.name = name # Name of the class
|
||||
self.rules = {} # The css rules
|
||||
for arr in args:
|
||||
for value in arr:
|
||||
# Only define properties if they are already not defined
|
||||
# This allows "cascading" if rules to be applied
|
||||
if value.startswith('#') and 'color' not in self.rules:
|
||||
self.rules['color'] = value
|
||||
else:
|
||||
if 'italic' in value and 'font-style' not in self.rules:
|
||||
self.rules['font-style'] = 'italic'
|
||||
if 'underline' in value and 'text-decoration' not in self.rules:
|
||||
self.rules['text-decoration'] = 'underline'
|
||||
if 'bold' in value and 'font-weight' not in self.rules:
|
||||
self.rules['font-weight'] = 'bold'
|
||||
|
||||
# Helper method for creating the css rule
|
||||
def _join_attr(self):
|
||||
temp = []
|
||||
if(len(self.rules) == 0):
|
||||
return ''
|
||||
for key in self.rules:
|
||||
temp.append(key + ': ' + self.rules[key])
|
||||
return '; '.join(temp) + ';'
|
||||
|
||||
def toString(self):
|
||||
joined = self._join_attr()
|
||||
if joined:
|
||||
return "%s .%s { %s }" % (Style.PREFIX, self.name, joined)
|
||||
return ''
|
||||
|
||||
|
||||
# Crappy xml parsing function for getting the
|
||||
# colors and font styles from colortheme file
|
||||
|
||||
|
||||
def get_settings(color_scheme_content):
|
||||
settings = defaultdict(lambda: [])
|
||||
dom = parseString(color_scheme_content)
|
||||
arr = dom.getElementsByTagName('array')[0]
|
||||
editor_cfg = arr.getElementsByTagName('dict')[0].getElementsByTagName('dict')[0]
|
||||
editor_vals = editor_cfg.getElementsByTagName('string')
|
||||
background = editor_vals[0].firstChild.nodeValue
|
||||
text_color = editor_vals[2].firstChild.nodeValue
|
||||
settings['editor_bg'] = background
|
||||
settings['text_color'] = text_color
|
||||
for node in arr.childNodes:
|
||||
if node.nodeName == "dict":
|
||||
try:
|
||||
setting = node.getElementsByTagName('string')[1].firstChild.nodeValue
|
||||
attrs = []
|
||||
values = node.getElementsByTagName('dict')[0].getElementsByTagName('string')
|
||||
for v in values:
|
||||
if v.firstChild:
|
||||
a = str(v.firstChild.nodeValue).strip()
|
||||
attrs.append(a)
|
||||
for s in setting.split(', '):
|
||||
settings[s] = attrs
|
||||
except:
|
||||
continue
|
||||
return settings
|
||||
|
||||
|
||||
def pygments_from_theme(color_scheme_content):
|
||||
settings = get_settings(color_scheme_content)
|
||||
styles = []
|
||||
|
||||
#Generic
|
||||
styles.append(Style('ge', ['italic']))
|
||||
styles.append(Style('gs', ['bold']))
|
||||
|
||||
# Comments
|
||||
styles.append(Style('c', settings['comment']))
|
||||
styles.append(Style('cp', settings['comment']))
|
||||
styles.append(Style('c1', settings['comment']))
|
||||
styles.append(Style('cs', settings['comment']))
|
||||
styles.append(Style('cm', settings['comment.block'], settings['comment']))
|
||||
|
||||
# Constants
|
||||
styles.append(Style('m', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
||||
styles.append(Style('mf', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
||||
styles.append(Style('mi', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
||||
styles.append(Style('mo', settings['constant.numeric'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
||||
styles.append(Style('se', settings['constant.language'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
||||
styles.append(Style('kc', settings['constant.language'], settings['constant.other'], settings['constant'], settings['support.constant']))
|
||||
|
||||
#Keywords
|
||||
styles.append(Style('k', settings['entity.name.type'], settings['support.type'], settings['keyword']))
|
||||
styles.append(Style('kd', settings['storage.type'], settings['storage']))
|
||||
styles.append(Style('kn', settings['support.function.construct'], settings['keyword.control'], settings['keyword']))
|
||||
styles.append(Style('kt', settings['entity.name.type'], settings['support.type'], settings['support.constant']))
|
||||
|
||||
#String
|
||||
styles.append(Style('settings', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
||||
styles.append(Style('sb', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
||||
styles.append(Style('sc', settings['string.quoted.single'], settings['string.quoted'], settings['string']))
|
||||
styles.append(Style('sd', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
||||
styles.append(Style('s2', settings['string.quoted.double'], settings['string.quoted'], settings['string']))
|
||||
styles.append(Style('sh', settings['string']))
|
||||
styles.append(Style('si', settings['string.interpolated'], settings['string']))
|
||||
styles.append(Style('sx', settings['string.other'], settings['string']))
|
||||
styles.append(Style('sr', settings['string.regexp'], settings['string']))
|
||||
styles.append(Style('s1', settings['string.quoted.single'], settings['string']))
|
||||
styles.append(Style('ss', settings['string']))
|
||||
|
||||
#Name
|
||||
styles.append(Style('na', settings['entity.other.attribute-name'], settings['entity.other']))
|
||||
styles.append(Style('bp', settings['variable.language'], settings['variable']))
|
||||
styles.append(Style('nc', settings['entity.name.class'], settings['entity.other.inherited-class'], settings['support.class']))
|
||||
styles.append(Style('no', settings['constant.language'], settings['constant']))
|
||||
styles.append(Style('nd', settings['entity.name.class']))
|
||||
styles.append(Style('ne', settings['entity.name.class']))
|
||||
styles.append(Style('nf', settings['entity.name.function'], settings['support.function']))
|
||||
styles.append(Style('nt', settings['entity.name.tag'], settings['keyword']))
|
||||
styles.append(Style('nv', settings['variable'], [settings['text_color']]))
|
||||
styles.append(Style('vc', settings['variable.language']))
|
||||
styles.append(Style('vg', settings['variable.language']))
|
||||
styles.append(Style('vi', settings['variable.language']))
|
||||
|
||||
#Operator
|
||||
styles.append(Style('ow', settings['keyword.operator'], settings['keyword.operator'], settings['keyword']))
|
||||
styles.append(Style('o', settings['keyword.operator'], settings['keyword.operator'], settings['keyword']))
|
||||
|
||||
# Text
|
||||
styles.append(Style('n', [settings['text_color']]))
|
||||
styles.append(Style('nl', [settings['text_color']]))
|
||||
styles.append(Style('nn', [settings['text_color']]))
|
||||
styles.append(Style('nx', [settings['text_color']]))
|
||||
styles.append(Style('bp', settings['variable.language'], settings['variable'], [settings['text_color']]))
|
||||
styles.append(Style('p', [settings['text_color']]))
|
||||
|
||||
css = '{} {{ background-color: {}; color: {}; }}\n'.format(Style.PREFIX, settings['editor_bg'], settings['text_color'])
|
||||
for st in styles:
|
||||
css_style = st.toString()
|
||||
if css_style:
|
||||
css += css_style + '\n'
|
||||
|
||||
return css
|
||||
|
||||
if __name__ == "__main__":
|
||||
args = sys.argv[1:]
|
||||
if len(args) < 1:
|
||||
print("Please provide the .tmTheme file!", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
print(pygments_from_theme(args[0]))
|
||||
18
live-testing/images.md
Normal file
@ -0,0 +1,18 @@
|
||||
I'm not sure that it **actually** going to work, but it seems nicer than the [previous version][prev]
|
||||
|
||||
this is a test, hello world
|
||||
|
||||
This is the first image from the local file system (absolute path, sorry, it's not going
|
||||
to work on your system unless your username is math2001):
|
||||
|
||||

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

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

|
||||
|
||||
[prev]: https://github.com/math2001/MarkdownLivePreview/tree/d4c477749ce7e77b8e9fc85464a2488f003c45bc
|
||||
BIN
live-testing/sublime_merge.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
live-testing/sublime_text.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
18
live-testing/test.md
Normal file
@ -0,0 +1,18 @@
|
||||
# hello world
|
||||
|
||||
This is a *test*. Some inline `[2]code()`.
|
||||
|
||||
what the hell...
|
||||
|
||||
```python
|
||||
import this
|
||||
|
||||
if input("answer yes") != 'yes':
|
||||
print("Really?")
|
||||
```
|
||||
|
||||
this flickering is really annoying...
|
||||
|
||||
It looks like it's gone... Oh wait nah, it's still here...
|
||||
|
||||
This should still be working, and it is!
|
||||
@ -1 +0,0 @@
|
||||
data:image/png;base64,R0lGODlhZABkAPU+AAAAAAwNDRweHyYpKzg8Pzo+QUBFSERJTEdMT05UV1NYXFVbX1hfY1lfZGFobWJpbmhvdGxzeHF5fnJ6gHV9g3Z+hHqDiXuEin+IjoCIjoKLkYKMkoSNk4eQl4iSmIqTmouUm42XnY+ZoJKco5OdpJOepZSeppahqJeiqZmjqpumrZ6psJ+qsqOutqSvt6SwuKezu6i0vKm1vay4wK66wq+7w6+8xLK+xrK/x7TAybXCy7bDy7jEzbjFzrzJ0gAAACH5BAUAAD4AIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAZABkAAAG/kCfcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiW8IAAAUilUpjQABkEsmMUchkwBIOTOQBQICGUabk0ctFhYdiSajAgOZRKeNRjkYqxaghyuwAgxFtZ1FJBe6NokHvya0nEUzuhYgijG/B86oRCDSOZAPv6VCw0SquiiWNwOwAzfjz0I8uasYPIMvDQ0kRhm/Ee/afKiQ1sIIDBAgkuUxQKDhA29ERMHK9GJSJR85pLUiwkOELgx6Goo0sG/IK1gVhCig9MjHimOreAmBMU+XngciRTrAMSQB/qxmR6KtEjGko7Shey7kbGgA6A0GBz4oOUjCno8YNXWp6NOCwVICD6UYPQqiBiANDHNOkILiqIYVg2Y0yPlAikddICASQtuwJJQY9OAimqFCZpRPei0pPnKjg4fHkB936JBYyg4VmDNrVlH5zYMFoEOLZgDBSoejqDfQEc1atBXUsOl8bi26bpUNsKWpnlPjg+PIj32brZJjs/HOi5PjiMFzCo4ZyAWpqCBhwgspMFa9NZRjg4TvEjZCEQFzWvQ9KiiA/+73SVtpGAT7mcFh/XcPVqH0uCsNhDs+J9gnAQXX+cADCSDMggRVVtGE2lZ6fCAgfkPcdYFhRAhlAVHxxfCnC4d42EdghtII1hYGLgjxki6GOSiNHtR990F+QpymizcZ0SNEjquI1+FHetDHQYFEuCANhBpaMMRAuqRYxEEJDSLPR1YlWVRN9Vjy3ioFCWHlEC6Uh44iOcB0gQck2kSEB90o4sEFx1yY5irQ9JdIDdIANcSXRBiDzGAfVcbnELiwmEgHx3Q5p5JGmOPjIdAF9eIRnyRnhA1AWvqEn4pq6umnoIYq6qiklmrqqaimquqqrLbq6quwxirrrLTWauuttwYBADs=
|
||||
119
markdown2html.py
Normal file
@ -0,0 +1,119 @@
|
||||
import copy
|
||||
import os.path
|
||||
import concurrent.futures
|
||||
import urllib.request
|
||||
import base64
|
||||
import bs4
|
||||
|
||||
from functools import lru_cache, partial
|
||||
|
||||
from .lib.markdown2 import Markdown
|
||||
|
||||
__all__ = ('markdown2html', )
|
||||
|
||||
markdowner = Markdown(extras=['fenced-code-blocks'])
|
||||
|
||||
# FIXME: how do I choose how many workers I want? Does thread pool reuse threads or
|
||||
# does it stupidly throw them out? (we could implement something of our own)
|
||||
executor = concurrent.futures.ThreadPoolExecutor(max_workers=5)
|
||||
|
||||
images_cache = {}
|
||||
|
||||
class LoadingError(Exception):
|
||||
pass
|
||||
|
||||
def markdown2html(markdown, basepath, re_render, resources):
|
||||
""" converts the markdown to html, loads the images and puts in base64 for sublime
|
||||
to understand them correctly. That means that we are responsible for loading the
|
||||
images from the internet. Hence, we take in re_render, which is just a function we
|
||||
call when an image has finished loading to retrigger a render (see #90)
|
||||
"""
|
||||
html = markdowner.convert(markdown)
|
||||
|
||||
soup = bs4.BeautifulSoup(html, "html.parser")
|
||||
for img_element in soup.find_all('img'):
|
||||
src = img_element['src']
|
||||
|
||||
# already in base64, or something of the like
|
||||
# FIXME: what other types are possible? Are they handled by ST? If not, could we
|
||||
# convert it into base64? is it worth the effort?
|
||||
if src.startswith('data:image/'):
|
||||
continue
|
||||
|
||||
if src.startswith('http://') or src.startswith('https://'):
|
||||
path = src
|
||||
elif src.startswith('file://'):
|
||||
path = src[len('file://'):]
|
||||
else:
|
||||
# expanduser: ~ -> /home/math2001
|
||||
# realpath: simplify that paths so that we don't have duplicated caches
|
||||
path = os.path.realpath(os.path.expanduser(os.path.join(basepath, src)))
|
||||
|
||||
try:
|
||||
base64 = get_base64_image(path, re_render)
|
||||
except FileNotFoundError as e:
|
||||
base64 = resources['base64_404_image']
|
||||
except LoadingError:
|
||||
base64 = resources['base64_loading_image']
|
||||
|
||||
img_element['src'] = base64
|
||||
|
||||
# remove comments, because they pollute the console with error messages
|
||||
for comment_element in soup.find_all(text=lambda text: isinstance(text, bs4.Comment)):
|
||||
comment_element.extract()
|
||||
|
||||
# FIXME: how do tables look? should we use ascii tables?
|
||||
|
||||
# pre aren't handled by ST3. The require manual adjustment
|
||||
for pre_element in soup.find_all('pre'):
|
||||
# select the first child, <code>
|
||||
code_element = next(pre_element.children)
|
||||
|
||||
# FIXME: this method sucks, but can we do better?
|
||||
fixed_pre = str(code_element) \
|
||||
.replace(' ', '<i class="space">.</i>') \
|
||||
.replace('\n', '<br />')
|
||||
|
||||
code_element.replace_with(bs4.BeautifulSoup(fixed_pre, "html.parser"))
|
||||
|
||||
# FIXME: highlight the code using Sublime's syntax
|
||||
|
||||
# FIXME: report that ST doesn't support <br/> but does work with <br />... WTF?
|
||||
return "<style>\n{}\n</style>\n\n{}".format(resources['stylesheet'], soup).replace('<br/>', '<br />')
|
||||
|
||||
def get_base64_image(path, re_render):
|
||||
|
||||
def callback(url, future):
|
||||
# this is "safe" to do because callback is called in the same thread as
|
||||
# add_done_callback:
|
||||
# > Added callables are called in the order that they were added and are always
|
||||
# > called in a thread belonging to the process that added them
|
||||
# > --- Python docs
|
||||
images_cache[url] = future.result()
|
||||
# we render, which means this function will be called again, but this time, we
|
||||
# will read from the cache
|
||||
re_render()
|
||||
|
||||
if path.startswith('http://') or path.startswith('https://'):
|
||||
if path in images_cache:
|
||||
return images_cache[path]
|
||||
executor.submit(load_image, path).add_done_callback(partial(callback, path))
|
||||
raise LoadingError()
|
||||
|
||||
# FIXME: use some kind of cache for this as well, because it decodes on every
|
||||
# keystroke here...
|
||||
with open(path, 'rb') as fp:
|
||||
return 'data:image/png;base64,' + base64.b64encode(fp.read()).decode('utf-8')
|
||||
|
||||
# FIXME: wait what the hell? Why do I have two caches? (lru and images_cache)
|
||||
# FIXME: This is an in memory cache. 20 seems like a fair bit of images... Should it be
|
||||
# bigger? Should the user be allowed to chose? There definitely should be a limit
|
||||
# because we don't wanna use to much memory, we're a simple markdown preview plugin
|
||||
# NOTE: > The LRU feature performs best when maxsize is a power-of-two. --- python docs
|
||||
@lru_cache(maxsize=2 ** 4)
|
||||
def load_image(url):
|
||||
with urllib.request.urlopen(url, timeout=60) as conn:
|
||||
content_type = conn.info().get_content_type()
|
||||
if 'image' not in content_type:
|
||||
raise ValueError("{!r} doesn't point to an image, but to a {!r}".format(url, content_type))
|
||||
return 'data:image/png;base64,' + base64.b64encode(conn.read()).decode('utf-8')
|
||||
35
mkdocs.yml
@ -1,35 +0,0 @@
|
||||
site_name: MarkdownLivePreview
|
||||
theme: material
|
||||
repo_name: math2001/MarkdownLivePreview
|
||||
repo_url: https://github.com/math2001/MarkdownLivePreview
|
||||
site_description: Sublime Text 3 Plugin MarkdownLivePreview's documentation
|
||||
site_author: math2001
|
||||
|
||||
markdown_extensions:
|
||||
- toc(permalink=true)
|
||||
- pymdownx.arithmatex
|
||||
- pymdownx.betterem(smart_enable=all)
|
||||
- pymdownx.caret
|
||||
- pymdownx.critic
|
||||
- pymdownx.emoji:
|
||||
emoji_generator: !!python/name:pymdownx.emoji.to_svg
|
||||
- pymdownx.inlinehilite
|
||||
- pymdownx.magiclink
|
||||
- pymdownx.mark
|
||||
- pymdownx.smartsymbols
|
||||
- pymdownx.superfences
|
||||
- pymdownx.tasklist(custom_checkbox=true)
|
||||
- pymdownx.tilde
|
||||
- admonition
|
||||
- codehilite
|
||||
|
||||
extra:
|
||||
logo: imgs/MarkdownLivePreview-opposite.svg
|
||||
palette:
|
||||
primary: Blue
|
||||
accent: Indigo
|
||||
social:
|
||||
- type: github
|
||||
link: https://github.com/math2001
|
||||
- type: twitter
|
||||
link: https://twitter.com/_math2001
|
||||
1
resources/404.base64
Normal file
|
Before Width: | Height: | Size: 5.8 KiB After Width: | Height: | Size: 5.8 KiB |
9
resources/convertresources.py
Normal file
@ -0,0 +1,9 @@
|
||||
""" A small script to convert the images into base64 data """
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
with open('404.png', 'rb') as png, open('404.base64', 'wb') as base64:
|
||||
base64.write(b64encode(png.read()))
|
||||
|
||||
with open('loading.png', 'rb') as png, open('loading.base64', 'wb') as base64:
|
||||
base64.write(b64encode(png.read()))
|
||||
1
resources/loading.base64
Normal file
@ -0,0 +1 @@
|
||||
R0lGODlhZABkAPU+AAAAAAwNDRweHyYpKzg8Pzo+QUBFSERJTEdMT05UV1NYXFVbX1hfY1lfZGFobWJpbmhvdGxzeHF5fnJ6gHV9g3Z+hHqDiXuEin+IjoCIjoKLkYKMkoSNk4eQl4iSmIqTmouUm42XnY+ZoJKco5OdpJOepZSeppahqJeiqZmjqpumrZ6psJ+qsqOutqSvt6SwuKezu6i0vKm1vay4wK66wq+7w6+8xLK+xrK/x7TAybXCy7bDy7jEzbjFzrzJ0gAAACH5BAUAAD4AIf8LTkVUU0NBUEUyLjADAQAAACwAAAAAZABkAAAG/kCfcEgsGo/IpHLJbDqf0Kh0Sq1ar9isdsvter/gsHhMLpvP6LR6zW673/C4fE6v2+/4vH7P7/v/gIGCg4SFhoeIiW8IAAAUilUpjQABkEsmMUchkwBIOTOQBQICGUabk0ctFhYdiSajAgOZRKeNRjkYqxaghyuwAgxFtZ1FJBe6NokHvya0nEUzuhYgijG/B86oRCDSOZAPv6VCw0SquiiWNwOwAzfjz0I8uasYPIMvDQ0kRhm/Ee/afKiQ1sIIDBAgkuUxQKDhA29ERMHK9GJSJR85pLUiwkOELgx6Goo0sG/IK1gVhCig9MjHimOreAmBMU+XngciRTrAMSQB/qxmR6KtEjGko7Shey7kbGgA6A0GBz4oOUjCno8YNXWp6NOCwVICD6UYPQqiBiANDHNOkILiqIYVg2Y0yPlAikddICASQtuwJJQY9OAimqFCZpRPei0pPnKjg4fHkB936JBYyg4VmDNrVlH5zYMFoEOLZgDBSoejqDfQEc1atBXUsOl8bi26bpUNsKWpnlPjg+PIj32brZJjs/HOi5PjiMFzCo4ZyAWpqCBhwgspMFa9NZRjg4TvEjZCEQFzWvQ9KiiA/+73SVtpGAT7mcFh/XcPVqH0uCsNhDs+J9gnAQXX+cADCSDMggRVVtGE2lZ6fCAgfkPcdYFhRAhlAVHxxfCnC4d42EdghtII1hYGLgjxki6GOSiNHtR990F+QpymizcZ0SNEjquI1+FHetDHQYFEuCANhBpaMMRAuqRYxEEJDSLPR1YlWVRN9Vjy3ioFCWHlEC6Uh44iOcB0gQck2kSEB90o4sEFx1yY5irQ9JdIDdIANcSXRBiDzGAfVcbnELiwmEgHx3Q5p5JGmOPjIdAF9eIRnyRnhA1AWvqEn4pq6umnoIYq6qiklmrqqaimquqqrLbq6quwxirrrLTWauuttwYBADs=
|
||||
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 953 B |
44
resources/stylesheet.css
Normal file
@ -0,0 +1,44 @@
|
||||
html {
|
||||
--light-bg: color(var(--background) blend(#fff 90%));
|
||||
--very-light-bg: color(var(--background) blend(#fff 85%));
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Ubuntu", "DejaVu Sans", "Open Sans", sans-serif;
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
font-style: italic;
|
||||
display: block;
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
background-color: var(--very-light-bg);
|
||||
}
|
||||
|
||||
code {
|
||||
padding-left: 0.2rem;
|
||||
padding-right: 0.2rem;
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
code {
|
||||
padding-left: 0.2rem;
|
||||
padding-right: 0.2rem;
|
||||
background-color: var(--very-light-bg);
|
||||
margin: 0;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
pre code {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
i.space {
|
||||
color: var(--very-light-bg);
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
# -*- encoding: utf-8 -*-
|
||||
|
||||
PREVIEW_ENABLED = 'markdown_live_preview_enabled'
|
||||
PREVIEW_ID = 'markdown_live_preview_id'
|
||||
IS_PREVIEW = 'is_markdown_live_preview'
|
||||
IS_HIDDEN = 'is_hidden_markdown_live_preview'
|
||||
MD_VIEW_ID = 'markdown_live_preview_md_id'
|
||||
PREVIEW_WINDOW = 'markdown_live_preview_window'
|
||||
ON_OPEN = 'markdown_live_preview_on_open'
|
||||
LAST_UPDATE = 'markdonw_live_preview_last_run'
|
||||
16
test.md
@ -1,16 +0,0 @@
|
||||
```python
|
||||
import this
|
||||
|
||||
if you.are('new'):
|
||||
print('Welcome!')
|
||||
if you.are('brand new'):
|
||||
print("You'll see, python's just awesome")
|
||||
else:
|
||||
print('Hello!')
|
||||
```
|
||||
|
||||

|
||||
|
||||
<ol>
|
||||
<li>test</li>
|
||||
</ol>
|
||||
38
todo
@ -1,38 +0,0 @@
|
||||
Fast:
|
||||
☐ cache image in object when used, so that it's faster @needsTest
|
||||
|
||||
Medium:
|
||||
☐ auto refresh preview if loading images
|
||||
☐ use alt attribute for 404 error
|
||||
☐ optimize usage of BeautifulSoup
|
||||
|
||||
Long:
|
||||
☐ support anchor (TOC) @big
|
||||
|
||||
Unknown:
|
||||
☐ properly convert tmtheme to css
|
||||
|
||||
___________________
|
||||
Archive:
|
||||
✔ add settings to keep md view open #13 @done Sat 11 Feb 2017 at 09:10 @project(Fast)
|
||||
✔ fix custom css @bug @done Sun 22 Jan 2017 at 18:40 @project(Medium)
|
||||
✘ check how many times is the show_html function called @cancelled Sun 22 Jan 2017 at 18:40 @project(Unknown)
|
||||
✔ sync scroll @needsUpdate(because of images) @done Sun 22 Jan 2017 at 18:39 @project(Fast)
|
||||
✔ fix #4 @high @done Mon 09 Jan 2017 at 18:42 @project(Long)
|
||||
✔ use MarkdownLivePreview syntax, so we can use syntax's settings @done Mon 09 Jan 2017 at 18:41 @project(Medium)
|
||||
✔ add clear cache command @done Mon 09 Jan 2017 at 18:41 @project(Fast)
|
||||
✔ update README for settings in view @done Mon 09 Jan 2017 at 18:41 @project(Fast)
|
||||
✔ add edit settings @done Mon 09 Jan 2017 at 18:41 @project(Fast)
|
||||
✘ listen for settings to change @cancelled Mon 09 Jan 2017 at 18:41 @project(Medium)
|
||||
✘ call settings listener on_new too - might be too heavy @cancelled Sun 08 Jan 2017 at 19:33 @project(Fast)
|
||||
✔ fix relative source @done Sun 08 Jan 2017 at 19:22 @project(Medium)
|
||||
✔ add settings for the preview @done Sun 08 Jan 2017 at 17:36 @project(Fast)
|
||||
✔ regive focus to the right markdown view @done Mon 02 Jan 2017 at 18:34 @project(Fast)
|
||||
✔ try/except for 404 @done Mon 02 Jan 2017 at 18:03 @project(Fast)
|
||||
✔ fix bug when empty `src` @done Mon 02 Jan 2017 at 17:15 @project(Fast)
|
||||
✔ preview.set_scratch(True) @done Mon 02 Jan 2017 at 16:58
|
||||
✔ set the title of the preview @done Mon 02 Jan 2017 at 16:58
|
||||
✔ preview.wordWrap => True @done Mon 02 Jan 2017 at 16:58
|
||||
✔ clean the code (syntax) @done Mon 02 Jan 2017 at 16:58
|
||||
✔ add 404 image @done Mon 02 Jan 2017 at 16:57
|
||||
✔ load images from internet (`https:`) @done Mon 02 Jan 2017 at 16:57
|
||||
23
utils.py
Normal file
@ -0,0 +1,23 @@
|
||||
# import sublime
|
||||
import time
|
||||
|
||||
def get_settings():
|
||||
return sublime.get_settings("MarkdownLivePreview.sublime-settings")
|
||||
|
||||
def min_time_between_call(timeout, on_block=lambda *args, **kwargs: None):
|
||||
""" Enforces a timeout between each call to the function
|
||||
timeout is in seconds
|
||||
"""
|
||||
last_call = 0
|
||||
|
||||
def outer(func):
|
||||
def wrapper(*args, **kwargs):
|
||||
nonlocal last_call
|
||||
|
||||
if time.time() - last_call < timeout:
|
||||
time.sleep(timeout - (time.time() - last_call))
|
||||
|
||||
last_call = time.time()
|
||||
return func(*args, **kwargs)
|
||||
return wrapper
|
||||
return outer
|
||||