Compare commits
140 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 2365d6fec2 | |||
| 35c8a954d0 | |||
| e914a2d4e9 | |||
| 7fbb23b480 | |||
| ec27d980a3 | |||
| 9fe7369029 | |||
| e462e8b3bc | |||
| cf68b2c202 | |||
| c10bc95e54 | |||
| 84fb15aec3 | |||
| 04989f8660 | |||
| 192f61bf0c | |||
| 2785df74ce | |||
| e13842ede4 | |||
| c6ac821c4a | |||
| 9ad3f25d14 | |||
| eaa357a65f | |||
| 0f5630c3dc | |||
| c14c28b56b | |||
| 0dea8afba4 | |||
| e3896a6b3d | |||
| 6016f07cd1 | |||
| c0c9867cc8 | |||
| 5f2cac54e8 | |||
| 8c1012eb8c | |||
| cc28bfef96 | |||
| ef9b2daf6d | |||
| bae26fc452 | |||
| 5738f6b5ff | |||
| 6bb8e6ebaa | |||
| 8eb6882d60 | |||
| 61cf2984eb | |||
| 7f7dcd6ba8 | |||
| d3d88ddb49 | |||
| 9a8ac3886e | |||
| d4c477749c | |||
| 79c785176f | |||
| 82ad98085f | |||
| dd184c5fdd | |||
| c334c49592 | |||
| 41c28e2b24 | |||
| e1eb17fe96 | |||
| 823d22afee | |||
| 91f4bc5052 | |||
| 7126c0e090 | |||
| 6a3dd6ac2f | |||
| 1542e5e898 | |||
| 05c471b5d9 | |||
| 76f580ba29 | |||
| 119acbb092 | |||
| 7c4354fb2e | |||
| b93aea6698 | |||
| b3fb5779d3 | |||
| 7bdda5f5c7 | |||
| 7257cb467e | |||
| 40a563fb1e | |||
| 3e0d6ad265 | |||
| f65a068b4e | |||
| bc328642e7 | |||
| d2053be41e | |||
| eb48b1c79f | |||
| 8317fa738c | |||
| 3be12b0539 | |||
| c92d78fb20 | |||
| 30d75f159d | |||
| 52e4b917e5 | |||
| 48a68b2a79 | |||
| 8eb0172eb4 | |||
| 52e35fb610 | |||
| 84f809e57f | |||
| 351e8bd355 | |||
| 5babc862b4 | |||
| dc7139fbe7 | |||
| bbbeae6fe9 | |||
| 271c7c619a | |||
| 8cc6b2b263 | |||
| c7961ce94c | |||
| eae91fa428 | |||
| 6f18e8e4a2 | |||
| 48c1800065 | |||
| bad1cb74c6 | |||
| 4198504fd1 | |||
| 1bef00de14 | |||
| d707cf7a47 | |||
| c27cd5f210 | |||
| 41bbc3d03d | |||
| 1e651bebc6 | |||
| ea309f2323 | |||
| 3306d6ad5e | |||
| 0505ca30bc | |||
| c49ae26720 | |||
| c2618ead1d | |||
| 6e113fef6e | |||
| e5378e2300 | |||
| caf932b536 | |||
| fa106c8206 | |||
| 0ac9fd9aaa | |||
| 5bbfb4606d | |||
| 37703e9bab | |||
| 3b920f4336 | |||
| abe151fdb7 | |||
| acc8beb3be | |||
| 9206b6de62 | |||
| cda4532833 | |||
| ff8c94bda5 | |||
| ded9c28096 | |||
| 0e6660a331 | |||
| 0143428114 | |||
| f48ef63956 | |||
| a4e670de43 | |||
| d466a29cd4 | |||
| 0f0e53ff34 | |||
| 30ac30082f | |||
| e4e7c44c3c | |||
| 75a8cf53f9 | |||
| 3dcaed0ede | |||
| c605ffb3db | |||
| aa5fd4faf8 | |||
| 613e3fb1b2 | |||
| 057f770859 | |||
| 76d56deff6 | |||
| d2863e4a43 | |||
| ed57d2813a | |||
| b4038c3575 | |||
| fd633e0bc0 | |||
| 4a3caf30bb | |||
| 28a7274b05 | |||
| 6ad9e79926 | |||
| ac09c523e7 | |||
| 3ad29ede37 | |||
| 576956a8d1 | |||
| 93a04733da | |||
| 6a0267fb3b | |||
| f52bf98470 | |||
| 5a3b1a7f81 | |||
| 182862ecce | |||
| 518f6f1ed4 | |||
| b44151ed69 | |||
| e4a5ea886a | |||
| e6ea13f0a2 |
3
.gitattributes
vendored
Normal file
3
.gitattributes
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
docs/ export-ignore
|
||||||
|
resources/
|
||||||
|
!resources/*.base64
|
||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1 +1 @@
|
|||||||
Thumbs.db
|
__pycache__
|
||||||
|
|||||||
@ -1,16 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"keys": ["alt+m"],
|
|
||||||
"command": "toggle_setting",
|
|
||||||
"args": {
|
|
||||||
"setting": "markdown_preview_enabled"
|
|
||||||
},
|
|
||||||
"context": [
|
|
||||||
{
|
|
||||||
"key": "selector",
|
|
||||||
"operator": "equal",
|
|
||||||
"operand": "text.html.markdown"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
7
LICENSE
Normal file
7
LICENSE
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
Copyright 2017 Mathieu PATUREL
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated 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,20 +0,0 @@
|
|||||||
[
|
|
||||||
{
|
|
||||||
"id": "preferences",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"id": "package-settings",
|
|
||||||
"children": [
|
|
||||||
{
|
|
||||||
"caption": "MarkdownLivePreview",
|
|
||||||
"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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
]
|
|
||||||
271
MarkdownLivePreview.py
Normal file
271
MarkdownLivePreview.py
Normal file
@ -0,0 +1,271 @@
|
|||||||
|
"""
|
||||||
|
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
|
||||||
|
"""
|
||||||
|
|
||||||
|
import time
|
||||||
|
import os.path
|
||||||
|
import struct
|
||||||
|
import sublime
|
||||||
|
import sublime_plugin
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from .markdown2html import markdown2html
|
||||||
|
|
||||||
|
MARKDOWN_VIEW_INFOS = "markdown_view_infos"
|
||||||
|
PREVIEW_VIEW_INFOS = "preview_view_infos"
|
||||||
|
SETTING_DELAY_BETWEEN_UPDATES = "delay_between_updates"
|
||||||
|
SETTING_FONT_SCALE = "font_scale"
|
||||||
|
|
||||||
|
resources = {}
|
||||||
|
|
||||||
|
|
||||||
|
def plugin_loaded():
|
||||||
|
global DELAY
|
||||||
|
resources["base64_404_image"] = parse_image_resource(get_resource("404.base64"))
|
||||||
|
resources["base64_loading_image"] = parse_image_resource(
|
||||||
|
get_resource("loading.base64")
|
||||||
|
)
|
||||||
|
resources["stylesheet"] = get_resource("stylesheet.css")
|
||||||
|
# FIXME: how could we make this setting update without restarting sublime text
|
||||||
|
# and not loading it every update as well
|
||||||
|
DELAY = get_settings().get(SETTING_DELAY_BETWEEN_UPDATES)
|
||||||
|
|
||||||
|
|
||||||
|
class MdlpInsertCommand(sublime_plugin.TextCommand):
|
||||||
|
def run(self, edit, point, string):
|
||||||
|
self.view.insert(edit, point, string)
|
||||||
|
|
||||||
|
|
||||||
|
class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
|
||||||
|
def run(self, edit):
|
||||||
|
|
||||||
|
""" If the file is saved exists on disk, we close it, and reopen it in a new
|
||||||
|
window. Otherwise, we copy the content, erase it all (to close the file without
|
||||||
|
a dialog) and re-insert it into a new view into a new window """
|
||||||
|
|
||||||
|
original_view = self.view
|
||||||
|
original_window_id = original_view.window().id()
|
||||||
|
file_name = original_view.file_name()
|
||||||
|
|
||||||
|
syntax_file = original_view.settings().get("syntax")
|
||||||
|
|
||||||
|
if file_name:
|
||||||
|
original_view.close()
|
||||||
|
else:
|
||||||
|
# the file isn't saved, we need to restore the content manually
|
||||||
|
total_region = sublime.Region(0, original_view.size())
|
||||||
|
content = original_view.substr(total_region)
|
||||||
|
original_view.erase(edit, total_region)
|
||||||
|
original_view.close()
|
||||||
|
# FIXME: save the document to a temporary file, so that if we crash,
|
||||||
|
# the user doesn't lose what he wrote
|
||||||
|
|
||||||
|
sublime.run_command("new_window")
|
||||||
|
preview_window = sublime.active_window()
|
||||||
|
|
||||||
|
preview_window.run_command(
|
||||||
|
"set_layout",
|
||||||
|
{
|
||||||
|
"cols": [0.0, 0.5, 1.0],
|
||||||
|
"rows": [0.0, 1.0],
|
||||||
|
"cells": [[0, 0, 1, 1], [1, 0, 2, 1]],
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
preview_window.focus_group(1)
|
||||||
|
preview_view = preview_window.new_file()
|
||||||
|
preview_view.set_scratch(True)
|
||||||
|
preview_view.settings().set(PREVIEW_VIEW_INFOS, {})
|
||||||
|
preview_view.set_name("Preview")
|
||||||
|
# FIXME: hide number lines on preview
|
||||||
|
|
||||||
|
preview_window.focus_group(0)
|
||||||
|
if file_name:
|
||||||
|
markdown_view = preview_window.open_file(file_name)
|
||||||
|
else:
|
||||||
|
markdown_view = preview_window.new_file()
|
||||||
|
markdown_view.run_command("mdlp_insert", {"point": 0, "string": content})
|
||||||
|
markdown_view.set_scratch(True)
|
||||||
|
|
||||||
|
markdown_view.set_syntax_file(syntax_file)
|
||||||
|
markdown_view.settings().set(
|
||||||
|
MARKDOWN_VIEW_INFOS, {"original_window_id": original_window_id,},
|
||||||
|
)
|
||||||
|
|
||||||
|
def is_enabled(self):
|
||||||
|
# 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):
|
||||||
|
|
||||||
|
phantom_sets = {
|
||||||
|
# markdown_view.id(): phantom set
|
||||||
|
}
|
||||||
|
|
||||||
|
# we schedule an update for every key stroke, with a delay of DELAY
|
||||||
|
# then, we update only if now() - last_update > DELAY
|
||||||
|
last_update = 0
|
||||||
|
|
||||||
|
# FIXME: maybe we shouldn't restore the file in the original window...
|
||||||
|
|
||||||
|
def on_pre_close(self, markdown_view):
|
||||||
|
""" Close the view in the preview window, and store information for the on_close
|
||||||
|
listener (see doc there)
|
||||||
|
"""
|
||||||
|
if not markdown_view.settings().get(MARKDOWN_VIEW_INFOS):
|
||||||
|
return
|
||||||
|
|
||||||
|
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:
|
||||||
|
self.content = None
|
||||||
|
|
||||||
|
def on_load_async(self, markdown_view):
|
||||||
|
infos = markdown_view.settings().get(MARKDOWN_VIEW_INFOS)
|
||||||
|
if not infos:
|
||||||
|
return
|
||||||
|
|
||||||
|
preview_view = markdown_view.window().active_view_in_group(1)
|
||||||
|
|
||||||
|
self.phantom_sets[markdown_view.id()] = sublime.PhantomSet(preview_view)
|
||||||
|
self._update_preview(markdown_view)
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
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)
|
||||||
|
# Reload settings each time to catch changes
|
||||||
|
settings = get_settings()
|
||||||
|
delay = settings.get(SETTING_DELAY_BETWEEN_UPDATES, 100) # Provide default
|
||||||
|
font_scale = settings.get(SETTING_FONT_SCALE, 1.0) # Provide default
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
preview_view = markdown_view.window().active_view_in_group(1)
|
||||||
|
# Get viewport_width, default to a large value if view isn't ready
|
||||||
|
viewport_extent = preview_view.viewport_extent()
|
||||||
|
viewport_width = viewport_extent[0] if viewport_extent else 1024
|
||||||
|
|
||||||
|
|
||||||
|
basepath = os.path.dirname(markdown_view.file_name()) if markdown_view.file_name() else '.' # Handle unsaved files
|
||||||
|
html = markdown2html(
|
||||||
|
markdown,
|
||||||
|
basepath,
|
||||||
|
partial(self._update_preview, markdown_view),
|
||||||
|
resources,
|
||||||
|
viewport_width,
|
||||||
|
font_scale,
|
||||||
|
)
|
||||||
|
|
||||||
|
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}),
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_settings():
|
||||||
|
return sublime.load_settings("MarkdownLivePreview.sublime-settings")
|
||||||
|
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
|
||||||
|
def parse_image_resource(text):
|
||||||
|
width, height, base64_image = text.splitlines()
|
||||||
|
return base64_image, (int(width), int(height))
|
||||||
|
|
||||||
|
|
||||||
|
# try to reload the resources if we save this file
|
||||||
|
try:
|
||||||
|
plugin_loaded()
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
@ -1,9 +1,15 @@
|
|||||||
[
|
[
|
||||||
|
|
||||||
{
|
{
|
||||||
"caption": "MarkdownLivePreview: Toggle",
|
"caption": "MarkdownLivePreview: Open Preview",
|
||||||
"command": "toggle_setting",
|
"command": "open_markdown_preview"
|
||||||
"args": {
|
},
|
||||||
"setting": "markdown_preview_enabled"
|
{
|
||||||
}
|
"caption": "MarkdownLivePreview: Open Settings",
|
||||||
|
"command": "edit_settings", "args":
|
||||||
|
{
|
||||||
|
"base_file": "${packages}/MarkdownLivePreview/MarkdownLivePreview.sublime-settings",
|
||||||
|
"default": "{\n\t$0\n}\n"
|
||||||
|
},
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
7
MarkdownLivePreview.sublime-settings
Normal file
7
MarkdownLivePreview.sublime-settings
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
// minimum number of milliseconds to wait before updating the preview again
|
||||||
|
"delay_between_updates": 100,
|
||||||
|
|
||||||
|
// scale factor for the font size relative to markdown settings
|
||||||
|
"font_scale": 1.0
|
||||||
|
}
|
||||||
74
README.md
74
README.md
@ -1,47 +1,65 @@
|
|||||||
# MarkdownLivePreview
|
# MarkdownLivePreview
|
||||||
|
|
||||||
This is a sublime text **3** plugin that allows you to preview your markdown instantly *in* it!
|
A simple plugin to preview your markdown as you type right in Sublime Text.
|
||||||
|
No dependencies!
|
||||||
|
|
||||||
### Dependencies
|
## How to install
|
||||||
|
|
||||||
**None**! There is no dependency! It uses [markdown2](https://github.com/trentm/python-markdown2) but it's a one file plugin, so it's included in the package.
|
It's available on package control!
|
||||||
|
|
||||||
## Installation
|
## Setting a keybinding
|
||||||
|
|
||||||
Although MarkdownLivePreview is not available on the default channel of [PackageControl](http://packagecontrol.io), you can still use it to download this little package.
|
The open the preview, you can search up in the command palette
|
||||||
|
(<kbd>ctrl+shift+p</kbd>) `MarkdownLivePreview: Open Preview`. But if you
|
||||||
|
prefer to have a shortcut, add this to your keybindings file:
|
||||||
|
|
||||||
1. Open the command palette (`ctrl+shift+p`)
|
```json
|
||||||
2. Search for: `Package Control: Add Repository`
|
{
|
||||||
3. Enter in the input at the bottom of ST the path to this repo: <https://github.com/math2001/MarkdownLivePreview> (tip: just drag the link in)
|
"keys": ["alt+m"],
|
||||||
4. Hit <kbd>enter</kbd>
|
"command": "open_markdown_preview"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
What this does is simply adding this repo to the list of packages you get when you install a package using PC.
|
## How to contribute
|
||||||
|
|
||||||
So, as you probably understood, now you just need to install MarkdownLivePreview as if it was available on the default channel:
|
If you know what feature you want to implement, or what bug you wanna fix, then
|
||||||
|
go ahead and hack! Maybe raise an issue before hand so that we can talk about
|
||||||
|
it if it's a big feature.
|
||||||
|
|
||||||
1. Open the command palette (`ctrl+shift+p`)
|
But if you wanna contribute just to say thanks, and don't really know what you
|
||||||
2. Search for: `Package Control: Install Package`
|
could be working on, then there are a bunch of `FIXME`s all over this package.
|
||||||
3. Search for: `MarkdownLivePreview`
|
Just pick one and fix it :-)
|
||||||
4. hit <kbd>enter</kbd>
|
|
||||||
|
|
||||||
Done!
|
```
|
||||||
|
$ git clone https://github.com/math2001/MarkdownLivePreview
|
||||||
|
$ cd MarkdownLivePreview
|
||||||
|
$ grep -R FIXME
|
||||||
|
```
|
||||||
|
|
||||||
### Usage
|
### Hack it!
|
||||||
|
|
||||||
Sometimes, you just want to open a markdown file to edit it quickly, you don't care about the preview, and even worse, **you don't want it**. So, if you want to have the preview, press `alt+m`, edit your file, and you'll get a nice preview.
|
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!
|
||||||
|
|
||||||
### In dev
|
### Known limitations
|
||||||
|
|
||||||
This plugin is not finished, there's still some things to fix (custom css, focus, etc). So, don't run away if you have any trouble, just submit an issue [here](http://github.com/math2001/MarkdownLivePreview/issues).
|
#### Numbered lists are rendered as unordered lists
|
||||||
|
|
||||||
### Demo
|
```md
|
||||||
|
1. first
|
||||||
|
2. second
|
||||||
|
3. third
|
||||||
|
```
|
||||||
|
|
||||||

|
will be previewed the exact same way as
|
||||||
|
|
||||||
### Custom css
|
```md
|
||||||
|
- first
|
||||||
|
- second
|
||||||
|
- third
|
||||||
|
```
|
||||||
|
|
||||||
It is possible to set your own css. But, be carefull, you have to respect [those rules](http://www.sublimetext.com/docs/3/minihtml.html#css). Just go to `Preferences -> Package Settings -> MarkdownLivePreview`. It will open a css file, here: `$packages/User/MarkdownLivePreview.css`. Just save it and it will automatically use it instead of the default one.
|
The issue comes from [Sublime Text's minihtml](https://www.sublimetext.com/docs/3/minihtml.html) which [doesn't support ordered lists](https://github.com/sublimehq/sublime_text/issues/1767). If you think feel like implementing a workaround, feel free to contribute, but it's not something I'm planning on doing. It isn't a critical feature, and support should come with time...
|
||||||
|
|
||||||
### How to open the [README](http://github.com/math2001/MarkdownLivePreview/README.md)
|
|
||||||
|
|
||||||
Some of the package add a command in the menus, others in the command palette, or other nowhere. None of those options are really good, especially the last one on ST3 because the packages are compressed. But, fortunately, there is plugin that exists and **will solve this problem** for us (and he has a really cute name, don't you think?): [ReadmePlease](https://packagecontrol.io/packages/ReadmePlease).
|
|
||||||
|
|||||||
7
dependencies.json
Normal file
7
dependencies.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"*": {
|
||||||
|
"*": [
|
||||||
|
"bs4"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
18
live-testing/images.md
Normal file
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
BIN
live-testing/sublime_merge.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
BIN
live-testing/sublime_text.png
Normal file
BIN
live-testing/sublime_text.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 38 KiB |
52
live-testing/test.md
Normal file
52
live-testing/test.md
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
# Why?
|
||||||
|
Why do you want to use fancy symbols in your standard monospace font? Obviously to have a fancy prompt like mine :-)
|
||||||
|
|
||||||
|
<!--  -->
|
||||||
|
|
||||||
|
And because when you live in a terminal a symbol can convey more informations in less space creating a dense and beautiful (for those who have a certain aesthetic taste) informative workspace
|
||||||
|
|
||||||
|
Heavily inspired by <https://github.com/Lokaltog/vim-powerline> and the relative patch script from **Kim Silkebækken** (kim.silkebaekken+vim@gmail.com)
|
||||||
|
|
||||||
|
## Patching vs Fallback
|
||||||
|
There are two strategies that could be used to have symbols in a terminal
|
||||||
|
* you can take a bunch of symbol fonts, your favourite monospace font and merge them together (patching strategy)
|
||||||
|
* you can use a feature of `freetype2` font engine, basically you can say that whenever the current font doesn't have a glyph for a certain codepoint then fallback and go look into other fonts (fallback strategy)
|
||||||
|
|
||||||
|
Initially I used the first strategy, later I switched to the second. The patching strategy it's more reliable and portable, the problem is that you need to patch every monospace font you want to use and patching a single font it's a lot of manual fine tuning. If you want you can find all previous patched fonts in [patching-strategy branch](https://github.com/gabrielelana/awesome-terminal-fonts/tree/patching-strategy)
|
||||||
|
|
||||||
|
## Font Maps
|
||||||
|
Referring to glyphs by codepints (eg. `\uf00c`) in your scripts or shell configuration it's not recommended because icon fonts like [Font Awesome](http://fontawesome.io/) use [code points ranges](https://en.wikipedia.org/wiki/Private_Use_Areas) those ranges are not disciplined by the unicode consortium, every font can associate every glyphs to those codepoints. This means that [Font Awesome](http://fontawesome.io/) can choose to move glyphs around freely, today `\uf00c` is associated to the `check` symbol, tomorrow it can be associated to something else. Moreover, more than one icon font can use the same codepoint for different glyphs and if we want to use them both we need to move one of them. So, if you use a codepoint to refer to a glyph after an update that codepoint can point to another glyph. To avoid this situation you can use the font maps in the `./build` directory, font maps are scripts which define shell variables that give names to glyphs, by sourcing those files in your shell you can refer to glyphs by name (eg. `$CODEPOINT_OF_AWESOME_CHECK`).
|
||||||
|
|
||||||
|
TLDR: don't refer to glyphs by codepoints (eg. `\uf00c`) but by name (eg. `$CODEPOINT_OF_AWESOME_CHECK`) to make your scripts and shell configurations resilient to future updates. To do that don't forget to copy font maps (`*.sh` files) in the `./build` directory in your home directory and to source them in your shell startup
|
||||||
|
|
||||||
|
## Included Fonts
|
||||||
|
In this repository you can find a bunch of fonts that I use as symbol fonts with the relative font maps
|
||||||
|
* **Font Awesome 4.7.0**: `./fonts/fontawesome-regular.ttf`, for further informations and license see http://fortawesome.github.io/Font-Awesome
|
||||||
|
* **Devicons 1.8.0**: `./fonts/devicons-regular.ttf`, for further informations and license see https://github.com/vorillaz/devicons
|
||||||
|
* **Octicons 1.0.0**: `./fonts/octicons-regular.ttf`, for further informations and license see https://github.com/blog/1135-the-making-of-octicons
|
||||||
|
* **Pomicons 1.0.0**: `./fonts/pomicons-regular.ttf`, for further informations and license see https://github.com/gabrielelana/pomicons
|
||||||
|
|
||||||
|
## How to install (Linux)
|
||||||
|
* copy all the fonts from `./build` directory to `~/.fonts` directory
|
||||||
|
* copy all the font maps (all `*.sh` files) from `./build` directory to `~/.fonts` directory
|
||||||
|
* run `fc-cache -fv ~/.fonts` to let freetype2 know of those fonts
|
||||||
|
* customize the configuration file `./config/10-symbols.conf` replacing `PragmataPro` with the name of the font you want to use in the terminal (I will add more fonts in the future so that this step could be skippable)
|
||||||
|
* copy the above configuration file to `~/.config/fontconfig/conf.d` directory
|
||||||
|
* source the font maps (`source ~/.fonts/*.sh`) in your shell startup script (eg. `~/.bashrc` or `~/.zshrc`)
|
||||||
|
|
||||||
|
### Arch Linux
|
||||||
|
We have been included in the [official repositories](https://www.archlinux.org/packages/community/any/awesome-terminal-fonts/), so if you are running an Arch Linux
|
||||||
|
* run `pacman -Syu awesome-terminal-fonts`
|
||||||
|
|
||||||
|
## How to install (OSX)
|
||||||
|
* follow [this detailed instructions](https://github.com/gabrielelana/awesome-terminal-fonts/wiki/OS-X) contributed by [@inkrement](https://github.com/inkrement)
|
||||||
|
* copy all the fonts maps (all `*.sh` files) from `./build` directory to `~/.fonts` directory
|
||||||
|
* source the font maps (`source ~/.fonts/*.sh`) in your shell startup script (eg. `~/.bashrc` or `~/.zshrc`)
|
||||||
|
* If it still doesn't work, consider to use the [patching strategy](#patching-vs-fallback)
|
||||||
|
|
||||||
|
## How to install (Windows)
|
||||||
|
* make sure you have permissions to execute Powershell scripts in your machine. To do so, open Windows Powershell as Administrator and paste & run the following command `Set-ExecutionPolicy RemoteSigned`
|
||||||
|
* then run the install script `./install.ps1`
|
||||||
|
|
||||||
|
## License
|
||||||
|
[MIT](https://github.com/gabrielelana/awesome-terminal-fonts/blob/master/LICENSE)
|
||||||
226
markdown2html.py
Normal file
226
markdown2html.py
Normal file
@ -0,0 +1,226 @@
|
|||||||
|
""" Notice how this file is completely independent of sublime text
|
||||||
|
|
||||||
|
I think it should be kept this way, just because it gives a bit more organisation,
|
||||||
|
and makes it a lot easier to think about, and for anyone who would want to, test since
|
||||||
|
markdown2html is just a pure function
|
||||||
|
"""
|
||||||
|
|
||||||
|
import io
|
||||||
|
import struct
|
||||||
|
import os.path
|
||||||
|
import concurrent.futures
|
||||||
|
import urllib.request
|
||||||
|
import base64
|
||||||
|
import bs4
|
||||||
|
|
||||||
|
from functools import partial
|
||||||
|
|
||||||
|
from .lib.markdown2 import Markdown
|
||||||
|
|
||||||
|
__all__ = ("markdown2html",)
|
||||||
|
|
||||||
|
markdowner = Markdown(extras=["fenced-code-blocks", "cuddled-lists"])
|
||||||
|
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
def markdown2html(markdown, basepath, re_render, resources, viewport_width, font_scale=1.0):
|
||||||
|
""" 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)))
|
||||||
|
|
||||||
|
base64, (width, height) = get_base64_image(path, re_render, resources)
|
||||||
|
|
||||||
|
img_element["src"] = base64
|
||||||
|
if width > viewport_width:
|
||||||
|
img_element["width"] = viewport_width
|
||||||
|
img_element["height"] = viewport_width * (height / width)
|
||||||
|
|
||||||
|
# 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?
|
||||||
|
# Add font scaling CSS rule
|
||||||
|
font_scale_css = "body {{ font-size: {}em; }}\n".format(font_scale)
|
||||||
|
stylesheet = font_scale_css + resources["stylesheet"]
|
||||||
|
|
||||||
|
return "<style>\n{}\n</style>\n\n{}".format(stylesheet, soup).replace(
|
||||||
|
"<br/>", "<br />"
|
||||||
|
)
|
||||||
|
|
||||||
|
images_cache = {}
|
||||||
|
images_loading = []
|
||||||
|
|
||||||
|
def get_base64_image(path, re_render, resources):
|
||||||
|
""" Gets the base64 for the image (local and remote images). re_render is a
|
||||||
|
callback which is called when we finish loading an image from the internet
|
||||||
|
to trigger an update of the preview (the image will then be loaded from the cache)
|
||||||
|
|
||||||
|
return base64_data, (width, height)
|
||||||
|
"""
|
||||||
|
|
||||||
|
def callback(path, resources, future):
|
||||||
|
# altering images_cache is "safe" to do because callback is called in the same
|
||||||
|
# thread as add_done_callback:
|
||||||
|
# > Added callables are called in the order that they were added and are always
|
||||||
|
# > called in a thread belonging to the process that added them
|
||||||
|
# > --- Python docs
|
||||||
|
try:
|
||||||
|
images_cache[path] = future.result()
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
images_cache[path] = resources['base64_404_image']
|
||||||
|
print("Error loading {!r}: {!r}".format(path, e))
|
||||||
|
|
||||||
|
images_loading.remove(path)
|
||||||
|
|
||||||
|
# we render, which means this function will be called again, but this time, we
|
||||||
|
# will read from the cache
|
||||||
|
re_render()
|
||||||
|
|
||||||
|
if path in images_cache:
|
||||||
|
return images_cache[path]
|
||||||
|
|
||||||
|
if path.startswith("http://") or path.startswith("https://"):
|
||||||
|
# FIXME: submiting a load of loaders, we should only have one
|
||||||
|
if path not in images_loading:
|
||||||
|
executor.submit(load_image, path).add_done_callback(partial(callback, path, resources))
|
||||||
|
images_loading.append(path)
|
||||||
|
return resources['base64_loading_image']
|
||||||
|
|
||||||
|
with open(path, "rb") as fhandle:
|
||||||
|
image_content = fhandle.read()
|
||||||
|
width, height = get_image_size(io.BytesIO(image_content), path)
|
||||||
|
|
||||||
|
image = "data:image/png;base64," + base64.b64encode(image_content).decode(
|
||||||
|
"utf-8"
|
||||||
|
)
|
||||||
|
images_cache[path] = image, (width, height)
|
||||||
|
return images_cache[path]
|
||||||
|
|
||||||
|
|
||||||
|
def load_image(url):
|
||||||
|
with urllib.request.urlopen(url, timeout=60) as conn:
|
||||||
|
|
||||||
|
image_content = conn.read()
|
||||||
|
width, height = get_image_size(io.BytesIO(image_content), url)
|
||||||
|
|
||||||
|
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(image_content).decode("utf-8"),
|
||||||
|
(width, height),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_size(fhandle, pathlike):
|
||||||
|
""" Thanks to https://stackoverflow.com/a/20380514/6164984 for providing the basis
|
||||||
|
of a working solution.
|
||||||
|
|
||||||
|
fhandle should be a seekable stream. It's not the best for non-seekable streams,
|
||||||
|
but in our case, we have to load the whole stream into memory anyway because base64
|
||||||
|
library only accepts bytes-like objects, and not streams.
|
||||||
|
|
||||||
|
pathlike is the filename/path/url of the image so that we can guess the file format
|
||||||
|
"""
|
||||||
|
|
||||||
|
format_ = os.path.splitext(os.path.basename(pathlike))[1][1:]
|
||||||
|
|
||||||
|
head = fhandle.read(24)
|
||||||
|
if len(head) != 24:
|
||||||
|
return "invalid head"
|
||||||
|
if format_ == "png":
|
||||||
|
check = struct.unpack(">i", head[4:8])[0]
|
||||||
|
if check != 0x0D0A1A0A:
|
||||||
|
return
|
||||||
|
width, height = struct.unpack(">ii", head[16:24])
|
||||||
|
elif format_ == "gif":
|
||||||
|
width, height = struct.unpack("<HH", head[6:10])
|
||||||
|
elif format_ == "jpeg":
|
||||||
|
try:
|
||||||
|
fhandle.seek(0) # Read 0xff next
|
||||||
|
|
||||||
|
size = 2
|
||||||
|
ftype = 0
|
||||||
|
while not 0xC0 <= ftype <= 0xCF:
|
||||||
|
fhandle.seek(size, 1)
|
||||||
|
byte = fhandle.read(1)
|
||||||
|
if byte == b"":
|
||||||
|
fhandle = end
|
||||||
|
byte = fhandle.read(1)
|
||||||
|
|
||||||
|
while ord(byte) == 0xFF:
|
||||||
|
byte = fhandle.read(1)
|
||||||
|
ftype = ord(byte)
|
||||||
|
size = struct.unpack(">H", fhandle.read(2))[0] - 2
|
||||||
|
# We are at a SOFn block
|
||||||
|
fhandle.seek(1, 1) # Skip `precision' byte.
|
||||||
|
height, width = struct.unpack(">HH", fhandle.read(4))
|
||||||
|
except Exception as e: # IGNORE:W0703
|
||||||
|
raise e
|
||||||
|
else:
|
||||||
|
return "unknown format {!r}".format(format_)
|
||||||
|
return width, height
|
||||||
|
|
||||||
|
|
||||||
|
def independent_markdown2html(markdown):
|
||||||
|
return markdown2html(
|
||||||
|
markdown,
|
||||||
|
".",
|
||||||
|
lambda: None,
|
||||||
|
{
|
||||||
|
"base64_404_image": ("", (0, 0)),
|
||||||
|
"base64_loading_image": ("", (0, 0)),
|
||||||
|
"stylesheet": "",
|
||||||
|
},
|
||||||
|
960,
|
||||||
|
1.0, # Add default font_scale for independent call
|
||||||
|
)
|
||||||
200
md_in_popup.py
200
md_in_popup.py
@ -1,200 +0,0 @@
|
|||||||
import sublime
|
|
||||||
import sublime_plugin
|
|
||||||
from . import markdown2
|
|
||||||
import os.path
|
|
||||||
import re
|
|
||||||
|
|
||||||
from html.parser import HTMLParser
|
|
||||||
|
|
||||||
# Main sublime tools function
|
|
||||||
|
|
||||||
def md(*t, **kwargs):
|
|
||||||
sublime.message_dialog(kwargs.get('sep', '\n').join([str(el) for el in t]))
|
|
||||||
|
|
||||||
def sm(*t, **kwargs):
|
|
||||||
sublime.status_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
|
||||||
|
|
||||||
def em(*t, **kwargs):
|
|
||||||
sublime.error_message(kwargs.get('sep', ' ').join([str(el) for el in t]))
|
|
||||||
|
|
||||||
def mini(val, min):
|
|
||||||
if val < min:
|
|
||||||
return min
|
|
||||||
return val
|
|
||||||
|
|
||||||
STYLE_FILE = os.path.join(sublime.packages_path(), 'User', 'MarkdownLivePreview.css')
|
|
||||||
def get_style():
|
|
||||||
content = None
|
|
||||||
if os.path.exists(STYLE_FILE):
|
|
||||||
with open(STYLE_FILE) as fp:
|
|
||||||
content = fp.read()
|
|
||||||
return content
|
|
||||||
if not content:
|
|
||||||
content = """
|
|
||||||
html {
|
|
||||||
--light-bg: color(var(--background) blend(#999 85%))
|
|
||||||
}
|
|
||||||
body {
|
|
||||||
padding:10px;
|
|
||||||
padding-top: 0px;
|
|
||||||
font-family: "Open Sans", sans-serif;
|
|
||||||
background-color: var(--background);
|
|
||||||
font-size: 15px;
|
|
||||||
}
|
|
||||||
|
|
||||||
blockquote {
|
|
||||||
font-style: italic;
|
|
||||||
display: block;
|
|
||||||
margin-left: 30px;
|
|
||||||
border: 1px solid red;
|
|
||||||
}
|
|
||||||
|
|
||||||
code {
|
|
||||||
padding-left: 0.2rem;
|
|
||||||
padding-right: 0.2rem;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
margin: 0;
|
|
||||||
border-radius: 3px;
|
|
||||||
margin: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
pre {
|
|
||||||
display: block;
|
|
||||||
margin-top: 20px;
|
|
||||||
line-height: 1.7;
|
|
||||||
background-color: var(--light-bg);
|
|
||||||
padding-left: 10px;
|
|
||||||
width: 100%;
|
|
||||||
border-radius: 3px;
|
|
||||||
}
|
|
||||||
pre code {
|
|
||||||
padding-left: 0;
|
|
||||||
}
|
|
||||||
"""
|
|
||||||
return content + "pre code .space {color: var(--light-bg)}"
|
|
||||||
|
|
||||||
def pre_with_br(html):
|
|
||||||
"""Because the phantoms of sublime text does not support <pre> blocks
|
|
||||||
this function replaces every \n with a <br> in a <pre>"""
|
|
||||||
|
|
||||||
while True:
|
|
||||||
obj = re.search(r'<pre>(.*?)</pre>', html, re.DOTALL)
|
|
||||||
if not obj:
|
|
||||||
break
|
|
||||||
html = list(html)
|
|
||||||
html[obj.start(0):obj.end(0)] = '<pre >' + ''.join(html[obj.start(1):obj.end(1)]) \
|
|
||||||
.replace('\n', '<br>') \
|
|
||||||
.replace(' ', ' ') + '</pre>'
|
|
||||||
html = ''.join(html)
|
|
||||||
return html
|
|
||||||
|
|
||||||
def close_preview(md_view_settings, preview):
|
|
||||||
preview.close()
|
|
||||||
md_view_settings.erase('markdown_preview_id')
|
|
||||||
md_view_settings.erase('markdown_preview_enabled')
|
|
||||||
|
|
||||||
def create_preview(window, md_view):
|
|
||||||
focus_group, focus_view = window.get_view_index(md_view)
|
|
||||||
preview = window.new_file()
|
|
||||||
window.run_command('new_pane') # move the preview to a new group
|
|
||||||
preview.set_name(os.path.basename(md_view.file_name()) + ' - Preview')
|
|
||||||
|
|
||||||
preview_settings = preview.settings()
|
|
||||||
preview_settings.set('gutter', False)
|
|
||||||
preview_settings.set('is_markdown_preview', True)
|
|
||||||
preview_settings.set('markdown_view_id', md_view.id())
|
|
||||||
|
|
||||||
md_view.settings().set('markdown_preview_id', preview.id())
|
|
||||||
window.focus_group(focus_group)
|
|
||||||
window.focus_view(md_view)
|
|
||||||
|
|
||||||
return preview
|
|
||||||
|
|
||||||
def show_html(md_view, preview):
|
|
||||||
html = ('<style>{}</style>'.format(get_style()) +
|
|
||||||
pre_with_br(markdown2.markdown(get_view_content(md_view),
|
|
||||||
extras=['fenced-code-blocks', 'no-code-highlighting'])))
|
|
||||||
|
|
||||||
# the option no-code-highlighting does not exists
|
|
||||||
# in the official version of markdown2 for now
|
|
||||||
# I personaly edited the file (markdown2.py:1743)
|
|
||||||
|
|
||||||
html = html.replace(' ', ' espace;') # save where are the spaces
|
|
||||||
|
|
||||||
html = HTMLParser().unescape(html)
|
|
||||||
|
|
||||||
# exception, again, because <pre> aren't supported by the phantoms
|
|
||||||
html = html.replace(' espace;', '<i class="space">.</i>')
|
|
||||||
preview.erase_phantoms('markdown_preview')
|
|
||||||
preview.add_phantom('markdown_preview',
|
|
||||||
sublime.Region(-1),
|
|
||||||
html,
|
|
||||||
sublime.LAYOUT_BLOCK,
|
|
||||||
lambda href: sublime.run_command('open_url', {'url': href}))
|
|
||||||
# 0 < y < 1
|
|
||||||
y = md_view.text_to_layout(md_view.sel()[0].begin())[1] / md_view.layout_extent()[1]
|
|
||||||
vector = [0, y * preview.layout_extent()[1]]
|
|
||||||
# remove half of the viewport_extent.y to center it on the screen (verticaly)
|
|
||||||
vector[1] -= preview.viewport_extent()[1] / 2
|
|
||||||
vector[1] = mini(vector[1], 0)
|
|
||||||
vector[1] += preview.line_height()
|
|
||||||
preview.set_viewport_position(vector, animate=False)
|
|
||||||
|
|
||||||
def get_view_content(view):
|
|
||||||
return view.substr(sublime.Region(0, view.size()))
|
|
||||||
|
|
||||||
def get_view_from_id(window, id):
|
|
||||||
for view in window.views():
|
|
||||||
if view.id() == id:
|
|
||||||
return view
|
|
||||||
|
|
||||||
class MarkdownInPopupCommand(sublime_plugin.EventListener):
|
|
||||||
|
|
||||||
def on_load(self, view):
|
|
||||||
settings = view.settings()
|
|
||||||
if not 'markdown' in settings.get('syntax').lower():
|
|
||||||
return
|
|
||||||
settings.add_on_change('markdown_preview_enabled', lambda: self.on_modified(view))
|
|
||||||
|
|
||||||
def on_modified(self, md_view):
|
|
||||||
window = md_view.window()
|
|
||||||
md_view_settings = md_view.settings()
|
|
||||||
|
|
||||||
if not 'markdown' in md_view_settings.get('syntax').lower():
|
|
||||||
return
|
|
||||||
|
|
||||||
markdown_preview_enabled = md_view_settings.get('markdown_preview_enabled') is True
|
|
||||||
preview_id = md_view_settings.get('markdown_preview_id', None)
|
|
||||||
|
|
||||||
if not markdown_preview_enabled:
|
|
||||||
if preview_id is not None:
|
|
||||||
preview = get_view_from_id(window, preview_id)
|
|
||||||
if preview:
|
|
||||||
close_preview(md_view_settings, preview)
|
|
||||||
return
|
|
||||||
|
|
||||||
if preview_id is None:
|
|
||||||
preview = create_preview(window, md_view)
|
|
||||||
else:
|
|
||||||
preview = get_view_from_id(window, preview_id)
|
|
||||||
if not preview:
|
|
||||||
md_view_settings.erase('markdown_preview_id')
|
|
||||||
md_view_settings.erase('markdown_preview_enabled')
|
|
||||||
return
|
|
||||||
|
|
||||||
show_html(md_view, preview)
|
|
||||||
|
|
||||||
def on_pre_close(self, view):
|
|
||||||
settings = view.settings()
|
|
||||||
if settings.get('markdown_preview_enabled') is True:
|
|
||||||
preview = get_view_from_id(view.window(), settings.get('markdown_preview_id'))
|
|
||||||
if preview:
|
|
||||||
sublime.set_timeout_async(lambda: preview.close(), 250)
|
|
||||||
elif settings.get('is_markdown_preview') is True:
|
|
||||||
md_view = get_view_from_id(view.window(), settings.get('markdown_view_id'))
|
|
||||||
if md_view:
|
|
||||||
def callback():
|
|
||||||
md_view_settings = md_view.settings()
|
|
||||||
md_view_settings.erase('markdown_preview_enabled')
|
|
||||||
md_view_settings.erase('markdown_preview_id')
|
|
||||||
sublime.set_timeout_async(callback, 250)
|
|
||||||
7
messages.json
Normal file
7
messages.json
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"install": "messages/install.txt",
|
||||||
|
"1.1.2": "messages/1.1.2.txt",
|
||||||
|
"2.0.1": "messages/2.0.1.txt",
|
||||||
|
"2.2.1": "messages/2.2.0.txt",
|
||||||
|
"2.4.1": "messages/2.4.txt"
|
||||||
|
}
|
||||||
12
messages/1.1.2.txt
Normal file
12
messages/1.1.2.txt
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
Small changes occured on MarkdownLivePreview.
|
||||||
|
|
||||||
|
Changes:
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
Main callback is now async (prevent ST from crashing)
|
||||||
|
|
||||||
|
|
||||||
|
Tip of the day: Delete all the content till the end of the line: Ctrl+k, Ctrl+k
|
||||||
|
You'll use it much more than you think ;)
|
||||||
22
messages/2.0.1.txt
Normal file
22
messages/2.0.1.txt
Normal file
@ -0,0 +1,22 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
Some quite important changes have occured on MarkdownLivePreview.
|
||||||
|
|
||||||
|
The first version was quite buggy: it made Sublime Text 3 crash. So I released
|
||||||
|
a whole new version, which is working differently.
|
||||||
|
|
||||||
|
When you active MarkdownLivePreview, it'll open a new window with the markdown
|
||||||
|
view on the left, and the preview on the right.
|
||||||
|
|
||||||
|
When you'll close any file in this window, it'll close the *entire* window.
|
||||||
|
|
||||||
|
I hope you'll still enjoy using MarkdownLivePreview. If you have any request,
|
||||||
|
just let me know by raising an issue here:
|
||||||
|
|
||||||
|
github.com/math2001/MarkdownLivePreview/issues
|
||||||
|
|
||||||
|
Tip of the day: you can duplicate a line by pressing 'ctrl+d'
|
||||||
|
remove a line by pressing 'ctrl+shift+k'
|
||||||
|
join lines by pressing 'ctrl+j'
|
||||||
|
get use to use these few shortcuts, and you'll speed up
|
||||||
|
significantly.
|
||||||
8
messages/2.1.0.txt
Normal file
8
messages/2.1.0.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
A settings is now available: `markdown_live_preview_on_open`. If set to true,
|
||||||
|
it opens the window preview as soon as you open a markdown file. See the
|
||||||
|
README for more infos.
|
||||||
|
|
||||||
|
Tip of the day: `ctrl+w`: closes the current file
|
||||||
|
`ctrl+shift+w`: closes the current window
|
||||||
8
messages/2.2.0.txt
Normal file
8
messages/2.2.0.txt
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
Something changed on MarkdownLivePreview: the preview is now scrolled to where
|
||||||
|
you are editing your markdown file! It doesn't scroll back up all the time any
|
||||||
|
more!
|
||||||
|
|
||||||
|
Tip of the day: You can center the screen on your cursor by pressing:
|
||||||
|
`ctrl+k, ctrl+v` (on OSX `cmd+k, cmd+v`)
|
||||||
10
messages/2.4.txt
Normal file
10
messages/2.4.txt
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
Sorry to interrupt you... :(
|
||||||
|
|
||||||
|
Some stuff changed on MarkdownLivePreview. It now supports YAML/TOML front matters. You can hide it,
|
||||||
|
or show it in a pre block (edit your settings for this).
|
||||||
|
|
||||||
|
Hope you'll enjoy it!
|
||||||
|
|
||||||
|
Tip of the day: If you want a VIM-like search feature, then just press 'ctrl/cmd+i'
|
||||||
|
(Find → Incremental find). You can still go the next match by pressing 'f3', and to
|
||||||
|
the previous one by pressing 'shift+f3'
|
||||||
42
messages/install.txt
Normal file
42
messages/install.txt
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
__ __ _ _ _ _ _____ _
|
||||||
|
| \/ | | | | | | | (_) | __ \ (_)
|
||||||
|
| \ / | __ _ _ __| | ____| | _____ ___ __ | | ___ _____| |__) | __ _____ ___ _____ __
|
||||||
|
| |\/| |/ _` | '__| |/ / _` |/ _ \ \ /\ / / '_ \| | | \ \ / / _ \ ___/ '__/ _ \ \ / / |/ _ \ \ /\ / /
|
||||||
|
| | | | (_| | | | < (_| | (_) \ V V /| | | | |____| |\ V / __/ | | | | __/\ V /| | __/\ V V /
|
||||||
|
|_| |_|\__,_|_| |_|\_\__,_|\___/ \_/\_/ |_| |_|______|_| \_/ \___|_| |_| \___| \_/ |_|\___| \_/\_/
|
||||||
|
|
||||||
|
Thanks for installing MarkdownLivePreview! I hope you'll enjoy using it!
|
||||||
|
|
||||||
|
Quick Start:
|
||||||
|
~~~~~~~~~~~~
|
||||||
|
|
||||||
|
To enable MarkdownLivePreview, you need to be on a markdown view (works with
|
||||||
|
Markdown Extended). Then just press `alt+m`, or search up in the command
|
||||||
|
palette: 'MarkdownLivePreview: Toggle'. Hit enter and you're done. As soon as
|
||||||
|
you type anything in, it'll show up with the preview in a new group.
|
||||||
|
|
||||||
|
Say thanks:
|
||||||
|
~~~~~~~~~~~
|
||||||
|
|
||||||
|
Just letting me know you're enjoying this plugin is a great way to say thanks!
|
||||||
|
|
||||||
|
You can do so by staring the GitHub repo, or sending a tweet (@_math2001) for
|
||||||
|
example!
|
||||||
|
|
||||||
|
Troubles?
|
||||||
|
~~~~~~~~~
|
||||||
|
|
||||||
|
If you have any kind of trouble with it, just let me now by raising an issue on
|
||||||
|
the GitHub issue tracker here:
|
||||||
|
|
||||||
|
https://github.com/math2001/MarkdownLivePreview/issues
|
||||||
|
|
||||||
|
Tip of the day: Right click on a URL and choose 'Open <the url>' to open it in
|
||||||
|
your default browser.
|
||||||
|
|
||||||
|
Credits:
|
||||||
|
~~~~~~~~
|
||||||
|
|
||||||
|
The ASCII MLP has been generated using the ASCII Decorator.
|
||||||
|
|
||||||
|
https://github.com/viisual/ASCII-Decorator
|
||||||
15
repository.json
Normal file
15
repository.json
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"schema_version": "3",
|
||||||
|
"packages": [
|
||||||
|
{
|
||||||
|
"name": "MarkdownLivePreview-Fork",
|
||||||
|
"description": "My enhanced live-preview fork of MarkdownLivePreview",
|
||||||
|
"releases": [
|
||||||
|
{
|
||||||
|
"version": "1.0.0",
|
||||||
|
"url": "https://git.0x42.cloud/christian.morpurgo/MarkdownLivePreview/archive/v0.0.0.zip"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
3
resources/404.base64
Normal file
3
resources/404.base64
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/404.png
Normal file
BIN
resources/404.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.8 KiB |
34
resources/convertresources.py
Normal file
34
resources/convertresources.py
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
""" A small script to convert the images into base64 data """
|
||||||
|
|
||||||
|
import struct
|
||||||
|
from base64 import b64encode
|
||||||
|
|
||||||
|
|
||||||
|
def get_image_size(fhandle):
|
||||||
|
"""https://stackoverflow.com/a/20380514/6164984"""
|
||||||
|
head = fhandle.read(24)
|
||||||
|
if len(head) != 24:
|
||||||
|
return
|
||||||
|
|
||||||
|
# always going to be png
|
||||||
|
check = struct.unpack(">i", head[4:8])[0]
|
||||||
|
if check != 0x0D0A1A0A:
|
||||||
|
raise ValueError("invalid check (?)")
|
||||||
|
|
||||||
|
width, height = struct.unpack(">ii", head[16:24])
|
||||||
|
return width, height
|
||||||
|
|
||||||
|
|
||||||
|
def make_cache(image_name):
|
||||||
|
with open("{}.png".format(image_name), "rb") as png, open(
|
||||||
|
"{}.base64".format(image_name), "wb"
|
||||||
|
) as base64:
|
||||||
|
width, height = get_image_size(png)
|
||||||
|
png.seek(0)
|
||||||
|
base64.write(bytes("{}\n{}\n".format(width, height), encoding="utf-8"))
|
||||||
|
base64.write(b'data:image/png;base64,')
|
||||||
|
base64.write(b64encode(png.read()))
|
||||||
|
|
||||||
|
|
||||||
|
make_cache("404")
|
||||||
|
make_cache("loading")
|
||||||
3
resources/loading.base64
Normal file
3
resources/loading.base64
Normal file
File diff suppressed because one or more lines are too long
BIN
resources/loading.png
Normal file
BIN
resources/loading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 7.2 KiB |
BIN
resources/loading.xcf
Normal file
BIN
resources/loading.xcf
Normal file
Binary file not shown.
45
resources/stylesheet.css
Normal file
45
resources/stylesheet.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
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);
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
BIN
resources/transparent-loading.png
Normal file
BIN
resources/transparent-loading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 953 B |
46
sample.md
46
sample.md
@ -1,46 +0,0 @@
|
|||||||
# Hello world!
|
|
||||||
|
|
||||||
Lorem ipsum dolor sit amet, consectetur adipisicing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
|
|
||||||
|
|
||||||
### The Zen of Python, by Tim Peters
|
|
||||||
|
|
||||||
> Beautiful is better than ugly.
|
|
||||||
> Explicit is better than implicit.
|
|
||||||
> Simple is better than complex.
|
|
||||||
> Complex is better than complicated.
|
|
||||||
> Flat is better than nested.
|
|
||||||
> Sparse is better than dense.
|
|
||||||
> Readability counts.
|
|
||||||
> Special cases aren't special enough to break the rules.
|
|
||||||
> Although practicality beats purity.
|
|
||||||
> Errors should never pass silently.
|
|
||||||
> Unless explicitly silenced.
|
|
||||||
> In the face of ambiguity, refuse the temptation to guess.
|
|
||||||
> There should be one-- and preferably only one --obvious way to do it.
|
|
||||||
> Although that way may not be obvious at first unless you're Dutch.
|
|
||||||
> Now is better than never.
|
|
||||||
> Although never is often better than *right* now.
|
|
||||||
> If the implementation is hard to explain, it's a bad idea.
|
|
||||||
> If the implementation is easy to explain, it may be a good idea.
|
|
||||||
> Namespaces are one honking great idea -- let's do more of those!
|
|
||||||
|
|
||||||
> Code tells you how, comments tells you why
|
|
||||||
|
|
||||||
print('hello world')
|
|
||||||
print('hi')
|
|
||||||
|
|
||||||
```python
|
|
||||||
print('This is some pretty')
|
|
||||||
print('cool stuff')
|
|
||||||
if test:
|
|
||||||
print('hello world')
|
|
||||||
```
|
|
||||||
|
|
||||||
This is some `code`
|
|
||||||
|
|
||||||
- a
|
|
||||||
- list
|
|
||||||
|
|
||||||
1. and
|
|
||||||
2. other
|
|
||||||
3. list
|
|
||||||
12
todo.md
12
todo.md
@ -1,12 +0,0 @@
|
|||||||
# todo
|
|
||||||
|
|
||||||
- add message in status bar @notGoodIdea
|
|
||||||
|
|
||||||
|
|
||||||
- add **custom css** feature @done
|
|
||||||
- sync scroll @done
|
|
||||||
- regive focus to the right markdown view @done
|
|
||||||
- set the title of the preview @done
|
|
||||||
- disable previewing when the preview is closed @done
|
|
||||||
- check when setting is activated and create panel and stuff @done
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user