-
Notifications
You must be signed in to change notification settings - Fork 1
/
aur_talk.py
executable file
·159 lines (142 loc) · 6.49 KB
/
aur_talk.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
#!/usr/bin/env python3
'''aur_talk.py <PACKAGE-NAME>: fetch and display and AUR package's comments.'''
import argparse
import textwrap
import re
import shutil
from urllib.request import urlopen
from urllib.error import HTTPError
import lxml.html
from lxml import etree
import html2text
URL = 'https://aur.archlinux.org/packages/{}/?O=0&PP={}'
# Setup HTML to Markdown converter.
MARKDOWN_CONVERTER = html2text.HTML2Text()
MARKDOWN_CONVERTER.ignore_links = False
MARKDOWN_CONVERTER.ul_item_mark = '-'
RE_STRONG = re.compile('\033\\[1m')
RE_EMPHASIS = re.compile('\033\\[3m')
def fetch_package_comments(package_name, num_comments):
'''Return an AUR package's comments section as a list of HtmlElements.'''
url = URL.format(package_name, num_comments)
try:
with urlopen(url) as resp:
html = lxml.html.parse(resp)
except HTTPError:
print('Package "{}" not found.'.format(package_name))
exit(1)
return html.xpath(
'/html/body/div[2]/div[@class="comments package-comments"]')
def print_author_and_date(element, args):
'''Format and display a comment's author/date.'''
text = element.text_content().strip()
text = text if args.width == 0 else textwrap.fill(text, args.width,
break_long_words=False)
print('\033[1m{}\033[0m'.format(text))
def correct_ansi_escapes(text, regex, repl):
'''Replace every 2nd ANSI escape code (regex) with a reset code (repl).'''
offset = 0
for match in list(re.finditer(regex, text))[1::2]:
pos = match.span()[1] - 2 + offset
text = text[:pos] + repl + text[pos + 1:]
offset += len(repl) - 1
return text
def print_comment_body(element, args):
'''Format and display a comment's body.'''
text = etree.tostring(element).decode('utf-8')
text = MARKDOWN_CONVERTER.handle(text).strip().replace('\n\n\n', '\n')
if args.bold:
text = correct_ansi_escapes(text, RE_STRONG, '22m\033[2')
if args.italic:
text = correct_ansi_escapes(text, RE_EMPHASIS, '23')
# html2text does not wrap list items, so we're gonna have to do it
# manually.
if args.width:
lines = []
for line in text.split('\n'):
# If it's a list item, wrap it and indent it correctly.
if line.startswith(' - '):
num_spaces = len(line) - len(line.lstrip(' '))
lines_ = textwrap.wrap(line.strip(),
max(1, args.width - num_spaces),
break_long_words=False,
initial_indent=' ',
subsequent_indent=' ')
lines.extend(lines_)
else:
lines.append(line)
text = '\n'.join(lines)
print('\033[2m{}\033[0m\n'.format(text))
def print_package_comments(args):
'''Fetch and display an AUR package's comments.'''
comments_sections = fetch_package_comments(args.package_name,
args.num_comments)
if not comments_sections:
print('No comments.')
return
has_pinned_comments = comments_sections[0].xpath(
'div/h3/span[@class="text"]/text()')[0] == 'Pinned Comments'
if args.latest_only and len(comments_sections) > 1:
comments_sections.pop(0)
elif args.pinned_only and has_pinned_comments:
comments_sections.pop(1)
elif args.pinned_only and not has_pinned_comments:
print('No pinned comments.')
return
# Handle the comment sections ("Pinned" and/or "Latest Comments").
for section in comments_sections:
# Only print the section's title if there are multiple sections.
if len(comments_sections) > 1:
title = section[0].xpath('h3/span[@class="text"]/text()')[0]
title = ' {} '.format(title)
print('\033[1m{:—^{cols}}\033[0m\n'.format(
title, cols=80 if args.width == 0 else args.width))
# Print the comments.
for element in section[1:]:
if element.tag == 'h4':
print_author_and_date(element, args)
else:
print_comment_body(element, args)
def main():
'''Parse command-line arguments and fetch/display comments.'''
arg_parser = argparse.ArgumentParser(
description='Display AUR comments for PACKAGE-NAME.')
group = arg_parser.add_mutually_exclusive_group()
group.add_argument('-a', '--all', dest='num_comments', default=10,
action='store_const', const=1000,
help='Fetch all comments.')
group.add_argument('-n', '--num-comments', type=int,
help=('Number of comments to fetch. Pinned comments are'
' always fetched. Default is 10.'))
group = arg_parser.add_mutually_exclusive_group()
group.add_argument('-p', '--pinned-only', action='store_true',
help='Display the pinned comments only.')
group.add_argument('-l', '--latest-only', action='store_true',
help='Display the latest comments only.')
arg_parser.add_argument('-w', '--width', type=int, default=0,
help=('Number of columns for formatting output. '
'Default is full available width.'))
arg_parser.add_argument('-b', '--bold', action='store_true',
help=('Render **bold** text. May not work '
'depending on your terminal.'))
arg_parser.add_argument('-i', '--italic', action='store_true',
help=('Render _italic_ text. May not work '
'depending on your terminal.'))
arg_parser.add_argument('package_name', metavar='PACKAGE-NAME',
help=argparse.SUPPRESS)
args = arg_parser.parse_args()
MARKDOWN_CONVERTER.body_width = args.width
# We're replacing both the starting and ending mark with an ANSI "start"
# escape code. We'll replace every 2nd one with an "end" escape code in
# `print_comment_body()`.
if args.bold:
MARKDOWN_CONVERTER.strong_mark = '\033[1m'
if args.italic:
MARKDOWN_CONVERTER.emphasis_mark = '\033[3m'
# We only want pinned comments, so only fetch the bare minimum of normal
# comments. Pinned comments are always returned by the server.
if args.pinned_only:
args.num_comments = 1
print_package_comments(args)
if __name__ == '__main__':
main()