Skip to content

Commit 1f0947e

Browse files
committed
Add pelican render_math plugin
1 parent d098b4a commit 1f0947e

4 files changed

Lines changed: 607 additions & 0 deletions

File tree

pelicanconf.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@
8989
"download_metadata",
9090
"draft_override",
9191
"md_yaml",
92+
"render_math",
9293
]
9394

9495

plugins/render_math/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
from .math import * # noqa: F403,PGH004,RUF100
Lines changed: 194 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,194 @@
1+
"""Pelican Mathjax Markdown Extension.
2+
3+
An extension for the Python-Markdown module that enables Pelican to process MathJax.
4+
This extension enables Pelican to use Mathjax as a first-class citizen.
5+
"""
6+
7+
from xml.etree.ElementTree import Element
8+
9+
import markdown
10+
from markdown.util import AtomicString
11+
12+
13+
class PelicanMathJaxPattern(markdown.inlinepatterns.Pattern):
14+
"""Inline markdown processing that matches MathJax."""
15+
16+
def __init__(self, pelican_mathjax_extension, tag, pattern):
17+
super().__init__(pattern)
18+
self.math_tag_class = pelican_mathjax_extension.getConfig("math_tag_class")
19+
self.pelican_mathjax_extension = pelican_mathjax_extension
20+
self.tag = tag
21+
22+
def handleMatch(self, m):
23+
node = Element(self.tag)
24+
node.set("class", self.math_tag_class)
25+
26+
prefix = "\\(" if m.group("prefix") == "$" else m.group("prefix")
27+
suffix = "\\)" if m.group("suffix") == "$" else m.group("suffix")
28+
node.text = markdown.util.AtomicString(prefix + m.group("math") + suffix)
29+
30+
# If mathjax was successfully matched, then JavaScript needs to be added
31+
# for rendering. The boolean below indicates this
32+
self.pelican_mathjax_extension.mathjax_needed = True
33+
return node
34+
35+
36+
class PelicanMathJaxCorrectDisplayMath(markdown.treeprocessors.Treeprocessor):
37+
"""Correct invalid HTML when a <div> is placed inside a <p> for displayed math."""
38+
39+
def __init__(self, pelican_mathjax_extension):
40+
self.pelican_mathjax_extension = pelican_mathjax_extension
41+
42+
def correct_html(self, root, children, div_math, insert_idx, text):
43+
"""Separate out <div class="math"> from the parent tag <p>.
44+
45+
Anything in-between is put into its own parent <p> tag.
46+
"""
47+
current_idx = 0
48+
49+
for idx in div_math:
50+
el = Element("p")
51+
el.text = text
52+
el.extend(children[current_idx:idx])
53+
54+
# Test to ensure that empty <p> is not inserted
55+
if len(el) != 0 or (el.text and not el.text.isspace()):
56+
root.insert(insert_idx, el)
57+
insert_idx += 1
58+
59+
text = children[idx].tail
60+
children[idx].tail = None
61+
root.insert(insert_idx, children[idx])
62+
insert_idx += 1
63+
current_idx = idx + 1
64+
65+
el = Element("p")
66+
el.text = text
67+
el.extend(children[current_idx:])
68+
69+
if len(el) != 0 or (el.text and not el.text.isspace()):
70+
root.insert(insert_idx, el)
71+
72+
def run(self, root):
73+
"""Search for <div class="math"> that are children in <p> tags.
74+
75+
And correct the invalid HTML that results.
76+
"""
77+
math_tag_class = self.pelican_mathjax_extension.getConfig("math_tag_class")
78+
79+
for parent in root:
80+
div_math = []
81+
children = list(parent)
82+
83+
for div in parent.findall("div"):
84+
if div.get("class") == math_tag_class:
85+
div_math.append(children.index(div))
86+
87+
# Do not process further if no displayed math has been found
88+
if not div_math:
89+
continue
90+
91+
insert_idx = list(root).index(parent)
92+
self.correct_html(root, children, div_math, insert_idx, parent.text)
93+
root.remove(
94+
parent
95+
) # Parent must be removed last for correct insertion index
96+
97+
return root
98+
99+
100+
class PelicanMathJaxAddJavaScript(markdown.treeprocessors.Treeprocessor):
101+
"""Tree Processor for adding Mathjax JavaScript to the blog."""
102+
103+
def __init__(self, pelican_mathjax_extension):
104+
self.pelican_mathjax_extension = pelican_mathjax_extension
105+
106+
def run(self, root):
107+
# If no mathjax was present, then exit
108+
if not self.pelican_mathjax_extension.mathjax_needed:
109+
return root
110+
111+
# Add the mathjax script to the html document
112+
mathjax_script = Element("script")
113+
mathjax_script.set("type", "text/javascript")
114+
mathjax_script.text = AtomicString(
115+
self.pelican_mathjax_extension.getConfig("mathjax_script")
116+
)
117+
root.append(mathjax_script)
118+
119+
# Reset the boolean switch to false so that script is only added
120+
# to other pages if needed
121+
self.pelican_mathjax_extension.mathjax_needed = False
122+
return root
123+
124+
125+
class PelicanMathJaxExtension(markdown.Extension):
126+
"""Markdown extension enabling MathJax processing in Markdown for Pelican."""
127+
128+
def __init__(self, config):
129+
try:
130+
# Needed for markdown versions >= 2.5
131+
self.config["mathjax_script"] = ["", "Mathjax JavaScript script"]
132+
self.config["math_tag_class"] = [
133+
"math",
134+
"The class of the tag in which mathematics is wrapped",
135+
]
136+
self.config["auto_insert"] = [
137+
True,
138+
"Determines if mathjax script is automatically inserted into content",
139+
]
140+
super().__init__(**config)
141+
except AttributeError:
142+
# Markdown versions < 2.5
143+
config["mathjax_script"] = [
144+
config["mathjax_script"],
145+
"Mathjax JavaScript script",
146+
]
147+
config["math_tag_class"] = [
148+
config["math_tag_class"],
149+
"The class of the tag in which mathematic is wrapped",
150+
]
151+
config["auto_insert"] = [
152+
config["auto_insert"],
153+
"Determines if mathjax script is automatically inserted into content",
154+
]
155+
super().__init__(config)
156+
157+
# Used as a flag to determine if javascript
158+
# needs to be injected into a document
159+
self.mathjax_needed = False
160+
161+
def extendMarkdown(self, md):
162+
# Regex to detect mathjax
163+
mathjax_inline_regex = r"(?P<prefix>\$)(?P<math>.+?)(?P<suffix>(?<!\s)\2)"
164+
mathjax_display_regex = (
165+
r"(?P<prefix>\$\$|\\begin\{(.+?)\})(?P<math>.+?)(?P<suffix>\2|\\end\{\3\})"
166+
)
167+
168+
# Process mathjax before escapes are processed since escape processing will
169+
# interfere with mathjax. The order in which the displayed and inlined math
170+
# is registered below matters: we should have higher priority than 'escape',
171+
# which has 180.
172+
md.inlinePatterns.register(
173+
PelicanMathJaxPattern(self, "div", mathjax_display_regex),
174+
"mathjax_displayed",
175+
186,
176+
)
177+
md.inlinePatterns.register(
178+
PelicanMathJaxPattern(self, "span", mathjax_inline_regex),
179+
"mathjax_inlined",
180+
185,
181+
)
182+
183+
# Correct the invalid HTML that results from the displayed math
184+
# (<div> tag within a <p> tag)
185+
md.treeprocessors.register(
186+
PelicanMathJaxCorrectDisplayMath(self), "mathjax_correctdisplayedmath", 15
187+
)
188+
189+
# If necessary, add the JavaScript Mathjax library to the document. This must
190+
# be last in the ordered dict (hence it is given the position '_end')
191+
if self.getConfig("auto_insert"):
192+
md.treeprocessors.register(
193+
PelicanMathJaxAddJavaScript(self), "mathjax_addjavascript", 0
194+
)

0 commit comments

Comments
 (0)