Created
May 28, 2020 22:16
-
-
Save michaelkrupp/564e077eef44a9239b4e7ba7693fef0e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
from bs4 import BeautifulSoup | |
from distutils.util import strtobool | |
from glob import iglob | |
from markdown import Extension | |
from markdown.preprocessors import Preprocessor | |
import re | |
import codecs | |
import os | |
RE_ALL_SNIPPETS = re.compile( | |
r'''(?x) | |
^(?P<space>[ \t]*) | |
(?P<all> | |
(?: | |
(?P<block_marker>-{2,}8<-{2,}) | |
(?P<options> | |
(?: | |
[ \t]+ | |
[a-zA-Z][a-zA-Z0-9_]*=(?:(?P<quot>"|').*?(?P=quot))? | |
)* | |
) | |
(?![ \t]) | |
) | |
)\r?$ | |
''' | |
) | |
RE_SNIPPET = re.compile( | |
r'''(?x) | |
^(?P<space>[ \t]*) | |
(?P<snippet>.*?)\r?$ | |
''' | |
) | |
RE_OPTIONS = re.compile( | |
r'''(?x) | |
(?P<key>[a-zA-Z][a-zA-Z0-9_]*)=(?:(?P<quot>"|')(?P<value>.*?)(?P=quot))? | |
''' | |
) | |
RE_INDENT = re.compile('^(\s+)', re.MULTILINE) | |
class SnippetPreprocessor(Preprocessor): | |
"""Handle snippets in Markdown content.""" | |
def __init__(self, config, md): | |
"""Initialize.""" | |
self._md = md | |
self.base_path = config.get('base_path') | |
self.encoding = config.get('encoding') | |
self.check_paths = config.get('check_paths') | |
self.tab_length = md.tab_length | |
super(SnippetPreprocessor, self).__init__() | |
def parse_options(self, string): | |
options = {} | |
if string is not None: | |
for m in RE_OPTIONS.finditer(string): | |
key = m.group('key') | |
value = m.group('value') | |
if value is None: | |
value = True | |
options[key] = value | |
return options | |
def parse_snippets(self, lines, file_name=None): | |
"""Parse snippets snippet.""" | |
new_lines = [] | |
block_lines = [] | |
block = False | |
options = None | |
space = None | |
for line in lines: | |
m = RE_ALL_SNIPPETS.match(line) | |
if m: | |
block = not block | |
if block: | |
options = self.parse_options(m.group('options')) | |
else: | |
html = "\n".join(block_lines) | |
block_lines.clear() | |
if strtobool(options.get('postprocess', 'false')): | |
html = self._md.convert(html) | |
if strtobool(options.get('prettify', 'false')): | |
soup = BeautifulSoup(html, 'html.parser') | |
html = soup.prettify(formatter="html") | |
html = RE_INDENT.sub(lambda m: "\t" * len(m.group(1)), html) | |
new_lines.extend([space + l for l in html.splitlines()]) | |
continue | |
elif not block: | |
new_lines.append(line) | |
continue | |
if block: | |
m = RE_SNIPPET.match(line) | |
if m: | |
space = m.group('space').expandtabs(self.tab_length) | |
path = m.group('snippet').strip() | |
# Block path handling | |
if not path: | |
# Empty path line, insert a blank line | |
block_lines.append('') | |
continue | |
for realpath in self.get_paths(path, options): | |
if realpath in self.seen: | |
# This is in the stack and we don't want an infinite loop! | |
continue | |
self.seen.add(realpath) | |
for line in self.render_snippet(realpath, space, options): | |
block_lines.append(line) | |
try: | |
self.seen.remove(realpath) | |
except KeyError: | |
pass # has already been removed in previous iteration | |
return new_lines | |
def render_snippet(self, filename, space, options): | |
with codecs.open(filename, 'r', encoding=self.encoding) as f: | |
if strtobool(options.get('recursive', 'true')): | |
return self.parse_snippets([l.rstrip('\r\n') for l in f], filename) | |
else: | |
return [l.rstrip('\r\n') for l in f] | |
def get_paths(self, path, options): | |
check = strtobool(options.get('check_paths', str(self.check_paths))) | |
basepath = os.path.realpath(self.base_path) | |
normpath = os.path.normpath(os.path.join(basepath, path)) | |
realpaths = [] | |
for realpath in sorted(iglob(normpath, recursive=True)): | |
if not realpath.startswith(basepath): | |
raise ValueError("Snippet at path %s is not located under %s" % ( | |
path, self.base_path)) | |
if not os.path.exists(realpath): | |
if check: | |
raise IOError("Snippet at path %s could not be found" % path) | |
continue | |
realpaths.append(realpath) | |
return realpaths | |
def run(self, lines): | |
"""Process snippets.""" | |
self.seen = set() | |
return self.parse_snippets(lines) | |
class SnippetExtension(Extension): | |
"""Snippet extension.""" | |
def __init__(self, *args, **kwargs): | |
"""Initialize.""" | |
self.config = { | |
'base_path': [".", "Base path for snippet paths - Default: \"\""], | |
'encoding': ["utf-8", "Encoding of snippets - Default: \"utf-8\""], | |
'check_paths': [False, "Make the build fail if a snippet can't be found - Default: \"false\""] | |
} | |
super(SnippetExtension, self).__init__(*args, **kwargs) | |
def extendMarkdown(self, md): | |
"""Register the extension.""" | |
self.md = md | |
md.registerExtension(self) | |
config = self.getConfigs() | |
snippet = SnippetPreprocessor(config, md) | |
md.preprocessors.register(snippet, "snippet", 32) | |
def makeExtension(*args, **kwargs): | |
"""Return extension.""" | |
return SnippetExtension(*args, **kwargs) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment