git-ew 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,185 @@
1
+ # SPDX-License-Identifier: ISC
2
+
3
+ # Copyright (c) 2021, Timothée Mazzucotelli and contributors
4
+
5
+ # Permission to use, copy, modify, and/or distribute this software for any
6
+ # purpose with or without fee is hereby granted, provided that the above
7
+ # copyright notice and this permission notice appear in all copies.
8
+
9
+ # THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ # WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ # MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ # ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ # WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ # ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ # OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ # Thread organization and rendering utilities for git-ew.
18
+
19
+ from __future__ import annotations
20
+
21
+ from dataclasses import dataclass
22
+ from typing import TYPE_CHECKING
23
+
24
+ import markdown
25
+ from pymdownx.emoji import to_svg, twemoji
26
+
27
+ if TYPE_CHECKING:
28
+ from git_ew._internal.models import Message
29
+
30
+ # Markdown instance with configured extensions
31
+ _md = markdown.Markdown(
32
+ extensions=[
33
+ "abbr",
34
+ "admonition",
35
+ "attr_list",
36
+ "def_list",
37
+ "footnotes",
38
+ "md_in_html",
39
+ "nl2br",
40
+ "toc",
41
+ "pymdownx.arithmatex",
42
+ "pymdownx.betterem",
43
+ "pymdownx.caret",
44
+ "pymdownx.details",
45
+ "pymdownx.emoji",
46
+ "pymdownx.highlight",
47
+ "pymdownx.inlinehilite",
48
+ "pymdownx.keys",
49
+ "pymdownx.magiclink",
50
+ "pymdownx.mark",
51
+ "pymdownx.smartsymbols",
52
+ "pymdownx.superfences",
53
+ "pymdownx.tabbed",
54
+ "pymdownx.tasklist",
55
+ "pymdownx.tilde",
56
+ ],
57
+ extension_configs={
58
+ "toc": {"permalink": True},
59
+ "pymdownx.arithmatex": {"generic": True},
60
+ "pymdownx.betterem": {"smart_enable": "all"},
61
+ "pymdownx.emoji": {
62
+ "emoji_index": twemoji,
63
+ "emoji_generator": to_svg,
64
+ },
65
+ "pymdownx.tabbed": {"alternate_style": True},
66
+ "pymdownx.tasklist": {"custom_checkbox": True},
67
+ },
68
+ )
69
+
70
+
71
+ def render_markdown(text: str) -> str:
72
+ """Render plain text as markdown HTML.
73
+
74
+ Args:
75
+ text: Plain text to render.
76
+
77
+ Returns:
78
+ HTML string.
79
+ """
80
+ _md.reset()
81
+ return _md.convert(text)
82
+
83
+
84
+ def _split_quoted_reply(body: str) -> tuple[str, str]:
85
+ """Split trailing quoted lines from an email body.
86
+
87
+ Args:
88
+ body: Email body text.
89
+
90
+ Returns:
91
+ The new text and trailing quoted text.
92
+ """
93
+ if not body:
94
+ return "", ""
95
+
96
+ lines = body.rstrip().splitlines()
97
+ quote_start_idx = len(lines)
98
+ while quote_start_idx > 0 and lines[quote_start_idx - 1].startswith(">"):
99
+ quote_start_idx -= 1
100
+
101
+ return (
102
+ "\n".join(lines[:quote_start_idx]).strip(),
103
+ "\n".join(lines[quote_start_idx:]).strip(),
104
+ )
105
+
106
+
107
+ @dataclass
108
+ class ThreadNode:
109
+ """Represents a node in a thread tree."""
110
+
111
+ message: Message
112
+ """The message at this node."""
113
+ children: list[ThreadNode]
114
+ """Child nodes in the thread tree."""
115
+
116
+
117
+ @dataclass
118
+ class _RenderedMessage:
119
+ """Store a message and its presentation-only content."""
120
+
121
+ message: Message
122
+ body: str
123
+ quoted_body: str
124
+ children: list[_RenderedMessage]
125
+
126
+
127
+ def build_thread_tree(messages: list[Message]) -> list[ThreadNode]:
128
+ """Build a tree structure from a flat list of messages.
129
+
130
+ Args:
131
+ messages: List of messages in the thread.
132
+
133
+ Returns:
134
+ List of root ThreadNodes.
135
+ """
136
+ # Create nodes for each message
137
+ nodes = {msg.message_id: ThreadNode(message=msg, children=[]) for msg in messages}
138
+
139
+ # Build the tree by linking children to parents
140
+ roots = []
141
+
142
+ for msg in messages:
143
+ node = nodes[msg.message_id]
144
+
145
+ if msg.in_reply_to and msg.in_reply_to in nodes:
146
+ # This message is a reply to another message
147
+ parent_node = nodes[msg.in_reply_to]
148
+ parent_node.children.append(node)
149
+ else:
150
+ # This is a root message
151
+ roots.append(node)
152
+
153
+ return roots
154
+
155
+
156
+ def thread_to_nested_structure(roots: list[ThreadNode], *, flatten: bool = True) -> list[_RenderedMessage]:
157
+ """Convert thread tree to nested structure, with single-children popped out to sibling level.
158
+
159
+ Also detects and marks quoted sections in message bodies.
160
+
161
+ Args:
162
+ roots: List of root ThreadNodes.
163
+ flatten: Place a sole child beside its parent when true.
164
+
165
+ Returns:
166
+ Nested list of messages.
167
+ """
168
+ result: list[_RenderedMessage] = []
169
+ for root in roots:
170
+ body, quoted_body = _split_quoted_reply(root.message.body)
171
+ rendered = _RenderedMessage(
172
+ message=root.message,
173
+ body=body,
174
+ quoted_body=quoted_body,
175
+ children=[],
176
+ )
177
+ if flatten and len(root.children) == 1:
178
+ result.append(rendered)
179
+ result.extend(thread_to_nested_structure(root.children, flatten=flatten))
180
+ elif root.children:
181
+ rendered.children = thread_to_nested_structure(root.children, flatten=flatten)
182
+ result.append(rendered)
183
+ else:
184
+ result.append(rendered)
185
+ return result
git_ew/py.typed ADDED
File without changes