115 lines
3.4 KiB
Python
115 lines
3.4 KiB
Python
"""
|
|
The md module extends markdown to add macros.
|
|
|
|
Macros can be added to the markdown text in the following format.
|
|
|
|
{{ MacroName("macro-argument") }}
|
|
|
|
These macros will be rendered using a pluggable mechanism.
|
|
|
|
Apps can provide a hook school_markdown_macro_renderers, a
|
|
dictionary mapping the macro name to the function that to render
|
|
that macro. The function will get the argument passed to the macro
|
|
as argument.
|
|
"""
|
|
|
|
import frappe
|
|
import re
|
|
from bs4 import BeautifulSoup
|
|
import markdown
|
|
from markdown import Extension
|
|
from markdown.inlinepatterns import InlineProcessor
|
|
import xml.etree.ElementTree as etree
|
|
import html as HTML
|
|
|
|
def markdown_to_html(text):
|
|
"""Renders markdown text into html.
|
|
"""
|
|
return markdown.markdown(text, extensions=['fenced_code', MacroExtension()])
|
|
|
|
def find_macros(text):
|
|
"""Returns all macros in the given text.
|
|
|
|
>>> find_macros(text)
|
|
[
|
|
('YouTubeVideo': 'abcd1234')
|
|
('Exercise', 'two-circles'),
|
|
('Exercise', 'four-circles')
|
|
]
|
|
"""
|
|
if not text:
|
|
return []
|
|
macros = re.findall(MACRO_RE, text)
|
|
# remove the quotes around the argument
|
|
return [(name, _remove_quotes(arg)) for name, arg in macros]
|
|
|
|
def _remove_quotes(value):
|
|
"""Removes quotes around a value.
|
|
|
|
Also strips the whitespace.
|
|
|
|
>>> _remove_quotes('"hello"')
|
|
'hello'
|
|
>>> _remove_quotes("'hello'")
|
|
'hello'
|
|
>>> _remove_quotes("hello")
|
|
'hello'
|
|
"""
|
|
return value.strip(" '\"")
|
|
|
|
|
|
def get_macro_registry():
|
|
d = frappe.get_hooks("school_markdown_macro_renderers") or {}
|
|
return {name: frappe.get_attr(klass[0]) for name, klass in d.items()}
|
|
|
|
def render_macro(macro_name, macro_argument):
|
|
# stripping the quotes on either side of the argument
|
|
macro_argument = _remove_quotes(macro_argument)
|
|
|
|
registry = get_macro_registry()
|
|
if macro_name in registry:
|
|
return registry[macro_name](macro_argument)
|
|
else:
|
|
return f"<p>Unknown macro: {macro_name}</p>"
|
|
|
|
MACRO_RE = r'{{ *(\w+)\(([^{}]*)\) *}}'
|
|
|
|
class MacroExtension(Extension):
|
|
"""MacroExtension is a markdown extension to support macro syntax.
|
|
"""
|
|
def extendMarkdown(self, md):
|
|
self.md = md
|
|
pattern = MacroInlineProcessor(MACRO_RE)
|
|
pattern.md = md
|
|
md.inlinePatterns.register(pattern, 'macro', 75)
|
|
|
|
class MacroInlineProcessor(InlineProcessor):
|
|
"""MacroInlineProcessor is class that is handles the logic
|
|
of how to render each macro occurence in the markdown text.
|
|
"""
|
|
def handleMatch(self, m, data):
|
|
"""Handles each macro match and return rendered contents
|
|
for that macro as an etree node.
|
|
"""
|
|
macro = m.group(1)
|
|
arg = m.group(2)
|
|
html = render_macro(macro, arg)
|
|
html = sanitize_html(str(html), macro)
|
|
e = etree.fromstring(html)
|
|
return e, m.start(0), m.end(0)
|
|
|
|
def sanitize_html(html, macro):
|
|
"""Sanotize the html using BeautifulSoup.
|
|
|
|
The markdown processor request the correct markup and crashes on
|
|
any broken tags. This makes sures that all those things are fixed
|
|
before passing to the etree parser.
|
|
"""
|
|
soup = BeautifulSoup(html, features="lxml")
|
|
nodes = soup.body.children
|
|
classname = ""
|
|
if macro == "YouTubeVideo":
|
|
classname = "lesson-video"
|
|
|
|
return "<div class='" + classname + "'>" + "\n".join(str(node) for node in nodes) + "</div>"
|