generated from itdominator/Python-With-Gtk-Template
Make master branch jus ino about the relese branches
This commit is contained in:
parent
30cf03b76f
commit
ce8361238f
13
README.md
13
README.md
@ -7,13 +7,18 @@ A Python and Gtk+ quasi-IDE.
|
||||
* pyxdg
|
||||
|
||||
### Note
|
||||
Currently very much a WIP effort.
|
||||
This is currently very much a WIP effort with two branches to take note of. The first uses GtkSource.View widget to handle text edits and display. The second uses the Gtk Webkit2 widget along with Ace Editor to handle text editing. The first is more traditional in visual design and editing. While the second is more like Vi, Vim, NeoVim.
|
||||
|
||||
* Move respetive sub folder content under user_config to the same places in Linux. Though, /user/share/newton can go to ~/.config folder if prefered.
|
||||
* In additiion, place the plugins folder in the same app folder you moved to /usr/share/newton or ~/.config/newton .
|
||||
* release/newton-with-gtksource
|
||||
* release/newton-with-webkit2
|
||||
|
||||
# Images
|
||||
* GtkSource.View
|
||||
![1 Newton default view. ](images/pic1.png)
|
||||
![2 Newton search and replace plus menu shown. ](images/pic2.png)
|
||||
![3 Newton displaying inline colors. ](images/pic3.png)
|
||||
![4 Newton as transparent with youtube playing below it. ](images/pic4.png)
|
||||
![4 Newton as transparent with youtube playing below it. ](images/pic4.png)
|
||||
|
||||
* Gtk Webkit2 + Ace Editor
|
||||
![5 Newton default view. ](images/pic5.png)
|
||||
![6 Bufers list. ](images/pic6.png)
|
||||
|
BIN
images/pic5.png
Normal file
BIN
images/pic5.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 826 KiB |
BIN
images/pic6.png
Normal file
BIN
images/pic6.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 517 KiB |
@ -1,2 +0,0 @@
|
||||
### Note
|
||||
Copy the example and rename it to your desired name. The Main class and passed in arguments are required. You don't necessarily need to use the passed in socket_id or event_system.
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Autopairs",
|
||||
"author": "ITDominator",
|
||||
"credit": "Hamad Al Marri",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"pass_events": "true"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,140 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
import threading
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Autopairs" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
|
||||
self.chars = {
|
||||
"quotedbl": "\"",
|
||||
"apostrophe": "'",
|
||||
"parenleft": "(",
|
||||
"bracketleft": "[",
|
||||
"braceleft": "{",
|
||||
"less": "<",
|
||||
"grave": "`",
|
||||
}
|
||||
|
||||
self.close = {
|
||||
"\"": "\"",
|
||||
"'": "'",
|
||||
"(": ")",
|
||||
"[": "]",
|
||||
"{": "}",
|
||||
"<": ">",
|
||||
"`": "`",
|
||||
}
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
...
|
||||
|
||||
def run(self):
|
||||
...
|
||||
|
||||
def subscribe_to_events(self):
|
||||
self._event_system.subscribe("set_active_src_view", self._set_active_src_view)
|
||||
self._event_system.subscribe("autopairs", self._autopairs)
|
||||
|
||||
def _set_active_src_view(self, source_view):
|
||||
self._active_src_view = source_view
|
||||
self._buffer = self._active_src_view.get_buffer()
|
||||
self._tag_table = self._buffer.get_tag_table()
|
||||
|
||||
def _autopairs(self, keyval_name, ctrl, alt, shift):
|
||||
if keyval_name in self.chars:
|
||||
return self.text_insert(self._buffer, keyval_name)
|
||||
|
||||
# NOTE: All of below to EOF, lovingly taken from Hamad Al Marri's Gamma
|
||||
# text editor. I did do some cleanup of comments but otherwise pretty
|
||||
# much the same code just fitted to my plugin architecture.
|
||||
# Link: https://gitlab.com/hamadmarri/gamma-text-editor
|
||||
def text_insert(self, buffer, text):
|
||||
selection = buffer.get_selection_bounds()
|
||||
if selection == ():
|
||||
return self.add_close(buffer, text, )
|
||||
else:
|
||||
return self.add_enclose(buffer, text, selection)
|
||||
|
||||
def add_close(self, buffer, text):
|
||||
text = self.chars[text]
|
||||
text += self.close[text]
|
||||
|
||||
position = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
|
||||
c = position.get_char()
|
||||
if not c in (" ", "", ";", ":", "\t", ",", ".", "\n", "\r") \
|
||||
and not c in list(self.close.values()):
|
||||
return False
|
||||
|
||||
buffer.insert(position, text)
|
||||
|
||||
position = buffer.get_iter_at_mark(buffer.get_insert())
|
||||
position.backward_char()
|
||||
buffer.place_cursor(position)
|
||||
|
||||
return True
|
||||
|
||||
def add_enclose(self, buffer, text, selection):
|
||||
(start, end) = selection
|
||||
selected = buffer.get_text(start, end, False)
|
||||
if len(selected) <= 3 and selected in ("<", ">", ">>>"
|
||||
"<<", ">>",
|
||||
"\"", "'", "`",
|
||||
"(", ")",
|
||||
"[", "]",
|
||||
"{", "}",
|
||||
"=", "==",
|
||||
"!=", "==="):
|
||||
return False
|
||||
|
||||
start_mark = buffer.create_mark("startclose", start, False)
|
||||
end_mark = buffer.create_mark("endclose", end, False)
|
||||
|
||||
buffer.begin_user_action()
|
||||
|
||||
t = self.chars[text]
|
||||
buffer.insert(start, t)
|
||||
end = buffer.get_iter_at_mark(end_mark)
|
||||
t = self.close[t]
|
||||
buffer.insert(end, t)
|
||||
|
||||
start = buffer.get_iter_at_mark(start_mark)
|
||||
end = buffer.get_iter_at_mark(end_mark)
|
||||
end.backward_char()
|
||||
buffer.select_range(start, end)
|
||||
|
||||
buffer.end_user_action()
|
||||
|
||||
return True
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,107 +0,0 @@
|
||||
# Python imports
|
||||
import colorsys
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class ColorConverterMixin:
|
||||
# NOTE: HSV HSL, and Hex Alpha parsing are available in Gtk 4.0- not lower.
|
||||
# So, for compatability we're gunna convert to rgba string ourselves...
|
||||
def get_color_text(self, buffer, start, end):
|
||||
text = buffer.get_text(start, end, include_hidden_chars = False)
|
||||
|
||||
try:
|
||||
if "hsl" in text:
|
||||
text = self.hsl_to_rgb(text)
|
||||
|
||||
if "hsv" in text:
|
||||
text = self.hsv_to_rgb(text)
|
||||
|
||||
if "#" == text[0]:
|
||||
hex = text[1:]
|
||||
size = len(hex)
|
||||
if size in [4, 8, 16]:
|
||||
rgba = self.hex_to_rgba(hex, size)
|
||||
print(rgba)
|
||||
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
return text
|
||||
|
||||
def hex_to_rgba(self, hex, size):
|
||||
rgba = []
|
||||
slots = None
|
||||
step = 2
|
||||
bytes = 16
|
||||
|
||||
if size == 4: # NOTE: RGBA
|
||||
step = 1
|
||||
slots = (0, 1, 2, 3)
|
||||
|
||||
if size == 6: # NOTE: RR GG BB
|
||||
slots = (0, 2, 4)
|
||||
|
||||
if size == 8: # NOTE: RR GG BB AA
|
||||
step = 2
|
||||
slots = (0, 2, 4, 6)
|
||||
|
||||
if size == 16: # NOTE: RRRR GGGG BBBB AAAA
|
||||
step = 4
|
||||
slots = (0, 4, 8, 12)
|
||||
|
||||
for i in slots:
|
||||
v = int(hex[i : i + step], bytes)
|
||||
rgba.append(v)
|
||||
|
||||
|
||||
rgb_sub = ','.join(map(str, tuple(rgba)))
|
||||
|
||||
return f"rgba({rgb_sub})"
|
||||
|
||||
# return tuple(rgba)
|
||||
|
||||
|
||||
|
||||
def hsl_to_rgb(self, text):
|
||||
_h, _s , _l = text.replace("hsl", "") \
|
||||
.replace("deg", "") \
|
||||
.replace("(", "") \
|
||||
.replace(")", "") \
|
||||
.replace("%", "") \
|
||||
.replace(" ", "") \
|
||||
.split(",")
|
||||
|
||||
h = None
|
||||
s = None
|
||||
l = None
|
||||
|
||||
h, s , l = int(_h) / 360, float(_s) / 100, float(_l) / 100
|
||||
|
||||
rgb = tuple(round(i * 255) for i in colorsys.hls_to_rgb(h, l, s))
|
||||
rgb_sub = ','.join(map(str, rgb))
|
||||
|
||||
return f"rgb({rgb_sub})"
|
||||
|
||||
|
||||
def hsv_to_rgb(self, text):
|
||||
_h, _s , _v = text.replace("hsv", "") \
|
||||
.replace("deg", "") \
|
||||
.replace("(", "") \
|
||||
.replace(")", "") \
|
||||
.replace("%", "") \
|
||||
.replace(" ", "") \
|
||||
.split(",")
|
||||
|
||||
h = None
|
||||
s = None
|
||||
v = None
|
||||
|
||||
h, s , v = int(_h) / 360, float(_s) / 100, float(_v) / 100
|
||||
|
||||
rgb = tuple(round(i * 255) for i in colorsys.hsv_to_rgb(h,s,v))
|
||||
rgb_sub = ','.join(map(str, rgb))
|
||||
|
||||
return f"rgb({rgb_sub})"
|
@ -1,11 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Colorize",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"pass_events": "true"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,228 +0,0 @@
|
||||
# Python imports
|
||||
import random
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
from .color_converter_mixin import ColorConverterMixin
|
||||
|
||||
|
||||
|
||||
class Plugin(ColorConverterMixin, PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Colorize" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self.tag_stub_name = "colorize_tag"
|
||||
self._buffer = None
|
||||
|
||||
|
||||
def run(self):
|
||||
...
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
...
|
||||
|
||||
def subscribe_to_events(self):
|
||||
self._event_system.subscribe("set_active_src_view", self._set_active_src_view)
|
||||
self._event_system.subscribe("buffer_changed_first_load", self._buffer_changed_first_load)
|
||||
self._event_system.subscribe("buffer_changed", self._buffer_changed)
|
||||
|
||||
|
||||
def _set_active_src_view(self, source_view):
|
||||
self._active_src_view = source_view
|
||||
|
||||
|
||||
def _buffer_changed_first_load(self, buffer):
|
||||
self._buffer = buffer
|
||||
self._do_colorize(buffer)
|
||||
|
||||
def _buffer_changed(self, buffer):
|
||||
self._event_system.emit("pause_event_processing")
|
||||
self._handle_colorize(buffer)
|
||||
self._event_system.emit("resume_event_processing")
|
||||
|
||||
def _handle_colorize(self, buffer):
|
||||
self._buffer = buffer
|
||||
tag_table = buffer.get_tag_table()
|
||||
mark = buffer.get_insert()
|
||||
start = None
|
||||
end = buffer.get_iter_at_mark(mark)
|
||||
|
||||
i = 0
|
||||
walker_iter = end.copy()
|
||||
working_tag = self.find_working_tag(walker_iter, i)
|
||||
if working_tag:
|
||||
start = self.find_start_range(walker_iter, working_tag)
|
||||
|
||||
self.find_end_range(end, working_tag)
|
||||
buffer.remove_tag(working_tag, start, end)
|
||||
else:
|
||||
start = self.traverse_backward_25_or_less(walker_iter)
|
||||
self.traverse_forward_25_or_less(end)
|
||||
|
||||
self._do_colorize(buffer, start, end)
|
||||
|
||||
|
||||
|
||||
def find_working_tag(self, walker_iter, i):
|
||||
tags = walker_iter.get_tags()
|
||||
for tag in tags:
|
||||
if tag.props.name and self.tag_stub_name in tag.props.name:
|
||||
return tag
|
||||
|
||||
res = walker_iter.backward_char()
|
||||
|
||||
if not res: return
|
||||
if i > 25: return
|
||||
return self.find_working_tag(walker_iter, i + 1)
|
||||
|
||||
def find_start_range(self, walker_iter, working_tag):
|
||||
tags = walker_iter.get_tags()
|
||||
for tag in tags:
|
||||
if tag.props.name and working_tag.props.name in tag.props.name:
|
||||
res = walker_iter.backward_char()
|
||||
if res:
|
||||
self.find_start_range(walker_iter, working_tag)
|
||||
|
||||
return walker_iter
|
||||
|
||||
def find_end_range(self, end, working_tag):
|
||||
tags = end.get_tags()
|
||||
for tag in tags:
|
||||
if tag.props.name and working_tag.props.name in tag.props.name:
|
||||
res = end.forward_char()
|
||||
if res:
|
||||
self.find_end_range(end, working_tag)
|
||||
|
||||
def traverse_backward_25_or_less(self, walker_iter):
|
||||
i = 1
|
||||
while i <= 25:
|
||||
res = walker_iter.backward_char()
|
||||
if not res: break
|
||||
i += 1
|
||||
|
||||
def traverse_forward_25_or_less(self, end):
|
||||
i = 1
|
||||
while i <= 25:
|
||||
res = end.forward_char()
|
||||
if not res: break
|
||||
i += 1
|
||||
|
||||
def _do_colorize(self, buffer = None, start_itr = None, end_itr = None):
|
||||
# rgb(a), hsl, hsv
|
||||
results = self.finalize_non_hex_matches( self.collect_preliminary_results(buffer, start_itr, end_itr) )
|
||||
self.process_results(buffer, results)
|
||||
|
||||
# hex color search
|
||||
results = self.finalize_hex_matches( self.collect_preliminary_hex_results(buffer, start_itr, end_itr) )
|
||||
self.process_results(buffer, results)
|
||||
|
||||
|
||||
def collect_preliminary_results(self, buffer = None, start_itr = None, end_itr = None):
|
||||
if not buffer: return []
|
||||
|
||||
if not start_itr:
|
||||
start_itr = buffer.get_start_iter()
|
||||
|
||||
results1 = self.search(start_itr, end_itr, "rgb")
|
||||
results2 = self.search(start_itr, end_itr, "hsl")
|
||||
results3 = self.search(start_itr, end_itr, "hsv")
|
||||
|
||||
return results1 + results2 + results3
|
||||
|
||||
def collect_preliminary_hex_results(self, buffer = None, start_itr = None, end_itr = None):
|
||||
if not buffer: return []
|
||||
|
||||
if not start_itr:
|
||||
start_itr = buffer.get_start_iter()
|
||||
|
||||
results1 = self.search(start_itr, end_itr, "#")
|
||||
|
||||
return results1
|
||||
|
||||
def search(self, start_itr = None, end_itr = None, query = None):
|
||||
if not start_itr or not query: return None, None
|
||||
|
||||
results = []
|
||||
flags = Gtk.TextSearchFlags.VISIBLE_ONLY | Gtk.TextSearchFlags.TEXT_ONLY
|
||||
while True:
|
||||
result = start_itr.forward_search(query, flags, end_itr)
|
||||
if not result: break
|
||||
|
||||
results.append(result)
|
||||
start_itr = result[1]
|
||||
|
||||
return results
|
||||
|
||||
def finalize_non_hex_matches(self, result_hits: [] = []):
|
||||
results = []
|
||||
|
||||
for start, end in result_hits:
|
||||
if end.get_char() == "a":
|
||||
end.forward_char()
|
||||
|
||||
if end.get_char() != "(":
|
||||
continue
|
||||
|
||||
end.forward_chars(21)
|
||||
if end.get_char() == ")":
|
||||
end.forward_char()
|
||||
results.append([start, end])
|
||||
continue
|
||||
|
||||
while end.get_char() != "(":
|
||||
if end.get_char() == ")":
|
||||
end.forward_char()
|
||||
results.append([start, end])
|
||||
break
|
||||
|
||||
end.forward_chars(-1)
|
||||
|
||||
return results
|
||||
|
||||
def finalize_hex_matches(self, result_hits: [] = []):
|
||||
results = []
|
||||
|
||||
for start, end in result_hits:
|
||||
i = 0
|
||||
_ch = end.get_char()
|
||||
ch = ord(end.get_char()) if _ch else -1
|
||||
|
||||
while ((ch >= 48 and ch <= 57) or (ch >= 65 and ch <= 70) or (ch >= 97 and ch <= 102)):
|
||||
if i > 16: break
|
||||
|
||||
i += 1
|
||||
end.forward_char()
|
||||
_ch = end.get_char()
|
||||
ch = ord(end.get_char()) if _ch else -1
|
||||
|
||||
if i in [3, 4, 6, 8, 9, 12, 16]:
|
||||
results.append([start, end])
|
||||
|
||||
return results
|
||||
|
||||
def process_results(self, buffer, results):
|
||||
for start, end in results:
|
||||
text = self.get_color_text(buffer, start, end)
|
||||
color = Gdk.RGBA()
|
||||
|
||||
if color.parse(text):
|
||||
tag = self.get_colorized_tag(buffer, text, color)
|
||||
buffer.apply_tag(tag, start, end)
|
||||
|
||||
def get_colorized_tag(self, buffer, tag, color: Gdk.RGBA):
|
||||
tag_table = buffer.get_tag_table()
|
||||
colorize_tag = f"{self.tag_stub_name}_{tag}"
|
||||
search_tag = tag_table.lookup(colorize_tag)
|
||||
if not search_tag:
|
||||
search_tag = buffer.create_tag(colorize_tag, background_rgba = color)
|
||||
|
||||
return search_tag
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,66 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class AddCommentMixin:
|
||||
def add_comment_characters(self, buffer, start_tag, end_tag, start, end, deselect, oldPos):
|
||||
smark = buffer.create_mark("start", start, False)
|
||||
imark = buffer.create_mark("iter", start, False)
|
||||
emark = buffer.create_mark("end", end, False)
|
||||
number_lines = end.get_line() - start.get_line() + 1
|
||||
comment_pos_iter = None
|
||||
count = 0
|
||||
|
||||
buffer.begin_user_action()
|
||||
|
||||
for i in range(0, number_lines):
|
||||
iter = buffer.get_iter_at_mark(imark)
|
||||
if not comment_pos_iter:
|
||||
(comment_pos_iter, count) = self.discard_white_spaces(iter)
|
||||
|
||||
if self.is_commented(comment_pos_iter, start_tag):
|
||||
new_code = self.remove_comment_characters(buffer, start_tag, end_tag, start, end)
|
||||
return
|
||||
else:
|
||||
comment_pos_iter = iter
|
||||
for i in range(count):
|
||||
c = iter.get_char()
|
||||
if not c in (" ", "\t"):
|
||||
break
|
||||
|
||||
iter.forward_char()
|
||||
|
||||
buffer.insert(comment_pos_iter, start_tag)
|
||||
buffer.insert(comment_pos_iter, " ")
|
||||
|
||||
if end_tag:
|
||||
if i != number_lines -1:
|
||||
iter = buffer.get_iter_at_mark(imark)
|
||||
iter.forward_to_line_end()
|
||||
buffer.insert(iter, end_tag)
|
||||
else:
|
||||
iter = buffer.get_iter_at_mark(emark)
|
||||
buffer.insert(iter, end_tag)
|
||||
|
||||
iter = buffer.get_iter_at_mark(imark)
|
||||
iter.forward_line()
|
||||
buffer.delete_mark(imark)
|
||||
imark = buffer.create_mark("iter", iter, True)
|
||||
|
||||
buffer.end_user_action()
|
||||
|
||||
buffer.delete_mark(imark)
|
||||
new_start = buffer.get_iter_at_mark(smark)
|
||||
new_end = buffer.get_iter_at_mark(emark)
|
||||
|
||||
buffer.select_range(new_start, new_end)
|
||||
buffer.delete_mark(smark)
|
||||
buffer.delete_mark(emark)
|
||||
|
||||
if deselect:
|
||||
oldPosIter = buffer.get_iter_at_offset(oldPos + 2)
|
||||
buffer.place_cursor(oldPosIter)
|
@ -1,30 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class CodeCommentTags:
|
||||
def get_comment_tags(self, lang):
|
||||
(s, e) = self.get_line_comment_tags(lang)
|
||||
if (s, e) == (None, None):
|
||||
(s, e) = self.get_block_comment_tags(lang)
|
||||
|
||||
return (s, e)
|
||||
|
||||
def get_block_comment_tags(self, lang):
|
||||
start_tag = lang.get_metadata('block-comment-start')
|
||||
end_tag = lang.get_metadata('block-comment-end')
|
||||
if start_tag and end_tag:
|
||||
return (start_tag, end_tag)
|
||||
|
||||
return (None, None)
|
||||
|
||||
def get_line_comment_tags(self, lang):
|
||||
start_tag = lang.get_metadata('line-comment-start')
|
||||
if start_tag:
|
||||
return (start_tag, None)
|
||||
|
||||
return (None, None)
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Commentzar",
|
||||
"author": "ITDominator",
|
||||
"credit": "Hamad Al Marri",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"pass_events": "true",
|
||||
"bind_keys": ["Commentzar||keyboard_tggl_comment:<Control>slash"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,118 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
from .codecomment_tags import CodeCommentTags
|
||||
from .remove_comment_mixin import RemoveCommentMixin
|
||||
from .add_comment_mixin import AddCommentMixin
|
||||
|
||||
|
||||
|
||||
class Plugin(AddCommentMixin, RemoveCommentMixin, CodeCommentTags, PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Commentzar" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
...
|
||||
|
||||
def run(self):
|
||||
...
|
||||
|
||||
def subscribe_to_events(self):
|
||||
self._event_system.subscribe("keyboard_tggl_comment", self._keyboard_tggl_comment)
|
||||
self._event_system.subscribe("set_active_src_view", self._set_active_src_view)
|
||||
|
||||
def _set_active_src_view(self, source_view):
|
||||
self._active_src_view = source_view
|
||||
self._buffer = self._active_src_view.get_buffer()
|
||||
self._tag_table = self._buffer.get_tag_table()
|
||||
|
||||
|
||||
def _keyboard_tggl_comment(self):
|
||||
buffer = self._buffer
|
||||
lang = buffer.get_language()
|
||||
if lang is None:
|
||||
return
|
||||
|
||||
(start_tag, end_tag) = self.get_comment_tags(lang)
|
||||
if not start_tag and not end_tag:
|
||||
return
|
||||
|
||||
sel = buffer.get_selection_bounds()
|
||||
currentPosMark = buffer.get_insert()
|
||||
oldPos = 0
|
||||
|
||||
# if user selected chars or multilines
|
||||
if sel != ():
|
||||
deselect = False
|
||||
(start, end) = sel
|
||||
if not start.starts_line():
|
||||
start.set_line_offset(0)
|
||||
if not end.ends_line():
|
||||
end.forward_to_line_end()
|
||||
else:
|
||||
deselect = True
|
||||
start = buffer.get_iter_at_mark(currentPosMark)
|
||||
oldPos = buffer.get_iter_at_mark(currentPosMark).get_offset()
|
||||
start.set_line_offset(0)
|
||||
end = start.copy()
|
||||
|
||||
if not end.ends_line():
|
||||
end.forward_to_line_end()
|
||||
|
||||
if start.get_offset() == end.get_offset():
|
||||
buffer.begin_user_action()
|
||||
buffer.insert(start, start_tag)
|
||||
buffer.insert(start, " ")
|
||||
buffer.end_user_action()
|
||||
return
|
||||
|
||||
self._event_system.emit("pause_event_processing")
|
||||
new_code = self.add_comment_characters(buffer, start_tag, end_tag, start, end, deselect, oldPos)
|
||||
self._event_system.emit("resume_event_processing")
|
||||
|
||||
def discard_white_spaces(self, iter):
|
||||
count = 0
|
||||
while not iter.ends_line():
|
||||
c = iter.get_char()
|
||||
if not c in (" ", "\t"):
|
||||
return (iter, count)
|
||||
|
||||
iter.forward_char()
|
||||
count += 1
|
||||
|
||||
return (iter, 0)
|
||||
|
||||
def is_commented(self, comment_pos_iter, start_tag):
|
||||
head_iter = comment_pos_iter.copy()
|
||||
self.forward_tag(head_iter, start_tag)
|
||||
s = comment_pos_iter.get_slice(head_iter)
|
||||
if s == start_tag:
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def forward_tag(self, iter, tag):
|
||||
iter.forward_chars(len(tag))
|
||||
|
||||
def backward_tag(self, iter, tag):
|
||||
iter.backward_chars(len(tag))
|
||||
|
||||
def get_tag_position_in_line(self, tag, head_iter, iter):
|
||||
while not iter.ends_line():
|
||||
s = iter.get_slice(head_iter)
|
||||
if s == tag:
|
||||
return True
|
||||
else:
|
||||
head_iter.forward_char()
|
||||
iter.forward_char()
|
||||
return False
|
@ -1,49 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class RemoveCommentMixin:
|
||||
def remove_comment_characters(self, buffer, start_tag, end_tag, start, end):
|
||||
smark = buffer.create_mark("start", start, False)
|
||||
emark = buffer.create_mark("end", end, False)
|
||||
number_lines = end.get_line() - start.get_line() + 1
|
||||
iter = start.copy()
|
||||
head_iter = iter.copy()
|
||||
self.forward_tag(head_iter, start_tag)
|
||||
|
||||
buffer.begin_user_action()
|
||||
|
||||
for i in range(0, number_lines):
|
||||
if self.get_tag_position_in_line(start_tag, head_iter, iter):
|
||||
dmark = buffer.create_mark("delete", iter, False)
|
||||
buffer.delete(iter, head_iter)
|
||||
|
||||
space_iter = head_iter.copy()
|
||||
space_iter.forward_char()
|
||||
s = head_iter.get_slice(space_iter)
|
||||
if s == " ":
|
||||
buffer.delete(head_iter, space_iter)
|
||||
|
||||
if end_tag:
|
||||
iter = buffer.get_iter_at_mark(dmark)
|
||||
head_iter = iter.copy()
|
||||
self.forward_tag(head_iter, end_tag)
|
||||
if self.get_tag_position_in_line(end_tag, head_iter, iter):
|
||||
buffer.delete(iter, head_iter)
|
||||
buffer.delete_mark(dmark)
|
||||
|
||||
iter = buffer.get_iter_at_mark(smark)
|
||||
iter.forward_line()
|
||||
buffer.delete_mark(smark)
|
||||
head_iter = iter.copy()
|
||||
self.forward_tag(head_iter, start_tag)
|
||||
smark = buffer.create_mark("iter", iter, True)
|
||||
|
||||
buffer.end_user_action()
|
||||
|
||||
buffer.delete_mark(smark)
|
||||
buffer.delete_mark(emark)
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,201 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class Capabilities:
|
||||
data = {
|
||||
"textDocument": {
|
||||
"codeAction": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"codeLens": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"colorProvider": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"completion": {
|
||||
"completionItem": {
|
||||
"commitCharactersSupport": True,
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"snippetSupport": True
|
||||
},
|
||||
"completionItemKind": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25
|
||||
]
|
||||
},
|
||||
"contextSupport": True,
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"definition": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"documentHighlight": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"documentLink": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"documentSymbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26
|
||||
]
|
||||
}
|
||||
},
|
||||
"formatting": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"hover": {
|
||||
"contentFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
],
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"implementation": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"onTypeFormatting": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"publishDiagnostics": {
|
||||
"relatedInformation": True
|
||||
},
|
||||
"rangeFormatting": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"references": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"rename": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"signatureHelp": {
|
||||
"dynamicRegistration": True,
|
||||
"signatureInformation": {
|
||||
"documentationFormat": [
|
||||
"markdown",
|
||||
"plaintext"
|
||||
]
|
||||
}
|
||||
},
|
||||
"synchronization": {
|
||||
"didSave": True,
|
||||
"dynamicRegistration": True,
|
||||
"willSave": True,
|
||||
"willSaveWaitUntil": True
|
||||
},
|
||||
"typeDefinition": {
|
||||
"dynamicRegistration": True
|
||||
}
|
||||
},
|
||||
"workspace": {
|
||||
"applyEdit": True,
|
||||
"configuration": True,
|
||||
"didChangeConfiguration": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"didChangeWatchedFiles": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"executeCommand": {
|
||||
"dynamicRegistration": True
|
||||
},
|
||||
"symbol": {
|
||||
"dynamicRegistration": True,
|
||||
"symbolKind": {
|
||||
"valueSet": [
|
||||
1,
|
||||
2,
|
||||
3,
|
||||
4,
|
||||
5,
|
||||
6,
|
||||
7,
|
||||
8,
|
||||
9,
|
||||
10,
|
||||
11,
|
||||
12,
|
||||
13,
|
||||
14,
|
||||
15,
|
||||
16,
|
||||
17,
|
||||
18,
|
||||
19,
|
||||
20,
|
||||
21,
|
||||
22,
|
||||
23,
|
||||
24,
|
||||
25,
|
||||
26
|
||||
]
|
||||
}
|
||||
},
|
||||
"workspaceEdit": {
|
||||
"documentChanges": True
|
||||
},
|
||||
"workspaceFolders": True
|
||||
}
|
||||
}
|
@ -1,179 +0,0 @@
|
||||
# Python imports
|
||||
import subprocess
|
||||
import threading
|
||||
|
||||
# Lib imports
|
||||
from . import pylspclient
|
||||
|
||||
# Application imports
|
||||
from .capabilities import Capabilities
|
||||
|
||||
|
||||
|
||||
class ReadPipe(threading.Thread):
|
||||
def __init__(self, pipe):
|
||||
threading.Thread.__init__(self)
|
||||
self.pipe = pipe
|
||||
|
||||
def run(self):
|
||||
line = self.pipe.readline().decode('utf-8')
|
||||
while line:
|
||||
line = self.pipe.readline().decode('utf-8')
|
||||
|
||||
|
||||
|
||||
class LSPController:
|
||||
def __init__(self, lsp_servers_config = {}):
|
||||
super().__init__()
|
||||
|
||||
self.lsp_servers_config = lsp_servers_config
|
||||
self.lsp_clients = {}
|
||||
|
||||
def _blame(self, response):
|
||||
for d in response['diagnostics']:
|
||||
if d['severity'] == 1:
|
||||
print(f"An error occurs in {response['uri']} at {d['range']}:")
|
||||
print(f"\t[{d['source']}] {d['message']}")
|
||||
|
||||
def _shutting_down(self):
|
||||
keys = self.lsp_clients.keys()
|
||||
for key in keys:
|
||||
print(f"LSP Server: ( {key} ) Shutting Down...")
|
||||
self.lsp_clients[key].shutdown()
|
||||
self.lsp_clients[key].exit()
|
||||
|
||||
def _generate_client(self, language = "", server_proc = None):
|
||||
if not language or not server_proc: return False
|
||||
|
||||
json_rpc_endpoint = pylspclient.JsonRpcEndpoint(server_proc.stdin, server_proc.stdout)
|
||||
|
||||
callbacks = {
|
||||
"window/showMessage": print,
|
||||
"textDocument/symbolStatus": print,
|
||||
"textDocument/publishDiagnostics": self._blame,
|
||||
}
|
||||
|
||||
lsp_endpoint = pylspclient.LspEndpoint(json_rpc_endpoint, notify_callbacks = callbacks)
|
||||
lsp_client = pylspclient.LspClient(lsp_endpoint)
|
||||
|
||||
self.lsp_clients[language] = lsp_client
|
||||
return lsp_client
|
||||
|
||||
def create_client(self, language = "", server_proc = None, initialization_options = None):
|
||||
if not language or not server_proc: return False
|
||||
|
||||
root_path = None
|
||||
root_uri = 'file:///home/abaddon/Coding/Projects/Active/C_n_CPP_Projects/gtk/Newton/src/'
|
||||
workspace_folders = [{'name': 'python-lsp', 'uri': root_uri}]
|
||||
|
||||
lsp_client = self._generate_client(language, server_proc)
|
||||
lsp_client.initialize(
|
||||
processId = server_proc.pid, \
|
||||
rootPath = root_path, \
|
||||
rootUri = root_uri, \
|
||||
initializationOptions = initialization_options, \
|
||||
capabilities = Capabilities.data, \
|
||||
trace = "off", \
|
||||
workspaceFolders = workspace_folders
|
||||
)
|
||||
|
||||
lsp_client.initialized()
|
||||
|
||||
return True
|
||||
|
||||
def create_lsp_server(self, server_command: [] = []):
|
||||
if not server_command: return None
|
||||
|
||||
server_proc = subprocess.Popen(server_command, stdin = subprocess.PIPE, stdout = subprocess.PIPE, stderr = subprocess.PIPE)
|
||||
read_pipe = ReadPipe(server_proc.stderr)
|
||||
read_pipe.start()
|
||||
|
||||
return server_proc
|
||||
|
||||
|
||||
def do_open(self, language_id, uri):
|
||||
if language_id in self.lsp_clients.keys():
|
||||
lsp_client = self.lsp_clients[language_id]
|
||||
else:
|
||||
lsp_client = self.load_lsp_server(language_id)
|
||||
|
||||
if lsp_client:
|
||||
self.register_opened_file(language_id, uri, lsp_client)
|
||||
|
||||
def do_save(self, language_id, uri):
|
||||
if language_id in self.lsp_clients.keys():
|
||||
self.lsp_clients[language_id].didSave(
|
||||
pylspclient.lsp_structs.TextDocumentIdentifier(uri)
|
||||
)
|
||||
|
||||
def do_close(self, language_id, uri):
|
||||
if language_id in self.lsp_clients.keys():
|
||||
self.lsp_clients[language_id].didClose(
|
||||
pylspclient.lsp_structs.TextDocumentIdentifier(uri)
|
||||
)
|
||||
|
||||
def do_goto(self, language_id, uri, line, offset):
|
||||
if language_id in self.lsp_clients.keys():
|
||||
return self.lsp_clients[language_id].definition(
|
||||
pylspclient.lsp_structs.TextDocumentIdentifier(uri),
|
||||
pylspclient.lsp_structs.Position(line, offset)
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
def do_change(self, language_id, line, start, end, text):
|
||||
if language_id in self.lsp_clients.keys():
|
||||
|
||||
start_pos = pylspclient.lsp_structs.Position(line, start.get_line_offset())
|
||||
end_pos = pylspclient.lsp_structs.Position(line, end.get_line_offset())
|
||||
range_info = pylspclient.lsp_structs.Range(start_pos, end_pos)
|
||||
text_length = len(text)
|
||||
change_event = pylspclient.lsp_structs.TextDocumentContentChangeEvent(range_info, text_length, text)
|
||||
|
||||
return self.lsp_clients[language_id].didChange( None, change_event )
|
||||
|
||||
return []
|
||||
|
||||
def do_completion(self, language_id, uri, line, offset, _char, is_invoked = False):
|
||||
if language_id in self.lsp_clients.keys():
|
||||
trigger = pylspclient.lsp_structs.CompletionTriggerKind.TriggerCharacter
|
||||
|
||||
if _char in [".", " "]:
|
||||
trigger = pylspclient.lsp_structs.CompletionTriggerKind.TriggerCharacter
|
||||
elif is_invoked:
|
||||
trigger = pylspclient.lsp_structs.CompletionTriggerKind.Invoked
|
||||
else:
|
||||
trigger = pylspclient.lsp_structs.CompletionTriggerKind.TriggerForIncompleteCompletions
|
||||
|
||||
return self.lsp_clients[language_id].completion(
|
||||
pylspclient.lsp_structs.TextDocumentIdentifier(uri),
|
||||
pylspclient.lsp_structs.Position(line, offset),
|
||||
pylspclient.lsp_structs.CompletionContext(trigger, _char)
|
||||
)
|
||||
|
||||
return []
|
||||
|
||||
|
||||
def load_lsp_server(self, language_id):
|
||||
if not language_id in self.lsp_servers_config.keys():
|
||||
return
|
||||
|
||||
command = self.lsp_servers_config[language_id]["command"]
|
||||
config_options = self.lsp_servers_config[language_id]["initialization_options"]
|
||||
|
||||
if command:
|
||||
server_proc = self.create_lsp_server(command)
|
||||
if self.create_client(language_id, server_proc, config_options):
|
||||
return self.lsp_clients[language_id]
|
||||
|
||||
return None
|
||||
|
||||
def register_opened_file(self, language_id = "", uri = "", lsp_client = None):
|
||||
if not language_id or not uri: return
|
||||
|
||||
text = open(uri[7:], "r").read()
|
||||
version = 1
|
||||
|
||||
lsp_client.didOpen(
|
||||
pylspclient.lsp_structs.TextDocumentItem(uri, language_id, version, text)
|
||||
)
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "LSP Client",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"credit": "Avi Yeger for the pylspclient used by this plugin. Link: https://github.com/yeger00/pylspclient",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"pass_events": "true"
|
||||
}
|
||||
}
|
||||
}
|
@ -1,124 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
import json
|
||||
import threading
|
||||
|
||||
# Lib imports
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
from plugins.plugin_base import PluginBase
|
||||
from .lsp_controller import LSPController
|
||||
|
||||
|
||||
|
||||
class LSPPliginException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "LSP Client" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self.lsp_config_path: str = os.path.dirname(os.path.realpath(__file__)) + "/../../lsp_servers_config.json"
|
||||
self.lsp_servers_config: dict = {}
|
||||
self.lsp_controller = None
|
||||
self.timer = None
|
||||
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
...
|
||||
|
||||
def run(self):
|
||||
if os.path.exists(self.lsp_config_path):
|
||||
with open(self.lsp_config_path, "r") as f:
|
||||
self.lsp_servers_config = json.load(f)
|
||||
else:
|
||||
text = f"LSP NOT Enabled.\nFile:\n\t{self.lsp_config_path}\ndoes no exsist..."
|
||||
self._event_system.emit("bubble_message", ("warning", self.name, text,))
|
||||
return
|
||||
|
||||
if len(self.lsp_servers_config.keys()) > 0:
|
||||
self.lsp_controller = LSPController(self.lsp_servers_config)
|
||||
self.inner_subscribe_to_events()
|
||||
|
||||
def subscribe_to_events(self):
|
||||
...
|
||||
|
||||
def inner_subscribe_to_events(self):
|
||||
self._event_system.subscribe("shutting_down", self._shutting_down)
|
||||
|
||||
self._event_system.subscribe("textDocument/didChange", self._buffer_changed)
|
||||
self._event_system.subscribe("textDocument/didOpen", self.lsp_controller.do_open)
|
||||
self._event_system.subscribe("textDocument/didSave", self.lsp_controller.do_save)
|
||||
self._event_system.subscribe("textDocument/didClose", self.lsp_controller.do_close)
|
||||
self._event_system.subscribe("textDocument/definition", self._do_goto)
|
||||
self._event_system.subscribe("textDocument/completion", self.completion)
|
||||
|
||||
def _shutting_down(self):
|
||||
self.lsp_controller._shutting_down()
|
||||
|
||||
def cancel_timer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
GLib.idle_remove_by_data(None)
|
||||
|
||||
def delay_completion_glib(self, source_view, context, callback):
|
||||
GLib.idle_add(self._do_completion, source_view, context, callback)
|
||||
|
||||
def delay_completion(self, source_view, context, callback):
|
||||
self.timer = threading.Timer(0.8, self.delay_completion_glib, (source_view, context, callback,))
|
||||
self.timer.daemon = True
|
||||
self.timer.start()
|
||||
|
||||
def _buffer_changed(self, language_id, buffer):
|
||||
iter = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
line = iter.get_line()
|
||||
start = iter.copy()
|
||||
end = iter.copy()
|
||||
|
||||
start.backward_line()
|
||||
start.forward_line()
|
||||
end.forward_to_line_end()
|
||||
|
||||
text = buffer.get_text(start, end, include_hidden_chars = False)
|
||||
result = self.lsp_controller.do_change(language_id, line, start, end, text)
|
||||
# print(result)
|
||||
|
||||
|
||||
def completion(self, source_view, context, callback):
|
||||
self.cancel_timer()
|
||||
self.delay_completion(source_view, context, callback)
|
||||
|
||||
def _do_completion(self, source_view, context, callback):
|
||||
filepath = source_view.get_current_file()
|
||||
|
||||
if not filepath: return
|
||||
|
||||
uri = filepath.get_uri()
|
||||
buffer = source_view.get_buffer()
|
||||
iter = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
line = iter.get_line() + 1
|
||||
|
||||
_char = iter.get_char()
|
||||
if iter.backward_char():
|
||||
_char = iter.get_char()
|
||||
|
||||
offset = iter.get_line_offset()
|
||||
result = self.lsp_controller.do_completion(source_view.get_filetype(), uri, line, offset, _char)
|
||||
callback(context, result)
|
||||
|
||||
def _do_goto(self, language_id, uri, line, offset):
|
||||
results = self.lsp_controller.do_goto(language_id, uri, line, offset)
|
||||
|
||||
if len(results) == 1:
|
||||
result = results[0]
|
||||
file = result.uri[7:]
|
||||
line = result.range.end.line
|
||||
message = f"FILE|{file}:{line}"
|
||||
self._event_system.emit("post_file_to_ipc", message)
|
@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Avi Yeger
|
||||
|
||||
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,6 +0,0 @@
|
||||
__all__ = []
|
||||
|
||||
from .json_rpc_endpoint import JsonRpcEndpoint
|
||||
from .lsp_client import LspClient
|
||||
from .lsp_endpoint import LspEndpoint
|
||||
from . import lsp_structs
|
@ -1,105 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import threading
|
||||
import json
|
||||
|
||||
from . import lsp_structs
|
||||
|
||||
|
||||
|
||||
JSON_RPC_REQ_FORMAT = "Content-Length: {json_string_len}\r\n\r\n{json_string}"
|
||||
LEN_HEADER = "Content-Length: "
|
||||
TYPE_HEADER = "Content-Type: "
|
||||
|
||||
|
||||
# TODO: add content-type
|
||||
|
||||
|
||||
class MyEncoder(json.JSONEncoder):
|
||||
"""
|
||||
Encodes an object in JSON
|
||||
"""
|
||||
|
||||
def default(self, o): # pylint: disable=E0202
|
||||
return o.__dict__
|
||||
|
||||
|
||||
class JsonRpcEndpoint(object):
|
||||
'''
|
||||
Thread safe JSON RPC endpoint implementation. Responsible to recieve and
|
||||
send JSON RPC messages, as described in the protocol. More information can
|
||||
be found: https://www.jsonrpc.org/
|
||||
'''
|
||||
|
||||
def __init__(self, stdin, stdout):
|
||||
self.stdin = stdin
|
||||
self.stdout = stdout
|
||||
self.read_lock = threading.Lock()
|
||||
self.write_lock = threading.Lock()
|
||||
|
||||
@staticmethod
|
||||
def __add_header(json_string):
|
||||
'''
|
||||
Adds a header for the given json string
|
||||
|
||||
:param str json_string: The string
|
||||
:return: the string with the header
|
||||
'''
|
||||
return JSON_RPC_REQ_FORMAT.format(json_string_len = len(json_string), json_string = json_string)
|
||||
|
||||
def send_request(self, message):
|
||||
'''
|
||||
Sends the given message.
|
||||
|
||||
:param dict message: The message to send.
|
||||
'''
|
||||
json_string = json.dumps(message, cls = MyEncoder)
|
||||
jsonrpc_req = self.__add_header(json_string)
|
||||
with self.write_lock:
|
||||
self.stdin.write(jsonrpc_req.encode())
|
||||
self.stdin.flush()
|
||||
|
||||
def recv_response(self):
|
||||
'''
|
||||
Recives a message.
|
||||
|
||||
:return: a message
|
||||
'''
|
||||
with self.read_lock:
|
||||
message_size = None
|
||||
while True:
|
||||
# read header
|
||||
line = self.stdout.readline()
|
||||
if not line:
|
||||
# server quit
|
||||
return None
|
||||
line = line.decode("utf-8")
|
||||
if not line.endswith("\r\n"):
|
||||
raise lsp_structs.ResponseError(
|
||||
lsp_structs.ErrorCodes.ParseError,
|
||||
"Bad header: missing newline")
|
||||
# remove the "\r\n"
|
||||
line = line[:-2]
|
||||
if line == "":
|
||||
# done with the headers
|
||||
break
|
||||
elif line.startswith(LEN_HEADER):
|
||||
line = line[len(LEN_HEADER):]
|
||||
if not line.isdigit():
|
||||
raise lsp_structs.ResponseError(
|
||||
lsp_structs.ErrorCodes.ParseError,
|
||||
"Bad header: size is not int")
|
||||
message_size = int(line)
|
||||
elif line.startswith(TYPE_HEADER):
|
||||
...
|
||||
else:
|
||||
raise lsp_structs.ResponseError(
|
||||
lsp_structs.ErrorCodes.ParseError,
|
||||
"Bad header: unkown header")
|
||||
if not message_size:
|
||||
raise lsp_structs.ResponseError(
|
||||
lsp_structs.ErrorCodes.ParseError,
|
||||
"Bad header: missing size")
|
||||
|
||||
jsonrpc_res = self.stdout.read(message_size).decode("utf-8")
|
||||
return json.loads(jsonrpc_res)
|
@ -1,258 +0,0 @@
|
||||
from . import lsp_structs
|
||||
|
||||
|
||||
|
||||
class LspClient(object):
|
||||
def __init__(self, lsp_endpoint):
|
||||
"""
|
||||
Constructs a new LspClient instance.
|
||||
|
||||
:param lsp_endpoint: TODO
|
||||
"""
|
||||
|
||||
self.lsp_endpoint = lsp_endpoint
|
||||
|
||||
def initialize(self, processId, rootPath, rootUri, initializationOptions, capabilities, trace, workspaceFolders):
|
||||
"""
|
||||
The initialize request is sent as the first request from the client to the server. If the server receives a request or notification
|
||||
before the initialize request it should act as follows:
|
||||
|
||||
1. For a request the response should be an error with code: -32002. The message can be picked by the server.
|
||||
2. Notifications should be dropped, except for the exit notification. This will allow the exit of a server without an initialize request.
|
||||
|
||||
Until the server has responded to the initialize request with an InitializeResult, the client must not send any additional requests or
|
||||
notifications to the server. In addition the server is not allowed to send any requests or notifications to the client until it has responded
|
||||
with an InitializeResult, with the exception that during the initialize request the server is allowed to send the notifications window/showMessage,
|
||||
window/logMessage and telemetry/event as well as the window/showMessageRequest request to the client.
|
||||
|
||||
The initialize request may only be sent once.
|
||||
|
||||
:param int processId: The process Id of the parent process that started the server. Is null if the process has not been started by another process.
|
||||
If the parent process is not alive then the server should exit (see exit notification) its process.
|
||||
:param str rootPath: The rootPath of the workspace. Is null if no folder is open. Deprecated in favour of rootUri.
|
||||
:param DocumentUri rootUri: The rootUri of the workspace. Is null if no folder is open. If both `rootPath` and `rootUri` are set
|
||||
`rootUri` wins.
|
||||
:param any initializationOptions: User provided initialization options.
|
||||
:param ClientCapabilities capabilities: The capabilities provided by the client (editor or tool).
|
||||
:param Trace trace: The initial trace setting. If omitted trace is disabled ('off').
|
||||
:param list workspaceFolders: The workspace folders configured in the client when the server starts. This property is only available if the client supports workspace folders.
|
||||
It can be `null` if the client supports workspace folders but none are configured.
|
||||
"""
|
||||
|
||||
self.lsp_endpoint.start()
|
||||
return self.lsp_endpoint.call_method("initialize", \
|
||||
processId = processId, \
|
||||
rootPath = rootPath, \
|
||||
rootUri = rootUri, \
|
||||
initializationOptions = initializationOptions, \
|
||||
capabilities = capabilities, \
|
||||
trace = trace, \
|
||||
workspaceFolders = workspaceFolders
|
||||
)
|
||||
|
||||
def initialized(self):
|
||||
"""
|
||||
The initialized notification is sent from the client to the server after the client received the result of the initialize request
|
||||
but before the client is sending any other request or notification to the server. The server can use the initialized notification
|
||||
for example to dynamically register capabilities. The initialized notification may only be sent once.
|
||||
"""
|
||||
|
||||
self.lsp_endpoint.send_notification("initialized")
|
||||
|
||||
def shutdown(self):
|
||||
"""
|
||||
The initialized notification is sent from the client to the server after the client received the result of the initialize request
|
||||
but before the client is sending any other request or notification to the server. The server can use the initialized notification
|
||||
for example to dynamically register capabilities. The initialized notification may only be sent once.
|
||||
"""
|
||||
|
||||
return self.lsp_endpoint.call_method("shutdown")
|
||||
|
||||
def exit(self):
|
||||
"""
|
||||
The initialized notification is sent from the client to the server after the client received the result of the initialize request
|
||||
but before the client is sending any other request or notification to the server. The server can use the initialized notification
|
||||
for example to dynamically register capabilities. The initialized notification may only be sent once.
|
||||
"""
|
||||
|
||||
self.lsp_endpoint.send_notification("exit")
|
||||
self.lsp_endpoint.stop()
|
||||
|
||||
|
||||
def didOpen(self, textDocument):
|
||||
"""
|
||||
The document open notification is sent from the client to the server to signal newly opened text documents. The document's truth is
|
||||
now managed by the client and the server must not try to read the document's truth using the document's uri. Open in this sense
|
||||
means it is managed by the client. It doesn't necessarily mean that its content is presented in an editor. An open notification must
|
||||
not be sent more than once without a corresponding close notification send before. This means open and close notification must be
|
||||
balanced and the max open count for a particular textDocument is one. Note that a server's ability to fulfill requests is independent
|
||||
of whether a text document is open or closed.
|
||||
|
||||
The DidOpenTextDocumentParams contain the language id the document is associated with. If the language Id of a document changes, the
|
||||
client needs to send a textDocument/didClose to the server followed by a textDocument/didOpen with the new language id if the server
|
||||
handles the new language id as well.
|
||||
|
||||
:param TextDocumentItem textDocument: The document that was opened.
|
||||
"""
|
||||
|
||||
return self.lsp_endpoint.send_notification("textDocument/didOpen", textDocument = textDocument)
|
||||
|
||||
def didSave(self, textDocument):
|
||||
"""
|
||||
:param TextDocumentIdentifier textDocument: The document that was saved.
|
||||
"""
|
||||
|
||||
return self.lsp_endpoint.send_notification("textDocument/didSave", textDocument = textDocument)
|
||||
|
||||
def didClose(self, textDocument):
|
||||
"""
|
||||
:param TextDocumentIdentifier textDocument: The document that was closed.
|
||||
"""
|
||||
return self.lsp_endpoint.send_notification("textDocument/didClose", textDocument = textDocument)
|
||||
|
||||
def didChange(self, textDocument, contentChanges):
|
||||
"""
|
||||
The document change notification is sent from the client to the server to signal changes to a text document.
|
||||
In 2.0 the shape of the params has changed to include proper version numbers and language ids.
|
||||
|
||||
:param VersionedTextDocumentIdentifier textDocument: The initial trace setting. If omitted trace is disabled ('off').
|
||||
:param TextDocumentContentChangeEvent[] contentChanges: The actual content changes. The content changes describe single state changes
|
||||
to the document. So if there are two content changes c1 and c2 for a document in state S then c1 move the document
|
||||
to S' and c2 to S''.
|
||||
"""
|
||||
|
||||
return self.lsp_endpoint.send_notification("textDocument/didChange", textDocument = textDocument, contentChanges = contentChanges)
|
||||
|
||||
def documentSymbol(self, textDocument):
|
||||
"""
|
||||
The document symbol request is sent from the client to the server to
|
||||
return a flat list of all symbols found in a given text document.
|
||||
Neither the symbol's location range nor the symbol's container name
|
||||
should be used to infer a hierarchy.
|
||||
|
||||
:param TextDocumentItem textDocument: The text document.
|
||||
"""
|
||||
result_dict = self.lsp_endpoint.call_method( "textDocument/documentSymbol", textDocument=textDocument )
|
||||
|
||||
if not result_dict: return []
|
||||
return [lsp_structs.SymbolInformation(**sym) for sym in result_dict]
|
||||
|
||||
def declaration(self, textDocument, position):
|
||||
"""
|
||||
The go to declaration request is sent from the client to the server to
|
||||
resolve the declaration location of a symbol at a given text document
|
||||
position.
|
||||
|
||||
The result type LocationLink[] got introduce with version 3.14.0 and
|
||||
depends in the corresponding client capability
|
||||
`clientCapabilities.textDocument.declaration.linkSupport`.
|
||||
|
||||
:param TextDocumentItem textDocument: The text document.
|
||||
:param Position position: The position inside the text document.
|
||||
"""
|
||||
|
||||
result_dict = self.lsp_endpoint.call_method("textDocument/declaration",
|
||||
textDocument = textDocument,
|
||||
position = position
|
||||
)
|
||||
|
||||
if not result_dict: return []
|
||||
|
||||
if "uri" in result_dict:
|
||||
return lsp_structs.Location(**result_dict)
|
||||
|
||||
return [lsp_structs.Location(**loc) if "uri" in loc else lsp_structs.LinkLocation(**loc) for loc in result_dict]
|
||||
|
||||
def definition(self, textDocument, position):
|
||||
"""
|
||||
The goto definition request is sent from the client to the server to
|
||||
resolve the definition location of a symbol at a given text document
|
||||
position.
|
||||
|
||||
:param TextDocumentItem textDocument: The text document.
|
||||
:param Position position: The position inside the text document.
|
||||
"""
|
||||
|
||||
result_dict = self.lsp_endpoint.call_method("textDocument/definition",
|
||||
textDocument = textDocument,
|
||||
position = position
|
||||
)
|
||||
|
||||
if not result_dict: return []
|
||||
return [lsp_structs.Location(**loc) for loc in result_dict]
|
||||
|
||||
def typeDefinition(self, textDocument, position):
|
||||
"""
|
||||
The goto type definition request is sent from the client to the server
|
||||
to resolve the type definition location of a symbol at a given text
|
||||
document position.
|
||||
|
||||
:param TextDocumentItem textDocument: The text document.
|
||||
:param Position position: The position inside the text document.
|
||||
"""
|
||||
|
||||
result_dict = self.lsp_endpoint.call_method("textDocument/typeDefinition",
|
||||
textDocument = textDocument,
|
||||
position = position
|
||||
)
|
||||
|
||||
if not result_dict: return []
|
||||
return [lsp_structs.Location(**loc) for loc in result_dict]
|
||||
|
||||
def signatureHelp(self, textDocument, position):
|
||||
"""
|
||||
The signature help request is sent from the client to the server to
|
||||
request signature information at a given cursor position.
|
||||
|
||||
:param TextDocumentItem textDocument: The text document.
|
||||
:param Position position: The position inside the text document.
|
||||
"""
|
||||
|
||||
result_dict = self.lsp_endpoint.call_method( "textDocument/signatureHelp",
|
||||
textDocument = textDocument,
|
||||
position = position
|
||||
)
|
||||
|
||||
if not result_dict: return []
|
||||
return lsp_structs.SignatureHelp(**result_dict)
|
||||
|
||||
def completion(self, textDocument, position, context):
|
||||
"""
|
||||
The signature help request is sent from the client to the server to
|
||||
request signature information at a given cursor position.
|
||||
|
||||
:param TextDocumentItem textDocument: The text document.
|
||||
:param Position position: The position inside the text document.
|
||||
:param CompletionContext context: The completion context. This is only
|
||||
available if the client specifies
|
||||
to send this using `ClientCapabilities.textDocument.completion.contextSupport === true`
|
||||
"""
|
||||
|
||||
result_dict = self.lsp_endpoint.call_method("textDocument/completion",
|
||||
textDocument = textDocument,
|
||||
position = position,
|
||||
context = context
|
||||
)
|
||||
if not result_dict: return []
|
||||
|
||||
if "isIncomplete" in result_dict:
|
||||
return lsp_structs.CompletionList(**result_dict)
|
||||
|
||||
return [lsp_structs.CompletionItem(**loc) for loc in result_dict]
|
||||
|
||||
def references(self, textDocument, position):
|
||||
"""
|
||||
The references request is sent from the client to the server to resolve
|
||||
project-wide references for the symbol denoted by the given text
|
||||
document position.
|
||||
|
||||
:param TextDocumentItem textDocument: The text document.
|
||||
:param Position position: The position inside the text document.
|
||||
"""
|
||||
|
||||
result_dict = self.lsp_endpoint.call_method("textDocument/references",
|
||||
textDocument = textDocument,
|
||||
position = position)
|
||||
|
||||
if not result_dict: return []
|
||||
return [lsp_structs.Location(**loc) for loc in result_dict]
|
@ -1,111 +0,0 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import threading
|
||||
|
||||
from . import lsp_structs
|
||||
|
||||
|
||||
|
||||
class LspEndpoint(threading.Thread):
|
||||
def __init__(self, json_rpc_endpoint, method_callbacks = {}, notify_callbacks = {}, timeout = 2):
|
||||
threading.Thread.__init__(self)
|
||||
self.json_rpc_endpoint = json_rpc_endpoint
|
||||
self.notify_callbacks = notify_callbacks
|
||||
self.method_callbacks = method_callbacks
|
||||
self.event_dict = {}
|
||||
self.response_dict = {}
|
||||
self.next_id = 0
|
||||
self._timeout = timeout
|
||||
self.shutdown_flag = False
|
||||
|
||||
def handle_result(self, rpc_id, result, error):
|
||||
self.response_dict[rpc_id] = (result, error)
|
||||
cond = self.event_dict[rpc_id]
|
||||
cond.acquire()
|
||||
cond.notify()
|
||||
cond.release()
|
||||
|
||||
def stop(self):
|
||||
self.shutdown_flag = True
|
||||
|
||||
def run(self):
|
||||
while not self.shutdown_flag:
|
||||
try:
|
||||
jsonrpc_message = self.json_rpc_endpoint.recv_response()
|
||||
if jsonrpc_message is None: break
|
||||
|
||||
method = jsonrpc_message.get("method")
|
||||
result = jsonrpc_message.get("result")
|
||||
error = jsonrpc_message.get("error")
|
||||
rpc_id = jsonrpc_message.get("id")
|
||||
params = jsonrpc_message.get("params")
|
||||
|
||||
if method:
|
||||
if rpc_id is not None:
|
||||
if method not in self.method_callbacks:
|
||||
raise lsp_structs.ResponseError(
|
||||
lsp_structs.ErrorCodes.MethodNotFound,
|
||||
"Method not found: {method}"
|
||||
.format(method=method))
|
||||
result = self.method_callbacks[method](params)
|
||||
self.send_response(rpc_id, result, None)
|
||||
else:
|
||||
if method not in self.notify_callbacks:
|
||||
print("Notify method not found: {method}.".format(method=method))
|
||||
else:
|
||||
self.notify_callbacks[method](params)
|
||||
else:
|
||||
self.handle_result(rpc_id, result, error)
|
||||
except lsp_structs.ResponseError as e:
|
||||
self.send_response(rpc_id, None, e)
|
||||
|
||||
def send_response(self, id, result, error):
|
||||
message_dict = {}
|
||||
message_dict["jsonrpc"] = "2.0"
|
||||
message_dict["id"] = id
|
||||
|
||||
if result:
|
||||
message_dict["result"] = result
|
||||
if error:
|
||||
message_dict["error"] = error
|
||||
|
||||
self.json_rpc_endpoint.send_request(message_dict)
|
||||
|
||||
def send_message(self, method_name, params, id=None):
|
||||
message_dict = {}
|
||||
message_dict["jsonrpc"] = "2.0"
|
||||
|
||||
if id is not None:
|
||||
message_dict["id"] = id
|
||||
|
||||
message_dict["method"] = method_name
|
||||
message_dict["params"] = params
|
||||
|
||||
self.json_rpc_endpoint.send_request(message_dict)
|
||||
|
||||
def call_method(self, method_name, **kwargs):
|
||||
current_id = self.next_id
|
||||
self.next_id += 1
|
||||
cond = threading.Condition()
|
||||
self.event_dict[current_id] = cond
|
||||
|
||||
cond.acquire()
|
||||
self.send_message(method_name, kwargs, current_id)
|
||||
if self.shutdown_flag:
|
||||
return None
|
||||
|
||||
if not cond.wait(timeout=self._timeout):
|
||||
raise TimeoutError()
|
||||
cond.release()
|
||||
|
||||
self.event_dict.pop(current_id)
|
||||
result, error = self.response_dict.pop(current_id)
|
||||
if error:
|
||||
raise lsp_structs.ResponseError(error.get("code"),
|
||||
error.get("message"),
|
||||
error.get("data"))
|
||||
|
||||
return result
|
||||
|
||||
def send_notification(self, method_name, **kwargs):
|
||||
self.send_message(method_name, kwargs)
|
@ -1,566 +0,0 @@
|
||||
import enum
|
||||
|
||||
|
||||
|
||||
def to_type(o, new_type):
|
||||
'''
|
||||
Helper funciton that receives an object or a dict and convert it to a new
|
||||
given type.
|
||||
|
||||
:param object|dict o: The object to convert
|
||||
:param Type new_type: The type to convert to.
|
||||
'''
|
||||
|
||||
return o if new_type == type(o) else new_type(**o)
|
||||
|
||||
|
||||
class Position(object):
|
||||
def __init__(self, line, character):
|
||||
"""
|
||||
Constructs a new Position instance.
|
||||
|
||||
:param int line: Line position in a document (zero-based).
|
||||
:param int character: Character offset on a line in a document
|
||||
(zero-based).
|
||||
"""
|
||||
self.line = line
|
||||
self.character = character
|
||||
|
||||
|
||||
class Range(object):
|
||||
def __init__(self, start, end):
|
||||
"""
|
||||
Constructs a new Range instance.
|
||||
|
||||
:param Position start: The range's start position.
|
||||
:param Position end: The range's end position.
|
||||
"""
|
||||
self.start = to_type(start, Position)
|
||||
self.end = to_type(end, Position)
|
||||
|
||||
|
||||
class Location(object):
|
||||
"""
|
||||
Represents a location inside a resource, such as a line inside a text file.
|
||||
"""
|
||||
|
||||
def __init__(self, uri, range):
|
||||
"""
|
||||
Constructs a new Location instance.
|
||||
|
||||
:param str uri: Resource file.
|
||||
:param Range range: The range inside the file
|
||||
"""
|
||||
self.uri = uri
|
||||
self.range = to_type(range, Range)
|
||||
|
||||
|
||||
class LocationLink(object):
|
||||
"""
|
||||
Represents a link between a source and a target location.
|
||||
"""
|
||||
|
||||
def __init__(self, originSelectionRange, targetUri, targetRange, targetSelectionRange):
|
||||
"""
|
||||
Constructs a new LocationLink instance.
|
||||
|
||||
:param Range originSelectionRange: Span of the origin of this link.
|
||||
Used as the underlined span for mouse interaction. Defaults to the word range at the mouse position.
|
||||
:param str targetUri: The target resource identifier of this link.
|
||||
:param Range targetRange: The full target range of this link. If the target for example is a symbol then target
|
||||
range is the range enclosing this symbol not including leading/trailing whitespace but everything else
|
||||
like comments. This information is typically used to highlight the range in the editor.
|
||||
:param Range targetSelectionRange: The range that should be selected and revealed when this link is being followed,
|
||||
e.g the name of a function. Must be contained by the the `targetRange`. See also `DocumentSymbol#range`
|
||||
"""
|
||||
self.originSelectionRange = to_type(originSelectionRange, Range)
|
||||
self.targetUri = targetUri
|
||||
self.targetRange = to_type(targetRange, Range)
|
||||
self.targetSelectionRange = to_type(targetSelectionRange, Range)
|
||||
|
||||
|
||||
class Diagnostic(object):
|
||||
def __init__(self, range, severity, code, source, message, relatedInformation):
|
||||
"""
|
||||
Constructs a new Diagnostic instance.
|
||||
:param Range range: The range at which the message applies.Resource file.
|
||||
:param int severity: The diagnostic's severity. Can be omitted. If omitted it is up to the
|
||||
client to interpret diagnostics as error, warning, info or hint.
|
||||
:param str code: The diagnostic's code, which might appear in the user interface.
|
||||
:param str source: A human-readable string describing the source of this
|
||||
diagnostic, e.g. 'typescript' or 'super lint'.
|
||||
:param str message: The diagnostic's message.
|
||||
:param list relatedInformation: An array of related diagnostic information, e.g. when symbol-names within
|
||||
a scope collide all definitions can be marked via this property.
|
||||
"""
|
||||
self.range = range
|
||||
self.severity = severity
|
||||
self.code = code
|
||||
self.source = source
|
||||
self.message = message
|
||||
self.relatedInformation = relatedInformation
|
||||
|
||||
|
||||
class DiagnosticSeverity(object):
|
||||
Error = 1
|
||||
Warning = 2 # TODO: warning is known in python
|
||||
Information = 3
|
||||
Hint = 4
|
||||
|
||||
|
||||
class DiagnosticRelatedInformation(object):
|
||||
def __init__(self, location, message):
|
||||
"""
|
||||
Constructs a new Diagnostic instance.
|
||||
:param Location location: The location of this related diagnostic information.
|
||||
:param str message: The message of this related diagnostic information.
|
||||
"""
|
||||
self.location = location
|
||||
self.message = message
|
||||
|
||||
|
||||
class Command(object):
|
||||
def __init__(self, title, command, arguments):
|
||||
"""
|
||||
Constructs a new Diagnostic instance.
|
||||
:param str title: Title of the command, like `save`.
|
||||
:param str command: The identifier of the actual command handler.
|
||||
:param list argusments: Arguments that the command handler should be invoked with.
|
||||
"""
|
||||
self.title = title
|
||||
self.command = command
|
||||
self.arguments = arguments
|
||||
|
||||
|
||||
class TextDocumentItem(object):
|
||||
"""
|
||||
An item to transfer a text document from the client to the server.
|
||||
"""
|
||||
def __init__(self, uri, languageId, version, text):
|
||||
"""
|
||||
Constructs a new Diagnostic instance.
|
||||
|
||||
:param DocumentUri uri: Title of the command, like `save`.
|
||||
:param str languageId: The identifier of the actual command handler.
|
||||
:param int version: Arguments that the command handler should be invoked with.
|
||||
:param str text: Arguments that the command handler should be invoked with.
|
||||
"""
|
||||
self.uri = uri
|
||||
self.languageId = languageId
|
||||
self.version = version
|
||||
self.text = text
|
||||
|
||||
|
||||
class TextDocumentIdentifier(object):
|
||||
"""
|
||||
Text documents are identified using a URI. On the protocol level, URIs are passed as strings.
|
||||
"""
|
||||
def __init__(self, uri):
|
||||
"""
|
||||
Constructs a new TextDocumentIdentifier instance.
|
||||
|
||||
:param DocumentUri uri: The text document's URI.
|
||||
"""
|
||||
self.uri = uri
|
||||
|
||||
|
||||
class VersionedTextDocumentIdentifier(TextDocumentIdentifier):
|
||||
"""
|
||||
An identifier to denote a specific version of a text document.
|
||||
"""
|
||||
def __init__(self, uri, version):
|
||||
"""
|
||||
Constructs a new TextDocumentIdentifier instance.
|
||||
|
||||
:param DocumentUri uri: The text document's URI.
|
||||
:param int version: The version number of this document. If a versioned
|
||||
text document identifier is sent from the server to the client and
|
||||
the file is not open in the editor (the server has not received an
|
||||
open notification before) the server can send `null` to indicate
|
||||
that the version is known and the content on disk is the truth (as
|
||||
speced with document content ownership).
|
||||
The version number of a document will increase after each change, including
|
||||
undo/redo. The number doesn't need to be consecutive.
|
||||
"""
|
||||
super(VersionedTextDocumentIdentifier, self).__init__(uri)
|
||||
self.version = version
|
||||
|
||||
|
||||
class TextDocumentContentChangeEvent(object):
|
||||
"""
|
||||
An event describing a change to a text document. If range and rangeLength are omitted
|
||||
the new text is considered to be the full content of the document.
|
||||
"""
|
||||
def __init__(self, range, rangeLength, text):
|
||||
"""
|
||||
Constructs a new TextDocumentContentChangeEvent instance.
|
||||
|
||||
:param Range range: The range of the document that changed.
|
||||
:param int rangeLength: The length of the range that got replaced.
|
||||
:param str text: The new text of the range/document.
|
||||
"""
|
||||
self.range = range
|
||||
self.rangeLength = rangeLength
|
||||
self.text = text
|
||||
|
||||
|
||||
class TextDocumentPositionParams(object):
|
||||
"""
|
||||
A parameter literal used in requests to pass a text document and a position inside that document.
|
||||
"""
|
||||
def __init__(self, textDocument, position):
|
||||
"""
|
||||
Constructs a new TextDocumentPositionParams instance.
|
||||
|
||||
:param TextDocumentIdentifier textDocument: The text document.
|
||||
:param Position position: The position inside the text document.
|
||||
"""
|
||||
self.textDocument = textDocument
|
||||
self.position = position
|
||||
|
||||
|
||||
class LANGUAGE_IDENTIFIER(object):
|
||||
BAT = "bat"
|
||||
BIBTEX = "bibtex"
|
||||
CLOJURE = "clojure"
|
||||
COFFESCRIPT = "coffeescript"
|
||||
C = "c"
|
||||
CPP = "cpp"
|
||||
CSHARP = "csharp"
|
||||
CSS = "css"
|
||||
DIFF = "diff"
|
||||
DOCKERFILE = "dockerfile"
|
||||
FSHARP = "fsharp"
|
||||
GIT_COMMIT = "git-commit"
|
||||
GIT_REBASE = "git-rebase"
|
||||
GO = "go"
|
||||
GROOVY = "groovy"
|
||||
HANDLEBARS = "handlebars"
|
||||
HTML = "html"
|
||||
INI = "ini"
|
||||
JAVA = "java"
|
||||
JAVASCRIPT = "javascript"
|
||||
JSON = "json"
|
||||
LATEX = "latex"
|
||||
LESS = "less"
|
||||
LUA = "lua"
|
||||
MAKEFILE = "makefile"
|
||||
MARKDOWN = "markdown"
|
||||
OBJECTIVE_C = "objective-c"
|
||||
OBJECTIVE_CPP = "objective-cpp"
|
||||
Perl = "perl"
|
||||
PHP = "php"
|
||||
POWERSHELL = "powershell"
|
||||
PUG = "jade"
|
||||
PYTHON = "python"
|
||||
R = "r"
|
||||
RAZOR = "razor"
|
||||
RUBY = "ruby"
|
||||
RUST = "rust"
|
||||
SASS = "sass"
|
||||
SCSS = "scss"
|
||||
ShaderLab = "shaderlab"
|
||||
SHELL_SCRIPT = "shellscript"
|
||||
SQL = "sql"
|
||||
SWIFT = "swift"
|
||||
TYPE_SCRIPT = "typescript"
|
||||
TEX = "tex"
|
||||
VB = "vb"
|
||||
XML = "xml"
|
||||
XSL = "xsl"
|
||||
YAML = "yaml"
|
||||
|
||||
|
||||
class SymbolKind(enum.Enum):
|
||||
File = 1
|
||||
Module = 2
|
||||
Namespace = 3
|
||||
Package = 4
|
||||
Class = 5
|
||||
Method = 6
|
||||
Property = 7
|
||||
Field = 8
|
||||
Constructor = 9
|
||||
Enum = 10
|
||||
Interface = 11
|
||||
Function = 12
|
||||
Variable = 13
|
||||
Constant = 14
|
||||
String = 15
|
||||
Number = 16
|
||||
Boolean = 17
|
||||
Array = 18
|
||||
Object = 19
|
||||
Key = 20
|
||||
Null = 21
|
||||
EnumMember = 22
|
||||
Struct = 23
|
||||
Event = 24
|
||||
Operator = 25
|
||||
TypeParameter = 26
|
||||
|
||||
|
||||
class SymbolInformation(object):
|
||||
"""
|
||||
Represents information about programming constructs like variables, classes, interfaces etc.
|
||||
"""
|
||||
def __init__(self, name, kind, location, containerName = None, deprecated = False):
|
||||
"""
|
||||
Constructs a new SymbolInformation instance.
|
||||
|
||||
:param str name: The name of this symbol.
|
||||
:param int kind: The kind of this symbol.
|
||||
:param bool Location: The location of this symbol. The location's range is used by a tool
|
||||
to reveal the location in the editor. If the symbol is selected in the
|
||||
tool the range's start information is used to position the cursor. So
|
||||
the range usually spans more then the actual symbol's name and does
|
||||
normally include things like visibility modifiers.
|
||||
|
||||
The range doesn't have to denote a node range in the sense of a abstract
|
||||
syntax tree. It can therefore not be used to re-construct a hierarchy of
|
||||
the symbols.
|
||||
:param str containerName: The name of the symbol containing this symbol. This information is for
|
||||
user interface purposes (e.g. to render a qualifier in the user interface
|
||||
if necessary). It can't be used to re-infer a hierarchy for the document
|
||||
symbols.
|
||||
:param bool deprecated: Indicates if this symbol is deprecated.
|
||||
"""
|
||||
self.name = name
|
||||
self.kind = SymbolKind(kind)
|
||||
self.deprecated = deprecated
|
||||
self.location = to_type(location, Location)
|
||||
self.containerName = containerName
|
||||
|
||||
|
||||
class ParameterInformation(object):
|
||||
"""
|
||||
Represents a parameter of a callable-signature. A parameter can
|
||||
have a label and a doc-comment.
|
||||
"""
|
||||
def __init__(self, label, documentation = ""):
|
||||
"""
|
||||
Constructs a new ParameterInformation instance.
|
||||
|
||||
:param str label: The label of this parameter. Will be shown in the UI.
|
||||
:param str documentation: The human-readable doc-comment of this parameter. Will be shown in the UI but can be omitted.
|
||||
"""
|
||||
self.label = label
|
||||
self.documentation = documentation
|
||||
|
||||
|
||||
class SignatureInformation(object):
|
||||
"""
|
||||
Represents the signature of something callable. A signature
|
||||
can have a label, like a function-name, a doc-comment, and
|
||||
a set of parameters.
|
||||
"""
|
||||
def __init__(self, label, documentation = "", parameters = []):
|
||||
"""
|
||||
Constructs a new SignatureInformation instance.
|
||||
|
||||
:param str label: The label of this signature. Will be shown in the UI.
|
||||
:param str documentation: The human-readable doc-comment of this signature. Will be shown in the UI but can be omitted.
|
||||
:param ParameterInformation[] parameters: The parameters of this signature.
|
||||
"""
|
||||
self.label = label
|
||||
self.documentation = documentation
|
||||
self.parameters = [to_type(parameter, ParameterInformation) for parameter in parameters]
|
||||
|
||||
|
||||
class SignatureHelp(object):
|
||||
"""
|
||||
Signature help represents the signature of something
|
||||
callable. There can be multiple signature but only one
|
||||
active and only one active parameter.
|
||||
"""
|
||||
def __init__(self, signatures, activeSignature = 0, activeParameter = 0):
|
||||
"""
|
||||
Constructs a new SignatureHelp instance.
|
||||
|
||||
:param SignatureInformation[] signatures: One or more signatures.
|
||||
:param int activeSignature:
|
||||
:param int activeParameter:
|
||||
"""
|
||||
self.signatures = [to_type(signature, SignatureInformation) for signature in signatures]
|
||||
self.activeSignature = activeSignature
|
||||
self.activeParameter = activeParameter
|
||||
|
||||
|
||||
class CompletionTriggerKind(object):
|
||||
Invoked = 1
|
||||
TriggerCharacter = 2
|
||||
TriggerForIncompleteCompletions = 3
|
||||
|
||||
|
||||
class CompletionContext(object):
|
||||
"""
|
||||
Contains additional information about the context in which a completion request is triggered.
|
||||
"""
|
||||
def __init__(self, triggerKind, triggerCharacter = None):
|
||||
"""
|
||||
Constructs a new CompletionContext instance.
|
||||
|
||||
:param CompletionTriggerKind triggerKind: How the completion was triggered.
|
||||
:param str triggerCharacter: The trigger character (a single character) that has trigger code complete.
|
||||
Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter`
|
||||
"""
|
||||
self.triggerKind = triggerKind
|
||||
if triggerCharacter:
|
||||
self.triggerCharacter = triggerCharacter
|
||||
|
||||
|
||||
class TextEdit(object):
|
||||
"""
|
||||
A textual edit applicable to a text document.
|
||||
"""
|
||||
def __init__(self, range, newText):
|
||||
"""
|
||||
:param Range range: The range of the text document to be manipulated. To insert
|
||||
text into a document create a range where start === end.
|
||||
:param str newText: The string to be inserted. For delete operations use an empty string.
|
||||
"""
|
||||
self.range = range
|
||||
self.newText = newText
|
||||
|
||||
|
||||
class InsertTextFormat(object):
|
||||
PlainText = 1
|
||||
Snippet = 2
|
||||
|
||||
|
||||
class CompletionItem(object):
|
||||
"""
|
||||
"""
|
||||
def __init__(self, label, \
|
||||
kind = None, \
|
||||
detail = None, \
|
||||
documentation = None, \
|
||||
deprecated = None, \
|
||||
preselect = None, \
|
||||
sortText = None, \
|
||||
filterText = None, \
|
||||
insertText = None, \
|
||||
insertTextFormat = None, \
|
||||
textEdit = None, \
|
||||
additionalTextEdits = None, \
|
||||
commitCharacters = None, \
|
||||
command = None, \
|
||||
data = None, \
|
||||
score = 0.0
|
||||
):
|
||||
"""
|
||||
:param str label: The label of this completion item. By default also the text that is inserted when selecting
|
||||
this completion.
|
||||
:param int kind: The kind of this completion item. Based of the kind an icon is chosen by the editor.
|
||||
:param str detail: A human-readable string with additional information about this item, like type or symbol information.
|
||||
:param tr ocumentation: A human-readable string that represents a doc-comment.
|
||||
:param bool deprecated: Indicates if this item is deprecated.
|
||||
:param bool preselect: Select this item when showing. Note: that only one completion item can be selected and that the
|
||||
tool / client decides which item that is. The rule is that the first item of those that match best is selected.
|
||||
:param str sortText: A string that should be used when comparing this item with other items. When `falsy` the label is used.
|
||||
:param str filterText: A string that should be used when filtering a set of completion items. When `falsy` the label is used.
|
||||
:param str insertText: A string that should be inserted into a document when selecting this completion. When `falsy` the label is used.
|
||||
The `insertText` is subject to interpretation by the client side. Some tools might not take the string literally. For example
|
||||
VS Code when code complete is requested in this example `con<cursor position>` and a completion item with an `insertText` of `console` is provided it
|
||||
will only insert `sole`. Therefore it is recommended to use `textEdit` instead since it avoids additional client side interpretation.
|
||||
@deprecated Use textEdit instead.
|
||||
:param InsertTextFormat insertTextFormat: The format of the insert text. The format applies to both the `insertText` property
|
||||
and the `newText` property of a provided `textEdit`.
|
||||
:param TextEdit textEdit: An edit which is applied to a document when selecting this completion. When an edit is provided the value of `insertText` is ignored.
|
||||
Note:* The range of the edit must be a single line range and it must contain the position at which completion
|
||||
has been requested.
|
||||
:param TextEdit additionalTextEdits: An optional array of additional text edits that are applied when selecting this completion.
|
||||
Edits must not overlap (including the same insert position) with the main edit nor with themselves.
|
||||
Additional text edits should be used to change text unrelated to the current cursor position
|
||||
(for example adding an import statement at the top of the file if the completion item will
|
||||
insert an unqualified type).
|
||||
:param str commitCharacters: An optional set of characters that when pressed while this completion is active will accept it first and
|
||||
then type that character. *Note* that all commit characters should have `length=1` and that superfluous
|
||||
characters will be ignored.
|
||||
:param Command command: An optional command that is executed *after* inserting this completion. Note: that
|
||||
additional modifications to the current document should be described with the additionalTextEdits-property.
|
||||
:param data: An data entry field that is preserved on a completion item between a completion and a completion resolve request.
|
||||
:param float score: Score of the code completion item.
|
||||
"""
|
||||
self.label = label
|
||||
self.kind = kind
|
||||
self.detail = detail
|
||||
self.documentation = documentation
|
||||
self.deprecated = deprecated
|
||||
self.preselect = preselect
|
||||
self.sortText = sortText
|
||||
self.filterText = filterText
|
||||
self.insertText = insertText
|
||||
self.insertTextFormat = insertTextFormat
|
||||
self.textEdit = textEdit
|
||||
self.additionalTextEdits = additionalTextEdits
|
||||
self.commitCharacters = commitCharacters
|
||||
self.command = command
|
||||
self.data = data
|
||||
self.score = score
|
||||
|
||||
|
||||
class CompletionItemKind(enum.Enum):
|
||||
Text = 1
|
||||
Method = 2
|
||||
Function = 3
|
||||
Constructor = 4
|
||||
Field = 5
|
||||
Variable = 6
|
||||
Class = 7
|
||||
Interface = 8
|
||||
Module = 9
|
||||
Property = 10
|
||||
Unit = 11
|
||||
Value = 12
|
||||
Enum = 13
|
||||
Keyword = 14
|
||||
Snippet = 15
|
||||
Color = 16
|
||||
File = 17
|
||||
Reference = 18
|
||||
Folder = 19
|
||||
EnumMember = 20
|
||||
Constant = 21
|
||||
Struct = 22
|
||||
Event = 23
|
||||
Operator = 24
|
||||
TypeParameter = 25
|
||||
|
||||
|
||||
class CompletionList(object):
|
||||
"""
|
||||
Represents a collection of [completion items](#CompletionItem) to be preselect in the editor.
|
||||
"""
|
||||
def __init__(self, isIncomplete, items):
|
||||
"""
|
||||
Constructs a new CompletionContext instance.
|
||||
|
||||
:param bool isIncomplete: This list it not complete. Further typing should result in recomputing this list.
|
||||
:param CompletionItem items: The completion items.
|
||||
"""
|
||||
self.isIncomplete = isIncomplete
|
||||
self.items = [to_type(i, CompletionItem) for i in items]
|
||||
|
||||
class ErrorCodes(enum.Enum):
|
||||
# Defined by JSON RPC
|
||||
ParseError = -32700
|
||||
InvalidRequest = -32600
|
||||
MethodNotFound = -32601
|
||||
InvalidParams = -32602
|
||||
InternalError = -32603
|
||||
serverErrorStart = -32099
|
||||
serverErrorEnd = -32000
|
||||
ServerNotInitialized = -32002
|
||||
UnknownErrorCode = -32001
|
||||
|
||||
# Defined by the protocol.
|
||||
RequestCancelled = -32800
|
||||
ContentModified = -32801
|
||||
|
||||
class ResponseError(Exception):
|
||||
def __init__(self, code, message, data = None):
|
||||
self.code = code
|
||||
self.message = message
|
||||
if data:
|
||||
self.data = data
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Markdown Preview",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"pass_events": "true",
|
||||
"pass_ui_objects": ["separator_right"],
|
||||
"bind_keys": ["Markdown Preview||tggle_markdown_preview:<Shift><Control>m"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,48 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# - Documentation: https://python-markdown.github.io/
|
||||
# - GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# - PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# - Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# - Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# - Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
Python-Markdown provides two public functions ([`markdown.markdown`][] and [`markdown.markdownFromFile`][])
|
||||
both of which wrap the public class [`markdown.Markdown`][]. All submodules support these public functions
|
||||
and class and/or provide extension support.
|
||||
|
||||
Modules:
|
||||
core: Core functionality.
|
||||
preprocessors: Pre-processors.
|
||||
blockparser: Core Markdown block parser.
|
||||
blockprocessors: Block processors.
|
||||
treeprocessors: Tree processors.
|
||||
inlinepatterns: Inline patterns.
|
||||
postprocessors: Post-processors.
|
||||
serializers: Serializers.
|
||||
util: Utility functions.
|
||||
htmlparser: HTML parser.
|
||||
test_tools: Testing utilities.
|
||||
extensions: Markdown extensions.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from .core import Markdown, markdown, markdownFromFile
|
||||
from .__meta__ import __version__, __version_info__ # noqa
|
||||
|
||||
# For backward compatibility as some extensions expect it...
|
||||
from .extensions import Extension # noqa
|
||||
|
||||
__all__ = ['Markdown', 'markdown', 'markdownFromFile']
|
@ -1,151 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import optparse
|
||||
import codecs
|
||||
import warnings
|
||||
import markdown
|
||||
try:
|
||||
# We use `unsafe_load` because users may need to pass in actual Python
|
||||
# objects. As this is only available from the CLI, the user has much
|
||||
# worse problems if an attacker can use this as an attach vector.
|
||||
from yaml import unsafe_load as yaml_load
|
||||
except ImportError: # pragma: no cover
|
||||
try:
|
||||
# Fall back to PyYAML <5.1
|
||||
from yaml import load as yaml_load
|
||||
except ImportError:
|
||||
# Fall back to JSON
|
||||
from json import load as yaml_load
|
||||
|
||||
import logging
|
||||
from logging import DEBUG, WARNING, CRITICAL
|
||||
|
||||
logger = logging.getLogger('MARKDOWN')
|
||||
|
||||
|
||||
def parse_options(args=None, values=None):
|
||||
"""
|
||||
Define and parse `optparse` options for command-line usage.
|
||||
"""
|
||||
usage = """%prog [options] [INPUTFILE]
|
||||
(STDIN is assumed if no INPUTFILE is given)"""
|
||||
desc = "A Python implementation of John Gruber's Markdown. " \
|
||||
"https://Python-Markdown.github.io/"
|
||||
ver = "%%prog %s" % markdown.__version__
|
||||
|
||||
parser = optparse.OptionParser(usage=usage, description=desc, version=ver)
|
||||
parser.add_option("-f", "--file", dest="filename", default=None,
|
||||
help="Write output to OUTPUT_FILE. Defaults to STDOUT.",
|
||||
metavar="OUTPUT_FILE")
|
||||
parser.add_option("-e", "--encoding", dest="encoding",
|
||||
help="Encoding for input and output files.",)
|
||||
parser.add_option("-o", "--output_format", dest="output_format",
|
||||
default='xhtml', metavar="OUTPUT_FORMAT",
|
||||
help="Use output format 'xhtml' (default) or 'html'.")
|
||||
parser.add_option("-n", "--no_lazy_ol", dest="lazy_ol",
|
||||
action='store_false', default=True,
|
||||
help="Observe number of first item of ordered lists.")
|
||||
parser.add_option("-x", "--extension", action="append", dest="extensions",
|
||||
help="Load extension EXTENSION.", metavar="EXTENSION")
|
||||
parser.add_option("-c", "--extension_configs",
|
||||
dest="configfile", default=None,
|
||||
help="Read extension configurations from CONFIG_FILE. "
|
||||
"CONFIG_FILE must be of JSON or YAML format. YAML "
|
||||
"format requires that a python YAML library be "
|
||||
"installed. The parsed JSON or YAML must result in a "
|
||||
"python dictionary which would be accepted by the "
|
||||
"'extension_configs' keyword on the markdown.Markdown "
|
||||
"class. The extensions must also be loaded with the "
|
||||
"`--extension` option.",
|
||||
metavar="CONFIG_FILE")
|
||||
parser.add_option("-q", "--quiet", default=CRITICAL,
|
||||
action="store_const", const=CRITICAL+10, dest="verbose",
|
||||
help="Suppress all warnings.")
|
||||
parser.add_option("-v", "--verbose",
|
||||
action="store_const", const=WARNING, dest="verbose",
|
||||
help="Print all warnings.")
|
||||
parser.add_option("--noisy",
|
||||
action="store_const", const=DEBUG, dest="verbose",
|
||||
help="Print debug messages.")
|
||||
|
||||
(options, args) = parser.parse_args(args, values)
|
||||
|
||||
if len(args) == 0:
|
||||
input_file = None
|
||||
else:
|
||||
input_file = args[0]
|
||||
|
||||
if not options.extensions:
|
||||
options.extensions = []
|
||||
|
||||
extension_configs = {}
|
||||
if options.configfile:
|
||||
with codecs.open(
|
||||
options.configfile, mode="r", encoding=options.encoding
|
||||
) as fp:
|
||||
try:
|
||||
extension_configs = yaml_load(fp)
|
||||
except Exception as e:
|
||||
message = "Failed parsing extension config file: %s" % \
|
||||
options.configfile
|
||||
e.args = (message,) + e.args[1:]
|
||||
raise
|
||||
|
||||
opts = {
|
||||
'input': input_file,
|
||||
'output': options.filename,
|
||||
'extensions': options.extensions,
|
||||
'extension_configs': extension_configs,
|
||||
'encoding': options.encoding,
|
||||
'output_format': options.output_format,
|
||||
'lazy_ol': options.lazy_ol
|
||||
}
|
||||
|
||||
return opts, options.verbose
|
||||
|
||||
|
||||
def run(): # pragma: no cover
|
||||
"""Run Markdown from the command line."""
|
||||
|
||||
# Parse options and adjust logging level if necessary
|
||||
options, logging_level = parse_options()
|
||||
if not options:
|
||||
sys.exit(2)
|
||||
logger.setLevel(logging_level)
|
||||
console_handler = logging.StreamHandler()
|
||||
logger.addHandler(console_handler)
|
||||
if logging_level <= WARNING:
|
||||
# Ensure deprecation warnings get displayed
|
||||
warnings.filterwarnings('default')
|
||||
logging.captureWarnings(True)
|
||||
warn_logger = logging.getLogger('py.warnings')
|
||||
warn_logger.addHandler(console_handler)
|
||||
|
||||
# Run
|
||||
markdown.markdownFromFile(**options)
|
||||
|
||||
|
||||
if __name__ == '__main__': # pragma: no cover
|
||||
# Support running module as a command line command.
|
||||
# python -m markdown [options] [args]
|
||||
run()
|
@ -1,51 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
# __version_info__ format:
|
||||
# (major, minor, patch, dev/alpha/beta/rc/final, #)
|
||||
# (1, 1, 2, 'dev', 0) => "1.1.2.dev0"
|
||||
# (1, 1, 2, 'alpha', 1) => "1.1.2a1"
|
||||
# (1, 2, 0, 'beta', 2) => "1.2b2"
|
||||
# (1, 2, 0, 'rc', 4) => "1.2rc4"
|
||||
# (1, 2, 0, 'final', 0) => "1.2"
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
|
||||
__version_info__ = (3, 5, 1, 'final', 0)
|
||||
|
||||
|
||||
def _get_version(version_info):
|
||||
" Returns a PEP 440-compliant version number from `version_info`. "
|
||||
assert len(version_info) == 5
|
||||
assert version_info[3] in ('dev', 'alpha', 'beta', 'rc', 'final')
|
||||
|
||||
parts = 2 if version_info[2] == 0 else 3
|
||||
v = '.'.join(map(str, version_info[:parts]))
|
||||
|
||||
if version_info[3] == 'dev':
|
||||
v += '.dev' + str(version_info[4])
|
||||
elif version_info[3] != 'final':
|
||||
mapping = {'alpha': 'a', 'beta': 'b', 'rc': 'rc'}
|
||||
v += mapping[version_info[3]] + str(version_info[4])
|
||||
|
||||
return v
|
||||
|
||||
|
||||
__version__ = _get_version(__version_info__)
|
@ -1,160 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
The block parser handles basic parsing of Markdown blocks. It doesn't concern
|
||||
itself with inline elements such as `**bold**` or `*italics*`, but rather just
|
||||
catches blocks, lists, quotes, etc.
|
||||
|
||||
The `BlockParser` is made up of a bunch of `BlockProcessors`, each handling a
|
||||
different type of block. Extensions may add/replace/remove `BlockProcessors`
|
||||
as they need to alter how Markdown blocks are parsed.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import xml.etree.ElementTree as etree
|
||||
from typing import TYPE_CHECKING, Iterable, Any
|
||||
from . import util
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
from .blockprocessors import BlockProcessor
|
||||
|
||||
|
||||
class State(list):
|
||||
""" Track the current and nested state of the parser.
|
||||
|
||||
This utility class is used to track the state of the `BlockParser` and
|
||||
support multiple levels if nesting. It's just a simple API wrapped around
|
||||
a list. Each time a state is set, that state is appended to the end of the
|
||||
list. Each time a state is reset, that state is removed from the end of
|
||||
the list.
|
||||
|
||||
Therefore, each time a state is set for a nested block, that state must be
|
||||
reset when we back out of that level of nesting or the state could be
|
||||
corrupted.
|
||||
|
||||
While all the methods of a list object are available, only the three
|
||||
defined below need be used.
|
||||
|
||||
"""
|
||||
|
||||
def set(self, state: Any):
|
||||
""" Set a new state. """
|
||||
self.append(state)
|
||||
|
||||
def reset(self) -> None:
|
||||
""" Step back one step in nested state. """
|
||||
self.pop()
|
||||
|
||||
def isstate(self, state: Any) -> bool:
|
||||
""" Test that top (current) level is of given state. """
|
||||
if len(self):
|
||||
return self[-1] == state
|
||||
else:
|
||||
return False
|
||||
|
||||
|
||||
class BlockParser:
|
||||
""" Parse Markdown blocks into an `ElementTree` object.
|
||||
|
||||
A wrapper class that stitches the various `BlockProcessors` together,
|
||||
looping through them and creating an `ElementTree` object.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, md: Markdown):
|
||||
""" Initialize the block parser.
|
||||
|
||||
Arguments:
|
||||
md: A Markdown instance.
|
||||
|
||||
Attributes:
|
||||
BlockParser.md (Markdown): A Markdown instance.
|
||||
BlockParser.state (State): Tracks the nesting level of current location in document being parsed.
|
||||
BlockParser.blockprocessors (util.Registry): A collection of
|
||||
[`blockprocessors`][markdown.blockprocessors].
|
||||
|
||||
"""
|
||||
self.blockprocessors: util.Registry[BlockProcessor] = util.Registry()
|
||||
self.state = State()
|
||||
self.md = md
|
||||
|
||||
def parseDocument(self, lines: Iterable[str]) -> etree.ElementTree:
|
||||
""" Parse a Markdown document into an `ElementTree`.
|
||||
|
||||
Given a list of lines, an `ElementTree` object (not just a parent
|
||||
`Element`) is created and the root element is passed to the parser
|
||||
as the parent. The `ElementTree` object is returned.
|
||||
|
||||
This should only be called on an entire document, not pieces.
|
||||
|
||||
Arguments:
|
||||
lines: A list of lines (strings).
|
||||
|
||||
Returns:
|
||||
An element tree.
|
||||
"""
|
||||
# Create an `ElementTree` from the lines
|
||||
self.root = etree.Element(self.md.doc_tag)
|
||||
self.parseChunk(self.root, '\n'.join(lines))
|
||||
return etree.ElementTree(self.root)
|
||||
|
||||
def parseChunk(self, parent: etree.Element, text: str) -> None:
|
||||
""" Parse a chunk of Markdown text and attach to given `etree` node.
|
||||
|
||||
While the `text` argument is generally assumed to contain multiple
|
||||
blocks which will be split on blank lines, it could contain only one
|
||||
block. Generally, this method would be called by extensions when
|
||||
block parsing is required.
|
||||
|
||||
The `parent` `etree` Element passed in is altered in place.
|
||||
Nothing is returned.
|
||||
|
||||
Arguments:
|
||||
parent: The parent element.
|
||||
text: The text to parse.
|
||||
|
||||
"""
|
||||
self.parseBlocks(parent, text.split('\n\n'))
|
||||
|
||||
def parseBlocks(self, parent: etree.Element, blocks: list[str]) -> None:
|
||||
""" Process blocks of Markdown text and attach to given `etree` node.
|
||||
|
||||
Given a list of `blocks`, each `blockprocessor` is stepped through
|
||||
until there are no blocks left. While an extension could potentially
|
||||
call this method directly, it's generally expected to be used
|
||||
internally.
|
||||
|
||||
This is a public method as an extension may need to add/alter
|
||||
additional `BlockProcessors` which call this method to recursively
|
||||
parse a nested block.
|
||||
|
||||
Arguments:
|
||||
parent: The parent element.
|
||||
blocks: The blocks of text to parse.
|
||||
|
||||
"""
|
||||
while blocks:
|
||||
for processor in self.blockprocessors:
|
||||
if processor.test(parent, blocks[0]):
|
||||
if processor.run(parent, blocks) is not False:
|
||||
# run returns True or None
|
||||
break
|
@ -1,636 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
A block processor parses blocks of text and adds new elements to the ElementTree. Blocks of text,
|
||||
separated from other text by blank lines, may have a different syntax and produce a differently
|
||||
structured tree than other Markdown. Block processors excel at handling code formatting, equation
|
||||
layouts, tables, etc.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import re
|
||||
import xml.etree.ElementTree as etree
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from . import util
|
||||
from .blockparser import BlockParser
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
|
||||
logger = logging.getLogger('MARKDOWN')
|
||||
|
||||
|
||||
def build_block_parser(md: Markdown, **kwargs: Any) -> BlockParser:
|
||||
""" Build the default block parser used by Markdown. """
|
||||
parser = BlockParser(md)
|
||||
parser.blockprocessors.register(EmptyBlockProcessor(parser), 'empty', 100)
|
||||
parser.blockprocessors.register(ListIndentProcessor(parser), 'indent', 90)
|
||||
parser.blockprocessors.register(CodeBlockProcessor(parser), 'code', 80)
|
||||
parser.blockprocessors.register(HashHeaderProcessor(parser), 'hashheader', 70)
|
||||
parser.blockprocessors.register(SetextHeaderProcessor(parser), 'setextheader', 60)
|
||||
parser.blockprocessors.register(HRProcessor(parser), 'hr', 50)
|
||||
parser.blockprocessors.register(OListProcessor(parser), 'olist', 40)
|
||||
parser.blockprocessors.register(UListProcessor(parser), 'ulist', 30)
|
||||
parser.blockprocessors.register(BlockQuoteProcessor(parser), 'quote', 20)
|
||||
parser.blockprocessors.register(ReferenceProcessor(parser), 'reference', 15)
|
||||
parser.blockprocessors.register(ParagraphProcessor(parser), 'paragraph', 10)
|
||||
return parser
|
||||
|
||||
|
||||
class BlockProcessor:
|
||||
""" Base class for block processors.
|
||||
|
||||
Each subclass will provide the methods below to work with the source and
|
||||
tree. Each processor will need to define it's own `test` and `run`
|
||||
methods. The `test` method should return True or False, to indicate
|
||||
whether the current block should be processed by this processor. If the
|
||||
test passes, the parser will call the processors `run` method.
|
||||
|
||||
Attributes:
|
||||
BlockProcessor.parser (BlockParser): The `BlockParser` instance this is attached to.
|
||||
BlockProcessor.tab_length (int): The tab length set on the `Markdown` instance.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, parser: BlockParser):
|
||||
self.parser = parser
|
||||
self.tab_length = parser.md.tab_length
|
||||
|
||||
def lastChild(self, parent: etree.Element) -> etree.Element | None:
|
||||
""" Return the last child of an `etree` element. """
|
||||
if len(parent):
|
||||
return parent[-1]
|
||||
else:
|
||||
return None
|
||||
|
||||
def detab(self, text: str, length: int | None = None) -> tuple[str, str]:
|
||||
""" Remove a tab from the front of each line of the given text. """
|
||||
if length is None:
|
||||
length = self.tab_length
|
||||
newtext = []
|
||||
lines = text.split('\n')
|
||||
for line in lines:
|
||||
if line.startswith(' ' * length):
|
||||
newtext.append(line[length:])
|
||||
elif not line.strip():
|
||||
newtext.append('')
|
||||
else:
|
||||
break
|
||||
return '\n'.join(newtext), '\n'.join(lines[len(newtext):])
|
||||
|
||||
def looseDetab(self, text: str, level: int = 1) -> str:
|
||||
""" Remove a tab from front of lines but allowing dedented lines. """
|
||||
lines = text.split('\n')
|
||||
for i in range(len(lines)):
|
||||
if lines[i].startswith(' '*self.tab_length*level):
|
||||
lines[i] = lines[i][self.tab_length*level:]
|
||||
return '\n'.join(lines)
|
||||
|
||||
def test(self, parent: etree.Element, block: str) -> bool:
|
||||
""" Test for block type. Must be overridden by subclasses.
|
||||
|
||||
As the parser loops through processors, it will call the `test`
|
||||
method on each to determine if the given block of text is of that
|
||||
type. This method must return a boolean `True` or `False`. The
|
||||
actual method of testing is left to the needs of that particular
|
||||
block type. It could be as simple as `block.startswith(some_string)`
|
||||
or a complex regular expression. As the block type may be different
|
||||
depending on the parent of the block (i.e. inside a list), the parent
|
||||
`etree` element is also provided and may be used as part of the test.
|
||||
|
||||
Keyword arguments:
|
||||
parent: An `etree` element which will be the parent of the block.
|
||||
block: A block of text from the source which has been split at blank lines.
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
def run(self, parent: etree.Element, blocks: list[str]) -> bool | None:
|
||||
""" Run processor. Must be overridden by subclasses.
|
||||
|
||||
When the parser determines the appropriate type of a block, the parser
|
||||
will call the corresponding processor's `run` method. This method
|
||||
should parse the individual lines of the block and append them to
|
||||
the `etree`.
|
||||
|
||||
Note that both the `parent` and `etree` keywords are pointers
|
||||
to instances of the objects which should be edited in place. Each
|
||||
processor must make changes to the existing objects as there is no
|
||||
mechanism to return new/different objects to replace them.
|
||||
|
||||
This means that this method should be adding `SubElements` or adding text
|
||||
to the parent, and should remove (`pop`) or add (`insert`) items to
|
||||
the list of blocks.
|
||||
|
||||
If `False` is returned, this will have the same effect as returning `False`
|
||||
from the `test` method.
|
||||
|
||||
Keyword arguments:
|
||||
parent: An `etree` element which is the parent of the current block.
|
||||
blocks: A list of all remaining blocks of the document.
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class ListIndentProcessor(BlockProcessor):
|
||||
""" Process children of list items.
|
||||
|
||||
Example
|
||||
|
||||
* a list item
|
||||
process this part
|
||||
|
||||
or this part
|
||||
|
||||
"""
|
||||
|
||||
ITEM_TYPES = ['li']
|
||||
""" List of tags used for list items. """
|
||||
LIST_TYPES = ['ul', 'ol']
|
||||
""" Types of lists this processor can operate on. """
|
||||
|
||||
def __init__(self, *args):
|
||||
super().__init__(*args)
|
||||
self.INDENT_RE = re.compile(r'^(([ ]{%s})+)' % self.tab_length)
|
||||
|
||||
def test(self, parent, block):
|
||||
return block.startswith(' '*self.tab_length) and \
|
||||
not self.parser.state.isstate('detabbed') and \
|
||||
(parent.tag in self.ITEM_TYPES or
|
||||
(len(parent) and parent[-1] is not None and
|
||||
(parent[-1].tag in self.LIST_TYPES)))
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
level, sibling = self.get_level(parent, block)
|
||||
block = self.looseDetab(block, level)
|
||||
|
||||
self.parser.state.set('detabbed')
|
||||
if parent.tag in self.ITEM_TYPES:
|
||||
# It's possible that this parent has a `ul` or `ol` child list
|
||||
# with a member. If that is the case, then that should be the
|
||||
# parent. This is intended to catch the edge case of an indented
|
||||
# list whose first member was parsed previous to this point
|
||||
# see `OListProcessor`
|
||||
if len(parent) and parent[-1].tag in self.LIST_TYPES:
|
||||
self.parser.parseBlocks(parent[-1], [block])
|
||||
else:
|
||||
# The parent is already a `li`. Just parse the child block.
|
||||
self.parser.parseBlocks(parent, [block])
|
||||
elif sibling.tag in self.ITEM_TYPES:
|
||||
# The sibling is a `li`. Use it as parent.
|
||||
self.parser.parseBlocks(sibling, [block])
|
||||
elif len(sibling) and sibling[-1].tag in self.ITEM_TYPES:
|
||||
# The parent is a list (`ol` or `ul`) which has children.
|
||||
# Assume the last child `li` is the parent of this block.
|
||||
if sibling[-1].text:
|
||||
# If the parent `li` has text, that text needs to be moved to a `p`
|
||||
# The `p` must be 'inserted' at beginning of list in the event
|
||||
# that other children already exist i.e.; a nested sub-list.
|
||||
p = etree.Element('p')
|
||||
p.text = sibling[-1].text
|
||||
sibling[-1].text = ''
|
||||
sibling[-1].insert(0, p)
|
||||
self.parser.parseChunk(sibling[-1], block)
|
||||
else:
|
||||
self.create_item(sibling, block)
|
||||
self.parser.state.reset()
|
||||
|
||||
def create_item(self, parent: etree.Element, block: str) -> None:
|
||||
""" Create a new `li` and parse the block with it as the parent. """
|
||||
li = etree.SubElement(parent, 'li')
|
||||
self.parser.parseBlocks(li, [block])
|
||||
|
||||
def get_level(self, parent: etree.Element, block: str) -> tuple[int, etree.Element]:
|
||||
""" Get level of indentation based on list level. """
|
||||
# Get indent level
|
||||
m = self.INDENT_RE.match(block)
|
||||
if m:
|
||||
indent_level = len(m.group(1))/self.tab_length
|
||||
else:
|
||||
indent_level = 0
|
||||
if self.parser.state.isstate('list'):
|
||||
# We're in a tight-list - so we already are at correct parent.
|
||||
level = 1
|
||||
else:
|
||||
# We're in a loose-list - so we need to find parent.
|
||||
level = 0
|
||||
# Step through children of tree to find matching indent level.
|
||||
while indent_level > level:
|
||||
child = self.lastChild(parent)
|
||||
if (child is not None and
|
||||
(child.tag in self.LIST_TYPES or child.tag in self.ITEM_TYPES)):
|
||||
if child.tag in self.LIST_TYPES:
|
||||
level += 1
|
||||
parent = child
|
||||
else:
|
||||
# No more child levels. If we're short of `indent_level`,
|
||||
# we have a code block. So we stop here.
|
||||
break
|
||||
return level, parent
|
||||
|
||||
|
||||
class CodeBlockProcessor(BlockProcessor):
|
||||
""" Process code blocks. """
|
||||
|
||||
def test(self, parent, block):
|
||||
return block.startswith(' '*self.tab_length)
|
||||
|
||||
def run(self, parent, blocks):
|
||||
sibling = self.lastChild(parent)
|
||||
block = blocks.pop(0)
|
||||
theRest = ''
|
||||
if (sibling is not None and sibling.tag == "pre" and
|
||||
len(sibling) and sibling[0].tag == "code"):
|
||||
# The previous block was a code block. As blank lines do not start
|
||||
# new code blocks, append this block to the previous, adding back
|
||||
# line breaks removed from the split into a list.
|
||||
code = sibling[0]
|
||||
block, theRest = self.detab(block)
|
||||
code.text = util.AtomicString(
|
||||
'{}\n{}\n'.format(code.text, util.code_escape(block.rstrip()))
|
||||
)
|
||||
else:
|
||||
# This is a new code block. Create the elements and insert text.
|
||||
pre = etree.SubElement(parent, 'pre')
|
||||
code = etree.SubElement(pre, 'code')
|
||||
block, theRest = self.detab(block)
|
||||
code.text = util.AtomicString('%s\n' % util.code_escape(block.rstrip()))
|
||||
if theRest:
|
||||
# This block contained unindented line(s) after the first indented
|
||||
# line. Insert these lines as the first block of the master blocks
|
||||
# list for future processing.
|
||||
blocks.insert(0, theRest)
|
||||
|
||||
|
||||
class BlockQuoteProcessor(BlockProcessor):
|
||||
""" Process blockquotes. """
|
||||
|
||||
RE = re.compile(r'(^|\n)[ ]{0,3}>[ ]?(.*)')
|
||||
|
||||
def test(self, parent, block):
|
||||
return bool(self.RE.search(block)) and not util.nearing_recursion_limit()
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
m = self.RE.search(block)
|
||||
if m:
|
||||
before = block[:m.start()] # Lines before blockquote
|
||||
# Pass lines before blockquote in recursively for parsing first.
|
||||
self.parser.parseBlocks(parent, [before])
|
||||
# Remove `> ` from beginning of each line.
|
||||
block = '\n'.join(
|
||||
[self.clean(line) for line in block[m.start():].split('\n')]
|
||||
)
|
||||
sibling = self.lastChild(parent)
|
||||
if sibling is not None and sibling.tag == "blockquote":
|
||||
# Previous block was a blockquote so set that as this blocks parent
|
||||
quote = sibling
|
||||
else:
|
||||
# This is a new blockquote. Create a new parent element.
|
||||
quote = etree.SubElement(parent, 'blockquote')
|
||||
# Recursively parse block with blockquote as parent.
|
||||
# change parser state so blockquotes embedded in lists use `p` tags
|
||||
self.parser.state.set('blockquote')
|
||||
self.parser.parseChunk(quote, block)
|
||||
self.parser.state.reset()
|
||||
|
||||
def clean(self, line: str) -> str:
|
||||
""" Remove `>` from beginning of a line. """
|
||||
m = self.RE.match(line)
|
||||
if line.strip() == ">":
|
||||
return ""
|
||||
elif m:
|
||||
return m.group(2)
|
||||
else:
|
||||
return line
|
||||
|
||||
|
||||
class OListProcessor(BlockProcessor):
|
||||
""" Process ordered list blocks. """
|
||||
|
||||
TAG: str = 'ol'
|
||||
""" The tag used for the the wrapping element. """
|
||||
STARTSWITH: str = '1'
|
||||
"""
|
||||
The integer (as a string ) with which the list starts. For example, if a list is initialized as
|
||||
`3. Item`, then the `ol` tag will be assigned an HTML attribute of `starts="3"`. Default: `"1"`.
|
||||
"""
|
||||
LAZY_OL: bool = True
|
||||
""" Ignore `STARTSWITH` if `True`. """
|
||||
SIBLING_TAGS: list[str] = ['ol', 'ul']
|
||||
"""
|
||||
Markdown does not require the type of a new list item match the previous list item type.
|
||||
This is the list of types which can be mixed.
|
||||
"""
|
||||
|
||||
def __init__(self, parser: BlockParser):
|
||||
super().__init__(parser)
|
||||
# Detect an item (`1. item`). `group(1)` contains contents of item.
|
||||
self.RE = re.compile(r'^[ ]{0,%d}\d+\.[ ]+(.*)' % (self.tab_length - 1))
|
||||
# Detect items on secondary lines. they can be of either list type.
|
||||
self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.)|[*+-])[ ]+(.*)' %
|
||||
(self.tab_length - 1))
|
||||
# Detect indented (nested) items of either type
|
||||
self.INDENT_RE = re.compile(r'^[ ]{%d,%d}((\d+\.)|[*+-])[ ]+.*' %
|
||||
(self.tab_length, self.tab_length * 2 - 1))
|
||||
|
||||
def test(self, parent, block):
|
||||
return bool(self.RE.match(block))
|
||||
|
||||
def run(self, parent, blocks):
|
||||
# Check for multiple items in one block.
|
||||
items = self.get_items(blocks.pop(0))
|
||||
sibling = self.lastChild(parent)
|
||||
|
||||
if sibling is not None and sibling.tag in self.SIBLING_TAGS:
|
||||
# Previous block was a list item, so set that as parent
|
||||
lst = sibling
|
||||
# make sure previous item is in a `p` - if the item has text,
|
||||
# then it isn't in a `p`
|
||||
if lst[-1].text:
|
||||
# since it's possible there are other children for this
|
||||
# sibling, we can't just `SubElement` the `p`, we need to
|
||||
# insert it as the first item.
|
||||
p = etree.Element('p')
|
||||
p.text = lst[-1].text
|
||||
lst[-1].text = ''
|
||||
lst[-1].insert(0, p)
|
||||
# if the last item has a tail, then the tail needs to be put in a `p`
|
||||
# likely only when a header is not followed by a blank line
|
||||
lch = self.lastChild(lst[-1])
|
||||
if lch is not None and lch.tail:
|
||||
p = etree.SubElement(lst[-1], 'p')
|
||||
p.text = lch.tail.lstrip()
|
||||
lch.tail = ''
|
||||
|
||||
# parse first block differently as it gets wrapped in a `p`.
|
||||
li = etree.SubElement(lst, 'li')
|
||||
self.parser.state.set('looselist')
|
||||
firstitem = items.pop(0)
|
||||
self.parser.parseBlocks(li, [firstitem])
|
||||
self.parser.state.reset()
|
||||
elif parent.tag in ['ol', 'ul']:
|
||||
# this catches the edge case of a multi-item indented list whose
|
||||
# first item is in a blank parent-list item:
|
||||
# * * subitem1
|
||||
# * subitem2
|
||||
# see also `ListIndentProcessor`
|
||||
lst = parent
|
||||
else:
|
||||
# This is a new list so create parent with appropriate tag.
|
||||
lst = etree.SubElement(parent, self.TAG)
|
||||
# Check if a custom start integer is set
|
||||
if not self.LAZY_OL and self.STARTSWITH != '1':
|
||||
lst.attrib['start'] = self.STARTSWITH
|
||||
|
||||
self.parser.state.set('list')
|
||||
# Loop through items in block, recursively parsing each with the
|
||||
# appropriate parent.
|
||||
for item in items:
|
||||
if item.startswith(' '*self.tab_length):
|
||||
# Item is indented. Parse with last item as parent
|
||||
self.parser.parseBlocks(lst[-1], [item])
|
||||
else:
|
||||
# New item. Create `li` and parse with it as parent
|
||||
li = etree.SubElement(lst, 'li')
|
||||
self.parser.parseBlocks(li, [item])
|
||||
self.parser.state.reset()
|
||||
|
||||
def get_items(self, block: str) -> list[str]:
|
||||
""" Break a block into list items. """
|
||||
items = []
|
||||
for line in block.split('\n'):
|
||||
m = self.CHILD_RE.match(line)
|
||||
if m:
|
||||
# This is a new list item
|
||||
# Check first item for the start index
|
||||
if not items and self.TAG == 'ol':
|
||||
# Detect the integer value of first list item
|
||||
INTEGER_RE = re.compile(r'(\d+)')
|
||||
self.STARTSWITH = INTEGER_RE.match(m.group(1)).group()
|
||||
# Append to the list
|
||||
items.append(m.group(3))
|
||||
elif self.INDENT_RE.match(line):
|
||||
# This is an indented (possibly nested) item.
|
||||
if items[-1].startswith(' '*self.tab_length):
|
||||
# Previous item was indented. Append to that item.
|
||||
items[-1] = '{}\n{}'.format(items[-1], line)
|
||||
else:
|
||||
items.append(line)
|
||||
else:
|
||||
# This is another line of previous item. Append to that item.
|
||||
items[-1] = '{}\n{}'.format(items[-1], line)
|
||||
return items
|
||||
|
||||
|
||||
class UListProcessor(OListProcessor):
|
||||
""" Process unordered list blocks. """
|
||||
|
||||
TAG: str = 'ul'
|
||||
""" The tag used for the the wrapping element. """
|
||||
|
||||
def __init__(self, parser: BlockParser):
|
||||
super().__init__(parser)
|
||||
# Detect an item (`1. item`). `group(1)` contains contents of item.
|
||||
self.RE = re.compile(r'^[ ]{0,%d}[*+-][ ]+(.*)' % (self.tab_length - 1))
|
||||
|
||||
|
||||
class HashHeaderProcessor(BlockProcessor):
|
||||
""" Process Hash Headers. """
|
||||
|
||||
# Detect a header at start of any line in block
|
||||
RE = re.compile(r'(?:^|\n)(?P<level>#{1,6})(?P<header>(?:\\.|[^\\])*?)#*(?:\n|$)')
|
||||
|
||||
def test(self, parent, block):
|
||||
return bool(self.RE.search(block))
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
m = self.RE.search(block)
|
||||
if m:
|
||||
before = block[:m.start()] # All lines before header
|
||||
after = block[m.end():] # All lines after header
|
||||
if before:
|
||||
# As the header was not the first line of the block and the
|
||||
# lines before the header must be parsed first,
|
||||
# recursively parse this lines as a block.
|
||||
self.parser.parseBlocks(parent, [before])
|
||||
# Create header using named groups from RE
|
||||
h = etree.SubElement(parent, 'h%d' % len(m.group('level')))
|
||||
h.text = m.group('header').strip()
|
||||
if after:
|
||||
# Insert remaining lines as first block for future parsing.
|
||||
blocks.insert(0, after)
|
||||
else: # pragma: no cover
|
||||
# This should never happen, but just in case...
|
||||
logger.warn("We've got a problem header: %r" % block)
|
||||
|
||||
|
||||
class SetextHeaderProcessor(BlockProcessor):
|
||||
""" Process Setext-style Headers. """
|
||||
|
||||
# Detect Setext-style header. Must be first 2 lines of block.
|
||||
RE = re.compile(r'^.*?\n[=-]+[ ]*(\n|$)', re.MULTILINE)
|
||||
|
||||
def test(self, parent, block):
|
||||
return bool(self.RE.match(block))
|
||||
|
||||
def run(self, parent, blocks):
|
||||
lines = blocks.pop(0).split('\n')
|
||||
# Determine level. `=` is 1 and `-` is 2.
|
||||
if lines[1].startswith('='):
|
||||
level = 1
|
||||
else:
|
||||
level = 2
|
||||
h = etree.SubElement(parent, 'h%d' % level)
|
||||
h.text = lines[0].strip()
|
||||
if len(lines) > 2:
|
||||
# Block contains additional lines. Add to master blocks for later.
|
||||
blocks.insert(0, '\n'.join(lines[2:]))
|
||||
|
||||
|
||||
class HRProcessor(BlockProcessor):
|
||||
""" Process Horizontal Rules. """
|
||||
|
||||
# Python's `re` module doesn't officially support atomic grouping. However you can fake it.
|
||||
# See https://stackoverflow.com/a/13577411/866026
|
||||
RE = r'^[ ]{0,3}(?=(?P<atomicgroup>(-+[ ]{0,2}){3,}|(_+[ ]{0,2}){3,}|(\*+[ ]{0,2}){3,}))(?P=atomicgroup)[ ]*$'
|
||||
# Detect hr on any line of a block.
|
||||
SEARCH_RE = re.compile(RE, re.MULTILINE)
|
||||
|
||||
def test(self, parent, block):
|
||||
m = self.SEARCH_RE.search(block)
|
||||
if m:
|
||||
# Save match object on class instance so we can use it later.
|
||||
self.match = m
|
||||
return True
|
||||
return False
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
match = self.match
|
||||
# Check for lines in block before `hr`.
|
||||
prelines = block[:match.start()].rstrip('\n')
|
||||
if prelines:
|
||||
# Recursively parse lines before `hr` so they get parsed first.
|
||||
self.parser.parseBlocks(parent, [prelines])
|
||||
# create hr
|
||||
etree.SubElement(parent, 'hr')
|
||||
# check for lines in block after `hr`.
|
||||
postlines = block[match.end():].lstrip('\n')
|
||||
if postlines:
|
||||
# Add lines after `hr` to master blocks for later parsing.
|
||||
blocks.insert(0, postlines)
|
||||
|
||||
|
||||
class EmptyBlockProcessor(BlockProcessor):
|
||||
""" Process blocks that are empty or start with an empty line. """
|
||||
|
||||
def test(self, parent, block):
|
||||
return not block or block.startswith('\n')
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
filler = '\n\n'
|
||||
if block:
|
||||
# Starts with empty line
|
||||
# Only replace a single line.
|
||||
filler = '\n'
|
||||
# Save the rest for later.
|
||||
theRest = block[1:]
|
||||
if theRest:
|
||||
# Add remaining lines to master blocks for later.
|
||||
blocks.insert(0, theRest)
|
||||
sibling = self.lastChild(parent)
|
||||
if (sibling is not None and sibling.tag == 'pre' and
|
||||
len(sibling) and sibling[0].tag == 'code'):
|
||||
# Last block is a code block. Append to preserve whitespace.
|
||||
sibling[0].text = util.AtomicString(
|
||||
'{}{}'.format(sibling[0].text, filler)
|
||||
)
|
||||
|
||||
|
||||
class ReferenceProcessor(BlockProcessor):
|
||||
""" Process link references. """
|
||||
RE = re.compile(
|
||||
r'^[ ]{0,3}\[([^\[\]]*)\]:[ ]*\n?[ ]*([^\s]+)[ ]*(?:\n[ ]*)?((["\'])(.*)\4[ ]*|\((.*)\)[ ]*)?$', re.MULTILINE
|
||||
)
|
||||
|
||||
def test(self, parent, block):
|
||||
return True
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
m = self.RE.search(block)
|
||||
if m:
|
||||
id = m.group(1).strip().lower()
|
||||
link = m.group(2).lstrip('<').rstrip('>')
|
||||
title = m.group(5) or m.group(6)
|
||||
self.parser.md.references[id] = (link, title)
|
||||
if block[m.end():].strip():
|
||||
# Add any content after match back to blocks as separate block
|
||||
blocks.insert(0, block[m.end():].lstrip('\n'))
|
||||
if block[:m.start()].strip():
|
||||
# Add any content before match back to blocks as separate block
|
||||
blocks.insert(0, block[:m.start()].rstrip('\n'))
|
||||
return True
|
||||
# No match. Restore block.
|
||||
blocks.insert(0, block)
|
||||
return False
|
||||
|
||||
|
||||
class ParagraphProcessor(BlockProcessor):
|
||||
""" Process Paragraph blocks. """
|
||||
|
||||
def test(self, parent, block):
|
||||
return True
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
if block.strip():
|
||||
# Not a blank block. Add to parent, otherwise throw it away.
|
||||
if self.parser.state.isstate('list'):
|
||||
# The parent is a tight-list.
|
||||
#
|
||||
# Check for any children. This will likely only happen in a
|
||||
# tight-list when a header isn't followed by a blank line.
|
||||
# For example:
|
||||
#
|
||||
# * # Header
|
||||
# Line 2 of list item - not part of header.
|
||||
sibling = self.lastChild(parent)
|
||||
if sibling is not None:
|
||||
# Insert after sibling.
|
||||
if sibling.tail:
|
||||
sibling.tail = '{}\n{}'.format(sibling.tail, block)
|
||||
else:
|
||||
sibling.tail = '\n%s' % block
|
||||
else:
|
||||
# Append to parent.text
|
||||
if parent.text:
|
||||
parent.text = '{}\n{}'.format(parent.text, block)
|
||||
else:
|
||||
parent.text = block.lstrip()
|
||||
else:
|
||||
# Create a regular paragraph
|
||||
p = etree.SubElement(parent, 'p')
|
||||
p.text = block.lstrip()
|
@ -1,510 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import codecs
|
||||
import sys
|
||||
import logging
|
||||
import importlib
|
||||
from typing import TYPE_CHECKING, Any, Callable, ClassVar, Mapping, Sequence, TextIO
|
||||
from . import util
|
||||
from .preprocessors import build_preprocessors
|
||||
from .blockprocessors import build_block_parser
|
||||
from .treeprocessors import build_treeprocessors
|
||||
from .inlinepatterns import build_inlinepatterns
|
||||
from .postprocessors import build_postprocessors
|
||||
from .extensions import Extension
|
||||
from .serializers import to_html_string, to_xhtml_string
|
||||
from .util import BLOCK_LEVEL_ELEMENTS
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
__all__ = ['Markdown', 'markdown', 'markdownFromFile']
|
||||
|
||||
|
||||
logger = logging.getLogger('MARKDOWN')
|
||||
|
||||
|
||||
class Markdown:
|
||||
"""
|
||||
A parser which converts Markdown to HTML.
|
||||
|
||||
Attributes:
|
||||
Markdown.tab_length (int): The number of spaces which correspond to a single tab. Default: `4`.
|
||||
Markdown.ESCAPED_CHARS (list[str]): List of characters which get the backslash escape treatment.
|
||||
Markdown.block_level_elements (list[str]): List of HTML tags which get treated as block-level elements.
|
||||
See [`markdown.util.BLOCK_LEVEL_ELEMENTS`][] for the full list of elements.
|
||||
Markdown.registeredExtensions (list[Extension]): List of extensions which have called
|
||||
[`registerExtension`][markdown.Markdown.registerExtension] during setup.
|
||||
Markdown.doc_tag (str): Element used to wrap document. Default: `div`.
|
||||
Markdown.stripTopLevelTags (bool): Indicates whether the `doc_tag` should be removed. Default: 'True'.
|
||||
Markdown.references (dict[str, tuple[str, str]]): A mapping of link references found in a parsed document
|
||||
where the key is the reference name and the value is a tuple of the URL and title.
|
||||
Markdown.htmlStash (util.HtmlStash): The instance of the `HtmlStash` used by an instance of this class.
|
||||
Markdown.output_formats (dict[str, Callable[xml.etree.ElementTree.Element]]): A mapping of known output
|
||||
formats by name and their respective serializers. Each serializer must be a callable which accepts an
|
||||
[`Element`][xml.etree.ElementTree.Element] and returns a `str`.
|
||||
Markdown.output_format (str): The output format set by
|
||||
[`set_output_format`][markdown.Markdown.set_output_format].
|
||||
Markdown.serializer (Callable[xml.etree.ElementTree.Element]): The serializer set by
|
||||
[`set_output_format`][markdown.Markdown.set_output_format].
|
||||
Markdown.preprocessors (util.Registry): A collection of [`preprocessors`][markdown.preprocessors].
|
||||
Markdown.parser (blockparser.BlockParser): A collection of [`blockprocessors`][markdown.blockprocessors].
|
||||
Markdown.inlinePatterns (util.Registry): A collection of [`inlinepatterns`][markdown.inlinepatterns].
|
||||
Markdown.treeprocessors (util.Registry): A collection of [`treeprocessors`][markdown.treeprocessors].
|
||||
Markdown.postprocessors (util.Registry): A collection of [`postprocessors`][markdown.postprocessors].
|
||||
|
||||
"""
|
||||
|
||||
doc_tag = "div" # Element used to wrap document - later removed
|
||||
|
||||
output_formats: ClassVar[dict[str, Callable[[Element], str]]] = {
|
||||
'html': to_html_string,
|
||||
'xhtml': to_xhtml_string,
|
||||
}
|
||||
"""
|
||||
A mapping of known output formats by name and their respective serializers. Each serializer must be a
|
||||
callable which accepts an [`Element`][xml.etree.ElementTree.Element] and returns a `str`.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""
|
||||
Creates a new Markdown instance.
|
||||
|
||||
Keyword Arguments:
|
||||
extensions (list[Extension | str]): A list of extensions.
|
||||
|
||||
If an item is an instance of a subclass of [`markdown.extensions.Extension`][],
|
||||
the instance will be used as-is. If an item is of type `str`, it is passed
|
||||
to [`build_extension`][markdown.Markdown.build_extension] with its corresponding
|
||||
`extension_configs` and the returned instance of [`markdown.extensions.Extension`][]
|
||||
is used.
|
||||
extension_configs (dict[str, dict[str, Any]]): Configuration settings for extensions.
|
||||
output_format (str): Format of output. Supported formats are:
|
||||
|
||||
* `xhtml`: Outputs XHTML style tags. Default.
|
||||
* `html`: Outputs HTML style tags.
|
||||
tab_length (int): Length of tabs in the source. Default: `4`
|
||||
|
||||
"""
|
||||
|
||||
self.tab_length: int = kwargs.get('tab_length', 4)
|
||||
|
||||
self.ESCAPED_CHARS: list[str] = [
|
||||
'\\', '`', '*', '_', '{', '}', '[', ']', '(', ')', '>', '#', '+', '-', '.', '!'
|
||||
]
|
||||
""" List of characters which get the backslash escape treatment. """
|
||||
|
||||
self.block_level_elements: list[str] = BLOCK_LEVEL_ELEMENTS.copy()
|
||||
|
||||
self.registeredExtensions: list[Extension] = []
|
||||
self.docType = "" # TODO: Maybe delete this. It does not appear to be used anymore.
|
||||
self.stripTopLevelTags: bool = True
|
||||
|
||||
self.build_parser()
|
||||
|
||||
self.references: dict[str, tuple[str, str]] = {}
|
||||
self.htmlStash: util.HtmlStash = util.HtmlStash()
|
||||
self.registerExtensions(extensions=kwargs.get('extensions', []),
|
||||
configs=kwargs.get('extension_configs', {}))
|
||||
self.set_output_format(kwargs.get('output_format', 'xhtml'))
|
||||
self.reset()
|
||||
|
||||
def build_parser(self) -> Markdown:
|
||||
"""
|
||||
Build the parser from the various parts.
|
||||
|
||||
Assigns a value to each of the following attributes on the class instance:
|
||||
|
||||
* **`Markdown.preprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of
|
||||
[`preprocessors`][markdown.preprocessors].
|
||||
* **`Markdown.parser`** ([`BlockParser`][markdown.blockparser.BlockParser]) -- A collection of
|
||||
[`blockprocessors`][markdown.blockprocessors].
|
||||
* **`Markdown.inlinePatterns`** ([`Registry`][markdown.util.Registry]) -- A collection of
|
||||
[`inlinepatterns`][markdown.inlinepatterns].
|
||||
* **`Markdown.treeprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of
|
||||
[`treeprocessors`][markdown.treeprocessors].
|
||||
* **`Markdown.postprocessors`** ([`Registry`][markdown.util.Registry]) -- A collection of
|
||||
[`postprocessors`][markdown.postprocessors].
|
||||
|
||||
This method could be redefined in a subclass to build a custom parser which is made up of a different
|
||||
combination of processors and patterns.
|
||||
|
||||
"""
|
||||
self.preprocessors = build_preprocessors(self)
|
||||
self.parser = build_block_parser(self)
|
||||
self.inlinePatterns = build_inlinepatterns(self)
|
||||
self.treeprocessors = build_treeprocessors(self)
|
||||
self.postprocessors = build_postprocessors(self)
|
||||
return self
|
||||
|
||||
def registerExtensions(
|
||||
self,
|
||||
extensions: Sequence[Extension | str],
|
||||
configs: Mapping[str, Mapping[str, Any]]
|
||||
) -> Markdown:
|
||||
"""
|
||||
Load a list of extensions into an instance of the `Markdown` class.
|
||||
|
||||
Arguments:
|
||||
extensions (list[Extension | str]): A list of extensions.
|
||||
|
||||
If an item is an instance of a subclass of [`markdown.extensions.Extension`][],
|
||||
the instance will be used as-is. If an item is of type `str`, it is passed
|
||||
to [`build_extension`][markdown.Markdown.build_extension] with its corresponding `configs` and the
|
||||
returned instance of [`markdown.extensions.Extension`][] is used.
|
||||
configs (dict[str, dict[str, Any]]): Configuration settings for extensions.
|
||||
|
||||
"""
|
||||
for ext in extensions:
|
||||
if isinstance(ext, str):
|
||||
ext = self.build_extension(ext, configs.get(ext, {}))
|
||||
if isinstance(ext, Extension):
|
||||
ext.extendMarkdown(self)
|
||||
logger.debug(
|
||||
'Successfully loaded extension "%s.%s".'
|
||||
% (ext.__class__.__module__, ext.__class__.__name__)
|
||||
)
|
||||
elif ext is not None:
|
||||
raise TypeError(
|
||||
'Extension "{}.{}" must be of type: "{}.{}"'.format(
|
||||
ext.__class__.__module__, ext.__class__.__name__,
|
||||
Extension.__module__, Extension.__name__
|
||||
)
|
||||
)
|
||||
return self
|
||||
|
||||
def build_extension(self, ext_name: str, configs: Mapping[str, Any]) -> Extension:
|
||||
"""
|
||||
Build extension from a string name, then return an instance using the given `configs`.
|
||||
|
||||
Arguments:
|
||||
ext_name: Name of extension as a string.
|
||||
configs: Configuration settings for extension.
|
||||
|
||||
Returns:
|
||||
An instance of the extension with the given configuration settings.
|
||||
|
||||
First attempt to load an entry point. The string name must be registered as an entry point in the
|
||||
`markdown.extensions` group which points to a subclass of the [`markdown.extensions.Extension`][] class.
|
||||
If multiple distributions have registered the same name, the first one found is returned.
|
||||
|
||||
If no entry point is found, assume dot notation (`path.to.module:ClassName`). Load the specified class and
|
||||
return an instance. If no class is specified, import the module and call a `makeExtension` function and return
|
||||
the [`markdown.extensions.Extension`][] instance returned by that function.
|
||||
"""
|
||||
configs = dict(configs)
|
||||
|
||||
entry_points = [ep for ep in util.get_installed_extensions() if ep.name == ext_name]
|
||||
if entry_points:
|
||||
ext = entry_points[0].load()
|
||||
return ext(**configs)
|
||||
|
||||
# Get class name (if provided): `path.to.module:ClassName`
|
||||
ext_name, class_name = ext_name.split(':', 1) if ':' in ext_name else (ext_name, '')
|
||||
|
||||
try:
|
||||
module = importlib.import_module(ext_name)
|
||||
logger.debug(
|
||||
'Successfully imported extension module "%s".' % ext_name
|
||||
)
|
||||
except ImportError as e:
|
||||
message = 'Failed loading extension "%s".' % ext_name
|
||||
e.args = (message,) + e.args[1:]
|
||||
raise
|
||||
|
||||
if class_name:
|
||||
# Load given class name from module.
|
||||
return getattr(module, class_name)(**configs)
|
||||
else:
|
||||
# Expect `makeExtension()` function to return a class.
|
||||
try:
|
||||
return module.makeExtension(**configs)
|
||||
except AttributeError as e:
|
||||
message = e.args[0]
|
||||
message = "Failed to initiate extension " \
|
||||
"'%s': %s" % (ext_name, message)
|
||||
e.args = (message,) + e.args[1:]
|
||||
raise
|
||||
|
||||
def registerExtension(self, extension: Extension) -> Markdown:
|
||||
"""
|
||||
Register an extension as having a resettable state.
|
||||
|
||||
Arguments:
|
||||
extension: An instance of the extension to register.
|
||||
|
||||
This should get called once by an extension during setup. A "registered" extension's
|
||||
`reset` method is called by [`Markdown.reset()`][markdown.Markdown.reset]. Not all extensions have or need a
|
||||
resettable state, and so it should not be assumed that all extensions are "registered."
|
||||
|
||||
"""
|
||||
self.registeredExtensions.append(extension)
|
||||
return self
|
||||
|
||||
def reset(self) -> Markdown:
|
||||
"""
|
||||
Resets all state variables to prepare the parser instance for new input.
|
||||
|
||||
Called once upon creation of a class instance. Should be called manually between calls
|
||||
to [`Markdown.convert`][markdown.Markdown.convert].
|
||||
"""
|
||||
self.htmlStash.reset()
|
||||
self.references.clear()
|
||||
|
||||
for extension in self.registeredExtensions:
|
||||
if hasattr(extension, 'reset'):
|
||||
extension.reset()
|
||||
|
||||
return self
|
||||
|
||||
def set_output_format(self, format: str) -> Markdown:
|
||||
"""
|
||||
Set the output format for the class instance.
|
||||
|
||||
Arguments:
|
||||
format: Must be a known value in `Markdown.output_formats`.
|
||||
|
||||
"""
|
||||
self.output_format = format.lower().rstrip('145') # ignore number
|
||||
try:
|
||||
self.serializer = self.output_formats[self.output_format]
|
||||
except KeyError as e:
|
||||
valid_formats = list(self.output_formats.keys())
|
||||
valid_formats.sort()
|
||||
message = 'Invalid Output Format: "%s". Use one of %s.' \
|
||||
% (self.output_format,
|
||||
'"' + '", "'.join(valid_formats) + '"')
|
||||
e.args = (message,) + e.args[1:]
|
||||
raise
|
||||
return self
|
||||
|
||||
# Note: the `tag` argument is type annotated `Any` as ElementTree uses many various objects as tags.
|
||||
# As there is no standardization in ElementTree, the type of a given tag is unpredictable.
|
||||
def is_block_level(self, tag: Any) -> bool:
|
||||
"""
|
||||
Check if the given `tag` is a block level HTML tag.
|
||||
|
||||
Returns `True` for any string listed in `Markdown.block_level_elements`. A `tag` which is
|
||||
not a string always returns `False`.
|
||||
|
||||
"""
|
||||
if isinstance(tag, str):
|
||||
return tag.lower().rstrip('/') in self.block_level_elements
|
||||
# Some ElementTree tags are not strings, so return False.
|
||||
return False
|
||||
|
||||
def convert(self, source: str) -> str:
|
||||
"""
|
||||
Convert a Markdown string to a string in the specified output format.
|
||||
|
||||
Arguments:
|
||||
source: Markdown formatted text as Unicode or ASCII string.
|
||||
|
||||
Returns:
|
||||
A string in the specified output format.
|
||||
|
||||
Markdown parsing takes place in five steps:
|
||||
|
||||
1. A bunch of [`preprocessors`][markdown.preprocessors] munge the input text.
|
||||
2. A [`BlockParser`][markdown.blockparser.BlockParser] parses the high-level structural elements of the
|
||||
pre-processed text into an [`ElementTree`][xml.etree.ElementTree.ElementTree] object.
|
||||
3. A bunch of [`treeprocessors`][markdown.treeprocessors] are run against the
|
||||
[`ElementTree`][xml.etree.ElementTree.ElementTree] object. One such `treeprocessor`
|
||||
([`markdown.treeprocessors.InlineProcessor`][]) runs [`inlinepatterns`][markdown.inlinepatterns]
|
||||
against the [`ElementTree`][xml.etree.ElementTree.ElementTree] object, parsing inline markup.
|
||||
4. Some [`postprocessors`][markdown.postprocessors] are run against the text after the
|
||||
[`ElementTree`][xml.etree.ElementTree.ElementTree] object has been serialized into text.
|
||||
5. The output is returned as a string.
|
||||
|
||||
"""
|
||||
|
||||
# Fix up the source text
|
||||
if not source.strip():
|
||||
return '' # a blank Unicode string
|
||||
|
||||
try:
|
||||
source = str(source)
|
||||
except UnicodeDecodeError as e: # pragma: no cover
|
||||
# Customize error message while maintaining original traceback
|
||||
e.reason += '. -- Note: Markdown only accepts Unicode input!'
|
||||
raise
|
||||
|
||||
# Split into lines and run the line preprocessors.
|
||||
self.lines = source.split("\n")
|
||||
for prep in self.preprocessors:
|
||||
self.lines = prep.run(self.lines)
|
||||
|
||||
# Parse the high-level elements.
|
||||
root = self.parser.parseDocument(self.lines).getroot()
|
||||
|
||||
# Run the tree-processors
|
||||
for treeprocessor in self.treeprocessors:
|
||||
newRoot = treeprocessor.run(root)
|
||||
if newRoot is not None:
|
||||
root = newRoot
|
||||
|
||||
# Serialize _properly_. Strip top-level tags.
|
||||
output = self.serializer(root)
|
||||
if self.stripTopLevelTags:
|
||||
try:
|
||||
start = output.index(
|
||||
'<%s>' % self.doc_tag) + len(self.doc_tag) + 2
|
||||
end = output.rindex('</%s>' % self.doc_tag)
|
||||
output = output[start:end].strip()
|
||||
except ValueError as e: # pragma: no cover
|
||||
if output.strip().endswith('<%s />' % self.doc_tag):
|
||||
# We have an empty document
|
||||
output = ''
|
||||
else:
|
||||
# We have a serious problem
|
||||
raise ValueError('Markdown failed to strip top-level '
|
||||
'tags. Document=%r' % output.strip()) from e
|
||||
|
||||
# Run the text post-processors
|
||||
for pp in self.postprocessors:
|
||||
output = pp.run(output)
|
||||
|
||||
return output.strip()
|
||||
|
||||
def convertFile(
|
||||
self,
|
||||
input: str | TextIO | None = None,
|
||||
output: str | TextIO | None = None,
|
||||
encoding: str | None = None,
|
||||
) -> Markdown:
|
||||
"""
|
||||
Converts a Markdown file and returns the HTML as a Unicode string.
|
||||
|
||||
Decodes the file using the provided encoding (defaults to `utf-8`),
|
||||
passes the file content to markdown, and outputs the HTML to either
|
||||
the provided stream or the file with provided name, using the same
|
||||
encoding as the source file. The
|
||||
[`xmlcharrefreplace`](https://docs.python.org/3/library/codecs.html#error-handlers)
|
||||
error handler is used when encoding the output.
|
||||
|
||||
**Note:** This is the only place that decoding and encoding of Unicode
|
||||
takes place in Python-Markdown. (All other code is Unicode-in /
|
||||
Unicode-out.)
|
||||
|
||||
Arguments:
|
||||
input: File object or path. Reads from `stdin` if `None`.
|
||||
output: File object or path. Writes to `stdout` if `None`.
|
||||
encoding: Encoding of input and output files. Defaults to `utf-8`.
|
||||
|
||||
"""
|
||||
|
||||
encoding = encoding or "utf-8"
|
||||
|
||||
# Read the source
|
||||
if input:
|
||||
if isinstance(input, str):
|
||||
input_file = codecs.open(input, mode="r", encoding=encoding)
|
||||
else:
|
||||
input_file = codecs.getreader(encoding)(input)
|
||||
text = input_file.read()
|
||||
input_file.close()
|
||||
else:
|
||||
text = sys.stdin.read()
|
||||
if not isinstance(text, str): # pragma: no cover
|
||||
text = text.decode(encoding)
|
||||
|
||||
text = text.lstrip('\ufeff') # remove the byte-order mark
|
||||
|
||||
# Convert
|
||||
html = self.convert(text)
|
||||
|
||||
# Write to file or stdout
|
||||
if output:
|
||||
if isinstance(output, str):
|
||||
output_file = codecs.open(output, "w",
|
||||
encoding=encoding,
|
||||
errors="xmlcharrefreplace")
|
||||
output_file.write(html)
|
||||
output_file.close()
|
||||
else:
|
||||
writer = codecs.getwriter(encoding)
|
||||
output_file = writer(output, errors="xmlcharrefreplace")
|
||||
output_file.write(html)
|
||||
# Don't close here. User may want to write more.
|
||||
else:
|
||||
# Encode manually and write bytes to stdout.
|
||||
html = html.encode(encoding, "xmlcharrefreplace")
|
||||
try:
|
||||
# Write bytes directly to buffer (Python 3).
|
||||
sys.stdout.buffer.write(html)
|
||||
except AttributeError: # pragma: no cover
|
||||
# Probably Python 2, which works with bytes by default.
|
||||
sys.stdout.write(html)
|
||||
|
||||
return self
|
||||
|
||||
|
||||
"""
|
||||
EXPORTED FUNCTIONS
|
||||
=============================================================================
|
||||
|
||||
Those are the two functions we really mean to export: `markdown()` and
|
||||
`markdownFromFile()`.
|
||||
"""
|
||||
|
||||
|
||||
def markdown(text: str, **kwargs: Any) -> str:
|
||||
"""
|
||||
Convert a markdown string to HTML and return HTML as a Unicode string.
|
||||
|
||||
This is a shortcut function for [`Markdown`][markdown.Markdown] class to cover the most
|
||||
basic use case. It initializes an instance of [`Markdown`][markdown.Markdown], loads the
|
||||
necessary extensions and runs the parser on the given text.
|
||||
|
||||
Arguments:
|
||||
text: Markdown formatted text as Unicode or ASCII string.
|
||||
|
||||
Keyword arguments:
|
||||
**kwargs: Any arguments accepted by the Markdown class.
|
||||
|
||||
Returns:
|
||||
A string in the specified output format.
|
||||
|
||||
"""
|
||||
md = Markdown(**kwargs)
|
||||
return md.convert(text)
|
||||
|
||||
|
||||
def markdownFromFile(**kwargs: Any):
|
||||
"""
|
||||
Read Markdown text from a file and write output to a file or a stream.
|
||||
|
||||
This is a shortcut function which initializes an instance of [`Markdown`][markdown.Markdown],
|
||||
and calls the [`convertFile`][markdown.Markdown.convertFile] method rather than
|
||||
[`convert`][markdown.Markdown.convert].
|
||||
|
||||
Keyword arguments:
|
||||
input (str | TextIO): A file name or readable object.
|
||||
output (str | TextIO): A file name or writable object.
|
||||
encoding (str): Encoding of input and output.
|
||||
**kwargs: Any arguments accepted by the `Markdown` class.
|
||||
|
||||
"""
|
||||
md = Markdown(**kwargs)
|
||||
md.convertFile(kwargs.get('input', None),
|
||||
kwargs.get('output', None),
|
||||
kwargs.get('encoding', None))
|
@ -1,145 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
Markdown accepts an [`Extension`][markdown.extensions.Extension] instance for each extension. Therefore, each extension
|
||||
must to define a class that extends [`Extension`][markdown.extensions.Extension] and over-rides the
|
||||
[`extendMarkdown`][markdown.extensions.Extension.extendMarkdown] method. Within this class one can manage configuration
|
||||
options for their extension and attach the various processors and patterns which make up an extension to the
|
||||
[`Markdown`][markdown.Markdown] instance.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any, Mapping, Sequence
|
||||
from ..util import parseBoolValue
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
|
||||
|
||||
class Extension:
|
||||
""" Base class for extensions to subclass. """
|
||||
|
||||
config: Mapping[str, list] = {}
|
||||
"""
|
||||
Default configuration for an extension.
|
||||
|
||||
This attribute is to be defined in a subclass and must be of the following format:
|
||||
|
||||
``` python
|
||||
config = {
|
||||
'key': ['value', 'description']
|
||||
}
|
||||
```
|
||||
|
||||
Note that [`setConfig`][markdown.extensions.Extension.setConfig] will raise a [`KeyError`][]
|
||||
if a default is not set for each option.
|
||||
"""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Initiate Extension and set up configs. """
|
||||
self.setConfigs(kwargs)
|
||||
|
||||
def getConfig(self, key: str, default: Any = '') -> Any:
|
||||
"""
|
||||
Return a single configuration option value.
|
||||
|
||||
Arguments:
|
||||
key: The configuration option name.
|
||||
default: Default value to return if key is not set.
|
||||
|
||||
Returns:
|
||||
Value of stored configuration option.
|
||||
"""
|
||||
if key in self.config:
|
||||
return self.config[key][0]
|
||||
else:
|
||||
return default
|
||||
|
||||
def getConfigs(self) -> dict[str, Any]:
|
||||
"""
|
||||
Return all configuration options.
|
||||
|
||||
Returns:
|
||||
All configuration options.
|
||||
"""
|
||||
return {key: self.getConfig(key) for key in self.config.keys()}
|
||||
|
||||
def getConfigInfo(self) -> list[tuple[str, str]]:
|
||||
"""
|
||||
Return descriptions of all configuration options.
|
||||
|
||||
Returns:
|
||||
All descriptions of configuration options.
|
||||
"""
|
||||
return [(key, self.config[key][1]) for key in self.config.keys()]
|
||||
|
||||
def setConfig(self, key: str, value: Any) -> None:
|
||||
"""
|
||||
Set a configuration option.
|
||||
|
||||
If the corresponding default value set in [`config`][markdown.extensions.Extension.config]
|
||||
is a `bool` value or `None`, then `value` is passed through
|
||||
[`parseBoolValue`][markdown.util.parseBoolValue] before being stored.
|
||||
|
||||
Arguments:
|
||||
key: Name of configuration option to set.
|
||||
value: Value to assign to option.
|
||||
|
||||
Raises:
|
||||
KeyError: If `key` is not known.
|
||||
"""
|
||||
if isinstance(self.config[key][0], bool):
|
||||
value = parseBoolValue(value)
|
||||
if self.config[key][0] is None:
|
||||
value = parseBoolValue(value, preserve_none=True)
|
||||
self.config[key][0] = value
|
||||
|
||||
def setConfigs(self, items: Mapping[str, Any] | Sequence[tuple[str, Any]]):
|
||||
"""
|
||||
Loop through a collection of configuration options, passing each to
|
||||
[`setConfig`][markdown.extensions.Extension.setConfig].
|
||||
|
||||
Arguments:
|
||||
items: Collection of configuration options.
|
||||
|
||||
Raises:
|
||||
KeyError: for any unknown key.
|
||||
"""
|
||||
if hasattr(items, 'items'):
|
||||
# it's a dict
|
||||
items = items.items()
|
||||
for key, value in items:
|
||||
self.setConfig(key, value)
|
||||
|
||||
def extendMarkdown(self, md: Markdown) -> None:
|
||||
"""
|
||||
Add the various processors and patterns to the Markdown Instance.
|
||||
|
||||
This method must be overridden by every extension.
|
||||
|
||||
Arguments:
|
||||
md: The Markdown instance.
|
||||
|
||||
"""
|
||||
raise NotImplementedError(
|
||||
'Extension "%s.%s" must define an "extendMarkdown"'
|
||||
'method.' % (self.__class__.__module__, self.__class__.__name__)
|
||||
)
|
@ -1,105 +0,0 @@
|
||||
# Abbreviation Extension for Python-Markdown
|
||||
# ==========================================
|
||||
|
||||
# This extension adds abbreviation handling to Python-Markdown.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/abbreviations
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/)
|
||||
# and [Seemant Kulleen](http://www.kulleen.org/)
|
||||
|
||||
# All changes Copyright 2008-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
This extension adds abbreviation handling to Python-Markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/abbreviations)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..blockprocessors import BlockProcessor
|
||||
from ..inlinepatterns import InlineProcessor
|
||||
from ..util import AtomicString
|
||||
import re
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
|
||||
class AbbrExtension(Extension):
|
||||
""" Abbreviation Extension for Python-Markdown. """
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Insert `AbbrPreprocessor` before `ReferencePreprocessor`. """
|
||||
md.parser.blockprocessors.register(AbbrPreprocessor(md.parser), 'abbr', 16)
|
||||
|
||||
|
||||
class AbbrPreprocessor(BlockProcessor):
|
||||
""" Abbreviation Preprocessor - parse text for abbr references. """
|
||||
|
||||
RE = re.compile(r'^[*]\[(?P<abbr>[^\]]*)\][ ]?:[ ]*\n?[ ]*(?P<title>.*)$', re.MULTILINE)
|
||||
|
||||
def test(self, parent, block):
|
||||
return True
|
||||
|
||||
def run(self, parent, blocks):
|
||||
"""
|
||||
Find and remove all Abbreviation references from the text.
|
||||
Each reference is set as a new `AbbrPattern` in the markdown instance.
|
||||
|
||||
"""
|
||||
block = blocks.pop(0)
|
||||
m = self.RE.search(block)
|
||||
if m:
|
||||
abbr = m.group('abbr').strip()
|
||||
title = m.group('title').strip()
|
||||
self.parser.md.inlinePatterns.register(
|
||||
AbbrInlineProcessor(self._generate_pattern(abbr), title), 'abbr-%s' % abbr, 2
|
||||
)
|
||||
if block[m.end():].strip():
|
||||
# Add any content after match back to blocks as separate block
|
||||
blocks.insert(0, block[m.end():].lstrip('\n'))
|
||||
if block[:m.start()].strip():
|
||||
# Add any content before match back to blocks as separate block
|
||||
blocks.insert(0, block[:m.start()].rstrip('\n'))
|
||||
return True
|
||||
# No match. Restore block.
|
||||
blocks.insert(0, block)
|
||||
return False
|
||||
|
||||
def _generate_pattern(self, text):
|
||||
"""
|
||||
Given a string, returns an regex pattern to match that string.
|
||||
|
||||
'HTML' -> r'(?P<abbr>[H][T][M][L])'
|
||||
|
||||
Note: we force each char as a literal match (in brackets) as we don't
|
||||
know what they will be beforehand.
|
||||
|
||||
"""
|
||||
chars = list(text)
|
||||
for i in range(len(chars)):
|
||||
chars[i] = r'[%s]' % chars[i]
|
||||
return r'(?P<abbr>\b%s\b)' % (r''.join(chars))
|
||||
|
||||
|
||||
class AbbrInlineProcessor(InlineProcessor):
|
||||
""" Abbreviation inline pattern. """
|
||||
|
||||
def __init__(self, pattern, title):
|
||||
super().__init__(pattern)
|
||||
self.title = title
|
||||
|
||||
def handleMatch(self, m, data):
|
||||
abbr = etree.Element('abbr')
|
||||
abbr.text = AtomicString(m.group('abbr'))
|
||||
abbr.set('title', self.title)
|
||||
return abbr, m.start(0), m.end(0)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return AbbrExtension(**kwargs)
|
@ -1,179 +0,0 @@
|
||||
# Admonition extension for Python-Markdown
|
||||
# ========================================
|
||||
|
||||
# Adds rST-style admonitions. Inspired by [rST][] feature with the same name.
|
||||
|
||||
# [rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/admonition
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright [Tiago Serafim](https://www.tiagoserafim.com/).
|
||||
|
||||
# All changes Copyright The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
|
||||
"""
|
||||
Adds rST-style admonitions. Inspired by [rST][] feature with the same name.
|
||||
|
||||
[rST]: http://docutils.sourceforge.net/docs/ref/rst/directives.html#specific-admonitions
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/admonition)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..blockprocessors import BlockProcessor
|
||||
import xml.etree.ElementTree as etree
|
||||
import re
|
||||
|
||||
|
||||
class AdmonitionExtension(Extension):
|
||||
""" Admonition extension for Python-Markdown. """
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add Admonition to Markdown instance. """
|
||||
md.registerExtension(self)
|
||||
|
||||
md.parser.blockprocessors.register(AdmonitionProcessor(md.parser), 'admonition', 105)
|
||||
|
||||
|
||||
class AdmonitionProcessor(BlockProcessor):
|
||||
|
||||
CLASSNAME = 'admonition'
|
||||
CLASSNAME_TITLE = 'admonition-title'
|
||||
RE = re.compile(r'(?:^|\n)!!! ?([\w\-]+(?: +[\w\-]+)*)(?: +"(.*?)")? *(?:\n|$)')
|
||||
RE_SPACES = re.compile(' +')
|
||||
|
||||
def __init__(self, parser):
|
||||
"""Initialization."""
|
||||
|
||||
super().__init__(parser)
|
||||
|
||||
self.current_sibling = None
|
||||
self.content_indention = 0
|
||||
|
||||
def parse_content(self, parent, block):
|
||||
"""Get sibling admonition.
|
||||
|
||||
Retrieve the appropriate sibling element. This can get tricky when
|
||||
dealing with lists.
|
||||
|
||||
"""
|
||||
|
||||
old_block = block
|
||||
the_rest = ''
|
||||
|
||||
# We already acquired the block via test
|
||||
if self.current_sibling is not None:
|
||||
sibling = self.current_sibling
|
||||
block, the_rest = self.detab(block, self.content_indent)
|
||||
self.current_sibling = None
|
||||
self.content_indent = 0
|
||||
return sibling, block, the_rest
|
||||
|
||||
sibling = self.lastChild(parent)
|
||||
|
||||
if sibling is None or sibling.tag != 'div' or sibling.get('class', '').find(self.CLASSNAME) == -1:
|
||||
sibling = None
|
||||
else:
|
||||
# If the last child is a list and the content is sufficiently indented
|
||||
# to be under it, then the content's sibling is in the list.
|
||||
last_child = self.lastChild(sibling)
|
||||
indent = 0
|
||||
while last_child is not None:
|
||||
if (
|
||||
sibling is not None and block.startswith(' ' * self.tab_length * 2) and
|
||||
last_child is not None and last_child.tag in ('ul', 'ol', 'dl')
|
||||
):
|
||||
|
||||
# The expectation is that we'll find an `<li>` or `<dt>`.
|
||||
# We should get its last child as well.
|
||||
sibling = self.lastChild(last_child)
|
||||
last_child = self.lastChild(sibling) if sibling is not None else None
|
||||
|
||||
# Context has been lost at this point, so we must adjust the
|
||||
# text's indentation level so it will be evaluated correctly
|
||||
# under the list.
|
||||
block = block[self.tab_length:]
|
||||
indent += self.tab_length
|
||||
else:
|
||||
last_child = None
|
||||
|
||||
if not block.startswith(' ' * self.tab_length):
|
||||
sibling = None
|
||||
|
||||
if sibling is not None:
|
||||
indent += self.tab_length
|
||||
block, the_rest = self.detab(old_block, indent)
|
||||
self.current_sibling = sibling
|
||||
self.content_indent = indent
|
||||
|
||||
return sibling, block, the_rest
|
||||
|
||||
def test(self, parent, block):
|
||||
|
||||
if self.RE.search(block):
|
||||
return True
|
||||
else:
|
||||
return self.parse_content(parent, block)[0] is not None
|
||||
|
||||
def run(self, parent, blocks):
|
||||
block = blocks.pop(0)
|
||||
m = self.RE.search(block)
|
||||
|
||||
if m:
|
||||
if m.start() > 0:
|
||||
self.parser.parseBlocks(parent, [block[:m.start()]])
|
||||
block = block[m.end():] # removes the first line
|
||||
block, theRest = self.detab(block)
|
||||
else:
|
||||
sibling, block, theRest = self.parse_content(parent, block)
|
||||
|
||||
if m:
|
||||
klass, title = self.get_class_and_title(m)
|
||||
div = etree.SubElement(parent, 'div')
|
||||
div.set('class', '{} {}'.format(self.CLASSNAME, klass))
|
||||
if title:
|
||||
p = etree.SubElement(div, 'p')
|
||||
p.text = title
|
||||
p.set('class', self.CLASSNAME_TITLE)
|
||||
else:
|
||||
# Sibling is a list item, but we need to wrap it's content should be wrapped in <p>
|
||||
if sibling.tag in ('li', 'dd') and sibling.text:
|
||||
text = sibling.text
|
||||
sibling.text = ''
|
||||
p = etree.SubElement(sibling, 'p')
|
||||
p.text = text
|
||||
|
||||
div = sibling
|
||||
|
||||
self.parser.parseChunk(div, block)
|
||||
|
||||
if theRest:
|
||||
# This block contained unindented line(s) after the first indented
|
||||
# line. Insert these lines as the first block of the master blocks
|
||||
# list for future processing.
|
||||
blocks.insert(0, theRest)
|
||||
|
||||
def get_class_and_title(self, match):
|
||||
klass, title = match.group(1).lower(), match.group(2)
|
||||
klass = self.RE_SPACES.sub(' ', klass)
|
||||
if title is None:
|
||||
# no title was provided, use the capitalized class name as title
|
||||
# e.g.: `!!! note` will render
|
||||
# `<p class="admonition-title">Note</p>`
|
||||
title = klass.split(' ', 1)[0].capitalize()
|
||||
elif title == '':
|
||||
# an explicit blank title should not be rendered
|
||||
# e.g.: `!!! warning ""` will *not* render `p` with a title
|
||||
title = None
|
||||
return klass, title
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return AdmonitionExtension(**kwargs)
|
@ -1,179 +0,0 @@
|
||||
# Attribute List Extension for Python-Markdown
|
||||
# ============================================
|
||||
|
||||
# Adds attribute list syntax. Inspired by
|
||||
# [Maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s
|
||||
# feature of the same name.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/attr_list
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2011 [Waylan Limberg](http://achinghead.com/).
|
||||
|
||||
# All changes Copyright 2011-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Adds attribute list syntax. Inspired by
|
||||
[Maruku](http://maruku.rubyforge.org/proposal.html#attribute_lists)'s
|
||||
feature of the same name.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/attr_list)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from . import Extension
|
||||
from ..treeprocessors import Treeprocessor
|
||||
import re
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from xml.etree.ElementTree import Element
|
||||
|
||||
|
||||
def _handle_double_quote(s, t):
|
||||
k, v = t.split('=', 1)
|
||||
return k, v.strip('"')
|
||||
|
||||
|
||||
def _handle_single_quote(s, t):
|
||||
k, v = t.split('=', 1)
|
||||
return k, v.strip("'")
|
||||
|
||||
|
||||
def _handle_key_value(s, t):
|
||||
return t.split('=', 1)
|
||||
|
||||
|
||||
def _handle_word(s, t):
|
||||
if t.startswith('.'):
|
||||
return '.', t[1:]
|
||||
if t.startswith('#'):
|
||||
return 'id', t[1:]
|
||||
return t, t
|
||||
|
||||
|
||||
_scanner = re.Scanner([
|
||||
(r'[^ =]+=".*?"', _handle_double_quote),
|
||||
(r"[^ =]+='.*?'", _handle_single_quote),
|
||||
(r'[^ =]+=[^ =]+', _handle_key_value),
|
||||
(r'[^ =]+', _handle_word),
|
||||
(r' ', None)
|
||||
])
|
||||
|
||||
|
||||
def get_attrs(str: str) -> list[tuple[str, str]]:
|
||||
""" Parse attribute list and return a list of attribute tuples. """
|
||||
return _scanner.scan(str)[0]
|
||||
|
||||
|
||||
def isheader(elem: Element) -> bool:
|
||||
return elem.tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6']
|
||||
|
||||
|
||||
class AttrListTreeprocessor(Treeprocessor):
|
||||
|
||||
BASE_RE = r'\{\:?[ ]*([^\}\n ][^\}\n]*)[ ]*\}'
|
||||
HEADER_RE = re.compile(r'[ ]+{}[ ]*$'.format(BASE_RE))
|
||||
BLOCK_RE = re.compile(r'\n[ ]*{}[ ]*$'.format(BASE_RE))
|
||||
INLINE_RE = re.compile(r'^{}'.format(BASE_RE))
|
||||
NAME_RE = re.compile(r'[^A-Z_a-z\u00c0-\u00d6\u00d8-\u00f6\u00f8-\u02ff'
|
||||
r'\u0370-\u037d\u037f-\u1fff\u200c-\u200d'
|
||||
r'\u2070-\u218f\u2c00-\u2fef\u3001-\ud7ff'
|
||||
r'\uf900-\ufdcf\ufdf0-\ufffd'
|
||||
r'\:\-\.0-9\u00b7\u0300-\u036f\u203f-\u2040]+')
|
||||
|
||||
def run(self, doc: Element):
|
||||
for elem in doc.iter():
|
||||
if self.md.is_block_level(elem.tag):
|
||||
# Block level: check for `attrs` on last line of text
|
||||
RE = self.BLOCK_RE
|
||||
if isheader(elem) or elem.tag in ['dt', 'td', 'th']:
|
||||
# header, def-term, or table cell: check for attributes at end of element
|
||||
RE = self.HEADER_RE
|
||||
if len(elem) and elem.tag == 'li':
|
||||
# special case list items. children may include a `ul` or `ol`.
|
||||
pos = None
|
||||
# find the `ul` or `ol` position
|
||||
for i, child in enumerate(elem):
|
||||
if child.tag in ['ul', 'ol']:
|
||||
pos = i
|
||||
break
|
||||
if pos is None and elem[-1].tail:
|
||||
# use tail of last child. no `ul` or `ol`.
|
||||
m = RE.search(elem[-1].tail)
|
||||
if m:
|
||||
self.assign_attrs(elem, m.group(1))
|
||||
elem[-1].tail = elem[-1].tail[:m.start()]
|
||||
elif pos is not None and pos > 0 and elem[pos-1].tail:
|
||||
# use tail of last child before `ul` or `ol`
|
||||
m = RE.search(elem[pos-1].tail)
|
||||
if m:
|
||||
self.assign_attrs(elem, m.group(1))
|
||||
elem[pos-1].tail = elem[pos-1].tail[:m.start()]
|
||||
elif elem.text:
|
||||
# use text. `ul` is first child.
|
||||
m = RE.search(elem.text)
|
||||
if m:
|
||||
self.assign_attrs(elem, m.group(1))
|
||||
elem.text = elem.text[:m.start()]
|
||||
elif len(elem) and elem[-1].tail:
|
||||
# has children. Get from tail of last child
|
||||
m = RE.search(elem[-1].tail)
|
||||
if m:
|
||||
self.assign_attrs(elem, m.group(1))
|
||||
elem[-1].tail = elem[-1].tail[:m.start()]
|
||||
if isheader(elem):
|
||||
# clean up trailing #s
|
||||
elem[-1].tail = elem[-1].tail.rstrip('#').rstrip()
|
||||
elif elem.text:
|
||||
# no children. Get from text.
|
||||
m = RE.search(elem.text)
|
||||
if m:
|
||||
self.assign_attrs(elem, m.group(1))
|
||||
elem.text = elem.text[:m.start()]
|
||||
if isheader(elem):
|
||||
# clean up trailing #s
|
||||
elem.text = elem.text.rstrip('#').rstrip()
|
||||
else:
|
||||
# inline: check for `attrs` at start of tail
|
||||
if elem.tail:
|
||||
m = self.INLINE_RE.match(elem.tail)
|
||||
if m:
|
||||
self.assign_attrs(elem, m.group(1))
|
||||
elem.tail = elem.tail[m.end():]
|
||||
|
||||
def assign_attrs(self, elem: Element, attrs: str) -> None:
|
||||
""" Assign `attrs` to element. """
|
||||
for k, v in get_attrs(attrs):
|
||||
if k == '.':
|
||||
# add to class
|
||||
cls = elem.get('class')
|
||||
if cls:
|
||||
elem.set('class', '{} {}'.format(cls, v))
|
||||
else:
|
||||
elem.set('class', v)
|
||||
else:
|
||||
# assign attribute `k` with `v`
|
||||
elem.set(self.sanitize_name(k), v)
|
||||
|
||||
def sanitize_name(self, name: str) -> str:
|
||||
"""
|
||||
Sanitize name as 'an XML Name, minus the ":"'.
|
||||
See https://www.w3.org/TR/REC-xml-names/#NT-NCName
|
||||
"""
|
||||
return self.NAME_RE.sub('_', name)
|
||||
|
||||
|
||||
class AttrListExtension(Extension):
|
||||
""" Attribute List extension for Python-Markdown """
|
||||
def extendMarkdown(self, md):
|
||||
md.treeprocessors.register(AttrListTreeprocessor(md), 'attr_list', 8)
|
||||
md.registerExtension(self)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return AttrListExtension(**kwargs)
|
@ -1,338 +0,0 @@
|
||||
# CodeHilite Extension for Python-Markdown
|
||||
# ========================================
|
||||
|
||||
# Adds code/syntax highlighting to standard Python-Markdown code blocks.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/code_hilite
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2006-2008 [Waylan Limberg](http://achinghead.com/).
|
||||
|
||||
# All changes Copyright 2008-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Adds code/syntax highlighting to standard Python-Markdown code blocks.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/code_hilite)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..treeprocessors import Treeprocessor
|
||||
from ..util import parseBoolValue
|
||||
|
||||
try: # pragma: no cover
|
||||
from pygments import highlight
|
||||
from pygments.lexers import get_lexer_by_name, guess_lexer
|
||||
from pygments.formatters import get_formatter_by_name
|
||||
from pygments.util import ClassNotFound
|
||||
pygments = True
|
||||
except ImportError: # pragma: no cover
|
||||
pygments = False
|
||||
|
||||
|
||||
def parse_hl_lines(expr: str) -> list[int]:
|
||||
"""Support our syntax for emphasizing certain lines of code.
|
||||
|
||||
`expr` should be like '1 2' to emphasize lines 1 and 2 of a code block.
|
||||
Returns a list of integers, the line numbers to emphasize.
|
||||
"""
|
||||
if not expr:
|
||||
return []
|
||||
|
||||
try:
|
||||
return list(map(int, expr.split()))
|
||||
except ValueError: # pragma: no cover
|
||||
return []
|
||||
|
||||
|
||||
# ------------------ The Main CodeHilite Class ----------------------
|
||||
class CodeHilite:
|
||||
"""
|
||||
Determine language of source code, and pass it on to the Pygments highlighter.
|
||||
|
||||
Usage:
|
||||
|
||||
```python
|
||||
code = CodeHilite(src=some_code, lang='python')
|
||||
html = code.hilite()
|
||||
```
|
||||
|
||||
Arguments:
|
||||
src: Source string or any object with a `.readline` attribute.
|
||||
|
||||
Keyword arguments:
|
||||
lang (str): String name of Pygments lexer to use for highlighting. Default: `None`.
|
||||
guess_lang (bool): Auto-detect which lexer to use.
|
||||
Ignored if `lang` is set to a valid value. Default: `True`.
|
||||
use_pygments (bool): Pass code to Pygments for code highlighting. If `False`, the code is
|
||||
instead wrapped for highlighting by a JavaScript library. Default: `True`.
|
||||
pygments_formatter (str): The name of a Pygments formatter or a formatter class used for
|
||||
highlighting the code blocks. Default: `html`.
|
||||
linenums (bool): An alias to Pygments `linenos` formatter option. Default: `None`.
|
||||
css_class (str): An alias to Pygments `cssclass` formatter option. Default: 'codehilite'.
|
||||
lang_prefix (str): Prefix prepended to the language. Default: "language-".
|
||||
|
||||
Other Options:
|
||||
|
||||
Any other options are accepted and passed on to the lexer and formatter. Therefore,
|
||||
valid options include any options which are accepted by the `html` formatter or
|
||||
whichever lexer the code's language uses. Note that most lexers do not have any
|
||||
options. However, a few have very useful options, such as PHP's `startinline` option.
|
||||
Any invalid options are ignored without error.
|
||||
|
||||
* **Formatter options**: <https://pygments.org/docs/formatters/#HtmlFormatter>
|
||||
* **Lexer Options**: <https://pygments.org/docs/lexers/>
|
||||
|
||||
Additionally, when Pygments is enabled, the code's language is passed to the
|
||||
formatter as an extra option `lang_str`, whose value being `{lang_prefix}{lang}`.
|
||||
This option has no effect to the Pygments' builtin formatters.
|
||||
|
||||
Advanced Usage:
|
||||
|
||||
```python
|
||||
code = CodeHilite(
|
||||
src = some_code,
|
||||
lang = 'php',
|
||||
startinline = True, # Lexer option. Snippet does not start with `<?php`.
|
||||
linenostart = 42, # Formatter option. Snippet starts on line 42.
|
||||
hl_lines = [45, 49, 50], # Formatter option. Highlight lines 45, 49, and 50.
|
||||
linenos = 'inline' # Formatter option. Avoid alignment problems.
|
||||
)
|
||||
html = code.hilite()
|
||||
```
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, src: str, **options):
|
||||
self.src = src
|
||||
self.lang = options.pop('lang', None)
|
||||
self.guess_lang = options.pop('guess_lang', True)
|
||||
self.use_pygments = options.pop('use_pygments', True)
|
||||
self.lang_prefix = options.pop('lang_prefix', 'language-')
|
||||
self.pygments_formatter = options.pop('pygments_formatter', 'html')
|
||||
|
||||
if 'linenos' not in options:
|
||||
options['linenos'] = options.pop('linenums', None)
|
||||
if 'cssclass' not in options:
|
||||
options['cssclass'] = options.pop('css_class', 'codehilite')
|
||||
if 'wrapcode' not in options:
|
||||
# Override Pygments default
|
||||
options['wrapcode'] = True
|
||||
# Disallow use of `full` option
|
||||
options['full'] = False
|
||||
|
||||
self.options = options
|
||||
|
||||
def hilite(self, shebang=True) -> str:
|
||||
"""
|
||||
Pass code to the [Pygments](https://pygments.org/) highlighter with
|
||||
optional line numbers. The output should then be styled with CSS to
|
||||
your liking. No styles are applied by default - only styling hooks
|
||||
(i.e.: `<span class="k">`).
|
||||
|
||||
returns : A string of html.
|
||||
|
||||
"""
|
||||
|
||||
self.src = self.src.strip('\n')
|
||||
|
||||
if self.lang is None and shebang:
|
||||
self._parseHeader()
|
||||
|
||||
if pygments and self.use_pygments:
|
||||
try:
|
||||
lexer = get_lexer_by_name(self.lang, **self.options)
|
||||
except ValueError:
|
||||
try:
|
||||
if self.guess_lang:
|
||||
lexer = guess_lexer(self.src, **self.options)
|
||||
else:
|
||||
lexer = get_lexer_by_name('text', **self.options)
|
||||
except ValueError: # pragma: no cover
|
||||
lexer = get_lexer_by_name('text', **self.options)
|
||||
if not self.lang:
|
||||
# Use the guessed lexer's language instead
|
||||
self.lang = lexer.aliases[0]
|
||||
lang_str = f'{self.lang_prefix}{self.lang}'
|
||||
if isinstance(self.pygments_formatter, str):
|
||||
try:
|
||||
formatter = get_formatter_by_name(self.pygments_formatter, **self.options)
|
||||
except ClassNotFound:
|
||||
formatter = get_formatter_by_name('html', **self.options)
|
||||
else:
|
||||
formatter = self.pygments_formatter(lang_str=lang_str, **self.options)
|
||||
return highlight(self.src, lexer, formatter)
|
||||
else:
|
||||
# just escape and build markup usable by JavaScript highlighting libraries
|
||||
txt = self.src.replace('&', '&')
|
||||
txt = txt.replace('<', '<')
|
||||
txt = txt.replace('>', '>')
|
||||
txt = txt.replace('"', '"')
|
||||
classes = []
|
||||
if self.lang:
|
||||
classes.append('{}{}'.format(self.lang_prefix, self.lang))
|
||||
if self.options['linenos']:
|
||||
classes.append('linenums')
|
||||
class_str = ''
|
||||
if classes:
|
||||
class_str = ' class="{}"'.format(' '.join(classes))
|
||||
return '<pre class="{}"><code{}>{}\n</code></pre>\n'.format(
|
||||
self.options['cssclass'],
|
||||
class_str,
|
||||
txt
|
||||
)
|
||||
|
||||
def _parseHeader(self):
|
||||
"""
|
||||
Determines language of a code block from shebang line and whether the
|
||||
said line should be removed or left in place. If the shebang line
|
||||
contains a path (even a single /) then it is assumed to be a real
|
||||
shebang line and left alone. However, if no path is given
|
||||
(e.i.: `#!python` or `:::python`) then it is assumed to be a mock shebang
|
||||
for language identification of a code fragment and removed from the
|
||||
code block prior to processing for code highlighting. When a mock
|
||||
shebang (e.i: `#!python`) is found, line numbering is turned on. When
|
||||
colons are found in place of a shebang (e.i.: `:::python`), line
|
||||
numbering is left in the current state - off by default.
|
||||
|
||||
Also parses optional list of highlight lines, like:
|
||||
|
||||
:::python hl_lines="1 3"
|
||||
"""
|
||||
|
||||
import re
|
||||
|
||||
# split text into lines
|
||||
lines = self.src.split("\n")
|
||||
# pull first line to examine
|
||||
fl = lines.pop(0)
|
||||
|
||||
c = re.compile(r'''
|
||||
(?:(?:^::+)|(?P<shebang>^[#]!)) # Shebang or 2 or more colons
|
||||
(?P<path>(?:/\w+)*[/ ])? # Zero or 1 path
|
||||
(?P<lang>[\w#.+-]*) # The language
|
||||
\s* # Arbitrary whitespace
|
||||
# Optional highlight lines, single- or double-quote-delimited
|
||||
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot))?
|
||||
''', re.VERBOSE)
|
||||
# search first line for shebang
|
||||
m = c.search(fl)
|
||||
if m:
|
||||
# we have a match
|
||||
try:
|
||||
self.lang = m.group('lang').lower()
|
||||
except IndexError: # pragma: no cover
|
||||
self.lang = None
|
||||
if m.group('path'):
|
||||
# path exists - restore first line
|
||||
lines.insert(0, fl)
|
||||
if self.options['linenos'] is None and m.group('shebang'):
|
||||
# Overridable and Shebang exists - use line numbers
|
||||
self.options['linenos'] = True
|
||||
|
||||
self.options['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
|
||||
else:
|
||||
# No match
|
||||
lines.insert(0, fl)
|
||||
|
||||
self.src = "\n".join(lines).strip("\n")
|
||||
|
||||
|
||||
# ------------------ The Markdown Extension -------------------------------
|
||||
|
||||
|
||||
class HiliteTreeprocessor(Treeprocessor):
|
||||
""" Highlight source code in code blocks. """
|
||||
|
||||
def code_unescape(self, text):
|
||||
"""Unescape code."""
|
||||
text = text.replace("<", "<")
|
||||
text = text.replace(">", ">")
|
||||
# Escaped '&' should be replaced at the end to avoid
|
||||
# conflicting with < and >.
|
||||
text = text.replace("&", "&")
|
||||
return text
|
||||
|
||||
def run(self, root):
|
||||
""" Find code blocks and store in `htmlStash`. """
|
||||
blocks = root.iter('pre')
|
||||
for block in blocks:
|
||||
if len(block) == 1 and block[0].tag == 'code':
|
||||
local_config = self.config.copy()
|
||||
code = CodeHilite(
|
||||
self.code_unescape(block[0].text),
|
||||
tab_length=self.md.tab_length,
|
||||
style=local_config.pop('pygments_style', 'default'),
|
||||
**local_config
|
||||
)
|
||||
placeholder = self.md.htmlStash.store(code.hilite())
|
||||
# Clear code block in `etree` instance
|
||||
block.clear()
|
||||
# Change to `p` element which will later
|
||||
# be removed when inserting raw html
|
||||
block.tag = 'p'
|
||||
block.text = placeholder
|
||||
|
||||
|
||||
class CodeHiliteExtension(Extension):
|
||||
""" Add source code highlighting to markdown code blocks. """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
# define default configs
|
||||
self.config = {
|
||||
'linenums': [
|
||||
None, "Use lines numbers. True|table|inline=yes, False=no, None=auto. Default: `None`."
|
||||
],
|
||||
'guess_lang': [
|
||||
True, "Automatic language detection - Default: `True`."
|
||||
],
|
||||
'css_class': [
|
||||
"codehilite", "Set class name for wrapper <div> - Default: `codehilite`."
|
||||
],
|
||||
'pygments_style': [
|
||||
'default', 'Pygments HTML Formatter Style (Colorscheme). Default: `default`.'
|
||||
],
|
||||
'noclasses': [
|
||||
False, 'Use inline styles instead of CSS classes - Default `False`.'
|
||||
],
|
||||
'use_pygments': [
|
||||
True, 'Highlight code blocks with pygments. Disable if using a JavaScript library. Default: `True`.'
|
||||
],
|
||||
'lang_prefix': [
|
||||
'language-', 'Prefix prepended to the language when `use_pygments` is false. Default: `language-`.'
|
||||
],
|
||||
'pygments_formatter': [
|
||||
'html', 'Use a specific formatter for Pygments highlighting. Default: `html`.'
|
||||
],
|
||||
}
|
||||
""" Default configuration options. """
|
||||
|
||||
for key, value in kwargs.items():
|
||||
if key in self.config:
|
||||
self.setConfig(key, value)
|
||||
else:
|
||||
# manually set unknown keywords.
|
||||
if isinstance(value, str):
|
||||
try:
|
||||
# Attempt to parse `str` as a boolean value
|
||||
value = parseBoolValue(value, preserve_none=True)
|
||||
except ValueError:
|
||||
pass # Assume it's not a boolean value. Use as-is.
|
||||
self.config[key] = [value, '']
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add `HilitePostprocessor` to Markdown instance. """
|
||||
hiliter = HiliteTreeprocessor(md)
|
||||
hiliter.config = self.getConfigs()
|
||||
md.treeprocessors.register(hiliter, 'hilite', 30)
|
||||
|
||||
md.registerExtension(self)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return CodeHiliteExtension(**kwargs)
|
@ -1,119 +0,0 @@
|
||||
# Definition List Extension for Python-Markdown
|
||||
# =============================================
|
||||
|
||||
# Adds parsing of Definition Lists to Python-Markdown.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/definition_lists
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2008 [Waylan Limberg](http://achinghead.com)
|
||||
|
||||
# All changes Copyright 2008-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Adds parsing of Definition Lists to Python-Markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/definition_lists)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..blockprocessors import BlockProcessor, ListIndentProcessor
|
||||
import xml.etree.ElementTree as etree
|
||||
import re
|
||||
|
||||
|
||||
class DefListProcessor(BlockProcessor):
|
||||
""" Process Definition Lists. """
|
||||
|
||||
RE = re.compile(r'(^|\n)[ ]{0,3}:[ ]{1,3}(.*?)(\n|$)')
|
||||
NO_INDENT_RE = re.compile(r'^[ ]{0,3}[^ :]')
|
||||
|
||||
def test(self, parent, block):
|
||||
return bool(self.RE.search(block))
|
||||
|
||||
def run(self, parent, blocks):
|
||||
|
||||
raw_block = blocks.pop(0)
|
||||
m = self.RE.search(raw_block)
|
||||
terms = [term.strip() for term in
|
||||
raw_block[:m.start()].split('\n') if term.strip()]
|
||||
block = raw_block[m.end():]
|
||||
no_indent = self.NO_INDENT_RE.match(block)
|
||||
if no_indent:
|
||||
d, theRest = (block, None)
|
||||
else:
|
||||
d, theRest = self.detab(block)
|
||||
if d:
|
||||
d = '{}\n{}'.format(m.group(2), d)
|
||||
else:
|
||||
d = m.group(2)
|
||||
sibling = self.lastChild(parent)
|
||||
if not terms and sibling is None:
|
||||
# This is not a definition item. Most likely a paragraph that
|
||||
# starts with a colon at the beginning of a document or list.
|
||||
blocks.insert(0, raw_block)
|
||||
return False
|
||||
if not terms and sibling.tag == 'p':
|
||||
# The previous paragraph contains the terms
|
||||
state = 'looselist'
|
||||
terms = sibling.text.split('\n')
|
||||
parent.remove(sibling)
|
||||
# Acquire new sibling
|
||||
sibling = self.lastChild(parent)
|
||||
else:
|
||||
state = 'list'
|
||||
|
||||
if sibling is not None and sibling.tag == 'dl':
|
||||
# This is another item on an existing list
|
||||
dl = sibling
|
||||
if not terms and len(dl) and dl[-1].tag == 'dd' and len(dl[-1]):
|
||||
state = 'looselist'
|
||||
else:
|
||||
# This is a new list
|
||||
dl = etree.SubElement(parent, 'dl')
|
||||
# Add terms
|
||||
for term in terms:
|
||||
dt = etree.SubElement(dl, 'dt')
|
||||
dt.text = term
|
||||
# Add definition
|
||||
self.parser.state.set(state)
|
||||
dd = etree.SubElement(dl, 'dd')
|
||||
self.parser.parseBlocks(dd, [d])
|
||||
self.parser.state.reset()
|
||||
|
||||
if theRest:
|
||||
blocks.insert(0, theRest)
|
||||
|
||||
|
||||
class DefListIndentProcessor(ListIndentProcessor):
|
||||
""" Process indented children of definition list items. """
|
||||
|
||||
# Definition lists need to be aware of all list types
|
||||
ITEM_TYPES = ['dd', 'li']
|
||||
""" Include `dd` in list item types. """
|
||||
LIST_TYPES = ['dl', 'ol', 'ul']
|
||||
""" Include `dl` is list types. """
|
||||
|
||||
def create_item(self, parent, block):
|
||||
""" Create a new `dd` or `li` (depending on parent) and parse the block with it as the parent. """
|
||||
|
||||
dd = etree.SubElement(parent, 'dd')
|
||||
self.parser.parseBlocks(dd, [block])
|
||||
|
||||
|
||||
class DefListExtension(Extension):
|
||||
""" Add definition lists to Markdown. """
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add an instance of `DefListProcessor` to `BlockParser`. """
|
||||
md.parser.blockprocessors.register(DefListIndentProcessor(md.parser), 'defindent', 85)
|
||||
md.parser.blockprocessors.register(DefListProcessor(md.parser), 'deflist', 25)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return DefListExtension(**kwargs)
|
@ -1,66 +0,0 @@
|
||||
# Python-Markdown Extra Extension
|
||||
# ===============================
|
||||
|
||||
# A compilation of various Python-Markdown extensions that imitates
|
||||
# [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/).
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/extra
|
||||
# for documentation.
|
||||
|
||||
# Copyright The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
A compilation of various Python-Markdown extensions that imitates
|
||||
[PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/).
|
||||
|
||||
Note that each of the individual extensions still need to be available
|
||||
on your `PYTHONPATH`. This extension simply wraps them all up as a
|
||||
convenience so that only one extension needs to be listed when
|
||||
initiating Markdown. See the documentation for each individual
|
||||
extension for specifics about that extension.
|
||||
|
||||
There may be additional extensions that are distributed with
|
||||
Python-Markdown that are not included here in Extra. Those extensions
|
||||
are not part of PHP Markdown Extra, and therefore, not part of
|
||||
Python-Markdown Extra. If you really would like Extra to include
|
||||
additional extensions, we suggest creating your own clone of Extra
|
||||
under a different name. You could also edit the `extensions` global
|
||||
variable defined below, but be aware that such changes may be lost
|
||||
when you upgrade to any future version of Python-Markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/extra)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
|
||||
extensions = [
|
||||
'fenced_code',
|
||||
'footnotes',
|
||||
'attr_list',
|
||||
'def_list',
|
||||
'tables',
|
||||
'abbr',
|
||||
'md_in_html'
|
||||
]
|
||||
""" The list of included extensions. """
|
||||
|
||||
|
||||
class ExtraExtension(Extension):
|
||||
""" Add various extensions to Markdown class."""
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" `config` is a dumb holder which gets passed to the actual extension later. """
|
||||
self.config = kwargs
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Register extension instances. """
|
||||
md.registerExtensions(extensions, self.config)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return ExtraExtension(**kwargs)
|
@ -1,182 +0,0 @@
|
||||
# Fenced Code Extension for Python Markdown
|
||||
# =========================================
|
||||
|
||||
# This extension adds Fenced Code Blocks to Python-Markdown.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/fenced_code_blocks
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com/).
|
||||
|
||||
# All changes Copyright 2008-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
This extension adds Fenced Code Blocks to Python-Markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/fenced_code_blocks)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from textwrap import dedent
|
||||
from . import Extension
|
||||
from ..preprocessors import Preprocessor
|
||||
from .codehilite import CodeHilite, CodeHiliteExtension, parse_hl_lines
|
||||
from .attr_list import get_attrs, AttrListExtension
|
||||
from ..util import parseBoolValue
|
||||
from ..serializers import _escape_attrib_html
|
||||
import re
|
||||
|
||||
|
||||
class FencedCodeExtension(Extension):
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
'lang_prefix': ['language-', 'Prefix prepended to the language. Default: "language-"']
|
||||
}
|
||||
""" Default configuration options. """
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add `FencedBlockPreprocessor` to the Markdown instance. """
|
||||
md.registerExtension(self)
|
||||
|
||||
md.preprocessors.register(FencedBlockPreprocessor(md, self.getConfigs()), 'fenced_code_block', 25)
|
||||
|
||||
|
||||
class FencedBlockPreprocessor(Preprocessor):
|
||||
""" Find and extract fenced code blocks. """
|
||||
|
||||
FENCED_BLOCK_RE = re.compile(
|
||||
dedent(r'''
|
||||
(?P<fence>^(?:~{3,}|`{3,}))[ ]* # opening fence
|
||||
((\{(?P<attrs>[^\}\n]*)\})| # (optional {attrs} or
|
||||
(\.?(?P<lang>[\w#.+-]*)[ ]*)? # optional (.)lang
|
||||
(hl_lines=(?P<quot>"|')(?P<hl_lines>.*?)(?P=quot)[ ]*)?) # optional hl_lines)
|
||||
\n # newline (end of opening fence)
|
||||
(?P<code>.*?)(?<=\n) # the code block
|
||||
(?P=fence)[ ]*$ # closing fence
|
||||
'''),
|
||||
re.MULTILINE | re.DOTALL | re.VERBOSE
|
||||
)
|
||||
|
||||
def __init__(self, md, config):
|
||||
super().__init__(md)
|
||||
self.config = config
|
||||
self.checked_for_deps = False
|
||||
self.codehilite_conf = {}
|
||||
self.use_attr_list = False
|
||||
# List of options to convert to boolean values
|
||||
self.bool_options = [
|
||||
'linenums',
|
||||
'guess_lang',
|
||||
'noclasses',
|
||||
'use_pygments'
|
||||
]
|
||||
|
||||
def run(self, lines):
|
||||
""" Match and store Fenced Code Blocks in the `HtmlStash`. """
|
||||
|
||||
# Check for dependent extensions
|
||||
if not self.checked_for_deps:
|
||||
for ext in self.md.registeredExtensions:
|
||||
if isinstance(ext, CodeHiliteExtension):
|
||||
self.codehilite_conf = ext.getConfigs()
|
||||
if isinstance(ext, AttrListExtension):
|
||||
self.use_attr_list = True
|
||||
|
||||
self.checked_for_deps = True
|
||||
|
||||
text = "\n".join(lines)
|
||||
while 1:
|
||||
m = self.FENCED_BLOCK_RE.search(text)
|
||||
if m:
|
||||
lang, id, classes, config = None, '', [], {}
|
||||
if m.group('attrs'):
|
||||
id, classes, config = self.handle_attrs(get_attrs(m.group('attrs')))
|
||||
if len(classes):
|
||||
lang = classes.pop(0)
|
||||
else:
|
||||
if m.group('lang'):
|
||||
lang = m.group('lang')
|
||||
if m.group('hl_lines'):
|
||||
# Support `hl_lines` outside of `attrs` for backward-compatibility
|
||||
config['hl_lines'] = parse_hl_lines(m.group('hl_lines'))
|
||||
|
||||
# If `config` is not empty, then the `codehighlite` extension
|
||||
# is enabled, so we call it to highlight the code
|
||||
if self.codehilite_conf and self.codehilite_conf['use_pygments'] and config.get('use_pygments', True):
|
||||
local_config = self.codehilite_conf.copy()
|
||||
local_config.update(config)
|
||||
# Combine classes with `cssclass`. Ensure `cssclass` is at end
|
||||
# as Pygments appends a suffix under certain circumstances.
|
||||
# Ignore ID as Pygments does not offer an option to set it.
|
||||
if classes:
|
||||
local_config['css_class'] = '{} {}'.format(
|
||||
' '.join(classes),
|
||||
local_config['css_class']
|
||||
)
|
||||
highliter = CodeHilite(
|
||||
m.group('code'),
|
||||
lang=lang,
|
||||
style=local_config.pop('pygments_style', 'default'),
|
||||
**local_config
|
||||
)
|
||||
|
||||
code = highliter.hilite(shebang=False)
|
||||
else:
|
||||
id_attr = lang_attr = class_attr = kv_pairs = ''
|
||||
if lang:
|
||||
prefix = self.config.get('lang_prefix', 'language-')
|
||||
lang_attr = f' class="{prefix}{_escape_attrib_html(lang)}"'
|
||||
if classes:
|
||||
class_attr = f' class="{_escape_attrib_html(" ".join(classes))}"'
|
||||
if id:
|
||||
id_attr = f' id="{_escape_attrib_html(id)}"'
|
||||
if self.use_attr_list and config and not config.get('use_pygments', False):
|
||||
# Only assign key/value pairs to code element if `attr_list` extension is enabled, key/value
|
||||
# pairs were defined on the code block, and the `use_pygments` key was not set to `True`. The
|
||||
# `use_pygments` key could be either set to `False` or not defined. It is omitted from output.
|
||||
kv_pairs = ''.join(
|
||||
f' {k}="{_escape_attrib_html(v)}"' for k, v in config.items() if k != 'use_pygments'
|
||||
)
|
||||
code = self._escape(m.group('code'))
|
||||
code = f'<pre{id_attr}{class_attr}><code{lang_attr}{kv_pairs}>{code}</code></pre>'
|
||||
|
||||
placeholder = self.md.htmlStash.store(code)
|
||||
text = f'{text[:m.start()]}\n{placeholder}\n{text[m.end():]}'
|
||||
else:
|
||||
break
|
||||
return text.split("\n")
|
||||
|
||||
def handle_attrs(self, attrs):
|
||||
""" Return tuple: `(id, [list, of, classes], {configs})` """
|
||||
id = ''
|
||||
classes = []
|
||||
configs = {}
|
||||
for k, v in attrs:
|
||||
if k == 'id':
|
||||
id = v
|
||||
elif k == '.':
|
||||
classes.append(v)
|
||||
elif k == 'hl_lines':
|
||||
configs[k] = parse_hl_lines(v)
|
||||
elif k in self.bool_options:
|
||||
configs[k] = parseBoolValue(v, fail_on_errors=False, preserve_none=True)
|
||||
else:
|
||||
configs[k] = v
|
||||
return id, classes, configs
|
||||
|
||||
def _escape(self, txt):
|
||||
""" basic html escaping """
|
||||
txt = txt.replace('&', '&')
|
||||
txt = txt.replace('<', '<')
|
||||
txt = txt.replace('>', '>')
|
||||
txt = txt.replace('"', '"')
|
||||
return txt
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return FencedCodeExtension(**kwargs)
|
@ -1,416 +0,0 @@
|
||||
# Footnotes Extension for Python-Markdown
|
||||
# =======================================
|
||||
|
||||
# Adds footnote handling to Python-Markdown.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/footnotes
|
||||
# for documentation.
|
||||
|
||||
# Copyright The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Adds footnote handling to Python-Markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/footnotes)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..blockprocessors import BlockProcessor
|
||||
from ..inlinepatterns import InlineProcessor
|
||||
from ..treeprocessors import Treeprocessor
|
||||
from ..postprocessors import Postprocessor
|
||||
from .. import util
|
||||
from collections import OrderedDict
|
||||
import re
|
||||
import copy
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
FN_BACKLINK_TEXT = util.STX + "zz1337820767766393qq" + util.ETX
|
||||
NBSP_PLACEHOLDER = util.STX + "qq3936677670287331zz" + util.ETX
|
||||
RE_REF_ID = re.compile(r'(fnref)(\d+)')
|
||||
|
||||
|
||||
class FootnoteExtension(Extension):
|
||||
""" Footnote Extension. """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
""" Setup configs. """
|
||||
|
||||
self.config = {
|
||||
'PLACE_MARKER': [
|
||||
'///Footnotes Go Here///', 'The text string that marks where the footnotes go'
|
||||
],
|
||||
'UNIQUE_IDS': [
|
||||
False, 'Avoid name collisions across multiple calls to `reset()`.'
|
||||
],
|
||||
'BACKLINK_TEXT': [
|
||||
'↩', "The text string that links from the footnote to the reader's place."
|
||||
],
|
||||
'SUPERSCRIPT_TEXT': [
|
||||
'{}', "The text string that links from the reader's place to the footnote."
|
||||
],
|
||||
'BACKLINK_TITLE': [
|
||||
'Jump back to footnote %d in the text',
|
||||
'The text string used for the title HTML attribute of the backlink. '
|
||||
'%d will be replaced by the footnote number.'
|
||||
],
|
||||
'SEPARATOR': [
|
||||
':', 'Footnote separator.'
|
||||
]
|
||||
}
|
||||
""" Default configuration options. """
|
||||
super().__init__(**kwargs)
|
||||
|
||||
# In multiple invocations, emit links that don't get tangled.
|
||||
self.unique_prefix = 0
|
||||
self.found_refs = {}
|
||||
self.used_refs = set()
|
||||
|
||||
self.reset()
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add pieces to Markdown. """
|
||||
md.registerExtension(self)
|
||||
self.parser = md.parser
|
||||
self.md = md
|
||||
# Insert a `blockprocessor` before `ReferencePreprocessor`
|
||||
md.parser.blockprocessors.register(FootnoteBlockProcessor(self), 'footnote', 17)
|
||||
|
||||
# Insert an inline pattern before `ImageReferencePattern`
|
||||
FOOTNOTE_RE = r'\[\^([^\]]*)\]' # blah blah [^1] blah
|
||||
md.inlinePatterns.register(FootnoteInlineProcessor(FOOTNOTE_RE, self), 'footnote', 175)
|
||||
# Insert a tree-processor that would actually add the footnote div
|
||||
# This must be before all other tree-processors (i.e., `inline` and
|
||||
# `codehilite`) so they can run on the the contents of the div.
|
||||
md.treeprocessors.register(FootnoteTreeprocessor(self), 'footnote', 50)
|
||||
|
||||
# Insert a tree-processor that will run after inline is done.
|
||||
# In this tree-processor we want to check our duplicate footnote tracker
|
||||
# And add additional `backrefs` to the footnote pointing back to the
|
||||
# duplicated references.
|
||||
md.treeprocessors.register(FootnotePostTreeprocessor(self), 'footnote-duplicate', 15)
|
||||
|
||||
# Insert a postprocessor after amp_substitute processor
|
||||
md.postprocessors.register(FootnotePostprocessor(self), 'footnote', 25)
|
||||
|
||||
def reset(self) -> None:
|
||||
""" Clear footnotes on reset, and prepare for distinct document. """
|
||||
self.footnotes: OrderedDict[str, str] = OrderedDict()
|
||||
self.unique_prefix += 1
|
||||
self.found_refs = {}
|
||||
self.used_refs = set()
|
||||
|
||||
def unique_ref(self, reference, found: bool = False):
|
||||
""" Get a unique reference if there are duplicates. """
|
||||
if not found:
|
||||
return reference
|
||||
|
||||
original_ref = reference
|
||||
while reference in self.used_refs:
|
||||
ref, rest = reference.split(self.get_separator(), 1)
|
||||
m = RE_REF_ID.match(ref)
|
||||
if m:
|
||||
reference = '%s%d%s%s' % (m.group(1), int(m.group(2))+1, self.get_separator(), rest)
|
||||
else:
|
||||
reference = '%s%d%s%s' % (ref, 2, self.get_separator(), rest)
|
||||
|
||||
self.used_refs.add(reference)
|
||||
if original_ref in self.found_refs:
|
||||
self.found_refs[original_ref] += 1
|
||||
else:
|
||||
self.found_refs[original_ref] = 1
|
||||
return reference
|
||||
|
||||
def findFootnotesPlaceholder(self, root):
|
||||
""" Return ElementTree Element that contains Footnote placeholder. """
|
||||
def finder(element):
|
||||
for child in element:
|
||||
if child.text:
|
||||
if child.text.find(self.getConfig("PLACE_MARKER")) > -1:
|
||||
return child, element, True
|
||||
if child.tail:
|
||||
if child.tail.find(self.getConfig("PLACE_MARKER")) > -1:
|
||||
return child, element, False
|
||||
child_res = finder(child)
|
||||
if child_res is not None:
|
||||
return child_res
|
||||
return None
|
||||
|
||||
res = finder(root)
|
||||
return res
|
||||
|
||||
def setFootnote(self, id, text) -> None:
|
||||
""" Store a footnote for later retrieval. """
|
||||
self.footnotes[id] = text
|
||||
|
||||
def get_separator(self):
|
||||
""" Get the footnote separator. """
|
||||
return self.getConfig("SEPARATOR")
|
||||
|
||||
def makeFootnoteId(self, id):
|
||||
""" Return footnote link id. """
|
||||
if self.getConfig("UNIQUE_IDS"):
|
||||
return 'fn%s%d-%s' % (self.get_separator(), self.unique_prefix, id)
|
||||
else:
|
||||
return 'fn{}{}'.format(self.get_separator(), id)
|
||||
|
||||
def makeFootnoteRefId(self, id, found: bool = False):
|
||||
""" Return footnote back-link id. """
|
||||
if self.getConfig("UNIQUE_IDS"):
|
||||
return self.unique_ref('fnref%s%d-%s' % (self.get_separator(), self.unique_prefix, id), found)
|
||||
else:
|
||||
return self.unique_ref('fnref{}{}'.format(self.get_separator(), id), found)
|
||||
|
||||
def makeFootnotesDiv(self, root):
|
||||
""" Return `div` of footnotes as `etree` Element. """
|
||||
|
||||
if not list(self.footnotes.keys()):
|
||||
return None
|
||||
|
||||
div = etree.Element("div")
|
||||
div.set('class', 'footnote')
|
||||
etree.SubElement(div, "hr")
|
||||
ol = etree.SubElement(div, "ol")
|
||||
surrogate_parent = etree.Element("div")
|
||||
|
||||
# Backward compatibility with old '%d' placeholder
|
||||
backlink_title = self.getConfig("BACKLINK_TITLE").replace("%d", "{}")
|
||||
|
||||
for index, id in enumerate(self.footnotes.keys(), start=1):
|
||||
li = etree.SubElement(ol, "li")
|
||||
li.set("id", self.makeFootnoteId(id))
|
||||
# Parse footnote with surrogate parent as `li` cannot be used.
|
||||
# List block handlers have special logic to deal with `li`.
|
||||
# When we are done parsing, we will copy everything over to `li`.
|
||||
self.parser.parseChunk(surrogate_parent, self.footnotes[id])
|
||||
for el in list(surrogate_parent):
|
||||
li.append(el)
|
||||
surrogate_parent.remove(el)
|
||||
backlink = etree.Element("a")
|
||||
backlink.set("href", "#" + self.makeFootnoteRefId(id))
|
||||
backlink.set("class", "footnote-backref")
|
||||
backlink.set(
|
||||
"title",
|
||||
backlink_title.format(index)
|
||||
)
|
||||
backlink.text = FN_BACKLINK_TEXT
|
||||
|
||||
if len(li):
|
||||
node = li[-1]
|
||||
if node.tag == "p":
|
||||
node.text = node.text + NBSP_PLACEHOLDER
|
||||
node.append(backlink)
|
||||
else:
|
||||
p = etree.SubElement(li, "p")
|
||||
p.append(backlink)
|
||||
return div
|
||||
|
||||
|
||||
class FootnoteBlockProcessor(BlockProcessor):
|
||||
""" Find all footnote references and store for later use. """
|
||||
|
||||
RE = re.compile(r'^[ ]{0,3}\[\^([^\]]*)\]:[ ]*(.*)$', re.MULTILINE)
|
||||
|
||||
def __init__(self, footnotes):
|
||||
super().__init__(footnotes.parser)
|
||||
self.footnotes = footnotes
|
||||
|
||||
def test(self, parent, block):
|
||||
return True
|
||||
|
||||
def run(self, parent, blocks):
|
||||
""" Find, set, and remove footnote definitions. """
|
||||
block = blocks.pop(0)
|
||||
m = self.RE.search(block)
|
||||
if m:
|
||||
id = m.group(1)
|
||||
fn_blocks = [m.group(2)]
|
||||
|
||||
# Handle rest of block
|
||||
therest = block[m.end():].lstrip('\n')
|
||||
m2 = self.RE.search(therest)
|
||||
if m2:
|
||||
# Another footnote exists in the rest of this block.
|
||||
# Any content before match is continuation of this footnote, which may be lazily indented.
|
||||
before = therest[:m2.start()].rstrip('\n')
|
||||
fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(before)]).lstrip('\n')
|
||||
# Add back to blocks everything from beginning of match forward for next iteration.
|
||||
blocks.insert(0, therest[m2.start():])
|
||||
else:
|
||||
# All remaining lines of block are continuation of this footnote, which may be lazily indented.
|
||||
fn_blocks[0] = '\n'.join([fn_blocks[0], self.detab(therest)]).strip('\n')
|
||||
|
||||
# Check for child elements in remaining blocks.
|
||||
fn_blocks.extend(self.detectTabbed(blocks))
|
||||
|
||||
footnote = "\n\n".join(fn_blocks)
|
||||
self.footnotes.setFootnote(id, footnote.rstrip())
|
||||
|
||||
if block[:m.start()].strip():
|
||||
# Add any content before match back to blocks as separate block
|
||||
blocks.insert(0, block[:m.start()].rstrip('\n'))
|
||||
return True
|
||||
# No match. Restore block.
|
||||
blocks.insert(0, block)
|
||||
return False
|
||||
|
||||
def detectTabbed(self, blocks) -> list[str]:
|
||||
""" Find indented text and remove indent before further processing.
|
||||
|
||||
Returns:
|
||||
A list of blocks with indentation removed.
|
||||
"""
|
||||
fn_blocks = []
|
||||
while blocks:
|
||||
if blocks[0].startswith(' '*4):
|
||||
block = blocks.pop(0)
|
||||
# Check for new footnotes within this block and split at new footnote.
|
||||
m = self.RE.search(block)
|
||||
if m:
|
||||
# Another footnote exists in this block.
|
||||
# Any content before match is continuation of this footnote, which may be lazily indented.
|
||||
before = block[:m.start()].rstrip('\n')
|
||||
fn_blocks.append(self.detab(before))
|
||||
# Add back to blocks everything from beginning of match forward for next iteration.
|
||||
blocks.insert(0, block[m.start():])
|
||||
# End of this footnote.
|
||||
break
|
||||
else:
|
||||
# Entire block is part of this footnote.
|
||||
fn_blocks.append(self.detab(block))
|
||||
else:
|
||||
# End of this footnote.
|
||||
break
|
||||
return fn_blocks
|
||||
|
||||
def detab(self, block):
|
||||
""" Remove one level of indent from a block.
|
||||
|
||||
Preserve lazily indented blocks by only removing indent from indented lines.
|
||||
"""
|
||||
lines = block.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
if line.startswith(' '*4):
|
||||
lines[i] = line[4:]
|
||||
return '\n'.join(lines)
|
||||
|
||||
|
||||
class FootnoteInlineProcessor(InlineProcessor):
|
||||
""" `InlineProcessor` for footnote markers in a document's body text. """
|
||||
|
||||
def __init__(self, pattern, footnotes):
|
||||
super().__init__(pattern)
|
||||
self.footnotes = footnotes
|
||||
|
||||
def handleMatch(self, m, data):
|
||||
id = m.group(1)
|
||||
if id in self.footnotes.footnotes.keys():
|
||||
sup = etree.Element("sup")
|
||||
a = etree.SubElement(sup, "a")
|
||||
sup.set('id', self.footnotes.makeFootnoteRefId(id, found=True))
|
||||
a.set('href', '#' + self.footnotes.makeFootnoteId(id))
|
||||
a.set('class', 'footnote-ref')
|
||||
a.text = self.footnotes.getConfig("SUPERSCRIPT_TEXT").format(
|
||||
list(self.footnotes.footnotes.keys()).index(id) + 1
|
||||
)
|
||||
return sup, m.start(0), m.end(0)
|
||||
else:
|
||||
return None, None, None
|
||||
|
||||
|
||||
class FootnotePostTreeprocessor(Treeprocessor):
|
||||
""" Amend footnote div with duplicates. """
|
||||
|
||||
def __init__(self, footnotes):
|
||||
self.footnotes = footnotes
|
||||
|
||||
def add_duplicates(self, li, duplicates) -> None:
|
||||
""" Adjust current `li` and add the duplicates: `fnref2`, `fnref3`, etc. """
|
||||
for link in li.iter('a'):
|
||||
# Find the link that needs to be duplicated.
|
||||
if link.attrib.get('class', '') == 'footnote-backref':
|
||||
ref, rest = link.attrib['href'].split(self.footnotes.get_separator(), 1)
|
||||
# Duplicate link the number of times we need to
|
||||
# and point the to the appropriate references.
|
||||
links = []
|
||||
for index in range(2, duplicates + 1):
|
||||
sib_link = copy.deepcopy(link)
|
||||
sib_link.attrib['href'] = '%s%d%s%s' % (ref, index, self.footnotes.get_separator(), rest)
|
||||
links.append(sib_link)
|
||||
self.offset += 1
|
||||
# Add all the new duplicate links.
|
||||
el = list(li)[-1]
|
||||
for link in links:
|
||||
el.append(link)
|
||||
break
|
||||
|
||||
def get_num_duplicates(self, li):
|
||||
""" Get the number of duplicate refs of the footnote. """
|
||||
fn, rest = li.attrib.get('id', '').split(self.footnotes.get_separator(), 1)
|
||||
link_id = '{}ref{}{}'.format(fn, self.footnotes.get_separator(), rest)
|
||||
return self.footnotes.found_refs.get(link_id, 0)
|
||||
|
||||
def handle_duplicates(self, parent) -> None:
|
||||
""" Find duplicate footnotes and format and add the duplicates. """
|
||||
for li in list(parent):
|
||||
# Check number of duplicates footnotes and insert
|
||||
# additional links if needed.
|
||||
count = self.get_num_duplicates(li)
|
||||
if count > 1:
|
||||
self.add_duplicates(li, count)
|
||||
|
||||
def run(self, root):
|
||||
""" Crawl the footnote div and add missing duplicate footnotes. """
|
||||
self.offset = 0
|
||||
for div in root.iter('div'):
|
||||
if div.attrib.get('class', '') == 'footnote':
|
||||
# Footnotes should be under the first ordered list under
|
||||
# the footnote div. So once we find it, quit.
|
||||
for ol in div.iter('ol'):
|
||||
self.handle_duplicates(ol)
|
||||
break
|
||||
|
||||
|
||||
class FootnoteTreeprocessor(Treeprocessor):
|
||||
""" Build and append footnote div to end of document. """
|
||||
|
||||
def __init__(self, footnotes):
|
||||
self.footnotes = footnotes
|
||||
|
||||
def run(self, root):
|
||||
footnotesDiv = self.footnotes.makeFootnotesDiv(root)
|
||||
if footnotesDiv is not None:
|
||||
result = self.footnotes.findFootnotesPlaceholder(root)
|
||||
if result:
|
||||
child, parent, isText = result
|
||||
ind = list(parent).index(child)
|
||||
if isText:
|
||||
parent.remove(child)
|
||||
parent.insert(ind, footnotesDiv)
|
||||
else:
|
||||
parent.insert(ind + 1, footnotesDiv)
|
||||
child.tail = None
|
||||
else:
|
||||
root.append(footnotesDiv)
|
||||
|
||||
|
||||
class FootnotePostprocessor(Postprocessor):
|
||||
""" Replace placeholders with html entities. """
|
||||
def __init__(self, footnotes):
|
||||
self.footnotes = footnotes
|
||||
|
||||
def run(self, text):
|
||||
text = text.replace(
|
||||
FN_BACKLINK_TEXT, self.footnotes.getConfig("BACKLINK_TEXT")
|
||||
)
|
||||
return text.replace(NBSP_PLACEHOLDER, " ")
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
""" Return an instance of the `FootnoteExtension` """
|
||||
return FootnoteExtension(**kwargs)
|
@ -1,67 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
An extension to Python Markdown which implements legacy attributes.
|
||||
|
||||
Prior to Python-Markdown version 3.0, the Markdown class had an `enable_attributes`
|
||||
keyword which was on by default and provided for attributes to be defined for elements
|
||||
using the format `{@key=value}`. This extension is provided as a replacement for
|
||||
backward compatibility. New documents should be authored using `attr_lists`. However,
|
||||
numerous documents exist which have been using the old attribute format for many
|
||||
years. This extension can be used to continue to render those documents correctly.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from markdown.treeprocessors import Treeprocessor, isString
|
||||
from markdown.extensions import Extension
|
||||
|
||||
|
||||
ATTR_RE = re.compile(r'\{@([^\}]*)=([^\}]*)}') # {@id=123}
|
||||
|
||||
|
||||
class LegacyAttrs(Treeprocessor):
|
||||
def run(self, doc):
|
||||
"""Find and set values of attributes ({@key=value}). """
|
||||
for el in doc.iter():
|
||||
alt = el.get('alt', None)
|
||||
if alt is not None:
|
||||
el.set('alt', self.handleAttributes(el, alt))
|
||||
if el.text and isString(el.text):
|
||||
el.text = self.handleAttributes(el, el.text)
|
||||
if el.tail and isString(el.tail):
|
||||
el.tail = self.handleAttributes(el, el.tail)
|
||||
|
||||
def handleAttributes(self, el, txt):
|
||||
""" Set attributes and return text without definitions. """
|
||||
def attributeCallback(match):
|
||||
el.set(match.group(1), match.group(2).replace('\n', ' '))
|
||||
return ATTR_RE.sub(attributeCallback, txt)
|
||||
|
||||
|
||||
class LegacyAttrExtension(Extension):
|
||||
def extendMarkdown(self, md):
|
||||
""" Add `LegacyAttrs` to Markdown instance. """
|
||||
md.treeprocessors.register(LegacyAttrs(md), 'legacyattrs', 15)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return LegacyAttrExtension(**kwargs)
|
@ -1,52 +0,0 @@
|
||||
# Legacy Em Extension for Python-Markdown
|
||||
# =======================================
|
||||
|
||||
# This extension provides legacy behavior for _connected_words_.
|
||||
|
||||
# Copyright 2015-2018 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
This extension provides legacy behavior for _connected_words_.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..inlinepatterns import UnderscoreProcessor, EmStrongItem, EM_STRONG2_RE, STRONG_EM2_RE
|
||||
import re
|
||||
|
||||
# _emphasis_
|
||||
EMPHASIS_RE = r'(_)([^_]+)\1'
|
||||
|
||||
# __strong__
|
||||
STRONG_RE = r'(_{2})(.+?)\1'
|
||||
|
||||
# __strong_em___
|
||||
STRONG_EM_RE = r'(_)\1(?!\1)([^_]+?)\1(?!\1)(.+?)\1{3}'
|
||||
|
||||
|
||||
class LegacyUnderscoreProcessor(UnderscoreProcessor):
|
||||
"""Emphasis processor for handling strong and em matches inside underscores."""
|
||||
|
||||
PATTERNS = [
|
||||
EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'),
|
||||
EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'),
|
||||
EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'),
|
||||
EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'),
|
||||
EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em')
|
||||
]
|
||||
|
||||
|
||||
class LegacyEmExtension(Extension):
|
||||
""" Add legacy_em extension to Markdown class."""
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Modify inline patterns. """
|
||||
md.inlinePatterns.register(LegacyUnderscoreProcessor(r'_'), 'em_strong2', 50)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
""" Return an instance of the `LegacyEmExtension` """
|
||||
return LegacyEmExtension(**kwargs)
|
@ -1,372 +0,0 @@
|
||||
# Python-Markdown Markdown in HTML Extension
|
||||
# ===============================
|
||||
|
||||
# An implementation of [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/)'s
|
||||
# parsing of Markdown syntax in raw HTML.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/raw_html
|
||||
# for documentation.
|
||||
|
||||
# Copyright The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
An implementation of [PHP Markdown Extra](http://michelf.com/projects/php-markdown/extra/)'s
|
||||
parsing of Markdown syntax in raw HTML.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/raw_html)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..blockprocessors import BlockProcessor
|
||||
from ..preprocessors import Preprocessor
|
||||
from ..postprocessors import RawHtmlPostprocessor
|
||||
from .. import util
|
||||
from ..htmlparser import HTMLExtractor, blank_line_re
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
|
||||
class HTMLExtractorExtra(HTMLExtractor):
|
||||
"""
|
||||
Override `HTMLExtractor` and create `etree` `Elements` for any elements which should have content parsed as
|
||||
Markdown.
|
||||
"""
|
||||
|
||||
def __init__(self, md, *args, **kwargs):
|
||||
# All block-level tags.
|
||||
self.block_level_tags = set(md.block_level_elements.copy())
|
||||
# Block-level tags in which the content only gets span level parsing
|
||||
self.span_tags = set(
|
||||
['address', 'dd', 'dt', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'legend', 'li', 'p', 'summary', 'td', 'th']
|
||||
)
|
||||
# Block-level tags which never get their content parsed.
|
||||
self.raw_tags = set(['canvas', 'math', 'option', 'pre', 'script', 'style', 'textarea'])
|
||||
|
||||
super().__init__(md, *args, **kwargs)
|
||||
|
||||
# Block-level tags in which the content gets parsed as blocks
|
||||
self.block_tags = set(self.block_level_tags) - (self.span_tags | self.raw_tags | self.empty_tags)
|
||||
self.span_and_blocks_tags = self.block_tags | self.span_tags
|
||||
|
||||
def reset(self):
|
||||
"""Reset this instance. Loses all unprocessed data."""
|
||||
self.mdstack = [] # When markdown=1, stack contains a list of tags
|
||||
self.treebuilder = etree.TreeBuilder()
|
||||
self.mdstate = [] # one of 'block', 'span', 'off', or None
|
||||
super().reset()
|
||||
|
||||
def close(self):
|
||||
"""Handle any buffered data."""
|
||||
super().close()
|
||||
# Handle any unclosed tags.
|
||||
if self.mdstack:
|
||||
# Close the outermost parent. `handle_endtag` will close all unclosed children.
|
||||
self.handle_endtag(self.mdstack[0])
|
||||
|
||||
def get_element(self):
|
||||
""" Return element from `treebuilder` and reset `treebuilder` for later use. """
|
||||
element = self.treebuilder.close()
|
||||
self.treebuilder = etree.TreeBuilder()
|
||||
return element
|
||||
|
||||
def get_state(self, tag, attrs):
|
||||
""" Return state from tag and `markdown` attribute. One of 'block', 'span', or 'off'. """
|
||||
md_attr = attrs.get('markdown', '0')
|
||||
if md_attr == 'markdown':
|
||||
# `<tag markdown>` is the same as `<tag markdown='1'>`.
|
||||
md_attr = '1'
|
||||
parent_state = self.mdstate[-1] if self.mdstate else None
|
||||
if parent_state == 'off' or (parent_state == 'span' and md_attr != '0'):
|
||||
# Only use the parent state if it is more restrictive than the markdown attribute.
|
||||
md_attr = parent_state
|
||||
if ((md_attr == '1' and tag in self.block_tags) or
|
||||
(md_attr == 'block' and tag in self.span_and_blocks_tags)):
|
||||
return 'block'
|
||||
elif ((md_attr == '1' and tag in self.span_tags) or
|
||||
(md_attr == 'span' and tag in self.span_and_blocks_tags)):
|
||||
return 'span'
|
||||
elif tag in self.block_level_tags:
|
||||
return 'off'
|
||||
else: # pragma: no cover
|
||||
return None
|
||||
|
||||
def handle_starttag(self, tag, attrs):
|
||||
# Handle tags that should always be empty and do not specify a closing tag
|
||||
if tag in self.empty_tags and (self.at_line_start() or self.intail):
|
||||
attrs = {key: value if value is not None else key for key, value in attrs}
|
||||
if "markdown" in attrs:
|
||||
attrs.pop('markdown')
|
||||
element = etree.Element(tag, attrs)
|
||||
data = etree.tostring(element, encoding='unicode', method='html')
|
||||
else:
|
||||
data = self.get_starttag_text()
|
||||
self.handle_empty_tag(data, True)
|
||||
return
|
||||
|
||||
if tag in self.block_level_tags and (self.at_line_start() or self.intail):
|
||||
# Valueless attribute (ex: `<tag checked>`) results in `[('checked', None)]`.
|
||||
# Convert to `{'checked': 'checked'}`.
|
||||
attrs = {key: value if value is not None else key for key, value in attrs}
|
||||
state = self.get_state(tag, attrs)
|
||||
if self.inraw or (state in [None, 'off'] and not self.mdstack):
|
||||
# fall back to default behavior
|
||||
attrs.pop('markdown', None)
|
||||
super().handle_starttag(tag, attrs)
|
||||
else:
|
||||
if 'p' in self.mdstack and tag in self.block_level_tags:
|
||||
# Close unclosed 'p' tag
|
||||
self.handle_endtag('p')
|
||||
self.mdstate.append(state)
|
||||
self.mdstack.append(tag)
|
||||
attrs['markdown'] = state
|
||||
self.treebuilder.start(tag, attrs)
|
||||
else:
|
||||
# Span level tag
|
||||
if self.inraw:
|
||||
super().handle_starttag(tag, attrs)
|
||||
else:
|
||||
text = self.get_starttag_text()
|
||||
if self.mdstate and self.mdstate[-1] == "off":
|
||||
self.handle_data(self.md.htmlStash.store(text))
|
||||
else:
|
||||
self.handle_data(text)
|
||||
if tag in self.CDATA_CONTENT_ELEMENTS:
|
||||
# This is presumably a standalone tag in a code span (see #1036).
|
||||
self.clear_cdata_mode()
|
||||
|
||||
def handle_endtag(self, tag):
|
||||
if tag in self.block_level_tags:
|
||||
if self.inraw:
|
||||
super().handle_endtag(tag)
|
||||
elif tag in self.mdstack:
|
||||
# Close element and any unclosed children
|
||||
while self.mdstack:
|
||||
item = self.mdstack.pop()
|
||||
self.mdstate.pop()
|
||||
self.treebuilder.end(item)
|
||||
if item == tag:
|
||||
break
|
||||
if not self.mdstack:
|
||||
# Last item in stack is closed. Stash it
|
||||
element = self.get_element()
|
||||
# Get last entry to see if it ends in newlines
|
||||
# If it is an element, assume there is no newlines
|
||||
item = self.cleandoc[-1] if self.cleandoc else ''
|
||||
# If we only have one newline before block element, add another
|
||||
if not item.endswith('\n\n') and item.endswith('\n'):
|
||||
self.cleandoc.append('\n')
|
||||
self.cleandoc.append(self.md.htmlStash.store(element))
|
||||
self.cleandoc.append('\n\n')
|
||||
self.state = []
|
||||
# Check if element has a tail
|
||||
if not blank_line_re.match(
|
||||
self.rawdata[self.line_offset + self.offset + len(self.get_endtag_text(tag)):]):
|
||||
# More content exists after `endtag`.
|
||||
self.intail = True
|
||||
else:
|
||||
# Treat orphan closing tag as a span level tag.
|
||||
text = self.get_endtag_text(tag)
|
||||
if self.mdstate and self.mdstate[-1] == "off":
|
||||
self.handle_data(self.md.htmlStash.store(text))
|
||||
else:
|
||||
self.handle_data(text)
|
||||
else:
|
||||
# Span level tag
|
||||
if self.inraw:
|
||||
super().handle_endtag(tag)
|
||||
else:
|
||||
text = self.get_endtag_text(tag)
|
||||
if self.mdstate and self.mdstate[-1] == "off":
|
||||
self.handle_data(self.md.htmlStash.store(text))
|
||||
else:
|
||||
self.handle_data(text)
|
||||
|
||||
def handle_startendtag(self, tag, attrs):
|
||||
if tag in self.empty_tags:
|
||||
attrs = {key: value if value is not None else key for key, value in attrs}
|
||||
if "markdown" in attrs:
|
||||
attrs.pop('markdown')
|
||||
element = etree.Element(tag, attrs)
|
||||
data = etree.tostring(element, encoding='unicode', method='html')
|
||||
else:
|
||||
data = self.get_starttag_text()
|
||||
else:
|
||||
data = self.get_starttag_text()
|
||||
self.handle_empty_tag(data, is_block=self.md.is_block_level(tag))
|
||||
|
||||
def handle_data(self, data):
|
||||
if self.intail and '\n' in data:
|
||||
self.intail = False
|
||||
if self.inraw or not self.mdstack:
|
||||
super().handle_data(data)
|
||||
else:
|
||||
self.treebuilder.data(data)
|
||||
|
||||
def handle_empty_tag(self, data, is_block):
|
||||
if self.inraw or not self.mdstack:
|
||||
super().handle_empty_tag(data, is_block)
|
||||
else:
|
||||
if self.at_line_start() and is_block:
|
||||
self.handle_data('\n' + self.md.htmlStash.store(data) + '\n\n')
|
||||
else:
|
||||
self.handle_data(self.md.htmlStash.store(data))
|
||||
|
||||
def parse_pi(self, i):
|
||||
if self.at_line_start() or self.intail or self.mdstack:
|
||||
# The same override exists in `HTMLExtractor` without the check
|
||||
# for `mdstack`. Therefore, use parent of `HTMLExtractor` instead.
|
||||
return super(HTMLExtractor, self).parse_pi(i)
|
||||
# This is not the beginning of a raw block so treat as plain data
|
||||
# and avoid consuming any tags which may follow (see #1066).
|
||||
self.handle_data('<?')
|
||||
return i + 2
|
||||
|
||||
def parse_html_declaration(self, i):
|
||||
if self.at_line_start() or self.intail or self.mdstack:
|
||||
# The same override exists in `HTMLExtractor` without the check
|
||||
# for `mdstack`. Therefore, use parent of `HTMLExtractor` instead.
|
||||
return super(HTMLExtractor, self).parse_html_declaration(i)
|
||||
# This is not the beginning of a raw block so treat as plain data
|
||||
# and avoid consuming any tags which may follow (see #1066).
|
||||
self.handle_data('<!')
|
||||
return i + 2
|
||||
|
||||
|
||||
class HtmlBlockPreprocessor(Preprocessor):
|
||||
"""Remove html blocks from the text and store them for later retrieval."""
|
||||
|
||||
def run(self, lines):
|
||||
source = '\n'.join(lines)
|
||||
parser = HTMLExtractorExtra(self.md)
|
||||
parser.feed(source)
|
||||
parser.close()
|
||||
return ''.join(parser.cleandoc).split('\n')
|
||||
|
||||
|
||||
class MarkdownInHtmlProcessor(BlockProcessor):
|
||||
"""Process Markdown Inside HTML Blocks which have been stored in the `HtmlStash`."""
|
||||
|
||||
def test(self, parent, block):
|
||||
# Always return True. `run` will return `False` it not a valid match.
|
||||
return True
|
||||
|
||||
def parse_element_content(self, element):
|
||||
"""
|
||||
Recursively parse the text content of an `etree` Element as Markdown.
|
||||
|
||||
Any block level elements generated from the Markdown will be inserted as children of the element in place
|
||||
of the text content. All `markdown` attributes are removed. For any elements in which Markdown parsing has
|
||||
been disabled, the text content of it and its children are wrapped in an `AtomicString`.
|
||||
"""
|
||||
|
||||
md_attr = element.attrib.pop('markdown', 'off')
|
||||
|
||||
if md_attr == 'block':
|
||||
# Parse content as block level
|
||||
# The order in which the different parts are parsed (text, children, tails) is important here as the
|
||||
# order of elements needs to be preserved. We can't be inserting items at a later point in the current
|
||||
# iteration as we don't want to do raw processing on elements created from parsing Markdown text (for
|
||||
# example). Therefore, the order of operations is children, tails, text.
|
||||
|
||||
# Recursively parse existing children from raw HTML
|
||||
for child in list(element):
|
||||
self.parse_element_content(child)
|
||||
|
||||
# Parse Markdown text in tail of children. Do this separate to avoid raw HTML parsing.
|
||||
# Save the position of each item to be inserted later in reverse.
|
||||
tails = []
|
||||
for pos, child in enumerate(element):
|
||||
if child.tail:
|
||||
block = child.tail.rstrip('\n')
|
||||
child.tail = ''
|
||||
# Use a dummy placeholder element.
|
||||
dummy = etree.Element('div')
|
||||
self.parser.parseBlocks(dummy, block.split('\n\n'))
|
||||
children = list(dummy)
|
||||
children.reverse()
|
||||
tails.append((pos + 1, children))
|
||||
|
||||
# Insert the elements created from the tails in reverse.
|
||||
tails.reverse()
|
||||
for pos, tail in tails:
|
||||
for item in tail:
|
||||
element.insert(pos, item)
|
||||
|
||||
# Parse Markdown text content. Do this last to avoid raw HTML parsing.
|
||||
if element.text:
|
||||
block = element.text.rstrip('\n')
|
||||
element.text = ''
|
||||
# Use a dummy placeholder element as the content needs to get inserted before existing children.
|
||||
dummy = etree.Element('div')
|
||||
self.parser.parseBlocks(dummy, block.split('\n\n'))
|
||||
children = list(dummy)
|
||||
children.reverse()
|
||||
for child in children:
|
||||
element.insert(0, child)
|
||||
|
||||
elif md_attr == 'span':
|
||||
# Span level parsing will be handled by inline processors.
|
||||
# Walk children here to remove any `markdown` attributes.
|
||||
for child in list(element):
|
||||
self.parse_element_content(child)
|
||||
|
||||
else:
|
||||
# Disable inline parsing for everything else
|
||||
if element.text is None:
|
||||
element.text = ''
|
||||
element.text = util.AtomicString(element.text)
|
||||
for child in list(element):
|
||||
self.parse_element_content(child)
|
||||
if child.tail:
|
||||
child.tail = util.AtomicString(child.tail)
|
||||
|
||||
def run(self, parent, blocks):
|
||||
m = util.HTML_PLACEHOLDER_RE.match(blocks[0])
|
||||
if m:
|
||||
index = int(m.group(1))
|
||||
element = self.parser.md.htmlStash.rawHtmlBlocks[index]
|
||||
if isinstance(element, etree.Element):
|
||||
# We have a matched element. Process it.
|
||||
blocks.pop(0)
|
||||
self.parse_element_content(element)
|
||||
parent.append(element)
|
||||
# Cleanup stash. Replace element with empty string to avoid confusing postprocessor.
|
||||
self.parser.md.htmlStash.rawHtmlBlocks.pop(index)
|
||||
self.parser.md.htmlStash.rawHtmlBlocks.insert(index, '')
|
||||
# Confirm the match to the `blockparser`.
|
||||
return True
|
||||
# No match found.
|
||||
return False
|
||||
|
||||
|
||||
class MarkdownInHTMLPostprocessor(RawHtmlPostprocessor):
|
||||
def stash_to_string(self, text):
|
||||
""" Override default to handle any `etree` elements still in the stash. """
|
||||
if isinstance(text, etree.Element):
|
||||
return self.md.serializer(text)
|
||||
else:
|
||||
return str(text)
|
||||
|
||||
|
||||
class MarkdownInHtmlExtension(Extension):
|
||||
"""Add Markdown parsing in HTML to Markdown class."""
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Register extension instances. """
|
||||
|
||||
# Replace raw HTML preprocessor
|
||||
md.preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20)
|
||||
# Add `blockprocessor` which handles the placeholders for `etree` elements
|
||||
md.parser.blockprocessors.register(
|
||||
MarkdownInHtmlProcessor(md.parser), 'markdown_block', 105
|
||||
)
|
||||
# Replace raw HTML postprocessor
|
||||
md.postprocessors.register(MarkdownInHTMLPostprocessor(md), 'raw_html', 30)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return MarkdownInHtmlExtension(**kwargs)
|
@ -1,85 +0,0 @@
|
||||
# Meta Data Extension for Python-Markdown
|
||||
# =======================================
|
||||
|
||||
# This extension adds Meta Data handling to markdown.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/meta_data
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2007-2008 [Waylan Limberg](http://achinghead.com).
|
||||
|
||||
# All changes Copyright 2008-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
This extension adds Meta Data handling to markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/meta_data)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..preprocessors import Preprocessor
|
||||
import re
|
||||
import logging
|
||||
|
||||
log = logging.getLogger('MARKDOWN')
|
||||
|
||||
# Global Vars
|
||||
META_RE = re.compile(r'^[ ]{0,3}(?P<key>[A-Za-z0-9_-]+):\s*(?P<value>.*)')
|
||||
META_MORE_RE = re.compile(r'^[ ]{4,}(?P<value>.*)')
|
||||
BEGIN_RE = re.compile(r'^-{3}(\s.*)?')
|
||||
END_RE = re.compile(r'^(-{3}|\.{3})(\s.*)?')
|
||||
|
||||
|
||||
class MetaExtension (Extension):
|
||||
""" Meta-Data extension for Python-Markdown. """
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add `MetaPreprocessor` to Markdown instance. """
|
||||
md.registerExtension(self)
|
||||
self.md = md
|
||||
md.preprocessors.register(MetaPreprocessor(md), 'meta', 27)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.md.Meta = {}
|
||||
|
||||
|
||||
class MetaPreprocessor(Preprocessor):
|
||||
""" Get Meta-Data. """
|
||||
|
||||
def run(self, lines):
|
||||
""" Parse Meta-Data and store in Markdown.Meta. """
|
||||
meta = {}
|
||||
key = None
|
||||
if lines and BEGIN_RE.match(lines[0]):
|
||||
lines.pop(0)
|
||||
while lines:
|
||||
line = lines.pop(0)
|
||||
m1 = META_RE.match(line)
|
||||
if line.strip() == '' or END_RE.match(line):
|
||||
break # blank line or end of YAML header - done
|
||||
if m1:
|
||||
key = m1.group('key').lower().strip()
|
||||
value = m1.group('value').strip()
|
||||
try:
|
||||
meta[key].append(value)
|
||||
except KeyError:
|
||||
meta[key] = [value]
|
||||
else:
|
||||
m2 = META_MORE_RE.match(line)
|
||||
if m2 and key:
|
||||
# Add another line to existing key
|
||||
meta[key].append(m2.group('value').strip())
|
||||
else:
|
||||
lines.insert(0, line)
|
||||
break # no meta data - done
|
||||
self.md.Meta = meta
|
||||
return lines
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return MetaExtension(**kwargs)
|
@ -1,41 +0,0 @@
|
||||
# `NL2BR` Extension
|
||||
# ===============
|
||||
|
||||
# A Python-Markdown extension to treat newlines as hard breaks; like
|
||||
# GitHub-flavored Markdown does.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/nl2br
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2011 [Brian Neal](https://deathofagremmie.com/)
|
||||
|
||||
# All changes Copyright 2011-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
A Python-Markdown extension to treat newlines as hard breaks; like
|
||||
GitHub-flavored Markdown does.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/nl2br)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..inlinepatterns import SubstituteTagInlineProcessor
|
||||
|
||||
BR_RE = r'\n'
|
||||
|
||||
|
||||
class Nl2BrExtension(Extension):
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add a `SubstituteTagInlineProcessor` to Markdown. """
|
||||
br_tag = SubstituteTagInlineProcessor(BR_RE, 'br')
|
||||
md.inlinePatterns.register(br_tag, 'nl', 5)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return Nl2BrExtension(**kwargs)
|
@ -1,65 +0,0 @@
|
||||
# Sane List Extension for Python-Markdown
|
||||
# =======================================
|
||||
|
||||
# Modify the behavior of Lists in Python-Markdown to act in a sane manor.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/sane_lists
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2011 [Waylan Limberg](http://achinghead.com)
|
||||
|
||||
# All changes Copyright 2011-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Modify the behavior of Lists in Python-Markdown to act in a sane manor.
|
||||
|
||||
See [documentation](https://Python-Markdown.github.io/extensions/sane_lists)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..blockprocessors import OListProcessor, UListProcessor
|
||||
import re
|
||||
|
||||
|
||||
class SaneOListProcessor(OListProcessor):
|
||||
""" Override `SIBLING_TAGS` to not include `ul` and set `LAZY_OL` to `False`. """
|
||||
|
||||
SIBLING_TAGS = ['ol']
|
||||
""" Exclude `ul` from list of siblings. """
|
||||
LAZY_OL = False
|
||||
""" Disable lazy list behavior. """
|
||||
|
||||
def __init__(self, parser):
|
||||
super().__init__(parser)
|
||||
self.CHILD_RE = re.compile(r'^[ ]{0,%d}((\d+\.))[ ]+(.*)' %
|
||||
(self.tab_length - 1))
|
||||
|
||||
|
||||
class SaneUListProcessor(UListProcessor):
|
||||
""" Override `SIBLING_TAGS` to not include `ol`. """
|
||||
|
||||
SIBLING_TAGS = ['ul']
|
||||
""" Exclude `ol` from list of siblings. """
|
||||
|
||||
def __init__(self, parser):
|
||||
super().__init__(parser)
|
||||
self.CHILD_RE = re.compile(r'^[ ]{0,%d}(([*+-]))[ ]+(.*)' %
|
||||
(self.tab_length - 1))
|
||||
|
||||
|
||||
class SaneListExtension(Extension):
|
||||
""" Add sane lists to Markdown. """
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Override existing Processors. """
|
||||
md.parser.blockprocessors.register(SaneOListProcessor(md.parser), 'olist', 40)
|
||||
md.parser.blockprocessors.register(SaneUListProcessor(md.parser), 'ulist', 30)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return SaneListExtension(**kwargs)
|
@ -1,265 +0,0 @@
|
||||
# Smarty extension for Python-Markdown
|
||||
# ====================================
|
||||
|
||||
# Adds conversion of ASCII dashes, quotes and ellipses to their HTML
|
||||
# entity equivalents.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/smarty
|
||||
# for documentation.
|
||||
|
||||
# Author: 2013, Dmitry Shachnev <mitya57@gmail.com>
|
||||
|
||||
# All changes Copyright 2013-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
# SmartyPants license:
|
||||
|
||||
# Copyright (c) 2003 John Gruber <https://daringfireball.net/>
|
||||
# All rights reserved.
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in
|
||||
# the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
|
||||
# * Neither the name "SmartyPants" nor the names of its contributors
|
||||
# may be used to endorse or promote products derived from this
|
||||
# software without specific prior written permission.
|
||||
|
||||
# This software is provided by the copyright holders and contributors "as
|
||||
# is" and any express or implied warranties, including, but not limited
|
||||
# to, the implied warranties of merchantability and fitness for a
|
||||
# particular purpose are disclaimed. In no event shall the copyright
|
||||
# owner or contributors be liable for any direct, indirect, incidental,
|
||||
# special, exemplary, or consequential damages (including, but not
|
||||
# limited to, procurement of substitute goods or services; loss of use,
|
||||
# data, or profits; or business interruption) however caused and on any
|
||||
# theory of liability, whether in contract, strict liability, or tort
|
||||
# (including negligence or otherwise) arising in any way out of the use
|
||||
# of this software, even if advised of the possibility of such damage.
|
||||
|
||||
|
||||
# `smartypants.py` license:
|
||||
|
||||
# `smartypants.py` is a derivative work of SmartyPants.
|
||||
# Copyright (c) 2004, 2007 Chad Miller <http://web.chad.org/>
|
||||
|
||||
# Redistribution and use in source and binary forms, with or without
|
||||
# modification, are permitted provided that the following conditions are
|
||||
# met:
|
||||
|
||||
# * Redistributions of source code must retain the above copyright
|
||||
# notice, this list of conditions and the following disclaimer.
|
||||
|
||||
# * Redistributions in binary form must reproduce the above copyright
|
||||
# notice, this list of conditions and the following disclaimer in
|
||||
# the documentation and/or other materials provided with the
|
||||
# distribution.
|
||||
|
||||
# This software is provided by the copyright holders and contributors "as
|
||||
# is" and any express or implied warranties, including, but not limited
|
||||
# to, the implied warranties of merchantability and fitness for a
|
||||
# particular purpose are disclaimed. In no event shall the copyright
|
||||
# owner or contributors be liable for any direct, indirect, incidental,
|
||||
# special, exemplary, or consequential damages (including, but not
|
||||
# limited to, procurement of substitute goods or services; loss of use,
|
||||
# data, or profits; or business interruption) however caused and on any
|
||||
# theory of liability, whether in contract, strict liability, or tort
|
||||
# (including negligence or otherwise) arising in any way out of the use
|
||||
# of this software, even if advised of the possibility of such damage.
|
||||
|
||||
"""
|
||||
Adds conversion of ASCII dashes, quotes and ellipses to their HTML
|
||||
entity equivalents.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/smarty)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..inlinepatterns import HtmlInlineProcessor, HTML_RE
|
||||
from ..treeprocessors import InlineProcessor
|
||||
from ..util import Registry
|
||||
|
||||
|
||||
# Constants for quote education.
|
||||
punctClass = r"""[!"#\$\%'()*+,-.\/:;<=>?\@\[\\\]\^_`{|}~]"""
|
||||
endOfWordClass = r"[\s.,;:!?)]"
|
||||
closeClass = r"[^\ \t\r\n\[\{\(\-\u0002\u0003]"
|
||||
|
||||
openingQuotesBase = (
|
||||
r'(\s' # a whitespace char
|
||||
r'| ' # or a non-breaking space entity
|
||||
r'|--' # or dashes
|
||||
r'|–|—' # or Unicode
|
||||
r'|&[mn]dash;' # or named dash entities
|
||||
r'|–|—' # or decimal entities
|
||||
r')'
|
||||
)
|
||||
|
||||
substitutions = {
|
||||
'mdash': '—',
|
||||
'ndash': '–',
|
||||
'ellipsis': '…',
|
||||
'left-angle-quote': '«',
|
||||
'right-angle-quote': '»',
|
||||
'left-single-quote': '‘',
|
||||
'right-single-quote': '’',
|
||||
'left-double-quote': '“',
|
||||
'right-double-quote': '”',
|
||||
}
|
||||
|
||||
|
||||
# Special case if the very first character is a quote
|
||||
# followed by punctuation at a non-word-break. Close the quotes by brute force:
|
||||
singleQuoteStartRe = r"^'(?=%s\B)" % punctClass
|
||||
doubleQuoteStartRe = r'^"(?=%s\B)' % punctClass
|
||||
|
||||
# Special case for double sets of quotes, e.g.:
|
||||
# <p>He said, "'Quoted' words in a larger quote."</p>
|
||||
doubleQuoteSetsRe = r""""'(?=\w)"""
|
||||
singleQuoteSetsRe = r"""'"(?=\w)"""
|
||||
|
||||
# Special case for decade abbreviations (the '80s):
|
||||
decadeAbbrRe = r"(?<!\w)'(?=\d{2}s)"
|
||||
|
||||
# Get most opening double quotes:
|
||||
openingDoubleQuotesRegex = r'%s"(?=\w)' % openingQuotesBase
|
||||
|
||||
# Double closing quotes:
|
||||
closingDoubleQuotesRegex = r'"(?=\s)'
|
||||
closingDoubleQuotesRegex2 = '(?<=%s)"' % closeClass
|
||||
|
||||
# Get most opening single quotes:
|
||||
openingSingleQuotesRegex = r"%s'(?=\w)" % openingQuotesBase
|
||||
|
||||
# Single closing quotes:
|
||||
closingSingleQuotesRegex = r"(?<=%s)'(?!\s|s\b|\d)" % closeClass
|
||||
closingSingleQuotesRegex2 = r"'(\s|s\b)"
|
||||
|
||||
# All remaining quotes should be opening ones
|
||||
remainingSingleQuotesRegex = r"'"
|
||||
remainingDoubleQuotesRegex = r'"'
|
||||
|
||||
HTML_STRICT_RE = HTML_RE + r'(?!\>)'
|
||||
|
||||
|
||||
class SubstituteTextPattern(HtmlInlineProcessor):
|
||||
def __init__(self, pattern, replace, md):
|
||||
""" Replaces matches with some text. """
|
||||
HtmlInlineProcessor.__init__(self, pattern)
|
||||
self.replace = replace
|
||||
self.md = md
|
||||
|
||||
def handleMatch(self, m, data):
|
||||
result = ''
|
||||
for part in self.replace:
|
||||
if isinstance(part, int):
|
||||
result += m.group(part)
|
||||
else:
|
||||
result += self.md.htmlStash.store(part)
|
||||
return result, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class SmartyExtension(Extension):
|
||||
""" Add Smarty to Markdown. """
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
'smart_quotes': [True, 'Educate quotes'],
|
||||
'smart_angled_quotes': [False, 'Educate angled quotes'],
|
||||
'smart_dashes': [True, 'Educate dashes'],
|
||||
'smart_ellipses': [True, 'Educate ellipses'],
|
||||
'substitutions': [{}, 'Overwrite default substitutions'],
|
||||
}
|
||||
""" Default configuration options. """
|
||||
super().__init__(**kwargs)
|
||||
self.substitutions = dict(substitutions)
|
||||
self.substitutions.update(self.getConfig('substitutions', default={}))
|
||||
|
||||
def _addPatterns(self, md, patterns, serie, priority):
|
||||
for ind, pattern in enumerate(patterns):
|
||||
pattern += (md,)
|
||||
pattern = SubstituteTextPattern(*pattern)
|
||||
name = 'smarty-%s-%d' % (serie, ind)
|
||||
self.inlinePatterns.register(pattern, name, priority-ind)
|
||||
|
||||
def educateDashes(self, md) -> None:
|
||||
emDashesPattern = SubstituteTextPattern(
|
||||
r'(?<!-)---(?!-)', (self.substitutions['mdash'],), md
|
||||
)
|
||||
enDashesPattern = SubstituteTextPattern(
|
||||
r'(?<!-)--(?!-)', (self.substitutions['ndash'],), md
|
||||
)
|
||||
self.inlinePatterns.register(emDashesPattern, 'smarty-em-dashes', 50)
|
||||
self.inlinePatterns.register(enDashesPattern, 'smarty-en-dashes', 45)
|
||||
|
||||
def educateEllipses(self, md) -> None:
|
||||
ellipsesPattern = SubstituteTextPattern(
|
||||
r'(?<!\.)\.{3}(?!\.)', (self.substitutions['ellipsis'],), md
|
||||
)
|
||||
self.inlinePatterns.register(ellipsesPattern, 'smarty-ellipses', 10)
|
||||
|
||||
def educateAngledQuotes(self, md) -> None:
|
||||
leftAngledQuotePattern = SubstituteTextPattern(
|
||||
r'\<\<', (self.substitutions['left-angle-quote'],), md
|
||||
)
|
||||
rightAngledQuotePattern = SubstituteTextPattern(
|
||||
r'\>\>', (self.substitutions['right-angle-quote'],), md
|
||||
)
|
||||
self.inlinePatterns.register(leftAngledQuotePattern, 'smarty-left-angle-quotes', 40)
|
||||
self.inlinePatterns.register(rightAngledQuotePattern, 'smarty-right-angle-quotes', 35)
|
||||
|
||||
def educateQuotes(self, md) -> None:
|
||||
lsquo = self.substitutions['left-single-quote']
|
||||
rsquo = self.substitutions['right-single-quote']
|
||||
ldquo = self.substitutions['left-double-quote']
|
||||
rdquo = self.substitutions['right-double-quote']
|
||||
patterns = (
|
||||
(singleQuoteStartRe, (rsquo,)),
|
||||
(doubleQuoteStartRe, (rdquo,)),
|
||||
(doubleQuoteSetsRe, (ldquo + lsquo,)),
|
||||
(singleQuoteSetsRe, (lsquo + ldquo,)),
|
||||
(decadeAbbrRe, (rsquo,)),
|
||||
(openingSingleQuotesRegex, (1, lsquo)),
|
||||
(closingSingleQuotesRegex, (rsquo,)),
|
||||
(closingSingleQuotesRegex2, (rsquo, 1)),
|
||||
(remainingSingleQuotesRegex, (lsquo,)),
|
||||
(openingDoubleQuotesRegex, (1, ldquo)),
|
||||
(closingDoubleQuotesRegex, (rdquo,)),
|
||||
(closingDoubleQuotesRegex2, (rdquo,)),
|
||||
(remainingDoubleQuotesRegex, (ldquo,))
|
||||
)
|
||||
self._addPatterns(md, patterns, 'quotes', 30)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
configs = self.getConfigs()
|
||||
self.inlinePatterns: Registry[HtmlInlineProcessor] = Registry()
|
||||
if configs['smart_ellipses']:
|
||||
self.educateEllipses(md)
|
||||
if configs['smart_quotes']:
|
||||
self.educateQuotes(md)
|
||||
if configs['smart_angled_quotes']:
|
||||
self.educateAngledQuotes(md)
|
||||
# Override `HTML_RE` from `inlinepatterns.py` so that it does not
|
||||
# process tags with duplicate closing quotes.
|
||||
md.inlinePatterns.register(HtmlInlineProcessor(HTML_STRICT_RE, md), 'html', 90)
|
||||
if configs['smart_dashes']:
|
||||
self.educateDashes(md)
|
||||
inlineProcessor = InlineProcessor(md)
|
||||
inlineProcessor.inlinePatterns = self.inlinePatterns
|
||||
md.treeprocessors.register(inlineProcessor, 'smarty', 2)
|
||||
md.ESCAPED_CHARS.extend(['"', "'"])
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return SmartyExtension(**kwargs)
|
@ -1,243 +0,0 @@
|
||||
# Tables Extension for Python-Markdown
|
||||
# ====================================
|
||||
|
||||
# Added parsing of tables to Python-Markdown.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/tables
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2009 [Waylan Limberg](http://achinghead.com)
|
||||
|
||||
# All changes Copyright 2008-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Added parsing of tables to Python-Markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/tables)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..blockprocessors import BlockProcessor
|
||||
import xml.etree.ElementTree as etree
|
||||
import re
|
||||
PIPE_NONE = 0
|
||||
PIPE_LEFT = 1
|
||||
PIPE_RIGHT = 2
|
||||
|
||||
|
||||
class TableProcessor(BlockProcessor):
|
||||
""" Process Tables. """
|
||||
|
||||
RE_CODE_PIPES = re.compile(r'(?:(\\\\)|(\\`+)|(`+)|(\\\|)|(\|))')
|
||||
RE_END_BORDER = re.compile(r'(?<!\\)(?:\\\\)*\|$')
|
||||
|
||||
def __init__(self, parser, config):
|
||||
self.border = False
|
||||
self.separator = ''
|
||||
self.config = config
|
||||
|
||||
super().__init__(parser)
|
||||
|
||||
def test(self, parent, block):
|
||||
"""
|
||||
Ensure first two rows (column header and separator row) are valid table rows.
|
||||
|
||||
Keep border check and separator row do avoid repeating the work.
|
||||
"""
|
||||
is_table = False
|
||||
rows = [row.strip(' ') for row in block.split('\n')]
|
||||
if len(rows) > 1:
|
||||
header0 = rows[0]
|
||||
self.border = PIPE_NONE
|
||||
if header0.startswith('|'):
|
||||
self.border |= PIPE_LEFT
|
||||
if self.RE_END_BORDER.search(header0) is not None:
|
||||
self.border |= PIPE_RIGHT
|
||||
row = self._split_row(header0)
|
||||
row0_len = len(row)
|
||||
is_table = row0_len > 1
|
||||
|
||||
# Each row in a single column table needs at least one pipe.
|
||||
if not is_table and row0_len == 1 and self.border:
|
||||
for index in range(1, len(rows)):
|
||||
is_table = rows[index].startswith('|')
|
||||
if not is_table:
|
||||
is_table = self.RE_END_BORDER.search(rows[index]) is not None
|
||||
if not is_table:
|
||||
break
|
||||
|
||||
if is_table:
|
||||
row = self._split_row(rows[1])
|
||||
is_table = (len(row) == row0_len) and set(''.join(row)) <= set('|:- ')
|
||||
if is_table:
|
||||
self.separator = row
|
||||
|
||||
return is_table
|
||||
|
||||
def run(self, parent, blocks):
|
||||
""" Parse a table block and build table. """
|
||||
block = blocks.pop(0).split('\n')
|
||||
header = block[0].strip(' ')
|
||||
rows = [] if len(block) < 3 else block[2:]
|
||||
|
||||
# Get alignment of columns
|
||||
align = []
|
||||
for c in self.separator:
|
||||
c = c.strip(' ')
|
||||
if c.startswith(':') and c.endswith(':'):
|
||||
align.append('center')
|
||||
elif c.startswith(':'):
|
||||
align.append('left')
|
||||
elif c.endswith(':'):
|
||||
align.append('right')
|
||||
else:
|
||||
align.append(None)
|
||||
|
||||
# Build table
|
||||
table = etree.SubElement(parent, 'table')
|
||||
thead = etree.SubElement(table, 'thead')
|
||||
self._build_row(header, thead, align)
|
||||
tbody = etree.SubElement(table, 'tbody')
|
||||
if len(rows) == 0:
|
||||
# Handle empty table
|
||||
self._build_empty_row(tbody, align)
|
||||
else:
|
||||
for row in rows:
|
||||
self._build_row(row.strip(' '), tbody, align)
|
||||
|
||||
def _build_empty_row(self, parent, align):
|
||||
"""Build an empty row."""
|
||||
tr = etree.SubElement(parent, 'tr')
|
||||
count = len(align)
|
||||
while count:
|
||||
etree.SubElement(tr, 'td')
|
||||
count -= 1
|
||||
|
||||
def _build_row(self, row, parent, align):
|
||||
""" Given a row of text, build table cells. """
|
||||
tr = etree.SubElement(parent, 'tr')
|
||||
tag = 'td'
|
||||
if parent.tag == 'thead':
|
||||
tag = 'th'
|
||||
cells = self._split_row(row)
|
||||
# We use align here rather than cells to ensure every row
|
||||
# contains the same number of columns.
|
||||
for i, a in enumerate(align):
|
||||
c = etree.SubElement(tr, tag)
|
||||
try:
|
||||
c.text = cells[i].strip(' ')
|
||||
except IndexError: # pragma: no cover
|
||||
c.text = ""
|
||||
if a:
|
||||
if self.config['use_align_attribute']:
|
||||
c.set('align', a)
|
||||
else:
|
||||
c.set('style', f'text-align: {a};')
|
||||
|
||||
def _split_row(self, row):
|
||||
""" split a row of text into list of cells. """
|
||||
if self.border:
|
||||
if row.startswith('|'):
|
||||
row = row[1:]
|
||||
row = self.RE_END_BORDER.sub('', row)
|
||||
return self._split(row)
|
||||
|
||||
def _split(self, row):
|
||||
""" split a row of text with some code into a list of cells. """
|
||||
elements = []
|
||||
pipes = []
|
||||
tics = []
|
||||
tic_points = []
|
||||
tic_region = []
|
||||
good_pipes = []
|
||||
|
||||
# Parse row
|
||||
# Throw out \\, and \|
|
||||
for m in self.RE_CODE_PIPES.finditer(row):
|
||||
# Store ` data (len, start_pos, end_pos)
|
||||
if m.group(2):
|
||||
# \`+
|
||||
# Store length of each tic group: subtract \
|
||||
tics.append(len(m.group(2)) - 1)
|
||||
# Store start of group, end of group, and escape length
|
||||
tic_points.append((m.start(2), m.end(2) - 1, 1))
|
||||
elif m.group(3):
|
||||
# `+
|
||||
# Store length of each tic group
|
||||
tics.append(len(m.group(3)))
|
||||
# Store start of group, end of group, and escape length
|
||||
tic_points.append((m.start(3), m.end(3) - 1, 0))
|
||||
# Store pipe location
|
||||
elif m.group(5):
|
||||
pipes.append(m.start(5))
|
||||
|
||||
# Pair up tics according to size if possible
|
||||
# Subtract the escape length *only* from the opening.
|
||||
# Walk through tic list and see if tic has a close.
|
||||
# Store the tic region (start of region, end of region).
|
||||
pos = 0
|
||||
tic_len = len(tics)
|
||||
while pos < tic_len:
|
||||
try:
|
||||
tic_size = tics[pos] - tic_points[pos][2]
|
||||
if tic_size == 0:
|
||||
raise ValueError
|
||||
index = tics[pos + 1:].index(tic_size) + 1
|
||||
tic_region.append((tic_points[pos][0], tic_points[pos + index][1]))
|
||||
pos += index + 1
|
||||
except ValueError:
|
||||
pos += 1
|
||||
|
||||
# Resolve pipes. Check if they are within a tic pair region.
|
||||
# Walk through pipes comparing them to each region.
|
||||
# - If pipe position is less that a region, it isn't in a region
|
||||
# - If it is within a region, we don't want it, so throw it out
|
||||
# - If we didn't throw it out, it must be a table pipe
|
||||
for pipe in pipes:
|
||||
throw_out = False
|
||||
for region in tic_region:
|
||||
if pipe < region[0]:
|
||||
# Pipe is not in a region
|
||||
break
|
||||
elif region[0] <= pipe <= region[1]:
|
||||
# Pipe is within a code region. Throw it out.
|
||||
throw_out = True
|
||||
break
|
||||
if not throw_out:
|
||||
good_pipes.append(pipe)
|
||||
|
||||
# Split row according to table delimiters.
|
||||
pos = 0
|
||||
for pipe in good_pipes:
|
||||
elements.append(row[pos:pipe])
|
||||
pos = pipe + 1
|
||||
elements.append(row[pos:])
|
||||
return elements
|
||||
|
||||
|
||||
class TableExtension(Extension):
|
||||
""" Add tables to Markdown. """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
'use_align_attribute': [False, 'True to use align attribute instead of style.'],
|
||||
}
|
||||
""" Default configuration options. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add an instance of `TableProcessor` to `BlockParser`. """
|
||||
if '|' not in md.ESCAPED_CHARS:
|
||||
md.ESCAPED_CHARS.append('|')
|
||||
processor = TableProcessor(md.parser, self.getConfigs())
|
||||
md.parser.blockprocessors.register(processor, 'table', 75)
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return TableExtension(**kwargs)
|
@ -1,408 +0,0 @@
|
||||
# Table of Contents Extension for Python-Markdown
|
||||
# ===============================================
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/toc
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright 2008 [Jack Miller](https://codezen.org/)
|
||||
|
||||
# All changes Copyright 2008-2014 The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Add table of contents support to Python-Markdown.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/toc)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..treeprocessors import Treeprocessor
|
||||
from ..util import code_escape, parseBoolValue, AMP_SUBSTITUTE, HTML_PLACEHOLDER_RE, AtomicString
|
||||
from ..treeprocessors import UnescapeTreeprocessor
|
||||
import re
|
||||
import html
|
||||
import unicodedata
|
||||
import xml.etree.ElementTree as etree
|
||||
|
||||
|
||||
def slugify(value, separator, unicode=False):
|
||||
""" Slugify a string, to make it URL friendly. """
|
||||
if not unicode:
|
||||
# Replace Extended Latin characters with ASCII, i.e. `žlutý` => `zluty`
|
||||
value = unicodedata.normalize('NFKD', value)
|
||||
value = value.encode('ascii', 'ignore').decode('ascii')
|
||||
value = re.sub(r'[^\w\s-]', '', value).strip().lower()
|
||||
return re.sub(r'[{}\s]+'.format(separator), separator, value)
|
||||
|
||||
|
||||
def slugify_unicode(value, separator):
|
||||
""" Slugify a string, to make it URL friendly while preserving Unicode characters. """
|
||||
return slugify(value, separator, unicode=True)
|
||||
|
||||
|
||||
IDCOUNT_RE = re.compile(r'^(.*)_([0-9]+)$')
|
||||
|
||||
|
||||
def unique(id, ids):
|
||||
""" Ensure id is unique in set of ids. Append '_1', '_2'... if not """
|
||||
while id in ids or not id:
|
||||
m = IDCOUNT_RE.match(id)
|
||||
if m:
|
||||
id = '%s_%d' % (m.group(1), int(m.group(2))+1)
|
||||
else:
|
||||
id = '%s_%d' % (id, 1)
|
||||
ids.add(id)
|
||||
return id
|
||||
|
||||
|
||||
def get_name(el):
|
||||
"""Get title name."""
|
||||
|
||||
text = []
|
||||
for c in el.itertext():
|
||||
if isinstance(c, AtomicString):
|
||||
text.append(html.unescape(c))
|
||||
else:
|
||||
text.append(c)
|
||||
return ''.join(text).strip()
|
||||
|
||||
|
||||
def stashedHTML2text(text, md, strip_entities: bool = True):
|
||||
""" Extract raw HTML from stash, reduce to plain text and swap with placeholder. """
|
||||
def _html_sub(m):
|
||||
""" Substitute raw html with plain text. """
|
||||
try:
|
||||
raw = md.htmlStash.rawHtmlBlocks[int(m.group(1))]
|
||||
except (IndexError, TypeError): # pragma: no cover
|
||||
return m.group(0)
|
||||
# Strip out tags and/or entities - leaving text
|
||||
res = re.sub(r'(<[^>]+>)', '', raw)
|
||||
if strip_entities:
|
||||
res = re.sub(r'(&[\#a-zA-Z0-9]+;)', '', res)
|
||||
return res
|
||||
|
||||
return HTML_PLACEHOLDER_RE.sub(_html_sub, text)
|
||||
|
||||
|
||||
def unescape(text):
|
||||
""" Unescape escaped text. """
|
||||
c = UnescapeTreeprocessor()
|
||||
return c.unescape(text)
|
||||
|
||||
|
||||
def nest_toc_tokens(toc_list):
|
||||
"""Given an unsorted list with errors and skips, return a nested one.
|
||||
|
||||
[{'level': 1}, {'level': 2}]
|
||||
=>
|
||||
[{'level': 1, 'children': [{'level': 2, 'children': []}]}]
|
||||
|
||||
A wrong list is also converted:
|
||||
|
||||
[{'level': 2}, {'level': 1}]
|
||||
=>
|
||||
[{'level': 2, 'children': []}, {'level': 1, 'children': []}]
|
||||
"""
|
||||
|
||||
ordered_list = []
|
||||
if len(toc_list):
|
||||
# Initialize everything by processing the first entry
|
||||
last = toc_list.pop(0)
|
||||
last['children'] = []
|
||||
levels = [last['level']]
|
||||
ordered_list.append(last)
|
||||
parents = []
|
||||
|
||||
# Walk the rest nesting the entries properly
|
||||
while toc_list:
|
||||
t = toc_list.pop(0)
|
||||
current_level = t['level']
|
||||
t['children'] = []
|
||||
|
||||
# Reduce depth if current level < last item's level
|
||||
if current_level < levels[-1]:
|
||||
# Pop last level since we know we are less than it
|
||||
levels.pop()
|
||||
|
||||
# Pop parents and levels we are less than or equal to
|
||||
to_pop = 0
|
||||
for p in reversed(parents):
|
||||
if current_level <= p['level']:
|
||||
to_pop += 1
|
||||
else: # pragma: no cover
|
||||
break
|
||||
if to_pop:
|
||||
levels = levels[:-to_pop]
|
||||
parents = parents[:-to_pop]
|
||||
|
||||
# Note current level as last
|
||||
levels.append(current_level)
|
||||
|
||||
# Level is the same, so append to
|
||||
# the current parent (if available)
|
||||
if current_level == levels[-1]:
|
||||
(parents[-1]['children'] if parents
|
||||
else ordered_list).append(t)
|
||||
|
||||
# Current level is > last item's level,
|
||||
# So make last item a parent and append current as child
|
||||
else:
|
||||
last['children'].append(t)
|
||||
parents.append(last)
|
||||
levels.append(current_level)
|
||||
last = t
|
||||
|
||||
return ordered_list
|
||||
|
||||
|
||||
class TocTreeprocessor(Treeprocessor):
|
||||
""" Step through document and build TOC. """
|
||||
|
||||
def __init__(self, md, config):
|
||||
super().__init__(md)
|
||||
|
||||
self.marker = config["marker"]
|
||||
self.title = config["title"]
|
||||
self.base_level = int(config["baselevel"]) - 1
|
||||
self.slugify = config["slugify"]
|
||||
self.sep = config["separator"]
|
||||
self.toc_class = config["toc_class"]
|
||||
self.title_class = config["title_class"]
|
||||
self.use_anchors = parseBoolValue(config["anchorlink"])
|
||||
self.anchorlink_class = config["anchorlink_class"]
|
||||
self.use_permalinks = parseBoolValue(config["permalink"], False)
|
||||
if self.use_permalinks is None:
|
||||
self.use_permalinks = config["permalink"]
|
||||
self.permalink_class = config["permalink_class"]
|
||||
self.permalink_title = config["permalink_title"]
|
||||
self.permalink_leading = parseBoolValue(config["permalink_leading"], False)
|
||||
self.header_rgx = re.compile("[Hh][123456]")
|
||||
if isinstance(config["toc_depth"], str) and '-' in config["toc_depth"]:
|
||||
self.toc_top, self.toc_bottom = [int(x) for x in config["toc_depth"].split('-')]
|
||||
else:
|
||||
self.toc_top = 1
|
||||
self.toc_bottom = int(config["toc_depth"])
|
||||
|
||||
def iterparent(self, node):
|
||||
""" Iterator wrapper to get allowed parent and child all at once. """
|
||||
|
||||
# We do not allow the marker inside a header as that
|
||||
# would causes an endless loop of placing a new TOC
|
||||
# inside previously generated TOC.
|
||||
for child in node:
|
||||
if not self.header_rgx.match(child.tag) and child.tag not in ['pre', 'code']:
|
||||
yield node, child
|
||||
yield from self.iterparent(child)
|
||||
|
||||
def replace_marker(self, root, elem) -> None:
|
||||
""" Replace marker with elem. """
|
||||
for (p, c) in self.iterparent(root):
|
||||
text = ''.join(c.itertext()).strip()
|
||||
if not text:
|
||||
continue
|
||||
|
||||
# To keep the output from screwing up the
|
||||
# validation by putting a `<div>` inside of a `<p>`
|
||||
# we actually replace the `<p>` in its entirety.
|
||||
|
||||
# The `<p>` element may contain more than a single text content
|
||||
# (`nl2br` can introduce a `<br>`). In this situation, `c.text` returns
|
||||
# the very first content, ignore children contents or tail content.
|
||||
# `len(c) == 0` is here to ensure there is only text in the `<p>`.
|
||||
if c.text and c.text.strip() == self.marker and len(c) == 0:
|
||||
for i in range(len(p)):
|
||||
if p[i] == c:
|
||||
p[i] = elem
|
||||
break
|
||||
|
||||
def set_level(self, elem) -> None:
|
||||
""" Adjust header level according to base level. """
|
||||
level = int(elem.tag[-1]) + self.base_level
|
||||
if level > 6:
|
||||
level = 6
|
||||
elem.tag = 'h%d' % level
|
||||
|
||||
def add_anchor(self, c, elem_id) -> None:
|
||||
anchor = etree.Element("a")
|
||||
anchor.text = c.text
|
||||
anchor.attrib["href"] = "#" + elem_id
|
||||
anchor.attrib["class"] = self.anchorlink_class
|
||||
c.text = ""
|
||||
for elem in c:
|
||||
anchor.append(elem)
|
||||
while len(c):
|
||||
c.remove(c[0])
|
||||
c.append(anchor)
|
||||
|
||||
def add_permalink(self, c, elem_id) -> None:
|
||||
permalink = etree.Element("a")
|
||||
permalink.text = ("%spara;" % AMP_SUBSTITUTE
|
||||
if self.use_permalinks is True
|
||||
else self.use_permalinks)
|
||||
permalink.attrib["href"] = "#" + elem_id
|
||||
permalink.attrib["class"] = self.permalink_class
|
||||
if self.permalink_title:
|
||||
permalink.attrib["title"] = self.permalink_title
|
||||
if self.permalink_leading:
|
||||
permalink.tail = c.text
|
||||
c.text = ""
|
||||
c.insert(0, permalink)
|
||||
else:
|
||||
c.append(permalink)
|
||||
|
||||
def build_toc_div(self, toc_list):
|
||||
""" Return a string div given a toc list. """
|
||||
div = etree.Element("div")
|
||||
div.attrib["class"] = self.toc_class
|
||||
|
||||
# Add title to the div
|
||||
if self.title:
|
||||
header = etree.SubElement(div, "span")
|
||||
if self.title_class:
|
||||
header.attrib["class"] = self.title_class
|
||||
header.text = self.title
|
||||
|
||||
def build_etree_ul(toc_list, parent):
|
||||
ul = etree.SubElement(parent, "ul")
|
||||
for item in toc_list:
|
||||
# List item link, to be inserted into the toc div
|
||||
li = etree.SubElement(ul, "li")
|
||||
link = etree.SubElement(li, "a")
|
||||
link.text = item.get('name', '')
|
||||
link.attrib["href"] = '#' + item.get('id', '')
|
||||
if item['children']:
|
||||
build_etree_ul(item['children'], li)
|
||||
return ul
|
||||
|
||||
build_etree_ul(toc_list, div)
|
||||
|
||||
if 'prettify' in self.md.treeprocessors:
|
||||
self.md.treeprocessors['prettify'].run(div)
|
||||
|
||||
return div
|
||||
|
||||
def run(self, doc):
|
||||
# Get a list of id attributes
|
||||
used_ids = set()
|
||||
for el in doc.iter():
|
||||
if "id" in el.attrib:
|
||||
used_ids.add(el.attrib["id"])
|
||||
|
||||
toc_tokens = []
|
||||
for el in doc.iter():
|
||||
if isinstance(el.tag, str) and self.header_rgx.match(el.tag):
|
||||
self.set_level(el)
|
||||
text = get_name(el)
|
||||
|
||||
# Do not override pre-existing ids
|
||||
if "id" not in el.attrib:
|
||||
innertext = unescape(stashedHTML2text(text, self.md))
|
||||
el.attrib["id"] = unique(self.slugify(innertext, self.sep), used_ids)
|
||||
|
||||
if int(el.tag[-1]) >= self.toc_top and int(el.tag[-1]) <= self.toc_bottom:
|
||||
toc_tokens.append({
|
||||
'level': int(el.tag[-1]),
|
||||
'id': el.attrib["id"],
|
||||
'name': unescape(stashedHTML2text(
|
||||
code_escape(el.attrib.get('data-toc-label', text)),
|
||||
self.md, strip_entities=False
|
||||
))
|
||||
})
|
||||
|
||||
# Remove the data-toc-label attribute as it is no longer needed
|
||||
if 'data-toc-label' in el.attrib:
|
||||
del el.attrib['data-toc-label']
|
||||
|
||||
if self.use_anchors:
|
||||
self.add_anchor(el, el.attrib["id"])
|
||||
if self.use_permalinks not in [False, None]:
|
||||
self.add_permalink(el, el.attrib["id"])
|
||||
|
||||
toc_tokens = nest_toc_tokens(toc_tokens)
|
||||
div = self.build_toc_div(toc_tokens)
|
||||
if self.marker:
|
||||
self.replace_marker(doc, div)
|
||||
|
||||
# serialize and attach to markdown instance.
|
||||
toc = self.md.serializer(div)
|
||||
for pp in self.md.postprocessors:
|
||||
toc = pp.run(toc)
|
||||
self.md.toc_tokens = toc_tokens
|
||||
self.md.toc = toc
|
||||
|
||||
|
||||
class TocExtension(Extension):
|
||||
|
||||
TreeProcessorClass = TocTreeprocessor
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
'marker': [
|
||||
'[TOC]',
|
||||
'Text to find and replace with Table of Contents. Set to an empty string to disable. '
|
||||
'Default: `[TOC]`.'
|
||||
],
|
||||
'title': [
|
||||
'', 'Title to insert into TOC `<div>`. Default: an empty string.'
|
||||
],
|
||||
'title_class': [
|
||||
'toctitle', 'CSS class used for the title. Default: `toctitle`.'
|
||||
],
|
||||
'toc_class': [
|
||||
'toc', 'CSS class(es) used for the link. Default: `toclink`.'
|
||||
],
|
||||
'anchorlink': [
|
||||
False, 'True if header should be a self link. Default: `False`.'
|
||||
],
|
||||
'anchorlink_class': [
|
||||
'toclink', 'CSS class(es) used for the link. Defaults: `toclink`.'
|
||||
],
|
||||
'permalink': [
|
||||
0, 'True or link text if a Sphinx-style permalink should be added. Default: `False`.'
|
||||
],
|
||||
'permalink_class': [
|
||||
'headerlink', 'CSS class(es) used for the link. Default: `headerlink`.'
|
||||
],
|
||||
'permalink_title': [
|
||||
'Permanent link', 'Title attribute of the permalink. Default: `Permanent link`.'
|
||||
],
|
||||
'permalink_leading': [
|
||||
False,
|
||||
'True if permalinks should be placed at start of the header, rather than end. Default: False.'
|
||||
],
|
||||
'baselevel': ['1', 'Base level for headers. Default: `1`.'],
|
||||
'slugify': [
|
||||
slugify, 'Function to generate anchors based on header text. Default: `slugify`.'
|
||||
],
|
||||
'separator': ['-', 'Word separator. Default: `-`.'],
|
||||
'toc_depth': [
|
||||
6,
|
||||
'Define the range of section levels to include in the Table of Contents. A single integer '
|
||||
'(b) defines the bottom section level (<h1>..<hb>) only. A string consisting of two digits '
|
||||
'separated by a hyphen in between (`2-5`) defines the top (t) and the bottom (b) (<ht>..<hb>). '
|
||||
'Default: `6` (bottom).'
|
||||
],
|
||||
}
|
||||
""" Default configuration options. """
|
||||
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
""" Add TOC tree processor to Markdown. """
|
||||
md.registerExtension(self)
|
||||
self.md = md
|
||||
self.reset()
|
||||
tocext = self.TreeProcessorClass(md, self.getConfigs())
|
||||
md.treeprocessors.register(tocext, 'toc', 5)
|
||||
|
||||
def reset(self) -> None:
|
||||
self.md.toc = ''
|
||||
self.md.toc_tokens = []
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return TocExtension(**kwargs)
|
@ -1,96 +0,0 @@
|
||||
# WikiLinks Extension for Python-Markdown
|
||||
# ======================================
|
||||
|
||||
# Converts [[WikiLinks]] to relative links.
|
||||
|
||||
# See https://Python-Markdown.github.io/extensions/wikilinks
|
||||
# for documentation.
|
||||
|
||||
# Original code Copyright [Waylan Limberg](http://achinghead.com/).
|
||||
|
||||
# All changes Copyright The Python Markdown Project
|
||||
|
||||
# License: [BSD](https://opensource.org/licenses/bsd-license.php)
|
||||
|
||||
"""
|
||||
Converts `[[WikiLinks]]` to relative links.
|
||||
|
||||
See the [documentation](https://Python-Markdown.github.io/extensions/wikilinks)
|
||||
for details.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import Extension
|
||||
from ..inlinepatterns import InlineProcessor
|
||||
import xml.etree.ElementTree as etree
|
||||
import re
|
||||
|
||||
|
||||
def build_url(label, base, end):
|
||||
""" Build a URL from the label, a base, and an end. """
|
||||
clean_label = re.sub(r'([ ]+_)|(_[ ]+)|([ ]+)', '_', label)
|
||||
return '{}{}{}'.format(base, clean_label, end)
|
||||
|
||||
|
||||
class WikiLinkExtension(Extension):
|
||||
""" Add inline processor to Markdown. """
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
self.config = {
|
||||
'base_url': ['/', 'String to append to beginning or URL.'],
|
||||
'end_url': ['/', 'String to append to end of URL.'],
|
||||
'html_class': ['wikilink', 'CSS hook. Leave blank for none.'],
|
||||
'build_url': [build_url, 'Callable formats URL from label.'],
|
||||
}
|
||||
""" Default configuration options. """
|
||||
super().__init__(**kwargs)
|
||||
|
||||
def extendMarkdown(self, md):
|
||||
self.md = md
|
||||
|
||||
# append to end of inline patterns
|
||||
WIKILINK_RE = r'\[\[([\w0-9_ -]+)\]\]'
|
||||
wikilinkPattern = WikiLinksInlineProcessor(WIKILINK_RE, self.getConfigs())
|
||||
wikilinkPattern.md = md
|
||||
md.inlinePatterns.register(wikilinkPattern, 'wikilink', 75)
|
||||
|
||||
|
||||
class WikiLinksInlineProcessor(InlineProcessor):
|
||||
""" Build link from `wikilink`. """
|
||||
|
||||
def __init__(self, pattern, config):
|
||||
super().__init__(pattern)
|
||||
self.config = config
|
||||
|
||||
def handleMatch(self, m, data):
|
||||
if m.group(1).strip():
|
||||
base_url, end_url, html_class = self._getMeta()
|
||||
label = m.group(1).strip()
|
||||
url = self.config['build_url'](label, base_url, end_url)
|
||||
a = etree.Element('a')
|
||||
a.text = label
|
||||
a.set('href', url)
|
||||
if html_class:
|
||||
a.set('class', html_class)
|
||||
else:
|
||||
a = ''
|
||||
return a, m.start(0), m.end(0)
|
||||
|
||||
def _getMeta(self):
|
||||
""" Return meta data or `config` data. """
|
||||
base_url = self.config['base_url']
|
||||
end_url = self.config['end_url']
|
||||
html_class = self.config['html_class']
|
||||
if hasattr(self.md, 'Meta'):
|
||||
if 'wiki_base_url' in self.md.Meta:
|
||||
base_url = self.md.Meta['wiki_base_url'][0]
|
||||
if 'wiki_end_url' in self.md.Meta:
|
||||
end_url = self.md.Meta['wiki_end_url'][0]
|
||||
if 'wiki_html_class' in self.md.Meta:
|
||||
html_class = self.md.Meta['wiki_html_class'][0]
|
||||
return base_url, end_url, html_class
|
||||
|
||||
|
||||
def makeExtension(**kwargs): # pragma: no cover
|
||||
return WikiLinkExtension(**kwargs)
|
@ -1,334 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
This module imports a copy of [`html.parser.HTMLParser`][] and modifies it heavily through monkey-patches.
|
||||
A copy is imported rather than the module being directly imported as this ensures that the user can import
|
||||
and use the unmodified library for their own needs.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import importlib.util
|
||||
import sys
|
||||
|
||||
|
||||
# Import a copy of the html.parser lib as `htmlparser` so we can monkeypatch it.
|
||||
# Users can still do `from html import parser` and get the default behavior.
|
||||
spec = importlib.util.find_spec('html.parser')
|
||||
htmlparser = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(htmlparser)
|
||||
sys.modules['htmlparser'] = htmlparser
|
||||
|
||||
# Monkeypatch `HTMLParser` to only accept `?>` to close Processing Instructions.
|
||||
htmlparser.piclose = re.compile(r'\?>')
|
||||
# Monkeypatch `HTMLParser` to only recognize entity references with a closing semicolon.
|
||||
htmlparser.entityref = re.compile(r'&([a-zA-Z][-.a-zA-Z0-9]*);')
|
||||
# Monkeypatch `HTMLParser` to no longer support partial entities. We are always feeding a complete block,
|
||||
# so the 'incomplete' functionality is unnecessary. As the `entityref` regex is run right before incomplete,
|
||||
# and the two regex are the same, then incomplete will simply never match and we avoid the logic within.
|
||||
htmlparser.incomplete = htmlparser.entityref
|
||||
# Monkeypatch `HTMLParser` to not accept a backtick in a tag name, attribute name, or bare value.
|
||||
htmlparser.locatestarttagend_tolerant = re.compile(r"""
|
||||
<[a-zA-Z][^`\t\n\r\f />\x00]* # tag name <= added backtick here
|
||||
(?:[\s/]* # optional whitespace before attribute name
|
||||
(?:(?<=['"\s/])[^`\s/>][^\s/=>]* # attribute name <= added backtick here
|
||||
(?:\s*=+\s* # value indicator
|
||||
(?:'[^']*' # LITA-enclosed value
|
||||
|"[^"]*" # LIT-enclosed value
|
||||
|(?!['"])[^`>\s]* # bare value <= added backtick here
|
||||
)
|
||||
(?:\s*,)* # possibly followed by a comma
|
||||
)?(?:\s|/(?!>))*
|
||||
)*
|
||||
)?
|
||||
\s* # trailing whitespace
|
||||
""", re.VERBOSE)
|
||||
|
||||
# Match a blank line at the start of a block of text (two newlines).
|
||||
# The newlines may be preceded by additional whitespace.
|
||||
blank_line_re = re.compile(r'^([ ]*\n){2}')
|
||||
|
||||
|
||||
class HTMLExtractor(htmlparser.HTMLParser):
|
||||
"""
|
||||
Extract raw HTML from text.
|
||||
|
||||
The raw HTML is stored in the [`htmlStash`][markdown.util.HtmlStash] of the
|
||||
[`Markdown`][markdown.Markdown] instance passed to `md` and the remaining text
|
||||
is stored in `cleandoc` as a list of strings.
|
||||
"""
|
||||
|
||||
def __init__(self, md, *args, **kwargs):
|
||||
if 'convert_charrefs' not in kwargs:
|
||||
kwargs['convert_charrefs'] = False
|
||||
|
||||
# Block tags that should contain no content (self closing)
|
||||
self.empty_tags = set(['hr'])
|
||||
|
||||
self.lineno_start_cache = [0]
|
||||
|
||||
# This calls self.reset
|
||||
super().__init__(*args, **kwargs)
|
||||
self.md = md
|
||||
|
||||
def reset(self):
|
||||
"""Reset this instance. Loses all unprocessed data."""
|
||||
self.inraw = False
|
||||
self.intail = False
|
||||
self.stack = [] # When `inraw==True`, stack contains a list of tags
|
||||
self._cache = []
|
||||
self.cleandoc = []
|
||||
self.lineno_start_cache = [0]
|
||||
|
||||
super().reset()
|
||||
|
||||
def close(self):
|
||||
"""Handle any buffered data."""
|
||||
super().close()
|
||||
if len(self.rawdata):
|
||||
# Temp fix for https://bugs.python.org/issue41989
|
||||
# TODO: remove this when the bug is fixed in all supported Python versions.
|
||||
if self.convert_charrefs and not self.cdata_elem: # pragma: no cover
|
||||
self.handle_data(htmlparser.unescape(self.rawdata))
|
||||
else:
|
||||
self.handle_data(self.rawdata)
|
||||
# Handle any unclosed tags.
|
||||
if len(self._cache):
|
||||
self.cleandoc.append(self.md.htmlStash.store(''.join(self._cache)))
|
||||
self._cache = []
|
||||
|
||||
@property
|
||||
def line_offset(self) -> int:
|
||||
"""Returns char index in `self.rawdata` for the start of the current line. """
|
||||
for ii in range(len(self.lineno_start_cache)-1, self.lineno-1):
|
||||
last_line_start_pos = self.lineno_start_cache[ii]
|
||||
lf_pos = self.rawdata.find('\n', last_line_start_pos)
|
||||
if lf_pos == -1:
|
||||
# No more newlines found. Use end of raw data as start of line beyond end.
|
||||
lf_pos = len(self.rawdata)
|
||||
self.lineno_start_cache.append(lf_pos+1)
|
||||
|
||||
return self.lineno_start_cache[self.lineno-1]
|
||||
|
||||
def at_line_start(self) -> bool:
|
||||
"""
|
||||
Returns True if current position is at start of line.
|
||||
|
||||
Allows for up to three blank spaces at start of line.
|
||||
"""
|
||||
if self.offset == 0:
|
||||
return True
|
||||
if self.offset > 3:
|
||||
return False
|
||||
# Confirm up to first 3 chars are whitespace
|
||||
return self.rawdata[self.line_offset:self.line_offset + self.offset].strip() == ''
|
||||
|
||||
def get_endtag_text(self, tag: str) -> str:
|
||||
"""
|
||||
Returns the text of the end tag.
|
||||
|
||||
If it fails to extract the actual text from the raw data, it builds a closing tag with `tag`.
|
||||
"""
|
||||
# Attempt to extract actual tag from raw source text
|
||||
start = self.line_offset + self.offset
|
||||
m = htmlparser.endendtag.search(self.rawdata, start)
|
||||
if m:
|
||||
return self.rawdata[start:m.end()]
|
||||
else: # pragma: no cover
|
||||
# Failed to extract from raw data. Assume well formed and lowercase.
|
||||
return '</{}>'.format(tag)
|
||||
|
||||
def handle_starttag(self, tag: str, attrs: list[tuple[str, str]]):
|
||||
# Handle tags that should always be empty and do not specify a closing tag
|
||||
if tag in self.empty_tags:
|
||||
self.handle_startendtag(tag, attrs)
|
||||
return
|
||||
|
||||
if self.md.is_block_level(tag) and (self.intail or (self.at_line_start() and not self.inraw)):
|
||||
# Started a new raw block. Prepare stack.
|
||||
self.inraw = True
|
||||
self.cleandoc.append('\n')
|
||||
|
||||
text = self.get_starttag_text()
|
||||
if self.inraw:
|
||||
self.stack.append(tag)
|
||||
self._cache.append(text)
|
||||
else:
|
||||
self.cleandoc.append(text)
|
||||
if tag in self.CDATA_CONTENT_ELEMENTS:
|
||||
# This is presumably a standalone tag in a code span (see #1036).
|
||||
self.clear_cdata_mode()
|
||||
|
||||
def handle_endtag(self, tag: str):
|
||||
text = self.get_endtag_text(tag)
|
||||
|
||||
if self.inraw:
|
||||
self._cache.append(text)
|
||||
if tag in self.stack:
|
||||
# Remove tag from stack
|
||||
while self.stack:
|
||||
if self.stack.pop() == tag:
|
||||
break
|
||||
if len(self.stack) == 0:
|
||||
# End of raw block.
|
||||
if blank_line_re.match(self.rawdata[self.line_offset + self.offset + len(text):]):
|
||||
# Preserve blank line and end of raw block.
|
||||
self._cache.append('\n')
|
||||
else:
|
||||
# More content exists after `endtag`.
|
||||
self.intail = True
|
||||
# Reset stack.
|
||||
self.inraw = False
|
||||
self.cleandoc.append(self.md.htmlStash.store(''.join(self._cache)))
|
||||
# Insert blank line between this and next line.
|
||||
self.cleandoc.append('\n\n')
|
||||
self._cache = []
|
||||
else:
|
||||
self.cleandoc.append(text)
|
||||
|
||||
def handle_data(self, data: str):
|
||||
if self.intail and '\n' in data:
|
||||
self.intail = False
|
||||
if self.inraw:
|
||||
self._cache.append(data)
|
||||
else:
|
||||
self.cleandoc.append(data)
|
||||
|
||||
def handle_empty_tag(self, data: str, is_block: bool):
|
||||
""" Handle empty tags (`<data>`). """
|
||||
if self.inraw or self.intail:
|
||||
# Append this to the existing raw block
|
||||
self._cache.append(data)
|
||||
elif self.at_line_start() and is_block:
|
||||
# Handle this as a standalone raw block
|
||||
if blank_line_re.match(self.rawdata[self.line_offset + self.offset + len(data):]):
|
||||
# Preserve blank line after tag in raw block.
|
||||
data += '\n'
|
||||
else:
|
||||
# More content exists after tag.
|
||||
self.intail = True
|
||||
item = self.cleandoc[-1] if self.cleandoc else ''
|
||||
# If we only have one newline before block element, add another
|
||||
if not item.endswith('\n\n') and item.endswith('\n'):
|
||||
self.cleandoc.append('\n')
|
||||
self.cleandoc.append(self.md.htmlStash.store(data))
|
||||
# Insert blank line between this and next line.
|
||||
self.cleandoc.append('\n\n')
|
||||
else:
|
||||
self.cleandoc.append(data)
|
||||
|
||||
def handle_startendtag(self, tag: str, attrs: list[tuple[str, str]]):
|
||||
self.handle_empty_tag(self.get_starttag_text(), is_block=self.md.is_block_level(tag))
|
||||
|
||||
def handle_charref(self, name: str):
|
||||
self.handle_empty_tag('&#{};'.format(name), is_block=False)
|
||||
|
||||
def handle_entityref(self, name: str):
|
||||
self.handle_empty_tag('&{};'.format(name), is_block=False)
|
||||
|
||||
def handle_comment(self, data: str):
|
||||
self.handle_empty_tag('<!--{}-->'.format(data), is_block=True)
|
||||
|
||||
def handle_decl(self, data: str):
|
||||
self.handle_empty_tag('<!{}>'.format(data), is_block=True)
|
||||
|
||||
def handle_pi(self, data: str):
|
||||
self.handle_empty_tag('<?{}?>'.format(data), is_block=True)
|
||||
|
||||
def unknown_decl(self, data: str):
|
||||
end = ']]>' if data.startswith('CDATA[') else ']>'
|
||||
self.handle_empty_tag('<![{}{}'.format(data, end), is_block=True)
|
||||
|
||||
def parse_pi(self, i: int) -> int:
|
||||
if self.at_line_start() or self.intail:
|
||||
return super().parse_pi(i)
|
||||
# This is not the beginning of a raw block so treat as plain data
|
||||
# and avoid consuming any tags which may follow (see #1066).
|
||||
self.handle_data('<?')
|
||||
return i + 2
|
||||
|
||||
def parse_html_declaration(self, i: int) -> int:
|
||||
if self.at_line_start() or self.intail:
|
||||
return super().parse_html_declaration(i)
|
||||
# This is not the beginning of a raw block so treat as plain data
|
||||
# and avoid consuming any tags which may follow (see #1066).
|
||||
self.handle_data('<!')
|
||||
return i + 2
|
||||
|
||||
# The rest has been copied from base class in standard lib to address #1036.
|
||||
# As `__startag_text` is private, all references to it must be in this subclass.
|
||||
# The last few lines of `parse_starttag` are reversed so that `handle_starttag`
|
||||
# can override `cdata_mode` in certain situations (in a code span).
|
||||
__starttag_text: str | None = None
|
||||
|
||||
def get_starttag_text(self) -> str:
|
||||
"""Return full source of start tag: `<...>`."""
|
||||
return self.__starttag_text
|
||||
|
||||
def parse_starttag(self, i: int) -> int: # pragma: no cover
|
||||
self.__starttag_text = None
|
||||
endpos = self.check_for_whole_start_tag(i)
|
||||
if endpos < 0:
|
||||
return endpos
|
||||
rawdata = self.rawdata
|
||||
self.__starttag_text = rawdata[i:endpos]
|
||||
|
||||
# Now parse the data between `i+1` and `j` into a tag and `attrs`
|
||||
attrs = []
|
||||
match = htmlparser.tagfind_tolerant.match(rawdata, i+1)
|
||||
assert match, 'unexpected call to parse_starttag()'
|
||||
k = match.end()
|
||||
self.lasttag = tag = match.group(1).lower()
|
||||
while k < endpos:
|
||||
m = htmlparser.attrfind_tolerant.match(rawdata, k)
|
||||
if not m:
|
||||
break
|
||||
attrname, rest, attrvalue = m.group(1, 2, 3)
|
||||
if not rest:
|
||||
attrvalue = None
|
||||
elif attrvalue[:1] == '\'' == attrvalue[-1:] or \
|
||||
attrvalue[:1] == '"' == attrvalue[-1:]: # noqa: E127
|
||||
attrvalue = attrvalue[1:-1]
|
||||
if attrvalue:
|
||||
attrvalue = htmlparser.unescape(attrvalue)
|
||||
attrs.append((attrname.lower(), attrvalue))
|
||||
k = m.end()
|
||||
|
||||
end = rawdata[k:endpos].strip()
|
||||
if end not in (">", "/>"):
|
||||
lineno, offset = self.getpos()
|
||||
if "\n" in self.__starttag_text:
|
||||
lineno = lineno + self.__starttag_text.count("\n")
|
||||
offset = len(self.__starttag_text) \
|
||||
- self.__starttag_text.rfind("\n") # noqa: E127
|
||||
else:
|
||||
offset = offset + len(self.__starttag_text)
|
||||
self.handle_data(rawdata[i:endpos])
|
||||
return endpos
|
||||
if end.endswith('/>'):
|
||||
# XHTML-style empty tag: `<span attr="value" />`
|
||||
self.handle_startendtag(tag, attrs)
|
||||
else:
|
||||
# *** set `cdata_mode` first so we can override it in `handle_starttag` (see #1036) ***
|
||||
if tag in self.CDATA_CONTENT_ELEMENTS:
|
||||
self.set_cdata_mode(tag)
|
||||
self.handle_starttag(tag, attrs)
|
||||
return endpos
|
@ -1,992 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
In version 3.0, a new, more flexible inline processor was added, [`markdown.inlinepatterns.InlineProcessor`][]. The
|
||||
original inline patterns, which inherit from [`markdown.inlinepatterns.Pattern`][] or one of its children are still
|
||||
supported, though users are encouraged to migrate.
|
||||
|
||||
The new `InlineProcessor` provides two major enhancements to `Patterns`:
|
||||
|
||||
1. Inline Processors no longer need to match the entire block, so regular expressions no longer need to start with
|
||||
`r'^(.*?)'` and end with `r'(.*?)%'`. This runs faster. The returned [`Match`][re.Match] object will only contain
|
||||
what is explicitly matched in the pattern, and extension pattern groups now start with `m.group(1)`.
|
||||
|
||||
2. The `handleMatch` method now takes an additional input called `data`, which is the entire block under analysis,
|
||||
not just what is matched with the specified pattern. The method now returns the element *and* the indexes relative
|
||||
to `data` that the return element is replacing (usually `m.start(0)` and `m.end(0)`). If the boundaries are
|
||||
returned as `None`, it is assumed that the match did not take place, and nothing will be altered in `data`.
|
||||
|
||||
This allows handling of more complex constructs than regular expressions can handle, e.g., matching nested
|
||||
brackets, and explicit control of the span "consumed" by the processor.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from . import util
|
||||
from typing import TYPE_CHECKING, Any, Collection, NamedTuple
|
||||
import re
|
||||
import xml.etree.ElementTree as etree
|
||||
try: # pragma: no cover
|
||||
from html import entities
|
||||
except ImportError: # pragma: no cover
|
||||
import htmlentitydefs as entities
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
|
||||
|
||||
def build_inlinepatterns(md: Markdown, **kwargs: Any) -> util.Registry[InlineProcessor]:
|
||||
"""
|
||||
Build the default set of inline patterns for Markdown.
|
||||
|
||||
The order in which processors and/or patterns are applied is very important - e.g. if we first replace
|
||||
`http://.../` links with `<a>` tags and _then_ try to replace inline HTML, we would end up with a mess. So, we
|
||||
apply the expressions in the following order:
|
||||
|
||||
* backticks and escaped characters have to be handled before everything else so that we can preempt any markdown
|
||||
patterns by escaping them;
|
||||
|
||||
* then we handle the various types of links (auto-links must be handled before inline HTML);
|
||||
|
||||
* then we handle inline HTML. At this point we will simply replace all inline HTML strings with a placeholder
|
||||
and add the actual HTML to a stash;
|
||||
|
||||
* finally we apply strong, emphasis, etc.
|
||||
|
||||
"""
|
||||
inlinePatterns = util.Registry()
|
||||
inlinePatterns.register(BacktickInlineProcessor(BACKTICK_RE), 'backtick', 190)
|
||||
inlinePatterns.register(EscapeInlineProcessor(ESCAPE_RE, md), 'escape', 180)
|
||||
inlinePatterns.register(ReferenceInlineProcessor(REFERENCE_RE, md), 'reference', 170)
|
||||
inlinePatterns.register(LinkInlineProcessor(LINK_RE, md), 'link', 160)
|
||||
inlinePatterns.register(ImageInlineProcessor(IMAGE_LINK_RE, md), 'image_link', 150)
|
||||
inlinePatterns.register(
|
||||
ImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'image_reference', 140
|
||||
)
|
||||
inlinePatterns.register(
|
||||
ShortReferenceInlineProcessor(REFERENCE_RE, md), 'short_reference', 130
|
||||
)
|
||||
inlinePatterns.register(
|
||||
ShortImageReferenceInlineProcessor(IMAGE_REFERENCE_RE, md), 'short_image_ref', 125
|
||||
)
|
||||
inlinePatterns.register(AutolinkInlineProcessor(AUTOLINK_RE, md), 'autolink', 120)
|
||||
inlinePatterns.register(AutomailInlineProcessor(AUTOMAIL_RE, md), 'automail', 110)
|
||||
inlinePatterns.register(SubstituteTagInlineProcessor(LINE_BREAK_RE, 'br'), 'linebreak', 100)
|
||||
inlinePatterns.register(HtmlInlineProcessor(HTML_RE, md), 'html', 90)
|
||||
inlinePatterns.register(HtmlInlineProcessor(ENTITY_RE, md), 'entity', 80)
|
||||
inlinePatterns.register(SimpleTextInlineProcessor(NOT_STRONG_RE), 'not_strong', 70)
|
||||
inlinePatterns.register(AsteriskProcessor(r'\*'), 'em_strong', 60)
|
||||
inlinePatterns.register(UnderscoreProcessor(r'_'), 'em_strong2', 50)
|
||||
return inlinePatterns
|
||||
|
||||
|
||||
# The actual regular expressions for patterns
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
NOIMG = r'(?<!\!)'
|
||||
""" Match not an image. Partial regular expression which matches if not preceded by `!`. """
|
||||
|
||||
BACKTICK_RE = r'(?:(?<!\\)((?:\\{2})+)(?=`+)|(?<!\\)(`+)(.+?)(?<!`)\2(?!`))'
|
||||
""" Match backtick quoted string (`` `e=f()` `` or ``` ``e=f("`")`` ```). """
|
||||
|
||||
ESCAPE_RE = r'\\(.)'
|
||||
""" Match a backslash escaped character (`\\<` or `\\*`). """
|
||||
|
||||
EMPHASIS_RE = r'(\*)([^\*]+)\1'
|
||||
""" Match emphasis with an asterisk (`*emphasis*`). """
|
||||
|
||||
STRONG_RE = r'(\*{2})(.+?)\1'
|
||||
""" Match strong with an asterisk (`**strong**`). """
|
||||
|
||||
SMART_STRONG_RE = r'(?<!\w)(_{2})(?!_)(.+?)(?<!_)\1(?!\w)'
|
||||
""" Match strong with underscore while ignoring middle word underscores (`__smart__strong__`). """
|
||||
|
||||
SMART_EMPHASIS_RE = r'(?<!\w)(_)(?!_)(.+?)(?<!_)\1(?!\w)'
|
||||
""" Match emphasis with underscore while ignoring middle word underscores (`_smart_emphasis_`). """
|
||||
|
||||
SMART_STRONG_EM_RE = r'(?<!\w)(\_)\1(?!\1)(.+?)(?<!\w)\1(?!\1)(.+?)\1{3}(?!\w)'
|
||||
""" Match strong emphasis with underscores (`__strong _em__`). """
|
||||
|
||||
EM_STRONG_RE = r'(\*)\1{2}(.+?)\1(.*?)\1{2}'
|
||||
""" Match emphasis strong with asterisk (`***strongem***` or `***em*strong**`). """
|
||||
|
||||
EM_STRONG2_RE = r'(_)\1{2}(.+?)\1(.*?)\1{2}'
|
||||
""" Match emphasis strong with underscores (`___emstrong___` or `___em_strong__`). """
|
||||
|
||||
STRONG_EM_RE = r'(\*)\1{2}(.+?)\1{2}(.*?)\1'
|
||||
""" Match strong emphasis with asterisk (`***strong**em*`). """
|
||||
|
||||
STRONG_EM2_RE = r'(_)\1{2}(.+?)\1{2}(.*?)\1'
|
||||
""" Match strong emphasis with underscores (`___strong__em_`). """
|
||||
|
||||
STRONG_EM3_RE = r'(\*)\1(?!\1)([^*]+?)\1(?!\1)(.+?)\1{3}'
|
||||
""" Match strong emphasis with asterisk (`**strong*em***`). """
|
||||
|
||||
LINK_RE = NOIMG + r'\['
|
||||
""" Match start of in-line link (`[text](url)` or `[text](<url>)` or `[text](url "title")`). """
|
||||
|
||||
IMAGE_LINK_RE = r'\!\['
|
||||
""" Match start of in-line image link (`![alttxt](url)` or `![alttxt](<url>)`). """
|
||||
|
||||
REFERENCE_RE = LINK_RE
|
||||
""" Match start of reference link (`[Label][3]`). """
|
||||
|
||||
IMAGE_REFERENCE_RE = IMAGE_LINK_RE
|
||||
""" Match start of image reference (`![alt text][2]`). """
|
||||
|
||||
NOT_STRONG_RE = r'((^|(?<=\s))(\*{1,3}|_{1,3})(?=\s|$))'
|
||||
""" Match a stand-alone `*` or `_`. """
|
||||
|
||||
AUTOLINK_RE = r'<((?:[Ff]|[Hh][Tt])[Tt][Pp][Ss]?://[^<>]*)>'
|
||||
""" Match an automatic link (`<http://www.example.com>`). """
|
||||
|
||||
AUTOMAIL_RE = r'<([^<> !]+@[^@<> ]+)>'
|
||||
""" Match an automatic email link (`<me@example.com>`). """
|
||||
|
||||
HTML_RE = r'(<(\/?[a-zA-Z][^<>@ ]*( [^<>]*)?|!--(?:(?!<!--|-->).)*--)>)'
|
||||
""" Match an HTML tag (`<...>`). """
|
||||
|
||||
ENTITY_RE = r'(&(?:\#[0-9]+|\#x[0-9a-fA-F]+|[a-zA-Z0-9]+);)'
|
||||
""" Match an HTML entity (`&` (decimal) or `&` (hex) or `&` (named)). """
|
||||
|
||||
LINE_BREAK_RE = r' \n'
|
||||
""" Match two spaces at end of line. """
|
||||
|
||||
|
||||
def dequote(string: str) -> str:
|
||||
"""Remove quotes from around a string."""
|
||||
if ((string.startswith('"') and string.endswith('"')) or
|
||||
(string.startswith("'") and string.endswith("'"))):
|
||||
return string[1:-1]
|
||||
else:
|
||||
return string
|
||||
|
||||
|
||||
class EmStrongItem(NamedTuple):
|
||||
"""Emphasis/strong pattern item."""
|
||||
pattern: re.Pattern[str]
|
||||
builder: str
|
||||
tags: str
|
||||
|
||||
|
||||
# The pattern classes
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
|
||||
class Pattern: # pragma: no cover
|
||||
"""
|
||||
Base class that inline patterns subclass.
|
||||
|
||||
Inline patterns are handled by means of `Pattern` subclasses, one per regular expression.
|
||||
Each pattern object uses a single regular expression and must support the following methods:
|
||||
[`getCompiledRegExp`][markdown.inlinepatterns.Pattern.getCompiledRegExp] and
|
||||
[`handleMatch`][markdown.inlinepatterns.Pattern.handleMatch].
|
||||
|
||||
All the regular expressions used by `Pattern` subclasses must capture the whole block. For this
|
||||
reason, they all start with `^(.*)` and end with `(.*)!`. When passing a regular expression on
|
||||
class initialization, the `^(.*)` and `(.*)!` are added automatically and the regular expression
|
||||
is pre-compiled.
|
||||
|
||||
It is strongly suggested that the newer style [`markdown.inlinepatterns.InlineProcessor`][] that
|
||||
use a more efficient and flexible search approach be used instead. However, the older style
|
||||
`Pattern` remains for backward compatibility with many existing third-party extensions.
|
||||
|
||||
"""
|
||||
|
||||
ANCESTOR_EXCLUDES: Collection[str] = tuple()
|
||||
"""
|
||||
A collection of elements which are undesirable ancestors. The processor will be skipped if it
|
||||
would cause the content to be a descendant of one of the listed tag names.
|
||||
"""
|
||||
|
||||
def __init__(self, pattern: str, md: Markdown | None = None):
|
||||
"""
|
||||
Create an instant of an inline pattern.
|
||||
|
||||
Arguments:
|
||||
pattern: A regular expression that matches a pattern.
|
||||
md: An optional pointer to the instance of `markdown.Markdown` and is available as
|
||||
`self.md` on the class instance.
|
||||
|
||||
|
||||
"""
|
||||
self.pattern = pattern
|
||||
self.compiled_re = re.compile(r"^(.*?)%s(.*)$" % pattern,
|
||||
re.DOTALL | re.UNICODE)
|
||||
|
||||
self.md = md
|
||||
|
||||
def getCompiledRegExp(self) -> re.Pattern:
|
||||
""" Return a compiled regular expression. """
|
||||
return self.compiled_re
|
||||
|
||||
def handleMatch(self, m: re.Match[str]) -> etree.Element | str:
|
||||
"""Return a ElementTree element from the given match.
|
||||
|
||||
Subclasses should override this method.
|
||||
|
||||
Arguments:
|
||||
m: A match object containing a match of the pattern.
|
||||
|
||||
Returns: An ElementTree Element object.
|
||||
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
def type(self) -> str:
|
||||
""" Return class name, to define pattern type """
|
||||
return self.__class__.__name__
|
||||
|
||||
def unescape(self, text: str) -> str:
|
||||
""" Return unescaped text given text with an inline placeholder. """
|
||||
try:
|
||||
stash = self.md.treeprocessors['inline'].stashed_nodes
|
||||
except KeyError: # pragma: no cover
|
||||
return text
|
||||
|
||||
def get_stash(m):
|
||||
id = m.group(1)
|
||||
if id in stash:
|
||||
value = stash.get(id)
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
else:
|
||||
# An `etree` Element - return text content only
|
||||
return ''.join(value.itertext())
|
||||
return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text)
|
||||
|
||||
|
||||
class InlineProcessor(Pattern):
|
||||
"""
|
||||
Base class that inline processors subclass.
|
||||
|
||||
This is the newer style inline processor that uses a more
|
||||
efficient and flexible search approach.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, pattern: str, md: Markdown | None = None):
|
||||
"""
|
||||
Create an instant of an inline processor.
|
||||
|
||||
Arguments:
|
||||
pattern: A regular expression that matches a pattern.
|
||||
md: An optional pointer to the instance of `markdown.Markdown` and is available as
|
||||
`self.md` on the class instance.
|
||||
|
||||
"""
|
||||
self.pattern = pattern
|
||||
self.compiled_re = re.compile(pattern, re.DOTALL | re.UNICODE)
|
||||
|
||||
# API for Markdown to pass `safe_mode` into instance
|
||||
self.safe_mode = False
|
||||
self.md = md
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | str | None, int | None, int | None]:
|
||||
"""Return a ElementTree element from the given match and the
|
||||
start and end index of the matched text.
|
||||
|
||||
If `start` and/or `end` are returned as `None`, it will be
|
||||
assumed that the processor did not find a valid region of text.
|
||||
|
||||
Subclasses should override this method.
|
||||
|
||||
Arguments:
|
||||
m: A re match object containing a match of the pattern.
|
||||
data: The buffer currently under analysis.
|
||||
|
||||
Returns:
|
||||
el: The ElementTree element, text or None.
|
||||
start: The start of the region that has been matched or None.
|
||||
end: The end of the region that has been matched or None.
|
||||
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class SimpleTextPattern(Pattern): # pragma: no cover
|
||||
""" Return a simple text of `group(2)` of a Pattern. """
|
||||
def handleMatch(self, m: re.Match[str]) -> str:
|
||||
""" Return string content of `group(2)` of a matching pattern. """
|
||||
return m.group(2)
|
||||
|
||||
|
||||
class SimpleTextInlineProcessor(InlineProcessor):
|
||||
""" Return a simple text of `group(1)` of a Pattern. """
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[str, int, int]:
|
||||
""" Return string content of `group(1)` of a matching pattern. """
|
||||
return m.group(1), m.start(0), m.end(0)
|
||||
|
||||
|
||||
class EscapeInlineProcessor(InlineProcessor):
|
||||
""" Return an escaped character. """
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[str | None, int, int]:
|
||||
"""
|
||||
If the character matched by `group(1)` of a pattern is in [`ESCAPED_CHARS`][markdown.Markdown.ESCAPED_CHARS]
|
||||
then return the integer representing the character's Unicode code point (as returned by [`ord`][]) wrapped
|
||||
in [`util.STX`][markdown.util.STX] and [`util.ETX`][markdown.util.ETX].
|
||||
|
||||
If the matched character is not in [`ESCAPED_CHARS`][markdown.Markdown.ESCAPED_CHARS], then return `None`.
|
||||
"""
|
||||
|
||||
char = m.group(1)
|
||||
if char in self.md.ESCAPED_CHARS:
|
||||
return '{}{}{}'.format(util.STX, ord(char), util.ETX), m.start(0), m.end(0)
|
||||
else:
|
||||
return None, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class SimpleTagPattern(Pattern): # pragma: no cover
|
||||
"""
|
||||
Return element of type `tag` with a text attribute of `group(3)`
|
||||
of a Pattern.
|
||||
|
||||
"""
|
||||
def __init__(self, pattern: str, tag: str):
|
||||
"""
|
||||
Create an instant of an simple tag pattern.
|
||||
|
||||
Arguments:
|
||||
pattern: A regular expression that matches a pattern.
|
||||
tag: Tag of element.
|
||||
|
||||
"""
|
||||
Pattern.__init__(self, pattern)
|
||||
self.tag = tag
|
||||
""" The tag of the rendered element. """
|
||||
|
||||
def handleMatch(self, m: re.Match[str]) -> etree.Element:
|
||||
"""
|
||||
Return [`Element`][xml.etree.ElementTree.Element] of type `tag` with the string in `group(3)` of a
|
||||
matching pattern as the Element's text.
|
||||
"""
|
||||
el = etree.Element(self.tag)
|
||||
el.text = m.group(3)
|
||||
return el
|
||||
|
||||
|
||||
class SimpleTagInlineProcessor(InlineProcessor):
|
||||
"""
|
||||
Return element of type `tag` with a text attribute of `group(2)`
|
||||
of a Pattern.
|
||||
|
||||
"""
|
||||
def __init__(self, pattern: str, tag: str):
|
||||
"""
|
||||
Create an instant of an simple tag processor.
|
||||
|
||||
Arguments:
|
||||
pattern: A regular expression that matches a pattern.
|
||||
tag: Tag of element.
|
||||
|
||||
"""
|
||||
InlineProcessor.__init__(self, pattern)
|
||||
self.tag = tag
|
||||
""" The tag of the rendered element. """
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]: # pragma: no cover
|
||||
"""
|
||||
Return [`Element`][xml.etree.ElementTree.Element] of type `tag` with the string in `group(2)` of a
|
||||
matching pattern as the Element's text.
|
||||
"""
|
||||
el = etree.Element(self.tag)
|
||||
el.text = m.group(2)
|
||||
return el, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class SubstituteTagPattern(SimpleTagPattern): # pragma: no cover
|
||||
""" Return an element of type `tag` with no children. """
|
||||
def handleMatch(self, m: re.Match[str]) -> etree.Element:
|
||||
""" Return empty [`Element`][xml.etree.ElementTree.Element] of type `tag`. """
|
||||
return etree.Element(self.tag)
|
||||
|
||||
|
||||
class SubstituteTagInlineProcessor(SimpleTagInlineProcessor):
|
||||
""" Return an element of type `tag` with no children. """
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
|
||||
""" Return empty [`Element`][xml.etree.ElementTree.Element] of type `tag`. """
|
||||
return etree.Element(self.tag), m.start(0), m.end(0)
|
||||
|
||||
|
||||
class BacktickInlineProcessor(InlineProcessor):
|
||||
""" Return a `<code>` element containing the escaped matching text. """
|
||||
def __init__(self, pattern):
|
||||
InlineProcessor.__init__(self, pattern)
|
||||
self.ESCAPED_BSLASH = '{}{}{}'.format(util.STX, ord('\\'), util.ETX)
|
||||
self.tag = 'code'
|
||||
""" The tag of the rendered element. """
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | str, int, int]:
|
||||
"""
|
||||
If the match contains `group(3)` of a pattern, then return a `code`
|
||||
[`Element`][xml.etree.ElementTree.Element] which contains HTML escaped text (with
|
||||
[`code_escape`][markdown.util.code_escape]) as an [`AtomicString`][markdown.util.AtomicString].
|
||||
|
||||
If the match does not contain `group(3)` then return the text of `group(1)` backslash escaped.
|
||||
|
||||
"""
|
||||
if m.group(3):
|
||||
el = etree.Element(self.tag)
|
||||
el.text = util.AtomicString(util.code_escape(m.group(3).strip()))
|
||||
return el, m.start(0), m.end(0)
|
||||
else:
|
||||
return m.group(1).replace('\\\\', self.ESCAPED_BSLASH), m.start(0), m.end(0)
|
||||
|
||||
|
||||
class DoubleTagPattern(SimpleTagPattern): # pragma: no cover
|
||||
"""Return a ElementTree element nested in tag2 nested in tag1.
|
||||
|
||||
Useful for strong emphasis etc.
|
||||
|
||||
"""
|
||||
def handleMatch(self, m: re.Match[str]) -> etree.Element:
|
||||
"""
|
||||
Return [`Element`][xml.etree.ElementTree.Element] in following format:
|
||||
`<tag1><tag2>group(3)</tag2>group(4)</tag2>` where `group(4)` is optional.
|
||||
|
||||
"""
|
||||
tag1, tag2 = self.tag.split(",")
|
||||
el1 = etree.Element(tag1)
|
||||
el2 = etree.SubElement(el1, tag2)
|
||||
el2.text = m.group(3)
|
||||
if len(m.groups()) == 5:
|
||||
el2.tail = m.group(4)
|
||||
return el1
|
||||
|
||||
|
||||
class DoubleTagInlineProcessor(SimpleTagInlineProcessor):
|
||||
"""Return a ElementTree element nested in tag2 nested in tag1.
|
||||
|
||||
Useful for strong emphasis etc.
|
||||
|
||||
"""
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]: # pragma: no cover
|
||||
"""
|
||||
Return [`Element`][xml.etree.ElementTree.Element] in following format:
|
||||
`<tag1><tag2>group(2)</tag2>group(3)</tag2>` where `group(3)` is optional.
|
||||
|
||||
"""
|
||||
tag1, tag2 = self.tag.split(",")
|
||||
el1 = etree.Element(tag1)
|
||||
el2 = etree.SubElement(el1, tag2)
|
||||
el2.text = m.group(2)
|
||||
if len(m.groups()) == 3:
|
||||
el2.tail = m.group(3)
|
||||
return el1, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class HtmlInlineProcessor(InlineProcessor):
|
||||
""" Store raw inline html and return a placeholder. """
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[str, int, int]:
|
||||
""" Store the text of `group(1)` of a pattern and return a placeholder string. """
|
||||
rawhtml = self.backslash_unescape(self.unescape(m.group(1)))
|
||||
place_holder = self.md.htmlStash.store(rawhtml)
|
||||
return place_holder, m.start(0), m.end(0)
|
||||
|
||||
def unescape(self, text):
|
||||
""" Return unescaped text given text with an inline placeholder. """
|
||||
try:
|
||||
stash = self.md.treeprocessors['inline'].stashed_nodes
|
||||
except KeyError: # pragma: no cover
|
||||
return text
|
||||
|
||||
def get_stash(m):
|
||||
id = m.group(1)
|
||||
value = stash.get(id)
|
||||
if value is not None:
|
||||
try:
|
||||
return self.md.serializer(value)
|
||||
except Exception:
|
||||
return r'\%s' % value
|
||||
|
||||
return util.INLINE_PLACEHOLDER_RE.sub(get_stash, text)
|
||||
|
||||
def backslash_unescape(self, text):
|
||||
""" Return text with backslash escapes undone (backslashes are restored). """
|
||||
try:
|
||||
RE = self.md.treeprocessors['unescape'].RE
|
||||
except KeyError: # pragma: no cover
|
||||
return text
|
||||
|
||||
def _unescape(m):
|
||||
return chr(int(m.group(1)))
|
||||
|
||||
return RE.sub(_unescape, text)
|
||||
|
||||
|
||||
class AsteriskProcessor(InlineProcessor):
|
||||
"""Emphasis processor for handling strong and em matches inside asterisks."""
|
||||
|
||||
PATTERNS = [
|
||||
EmStrongItem(re.compile(EM_STRONG_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'),
|
||||
EmStrongItem(re.compile(STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'),
|
||||
EmStrongItem(re.compile(STRONG_EM3_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'),
|
||||
EmStrongItem(re.compile(STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'),
|
||||
EmStrongItem(re.compile(EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em')
|
||||
]
|
||||
""" The various strong and emphasis patterns handled by this processor. """
|
||||
|
||||
def build_single(self, m, tag, idx):
|
||||
"""Return single tag."""
|
||||
el1 = etree.Element(tag)
|
||||
text = m.group(2)
|
||||
self.parse_sub_patterns(text, el1, None, idx)
|
||||
return el1
|
||||
|
||||
def build_double(self, m, tags, idx):
|
||||
"""Return double tag."""
|
||||
|
||||
tag1, tag2 = tags.split(",")
|
||||
el1 = etree.Element(tag1)
|
||||
el2 = etree.Element(tag2)
|
||||
text = m.group(2)
|
||||
self.parse_sub_patterns(text, el2, None, idx)
|
||||
el1.append(el2)
|
||||
if len(m.groups()) == 3:
|
||||
text = m.group(3)
|
||||
self.parse_sub_patterns(text, el1, el2, idx)
|
||||
return el1
|
||||
|
||||
def build_double2(self, m, tags, idx):
|
||||
"""Return double tags (variant 2): `<strong>text <em>text</em></strong>`."""
|
||||
|
||||
tag1, tag2 = tags.split(",")
|
||||
el1 = etree.Element(tag1)
|
||||
el2 = etree.Element(tag2)
|
||||
text = m.group(2)
|
||||
self.parse_sub_patterns(text, el1, None, idx)
|
||||
text = m.group(3)
|
||||
el1.append(el2)
|
||||
self.parse_sub_patterns(text, el2, None, idx)
|
||||
return el1
|
||||
|
||||
def parse_sub_patterns(self, data, parent, last, idx) -> None:
|
||||
"""
|
||||
Parses sub patterns.
|
||||
|
||||
`data` (`str`):
|
||||
text to evaluate.
|
||||
|
||||
`parent` (`etree.Element`):
|
||||
Parent to attach text and sub elements to.
|
||||
|
||||
`last` (`etree.Element`):
|
||||
Last appended child to parent. Can also be None if parent has no children.
|
||||
|
||||
`idx` (`int`):
|
||||
Current pattern index that was used to evaluate the parent.
|
||||
|
||||
"""
|
||||
|
||||
offset = 0
|
||||
pos = 0
|
||||
|
||||
length = len(data)
|
||||
while pos < length:
|
||||
# Find the start of potential emphasis or strong tokens
|
||||
if self.compiled_re.match(data, pos):
|
||||
matched = False
|
||||
# See if the we can match an emphasis/strong pattern
|
||||
for index, item in enumerate(self.PATTERNS):
|
||||
# Only evaluate patterns that are after what was used on the parent
|
||||
if index <= idx:
|
||||
continue
|
||||
m = item.pattern.match(data, pos)
|
||||
if m:
|
||||
# Append child nodes to parent
|
||||
# Text nodes should be appended to the last
|
||||
# child if present, and if not, it should
|
||||
# be added as the parent's text node.
|
||||
text = data[offset:m.start(0)]
|
||||
if text:
|
||||
if last is not None:
|
||||
last.tail = text
|
||||
else:
|
||||
parent.text = text
|
||||
el = self.build_element(m, item.builder, item.tags, index)
|
||||
parent.append(el)
|
||||
last = el
|
||||
# Move our position past the matched hunk
|
||||
offset = pos = m.end(0)
|
||||
matched = True
|
||||
if not matched:
|
||||
# We matched nothing, move on to the next character
|
||||
pos += 1
|
||||
else:
|
||||
# Increment position as no potential emphasis start was found.
|
||||
pos += 1
|
||||
|
||||
# Append any leftover text as a text node.
|
||||
text = data[offset:]
|
||||
if text:
|
||||
if last is not None:
|
||||
last.tail = text
|
||||
else:
|
||||
parent.text = text
|
||||
|
||||
def build_element(self, m, builder, tags, index):
|
||||
"""Element builder."""
|
||||
|
||||
if builder == 'double2':
|
||||
return self.build_double2(m, tags, index)
|
||||
elif builder == 'double':
|
||||
return self.build_double(m, tags, index)
|
||||
else:
|
||||
return self.build_single(m, tags, index)
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
|
||||
"""Parse patterns."""
|
||||
|
||||
el = None
|
||||
start = None
|
||||
end = None
|
||||
|
||||
for index, item in enumerate(self.PATTERNS):
|
||||
m1 = item.pattern.match(data, m.start(0))
|
||||
if m1:
|
||||
start = m1.start(0)
|
||||
end = m1.end(0)
|
||||
el = self.build_element(m1, item.builder, item.tags, index)
|
||||
break
|
||||
return el, start, end
|
||||
|
||||
|
||||
class UnderscoreProcessor(AsteriskProcessor):
|
||||
"""Emphasis processor for handling strong and em matches inside underscores."""
|
||||
|
||||
PATTERNS = [
|
||||
EmStrongItem(re.compile(EM_STRONG2_RE, re.DOTALL | re.UNICODE), 'double', 'strong,em'),
|
||||
EmStrongItem(re.compile(STRONG_EM2_RE, re.DOTALL | re.UNICODE), 'double', 'em,strong'),
|
||||
EmStrongItem(re.compile(SMART_STRONG_EM_RE, re.DOTALL | re.UNICODE), 'double2', 'strong,em'),
|
||||
EmStrongItem(re.compile(SMART_STRONG_RE, re.DOTALL | re.UNICODE), 'single', 'strong'),
|
||||
EmStrongItem(re.compile(SMART_EMPHASIS_RE, re.DOTALL | re.UNICODE), 'single', 'em')
|
||||
]
|
||||
""" The various strong and emphasis patterns handled by this processor. """
|
||||
|
||||
|
||||
class LinkInlineProcessor(InlineProcessor):
|
||||
""" Return a link element from the given match. """
|
||||
RE_LINK = re.compile(r'''\(\s*(?:(<[^<>]*>)\s*(?:('[^']*'|"[^"]*")\s*)?\))?''', re.DOTALL | re.UNICODE)
|
||||
RE_TITLE_CLEAN = re.compile(r'\s')
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
|
||||
""" Return an `a` [`Element`][xml.etree.ElementTree.Element] or `(None, None, None)`. """
|
||||
text, index, handled = self.getText(data, m.end(0))
|
||||
|
||||
if not handled:
|
||||
return None, None, None
|
||||
|
||||
href, title, index, handled = self.getLink(data, index)
|
||||
if not handled:
|
||||
return None, None, None
|
||||
|
||||
el = etree.Element("a")
|
||||
el.text = text
|
||||
|
||||
el.set("href", href)
|
||||
|
||||
if title is not None:
|
||||
el.set("title", title)
|
||||
|
||||
return el, m.start(0), index
|
||||
|
||||
def getLink(self, data, index):
|
||||
"""Parse data between `()` of `[Text]()` allowing recursive `()`. """
|
||||
|
||||
href = ''
|
||||
title = None
|
||||
handled = False
|
||||
|
||||
m = self.RE_LINK.match(data, pos=index)
|
||||
if m and m.group(1):
|
||||
# Matches [Text](<link> "title")
|
||||
href = m.group(1)[1:-1].strip()
|
||||
if m.group(2):
|
||||
title = m.group(2)[1:-1]
|
||||
index = m.end(0)
|
||||
handled = True
|
||||
elif m:
|
||||
# Track bracket nesting and index in string
|
||||
bracket_count = 1
|
||||
backtrack_count = 1
|
||||
start_index = m.end()
|
||||
index = start_index
|
||||
last_bracket = -1
|
||||
|
||||
# Primary (first found) quote tracking.
|
||||
quote = None
|
||||
start_quote = -1
|
||||
exit_quote = -1
|
||||
ignore_matches = False
|
||||
|
||||
# Secondary (second found) quote tracking.
|
||||
alt_quote = None
|
||||
start_alt_quote = -1
|
||||
exit_alt_quote = -1
|
||||
|
||||
# Track last character
|
||||
last = ''
|
||||
|
||||
for pos in range(index, len(data)):
|
||||
c = data[pos]
|
||||
if c == '(':
|
||||
# Count nested (
|
||||
# Don't increment the bracket count if we are sure we're in a title.
|
||||
if not ignore_matches:
|
||||
bracket_count += 1
|
||||
elif backtrack_count > 0:
|
||||
backtrack_count -= 1
|
||||
elif c == ')':
|
||||
# Match nested ) to (
|
||||
# Don't decrement if we are sure we are in a title that is unclosed.
|
||||
if ((exit_quote != -1 and quote == last) or (exit_alt_quote != -1 and alt_quote == last)):
|
||||
bracket_count = 0
|
||||
elif not ignore_matches:
|
||||
bracket_count -= 1
|
||||
elif backtrack_count > 0:
|
||||
backtrack_count -= 1
|
||||
# We've found our backup end location if the title doesn't resolve.
|
||||
if backtrack_count == 0:
|
||||
last_bracket = index + 1
|
||||
|
||||
elif c in ("'", '"'):
|
||||
# Quote has started
|
||||
if not quote:
|
||||
# We'll assume we are now in a title.
|
||||
# Brackets are quoted, so no need to match them (except for the final one).
|
||||
ignore_matches = True
|
||||
backtrack_count = bracket_count
|
||||
bracket_count = 1
|
||||
start_quote = index + 1
|
||||
quote = c
|
||||
# Secondary quote (in case the first doesn't resolve): [text](link'"title")
|
||||
elif c != quote and not alt_quote:
|
||||
start_alt_quote = index + 1
|
||||
alt_quote = c
|
||||
# Update primary quote match
|
||||
elif c == quote:
|
||||
exit_quote = index + 1
|
||||
# Update secondary quote match
|
||||
elif alt_quote and c == alt_quote:
|
||||
exit_alt_quote = index + 1
|
||||
|
||||
index += 1
|
||||
|
||||
# Link is closed, so let's break out of the loop
|
||||
if bracket_count == 0:
|
||||
# Get the title if we closed a title string right before link closed
|
||||
if exit_quote >= 0 and quote == last:
|
||||
href = data[start_index:start_quote - 1]
|
||||
title = ''.join(data[start_quote:exit_quote - 1])
|
||||
elif exit_alt_quote >= 0 and alt_quote == last:
|
||||
href = data[start_index:start_alt_quote - 1]
|
||||
title = ''.join(data[start_alt_quote:exit_alt_quote - 1])
|
||||
else:
|
||||
href = data[start_index:index - 1]
|
||||
break
|
||||
|
||||
if c != ' ':
|
||||
last = c
|
||||
|
||||
# We have a scenario: `[test](link"notitle)`
|
||||
# When we enter a string, we stop tracking bracket resolution in the main counter,
|
||||
# but we do keep a backup counter up until we discover where we might resolve all brackets
|
||||
# if the title string fails to resolve.
|
||||
if bracket_count != 0 and backtrack_count == 0:
|
||||
href = data[start_index:last_bracket - 1]
|
||||
index = last_bracket
|
||||
bracket_count = 0
|
||||
|
||||
handled = bracket_count == 0
|
||||
|
||||
if title is not None:
|
||||
title = self.RE_TITLE_CLEAN.sub(' ', dequote(self.unescape(title.strip())))
|
||||
|
||||
href = self.unescape(href).strip()
|
||||
|
||||
return href, title, index, handled
|
||||
|
||||
def getText(self, data, index):
|
||||
"""Parse the content between `[]` of the start of an image or link
|
||||
resolving nested square brackets.
|
||||
|
||||
"""
|
||||
bracket_count = 1
|
||||
text = []
|
||||
for pos in range(index, len(data)):
|
||||
c = data[pos]
|
||||
if c == ']':
|
||||
bracket_count -= 1
|
||||
elif c == '[':
|
||||
bracket_count += 1
|
||||
index += 1
|
||||
if bracket_count == 0:
|
||||
break
|
||||
text.append(c)
|
||||
return ''.join(text), index, bracket_count == 0
|
||||
|
||||
|
||||
class ImageInlineProcessor(LinkInlineProcessor):
|
||||
""" Return a `img` element from the given match. """
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
|
||||
""" Return an `img` [`Element`][xml.etree.ElementTree.Element] or `(None, None, None)`. """
|
||||
text, index, handled = self.getText(data, m.end(0))
|
||||
if not handled:
|
||||
return None, None, None
|
||||
|
||||
src, title, index, handled = self.getLink(data, index)
|
||||
if not handled:
|
||||
return None, None, None
|
||||
|
||||
el = etree.Element("img")
|
||||
|
||||
el.set("src", src)
|
||||
|
||||
if title is not None:
|
||||
el.set("title", title)
|
||||
|
||||
el.set('alt', self.unescape(text))
|
||||
return el, m.start(0), index
|
||||
|
||||
|
||||
class ReferenceInlineProcessor(LinkInlineProcessor):
|
||||
""" Match to a stored reference and return link element. """
|
||||
NEWLINE_CLEANUP_RE = re.compile(r'\s+', re.MULTILINE)
|
||||
|
||||
RE_LINK = re.compile(r'\s?\[([^\]]*)\]', re.DOTALL | re.UNICODE)
|
||||
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element | None, int | None, int | None]:
|
||||
"""
|
||||
Return [`Element`][xml.etree.ElementTree.Element] returned by `makeTag` method or `(None, None, None)`.
|
||||
|
||||
"""
|
||||
text, index, handled = self.getText(data, m.end(0))
|
||||
if not handled:
|
||||
return None, None, None
|
||||
|
||||
id, end, handled = self.evalId(data, index, text)
|
||||
if not handled:
|
||||
return None, None, None
|
||||
|
||||
# Clean up line breaks in id
|
||||
id = self.NEWLINE_CLEANUP_RE.sub(' ', id)
|
||||
if id not in self.md.references: # ignore undefined refs
|
||||
return None, m.start(0), end
|
||||
|
||||
href, title = self.md.references[id]
|
||||
|
||||
return self.makeTag(href, title, text), m.start(0), end
|
||||
|
||||
def evalId(self, data, index, text):
|
||||
"""
|
||||
Evaluate the id portion of `[ref][id]`.
|
||||
|
||||
If `[ref][]` use `[ref]`.
|
||||
"""
|
||||
m = self.RE_LINK.match(data, pos=index)
|
||||
if not m:
|
||||
return None, index, False
|
||||
else:
|
||||
id = m.group(1).lower()
|
||||
end = m.end(0)
|
||||
if not id:
|
||||
id = text.lower()
|
||||
return id, end, True
|
||||
|
||||
def makeTag(self, href: str, title: str, text: str) -> etree.Element:
|
||||
""" Return an `a` [`Element`][xml.etree.ElementTree.Element]. """
|
||||
el = etree.Element('a')
|
||||
|
||||
el.set('href', href)
|
||||
if title:
|
||||
el.set('title', title)
|
||||
|
||||
el.text = text
|
||||
return el
|
||||
|
||||
|
||||
class ShortReferenceInlineProcessor(ReferenceInlineProcessor):
|
||||
"""Short form of reference: `[google]`. """
|
||||
def evalId(self, data, index, text):
|
||||
"""Evaluate the id of `[ref]`. """
|
||||
|
||||
return text.lower(), index, True
|
||||
|
||||
|
||||
class ImageReferenceInlineProcessor(ReferenceInlineProcessor):
|
||||
""" Match to a stored reference and return `img` element. """
|
||||
def makeTag(self, href: str, title: str, text: str) -> etree.Element:
|
||||
""" Return an `img` [`Element`][xml.etree.ElementTree.Element]. """
|
||||
el = etree.Element("img")
|
||||
el.set("src", href)
|
||||
if title:
|
||||
el.set("title", title)
|
||||
el.set("alt", self.unescape(text))
|
||||
return el
|
||||
|
||||
|
||||
class ShortImageReferenceInlineProcessor(ImageReferenceInlineProcessor):
|
||||
""" Short form of image reference: `![ref]`. """
|
||||
def evalId(self, data, index, text):
|
||||
"""Evaluate the id of `[ref]`. """
|
||||
|
||||
return text.lower(), index, True
|
||||
|
||||
|
||||
class AutolinkInlineProcessor(InlineProcessor):
|
||||
""" Return a link Element given an auto-link (`<http://example/com>`). """
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
|
||||
""" Return an `a` [`Element`][xml.etree.ElementTree.Element] of `group(1)`. """
|
||||
el = etree.Element("a")
|
||||
el.set('href', self.unescape(m.group(1)))
|
||||
el.text = util.AtomicString(m.group(1))
|
||||
return el, m.start(0), m.end(0)
|
||||
|
||||
|
||||
class AutomailInlineProcessor(InlineProcessor):
|
||||
"""
|
||||
Return a `mailto` link Element given an auto-mail link (`<foo@example.com>`).
|
||||
"""
|
||||
def handleMatch(self, m: re.Match[str], data: str) -> tuple[etree.Element, int, int]:
|
||||
""" Return an [`Element`][xml.etree.ElementTree.Element] containing a `mailto` link of `group(1)`. """
|
||||
el = etree.Element('a')
|
||||
email = self.unescape(m.group(1))
|
||||
if email.startswith("mailto:"):
|
||||
email = email[len("mailto:"):]
|
||||
|
||||
def codepoint2name(code):
|
||||
"""Return entity definition by code, or the code if not defined."""
|
||||
entity = entities.codepoint2name.get(code)
|
||||
if entity:
|
||||
return "{}{};".format(util.AMP_SUBSTITUTE, entity)
|
||||
else:
|
||||
return "%s#%d;" % (util.AMP_SUBSTITUTE, code)
|
||||
|
||||
letters = [codepoint2name(ord(letter)) for letter in email]
|
||||
el.text = util.AtomicString(''.join(letters))
|
||||
|
||||
mailto = "mailto:" + email
|
||||
mailto = "".join([util.AMP_SUBSTITUTE + '#%d;' %
|
||||
ord(letter) for letter in mailto])
|
||||
el.set('href', mailto)
|
||||
return el, m.start(0), m.end(0)
|
@ -1,143 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
|
||||
Post-processors run on the text of the entire document after is has been serialized into a string.
|
||||
Postprocessors should be used to work with the text just before output. Usually, they are used add
|
||||
back sections that were extracted in a preprocessor, fix up outgoing encodings, or wrap the whole
|
||||
document.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import OrderedDict
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from . import util
|
||||
import re
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
|
||||
|
||||
def build_postprocessors(md: Markdown, **kwargs: Any) -> util.Registry[Postprocessor]:
|
||||
""" Build the default postprocessors for Markdown. """
|
||||
postprocessors = util.Registry()
|
||||
postprocessors.register(RawHtmlPostprocessor(md), 'raw_html', 30)
|
||||
postprocessors.register(AndSubstitutePostprocessor(), 'amp_substitute', 20)
|
||||
return postprocessors
|
||||
|
||||
|
||||
class Postprocessor(util.Processor):
|
||||
"""
|
||||
Postprocessors are run after the ElementTree it converted back into text.
|
||||
|
||||
Each Postprocessor implements a `run` method that takes a pointer to a
|
||||
text string, modifies it as necessary and returns a text string.
|
||||
|
||||
Postprocessors must extend `Postprocessor`.
|
||||
|
||||
"""
|
||||
|
||||
def run(self, text: str) -> str:
|
||||
"""
|
||||
Subclasses of `Postprocessor` should implement a `run` method, which
|
||||
takes the html document as a single text string and returns a
|
||||
(possibly modified) string.
|
||||
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class RawHtmlPostprocessor(Postprocessor):
|
||||
""" Restore raw html to the document. """
|
||||
|
||||
BLOCK_LEVEL_REGEX = re.compile(r'^\<\/?([^ >]+)')
|
||||
|
||||
def run(self, text: str):
|
||||
""" Iterate over html stash and restore html. """
|
||||
replacements = OrderedDict()
|
||||
for i in range(self.md.htmlStash.html_counter):
|
||||
html = self.stash_to_string(self.md.htmlStash.rawHtmlBlocks[i])
|
||||
if self.isblocklevel(html):
|
||||
replacements["<p>{}</p>".format(
|
||||
self.md.htmlStash.get_placeholder(i))] = html
|
||||
replacements[self.md.htmlStash.get_placeholder(i)] = html
|
||||
|
||||
def substitute_match(m):
|
||||
key = m.group(0)
|
||||
|
||||
if key not in replacements:
|
||||
if key[3:-4] in replacements:
|
||||
return f'<p>{ replacements[key[3:-4]] }</p>'
|
||||
else:
|
||||
return key
|
||||
|
||||
return replacements[key]
|
||||
|
||||
if replacements:
|
||||
base_placeholder = util.HTML_PLACEHOLDER % r'([0-9]+)'
|
||||
pattern = re.compile(f'<p>{ base_placeholder }</p>|{ base_placeholder }')
|
||||
processed_text = pattern.sub(substitute_match, text)
|
||||
else:
|
||||
return text
|
||||
|
||||
if processed_text == text:
|
||||
return processed_text
|
||||
else:
|
||||
return self.run(processed_text)
|
||||
|
||||
def isblocklevel(self, html: str) -> bool:
|
||||
""" Check is block of HTML is block-level. """
|
||||
m = self.BLOCK_LEVEL_REGEX.match(html)
|
||||
if m:
|
||||
if m.group(1)[0] in ('!', '?', '@', '%'):
|
||||
# Comment, PHP etc...
|
||||
return True
|
||||
return self.md.is_block_level(m.group(1))
|
||||
return False
|
||||
|
||||
def stash_to_string(self, text: str) -> str:
|
||||
""" Convert a stashed object to a string. """
|
||||
return str(text)
|
||||
|
||||
|
||||
class AndSubstitutePostprocessor(Postprocessor):
|
||||
""" Restore valid entities """
|
||||
|
||||
def run(self, text):
|
||||
text = text.replace(util.AMP_SUBSTITUTE, "&")
|
||||
return text
|
||||
|
||||
|
||||
@util.deprecated(
|
||||
"This class is deprecated and will be removed in the future; "
|
||||
"use [`UnescapeTreeprocessor`][markdown.treeprocessors.UnescapeTreeprocessor] instead."
|
||||
)
|
||||
class UnescapePostprocessor(Postprocessor):
|
||||
""" Restore escaped chars. """
|
||||
|
||||
RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX))
|
||||
|
||||
def unescape(self, m):
|
||||
return chr(int(m.group(1)))
|
||||
|
||||
def run(self, text):
|
||||
return self.RE.sub(self.unescape, text)
|
@ -1,91 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
Preprocessors work on source text before it is broken down into its individual parts.
|
||||
This is an excellent place to clean up bad characters or to extract portions for later
|
||||
processing that the parser may otherwise choke on.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from . import util
|
||||
from .htmlparser import HTMLExtractor
|
||||
import re
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
|
||||
|
||||
def build_preprocessors(md: Markdown, **kwargs: Any) -> util.Registry[Preprocessor]:
|
||||
""" Build and return the default set of preprocessors used by Markdown. """
|
||||
preprocessors = util.Registry()
|
||||
preprocessors.register(NormalizeWhitespace(md), 'normalize_whitespace', 30)
|
||||
preprocessors.register(HtmlBlockPreprocessor(md), 'html_block', 20)
|
||||
return preprocessors
|
||||
|
||||
|
||||
class Preprocessor(util.Processor):
|
||||
"""
|
||||
Preprocessors are run after the text is broken into lines.
|
||||
|
||||
Each preprocessor implements a `run` method that takes a pointer to a
|
||||
list of lines of the document, modifies it as necessary and returns
|
||||
either the same pointer or a pointer to a new list.
|
||||
|
||||
Preprocessors must extend `Preprocessor`.
|
||||
|
||||
"""
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
"""
|
||||
Each subclass of `Preprocessor` should override the `run` method, which
|
||||
takes the document as a list of strings split by newlines and returns
|
||||
the (possibly modified) list of lines.
|
||||
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class NormalizeWhitespace(Preprocessor):
|
||||
""" Normalize whitespace for consistent parsing. """
|
||||
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
source = '\n'.join(lines)
|
||||
source = source.replace(util.STX, "").replace(util.ETX, "")
|
||||
source = source.replace("\r\n", "\n").replace("\r", "\n") + "\n\n"
|
||||
source = source.expandtabs(self.md.tab_length)
|
||||
source = re.sub(r'(?<=\n) +\n', '\n', source)
|
||||
return source.split('\n')
|
||||
|
||||
|
||||
class HtmlBlockPreprocessor(Preprocessor):
|
||||
"""
|
||||
Remove html blocks from the text and store them for later retrieval.
|
||||
|
||||
The raw HTML is stored in the [`htmlStash`][markdown.util.HtmlStash] of the
|
||||
[`Markdown`][markdown.Markdown] instance.
|
||||
"""
|
||||
|
||||
def run(self, lines: list[str]) -> list[str]:
|
||||
source = '\n'.join(lines)
|
||||
parser = HTMLExtractor(self.md)
|
||||
parser.feed(source)
|
||||
parser.close()
|
||||
return ''.join(parser.cleandoc).split('\n')
|
@ -1,193 +0,0 @@
|
||||
# Add x/html serialization to `Elementree`
|
||||
# Taken from ElementTree 1.3 preview with slight modifications
|
||||
#
|
||||
# Copyright (c) 1999-2007 by Fredrik Lundh. All rights reserved.
|
||||
#
|
||||
# fredrik@pythonware.com
|
||||
# https://www.pythonware.com/
|
||||
#
|
||||
# --------------------------------------------------------------------
|
||||
# The ElementTree toolkit is
|
||||
#
|
||||
# Copyright (c) 1999-2007 by Fredrik Lundh
|
||||
#
|
||||
# By obtaining, using, and/or copying this software and/or its
|
||||
# associated documentation, you agree that you have read, understood,
|
||||
# and will comply with the following terms and conditions:
|
||||
#
|
||||
# Permission to use, copy, modify, and distribute this software and
|
||||
# its associated documentation for any purpose and without fee is
|
||||
# hereby granted, provided that the above copyright notice appears in
|
||||
# all copies, and that both that copyright notice and this permission
|
||||
# notice appear in supporting documentation, and that the name of
|
||||
# Secret Labs AB or the author not be used in advertising or publicity
|
||||
# pertaining to distribution of the software without specific, written
|
||||
# prior permission.
|
||||
#
|
||||
# SECRET LABS AB AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH REGARD
|
||||
# TO THIS SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANT-
|
||||
# ABILITY AND FITNESS. IN NO EVENT SHALL SECRET LABS AB OR THE AUTHOR
|
||||
# BE LIABLE FOR ANY SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY
|
||||
# DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS,
|
||||
# WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS
|
||||
# ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE
|
||||
# OF THIS SOFTWARE.
|
||||
# --------------------------------------------------------------------
|
||||
|
||||
"""
|
||||
Python-Markdown provides two serializers which render [`ElementTree.Element`][xml.etree.ElementTree.Element]
|
||||
objects to a string of HTML. Both functions wrap the same underlying code with only a few minor
|
||||
differences as outlined below:
|
||||
|
||||
1. Empty (self-closing) tags are rendered as `<tag>` for HTML and as `<tag />` for XHTML.
|
||||
2. Boolean attributes are rendered as `attrname` for HTML and as `attrname="attrname"` for XHTML.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from xml.etree.ElementTree import ProcessingInstruction
|
||||
from xml.etree.ElementTree import Comment, ElementTree, Element, QName, HTML_EMPTY
|
||||
import re
|
||||
|
||||
__all__ = ['to_html_string', 'to_xhtml_string']
|
||||
|
||||
RE_AMP = re.compile(r'&(?!(?:\#[0-9]+|\#x[0-9a-f]+|[0-9a-z]+);)', re.I)
|
||||
|
||||
|
||||
def _raise_serialization_error(text): # pragma: no cover
|
||||
raise TypeError(
|
||||
"cannot serialize {!r} (type {})".format(text, type(text).__name__)
|
||||
)
|
||||
|
||||
|
||||
def _escape_cdata(text):
|
||||
# escape character data
|
||||
try:
|
||||
# it's worth avoiding do-nothing calls for strings that are
|
||||
# shorter than 500 character, or so. assume that's, by far,
|
||||
# the most common case in most applications.
|
||||
if "&" in text:
|
||||
# Only replace & when not part of an entity
|
||||
text = RE_AMP.sub('&', text)
|
||||
if "<" in text:
|
||||
text = text.replace("<", "<")
|
||||
if ">" in text:
|
||||
text = text.replace(">", ">")
|
||||
return text
|
||||
except (TypeError, AttributeError): # pragma: no cover
|
||||
_raise_serialization_error(text)
|
||||
|
||||
|
||||
def _escape_attrib(text):
|
||||
# escape attribute value
|
||||
try:
|
||||
if "&" in text:
|
||||
# Only replace & when not part of an entity
|
||||
text = RE_AMP.sub('&', text)
|
||||
if "<" in text:
|
||||
text = text.replace("<", "<")
|
||||
if ">" in text:
|
||||
text = text.replace(">", ">")
|
||||
if "\"" in text:
|
||||
text = text.replace("\"", """)
|
||||
if "\n" in text:
|
||||
text = text.replace("\n", " ")
|
||||
return text
|
||||
except (TypeError, AttributeError): # pragma: no cover
|
||||
_raise_serialization_error(text)
|
||||
|
||||
|
||||
def _escape_attrib_html(text):
|
||||
# escape attribute value
|
||||
try:
|
||||
if "&" in text:
|
||||
# Only replace & when not part of an entity
|
||||
text = RE_AMP.sub('&', text)
|
||||
if "<" in text:
|
||||
text = text.replace("<", "<")
|
||||
if ">" in text:
|
||||
text = text.replace(">", ">")
|
||||
if "\"" in text:
|
||||
text = text.replace("\"", """)
|
||||
return text
|
||||
except (TypeError, AttributeError): # pragma: no cover
|
||||
_raise_serialization_error(text)
|
||||
|
||||
|
||||
def _serialize_html(write, elem, format):
|
||||
tag = elem.tag
|
||||
text = elem.text
|
||||
if tag is Comment:
|
||||
write("<!--%s-->" % _escape_cdata(text))
|
||||
elif tag is ProcessingInstruction:
|
||||
write("<?%s?>" % _escape_cdata(text))
|
||||
elif tag is None:
|
||||
if text:
|
||||
write(_escape_cdata(text))
|
||||
for e in elem:
|
||||
_serialize_html(write, e, format)
|
||||
else:
|
||||
namespace_uri = None
|
||||
if isinstance(tag, QName):
|
||||
# `QNAME` objects store their data as a string: `{uri}tag`
|
||||
if tag.text[:1] == "{":
|
||||
namespace_uri, tag = tag.text[1:].split("}", 1)
|
||||
else:
|
||||
raise ValueError('QName objects must define a tag.')
|
||||
write("<" + tag)
|
||||
items = elem.items()
|
||||
if items:
|
||||
items = sorted(items) # lexical order
|
||||
for k, v in items:
|
||||
if isinstance(k, QName):
|
||||
# Assume a text only `QName`
|
||||
k = k.text
|
||||
if isinstance(v, QName):
|
||||
# Assume a text only `QName`
|
||||
v = v.text
|
||||
else:
|
||||
v = _escape_attrib_html(v)
|
||||
if k == v and format == 'html':
|
||||
# handle boolean attributes
|
||||
write(" %s" % v)
|
||||
else:
|
||||
write(' {}="{}"'.format(k, v))
|
||||
if namespace_uri:
|
||||
write(' xmlns="%s"' % (_escape_attrib(namespace_uri)))
|
||||
if format == "xhtml" and tag.lower() in HTML_EMPTY:
|
||||
write(" />")
|
||||
else:
|
||||
write(">")
|
||||
if text:
|
||||
if tag.lower() in ["script", "style"]:
|
||||
write(text)
|
||||
else:
|
||||
write(_escape_cdata(text))
|
||||
for e in elem:
|
||||
_serialize_html(write, e, format)
|
||||
if tag.lower() not in HTML_EMPTY:
|
||||
write("</" + tag + ">")
|
||||
if elem.tail:
|
||||
write(_escape_cdata(elem.tail))
|
||||
|
||||
|
||||
def _write_html(root, format="html"):
|
||||
assert root is not None
|
||||
data = []
|
||||
write = data.append
|
||||
_serialize_html(write, root, format)
|
||||
return "".join(data)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------
|
||||
# public functions
|
||||
|
||||
|
||||
def to_html_string(element: Element) -> str:
|
||||
""" Serialize element and its children to a string of HTML5. """
|
||||
return _write_html(ElementTree(element).getroot(), format="html")
|
||||
|
||||
|
||||
def to_xhtml_string(element: Element) -> str:
|
||||
""" Serialize element and its children to a string of XHTML. """
|
||||
return _write_html(ElementTree(element).getroot(), format="xhtml")
|
@ -1,224 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
""" A collection of tools for testing the Markdown code base and extensions. """
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
import unittest
|
||||
import textwrap
|
||||
from typing import Any
|
||||
from . import markdown, Markdown, util
|
||||
|
||||
try:
|
||||
import tidylib
|
||||
except ImportError:
|
||||
tidylib = None
|
||||
|
||||
__all__ = ['TestCase', 'LegacyTestCase', 'Kwargs']
|
||||
|
||||
|
||||
class TestCase(unittest.TestCase):
|
||||
"""
|
||||
A [`unittest.TestCase`][] subclass with helpers for testing Markdown output.
|
||||
|
||||
Define `default_kwargs` as a `dict` of keywords to pass to Markdown for each
|
||||
test. The defaults can be overridden on individual tests.
|
||||
|
||||
The `assertMarkdownRenders` method accepts the source text, the expected
|
||||
output, and any keywords to pass to Markdown. The `default_kwargs` are used
|
||||
except where overridden by `kwargs`. The output and expected output are passed
|
||||
to `TestCase.assertMultiLineEqual`. An `AssertionError` is raised with a diff
|
||||
if the actual output does not equal the expected output.
|
||||
|
||||
The `dedent` method is available to dedent triple-quoted strings if
|
||||
necessary.
|
||||
|
||||
In all other respects, behaves as `unittest.TestCase`.
|
||||
"""
|
||||
|
||||
default_kwargs: dict[str, Any] = {}
|
||||
""" Default options to pass to Markdown for each test. """
|
||||
|
||||
def assertMarkdownRenders(self, source, expected, expected_attrs=None, **kwargs):
|
||||
"""
|
||||
Test that source Markdown text renders to expected output with given keywords.
|
||||
|
||||
`expected_attrs` accepts a `dict`. Each key should be the name of an attribute
|
||||
on the `Markdown` instance and the value should be the expected value after
|
||||
the source text is parsed by Markdown. After the expected output is tested,
|
||||
the expected value for each attribute is compared against the actual
|
||||
attribute of the `Markdown` instance using `TestCase.assertEqual`.
|
||||
"""
|
||||
|
||||
expected_attrs = expected_attrs or {}
|
||||
kws = self.default_kwargs.copy()
|
||||
kws.update(kwargs)
|
||||
md = Markdown(**kws)
|
||||
output = md.convert(source)
|
||||
self.assertMultiLineEqual(output, expected)
|
||||
for key, value in expected_attrs.items():
|
||||
self.assertEqual(getattr(md, key), value)
|
||||
|
||||
def dedent(self, text):
|
||||
"""
|
||||
Dedent text.
|
||||
"""
|
||||
|
||||
# TODO: If/when actual output ends with a newline, then use:
|
||||
# return textwrap.dedent(text.strip('/n'))
|
||||
return textwrap.dedent(text).strip()
|
||||
|
||||
|
||||
class recursionlimit:
|
||||
"""
|
||||
A context manager which temporarily modifies the Python recursion limit.
|
||||
|
||||
The testing framework, coverage, etc. may add an arbitrary number of levels to the depth. To maintain consistency
|
||||
in the tests, the current stack depth is determined when called, then added to the provided limit.
|
||||
|
||||
Example usage:
|
||||
|
||||
``` python
|
||||
with recursionlimit(20):
|
||||
# test code here
|
||||
```
|
||||
|
||||
See <https://stackoverflow.com/a/50120316/866026>.
|
||||
"""
|
||||
|
||||
def __init__(self, limit):
|
||||
self.limit = util._get_stack_depth() + limit
|
||||
self.old_limit = sys.getrecursionlimit()
|
||||
|
||||
def __enter__(self):
|
||||
sys.setrecursionlimit(self.limit)
|
||||
|
||||
def __exit__(self, type, value, tb):
|
||||
sys.setrecursionlimit(self.old_limit)
|
||||
|
||||
|
||||
#########################
|
||||
# Legacy Test Framework #
|
||||
#########################
|
||||
|
||||
|
||||
class Kwargs(dict):
|
||||
""" A `dict` like class for holding keyword arguments. """
|
||||
pass
|
||||
|
||||
|
||||
def _normalize_whitespace(text):
|
||||
""" Normalize whitespace for a string of HTML using `tidylib`. """
|
||||
output, errors = tidylib.tidy_fragment(text, options={
|
||||
'drop_empty_paras': 0,
|
||||
'fix_backslash': 0,
|
||||
'fix_bad_comments': 0,
|
||||
'fix_uri': 0,
|
||||
'join_styles': 0,
|
||||
'lower_literals': 0,
|
||||
'merge_divs': 0,
|
||||
'output_xhtml': 1,
|
||||
'quote_ampersand': 0,
|
||||
'newline': 'LF'
|
||||
})
|
||||
return output
|
||||
|
||||
|
||||
class LegacyTestMeta(type):
|
||||
def __new__(cls, name, bases, dct):
|
||||
|
||||
def generate_test(infile, outfile, normalize, kwargs):
|
||||
def test(self):
|
||||
with open(infile, encoding="utf-8") as f:
|
||||
input = f.read()
|
||||
with open(outfile, encoding="utf-8") as f:
|
||||
# Normalize line endings
|
||||
# (on Windows, git may have altered line endings).
|
||||
expected = f.read().replace("\r\n", "\n")
|
||||
output = markdown(input, **kwargs)
|
||||
if tidylib and normalize:
|
||||
try:
|
||||
expected = _normalize_whitespace(expected)
|
||||
output = _normalize_whitespace(output)
|
||||
except OSError:
|
||||
self.skipTest("Tidylib's c library not available.")
|
||||
elif normalize:
|
||||
self.skipTest('Tidylib not available.')
|
||||
self.assertMultiLineEqual(output, expected)
|
||||
return test
|
||||
|
||||
location = dct.get('location', '')
|
||||
exclude = dct.get('exclude', [])
|
||||
normalize = dct.get('normalize', False)
|
||||
input_ext = dct.get('input_ext', '.txt')
|
||||
output_ext = dct.get('output_ext', '.html')
|
||||
kwargs = dct.get('default_kwargs', Kwargs())
|
||||
|
||||
if os.path.isdir(location):
|
||||
for file in os.listdir(location):
|
||||
infile = os.path.join(location, file)
|
||||
if os.path.isfile(infile):
|
||||
tname, ext = os.path.splitext(file)
|
||||
if ext == input_ext:
|
||||
outfile = os.path.join(location, tname + output_ext)
|
||||
tname = tname.replace(' ', '_').replace('-', '_')
|
||||
kws = kwargs.copy()
|
||||
if tname in dct:
|
||||
kws.update(dct[tname])
|
||||
test_name = 'test_%s' % tname
|
||||
if tname not in exclude:
|
||||
dct[test_name] = generate_test(infile, outfile, normalize, kws)
|
||||
else:
|
||||
dct[test_name] = unittest.skip('Excluded')(lambda: None)
|
||||
|
||||
return type.__new__(cls, name, bases, dct)
|
||||
|
||||
|
||||
class LegacyTestCase(unittest.TestCase, metaclass=LegacyTestMeta):
|
||||
"""
|
||||
A [`unittest.TestCase`][] subclass for running Markdown's legacy file-based tests.
|
||||
|
||||
A subclass should define various properties which point to a directory of
|
||||
text-based test files and define various behaviors/defaults for those tests.
|
||||
The following properties are supported:
|
||||
|
||||
Attributes:
|
||||
location (str): A path to the directory of test files. An absolute path is preferred.
|
||||
exclude (list[str]): A list of tests to exclude. Each test name should comprise the filename
|
||||
without an extension.
|
||||
normalize (bool): A boolean value indicating if the HTML should be normalized. Default: `False`.
|
||||
input_ext (str): A string containing the file extension of input files. Default: `.txt`.
|
||||
output_ext (str): A string containing the file extension of expected output files. Default: `html`.
|
||||
default_kwargs (Kwargs[str, Any]): The default set of keyword arguments for all test files in the directory.
|
||||
|
||||
In addition, properties can be defined for each individual set of test files within
|
||||
the directory. The property should be given the name of the file without the file
|
||||
extension. Any spaces and dashes in the filename should be replaced with
|
||||
underscores. The value of the property should be a `Kwargs` instance which
|
||||
contains the keyword arguments that should be passed to `Markdown` for that
|
||||
test file. The keyword arguments will "update" the `default_kwargs`.
|
||||
|
||||
When the class instance is created, it will walk the given directory and create
|
||||
a separate `Unitttest` for each set of test files using the naming scheme:
|
||||
`test_filename`. One `Unittest` will be run for each set of input and output files.
|
||||
"""
|
||||
pass
|
@ -1,476 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
Tree processors manipulate the tree created by block processors. They can even create an entirely
|
||||
new `ElementTree` object. This is an excellent place for creating summaries, adding collected
|
||||
references, or last minute adjustments.
|
||||
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import xml.etree.ElementTree as etree
|
||||
from typing import TYPE_CHECKING, Any
|
||||
from . import util
|
||||
from . import inlinepatterns
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
|
||||
|
||||
def build_treeprocessors(md: Markdown, **kwargs: Any) -> util.Registry[Treeprocessor]:
|
||||
""" Build the default `treeprocessors` for Markdown. """
|
||||
treeprocessors = util.Registry()
|
||||
treeprocessors.register(InlineProcessor(md), 'inline', 20)
|
||||
treeprocessors.register(PrettifyTreeprocessor(md), 'prettify', 10)
|
||||
treeprocessors.register(UnescapeTreeprocessor(md), 'unescape', 0)
|
||||
return treeprocessors
|
||||
|
||||
|
||||
def isString(s: Any) -> bool:
|
||||
""" Return `True` if object is a string but not an [`AtomicString`][markdown.util.AtomicString]. """
|
||||
if not isinstance(s, util.AtomicString):
|
||||
return isinstance(s, str)
|
||||
return False
|
||||
|
||||
|
||||
class Treeprocessor(util.Processor):
|
||||
"""
|
||||
`Treeprocessor`s are run on the `ElementTree` object before serialization.
|
||||
|
||||
Each `Treeprocessor` implements a `run` method that takes a pointer to an
|
||||
`Element` and modifies it as necessary.
|
||||
|
||||
`Treeprocessors` must extend `markdown.Treeprocessor`.
|
||||
|
||||
"""
|
||||
def run(self, root: etree.Element) -> etree.Element | None:
|
||||
"""
|
||||
Subclasses of `Treeprocessor` should implement a `run` method, which
|
||||
takes a root `Element`. This method can return another `Element`
|
||||
object, and the existing root `Element` will be replaced, or it can
|
||||
modify the current tree and return `None`.
|
||||
"""
|
||||
pass # pragma: no cover
|
||||
|
||||
|
||||
class InlineProcessor(Treeprocessor):
|
||||
"""
|
||||
A `Treeprocessor` that traverses a tree, applying inline patterns.
|
||||
"""
|
||||
|
||||
def __init__(self, md):
|
||||
self.__placeholder_prefix = util.INLINE_PLACEHOLDER_PREFIX
|
||||
self.__placeholder_suffix = util.ETX
|
||||
self.__placeholder_length = 4 + len(self.__placeholder_prefix) \
|
||||
+ len(self.__placeholder_suffix)
|
||||
self.__placeholder_re = util.INLINE_PLACEHOLDER_RE
|
||||
self.md = md
|
||||
self.inlinePatterns = md.inlinePatterns
|
||||
self.ancestors = []
|
||||
|
||||
def __makePlaceholder(self, type) -> tuple[str, str]:
|
||||
""" Generate a placeholder """
|
||||
id = "%04d" % len(self.stashed_nodes)
|
||||
hash = util.INLINE_PLACEHOLDER % id
|
||||
return hash, id
|
||||
|
||||
def __findPlaceholder(self, data: str, index: int) -> tuple[str | None, int]:
|
||||
"""
|
||||
Extract id from data string, start from index.
|
||||
|
||||
Arguments:
|
||||
data: String.
|
||||
index: Index, from which we start search.
|
||||
|
||||
Returns:
|
||||
Placeholder id and string index, after the found placeholder.
|
||||
|
||||
"""
|
||||
m = self.__placeholder_re.search(data, index)
|
||||
if m:
|
||||
return m.group(1), m.end()
|
||||
else:
|
||||
return None, index + 1
|
||||
|
||||
def __stashNode(self, node, type) -> str:
|
||||
""" Add node to stash. """
|
||||
placeholder, id = self.__makePlaceholder(type)
|
||||
self.stashed_nodes[id] = node
|
||||
return placeholder
|
||||
|
||||
def __handleInline(self, data: str, patternIndex: int = 0) -> str:
|
||||
"""
|
||||
Process string with inline patterns and replace it with placeholders.
|
||||
|
||||
Arguments:
|
||||
data: A line of Markdown text.
|
||||
patternIndex: The index of the `inlinePattern` to start with.
|
||||
|
||||
Returns:
|
||||
String with placeholders.
|
||||
|
||||
"""
|
||||
if not isinstance(data, util.AtomicString):
|
||||
startIndex = 0
|
||||
count = len(self.inlinePatterns)
|
||||
while patternIndex < count:
|
||||
data, matched, startIndex = self.__applyPattern(
|
||||
self.inlinePatterns[patternIndex], data, patternIndex, startIndex
|
||||
)
|
||||
if not matched:
|
||||
patternIndex += 1
|
||||
return data
|
||||
|
||||
def __processElementText(self, node: etree.Element, subnode: etree.Element, isText: bool = True):
|
||||
"""
|
||||
Process placeholders in `Element.text` or `Element.tail`
|
||||
of Elements popped from `self.stashed_nodes`.
|
||||
|
||||
Arguments:
|
||||
node: Parent node.
|
||||
subnode: Processing node.
|
||||
isText: Boolean variable, True - it's text, False - it's a tail.
|
||||
|
||||
"""
|
||||
if isText:
|
||||
text = subnode.text
|
||||
subnode.text = None
|
||||
else:
|
||||
text = subnode.tail
|
||||
subnode.tail = None
|
||||
|
||||
childResult = self.__processPlaceholders(text, subnode, isText)
|
||||
|
||||
if not isText and node is not subnode:
|
||||
pos = list(node).index(subnode) + 1
|
||||
else:
|
||||
pos = 0
|
||||
|
||||
childResult.reverse()
|
||||
for newChild in childResult:
|
||||
node.insert(pos, newChild[0])
|
||||
|
||||
def __processPlaceholders(
|
||||
self,
|
||||
data: str,
|
||||
parent: etree.Element,
|
||||
isText: bool = True
|
||||
) -> list[tuple[etree.Element, Any]]:
|
||||
"""
|
||||
Process string with placeholders and generate `ElementTree` tree.
|
||||
|
||||
Arguments:
|
||||
data: String with placeholders instead of `ElementTree` elements.
|
||||
parent: Element, which contains processing inline data.
|
||||
isText: Boolean variable, True - it's text, False - it's a tail.
|
||||
|
||||
Returns:
|
||||
List with `ElementTree` elements with applied inline patterns.
|
||||
|
||||
"""
|
||||
def linkText(text):
|
||||
if text:
|
||||
if result:
|
||||
if result[-1][0].tail:
|
||||
result[-1][0].tail += text
|
||||
else:
|
||||
result[-1][0].tail = text
|
||||
elif not isText:
|
||||
if parent.tail:
|
||||
parent.tail += text
|
||||
else:
|
||||
parent.tail = text
|
||||
else:
|
||||
if parent.text:
|
||||
parent.text += text
|
||||
else:
|
||||
parent.text = text
|
||||
result = []
|
||||
strartIndex = 0
|
||||
while data:
|
||||
index = data.find(self.__placeholder_prefix, strartIndex)
|
||||
if index != -1:
|
||||
id, phEndIndex = self.__findPlaceholder(data, index)
|
||||
|
||||
if id in self.stashed_nodes:
|
||||
node = self.stashed_nodes.get(id)
|
||||
|
||||
if index > 0:
|
||||
text = data[strartIndex:index]
|
||||
linkText(text)
|
||||
|
||||
if not isString(node): # it's Element
|
||||
for child in [node] + list(node):
|
||||
if child.tail:
|
||||
if child.tail.strip():
|
||||
self.__processElementText(
|
||||
node, child, False
|
||||
)
|
||||
if child.text:
|
||||
if child.text.strip():
|
||||
self.__processElementText(child, child)
|
||||
else: # it's just a string
|
||||
linkText(node)
|
||||
strartIndex = phEndIndex
|
||||
continue
|
||||
|
||||
strartIndex = phEndIndex
|
||||
result.append((node, self.ancestors[:]))
|
||||
|
||||
else: # wrong placeholder
|
||||
end = index + len(self.__placeholder_prefix)
|
||||
linkText(data[strartIndex:end])
|
||||
strartIndex = end
|
||||
else:
|
||||
text = data[strartIndex:]
|
||||
if isinstance(data, util.AtomicString):
|
||||
# We don't want to loose the `AtomicString`
|
||||
text = util.AtomicString(text)
|
||||
linkText(text)
|
||||
data = ""
|
||||
|
||||
return result
|
||||
|
||||
def __applyPattern(
|
||||
self,
|
||||
pattern: inlinepatterns.Pattern,
|
||||
data: str,
|
||||
patternIndex: int,
|
||||
startIndex: int = 0
|
||||
) -> tuple[str, bool, int]:
|
||||
"""
|
||||
Check if the line fits the pattern, create the necessary
|
||||
elements, add it to `stashed_nodes`.
|
||||
|
||||
Arguments:
|
||||
data: The text to be processed.
|
||||
pattern: The pattern to be checked.
|
||||
patternIndex: Index of current pattern.
|
||||
startIndex: String index, from which we start searching.
|
||||
|
||||
Returns:
|
||||
String with placeholders instead of `ElementTree` elements.
|
||||
|
||||
"""
|
||||
new_style = isinstance(pattern, inlinepatterns.InlineProcessor)
|
||||
|
||||
for exclude in pattern.ANCESTOR_EXCLUDES:
|
||||
if exclude.lower() in self.ancestors:
|
||||
return data, False, 0
|
||||
|
||||
if new_style:
|
||||
match = None
|
||||
# Since `handleMatch` may reject our first match,
|
||||
# we iterate over the buffer looking for matches
|
||||
# until we can't find any more.
|
||||
for match in pattern.getCompiledRegExp().finditer(data, startIndex):
|
||||
node, start, end = pattern.handleMatch(match, data)
|
||||
if start is None or end is None:
|
||||
startIndex += match.end(0)
|
||||
match = None
|
||||
continue
|
||||
break
|
||||
else: # pragma: no cover
|
||||
match = pattern.getCompiledRegExp().match(data[startIndex:])
|
||||
leftData = data[:startIndex]
|
||||
|
||||
if not match:
|
||||
return data, False, 0
|
||||
|
||||
if not new_style: # pragma: no cover
|
||||
node = pattern.handleMatch(match)
|
||||
start = match.start(0)
|
||||
end = match.end(0)
|
||||
|
||||
if node is None:
|
||||
return data, True, end
|
||||
|
||||
if not isString(node):
|
||||
if not isinstance(node.text, util.AtomicString):
|
||||
# We need to process current node too
|
||||
for child in [node] + list(node):
|
||||
if not isString(node):
|
||||
if child.text:
|
||||
self.ancestors.append(child.tag.lower())
|
||||
child.text = self.__handleInline(
|
||||
child.text, patternIndex + 1
|
||||
)
|
||||
self.ancestors.pop()
|
||||
if child.tail:
|
||||
child.tail = self.__handleInline(
|
||||
child.tail, patternIndex
|
||||
)
|
||||
|
||||
placeholder = self.__stashNode(node, pattern.type())
|
||||
|
||||
if new_style:
|
||||
return "{}{}{}".format(data[:start],
|
||||
placeholder, data[end:]), True, 0
|
||||
else: # pragma: no cover
|
||||
return "{}{}{}{}".format(leftData,
|
||||
match.group(1),
|
||||
placeholder, match.groups()[-1]), True, 0
|
||||
|
||||
def __build_ancestors(self, parent, parents):
|
||||
"""Build the ancestor list."""
|
||||
ancestors = []
|
||||
while parent is not None:
|
||||
if parent is not None:
|
||||
ancestors.append(parent.tag.lower())
|
||||
parent = self.parent_map.get(parent)
|
||||
ancestors.reverse()
|
||||
parents.extend(ancestors)
|
||||
|
||||
def run(self, tree: etree.Element, ancestors: list[str] | None = None) -> etree.Element:
|
||||
"""Apply inline patterns to a parsed Markdown tree.
|
||||
|
||||
Iterate over `Element`, find elements with inline tag, apply inline
|
||||
patterns and append newly created Elements to tree. To avoid further
|
||||
processing of string with inline patterns, instead of normal string,
|
||||
use subclass [`AtomicString`][markdown.util.AtomicString]:
|
||||
|
||||
node.text = markdown.util.AtomicString("This will not be processed.")
|
||||
|
||||
Arguments:
|
||||
tree: `Element` object, representing Markdown tree.
|
||||
ancestors: List of parent tag names that precede the tree node (if needed).
|
||||
|
||||
Returns:
|
||||
An element tree object with applied inline patterns.
|
||||
|
||||
"""
|
||||
self.stashed_nodes: dict[str, etree.Element] = {}
|
||||
|
||||
# Ensure a valid parent list, but copy passed in lists
|
||||
# to ensure we don't have the user accidentally change it on us.
|
||||
tree_parents = [] if ancestors is None else ancestors[:]
|
||||
|
||||
self.parent_map = {c: p for p in tree.iter() for c in p}
|
||||
stack = [(tree, tree_parents)]
|
||||
|
||||
while stack:
|
||||
currElement, parents = stack.pop()
|
||||
|
||||
self.ancestors = parents
|
||||
self.__build_ancestors(currElement, self.ancestors)
|
||||
|
||||
insertQueue = []
|
||||
for child in currElement:
|
||||
if child.text and not isinstance(
|
||||
child.text, util.AtomicString
|
||||
):
|
||||
self.ancestors.append(child.tag.lower())
|
||||
text = child.text
|
||||
child.text = None
|
||||
lst = self.__processPlaceholders(
|
||||
self.__handleInline(text), child
|
||||
)
|
||||
for item in lst:
|
||||
self.parent_map[item[0]] = child
|
||||
stack += lst
|
||||
insertQueue.append((child, lst))
|
||||
self.ancestors.pop()
|
||||
if child.tail:
|
||||
tail = self.__handleInline(child.tail)
|
||||
dumby = etree.Element('d')
|
||||
child.tail = None
|
||||
tailResult = self.__processPlaceholders(tail, dumby, False)
|
||||
if dumby.tail:
|
||||
child.tail = dumby.tail
|
||||
pos = list(currElement).index(child) + 1
|
||||
tailResult.reverse()
|
||||
for newChild in tailResult:
|
||||
self.parent_map[newChild[0]] = currElement
|
||||
currElement.insert(pos, newChild[0])
|
||||
if len(child):
|
||||
self.parent_map[child] = currElement
|
||||
stack.append((child, self.ancestors[:]))
|
||||
|
||||
for element, lst in insertQueue:
|
||||
for i, obj in enumerate(lst):
|
||||
newChild = obj[0]
|
||||
element.insert(i, newChild)
|
||||
return tree
|
||||
|
||||
|
||||
class PrettifyTreeprocessor(Treeprocessor):
|
||||
""" Add line breaks to the html document. """
|
||||
|
||||
def _prettifyETree(self, elem):
|
||||
""" Recursively add line breaks to `ElementTree` children. """
|
||||
|
||||
i = "\n"
|
||||
if self.md.is_block_level(elem.tag) and elem.tag not in ['code', 'pre']:
|
||||
if (not elem.text or not elem.text.strip()) \
|
||||
and len(elem) and self.md.is_block_level(elem[0].tag):
|
||||
elem.text = i
|
||||
for e in elem:
|
||||
if self.md.is_block_level(e.tag):
|
||||
self._prettifyETree(e)
|
||||
if not elem.tail or not elem.tail.strip():
|
||||
elem.tail = i
|
||||
|
||||
def run(self, root: etree.Element) -> None:
|
||||
""" Add line breaks to `Element` object and its children. """
|
||||
|
||||
self._prettifyETree(root)
|
||||
# Do `<br />`'s separately as they are often in the middle of
|
||||
# inline content and missed by `_prettifyETree`.
|
||||
brs = root.iter('br')
|
||||
for br in brs:
|
||||
if not br.tail or not br.tail.strip():
|
||||
br.tail = '\n'
|
||||
else:
|
||||
br.tail = '\n%s' % br.tail
|
||||
# Clean up extra empty lines at end of code blocks.
|
||||
pres = root.iter('pre')
|
||||
for pre in pres:
|
||||
if len(pre) and pre[0].tag == 'code':
|
||||
code = pre[0]
|
||||
# Only prettify code containing text only
|
||||
if not len(code) and code.text is not None:
|
||||
code.text = util.AtomicString(code.text.rstrip() + '\n')
|
||||
|
||||
|
||||
class UnescapeTreeprocessor(Treeprocessor):
|
||||
""" Restore escaped chars """
|
||||
|
||||
RE = re.compile(r'{}(\d+){}'.format(util.STX, util.ETX))
|
||||
|
||||
def _unescape(self, m):
|
||||
return chr(int(m.group(1)))
|
||||
|
||||
def unescape(self, text: str) -> str:
|
||||
return self.RE.sub(self._unescape, text)
|
||||
|
||||
def run(self, root):
|
||||
""" Loop over all elements and unescape all text. """
|
||||
for elem in root.iter():
|
||||
# Unescape text content
|
||||
if elem.text and not elem.tag == 'code':
|
||||
elem.text = self.unescape(elem.text)
|
||||
# Unescape tail content
|
||||
if elem.tail:
|
||||
elem.tail = self.unescape(elem.tail)
|
||||
# Unescape attribute values
|
||||
for key, value in elem.items():
|
||||
elem.set(key, self.unescape(value))
|
@ -1,399 +0,0 @@
|
||||
# Python Markdown
|
||||
|
||||
# A Python implementation of John Gruber's Markdown.
|
||||
|
||||
# Documentation: https://python-markdown.github.io/
|
||||
# GitHub: https://github.com/Python-Markdown/markdown/
|
||||
# PyPI: https://pypi.org/project/Markdown/
|
||||
|
||||
# Started by Manfred Stienstra (http://www.dwerg.net/).
|
||||
# Maintained for a few years by Yuri Takhteyev (http://www.freewisdom.org).
|
||||
# Currently maintained by Waylan Limberg (https://github.com/waylan),
|
||||
# Dmitry Shachnev (https://github.com/mitya57) and Isaac Muse (https://github.com/facelessuser).
|
||||
|
||||
# Copyright 2007-2023 The Python Markdown Project (v. 1.7 and later)
|
||||
# Copyright 2004, 2005, 2006 Yuri Takhteyev (v. 0.2-1.6b)
|
||||
# Copyright 2004 Manfred Stienstra (the original version)
|
||||
|
||||
# License: BSD (see LICENSE.md for details).
|
||||
|
||||
"""
|
||||
This module contains various contacts, classes and functions which get referenced and used
|
||||
throughout the code base.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
import sys
|
||||
import warnings
|
||||
from functools import wraps, lru_cache
|
||||
from itertools import count
|
||||
from typing import TYPE_CHECKING, Generic, Iterator, NamedTuple, TypeVar, overload
|
||||
|
||||
if TYPE_CHECKING: # pragma: no cover
|
||||
from markdown import Markdown
|
||||
|
||||
_T = TypeVar('_T')
|
||||
|
||||
|
||||
"""
|
||||
Constants you might want to modify
|
||||
-----------------------------------------------------------------------------
|
||||
"""
|
||||
|
||||
|
||||
BLOCK_LEVEL_ELEMENTS: list[str] = [
|
||||
# Elements which are invalid to wrap in a `<p>` tag.
|
||||
# See https://w3c.github.io/html/grouping-content.html#the-p-element
|
||||
'address', 'article', 'aside', 'blockquote', 'details', 'div', 'dl',
|
||||
'fieldset', 'figcaption', 'figure', 'footer', 'form', 'h1', 'h2', 'h3',
|
||||
'h4', 'h5', 'h6', 'header', 'hgroup', 'hr', 'main', 'menu', 'nav', 'ol',
|
||||
'p', 'pre', 'section', 'table', 'ul',
|
||||
# Other elements which Markdown should not be mucking up the contents of.
|
||||
'canvas', 'colgroup', 'dd', 'body', 'dt', 'group', 'html', 'iframe', 'li', 'legend',
|
||||
'math', 'map', 'noscript', 'output', 'object', 'option', 'progress', 'script',
|
||||
'style', 'summary', 'tbody', 'td', 'textarea', 'tfoot', 'th', 'thead', 'tr', 'video'
|
||||
]
|
||||
"""
|
||||
List of HTML tags which get treated as block-level elements. Same as the `block_level_elements`
|
||||
attribute of the [`Markdown`][markdown.Markdown] class. Generally one should use the
|
||||
attribute on the class. This remains for compatibility with older extensions.
|
||||
"""
|
||||
|
||||
# Placeholders
|
||||
STX = '\u0002'
|
||||
""" "Start of Text" marker for placeholder templates. """
|
||||
ETX = '\u0003'
|
||||
""" "End of Text" marker for placeholder templates. """
|
||||
INLINE_PLACEHOLDER_PREFIX = STX+"klzzwxh:"
|
||||
""" Prefix for inline placeholder template. """
|
||||
INLINE_PLACEHOLDER = INLINE_PLACEHOLDER_PREFIX + "%s" + ETX
|
||||
""" Placeholder template for stashed inline text. """
|
||||
INLINE_PLACEHOLDER_RE = re.compile(INLINE_PLACEHOLDER % r'([0-9]+)')
|
||||
""" Regular Expression which matches inline placeholders. """
|
||||
AMP_SUBSTITUTE = STX+"amp"+ETX
|
||||
""" Placeholder template for HTML entities. """
|
||||
HTML_PLACEHOLDER = STX + "wzxhzdk:%s" + ETX
|
||||
""" Placeholder template for raw HTML. """
|
||||
HTML_PLACEHOLDER_RE = re.compile(HTML_PLACEHOLDER % r'([0-9]+)')
|
||||
""" Regular expression which matches HTML placeholders. """
|
||||
TAG_PLACEHOLDER = STX + "hzzhzkh:%s" + ETX
|
||||
""" Placeholder template for tags. """
|
||||
|
||||
|
||||
# Constants you probably do not need to change
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
RTL_BIDI_RANGES = (
|
||||
('\u0590', '\u07FF'),
|
||||
# Hebrew (0590-05FF), Arabic (0600-06FF),
|
||||
# Syriac (0700-074F), Arabic supplement (0750-077F),
|
||||
# Thaana (0780-07BF), Nko (07C0-07FF).
|
||||
('\u2D30', '\u2D7F') # Tifinagh
|
||||
)
|
||||
|
||||
|
||||
# AUXILIARY GLOBAL FUNCTIONS
|
||||
# =============================================================================
|
||||
|
||||
|
||||
@lru_cache(maxsize=None)
|
||||
def get_installed_extensions():
|
||||
""" Return all entry_points in the `markdown.extensions` group. """
|
||||
if sys.version_info >= (3, 10):
|
||||
from importlib import metadata
|
||||
else: # `<PY310` use backport
|
||||
import importlib_metadata as metadata
|
||||
# Only load extension entry_points once.
|
||||
return metadata.entry_points(group='markdown.extensions')
|
||||
|
||||
|
||||
def deprecated(message: str, stacklevel: int = 2):
|
||||
"""
|
||||
Raise a [`DeprecationWarning`][] when wrapped function/method is called.
|
||||
|
||||
Usage:
|
||||
|
||||
```python
|
||||
@deprecated("This method will be removed in version X; use Y instead.")
|
||||
def some_method():
|
||||
pass
|
||||
```
|
||||
"""
|
||||
def wrapper(func):
|
||||
@wraps(func)
|
||||
def deprecated_func(*args, **kwargs):
|
||||
warnings.warn(
|
||||
f"'{func.__name__}' is deprecated. {message}",
|
||||
category=DeprecationWarning,
|
||||
stacklevel=stacklevel
|
||||
)
|
||||
return func(*args, **kwargs)
|
||||
return deprecated_func
|
||||
return wrapper
|
||||
|
||||
|
||||
def parseBoolValue(value: str | None, fail_on_errors: bool = True, preserve_none: bool = False) -> bool | None:
|
||||
"""Parses a string representing a boolean value. If parsing was successful,
|
||||
returns `True` or `False`. If `preserve_none=True`, returns `True`, `False`,
|
||||
or `None`. If parsing was not successful, raises `ValueError`, or, if
|
||||
`fail_on_errors=False`, returns `None`."""
|
||||
if not isinstance(value, str):
|
||||
if preserve_none and value is None:
|
||||
return value
|
||||
return bool(value)
|
||||
elif preserve_none and value.lower() == 'none':
|
||||
return None
|
||||
elif value.lower() in ('true', 'yes', 'y', 'on', '1'):
|
||||
return True
|
||||
elif value.lower() in ('false', 'no', 'n', 'off', '0', 'none'):
|
||||
return False
|
||||
elif fail_on_errors:
|
||||
raise ValueError('Cannot parse bool value: %r' % value)
|
||||
|
||||
|
||||
def code_escape(text: str) -> str:
|
||||
"""HTML escape a string of code."""
|
||||
if "&" in text:
|
||||
text = text.replace("&", "&")
|
||||
if "<" in text:
|
||||
text = text.replace("<", "<")
|
||||
if ">" in text:
|
||||
text = text.replace(">", ">")
|
||||
return text
|
||||
|
||||
|
||||
def _get_stack_depth(size=2):
|
||||
"""Get current stack depth, performantly.
|
||||
"""
|
||||
frame = sys._getframe(size)
|
||||
|
||||
for size in count(size):
|
||||
frame = frame.f_back
|
||||
if not frame:
|
||||
return size
|
||||
|
||||
|
||||
def nearing_recursion_limit() -> bool:
|
||||
"""Return true if current stack depth is within 100 of maximum limit."""
|
||||
return sys.getrecursionlimit() - _get_stack_depth() < 100
|
||||
|
||||
|
||||
# MISC AUXILIARY CLASSES
|
||||
# =============================================================================
|
||||
|
||||
|
||||
class AtomicString(str):
|
||||
"""A string which should not be further processed."""
|
||||
pass
|
||||
|
||||
|
||||
class Processor:
|
||||
""" The base class for all processors.
|
||||
|
||||
Attributes:
|
||||
Processor.md: The `Markdown` instance passed in an initialization.
|
||||
|
||||
Arguments:
|
||||
md: The `Markdown` instance this processor is a part of.
|
||||
|
||||
"""
|
||||
def __init__(self, md: Markdown | None = None):
|
||||
self.md = md
|
||||
|
||||
|
||||
class HtmlStash:
|
||||
"""
|
||||
This class is used for stashing HTML objects that we extract
|
||||
in the beginning and replace with place-holders.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
""" Create an `HtmlStash`. """
|
||||
self.html_counter = 0 # for counting inline html segments
|
||||
self.rawHtmlBlocks = []
|
||||
self.tag_counter = 0
|
||||
self.tag_data = [] # list of dictionaries in the order tags appear
|
||||
|
||||
def store(self, html: str) -> str:
|
||||
"""
|
||||
Saves an HTML segment for later reinsertion. Returns a
|
||||
placeholder string that needs to be inserted into the
|
||||
document.
|
||||
|
||||
Keyword arguments:
|
||||
html: An html segment.
|
||||
|
||||
Returns:
|
||||
A placeholder string.
|
||||
|
||||
"""
|
||||
self.rawHtmlBlocks.append(html)
|
||||
placeholder = self.get_placeholder(self.html_counter)
|
||||
self.html_counter += 1
|
||||
return placeholder
|
||||
|
||||
def reset(self) -> None:
|
||||
""" Clear the stash. """
|
||||
self.html_counter = 0
|
||||
self.rawHtmlBlocks = []
|
||||
|
||||
def get_placeholder(self, key: int) -> str:
|
||||
return HTML_PLACEHOLDER % key
|
||||
|
||||
def store_tag(self, tag: str, attrs: list, left_index: int, right_index: int) -> str:
|
||||
"""Store tag data and return a placeholder."""
|
||||
self.tag_data.append({'tag': tag, 'attrs': attrs,
|
||||
'left_index': left_index,
|
||||
'right_index': right_index})
|
||||
placeholder = TAG_PLACEHOLDER % str(self.tag_counter)
|
||||
self.tag_counter += 1 # equal to the tag's index in `self.tag_data`
|
||||
return placeholder
|
||||
|
||||
|
||||
# Used internally by `Registry` for each item in its sorted list.
|
||||
# Provides an easier to read API when editing the code later.
|
||||
# For example, `item.name` is more clear than `item[0]`.
|
||||
class _PriorityItem(NamedTuple):
|
||||
name: str
|
||||
priority: float
|
||||
|
||||
|
||||
class Registry(Generic[_T]):
|
||||
"""
|
||||
A priority sorted registry.
|
||||
|
||||
A `Registry` instance provides two public methods to alter the data of the
|
||||
registry: `register` and `deregister`. Use `register` to add items and
|
||||
`deregister` to remove items. See each method for specifics.
|
||||
|
||||
When registering an item, a "name" and a "priority" must be provided. All
|
||||
items are automatically sorted by "priority" from highest to lowest. The
|
||||
"name" is used to remove ("deregister") and get items.
|
||||
|
||||
A `Registry` instance it like a list (which maintains order) when reading
|
||||
data. You may iterate over the items, get an item and get a count (length)
|
||||
of all items. You may also check that the registry contains an item.
|
||||
|
||||
When getting an item you may use either the index of the item or the
|
||||
string-based "name". For example:
|
||||
|
||||
registry = Registry()
|
||||
registry.register(SomeItem(), 'itemname', 20)
|
||||
# Get the item by index
|
||||
item = registry[0]
|
||||
# Get the item by name
|
||||
item = registry['itemname']
|
||||
|
||||
When checking that the registry contains an item, you may use either the
|
||||
string-based "name", or a reference to the actual item. For example:
|
||||
|
||||
someitem = SomeItem()
|
||||
registry.register(someitem, 'itemname', 20)
|
||||
# Contains the name
|
||||
assert 'itemname' in registry
|
||||
# Contains the item instance
|
||||
assert someitem in registry
|
||||
|
||||
The method `get_index_for_name` is also available to obtain the index of
|
||||
an item using that item's assigned "name".
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self._data: dict[str, _T] = {}
|
||||
self._priority = []
|
||||
self._is_sorted = False
|
||||
|
||||
def __contains__(self, item: str | _T) -> bool:
|
||||
if isinstance(item, str):
|
||||
# Check if an item exists by this name.
|
||||
return item in self._data.keys()
|
||||
# Check if this instance exists.
|
||||
return item in self._data.values()
|
||||
|
||||
def __iter__(self) -> Iterator[_T]:
|
||||
self._sort()
|
||||
return iter([self._data[k] for k, p in self._priority])
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: str | int) -> _T: # pragma: no cover
|
||||
...
|
||||
|
||||
@overload
|
||||
def __getitem__(self, key: slice) -> Registry[_T]: # pragma: no cover
|
||||
...
|
||||
|
||||
def __getitem__(self, key: str | int | slice) -> _T | Registry[_T]:
|
||||
self._sort()
|
||||
if isinstance(key, slice):
|
||||
data: Registry[_T] = Registry()
|
||||
for k, p in self._priority[key]:
|
||||
data.register(self._data[k], k, p)
|
||||
return data
|
||||
if isinstance(key, int):
|
||||
return self._data[self._priority[key].name]
|
||||
return self._data[key]
|
||||
|
||||
def __len__(self) -> int:
|
||||
return len(self._priority)
|
||||
|
||||
def __repr__(self):
|
||||
return '<{}({})>'.format(self.__class__.__name__, list(self))
|
||||
|
||||
def get_index_for_name(self, name: str) -> int:
|
||||
"""
|
||||
Return the index of the given name.
|
||||
"""
|
||||
if name in self:
|
||||
self._sort()
|
||||
return self._priority.index(
|
||||
[x for x in self._priority if x.name == name][0]
|
||||
)
|
||||
raise ValueError('No item named "{}" exists.'.format(name))
|
||||
|
||||
def register(self, item: _T, name: str, priority: float) -> None:
|
||||
"""
|
||||
Add an item to the registry with the given name and priority.
|
||||
|
||||
Arguments:
|
||||
item: The item being registered.
|
||||
name: A string used to reference the item.
|
||||
priority: An integer or float used to sort against all items.
|
||||
|
||||
If an item is registered with a "name" which already exists, the
|
||||
existing item is replaced with the new item. Treat carefully as the
|
||||
old item is lost with no way to recover it. The new item will be
|
||||
sorted according to its priority and will **not** retain the position
|
||||
of the old item.
|
||||
"""
|
||||
if name in self:
|
||||
# Remove existing item of same name first
|
||||
self.deregister(name)
|
||||
self._is_sorted = False
|
||||
self._data[name] = item
|
||||
self._priority.append(_PriorityItem(name, priority))
|
||||
|
||||
def deregister(self, name: str, strict: bool = True) -> None:
|
||||
"""
|
||||
Remove an item from the registry.
|
||||
|
||||
Set `strict=False` to fail silently. Otherwise a [`ValueError`][] is raised for an unknown `name`.
|
||||
"""
|
||||
try:
|
||||
index = self.get_index_for_name(name)
|
||||
del self._priority[index]
|
||||
del self._data[name]
|
||||
except ValueError:
|
||||
if strict:
|
||||
raise
|
||||
|
||||
def _sort(self):
|
||||
"""
|
||||
Sort the registry by priority from highest to lowest.
|
||||
|
||||
This method is called internally and should never be explicitly called.
|
||||
"""
|
||||
if not self._is_sorted:
|
||||
self._priority.sort(key=lambda item: item.priority, reverse=True)
|
||||
self._is_sorted = True
|
@ -1,121 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.40.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<requires lib="webkit2gtk" version="2.28"/>
|
||||
<object class="GtkImage" id="settings_img">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-justify-fill</property>
|
||||
</object>
|
||||
<object class="WebKitSettings" type-func="webkit_settings_get_type" id="web_view_settings">
|
||||
<property name="enable-offline-web-application-cache">False</property>
|
||||
<property name="enable-html5-local-storage">False</property>
|
||||
<property name="enable-html5-database">False</property>
|
||||
<property name="enable-xss-auditor">False</property>
|
||||
<property name="enable-hyperlink-auditing">False</property>
|
||||
<property name="enable-tabs-to-links">False</property>
|
||||
<property name="enable-fullscreen">False</property>
|
||||
<property name="print-backgrounds">False</property>
|
||||
<property name="enable-webaudio">False</property>
|
||||
<property name="enable-page-cache">False</property>
|
||||
<property name="user-agent">Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/15.0 Safari/605.1.15</property>
|
||||
<property name="enable-accelerated-2d-canvas">True</property>
|
||||
<property name="allow-file-access-from-file-urls">True</property>
|
||||
<property name="allow-universal-access-from-file-urls">True</property>
|
||||
<property name="enable-webrtc">True</property>
|
||||
</object>
|
||||
<object class="GtkPopover" id="markdown_preview_dialog">
|
||||
<property name="width-request">620</property>
|
||||
<property name="height-request">480</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="position">left</property>
|
||||
<property name="modal">False</property>
|
||||
<property name="transitions-enabled">False</property>
|
||||
<property name="constrain-to">none</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="hexpand">True</property>
|
||||
<property name="vexpand">True</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">end</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton">
|
||||
<property name="label">gtk-media-pause</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="use-stock">True</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="toggled" handler="_tggle_preview_updates" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="image">settings_img</property>
|
||||
<signal name="clicked" handler="_handle_settings" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkScrolledWindow">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="shadow-type">in</property>
|
||||
<child>
|
||||
<object class="GtkViewport">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="WebKitWebView" type-func="webkit_web_view_get_type" id="markdown_view">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="settings">web_view_settings</property>
|
||||
<property name="is-ephemeral">True</property>
|
||||
<property name="is-muted">True</property>
|
||||
<property name="default-content-security-policy">*</property>
|
||||
<child>
|
||||
<placeholder/>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
@ -1,42 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class MarkdownTemplateMixin:
|
||||
def wrap_html_to_body(self, html):
|
||||
return f"""\
|
||||
<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>Markdown View</title>
|
||||
<style media="screen">
|
||||
html, body {{
|
||||
display: block;
|
||||
background-color: #32383e00;
|
||||
color: #ffffff;
|
||||
text-wrap: wrap;
|
||||
}}
|
||||
|
||||
img {{
|
||||
width: 100%;
|
||||
height: auto;
|
||||
}}
|
||||
|
||||
code {{
|
||||
border: 1px solid #32383e;
|
||||
background-color: #32383e;
|
||||
padding: 4px;
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
{html}
|
||||
</body>
|
||||
</html>
|
||||
|
||||
"""
|
@ -1,114 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
gi.require_version('WebKit2', '4.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import WebKit2
|
||||
|
||||
# Application imports
|
||||
from . import markdown
|
||||
from .markdown_template_mixin import MarkdownTemplateMixin
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
|
||||
class Plugin(MarkdownTemplateMixin, PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Markdown Preview" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self._GLADE_FILE = f"{self.path}/markdown_preview.glade"
|
||||
|
||||
self.is_preview_paused = False
|
||||
self.is_md_file = False
|
||||
|
||||
|
||||
def run(self):
|
||||
WebKit2.WebView() # Need one initialized for webview to work from glade file
|
||||
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(self._GLADE_FILE)
|
||||
self._connect_builder_signals(self, self._builder)
|
||||
|
||||
separator_right = self._ui_objects[0]
|
||||
self._markdown_dialog = self._builder.get_object("markdown_preview_dialog")
|
||||
self._markdown_view = self._builder.get_object("markdown_view")
|
||||
self._web_view_settings = self._builder.get_object("web_view_settings")
|
||||
|
||||
self._markdown_dialog.set_relative_to(separator_right)
|
||||
self._markdown_view.set_settings(self._web_view_settings)
|
||||
self._markdown_view.set_background_color(Gdk.RGBA(0, 0, 0, 0.0))
|
||||
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
...
|
||||
|
||||
def subscribe_to_events(self):
|
||||
self._event_system.subscribe("tggle_markdown_preview", self._tggle_markdown_preview)
|
||||
self._event_system.subscribe("set_active_src_view", self._set_active_src_view)
|
||||
self._event_system.subscribe("buffer_changed", self._do_markdown_translate)
|
||||
|
||||
def _buffer_changed_first_load(self, buffer):
|
||||
self._buffer = buffer
|
||||
|
||||
self._do_markdown_translate(buffer)
|
||||
|
||||
def _set_active_src_view(self, source_view):
|
||||
self._active_src_view = source_view
|
||||
self._buffer = self._active_src_view.get_buffer()
|
||||
|
||||
self._do_markdown_translate(self._buffer)
|
||||
|
||||
def _handle_settings(self, widget = None, eve = None):
|
||||
...
|
||||
|
||||
def _tggle_preview_updates(self, widget = None, eve = None):
|
||||
self.is_preview_paused = not self.is_preview_paused
|
||||
widget.set_active(self.is_preview_paused)
|
||||
|
||||
if not self.is_preview_paused:
|
||||
self._do_markdown_translate(self._buffer)
|
||||
|
||||
def _tggle_markdown_preview(self, widget = None, eve = None):
|
||||
if not self._active_src_view: return
|
||||
|
||||
is_visible = self._markdown_dialog.is_visible()
|
||||
buffer = self._active_src_view.get_buffer()
|
||||
data = None
|
||||
|
||||
if not is_visible:
|
||||
self._markdown_dialog.popup();
|
||||
self._do_markdown_translate(buffer)
|
||||
elif not data and is_visible:
|
||||
self._markdown_dialog.popdown()
|
||||
|
||||
def _do_markdown_translate(self, buffer):
|
||||
if self.is_preview_paused: return
|
||||
|
||||
self.is_markdown_check()
|
||||
is_visible = self._markdown_dialog.is_visible()
|
||||
if not is_visible or not self.is_md_file: return
|
||||
self.render_markdown(buffer)
|
||||
|
||||
def render_markdown(self, buffer):
|
||||
start_iter = buffer.get_start_iter()
|
||||
end_iter = buffer.get_end_iter()
|
||||
text = buffer.get_text(start_iter, end_iter, include_hidden_chars = False)
|
||||
html = markdown.markdown(text)
|
||||
|
||||
path = self._active_src_view.get_current_file().get_parent().get_path()
|
||||
data = self.wrap_html_to_body(html)
|
||||
self._markdown_view.load_html(content = data, base_uri = f"file://{path}/")
|
||||
|
||||
def is_markdown_check(self):
|
||||
self.is_md_file = self._active_src_view.get_filetype() == "markdown"
|
||||
if not self.is_md_file:
|
||||
data = self.wrap_html_to_body("<h1>Not a Markdown file...</h1>")
|
||||
self._markdown_view.load_html(content = data, base_uri = None)
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,14 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Search/Replace",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"pass_events": "true",
|
||||
"pass_ui_objects": ["separator_botton"],
|
||||
"bind_keys": ["Search/Replace||tggl_search_replace:<Control>f"]
|
||||
|
||||
}
|
||||
}
|
||||
}
|
@ -1,221 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
import re
|
||||
import threading
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
from .styling_mixin import StylingMixin
|
||||
from .replace_mixin import ReplaceMixin
|
||||
|
||||
|
||||
|
||||
class Plugin(StylingMixin, ReplaceMixin, PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Search/Replace" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self.path = os.path.dirname(os.path.realpath(__file__))
|
||||
self._GLADE_FILE = f"{self.path}/search_replace.glade"
|
||||
|
||||
self._search_replace_dialog = None
|
||||
self._find_entry = None
|
||||
self._replace_entry = None
|
||||
self._active_src_view = None
|
||||
self._buffer = None
|
||||
self._tag_table = None
|
||||
|
||||
self.use_regex = False
|
||||
self.use_case_sensitive = False
|
||||
self.search_only_in_selection = False
|
||||
self.use_whole_word_search = False
|
||||
|
||||
self.timer = None
|
||||
self.search_time = 0.35
|
||||
self.find_text = ""
|
||||
self.search_tag = "search_tag"
|
||||
self.highlight_color = "#FBF719"
|
||||
self.text_color = "#000000"
|
||||
self.alpha_num_under = re.compile(r"[a-zA-Z0-9_]")
|
||||
|
||||
|
||||
def run(self):
|
||||
self._builder = Gtk.Builder()
|
||||
self._builder.add_from_file(self._GLADE_FILE)
|
||||
self._connect_builder_signals(self, self._builder)
|
||||
|
||||
separator_botton = self._ui_objects[0]
|
||||
self._search_replace_dialog = self._builder.get_object("search_replace_dialog")
|
||||
self._find_status_lbl = self._builder.get_object("find_status_lbl")
|
||||
self._find_options_lbl = self._builder.get_object("find_options_lbl")
|
||||
|
||||
self._find_entry = self._builder.get_object("find_entry")
|
||||
self._replace_entry = self._builder.get_object("replace_entry")
|
||||
|
||||
self._search_replace_dialog.set_relative_to(separator_botton)
|
||||
self._search_replace_dialog.set_hexpand(True)
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
...
|
||||
|
||||
def subscribe_to_events(self):
|
||||
self._event_system.subscribe("tggl_search_replace", self._tggl_search_replace)
|
||||
self._event_system.subscribe("set_active_src_view", self._set_active_src_view)
|
||||
|
||||
def _set_active_src_view(self, source_view):
|
||||
self._active_src_view = source_view
|
||||
self._buffer = self._active_src_view.get_buffer()
|
||||
self._tag_table = self._buffer.get_tag_table()
|
||||
self.search_for_string(self._find_entry)
|
||||
|
||||
def _show_search_replace(self, widget = None, eve = None):
|
||||
self._search_replace_dialog.popup()
|
||||
|
||||
def _tggl_search_replace(self, widget = None, eve = None):
|
||||
is_visible = self._search_replace_dialog.is_visible()
|
||||
buffer = self._active_src_view.get_buffer()
|
||||
data = None
|
||||
|
||||
if buffer.get_has_selection():
|
||||
start, end = buffer.get_selection_bounds()
|
||||
data = buffer.get_text(start, end, include_hidden_chars = False)
|
||||
|
||||
if data:
|
||||
self._find_entry.set_text(data)
|
||||
|
||||
if not is_visible:
|
||||
self._search_replace_dialog.popup();
|
||||
self._find_entry.grab_focus()
|
||||
elif not data and is_visible:
|
||||
self._search_replace_dialog.popdown()
|
||||
self._find_entry.set_text("")
|
||||
else:
|
||||
self._find_entry.grab_focus()
|
||||
|
||||
|
||||
def get_search_tag(self, buffer):
|
||||
tag_table = buffer.get_tag_table()
|
||||
search_tag = tag_table.lookup(self.search_tag)
|
||||
if not search_tag:
|
||||
search_tag = buffer.create_tag(self.search_tag, background = self.highlight_color, foreground = self.text_color)
|
||||
|
||||
buffer.remove_tag_by_name(self.search_tag, buffer.get_start_iter(), buffer.get_end_iter())
|
||||
return search_tag
|
||||
|
||||
|
||||
def cancel_timer(self):
|
||||
if self.timer:
|
||||
self.timer.cancel()
|
||||
GLib.idle_remove_by_data(None)
|
||||
|
||||
def delay_search_glib(self):
|
||||
GLib.idle_add(self._do_highlight)
|
||||
|
||||
def delay_search(self):
|
||||
wait_time = self.search_time / len(self.find_text)
|
||||
wait_time = max(wait_time, 0.05)
|
||||
|
||||
self.timer = threading.Timer(wait_time, self.delay_search_glib)
|
||||
self.timer.daemon = True
|
||||
self.timer.start()
|
||||
|
||||
|
||||
def on_enter_search(self, widget, eve):
|
||||
text = widget.get_text()
|
||||
if not text: return
|
||||
|
||||
keyname = Gdk.keyval_name(eve.keyval)
|
||||
if keyname == "Return":
|
||||
self.find_next(widget)
|
||||
|
||||
def search_for_string(self, widget):
|
||||
self.cancel_timer()
|
||||
|
||||
self.find_text = widget.get_text()
|
||||
if len(self.find_text) > 0 and len(self.find_text) < 5:
|
||||
self.delay_search()
|
||||
else:
|
||||
self._do_highlight(self.find_text)
|
||||
|
||||
|
||||
def _do_highlight(self, query = None):
|
||||
query = self.find_text if not query else query
|
||||
buffer = self._active_src_view.get_buffer()
|
||||
# Also clears tag from buffer so if no query we're clean in ui
|
||||
search_tag = self.get_search_tag(buffer)
|
||||
|
||||
self.update_style(1)
|
||||
if not query:
|
||||
self._find_status_lbl.set_label(f"Find in current buffer")
|
||||
self.update_style(0)
|
||||
return
|
||||
|
||||
start_itr = buffer.get_start_iter()
|
||||
end_itr = buffer.get_end_iter()
|
||||
|
||||
results, total_count = self.search(start_itr, query)
|
||||
self._update_status_lbl(total_count, query)
|
||||
for start, end in results:
|
||||
buffer.apply_tag(search_tag, start, end)
|
||||
|
||||
def search(self, start_itr = None, query = None, limit = None):
|
||||
if not start_itr or not query: return None, None
|
||||
|
||||
flags = Gtk.TextSearchFlags.VISIBLE_ONLY | Gtk.TextSearchFlags.TEXT_ONLY
|
||||
if not self.use_case_sensitive:
|
||||
flags = flags | Gtk.TextSearchFlags.CASE_INSENSITIVE
|
||||
|
||||
if self.search_only_in_selection and self._buffer.get_has_selection():
|
||||
start_itr, limit = self._buffer.get_selection_bounds()
|
||||
|
||||
_results = []
|
||||
while True:
|
||||
result = start_itr.forward_search(query, flags, limit)
|
||||
if not result: break
|
||||
|
||||
_results.append(result)
|
||||
start_itr = result[1]
|
||||
|
||||
results = self.apply_filters(_results, query)
|
||||
return results, len(results)
|
||||
|
||||
def apply_filters(self, _results, query):
|
||||
results = []
|
||||
for start, end in _results:
|
||||
text = self._buffer.get_slice(start, end, include_hidden_chars = False)
|
||||
if self.use_whole_word_search:
|
||||
if not self.is_whole_word(start, end):
|
||||
continue
|
||||
|
||||
results.append([start, end])
|
||||
|
||||
return results
|
||||
|
||||
def find_next(self, widget, eve = None, use_data = None):
|
||||
mark = self._buffer.get_insert()
|
||||
iter = self._buffer.get_iter_at_mark(mark)
|
||||
iter.forward_line()
|
||||
|
||||
search_tag = self._tag_table.lookup(self.search_tag)
|
||||
next_tag_found = iter.forward_to_tag_toggle(search_tag)
|
||||
if not next_tag_found:
|
||||
self._buffer.place_cursor( self._buffer.get_start_iter() )
|
||||
mark = self._buffer.get_insert()
|
||||
iter = self._buffer.get_iter_at_mark(mark)
|
||||
iter.forward_to_tag_toggle(search_tag)
|
||||
|
||||
self._buffer.place_cursor(iter)
|
||||
self._active_src_view.scroll_to_mark( self._buffer.get_insert(), 0.0, True, 0.0, 0.0 )
|
||||
|
||||
|
||||
def find_all(self, widget):
|
||||
...
|
@ -1,94 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class ReplaceMixin:
|
||||
def replace(self, widget):
|
||||
replace_text = self._replace_entry.get_text()
|
||||
if self.find_text and replace_text:
|
||||
self._buffer.begin_user_action()
|
||||
|
||||
iter = self._buffer.get_start_iter()
|
||||
search_tag = self._tag_table.lookup(self.search_tag)
|
||||
|
||||
iter.forward_to_tag_toggle(search_tag)
|
||||
self._do_replace(iter, replace_text)
|
||||
self._active_src_view.scroll_to_iter( iter, 0.0, True, 0.0, 0.0 )
|
||||
|
||||
self._buffer.end_user_action()
|
||||
|
||||
def replace_all(self, widget):
|
||||
replace_text = self._replace_entry.get_text()
|
||||
if self.find_text:
|
||||
self._buffer.begin_user_action()
|
||||
|
||||
mark = self._buffer.get_insert()
|
||||
iter = self._buffer.get_start_iter()
|
||||
search_tag = self._tag_table.lookup(self.search_tag)
|
||||
|
||||
while iter.forward_to_tag_toggle(search_tag):
|
||||
self._do_replace(iter, replace_text)
|
||||
iter = self._buffer.get_start_iter()
|
||||
|
||||
self._buffer.end_user_action()
|
||||
|
||||
|
||||
def _do_replace(self, iter, text):
|
||||
start, end = self.get_start_end(iter)
|
||||
self.replace_in_buffer(start, end, text)
|
||||
|
||||
def replace_in_buffer(self, start, end, text):
|
||||
pos_mark = self._buffer.create_mark("find-replace", end, True)
|
||||
self._buffer.delete(start, end)
|
||||
replace_iter = self._buffer.get_iter_at_mark(pos_mark)
|
||||
self._buffer.insert(replace_iter, text)
|
||||
|
||||
def get_start_end(self, iter):
|
||||
start = iter.copy()
|
||||
end = None
|
||||
|
||||
while True:
|
||||
iter.forward_char()
|
||||
tags = iter.get_tags()
|
||||
valid = False
|
||||
for tag in tags:
|
||||
if tag.props.name and self.search_tag in tag.props.name:
|
||||
valid = True
|
||||
break
|
||||
|
||||
if valid:
|
||||
continue
|
||||
|
||||
end = iter.copy()
|
||||
break
|
||||
|
||||
return start, end
|
||||
|
||||
# NOTE: Below, lovingly taken from Hamad Al Marri's Gamma text editor.
|
||||
# Link: https://gitlab.com/hamadmarri/gamma-text-editor
|
||||
def is_whole_word(self, match_start, match_end):
|
||||
is_prev_a_char = True
|
||||
is_next_a_char = True
|
||||
|
||||
prev_iter = match_start.copy()
|
||||
next_iter = match_end.copy()
|
||||
if not prev_iter.backward_char():
|
||||
is_prev_a_char = False
|
||||
else:
|
||||
c = prev_iter.get_char()
|
||||
is_prev_a_char = (c.isalpha() or c.isdigit())
|
||||
|
||||
if not next_iter:
|
||||
is_next_a_char = False
|
||||
else:
|
||||
c = next_iter.get_char()
|
||||
is_next_a_char = (c.isalpha() or c.isdigit())
|
||||
|
||||
is_word = (not is_prev_a_char and not is_next_a_char)
|
||||
|
||||
# Note: Both must be false to be a word...
|
||||
return is_word
|
@ -1,299 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- Generated with glade 3.40.0 -->
|
||||
<interface>
|
||||
<requires lib="gtk+" version="3.24"/>
|
||||
<object class="GtkImage" id="close_img">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="stock">gtk-close</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="only-in-selection">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="pixbuf">../../icons/only-in-selection.png</property>
|
||||
</object>
|
||||
<object class="GtkImage" id="whole-word">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="pixbuf">../../icons/whole-word.png</property>
|
||||
</object>
|
||||
<object class="GtkPopover" id="search_replace_dialog">
|
||||
<property name="can-focus">False</property>
|
||||
<property name="modal">False</property>
|
||||
<property name="transitions-enabled">False</property>
|
||||
<property name="constrain-to">none</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="orientation">vertical</property>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="find_status_lbl">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="label" translatable="yes">Find in Current Buffer</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<child>
|
||||
<object class="GtkLabel" id="find_options_lbl">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="xpad">20</property>
|
||||
<property name="label" translatable="yes">Finding with Options: Case Insensitive</property>
|
||||
<property name="xalign">0</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButtonBox">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="layout-style">start</property>
|
||||
<child>
|
||||
<object class="GtkToggleButton">
|
||||
<property name="label" translatable="yes">.*</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="sensitive">False</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Use Regex</property>
|
||||
<signal name="toggled" handler="tggle_regex" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton">
|
||||
<property name="label" translatable="yes">Aa</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Match Case</property>
|
||||
<signal name="toggled" handler="tggle_case_sensitive" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Only In Selection</property>
|
||||
<property name="image">only-in-selection</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="toggled" handler="tggle_selection_only_scan" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">2</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkToggleButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Whole Word</property>
|
||||
<property name="image">whole-word</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="toggled" handler="tggle_whole_word_search" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Close Panel</property>
|
||||
<property name="image">close_img</property>
|
||||
<property name="always-show-image">True</property>
|
||||
<signal name="clicked" handler="_tggl_search_replace" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">True</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">4</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="position">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<!-- n-columns=10 n-rows=2 -->
|
||||
<object class="GtkGrid">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">False</property>
|
||||
<property name="column-homogeneous">True</property>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">Replace All</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Replace All</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<signal name="clicked" handler="replace_all" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">9</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">Replace</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="tooltip-text" translatable="yes">Replace Next</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">10</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<signal name="clicked" handler="replace" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">8</property>
|
||||
<property name="top-attach">1</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">Find All</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<signal name="clicked" handler="find_all" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">9</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkButton">
|
||||
<property name="label" translatable="yes">Find</property>
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="focus-on-click">False</property>
|
||||
<property name="receives-default">True</property>
|
||||
<property name="margin-start">5</property>
|
||||
<property name="margin-end">5</property>
|
||||
<property name="margin-top">5</property>
|
||||
<property name="margin-bottom">5</property>
|
||||
<signal name="clicked" handler="find_next" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">8</property>
|
||||
<property name="top-attach">0</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="find_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<property name="placeholder-text" translatable="yes">Find in current buffer</property>
|
||||
<signal name="key-release-event" handler="on_enter_search" swapped="no"/>
|
||||
<signal name="search-changed" handler="search_for_string" swapped="no"/>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">0</property>
|
||||
<property name="width">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
<child>
|
||||
<object class="GtkSearchEntry" id="replace_entry">
|
||||
<property name="visible">True</property>
|
||||
<property name="can-focus">True</property>
|
||||
<property name="primary-icon-name">edit-find-symbolic</property>
|
||||
<property name="primary-icon-activatable">False</property>
|
||||
<property name="primary-icon-sensitive">False</property>
|
||||
<property name="placeholder-text" translatable="yes">Replace in current buffer</property>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="left-attach">0</property>
|
||||
<property name="top-attach">1</property>
|
||||
<property name="width">8</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
<packing>
|
||||
<property name="expand">False</property>
|
||||
<property name="fill">True</property>
|
||||
<property name="padding">10</property>
|
||||
<property name="position">3</property>
|
||||
</packing>
|
||||
</child>
|
||||
</object>
|
||||
</child>
|
||||
</object>
|
||||
</interface>
|
@ -1,66 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
|
||||
class StylingMixin:
|
||||
def tggle_regex(self, widget):
|
||||
self.use_regex = not widget.get_active()
|
||||
self._set_find_options_lbl()
|
||||
self.search_for_string(self._find_entry)
|
||||
|
||||
def tggle_case_sensitive(self, widget):
|
||||
self.use_case_sensitive = widget.get_active()
|
||||
self._set_find_options_lbl()
|
||||
self.search_for_string(self._find_entry)
|
||||
|
||||
def tggle_selection_only_scan(self, widget):
|
||||
self.search_only_in_selection = widget.get_active()
|
||||
self._set_find_options_lbl()
|
||||
self.search_for_string(self._find_entry)
|
||||
|
||||
def tggle_whole_word_search(self, widget):
|
||||
self.use_whole_word_search = widget.get_active()
|
||||
self._set_find_options_lbl()
|
||||
self.search_for_string(self._find_entry)
|
||||
|
||||
def _set_find_options_lbl(self):
|
||||
find_options = "Finding with Options: "
|
||||
|
||||
if self.use_regex:
|
||||
find_options += "Regex"
|
||||
|
||||
find_options += ", " if self.use_regex else ""
|
||||
find_options += "Case Sensitive" if self.use_case_sensitive else "Case Inensitive"
|
||||
|
||||
if self.search_only_in_selection:
|
||||
find_options += ", Within Current Selection"
|
||||
|
||||
if self.use_whole_word_search:
|
||||
find_options += ", Whole Word"
|
||||
|
||||
self._find_options_lbl.set_label(find_options)
|
||||
|
||||
def update_style(self, state):
|
||||
self._find_entry.get_style_context().remove_class("searching")
|
||||
self._find_entry.get_style_context().remove_class("search_success")
|
||||
self._find_entry.get_style_context().remove_class("search_fail")
|
||||
|
||||
if state == 0:
|
||||
self._find_entry.get_style_context().add_class("searching")
|
||||
elif state == 1:
|
||||
self._find_entry.get_style_context().add_class("search_success")
|
||||
elif state == 2:
|
||||
self._find_entry.get_style_context().add_class("search_fail")
|
||||
|
||||
def _update_status_lbl(self, total_count: int = 0, query: str = None):
|
||||
if not query: return
|
||||
|
||||
count = total_count if total_count > 0 else "No"
|
||||
plural = "s" if total_count > 1 else ""
|
||||
|
||||
if total_count == 0: self.update_style(2)
|
||||
self._find_status_lbl.set_label(f"{count} results{plural} found for '{query}'")
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,8 +0,0 @@
|
||||
"""
|
||||
A Coffescript Object Notation (CSON) parser for Python 2 and Python 3.
|
||||
See documentation at https://github.com/avaka/pycson
|
||||
"""
|
||||
|
||||
from .parser import load, loads
|
||||
from .writer import dump, dumps
|
||||
from .speg import ParseError
|
@ -1,295 +0,0 @@
|
||||
from .speg import peg
|
||||
import re, sys
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
_chr = unichr
|
||||
else:
|
||||
_chr = chr
|
||||
|
||||
def load(fin):
|
||||
return loads(fin.read())
|
||||
|
||||
def loads(s):
|
||||
if isinstance(s, bytes):
|
||||
s = s.decode('utf-8')
|
||||
if s.startswith(u'\ufeff'):
|
||||
s = s[1:]
|
||||
return peg(s.replace('\r\n', '\n'), _p_root)
|
||||
|
||||
def _p_ws(p):
|
||||
p('[ \t]*')
|
||||
|
||||
def _p_nl(p):
|
||||
p(r'([ \t]*(?:#[^\n]*)?\r?\n)+')
|
||||
|
||||
def _p_ews(p):
|
||||
with p:
|
||||
p(_p_nl)
|
||||
p(_p_ws)
|
||||
|
||||
def _p_id(p):
|
||||
return p(r'[$a-zA-Z_][$0-9a-zA-Z_]*')
|
||||
|
||||
_escape_table = {
|
||||
'r': '\r',
|
||||
'n': '\n',
|
||||
't': '\t',
|
||||
'f': '\f',
|
||||
'b': '\b',
|
||||
}
|
||||
def _p_unescape(p):
|
||||
esc = p('\\\\(?:u[0-9a-fA-F]{4}|[^\n])')
|
||||
if esc[1] == 'u':
|
||||
return _chr(int(esc[2:], 16))
|
||||
return _escape_table.get(esc[1:], esc[1:])
|
||||
|
||||
_re_indent = re.compile(r'[ \t]*')
|
||||
def _p_block_str(p, c):
|
||||
p(r'{c}{c}{c}'.format(c=c))
|
||||
lines = [['']]
|
||||
with p:
|
||||
while True:
|
||||
s = p(r'(?:{c}(?!{c}{c})|[^{c}\\])*'.format(c=c))
|
||||
l = s.split('\n')
|
||||
lines[-1].append(l[0])
|
||||
lines.extend([x] for x in l[1:])
|
||||
if p(r'(?:\\\n[ \t]*)*'):
|
||||
continue
|
||||
p.commit()
|
||||
lines[-1].append(p(_p_unescape))
|
||||
p(r'{c}{c}{c}'.format(c=c))
|
||||
|
||||
lines = [''.join(l) for l in lines]
|
||||
strip_ws = len(lines) > 1
|
||||
if strip_ws and all(c in ' \t' for c in lines[-1]):
|
||||
lines.pop()
|
||||
|
||||
indent = None
|
||||
for line in lines[1:]:
|
||||
if not line:
|
||||
continue
|
||||
if indent is None:
|
||||
indent = _re_indent.match(line).group(0)
|
||||
continue
|
||||
for i, (c1, c2) in enumerate(zip(indent, line)):
|
||||
if c1 != c2:
|
||||
indent = indent[:i]
|
||||
break
|
||||
|
||||
ind_len = len(indent or '')
|
||||
if strip_ws and all(c in ' \t' for c in lines[0]):
|
||||
lines = [line[ind_len:] for line in lines[1:]]
|
||||
else:
|
||||
lines[1:] = [line[ind_len:] for line in lines[1:]]
|
||||
|
||||
return '\n'.join(lines)
|
||||
|
||||
_re_mstr_nl = re.compile(r'(?:[ \t]*\n)+[ \t]*')
|
||||
_re_mstr_trailing_nl = re.compile(_re_mstr_nl.pattern + r'\Z')
|
||||
def _p_multiline_str(p, c):
|
||||
p('{c}(?!{c}{c})(?:[ \t]*\n[ \t]*)?'.format(c=c))
|
||||
string_parts = []
|
||||
with p:
|
||||
while True:
|
||||
string_parts.append(p(r'[^{c}\\]*'.format(c=c)))
|
||||
if p(r'(?:\\\n[ \t]*)*'):
|
||||
string_parts.append('')
|
||||
continue
|
||||
p.commit()
|
||||
string_parts.append(p(_p_unescape))
|
||||
p(c)
|
||||
string_parts[-1] = _re_mstr_trailing_nl.sub('', string_parts[-1])
|
||||
string_parts[::2] = [_re_mstr_nl.sub(' ', part) for part in string_parts[::2]]
|
||||
return ''.join(string_parts)
|
||||
|
||||
def _p_string(p):
|
||||
with p:
|
||||
return p(_p_block_str, '"')
|
||||
with p:
|
||||
return p(_p_block_str, "'")
|
||||
with p:
|
||||
return p(_p_multiline_str, '"')
|
||||
return p(_p_multiline_str, "'")
|
||||
|
||||
def _p_array_value(p):
|
||||
with p:
|
||||
p(_p_nl)
|
||||
return p(_p_object)
|
||||
with p:
|
||||
p(_p_ws)
|
||||
return p(_p_line_object)
|
||||
p(_p_ews)
|
||||
return p(_p_simple_value)
|
||||
|
||||
def _p_key(p):
|
||||
with p:
|
||||
return p(_p_id)
|
||||
return p(_p_string)
|
||||
|
||||
def _p_flow_kv(p):
|
||||
k = p(_p_key)
|
||||
p(_p_ews)
|
||||
p(':')
|
||||
with p:
|
||||
p(_p_nl)
|
||||
return k, p(_p_object)
|
||||
with p:
|
||||
p(_p_ws)
|
||||
return k, p(_p_line_object)
|
||||
p(_p_ews)
|
||||
return k, p(_p_simple_value)
|
||||
|
||||
def _p_flow_obj_sep(p):
|
||||
with p:
|
||||
p(_p_ews)
|
||||
p(',')
|
||||
p(_p_ews)
|
||||
return
|
||||
|
||||
p(_p_nl)
|
||||
p(_p_ws)
|
||||
|
||||
def _p_simple_value(p):
|
||||
with p:
|
||||
p('null')
|
||||
return None
|
||||
|
||||
with p:
|
||||
p('false')
|
||||
return False
|
||||
with p:
|
||||
p('true')
|
||||
return True
|
||||
|
||||
with p:
|
||||
return int(p('0b[01]+')[2:], 2)
|
||||
with p:
|
||||
return int(p('0o[0-7]+')[2:], 8)
|
||||
with p:
|
||||
return int(p('0x[0-9a-fA-F]+')[2:], 16)
|
||||
with p:
|
||||
return float(p(r'-?(?:[1-9][0-9]*|0)?\.[0-9]+(?:[Ee][\+-]?[0-9]+)?|(?:[1-9][0-9]*|0)(?:\.[0-9]+)?[Ee][\+-]?[0-9]+'))
|
||||
with p:
|
||||
return int(p('-?[1-9][0-9]*|0'), 10)
|
||||
|
||||
with p:
|
||||
return p(_p_string)
|
||||
|
||||
with p:
|
||||
p(r'\[')
|
||||
r = []
|
||||
with p:
|
||||
p.set('I', '')
|
||||
r.append(p(_p_array_value))
|
||||
with p:
|
||||
while True:
|
||||
with p:
|
||||
p(_p_ews)
|
||||
p(',')
|
||||
rr = p(_p_array_value)
|
||||
if not p:
|
||||
p(_p_nl)
|
||||
with p:
|
||||
rr = p(_p_object)
|
||||
if not p:
|
||||
p(_p_ews)
|
||||
rr = p(_p_simple_value)
|
||||
r.append(rr)
|
||||
p.commit()
|
||||
with p:
|
||||
p(_p_ews)
|
||||
p(',')
|
||||
p(_p_ews)
|
||||
p(r'\]')
|
||||
return r
|
||||
|
||||
p(r'\{')
|
||||
|
||||
r = {}
|
||||
p(_p_ews)
|
||||
with p:
|
||||
p.set('I', '')
|
||||
k, v = p(_p_flow_kv)
|
||||
r[k] = v
|
||||
with p:
|
||||
while True:
|
||||
p(_p_flow_obj_sep)
|
||||
k, v = p(_p_flow_kv)
|
||||
r[k] = v
|
||||
p.commit()
|
||||
p(_p_ews)
|
||||
with p:
|
||||
p(',')
|
||||
p(_p_ews)
|
||||
p(r'\}')
|
||||
return r
|
||||
|
||||
def _p_line_kv(p):
|
||||
k = p(_p_key)
|
||||
p(_p_ws)
|
||||
p(':')
|
||||
p(_p_ws)
|
||||
with p:
|
||||
p(_p_nl)
|
||||
p(p.get('I'))
|
||||
return k, p(_p_indented_object)
|
||||
with p:
|
||||
return k, p(_p_line_object)
|
||||
with p:
|
||||
return k, p(_p_simple_value)
|
||||
p(_p_nl)
|
||||
p(p.get('I'))
|
||||
p('[ \t]')
|
||||
p(_p_ws)
|
||||
return k, p(_p_simple_value)
|
||||
|
||||
def _p_line_object(p):
|
||||
k, v = p(_p_line_kv)
|
||||
r = { k: v }
|
||||
with p:
|
||||
while True:
|
||||
p(_p_ws)
|
||||
p(',')
|
||||
p(_p_ws)
|
||||
k, v = p(_p_line_kv)
|
||||
r[k] = v # uniqueness
|
||||
p.commit()
|
||||
return r
|
||||
|
||||
def _p_object(p):
|
||||
p.set('I', p.get('I') + p('[ \t]*'))
|
||||
r = p(_p_line_object)
|
||||
with p:
|
||||
while True:
|
||||
p(_p_ws)
|
||||
with p:
|
||||
p(',')
|
||||
p(_p_nl)
|
||||
p(p.get('I'))
|
||||
rr = p(_p_line_object)
|
||||
r.update(rr) # unqueness
|
||||
p.commit()
|
||||
return r
|
||||
|
||||
def _p_indented_object(p):
|
||||
p.set('I', p.get('I') + p('[ \t]'))
|
||||
return p(_p_object)
|
||||
|
||||
def _p_root(p):
|
||||
with p:
|
||||
p(_p_nl)
|
||||
|
||||
with p:
|
||||
p.set('I', '')
|
||||
r = p(_p_object)
|
||||
p(_p_ws)
|
||||
with p:
|
||||
p(',')
|
||||
|
||||
if not p:
|
||||
p(_p_ws)
|
||||
r = p(_p_simple_value)
|
||||
|
||||
p(_p_ews)
|
||||
p(p.eof)
|
||||
return r
|
@ -1,15 +0,0 @@
|
||||
from .errors import ParseError, ExpectedExpr, UnexpectedExpr, SemanticFailure
|
||||
from .peg import Parser
|
||||
from .rules import eof
|
||||
|
||||
def peg(text, root_rule):
|
||||
p = Parser(text)
|
||||
return p(root_rule)
|
||||
|
||||
def parse(text, root_rule):
|
||||
p = Parser(text)
|
||||
return p.parse(root_rule)
|
||||
|
||||
def hidden(fn):
|
||||
fn._speg_hidden = True
|
||||
return fn
|
@ -1,66 +0,0 @@
|
||||
from .rules import rule_to_str
|
||||
|
||||
class ExpectedExpr:
|
||||
def __init__(self, expr, callstack):
|
||||
self.expr = expr
|
||||
self.callstack = callstack
|
||||
|
||||
class UnexpectedExpr:
|
||||
def __init__(self, end_pos, rule, callstack):
|
||||
self.end_pos = end_pos
|
||||
self.rule = rule
|
||||
self.callstack = callstack
|
||||
|
||||
class SemanticFailure:
|
||||
def __init__(self, args, kw, callstack):
|
||||
self.args = args
|
||||
self.kw = kw
|
||||
self.callstack = callstack
|
||||
|
||||
class ParseError(RuntimeError):
|
||||
def __init__(self, message, text, start_pos, end_pos, failures):
|
||||
self.message = message
|
||||
self.text = text
|
||||
self.start_pos = start_pos
|
||||
self.end_pos = end_pos
|
||||
self.failures = failures
|
||||
|
||||
def __str__(self):
|
||||
return 'at {}:{}: {}'.format(
|
||||
self.start_pos.line, self.start_pos.col, self.message)
|
||||
|
||||
def _first(iterable):
|
||||
return next(iterable, None)
|
||||
|
||||
def raise_parsing_error(text, position, failures):
|
||||
end_pos = position
|
||||
msg = []
|
||||
|
||||
sema = _first(f for f in failures if isinstance(f, SemanticFailure))
|
||||
if sema is not None:
|
||||
msg.append(sema.args[0])
|
||||
else:
|
||||
unexps = [f for f in failures if isinstance(f, UnexpectedExpr)]
|
||||
if unexps:
|
||||
unexp = min(unexps,
|
||||
key=lambda f: f.end_pos.offset - position.offset)
|
||||
end_pos = unexp.end_pos
|
||||
msg.append('unexpected {}'.format(rule_to_str(unexp.rule)))
|
||||
|
||||
exps = [f for f in failures if isinstance(f, ExpectedExpr)]
|
||||
if exps:
|
||||
exp_syms = set()
|
||||
for f in exps:
|
||||
r = _first(se.fn for se in f.callstack
|
||||
if se.position == position and not getattr(se.fn, '_speg_hidden', False))
|
||||
if r is None:
|
||||
r = f.expr
|
||||
exp_syms.add(rule_to_str(r))
|
||||
exp_strs = sorted(exp_syms)
|
||||
|
||||
if len(exp_strs) == 1:
|
||||
msg.append('expected {}'.format(exp_strs[0]))
|
||||
else:
|
||||
msg.append('expected one of {}'.format(', '.join(exp_strs)))
|
||||
|
||||
raise ParseError('; '.join(msg), text, position, end_pos, failures)
|
@ -1,195 +0,0 @@
|
||||
import re
|
||||
import sys
|
||||
|
||||
import six
|
||||
|
||||
from .errors import ExpectedExpr, UnexpectedExpr, SemanticFailure, raise_parsing_error
|
||||
from .position import Position, get_line_at_position
|
||||
from .rules import eof
|
||||
|
||||
class _ParseBacktrackError(BaseException):
|
||||
pass
|
||||
|
||||
class _PegState:
|
||||
def __init__(self, position):
|
||||
self.position = position
|
||||
self.vars = {}
|
||||
self.committed = False
|
||||
self.failure_pos = None
|
||||
self.failures = None
|
||||
|
||||
class Node:
|
||||
def __init__(self, value, start_pos, end_pos):
|
||||
self.value = value
|
||||
self.start_pos = start_pos
|
||||
self.end_pos = end_pos
|
||||
|
||||
class Parser:
|
||||
def __init__(self, text):
|
||||
self._text = text
|
||||
|
||||
def __call__(self, r, *args, **kw):
|
||||
return self._parse(lambda p: p(r, *args, **kw))
|
||||
|
||||
def parse(self, r, *args, **kw):
|
||||
return self._parse(lambda p: p.consume(r, *args, **kw))
|
||||
|
||||
def _parse(self, fn):
|
||||
p = ParsingState(self._text)
|
||||
try:
|
||||
return fn(p)
|
||||
except _ParseBacktrackError:
|
||||
assert len(p._states) == 1
|
||||
assert p._states[0].failure_pos is not None
|
||||
raise_parsing_error(self._text, p._states[0].failure_pos, p._states[0].failures)
|
||||
|
||||
class CallstackEntry:
|
||||
def __init__(self, position, fn, args, kw):
|
||||
self.position = position
|
||||
self.fn = fn
|
||||
self.args = args
|
||||
self.kw = kw
|
||||
|
||||
def __repr__(self):
|
||||
return '{} at {}:{}'.format(self.fn.__name__, self.position.line, self.position.col)
|
||||
|
||||
class ParsingState(object):
|
||||
def __init__(self, s):
|
||||
self._s = s
|
||||
self._states = [_PegState(Position())]
|
||||
self._re_cache = {}
|
||||
self._callstack = []
|
||||
|
||||
def __call__(self, r, *args, **kw):
|
||||
st = self._states[-1]
|
||||
if r is eof:
|
||||
if st.position.offset != len(self._s):
|
||||
self._raise(ExpectedExpr, eof)
|
||||
return ''
|
||||
elif isinstance(r, six.string_types):
|
||||
flags = args[0] if args else 0
|
||||
compiled = self._re_cache.get((r, flags))
|
||||
if not compiled:
|
||||
compiled = re.compile(r, flags)
|
||||
self._re_cache[r, flags] = compiled
|
||||
m = compiled.match(self._s[st.position.offset:])
|
||||
if not m:
|
||||
self._raise(ExpectedExpr, r)
|
||||
|
||||
ms = m.group(0)
|
||||
st.position = st.position.advanced_by(ms)
|
||||
return ms
|
||||
else:
|
||||
kw.pop('err', None)
|
||||
self._callstack.append(CallstackEntry(st.position, r, args, kw))
|
||||
try:
|
||||
return r(self, *args, **kw)
|
||||
finally:
|
||||
self._callstack.pop()
|
||||
|
||||
def consume(self, r, *args, **kw):
|
||||
start_pos = self.position()
|
||||
value = self(r, *args, **kw)
|
||||
end_pos = self.position()
|
||||
return Node(value, start_pos, end_pos)
|
||||
|
||||
def position(self):
|
||||
return self._states[-1].position
|
||||
|
||||
def __repr__(self):
|
||||
line, line_offs = get_line_at_position(self._s, self._states[-1].position)
|
||||
return '<speg.ParsingState at {!r}>'.format('{}*{}'.format(line[:line_offs], line[line_offs:]))
|
||||
|
||||
@staticmethod
|
||||
def eof(p):
|
||||
return p(eof)
|
||||
|
||||
def error(self, *args, **kw):
|
||||
if not args:
|
||||
args = ['semantic error']
|
||||
self._raise(SemanticFailure, args, kw)
|
||||
|
||||
def _raise(self, failure_type, *args):
|
||||
st = self._states[-1]
|
||||
if st.failure_pos is None or st.failure_pos <= st.position:
|
||||
failure = failure_type(*args, callstack=tuple(self._callstack))
|
||||
if st.failure_pos != st.position:
|
||||
st.failure_pos = st.position
|
||||
st.failures = set([failure])
|
||||
else:
|
||||
st.failures.add(failure)
|
||||
raise _ParseBacktrackError()
|
||||
|
||||
def get(self, key, default=None):
|
||||
for state in self._states[::-1]:
|
||||
if key in state.vars:
|
||||
return state.vars[key][0]
|
||||
return default
|
||||
|
||||
def set(self, key, value):
|
||||
self._states[-1].vars[key] = value, False
|
||||
|
||||
def set_global(self, key, value):
|
||||
self._states[-1].vars[key] = value, True
|
||||
|
||||
def opt(self, *args, **kw):
|
||||
with self:
|
||||
return self(*args, **kw)
|
||||
|
||||
def not_(self, r, *args, **kw):
|
||||
self._states.append(_PegState(self._states[-1].position))
|
||||
try:
|
||||
n = self.consume(r)
|
||||
except _ParseBacktrackError:
|
||||
consumed = False
|
||||
else:
|
||||
consumed = True
|
||||
finally:
|
||||
self._states.pop()
|
||||
|
||||
if consumed:
|
||||
self._raise(UnexpectedExpr, n.end_pos, r)
|
||||
|
||||
def __enter__(self):
|
||||
self._states[-1].committed = False
|
||||
self._states.append(_PegState(self._states[-1].position))
|
||||
|
||||
def __exit__(self, type, value, traceback):
|
||||
if type is None:
|
||||
self.commit()
|
||||
else:
|
||||
cur = self._states[-1]
|
||||
prev = self._states[-2]
|
||||
assert cur.failure_pos is not None
|
||||
|
||||
if prev.failure_pos is None or prev.failure_pos < cur.failure_pos:
|
||||
prev.failure_pos = cur.failure_pos
|
||||
prev.failures = cur.failures
|
||||
elif prev.failure_pos == cur.failure_pos:
|
||||
prev.failures.update(cur.failures)
|
||||
|
||||
self._states.pop()
|
||||
return type is _ParseBacktrackError
|
||||
|
||||
def commit(self):
|
||||
cur = self._states[-1]
|
||||
prev = self._states[-2]
|
||||
|
||||
for key in cur.vars:
|
||||
val, g = cur.vars[key]
|
||||
if not g:
|
||||
continue
|
||||
if key in prev.vars:
|
||||
prev.vars[key] = val, prev.vars[key][1]
|
||||
else:
|
||||
prev.vars[key] = val, True
|
||||
|
||||
cur.failure_pos = None
|
||||
|
||||
prev.position = cur.position
|
||||
prev.committed = True
|
||||
|
||||
def __nonzero__(self):
|
||||
return self._states[-1].committed
|
||||
|
||||
__bool__ = __nonzero__
|
@ -1,48 +0,0 @@
|
||||
from functools import total_ordering
|
||||
|
||||
@total_ordering
|
||||
class Position(object):
|
||||
def __init__(self, offset=0, line=1, col=1):
|
||||
self.offset = offset
|
||||
self.line = line
|
||||
self.col = col
|
||||
|
||||
def advanced_by(self, text):
|
||||
text_len = len(text)
|
||||
offset = self.offset + text_len
|
||||
nl_pos = text.rfind('\n')
|
||||
if nl_pos < 0:
|
||||
line = self.line
|
||||
col = self.col + text_len
|
||||
else:
|
||||
line = self.line + text[:nl_pos].count('\n') + 1
|
||||
col = text_len - nl_pos
|
||||
return Position(offset, line, col)
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, Position):
|
||||
return NotImplemented
|
||||
return self.offset == other.offset
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self == other
|
||||
|
||||
def __hash__(self):
|
||||
return hash(self.offset)
|
||||
|
||||
def __lt__(self, other):
|
||||
if not isinstance(other, Position):
|
||||
return NotImplemented
|
||||
return self.offset < other.offset
|
||||
|
||||
def __repr__(self):
|
||||
return '{}({!r}, {!r}, {!r})'.format(Position.__name__,
|
||||
self.offset, self.line, self.col)
|
||||
|
||||
def get_line_at_position(text, pos):
|
||||
suffix = text[pos.offset - pos.col + 1:]
|
||||
stop = suffix.find('\n')
|
||||
if stop == -1:
|
||||
return suffix, pos.col - 1
|
||||
else:
|
||||
return suffix[:stop], pos.col - 1
|
@ -1,14 +0,0 @@
|
||||
from six import string_types
|
||||
|
||||
class Eof: pass
|
||||
eof = Eof()
|
||||
|
||||
def rule_to_str(rule):
|
||||
if rule is eof:
|
||||
return 'eof'
|
||||
if isinstance(rule, string_types):
|
||||
return repr(rule)
|
||||
fn_name = rule.__name__
|
||||
if fn_name.startswith('_'):
|
||||
fn_name = fn_name[1:]
|
||||
return '<{}>'.format(fn_name.replace('_', ' '))
|
@ -1,191 +0,0 @@
|
||||
import re, json, sys
|
||||
|
||||
if sys.version_info[0] == 2:
|
||||
def _is_num(o):
|
||||
return isinstance(o, int) or isinstance(o, long) or isinstance(o, float)
|
||||
def _stringify(o):
|
||||
if isinstance(o, str):
|
||||
return unicode(o)
|
||||
if isinstance(o, unicode):
|
||||
return o
|
||||
return None
|
||||
else:
|
||||
def _is_num(o):
|
||||
return isinstance(o, int) or isinstance(o, float)
|
||||
def _stringify(o):
|
||||
if isinstance(o, bytes):
|
||||
return o.decode()
|
||||
if isinstance(o, str):
|
||||
return o
|
||||
return None
|
||||
|
||||
_id_re = re.compile(r'[$a-zA-Z_][$0-9a-zA-Z_]*\Z')
|
||||
|
||||
class CSONEncoder:
|
||||
def __init__(self, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, sort_keys=False,
|
||||
indent=None, default=None):
|
||||
self._skipkeys = skipkeys
|
||||
self._ensure_ascii = ensure_ascii
|
||||
self._allow_nan = allow_nan
|
||||
self._sort_keys = sort_keys
|
||||
self._indent = ' ' * (indent or 4)
|
||||
self._default = default
|
||||
if check_circular:
|
||||
self._obj_stack = set()
|
||||
else:
|
||||
self._obj_stack = None
|
||||
|
||||
def _format_simple_val(self, o):
|
||||
if o is None:
|
||||
return 'null'
|
||||
if isinstance(o, bool):
|
||||
return 'true' if o else 'false'
|
||||
if _is_num(o):
|
||||
return str(o)
|
||||
s = _stringify(o)
|
||||
if s is not None:
|
||||
return self._escape_string(s)
|
||||
return None
|
||||
|
||||
def _escape_string(self, s):
|
||||
r = json.dumps(s, ensure_ascii=self._ensure_ascii)
|
||||
return u"'{}'".format(r[1:-1].replace("'", r"\'"))
|
||||
|
||||
def _escape_key(self, s):
|
||||
if s is None or isinstance(s, bool) or _is_num(s):
|
||||
s = str(s)
|
||||
s = _stringify(s)
|
||||
if s is None:
|
||||
if self._skipkeys:
|
||||
return None
|
||||
raise TypeError('keys must be a string')
|
||||
if not _id_re.match(s):
|
||||
return self._escape_string(s)
|
||||
return s
|
||||
|
||||
def _push_obj(self, o):
|
||||
if self._obj_stack is not None:
|
||||
if id(o) in self._obj_stack:
|
||||
raise ValueError('Circular reference detected')
|
||||
self._obj_stack.add(id(o))
|
||||
|
||||
def _pop_obj(self, o):
|
||||
if self._obj_stack is not None:
|
||||
self._obj_stack.remove(id(o))
|
||||
|
||||
def _encode(self, o, obj_val=False, indent='', force_flow=False):
|
||||
if isinstance(o, list):
|
||||
if not o:
|
||||
if obj_val:
|
||||
yield ' []\n'
|
||||
else:
|
||||
yield indent
|
||||
yield '[]\n'
|
||||
else:
|
||||
if obj_val:
|
||||
yield ' [\n'
|
||||
else:
|
||||
yield indent
|
||||
yield '[\n'
|
||||
indent = indent + self._indent
|
||||
self._push_obj(o)
|
||||
for v in o:
|
||||
for chunk in self._encode(v, obj_val=False, indent=indent, force_flow=True):
|
||||
yield chunk
|
||||
self._pop_obj(o)
|
||||
yield indent[:-len(self._indent)]
|
||||
yield ']\n'
|
||||
elif isinstance(o, dict):
|
||||
items = [(self._escape_key(k), v) for k, v in o.items()]
|
||||
if self._skipkeys:
|
||||
items = [(k, v) for k, v in items if k is not None]
|
||||
if self._sort_keys:
|
||||
items.sort()
|
||||
if force_flow or not items:
|
||||
if not items:
|
||||
if obj_val:
|
||||
yield ' {}\n'
|
||||
else:
|
||||
yield indent
|
||||
yield '{}\n'
|
||||
else:
|
||||
if obj_val:
|
||||
yield ' {\n'
|
||||
else:
|
||||
yield indent
|
||||
yield '{\n'
|
||||
indent = indent + self._indent
|
||||
self._push_obj(o)
|
||||
for k, v in items:
|
||||
yield indent
|
||||
yield k
|
||||
yield ':'
|
||||
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
|
||||
yield chunk
|
||||
self._pop_obj(o)
|
||||
yield indent[:-len(self._indent)]
|
||||
yield '}\n'
|
||||
else:
|
||||
if obj_val:
|
||||
yield '\n'
|
||||
self._push_obj(o)
|
||||
for k, v in items:
|
||||
yield indent
|
||||
yield k
|
||||
yield ':'
|
||||
for chunk in self._encode(v, obj_val=True, indent=indent + self._indent, force_flow=False):
|
||||
yield chunk
|
||||
self._pop_obj(o)
|
||||
else:
|
||||
v = self._format_simple_val(o)
|
||||
if v is None:
|
||||
self._push_obj(o)
|
||||
v = self.default(o)
|
||||
for chunk in self._encode(v, obj_val=obj_val, indent=indent, force_flow=force_flow):
|
||||
yield chunk
|
||||
self._pop_obj(o)
|
||||
else:
|
||||
if obj_val:
|
||||
yield ' '
|
||||
else:
|
||||
yield indent
|
||||
yield v
|
||||
yield '\n'
|
||||
|
||||
def iterencode(self, o):
|
||||
return self._encode(o)
|
||||
|
||||
def encode(self, o):
|
||||
return ''.join(self.iterencode(o))
|
||||
|
||||
def default(self, o):
|
||||
if self._default is None:
|
||||
raise TypeError('Cannot serialize an object of type {}'.format(type(o).__name__))
|
||||
return self._default(o)
|
||||
|
||||
def dump(obj, fp, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None,
|
||||
indent=None, default=None, sort_keys=False, **kw):
|
||||
if indent is None and cls is None:
|
||||
return json.dump(obj, fp, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
|
||||
|
||||
if cls is None:
|
||||
cls = CSONEncoder
|
||||
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
|
||||
|
||||
for chunk in encoder.iterencode(obj):
|
||||
fp.write(chunk)
|
||||
|
||||
def dumps(obj, skipkeys=False, ensure_ascii=True, check_circular=True, allow_nan=True, cls=None, indent=None,
|
||||
default=None, sort_keys=False, **kw):
|
||||
if indent is None and cls is None:
|
||||
return json.dumps(obj, skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||
allow_nan=allow_nan, default=default, sort_keys=sort_keys, separators=(',', ':'))
|
||||
|
||||
if cls is None:
|
||||
cls = CSONEncoder
|
||||
encoder = cls(skipkeys=skipkeys, ensure_ascii=ensure_ascii, check_circular=check_circular,
|
||||
allow_nan=allow_nan, sort_keys=sort_keys, indent=indent, default=default, **kw)
|
||||
|
||||
return encoder.encode(obj)
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Snippets",
|
||||
"author": "ITDominator",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"pass_events": "true",
|
||||
"bind_keys": ["Snippets||show_snippets_ui:<Control>i"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,103 +0,0 @@
|
||||
# Python imports
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
from . import cson
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Snippets" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
self.snippet_data = None
|
||||
self._file_type = None
|
||||
self.active_snippit_group = None
|
||||
self.snippit_groups = []
|
||||
self.snippit_prefixes = []
|
||||
self.snippit_group_keys = []
|
||||
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
...
|
||||
|
||||
def run(self):
|
||||
with open('snippets.cson', 'rb') as f:
|
||||
self.snippet_data = cson.load(f)
|
||||
self.snippit_groups = self.snippet_data.keys()
|
||||
|
||||
def subscribe_to_events(self):
|
||||
self._event_system.subscribe("set_active_src_view", self._set_active_src_view)
|
||||
self._event_system.subscribe("show_snippets_ui", self._show_snippets_ui)
|
||||
self._event_system.subscribe("buffer_changed_first_load", self._buffer_changed_first_load)
|
||||
self._event_system.subscribe("buffer_changed", self._buffer_changed)
|
||||
|
||||
|
||||
def _set_active_src_view(self, source_view):
|
||||
self._active_src_view = source_view
|
||||
self._buffer = source_view.get_buffer()
|
||||
self._file_type = source_view.get_filetype()
|
||||
self._tag_table = self._buffer.get_tag_table()
|
||||
|
||||
self.load_target_snippt_group()
|
||||
|
||||
def load_target_snippt_group(self):
|
||||
self.active_snippit_group = None
|
||||
for group in self.snippit_groups:
|
||||
if group in self._file_type:
|
||||
self.active_snippit_group = group
|
||||
break
|
||||
|
||||
if self.active_snippit_group:
|
||||
self.snippit_prefixes.clear()
|
||||
keys = self.snippet_data[self.active_snippit_group].keys()
|
||||
|
||||
self.snippit_group_keys.clear()
|
||||
for key in keys:
|
||||
self.snippit_group_keys.append(key)
|
||||
prefix = self.snippet_data[self.active_snippit_group][key]["prefix"]
|
||||
self.snippit_prefixes.append(prefix)
|
||||
|
||||
def _buffer_changed_first_load(self, buffer):
|
||||
self._buffer = buffer
|
||||
self._handle_update(buffer)
|
||||
|
||||
def _buffer_changed(self, buffer):
|
||||
self._event_system.emit("pause_event_processing")
|
||||
self._handle_update(buffer)
|
||||
self._event_system.emit("resume_event_processing")
|
||||
|
||||
|
||||
def _show_snippets_ui(self):
|
||||
print(f"Data: {self.snippit_groups}")
|
||||
|
||||
def _handle_update(self, buffer):
|
||||
if not self.active_snippit_group: return
|
||||
|
||||
end_iter = buffer.get_iter_at_mark( buffer.get_insert() )
|
||||
start_iter = end_iter.copy()
|
||||
start_iter.backward_word_start()
|
||||
|
||||
matches = []
|
||||
text = buffer.get_text(start_iter, end_iter, include_hidden_chars = False)
|
||||
for prefix in self.snippit_prefixes:
|
||||
if text in prefix:
|
||||
matches.append(prefix)
|
||||
|
||||
snippits = []
|
||||
for _match in matches:
|
||||
for key in self.snippit_group_keys:
|
||||
prefix = self.snippet_data[self.active_snippit_group][key]["prefix"]
|
||||
if prefix == _match:
|
||||
body = self.snippet_data[self.active_snippit_group][key]["body"]
|
||||
snippits.append(body)
|
||||
|
||||
# print(snippits)
|
||||
print("Snippits Plugin: _handle_update > results > stub...")
|
@ -1,614 +0,0 @@
|
||||
# Your snippets
|
||||
#
|
||||
# Atom snippets allow you to enter a simple prefix in the editor and hit tab to
|
||||
# expand the prefix into a larger code block with templated values.
|
||||
#
|
||||
# You can create a new snippet in this file by typing "snip" and then hitting
|
||||
# tab.
|
||||
#
|
||||
# An example CoffeeScript snippet to expand log to console.log:
|
||||
#
|
||||
# '.source.coffee':
|
||||
# 'Console log':
|
||||
# 'prefix': 'log'
|
||||
# 'body': 'console.log $1'
|
||||
#
|
||||
# Each scope (e.g. '.source.coffee' above) can only be declared once.
|
||||
#
|
||||
# This file uses CoffeeScript Object Notation (CSON).
|
||||
# If you are unfamiliar with CSON, you can read more about it in the
|
||||
# Atom Flight Manual:
|
||||
# http://flight-manual.atom.io/using-atom/sections/basic-customization/#_cson
|
||||
|
||||
|
||||
### HTML SNIPPETS ###
|
||||
'html':
|
||||
|
||||
'HTML Template':
|
||||
'prefix': 'html'
|
||||
'body': """<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link rel="shortcut icon" href="fave_icon.png">
|
||||
<link rel="stylesheet" href="resources/css/base.css">
|
||||
<link rel="stylesheet" href="resources/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script src="resources/js/.js" charset="utf-8"></script>
|
||||
<script src="resources/js/.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
'Canvas Tag':
|
||||
'prefix': 'canvas'
|
||||
'body': """<canvas id="canvas" width="800" height="600" style="border:1px solid #c3c3c3;"></canvas>"""
|
||||
|
||||
'Img Tag':
|
||||
'prefix': 'img'
|
||||
'body': """<img class="" src="" alt="" />"""
|
||||
|
||||
'Br Tag':
|
||||
'prefix': 'br'
|
||||
'body': """<br/>"""
|
||||
|
||||
'Hr Tag':
|
||||
'prefix': 'hr'
|
||||
'body': """<hr/>"""
|
||||
|
||||
'Server Side Events':
|
||||
'prefix': 'sse'
|
||||
'body': """// SSE events if supported
|
||||
if(typeof(EventSource) !== "undefined") {
|
||||
let source = new EventSource("resources/php/sse.php");
|
||||
source.onmessage = (event) => {
|
||||
if (event.data === "<yourDataStringToLookFor>") {
|
||||
// code here
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log("SSE Not Supported In Browser...");
|
||||
}
|
||||
"""
|
||||
|
||||
'AJAX Template Function':
|
||||
'prefix': 'ajax template'
|
||||
'body': """const doAjax = async (actionPath, data) => {
|
||||
let xhttp = new XMLHttpRequest();
|
||||
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
if (this.responseText != null) { // this.responseXML if getting XML fata
|
||||
handleReturnData(JSON.parse(this.responseText));
|
||||
} else {
|
||||
console.log("No content returned. Check the file path.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhttp.open("POST", actionPath, true);
|
||||
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
// Force return to be JSON NOTE: Use application/xml to force XML
|
||||
xhttp.overrideMimeType('application/json');
|
||||
xhttp.send(data);
|
||||
}
|
||||
"""
|
||||
|
||||
'CSS Message Colors':
|
||||
'prefix': 'css colors'
|
||||
'body': """.error { color: rgb(255, 0, 0); }
|
||||
.warning { color: rgb(255, 168, 0); }
|
||||
.success { color: rgb(136, 204, 39); }
|
||||
"""
|
||||
|
||||
|
||||
### JS SNIPPETS ###
|
||||
'js':
|
||||
|
||||
'Server Side Events':
|
||||
'prefix': 'sse'
|
||||
'body': """// SSE events if supported
|
||||
if(typeof(EventSource) !== "undefined") {
|
||||
let source = new EventSource("resources/php/sse.php");
|
||||
source.onmessage = (event) => {
|
||||
if (event.data === "<yourDataStringToLookFor>") {
|
||||
// code here
|
||||
}
|
||||
};
|
||||
} else {
|
||||
console.log("SSE Not Supported In Browser...");
|
||||
}
|
||||
"""
|
||||
|
||||
'AJAX Template Function':
|
||||
'prefix': 'ajax template'
|
||||
'body': """const doAjax = async (actionPath, data) => {
|
||||
let xhttp = new XMLHttpRequest();
|
||||
|
||||
xhttp.onreadystatechange = function() {
|
||||
if (this.readyState === 4 && this.status === 200) {
|
||||
if (this.responseText != null) { // this.responseXML if getting XML fata
|
||||
handleReturnData(JSON.parse(this.responseText));
|
||||
} else {
|
||||
console.log("No content returned. Check the file path.");
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
xhttp.open("POST", actionPath, true);
|
||||
xhttp.setRequestHeader("Content-type", "application/x-www-form-urlencoded");
|
||||
// Force return to be JSON NOTE: Use application/xml to force XML
|
||||
xhttp.overrideMimeType('application/json');
|
||||
xhttp.send(data);
|
||||
}
|
||||
"""
|
||||
|
||||
'SE6 Function':
|
||||
'prefix': 'function se6'
|
||||
'body': """const funcName = (arg = "") => {
|
||||
|
||||
}
|
||||
"""
|
||||
|
||||
### CSS SNIPPETS ###
|
||||
'css':
|
||||
|
||||
'CSS Message Colors':
|
||||
'prefix': 'css colors'
|
||||
'body': """.error { color: rgb(255, 0, 0); }
|
||||
.warning { color: rgb(255, 168, 0); }
|
||||
.success { color: rgb(136, 204, 39); }
|
||||
"""
|
||||
|
||||
### PHP SNIPPETS ###
|
||||
'php':
|
||||
|
||||
'SSE PHP':
|
||||
'prefix': 'sse php'
|
||||
'body': """<?php
|
||||
// Start the session
|
||||
session_start();
|
||||
|
||||
header('Content-Type: text/event-stream');
|
||||
header('Cache-Control: no-cache');
|
||||
|
||||
echo "data:dataToReturn\\\\n\\\\n";
|
||||
|
||||
flush();
|
||||
?>
|
||||
"""
|
||||
|
||||
'PHP Template':
|
||||
'prefix': 'php'
|
||||
'body': """<?php
|
||||
// Start the session
|
||||
session_start();
|
||||
|
||||
|
||||
// Determin action
|
||||
chdir("../../"); // Note: If in resources/php/
|
||||
if (isset($_POST['yourPostID'])) {
|
||||
// code here
|
||||
} else {
|
||||
$message = "Server: [Error] --> Illegal Access Method!";
|
||||
serverMessage("error", $message);
|
||||
}
|
||||
?>
|
||||
"""
|
||||
'HTML Template':
|
||||
'prefix': 'html'
|
||||
'body': """<!DOCTYPE html>
|
||||
<html lang="en" dir="ltr">
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title></title>
|
||||
<link rel="shortcut icon" href="fave_icon.png">
|
||||
<link rel="stylesheet" href="resources/css/base.css">
|
||||
<link rel="stylesheet" href="resources/css/main.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script src="resources/js/.js" charset="utf-8"></script>
|
||||
<script src="resources/js/.js" charset="utf-8"></script>
|
||||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
### BASH SNIPPETS ###
|
||||
'bash':
|
||||
|
||||
'Bash or Shell Template':
|
||||
'prefix': 'bash template'
|
||||
'body': """#!/bin/bash
|
||||
|
||||
# . CONFIG.sh
|
||||
|
||||
# set -o xtrace ## To debug scripts
|
||||
# set -o errexit ## To exit on error
|
||||
# set -o errunset ## To exit if a variable is referenced but not set
|
||||
|
||||
|
||||
function main() {
|
||||
cd "$(dirname "$0")"
|
||||
echo "Working Dir: " $(pwd)
|
||||
|
||||
file="$1"
|
||||
if [ -z "${file}" ]; then
|
||||
echo "ERROR: No file argument supplied..."
|
||||
exit
|
||||
fi
|
||||
|
||||
if [[ -f "${file}" ]]; then
|
||||
echo "SUCCESS: The path and file exists!"
|
||||
else
|
||||
echo "ERROR: The path or file '${file}' does NOT exist..."
|
||||
fi
|
||||
}
|
||||
main "$@";
|
||||
"""
|
||||
|
||||
|
||||
'Bash or Shell Config':
|
||||
'prefix': 'bash config'
|
||||
'body': """#!/bin/bash
|
||||
|
||||
shopt -s expand_aliases
|
||||
|
||||
alias echo="echo -e"
|
||||
"""
|
||||
|
||||
|
||||
### PYTHON SNIPPETS ###
|
||||
'python':
|
||||
|
||||
'Glade __main__ Class Template':
|
||||
'prefix': 'glade __main__ class'
|
||||
'body': """#!/usr/bin/python3
|
||||
|
||||
|
||||
# Python imports
|
||||
import argparse
|
||||
import faulthandler
|
||||
import traceback
|
||||
import signal
|
||||
from setproctitle import setproctitle
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
from app import Application
|
||||
|
||||
|
||||
def run():
|
||||
try:
|
||||
setproctitle('<replace this>')
|
||||
faulthandler.enable() # For better debug info
|
||||
|
||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
# Add long and short arguments
|
||||
parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.")
|
||||
parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.")
|
||||
parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.")
|
||||
|
||||
parser.add_argument("--new-tab", "-t", default="", help="Open a file into new tab.")
|
||||
parser.add_argument("--new-window", "-w", default="", help="Open a file into a new window.")
|
||||
|
||||
# Read arguments (If any...)
|
||||
args, unknownargs = parser.parse_known_args()
|
||||
|
||||
main = Application(args, unknownargs)
|
||||
Gtk.main()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
''' Set process title, get arguments, and create GTK main thread. '''
|
||||
run()
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
'Glade __main__ Testing Template':
|
||||
'prefix': 'glade testing class'
|
||||
'body': """#!/usr/bin/python3
|
||||
|
||||
|
||||
# Python imports
|
||||
import traceback
|
||||
import faulthandler
|
||||
import signal
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import GLib
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
app_name = "Gtk Quick Test"
|
||||
|
||||
|
||||
class Application(Gtk.ApplicationWindow):
|
||||
def __init__(self):
|
||||
super(Application, self).__init__()
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._load_widgets()
|
||||
|
||||
self.add(Gtk.Box())
|
||||
|
||||
self.show_all()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
self.set_default_size(1670, 830)
|
||||
self.set_title(f"{app_name}")
|
||||
# self.set_icon_from_file( settings.get_window_icon() )
|
||||
self.set_gravity(5) # 5 = CENTER
|
||||
self.set_position(1) # 1 = CENTER, 4 = CENTER_ALWAYS
|
||||
|
||||
def _setup_signals(self):
|
||||
self.connect("delete-event", Gtk.main_quit)
|
||||
|
||||
|
||||
def _load_widgets(self):
|
||||
...
|
||||
|
||||
|
||||
|
||||
|
||||
def run():
|
||||
try:
|
||||
faulthandler.enable() # For better debug info
|
||||
GLib.unix_signal_add(GLib.PRIORITY_DEFAULT, signal.SIGINT, Gtk.main_quit)
|
||||
|
||||
main = Application()
|
||||
Gtk.main()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
quit()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
''' Set process title, get arguments, and create GTK main thread. '''
|
||||
run()
|
||||
|
||||
|
||||
"""
|
||||
|
||||
|
||||
'Glade _init_ Class Template':
|
||||
'prefix': 'glade __init__ class'
|
||||
'body': """# Python imports
|
||||
import inspect
|
||||
|
||||
|
||||
# Lib imports
|
||||
|
||||
|
||||
# Application imports
|
||||
from utils import Settings
|
||||
from signal_classes import CrossClassSignals
|
||||
|
||||
|
||||
class Main:
|
||||
def __init__(self, args):
|
||||
settings = Settings()
|
||||
builder = settings.returnBuilder()
|
||||
|
||||
# Gets the methods from the classes and sets to handler.
|
||||
# Then, builder connects to any signals it needs.
|
||||
classes = [CrossClassSignals(settings)]
|
||||
|
||||
handlers = {}
|
||||
for c in classes:
|
||||
methods = inspect.getmembers(c, predicate=inspect.ismethod)
|
||||
handlers.update(methods)
|
||||
|
||||
builder.connect_signals(handlers)
|
||||
window = settings.createWindow()
|
||||
window.show()
|
||||
|
||||
"""
|
||||
|
||||
'Class Method':
|
||||
'prefix': 'def1'
|
||||
'body': """
|
||||
def fname(self):
|
||||
...
|
||||
"""
|
||||
|
||||
'Gtk Class Method':
|
||||
'prefix': 'def2'
|
||||
'body': """
|
||||
def fname(self, widget, eve):
|
||||
...
|
||||
"""
|
||||
|
||||
|
||||
'Python Glade Settings Template':
|
||||
'prefix': 'glade settings class'
|
||||
'body': """# Python imports
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
import gi, cairo
|
||||
gi.require_version('Gtk', '3.0')
|
||||
gi.require_version('Gdk', '3.0')
|
||||
|
||||
from gi.repository import Gtk
|
||||
from gi.repository import Gdk
|
||||
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class Settings:
|
||||
def __init__(self):
|
||||
self.SCRIPT_PTH = os.path.dirname(os.path.realpath(__file__)) + "/"
|
||||
self.builder = Gtk.Builder()
|
||||
self.builder.add_from_file(self.SCRIPT_PTH + "../resources/Main_Window.glade")
|
||||
|
||||
# 'Filters'
|
||||
self.office = ('.doc', '.docx', '.xls', '.xlsx', '.xlt', '.xltx', '.xlm',
|
||||
'.ppt', 'pptx', '.pps', '.ppsx', '.odt', '.rtf')
|
||||
self.vids = ('.mkv', '.avi', '.flv', '.mov', '.m4v', '.mpg', '.wmv',
|
||||
'.mpeg', '.mp4', '.webm')
|
||||
self.txt = ('.txt', '.text', '.sh', '.cfg', '.conf')
|
||||
self.music = ('.psf', '.mp3', '.ogg' , '.flac')
|
||||
self.images = ('.png', '.jpg', '.jpeg', '.gif')
|
||||
self.pdf = ('.pdf')
|
||||
|
||||
|
||||
def createWindow(self):
|
||||
# Get window and connect signals
|
||||
window = self.builder.get_object("Main_Window")
|
||||
window.connect("delete-event", gtk.main_quit)
|
||||
self.setWindowData(window, False)
|
||||
return window
|
||||
|
||||
def setWindowData(self, window, paintable):
|
||||
screen = window.get_screen()
|
||||
visual = screen.get_rgba_visual()
|
||||
|
||||
if visual != None and screen.is_composited():
|
||||
window.set_visual(visual)
|
||||
|
||||
# bind css file
|
||||
cssProvider = gtk.CssProvider()
|
||||
cssProvider.load_from_path(self.SCRIPT_PTH + '../resources/stylesheet.css')
|
||||
screen = Gdk.Screen.get_default()
|
||||
styleContext = Gtk.StyleContext()
|
||||
styleContext.add_provider_for_screen(screen, cssProvider, gtk.STYLE_PROVIDER_PRIORITY_USER)
|
||||
|
||||
window.set_app_paintable(paintable)
|
||||
if paintable:
|
||||
window.connect("draw", self.area_draw)
|
||||
|
||||
def getMonitorData(self):
|
||||
screen = self.builder.get_object("Main_Window").get_screen()
|
||||
monitors = []
|
||||
for m in range(screen.get_n_monitors()):
|
||||
monitors.append(screen.get_monitor_geometry(m))
|
||||
|
||||
for monitor in monitors:
|
||||
print(str(monitor.width) + "x" + str(monitor.height) + "+" + str(monitor.x) + "+" + str(monitor.y))
|
||||
|
||||
return monitors
|
||||
|
||||
def area_draw(self, widget, cr):
|
||||
cr.set_source_rgba(0, 0, 0, 0.54)
|
||||
cr.set_operator(cairo.OPERATOR_SOURCE)
|
||||
cr.paint()
|
||||
cr.set_operator(cairo.OPERATOR_OVER)
|
||||
|
||||
|
||||
def returnBuilder(self): return self.builder
|
||||
|
||||
# Filter returns
|
||||
def returnOfficeFilter(self): return self.office
|
||||
def returnVidsFilter(self): return self.vids
|
||||
def returnTextFilter(self): return self.txt
|
||||
def returnMusicFilter(self): return self.music
|
||||
def returnImagesFilter(self): return self.images
|
||||
def returnPdfFilter(self): return self.pdf
|
||||
|
||||
"""
|
||||
|
||||
'Python Glade CrossClassSignals Template':
|
||||
'prefix': 'glade crossClassSignals class'
|
||||
'body': """# Python imports
|
||||
import threading
|
||||
import subprocess
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs).start()
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class CrossClassSignals:
|
||||
def __init__(self, settings):
|
||||
self.settings = settings
|
||||
self.builder = self.settings.returnBuilder()
|
||||
|
||||
|
||||
def getClipboardData(self):
|
||||
proc = subprocess.Popen(['xclip','-selection', 'clipboard', '-o'], stdout=subprocess.PIPE)
|
||||
retcode = proc.wait()
|
||||
data = proc.stdout.read()
|
||||
return data.decode("utf-8").strip()
|
||||
|
||||
def setClipboardData(self, data):
|
||||
proc = subprocess.Popen(['xclip','-selection','clipboard'], stdin=subprocess.PIPE)
|
||||
proc.stdin.write(data)
|
||||
proc.stdin.close()
|
||||
retcode = proc.wait()
|
||||
|
||||
"""
|
||||
|
||||
|
||||
'Python Glade Generic Template':
|
||||
'prefix': 'glade generic class'
|
||||
'body': """# Python imports
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
|
||||
|
||||
class GenericClass:
|
||||
def __init__(self):
|
||||
super(GenericClass, self).__init__()
|
||||
|
||||
self._setup_styling()
|
||||
self._setup_signals()
|
||||
self._subscribe_to_events()
|
||||
self._load_widgets()
|
||||
|
||||
|
||||
def _setup_styling(self):
|
||||
...
|
||||
|
||||
def _setup_signals(self):
|
||||
...
|
||||
|
||||
def _subscribe_to_events(self):
|
||||
event_system.subscribe("handle_file_from_ipc", self.handle_file_from_ipc)
|
||||
|
||||
def _load_widgets(self):
|
||||
...
|
||||
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Module
|
||||
"""
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Pligin Package
|
||||
"""
|
@ -1,13 +0,0 @@
|
||||
{
|
||||
"manifest": {
|
||||
"name": "Example Plugin",
|
||||
"author": "John Doe",
|
||||
"version": "0.0.1",
|
||||
"support": "",
|
||||
"requests": {
|
||||
"ui_target": "plugin_control_list",
|
||||
"pass_events": "true",
|
||||
"bind_keys": ["Example Plugin||send_message:<Control>f"]
|
||||
}
|
||||
}
|
||||
}
|
@ -1,51 +0,0 @@
|
||||
# Python imports
|
||||
import os
|
||||
import threading
|
||||
import subprocess
|
||||
import time
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from plugins.plugin_base import PluginBase
|
||||
|
||||
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
|
||||
class Plugin(PluginBase):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
self.name = "Example Plugin" # NOTE: Need to remove after establishing private bidirectional 1-1 message bus
|
||||
# where self.name should not be needed for message comms
|
||||
|
||||
|
||||
def generate_reference_ui_element(self):
|
||||
button = Gtk.Button(label=self.name)
|
||||
button.connect("button-release-event", self.send_message)
|
||||
return button
|
||||
|
||||
def run(self):
|
||||
...
|
||||
|
||||
def send_message(self, widget=None, eve=None):
|
||||
message = "Hello, World!"
|
||||
event_system.emit("display_message", ("warning", message, None))
|
@ -1,50 +0,0 @@
|
||||
# Python imports
|
||||
import builtins
|
||||
import threading
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from libs.event_system import EventSystem
|
||||
from libs.endpoint_registry import EndpointRegistry
|
||||
from libs.keybindings import Keybindings
|
||||
from libs.logger import Logger
|
||||
from libs.settings_manager.manager import SettingsManager
|
||||
|
||||
|
||||
class BuiltinsException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
# NOTE: Threads WILL NOT die with parent's destruction.
|
||||
def threaded_wrapper(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=False).start()
|
||||
return wrapper
|
||||
|
||||
# NOTE: Threads WILL die with parent's destruction.
|
||||
def daemon_threaded_wrapper(fn):
|
||||
def wrapper(*args, **kwargs):
|
||||
threading.Thread(target=fn, args=args, kwargs=kwargs, daemon=True).start()
|
||||
return wrapper
|
||||
|
||||
|
||||
|
||||
# NOTE: Just reminding myself we can add to builtins two different ways...
|
||||
# __builtins__.update({"event_system": Builtins()})
|
||||
builtins.app_name = "Newton"
|
||||
builtins.keybindings = Keybindings()
|
||||
builtins.event_system = EventSystem()
|
||||
builtins.endpoint_registry = EndpointRegistry()
|
||||
builtins.settings_manager = SettingsManager()
|
||||
|
||||
settings_manager.load_settings()
|
||||
|
||||
builtins.settings = settings_manager.settings
|
||||
builtins.logger = Logger(settings_manager.get_home_config_path(), \
|
||||
_ch_log_lvl=settings.debugging.ch_log_lvl, \
|
||||
_fh_log_lvl=settings.debugging.fh_log_lvl).get_logger()
|
||||
|
||||
builtins.threaded = threaded_wrapper
|
||||
builtins.daemon_threaded = daemon_threaded_wrapper
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Start of package.
|
||||
"""
|
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/python3
|
||||
|
||||
# Python imports
|
||||
import argparse
|
||||
import faulthandler
|
||||
import traceback
|
||||
from setproctitle import setproctitle
|
||||
|
||||
import tracemalloc
|
||||
tracemalloc.start()
|
||||
|
||||
# Lib imports
|
||||
import gi
|
||||
gi.require_version('Gtk', '3.0')
|
||||
from gi.repository import Gtk
|
||||
|
||||
# Application imports
|
||||
from __builtins__ import *
|
||||
from app import Application
|
||||
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
''' Set process title, get arguments, and create GTK main thread. '''
|
||||
|
||||
try:
|
||||
setproctitle(f'{app_name}')
|
||||
faulthandler.enable() # For better debug info
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
# Add long and short arguments
|
||||
parser.add_argument("--debug", "-d", default="false", help="Do extra console messaging.")
|
||||
parser.add_argument("--trace-debug", "-td", default="false", help="Disable saves, ignore IPC lock, do extra console messaging.")
|
||||
parser.add_argument("--no-plugins", "-np", default="false", help="Do not load plugins.")
|
||||
parser.add_argument("--new-tab", "-t", default="", help="Open a file into new tab.")
|
||||
parser.add_argument("--new-window", "-w", default="", help="Open a file into a new window.")
|
||||
|
||||
# Read arguments (If any...)
|
||||
args, unknownargs = parser.parse_known_args()
|
||||
|
||||
if args.debug == "true":
|
||||
settings_manager.set_debug(True)
|
||||
|
||||
if args.trace_debug == "true":
|
||||
settings_manager.set_trace_debug(True)
|
||||
|
||||
settings_manager.do_dirty_start_check()
|
||||
Application(args, unknownargs)
|
||||
Gtk.main()
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
quit()
|
60
src/app.py
60
src/app.py
@ -1,60 +0,0 @@
|
||||
# Python imports
|
||||
import signal
|
||||
import os
|
||||
|
||||
# Lib imports
|
||||
|
||||
# Application imports
|
||||
from libs.debugging import debug_signal_handler
|
||||
from libs.ipc_server import IPCServer
|
||||
from core.window import Window
|
||||
|
||||
|
||||
|
||||
class AppLaunchException(Exception):
|
||||
...
|
||||
|
||||
|
||||
|
||||
class Application(IPCServer):
|
||||
""" docstring for Application. """
|
||||
|
||||
def __init__(self, args, unknownargs):
|
||||
super(Application, self).__init__()
|
||||
|
||||
if not settings_manager.is_trace_debug():
|
||||
self.socket_realization_check()
|
||||
|
||||
if not self.is_ipc_alive:
|
||||
for arg in unknownargs + [args.new_tab,]:
|
||||
if os.path.isfile(arg):
|
||||
message = f"FILE|{arg}"
|
||||
self.send_ipc_message(message)
|
||||
|
||||
raise AppLaunchException(f"{app_name} IPC Server Exists: Have sent path(s) to it and closing...")
|
||||
|
||||
self.setup_debug_hook()
|
||||
Window(args, unknownargs)
|
||||
|
||||
|
||||
def socket_realization_check(self):
|
||||
try:
|
||||
self.create_ipc_listener()
|
||||
except Exception:
|
||||
self.send_test_ipc_message()
|
||||
|
||||
try:
|
||||
self.create_ipc_listener()
|
||||
except Exception as e:
|
||||
...
|
||||
|
||||
def setup_debug_hook(self):
|
||||
try:
|
||||
# kill -SIGUSR2 <pid> from Linux/Unix or SIGBREAK signal from Windows
|
||||
signal.signal(
|
||||
vars(signal).get("SIGBREAK") or vars(signal).get("SIGUSR1"),
|
||||
debug_signal_handler
|
||||
)
|
||||
except ValueError:
|
||||
# Typically: ValueError: signal only works in main thread
|
||||
...
|
@ -1,3 +0,0 @@
|
||||
"""
|
||||
Gtk Bound Signal Module
|
||||
"""
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user