Compare commits
10 Commits
c6ac821c4a
...
ec27d980a3
| Author | SHA1 | Date | |
|---|---|---|---|
| ec27d980a3 | |||
| 9fe7369029 | |||
| e462e8b3bc | |||
| cf68b2c202 | |||
| c10bc95e54 | |||
| 84fb15aec3 | |||
| 04989f8660 | |||
| 192f61bf0c | |||
| 2785df74ce | |||
| e13842ede4 |
4
.gitattributes
vendored
4
.gitattributes
vendored
@ -1,3 +1,3 @@
|
||||
docs/ export-ignore
|
||||
resources/*.png export-ignore
|
||||
resources/*.py export-ignore
|
||||
resources/
|
||||
!resources/*.base64
|
||||
|
||||
@ -1,52 +1,47 @@
|
||||
"""
|
||||
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
|
||||
from .utils import *
|
||||
|
||||
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
|
||||
|
||||
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)
|
||||
SETTING_DELAY_BETWEEN_UPDATES = "delay_between_updates"
|
||||
|
||||
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')
|
||||
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)
|
||||
|
||||
# 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):
|
||||
|
||||
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
|
||||
@ -57,7 +52,7 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
|
||||
original_window_id = original_view.window().id()
|
||||
file_name = original_view.file_name()
|
||||
|
||||
syntax_file = original_view.settings().get('syntax')
|
||||
syntax_file = original_view.settings().get("syntax")
|
||||
|
||||
if file_name:
|
||||
original_view.close()
|
||||
@ -70,41 +65,45 @@ class OpenMarkdownPreviewCommand(sublime_plugin.TextCommand):
|
||||
# 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')
|
||||
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.run_command(
|
||||
"set_layout",
|
||||
{
|
||||
"cols": [0.0, 0.5, 1.0],
|
||||
"rows": [0.0, 1.0],
|
||||
"cells": [[0, 0, 1, 1], [1, 0, 2, 1]],
|
||||
},
|
||||
)
|
||||
|
||||
preview_window.focus_group(1)
|
||||
preview_view = preview_window.new_file()
|
||||
preview_view.set_scratch(True)
|
||||
preview_view.settings().set(PREVIEW_VIEW_INFOS, {})
|
||||
preview_view.set_name('Preview')
|
||||
|
||||
preview_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.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
|
||||
})
|
||||
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()
|
||||
return "markdown" in self.view.settings().get("syntax").lower()
|
||||
|
||||
|
||||
class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
||||
|
||||
@ -153,30 +152,36 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
||||
if not infos:
|
||||
return
|
||||
|
||||
assert markdown_view.id() == self.markdown_view.id(), \
|
||||
"pre_close view.id() != close view.id()"
|
||||
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')
|
||||
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'])
|
||||
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"
|
||||
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'))
|
||||
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
|
||||
@ -188,7 +193,7 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
||||
if not infos:
|
||||
return
|
||||
|
||||
# we schedule an update, which won't run if an
|
||||
# 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):
|
||||
@ -207,17 +212,50 @@ class MarkdownLivePreviewListener(sublime_plugin.EventListener):
|
||||
total_region = sublime.Region(0, markdown_view.size())
|
||||
markdown = markdown_view.substr(total_region)
|
||||
|
||||
preview_view = markdown_view.window().active_view_in_group(1)
|
||||
viewport_width = preview_view.viewport_extent()[0]
|
||||
|
||||
basepath = os.path.dirname(markdown_view.file_name())
|
||||
html = markdown2html(
|
||||
markdown,
|
||||
basepath,
|
||||
partial(self._update_preview, markdown_view),
|
||||
resources
|
||||
resources,
|
||||
viewport_width,
|
||||
)
|
||||
|
||||
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}))
|
||||
])
|
||||
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
|
||||
|
||||
@ -3,5 +3,13 @@
|
||||
{
|
||||
"caption": "MarkdownLivePreview: Open Preview",
|
||||
"command": "open_markdown_preview"
|
||||
},
|
||||
{
|
||||
"caption": "MarkdownLivePreview: Open Settings",
|
||||
"command": "edit_settings", "args":
|
||||
{
|
||||
"base_file": "${packages}/MarkdownLivePreview/MarkdownLivePreview.sublime-settings",
|
||||
"default": "{\n\t$0\n}\n"
|
||||
},
|
||||
}
|
||||
]
|
||||
4
MarkdownLivePreview.sublime-settings
Normal file
4
MarkdownLivePreview.sublime-settings
Normal file
@ -0,0 +1,4 @@
|
||||
{
|
||||
// minimum number of milliseconds to wait before updating the preview again
|
||||
"delay_between_updates": 100
|
||||
}
|
||||
47
README.md
47
README.md
@ -7,12 +7,59 @@ No dependencies!
|
||||
|
||||
It's available on package control!
|
||||
|
||||
## Setting a keybinding
|
||||
|
||||
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:
|
||||
|
||||
```json
|
||||
{
|
||||
"keys": ["alt+m"],
|
||||
"command": "open_markdown_preview"
|
||||
}
|
||||
```
|
||||
|
||||
## How to contribute
|
||||
|
||||
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.
|
||||
|
||||
But if you wanna contribute just to say thanks, and don't really know what you
|
||||
could be working on, then there are a bunch of `FIXME`s all over this package.
|
||||
Just pick one and fix it :-)
|
||||
|
||||
```
|
||||
$ git clone https://github.com/math2001/MarkdownLivePreview
|
||||
$ cd MarkdownLivePreview
|
||||
$ grep -R FIXME
|
||||
```
|
||||
|
||||
### Hack it!
|
||||
|
||||
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!
|
||||
|
||||
### Known limitations
|
||||
|
||||
#### Numbered lists are rendered as unordered lists
|
||||
|
||||
```md
|
||||
1. first
|
||||
2. second
|
||||
3. third
|
||||
```
|
||||
|
||||
will be previewed the exact same way as
|
||||
|
||||
```md
|
||||
- first
|
||||
- second
|
||||
- third
|
||||
```
|
||||
|
||||
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...
|
||||
|
||||
1253
lib/markdown2.py
1253
lib/markdown2.py
File diff suppressed because it is too large
Load Diff
@ -1,18 +1,52 @@
|
||||
# hello world
|
||||
# Why?
|
||||
Why do you want to use fancy symbols in your standard monospace font? Obviously to have a fancy prompt like mine :-)
|
||||
|
||||
This is a *test*. Some inline `[2]code()`.
|
||||
<!--  -->
|
||||
|
||||
what the hell...
|
||||
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
|
||||
|
||||
```python
|
||||
import this
|
||||
Heavily inspired by <https://github.com/Lokaltog/vim-powerline> and the relative patch script from **Kim Silkebækken** (kim.silkebaekken+vim@gmail.com)
|
||||
|
||||
if input("answer yes") != 'yes':
|
||||
print("Really?")
|
||||
```
|
||||
## 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)
|
||||
|
||||
this flickering is really annoying...
|
||||
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)
|
||||
|
||||
It looks like it's gone... Oh wait nah, it's still here...
|
||||
## 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`).
|
||||
|
||||
This should still be working, and it is!
|
||||
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)
|
||||
|
||||
206
markdown2html.py
206
markdown2html.py
@ -1,28 +1,31 @@
|
||||
import copy
|
||||
""" 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 lru_cache, partial
|
||||
from functools import partial
|
||||
|
||||
from .lib.markdown2 import Markdown
|
||||
|
||||
__all__ = ('markdown2html', )
|
||||
__all__ = ("markdown2html",)
|
||||
|
||||
markdowner = Markdown(extras=['fenced-code-blocks'])
|
||||
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)
|
||||
|
||||
images_cache = {}
|
||||
|
||||
class LoadingError(Exception):
|
||||
pass
|
||||
|
||||
def markdown2html(markdown, basepath, re_render, resources):
|
||||
def markdown2html(markdown, basepath, re_render, resources, viewport_width):
|
||||
""" 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
|
||||
@ -31,89 +34,188 @@ def markdown2html(markdown, basepath, re_render, resources):
|
||||
html = markdowner.convert(markdown)
|
||||
|
||||
soup = bs4.BeautifulSoup(html, "html.parser")
|
||||
for img_element in soup.find_all('img'):
|
||||
src = img_element['src']
|
||||
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/'):
|
||||
if src.startswith("data:image/"):
|
||||
continue
|
||||
|
||||
if src.startswith('http://') or src.startswith('https://'):
|
||||
if src.startswith("http://") or src.startswith("https://"):
|
||||
path = src
|
||||
elif src.startswith('file://'):
|
||||
path = src[len('file://'):]
|
||||
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']
|
||||
base64, (width, height) = get_base64_image(path, re_render, resources)
|
||||
|
||||
img_element['src'] = base64
|
||||
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)):
|
||||
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'):
|
||||
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 />')
|
||||
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 />')
|
||||
return "<style>\n{}\n</style>\n\n{}".format(resources["stylesheet"], soup).replace(
|
||||
"<br/>", "<br />"
|
||||
)
|
||||
|
||||
def get_base64_image(path, re_render):
|
||||
images_cache = {}
|
||||
images_loading = []
|
||||
|
||||
def callback(url, future):
|
||||
# this is "safe" to do because callback is called in the same thread as
|
||||
# add_done_callback:
|
||||
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
|
||||
images_cache[url] = future.result()
|
||||
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.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()
|
||||
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]
|
||||
|
||||
# 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:
|
||||
|
||||
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(conn.read()).decode('utf-8')
|
||||
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,
|
||||
)
|
||||
|
||||
File diff suppressed because one or more lines are too long
@ -1,9 +1,34 @@
|
||||
""" A small script to convert the images into base64 data """
|
||||
|
||||
import struct
|
||||
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()))
|
||||
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")
|
||||
|
||||
File diff suppressed because one or more lines are too long
Binary file not shown.
|
Before Width: | Height: | Size: 953 B After Width: | Height: | Size: 7.2 KiB |
BIN
resources/loading.xcf
Normal file
BIN
resources/loading.xcf
Normal file
Binary file not shown.
@ -15,8 +15,9 @@ blockquote {
|
||||
}
|
||||
|
||||
pre {
|
||||
display: block;
|
||||
background-color: var(--very-light-bg);
|
||||
display: block;
|
||||
background-color: var(--very-light-bg);
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
code {
|
||||
|
||||
BIN
resources/transparent-loading.png
Normal file
BIN
resources/transparent-loading.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 953 B |
23
utils.py
23
utils.py
@ -1,23 +0,0 @@
|
||||
# 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
|
||||
Reference in New Issue
Block a user