source: tracirclog/trunk/irclog.py @ 22

Last change on this file since 22 was 3, checked in by simon, 19 years ago

Initial import of Trac IRC snippet wiki processor.

File size: 11.9 KB
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
20import re
21from StringIO import StringIO
22
23IRCLOG2HTML_VERSION = "2.3"
24IRCLOG2HTML_RELEASE = "2005-03-28"
25
26URL_REGEXP = re.compile(r'((http|https|ftp|gopher|news)://[^ \'")>]*)')
27
28def createlinks(text):
29    """Replace possible URLs with links."""
30    return URL_REGEXP.sub(r'<a href="\1">\1</a>', text)
31
32def 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
49def 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
69class 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
79class 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
153class 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
177class 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
217class 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
298class 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
346def 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 repository browser.