root/tracirclog/trunk/irclog.py

Revision 3, 11.9 kB (checked in by simon, 3 years ago)

Initial import of Trac IRC snippet wiki processor.

Line 
1 """irclog -- an IRC log beautifier for the Trac wiki""" 
2
3 # Copyright (c) 2005, Simon E. Ward
4 # Copyright (c) 2005, Marius Gedminas
5 # Copyright (c) 2000, Jeffrey W. Waugh
6
7 # Trac Wiki-Processor:
8 #   Simon Ward <simon@mivok.net>
9 # Python port:
10 #   Marius Gedminas <marius@pov.lt>
11 # Original Author:
12 #   Jeff Waugh <jdub@perkypants.org>
13 # Contributors:
14 #   Rick Welykochy <rick@praxis.com.au>
15 #   Alexander Else <aelse@uu.net>
16 #
17 # Released under the terms of the GNU GPL
18 # http://www.gnu.org/copyleft/gpl.html
19
20 import re
21 from StringIO import StringIO
22
23 IRCLOG2HTML_VERSION = "2.3"
24 IRCLOG2HTML_RELEASE = "2005-03-28"
25
26 URL_REGEXP = re.compile(r'((http|https|ftp|gopher|news)://[^ \'")>]*)')
27
28 def createlinks(text):
29     """Replace possible URLs with links."""
30     return URL_REGEXP.sub(r'<a href="\1">\1</a>', text)
31
32 def escape(s):
33     """Replace ampersands, pointies, control characters.
34
35         >>> escape('Hello & <world>')
36         'Hello &amp; &lt;world&gt;'
37         >>> escape('Hello & <world>')
38         'Hello &amp; &lt;world&gt;'
39
40     Control characters (ASCII 0 to 31) are stripped away
41
42         >>> escape(''.join([chr(x) for x in range(32)]))
43         ''
44
45     """
46     s = s.replace('&', '&amp;').replace('<', '&lt;').replace('>', '&gt;')
47     return ''.join([c for c in s if ord(c) > 0x1F])
48
49 def shorttime(time):
50     """Strip date and seconds from time.
51
52         >>> shorttime('12:45:17')
53         '12:45'
54         >>> shorttime('12:45')
55         '12:45'
56         >>> shorttime('2005-02-04T12:45')
57         '12:45'
58
59     """
60     if 'T' in time:
61         time = time.split('T')[-1]
62     elif ' ' in time:
63         time = time.split(' ')[-1]
64     if time.count(':') > 1:
65         time = ':'.join(time.split(':')[:2])
66     return time
67
68
69 class Enum(object):
70     """Enumerated value."""
71
72     def __init__(self, value):
73         self.value = value
74
75     def __repr__(self):
76         return self.value
77
78
79 class LogParser(object):
80     """Parse an IRC log file.
81
82     When iterated, yields the following events:
83
84         time, COMMENT, (nick, text)
85         time, ACTION, text
86         time, JOIN, text
87         time, PART, text,
88         time, NICKCHANGE, (text, oldnick, newnick)
89         time, SERVER, text
90
91     """
92
93     COMMENT = Enum('COMMENT')
94     ACTION = Enum('ACTION')
95     JOIN = Enum('JOIN')
96     PART = Enum('PART')
97     NICKCHANGE = Enum('NICKCHANGE')
98     SERVER = Enum('SERVER')
99     OTHER = Enum('OTHER')
100
101     TIME_REGEXP = re.compile(
102             r'^\[?(' # Optional [
103             r'(?:\d{4}-\d{2}-\d{2}T|\d{2}-\w{3}-\d{4} |\w{3} \d{2} )?'
104                 # Optional date
105             r'\d\d:\d\d(:\d\d)?' # Mandatory HH:MM, optional :SS
106             r')\]? +') # Optional ], mandatory space
107     NICK_REGEXP = re.compile(r'^(?:<(.*?)>|([a-zA-Z\[\\\]^_`{|}][a-zA-Z0-9\[\\\]^_`{|}-]+))\s')
108     JOIN_REGEXP = re.compile(r'^(?:\*\*\*|-->)\s.*joined')
109     PART_REGEXP = re.compile(r'^(?:\*\*\*|<--)\s.*(quit|left)')
110     SERVMSG_REGEXP = re.compile(r'^(?:\*\*\*|---)\s')
111     NICK_CHANGE_REGEXP = re.compile(
112             r'^(?:\*\*\*|---)\s+(.*?) (?:are|is) now known as (.*)')
113
114     def __init__(self, infile):
115         self.infile = infile
116
117     def __iter__(self):
118         for line in self.infile:
119             line = line.rstrip('\r\n')
120             if not line:
121                 continue
122
123             m = self.TIME_REGEXP.match(line)
124             if m:
125                 time = m.group(1)
126                 line = line[len(m.group(0)):]
127             else:
128                 time = None
129
130             m = self.NICK_REGEXP.match(line)
131             if m:
132                 nick = m.group(1) or m.group(2)
133                 text = line[len(m.group(0)):]
134                 yield time, self.COMMENT, (nick, text)
135             elif line.startswith('* ') or line.startswith('*\t'):
136                 yield time, self.ACTION, line
137             elif self.JOIN_REGEXP.match(line):
138                 yield time, self.JOIN, line
139             elif self.PART_REGEXP.match(line):
140                 yield time, self.PART, line
141             else:
142                 m = self.NICK_CHANGE_REGEXP.match(line)
143                 if m:
144                     oldnick = m.group(1)
145                     newnick = m.group(2)
146                     yield time, self.NICKCHANGE, (line, oldnick, newnick)
147                 elif self.SERVMSG_REGEXP.match(line):
148                     yield time, self.SERVER, line
149                 else:
150                     yield time, self.OTHER, line
151
152
153 class NickClassifier(object):
154     """Assign style classes to nicknames."""
155
156     def __init__(self, maxclasses=30, default_classes=None):
157         self.nickcount = 0
158         self.maxclasses = maxclasses
159         self.nick_classes = {}
160         if default_classes:
161             self.nick_classes.update(default_classes)
162
163     def __getitem__(self, nick):
164         cls = self.nick_classes.get(nick)
165         if not cls:
166             self.nickcount += 1
167             fieldlen = len(str(self.maxclasses))
168             cls = ('nc%%0%dd' % fieldlen) % (self.nickcount % self.maxclasses)
169             self.nick_classes[nick] = cls
170         return cls
171
172     def change(self, oldnick, newnick):
173         if oldnick in self.nick_classes:
174             self.nick_classes[newnick] = self.nick_classes.pop(oldnick)
175
176
177 class AbstractStyle(object):
178     """A style defines the way output is formatted.
179
180     This is not a real class, rather it is an description of how style
181     classes should be written.
182     """
183
184     name = "stylename"
185     description = "Single-line description"
186
187     def __init__(self, outfile, classes=None):
188         """Create a text formatter for writing to outfile.
189
190         `classes` may have the following attributes:
191            part
192            join
193            server
194            nickchange
195            action
196         """
197         self.outfile = outfile
198         self.classes = classes or {}
199
200     def servermsg(self, time, what, line):
201         """Output a generic server message.
202
203         `time` is a string.
204         `line` is not escaped.
205         `what` is one of LogParser event constants (e.g. LogParser.JOIN).
206         """
207
208     def nicktext(self, time, nick, text, htmlclass):
209         """Output a comment uttered by someone.
210
211         `time` is a string.
212         `nick` and `text` are not escaped.
213         `htmlclass` is a string.
214         """
215
216
217 class XHTMLStyle(AbstractStyle):
218     """Text style, produces XHTML that can be styled with CSS"""
219
220     name = 'xhtml'
221     description = __doc__
222
223     CLASSMAP = {
224         LogParser.ACTION: 'action',
225         LogParser.JOIN: 'join',
226         LogParser.PART: 'part',
227         LogParser.NICKCHANGE: 'nickchange',
228         LogParser.SERVER: 'servermsg',
229         LogParser.OTHER: 'other',
230     }
231
232     prefix = '<div class="irclog">'
233     suffix = """</div>
234 <div class="generatedby">
235 <p>Generated by irclog wiki-processor
236  (<a href="mailto:simon@mivok.net">Simon Ward</a>),
237  based on irclog2html.py %(VERSION)s by
238  <a href="mailto:marius@pov.lt">Marius Gedminas</a>
239  - find it at <a href="http://mg.pov.lt/irclog2html/">mg.pov.lt</a>!</p>
240 </div> """ % { 'VERSION': IRCLOG2HTML_VERSION }
241
242     def link(self, url, title):
243         # Intentionally not escaping title so that &entities; work
244         if url:
245             print >> self.outfile, ('<a href="%s">%s</a>'
246                                     % (escape(urllib.quote(url)),
247                                        title or escape(url))),
248         elif title:
249             print >> self.outfile, ('<span class="disabled">%s</span>'
250                                     % title),
251
252     def servermsg(self, time, what, text):
253         """Output a generic server message.
254
255         `time` is a string.
256         `line` is not escaped.
257         `what` is one of LogParser event constants (e.g. LogParser.JOIN).
258         """
259         text = escape(text)
260         text = createlinks(text)
261         if time:
262             displaytime = shorttime(time)
263             print >> self.outfile, ('<p id="t%s" class="%s">'
264                                     '<a href="#t%s" class="time">%s</a> '
265                                     '%s</p>'
266                                     % (time, self.CLASSMAP[what], time,
267                                        displaytime, text))
268         else:
269             print >> self.outfile, ('<p class="%s">%s</p>'
270                                     % (self.CLASSMAP[what], text))
271
272     def nicktext(self, time, nick, text, htmlclass):
273         """Output a comment uttered by someone.
274
275         `time` is a string.
276         `nick` and `text` are not escaped.
277         `htmlclass` is a string.
278         """
279         nick = escape(nick)
280         text = escape(text)
281         text = createlinks(text)
282         text = text.replace('  ', '&nbsp;&nbsp;')
283         if time:
284             displaytime = shorttime(time)
285             print >> self.outfile, ('<p id="t%s" class="comment %s">'
286                                     '<a href="#t%s" class="time">%s</a> '
287                                     '<span class="nick">&lt;%s&gt;</span>'
288                                     ' <span class="text">%s</span></p>'
289                                     % (time, htmlclass, time, displaytime,
290                                        nick, text))
291         else:
292             print >> self.outfile, ('<p class="comment %s">'
293                                     '<span class="nick">&lt;%s&gt;</span>'
294                                     ' <span class="text">%s</span></p>'
295                                     % (htmlclass, nick, text))
296
297
298 class XHTMLTableStyle(XHTMLStyle):
299     """Table style, produces XHTML that can be styled with CSS"""
300
301     name = 'xhtmltable'
302     description = __doc__
303
304     prefix = '<table class="irclog">'
305     suffix = '</table>'
306
307     def servermsg(self, time, what, text):
308         text = escape(text)
309         text = createlinks(text)
310         if time:
311             displaytime = shorttime(time)
312             print >> self.outfile, ('<tr id="t%s">'
313                                     '<td class="%s" colspan="2">%s</td>'
314                                     '<td><a href="#t%s" class="time">%s</a></td>'
315                                     '</tr>'
316                                     % (time, self.CLASSMAP[what], text,
317                                        time, displaytime))
318         else:
319             print >> self.outfile, ('<tr>'
320                                     '<td class="%s" colspan="3">%s</td>'
321                                     '</tr>'
322                                     % (self.CLASSMAP[what], text))
323
324     def nicktext(self, time, nick, text, htmlclass):
325         nick = escape(nick)
326         text = escape(text)
327         text = createlinks(text)
328         text = text.replace('  ', '&nbsp;&nbsp;')
329         if time:
330             displaytime = shorttime(time)
331             print >> self.outfile, ('<tr class="%s" id="t%s">'
332                                     '<th class="nick">%s</th>'
333                                     '<td class="text">%s</td>'
334                                     '<td class="time">'
335                                     '<a href="#t%s" class="time">%s</a></td>'
336                                     '</tr>'
337                                     % (htmlclass, time, nick, text,
338                                        time, displaytime))
339         else:
340             print >> self.outfile, ('<tr class="%s">'
341                                     '<th class="nick">%s</th>'
342                                     '<td class="text" colspan="2">%s</td>'
343                                     '</tr>' % (htmlclass, nick, text))
344
345
346 def execute(hdf, text, env):
347     textbuf = StringIO(text)
348     htmlbuf = StringIO()
349
350     parser = LogParser(textbuf)
351     formatter = XHTMLTableStyle(htmlbuf)
352     nick_classes = NickClassifier(maxclasses=20)
353     htmlbuf.write(formatter.prefix)
354     for time, what, info in parser:
355         if what == LogParser.COMMENT:
356             nick, text = info
357             nickclass = nick_classes[nick]
358             formatter.nicktext(time, nick, text, nickclass)
359         else:
360             if what == LogParser.NICKCHANGE:
361                 text, oldnick, newnick = info
362                 nick_classes.change(oldnick, newnick)
363             else:
364                 text = info
365             formatter.servermsg(time, what, text)
366
367     # Footer
368     htmlbuf.write(formatter.suffix)
369     return htmlbuf.getvalue()
Note: See TracBrowser for help on using the browser.