self._walk(
root,
- stack=[],
+ ancestry=[],
visitor_fn=visitor_fn or self._visitor_fn,
)
- def _walk(self, node, *, stack, visitor_fn):
+ def _walk(self, node, *, ancestry, visitor_fn):
# Let's start by enumerating the parts at the current level. At the
- # root level, stack will be the empty list, and we expect a multipart/*
- # container at this level. Later, e.g. within a mutlipart/alternative
- # container, the subtree will just be the alternative parts, while the
- # top of the stack will be the multipart/alternative container, which
- # we will process after the following loop.
-
- lead = f"{'| '*len(stack)}|-"
+ # root level, ancestry will be the empty list, and we expect a
+ # multipart/* container at this level. Later, e.g. within a
+ # mutlipart/alternative container, the subtree will just be the
+ # alternative parts, while the top of the ancestry will be the
+ # multipart/alternative container, which we will process after the
+ # following loop.
+
+ lead = f"{'│ '*len(ancestry)}"
if isinstance(node, Multipart):
self.debugprint(
- f"{lead}{node} parents={[s.subtype for s in stack]}"
+ f"{lead}├{node} ancestry={[s.subtype for s in ancestry]}"
)
- # Depth-first, so push the current container onto the stack,
- # then descend …
- stack.append(node)
- self.debugprint("| " * (len(stack) + 1))
+ # Depth-first, so push the current container onto the ancestry
+ # stack, then descend …
+ ancestry.append(node)
+ self.debugprint(lead + "│ " * 2)
for child in node.children:
self._walk(
child,
- stack=stack,
+ ancestry=ancestry,
visitor_fn=visitor_fn,
)
- self.debugprint("| " * len(stack))
- assert stack.pop() == node
+ assert ancestry.pop() == node
else:
- self.debugprint(f"{lead}{node}")
+ self.debugprint(f"{lead}├{node}")
+
+ if False and ancestry:
+ self.debugprint(lead[:-1] + " │")
if visitor_fn:
- visitor_fn(node, stack, debugprint=self.debugprint)
+ visitor_fn(node, ancestry, debugprint=self.debugprint)
def debugprint(self, s, **kwargs):
if self._debug:
mimetree = MIMETreeDFWalker(debug=debug_walk)
- def visitor_fn(item, stack, *, debugprint=None):
+ state = dict(pos=1, tags={}, parts=1)
+
+ def visitor_fn(item, ancestry, *, debugprint=None):
"""
Visitor function called for every node (part) of the MIME tree,
depth-first, and responsible for telling NeoMutt how to assemble
# The original source already exists in the NeoMutt tree, but
# the underlying file may have been modified, so we need to
# update the encoding, but that's it:
+ cmds.push("<first-entry>")
cmds.push("<update-encoding>")
+
+ # We really just need to be able to assume that at this point,
+ # NeoMutt is at position 1, and that we've processed only this
+ # part so far. Nevermind about actual attachments, we can
+ # safely ignore those as they stay at the end.
+ assert state["pos"] == 1
+ assert state["parts"] == 1
else:
# … whereas all other parts need to be added, and they're all
# considered to be temporary and inline:
cmds.push(f"<attach-file>{item.path}<enter>")
cmds.push("<toggle-unlink><toggle-disposition>")
+ # This added a part at the end of the list of parts, and that's
+ # just how many parts we've seen so far, so it's position in
+ # the NeoMutt compose list is the count of parts
+ state["parts"] += 1
+ state["pos"] = state["parts"]
+
# If the item (including the original) comes with additional
# information, then we might just as well update the NeoMutt
# tree now:
elif isinstance(item, Multipart):
# This node has children, but we already visited them (see
- # above), and so they have been tagged in NeoMutt's compose
- # window. Now it's just a matter of telling NeoMutt to do the
- # appropriate grouping:
+ # above). The tags dictionary of State should contain a list of
+ # their positions in the NeoMutt compose window, so iterate those
+ # and tag the parts there:
+ for tag in state["tags"][item]:
+ cmds.push(f"<jump>{tag}<enter><tag-entry>")
+
if item.subtype == "alternative":
cmds.push("<group-alternatives>")
elif item.subtype in ("relative", "related"):
cmds.push("<group-related>")
elif item.subtype == "multilingual":
cmds.push("<group-multilingual>")
+ else:
+ raise NotImplementedError(
+ f"Handling of multipart/{item.subtype} is not implemented"
+ )
+
+ state["pos"] -= len(state["tags"][item]) - 1
+ state["parts"] += 1
+ del state["tags"][item]
else:
# We should never get here
- assert not "is valid part"
+ raise RuntimeError(f"Type {type(item)} is unexpected: {item}")
# If the item has a description, we might just as well add it
if item.desc:
cmds.push(f"<edit-description>{KILL_LINE}{item.desc}<enter>")
- # Finally, if we're at non-root level, tag the new container,
- # as it might itself be part of a container, to be processed
- # one level up:
- if stack:
- cmds.push("<tag-entry>")
+ if ancestry:
+ # If there's an ancestry, record the current (assumed) position in
+ # the NeoMutt compose window as needed-to-tag by our direct parent
+ # (i.e. the last item of the ancestry)
+ state["tags"].setdefault(ancestry[-1], []).append(state["pos"])
+
+ lead = "│ " * (len(ancestry) + 1) + "* "
+ debugprint(
+ f"{lead}ancestry={[a.subtype for a in ancestry]}\n"
+ f"{lead}children_positions={state['tags'][ancestry[-1]]}\n"
+ f"{lead}pos={state['pos']}, parts={state['parts']}"
+ )
# -----------------
# End of visitor_fn
mimetree = MIMETreeDFWalker()
items = []
- def visitor_fn(item, stack, debugprint):
+ def visitor_fn(item, ancestry, debugprint):
items.append(item)
p = Part("text", "plain", const1)
):
items = []
- def visitor_fn(item, stack, debugprint):
+ def visitor_fn(item, ancestry, debugprint):
items.append(item)
mimetree = MIMETreeDFWalker(visitor_fn=visitor_fn)