X-Git-Url: https://git.madduck.net/etc/neomutt.git/blobdiff_plain/429c18815c5cdddb5ae4bed838e357cc1d7ad6d9..ecd2dcdf8b56f8a63c3c6a941fd5d004c6788889:/.config/neomutt/buildmimetree.py diff --git a/.config/neomutt/buildmimetree.py b/.config/neomutt/buildmimetree.py index 1ff0cb0..a4805b2 100755 --- a/.config/neomutt/buildmimetree.py +++ b/.config/neomutt/buildmimetree.py @@ -54,7 +54,11 @@ from contextlib import contextmanager from collections import namedtuple, OrderedDict from markdown.extensions import Extension from markdown.blockprocessors import BlockProcessor -from markdown.inlinepatterns import ImageInlineProcessor, IMAGE_LINK_RE +from markdown.inlinepatterns import ( + SimpleTextInlineProcessor, + ImageInlineProcessor, + IMAGE_LINK_RE, +) from email.utils import make_msgid from urllib import request @@ -121,6 +125,11 @@ def parse_cli_args(*args, **kwargs): help="Only build, don't send the message", ) + parser.add_argument( + "--domain", + help="Domain to use in content IDs", + ) + parser.add_argument( "--tempdir", metavar="DIR", @@ -176,7 +185,6 @@ def parse_cli_args(*args, **kwargs): class File: - class Op(enum.Enum): R = enum.auto() W = enum.auto() @@ -195,10 +203,7 @@ class File: if content and not re.search(r"[r+]", mode): raise RuntimeError("Cannot specify content without read mode") - self._cache = { - File.Op.R: [content] if content else [], - File.Op.W: [] - } + self._cache = {File.Op.R: [content] if content else [], File.Op.W: []} self._lastop = None self._mode = mode self._kwargs = kwargs @@ -250,7 +255,6 @@ class File: return self._file.read() def write(self, s, *, cache=True): - if self._lastop == File.Op.R: try: self._file.seek(0) @@ -350,12 +354,12 @@ class ImageRegistry: def __init__(self): self._images = OrderedDict() - def register(self, path, description=None): + def register(self, path, description=None, *, domain=None): # path = str(pathlib.Path(path).expanduser()) path = os.path.expanduser(path) if path.startswith("/"): path = f"file://{path}" - cid = make_msgid()[1:-1] + cid = make_msgid(domain=domain)[1:-1] self._images[path] = InlineImageInfo(cid, description) return cid @@ -476,11 +480,22 @@ def apply_styling(html, css): ) +# [ FORMAT=FLOWED HANDLING ] ################################################## + + +class FormatFlowedNewlineExtension(Extension): + FFNL_RE = r"(?!\S)(\s)\n" + + def extendMarkdown(self, md): + ffnl = SimpleTextInlineProcessor(self.FFNL_RE) + md.inlinePatterns.register(ffnl, "ffnl", 125) + + # [ QUOTE HANDLING ] ########################################################## class QuoteToAdmonitionExtension(Extension): - class EmailQuoteBlockProcessor(BlockProcessor): + class BlockProcessor(BlockProcessor): RE = re.compile(r"(?:^|\n)>\s*(.*)") def __init__(self, parser): @@ -528,9 +543,7 @@ class QuoteToAdmonitionExtension(Extension): admonition[0].set("class", "admonition-title") with self.disable(): - self.parser.parseChunk( - admonition, "\n".join(quotelines) - ) + self.parser.parseChunk(admonition, "\n".join(quotelines)) @contextmanager def disable(self): @@ -545,7 +558,7 @@ class QuoteToAdmonitionExtension(Extension): def extendMarkdown(self, md): md.registerExtension(self) - email_quote_proc = self.EmailQuoteBlockProcessor(md.parser) + email_quote_proc = self.BlockProcessor(md.parser) md.parser.blockprocessors.register(email_quote_proc, "emailquote", 25) @@ -692,6 +705,7 @@ def convert_markdown_to_html( tempdir=None, extensions=None, extension_configs=None, + domain=None, ): # TODO extension_configs need to be handled differently extension_configs = extension_configs or {} @@ -700,6 +714,7 @@ def convert_markdown_to_html( ] = _CODEHILITE_CLASS extensions = extensions or [] + extensions.append(FormatFlowedNewlineExtension()) extensions.append(QuoteToAdmonitionExtension()) draft = draft_f.read() @@ -725,7 +740,7 @@ def convert_markdown_to_html( for img in soup.find_all("img"): uri = img.attrs["src"] desc = img.attrs.get("title", img.attrs.get("alt")) - cid = image_registry.register(uri, desc) + cid = image_registry.register(uri, desc, domain=domain) img.attrs["src"] = f"cid:{cid}" htmlsig = str(soup) @@ -955,6 +970,7 @@ def do_massage( only_build=False, max_other_attachments=20, tempdir=None, + domain=None, debug_commands=False, debug_walk=False, ): @@ -976,6 +992,7 @@ def do_massage( related_to_html_only=related_to_html_only, tempdir=tempdir, extensions=extensions, + domain=domain, ) mimetree = MIMETreeDFWalker(debug=debug_walk) @@ -1148,6 +1165,7 @@ if __name__ == "__main__": max_other_attachments=args.max_number_other_attachments, only_build=args.only_build, tempdir=args.tempdir, + domain=args.domain, debug_commands=args.debug_commands, debug_walk=args.debug_walk, ) @@ -1768,7 +1786,7 @@ try: with ( File() as draft_f, File(mode="w") as cmd_f, - File(content=css) as css_f + File(content=css) as css_f, ): do_massage( draft_f=draft_f, @@ -1859,6 +1877,15 @@ try: assert not cid.endswith(">") assert const1 in reg + @pytest.mark.imgproc + def test_image_registry_domain(self, const1, const2): + reg = ImageRegistry() + cid = reg.register(const1, domain=const2) + assert f"@{const2}" in cid + assert not cid.startswith("<") + assert not cid.endswith(">") + assert const1 in reg + @pytest.mark.imgproc def test_image_registry_file_uri(self, const1): reg = ImageRegistry() @@ -1916,9 +1943,7 @@ try: "This is the plain-text version", ) htmlsig = "HTML Signature from {path} but as a string" - html = ( - f'

