# - python3-markdown
# Optional:
# - pytest
-# - Pynliner
-# - Pygments, if installed, then syntax highlighting is enabled
+# - Pynliner, provides --css-file and thus inline styling of HTML output
+# - Pygments, then syntax highlighting for fenced code is enabled
#
# Latest version:
# https://git.madduck.net/etc/neomutt.git/blob_plain/HEAD:/.config/neomutt/buildmimetree.py
)
)
parser.epilog = (
- "Copyright © 2022 martin f. krafft <madduck@madduck.net>.\n"
+ "Copyright © 2023 martin f. krafft <madduck@madduck.net>.\n"
"Released under the MIT licence"
)
"--extensions",
type=str,
default="",
- help="Markdown extension to use (comma-separated list)"
+ help="Markdown extension to use (comma-separated list)",
)
+ if _PYNLINER:
+ parser.add_argument(
+ "--css-file",
+ type=str,
+ default="",
+ help="CSS file to merge with the final HTML",
+ )
+ else:
+ parser.set_defaults(css_file=None)
+
parser.add_argument(
"--only-build",
action="store_true",
)
subp = parser.add_subparsers(help="Sub-command parsers", dest="mode")
- massage_p = subp.add_parser("massage", help="Massaging phase (internal use)")
+ massage_p = subp.add_parser(
+ "massage", help="Massaging phase (internal use)"
+ )
massage_p.add_argument(
"--write-commands-to",
+ "-o",
metavar="PATH",
dest="cmdpath",
+ required=True,
help="Temporary file path to write commands to",
)
return self._images
-def markdown_with_inline_image_support(text, *, extensions=None):
+def markdown_with_inline_image_support(
+ text, *, extensions=None, extension_configs=None
+):
inline_image_handler = InlineImageExtension()
extensions = extensions or []
extensions.append(inline_image_handler)
- mdwn = markdown.Markdown(extensions=extensions)
+ mdwn = markdown.Markdown(
+ extensions=extensions, extension_configs=extension_configs
+ )
htmltext = mdwn.convert(text)
images = inline_image_handler.get_images()
return text, htmltext, images
+# [ CSS STYLING ] #############################################################
+
+try:
+ import pynliner
+
+ _PYNLINER = True
+
+except ImportError:
+ _PYNLINER = False
+
+try:
+ from pygments.formatters import get_formatter_by_name
+
+ _CODEHILITE_CLASS = "codehilite"
+
+ _PYGMENTS_CSS = get_formatter_by_name(
+ "html", style="default"
+ ).get_style_defs(f".{_CODEHILITE_CLASS}")
+
+except ImportError:
+ _PYGMENTS_CSS = None
+
+
+def apply_styling(html, css):
+ return (
+ pynliner.Pynliner()
+ .from_string(html)
+ .with_cssString("\n".join(s for s in [_PYGMENTS_CSS, css] if s))
+ .run()
+ )
+
+
# [ PARTS GENERATION ] ########################################################
def __str__(self):
return f"<multipart/{self.subtype}> children={len(self.children)}"
+ def __hash__(self):
+ return hash(str(self.subtype) + "".join(str(self.children)))
+
def filewriter_fn(path, content, mode="w", **kwargs):
with open(path, mode, **kwargs) as out_f:
):
relparts = []
for path, info in images.items():
+ if path.startswith("cid:"):
+ continue
+
data = request.urlopen(path)
mimetype = data.headers["Content-Type"]
filewriter_fn(path, data.read(), "w+b")
relparts.append(
- Part(*mimetype.split("/"), path, cid=info.cid, desc=f"Image: {info.desc}")
+ Part(
+ *mimetype.split("/"),
+ path,
+ cid=info.cid,
+ desc=f"Image: {info.desc}",
+ )
)
return relparts
origtext,
draftpath,
*,
+ cssfile=None,
filewriter_fn=filewriter_fn,
tempdir=None,
extensions=None,
+ extension_configs=None,
):
+ # TODO extension_configs need to be handled differently
+ extension_configs = extension_configs or {}
+ extension_configs.setdefault("pymdownx.highlight", {})
+ extension_configs["pymdownx.highlight"]["css_class"] = _CODEHILITE_CLASS
+
origtext, htmltext, images = markdown_with_inline_image_support(
- origtext, extensions=extensions
+ origtext, extensions=extensions, extension_configs=extension_configs
)
filewriter_fn(draftpath, origtext, encoding="utf-8")
"text", "plain", draftpath, "Plain-text version", orig=True
)
+ htmltext = apply_styling(htmltext, cssfile)
+
htmlpath = draftpath.with_suffix(".html")
filewriter_fn(
htmlpath, htmltext, encoding="utf-8", errors="xmlcharrefreplace"
class MIMETreeDFWalker:
def __init__(self, *, visitor_fn=None, debug=False):
- self._visitor_fn = visitor_fn
+ self._visitor_fn = visitor_fn or self._echovisit
self._debug = debug
+ def _echovisit(self, node, ancestry, debugprint):
+ debugprint(f"node={node} ancestry={ancestry}")
+
def walk(self, root, *, visitor_fn=None):
"""
Recursive function to implement a depth-dirst walk of the MIME-tree
cmd_f,
*,
extensions=None,
+ cssfile=None,
converter=convert_markdown_to_html,
only_build=False,
tempdir=None,
cmds.flush()
extensions = extensions.split(",") if extensions else []
- tree = converter(draft_f.read(), draftpath, tempdir=tempdir, extensions=extensions)
+ tree = converter(
+ draft_f.read(),
+ draftpath,
+ cssfile=cssfile,
+ tempdir=tempdir,
+ extensions=extensions,
+ )
mimetree = MIMETreeDFWalker(debug=debug_walk)
depth-first, and responsible for telling NeoMutt how to assemble
the tree.
"""
- KILL_LINE=r'\Ca\Ck'
+ KILL_LINE = r"\Ca\Ck"
if isinstance(item, Part):
# We've hit a leaf-node, i.e. an alternative or a related part
pathlib.Path(args.MAILDRAFT),
cmd_f,
extensions=args.extensions,
+ cssfile=args.css_file,
only_build=args.only_build,
tempdir=args.tempdir,
debug_commands=args.debug_commands,
return StringIO(text or const1)
def test_do_massage_basic(self, const1, string_io, capsys):
- def converter(drafttext, draftpath, extensions, tempdir):
+ def converter(drafttext, draftpath, cssfile, extensions, tempdir):
return Part("text", "plain", draftpath, orig=True)
do_massage(
def test_do_massage_fulltree(
self, string_io, const1, basic_mime_tree, capsys
):
- def converter(drafttext, draftpath, extensions, tempdir):
+ def converter(drafttext, draftpath, cssfile, extensions, tempdir):
return basic_mime_tree
do_massage(
assert tree.children[1].path == written[0]
assert written[1] == request.urlopen(test_png).read()
+ def test_converter_tree_inline_image_cid(
+ self, const1, fake_filewriter
+ ):
+ text = f"![inline base64 image](cid:{const1})"
+ path = pathlib.Path(const1)
+ tree = convert_markdown_to_html(
+ text,
+ path,
+ filewriter_fn=fake_filewriter,
+ related_to_html_only=False,
+ )
+ assert len(tree.children) == 2
+ assert tree.children[0].cid != const1
+ assert tree.children[0].type != "image"
+ assert tree.children[1].cid != const1
+ assert tree.children[1].type != "image"
+
def test_inline_image_collection(
self, test_png, const1, const2, fake_filewriter
):
assert relparts[0].cid == const1
assert relparts[0].desc.endswith(const2)
+ def test_apply_stylesheet(self):
+ if _PYNLINER:
+ html = "<p>Hello, world!</p>"
+ css = "p { color:red }"
+ out = apply_styling(html, css)
+ assert 'p style="color' in out
+
+ def test_apply_stylesheet_pygments(self):
+ if _PYGMENTS_CSS:
+ html = (
+ f'<div class="{_CODEHILITE_CLASS}">'
+ "<pre>def foo():\n return</pre></div>"
+ )
+ out = apply_styling(html, _PYGMENTS_CSS)
+ assert f'{_CODEHILITE_CLASS}" style="' in out
+
+
except ImportError:
pass