{htmlsig.format(path=fakepath2)}

' - ) + html = f'

{htmlsig.format(path=fakepath2)}

' sig_f = fakefilefactory(fakepath2, content=html) @@ -2117,6 +2142,46 @@ try: == mailparts[-2] ) + @pytest.mark.converter + def test_converter_format_flowed_with_nl2br( + self, fakepath, fakefilefactory + ): + mailparts = ( + "This is format=flowed text ", + "with spaces at the end ", + "and there ought be no newlines.", + "", + "[link](https://example.org) ", + "and text.", + "", + "[link text ", + "broken up](https://example.org).", + "", + "This is on a new line with a hard break ", + "due to the double space", + ) + with fakefilefactory( + fakepath, content="\n".join(mailparts) + ) as draft_f: + convert_markdown_to_html( + draft_f, extensions=["nl2br"], filefactory=fakefilefactory + ) + + soup = bs4.BeautifulSoup( + fakefilefactory[fakepath.with_suffix(".html")].read(), + "html.parser", + ) + import ipdb + + p = soup.p.extract().text + assert "".join(mailparts[0:3]) == p + p = ''.join(map(str, soup.p.extract().contents)) + assert p == 'link and text.' + p = ''.join(map(str, soup.p.extract().contents)) + assert ( + p == 'link text broken up.' + ) + @pytest.mark.fileio def test_file_class_contextmanager(self, const1, monkeypatch): state = dict(o=False, c=False)