# coding: utf-8
"""

Defines Meeting and MeetingEvent classes.

Can turn a sequence of ChatEvents into a Meeting + MeetingEvents.

Meeting and MeetingEvent can all be serialized nicely as HTML.  (And
possibly as mediawiki source, but that code is stale.)


TODO:

    fix bug: it comes out c&amp;p instead of c&p -- excess quoting.
            http://www.w3.org/2008/06/wiki_scribe/?source=http://www.w3.org/2007/OWL/wiki/Chatlog_2008-10-23
           ... plain comparsion, "h &gt; w" 
           
    gather issues/actions as entities?

    do resolutions normal-style insite TOC

    +name, -name ---   time range for folks...

    s/foo/bar processing.(or should that be in wikilogger?)
    
    dated names    add/drop/one-time
    
    keep/display last-modified date?

    track approved?

    clean up chatevents -- make them be a "source" for a meetingevent

 *  chatevents parse errors should turn into ScribeErrors
    (ie we call over there)

    multi-day meetings - input is several chatlogs (on different days)

    add s/foo/bar/

    add "comment:" for not-as-scribe

    data/links for next/previous meeting, next/previous version, etc
           _latest version_, _this version_, _previou
           version: _first_, _previous_, _this_, _next_, _latest_
           Versions of this Document:
                     |< v1  << v6  [this is v7]  v8 >>  v12 >|

             Document Revisions:
               (at -vn:)

               This is _revision 7_ and archival snapshot which should
               not change.  The source document may have changed since
               this snapshot was made.  If you want the up-to-date
               version, use this link: _latest version_.  See also
               _list of revisions_.

               (at other):

               This document may change in place.  An archival
               snapshot of this version will be kept at: _revision 7
               snapshot_.  See also _list of revisions_.

                    (table includes comments, diff-links.)

              OR: this version, latest version, previous version, all versions

           OWL Working Group Meetings:
                     |< 2007-12-02  << 2008-06-05  [this is v7]  v8 >>  v12 >|

           "<> APPROVED: (pointer) " in minutes, I guess.

    document Scribe Conventions:     before meeting, during, after

    link to version with interleaved chat events...
    (or JS to hide/show them)
    
"""
import sys
import re
import time
import html as h

import person

import local_groups_config

anchorCount = 0

class ScribeError (RuntimeError):
    pass

class DontListBots(ScribeError):
    def __init__(self, name):
        self.name = name
    def toHTML(self):
        return h.p("Scribe problem: the name ",`self.name`, " looks too much like the name of one of the common bots.   The bots should not be listed among the meeting attendees.", class_="error")
        
class AmbiguousName(ScribeError):
    def __init__(self, text, matched):
        self.text = text
        self.matched = matched
    def toHTML(self):
        return h.p("Scribe problem: the name ",`self.text`, " is ambiguous.  It could be any of: ",
                   [h.span(x.toHTML(), " ") for x in self.matched], ".   Either change the name used or insert a 'PRESENT: ...' line to restrict the active names.", class_="error")

class UnmatchedName(ScribeError):
    def __init__(self, text, listing):
        self.text = text
        self.listing = listing
    def toHTML(self):
        return h.p("Scribe problem: the name %s does not match any of the %d active names.  Either change the name used, or request the list of names be altered." %
                   (`self.text`, len([x for x in self.listing])),
                   "Active names: ",
                   [h.span(x.toHTML(), " ") for x in self.listing],
                   class_="error"
                   )


# hierarchical MeetingSegment ...?

class Meeting:
    """
    construct from chat-event-log,
    and ... meeting information page on wiki...
    and ... user directory?
    """

    def __init__(self):
        self.allPeople = []
        self.activeScribe = 'No Scribe Selected'
        self.events = []
        self.lastEventTime = 0
        self.resCount = 0
        self.meetingName = "Unnamed Meeting"
        self.group = None
        self.subgroup = None
        self.date = 0
        self.rootSegment = None
        
        self.scribe = []
        self.chair = []
        self.regrets = []
        self.present = []
        self.observers = []
        self.remote = []
        self.ircOnly = []

        self.bots = [
            person.Person("Zakim () IRC Bot", isBot=True),
            person.Person("Trackbot (trackbot-ng) IRC Bot", isBot=True),
            person.Person("RRSAgent () IRC Bot", isBot=True),
            ]

    sourceURLPattern = re.compile(r"""^Chatlog_(20\d\d)-(\d+)-(\d+)(_(.*))?$""")
    def useWikiSourceURL(self, source):
        """
        get:   name of group
               name of subgroup
               date
               relevant people

        by assuming a naming convention like:

        http://www.w3.org/2007/OWL/wiki/Chatlog_2008-06-04
        ===============================         ==========
                  = OWL                            = Date

        http://www.w3.org/2007/OWL/wiki/Chatlog_2008-06-09_UFDTF
                                                           =====
                                                             = subgroup

        and People Page is ./People                                                              

        >>> m = Meeting()
        >>> m.useWikiSourceURL("http://www.w3.org/2007/OWL/wiki/Chatlog_2008-06-04")
        >>> print m.group.name
        OWL Working Group
        >>> print m.date
        [2008, 6, 4, 0, 0, 0, 0, 1, -1]
        >>> print m.subgroup
        None
        
        """
        try:
            (main, extra) = source.split("&", 1)
        except ValueError:
            main = source
        (pre, post) = main.rsplit("/", 1)
        m = self.sourceURLPattern.match(post)
        if m:
            self.source = source
            (self.group, self.subgroup) = (
                local_groups_config.lookupFromWikiPrefix(pre, m.group(5))
                )
            assert(self.group)
            t = [1900, 1, 1, 0, 0, 0, 0, 1, -1]
            t[0] = int(m.group(1))
            t[1] = int(m.group(2))
            t[2] = int(m.group(3))
            self.date = t   # time.mktime(t)

            try:
                peoplePageURL = self.group.peoplePageURL
            except AttributeError:
                peoplePageURL = pre+"/Participants2"
            peoplePage = person.PageOfPeople(peoplePageURL)
            #print >>sys.stderr, "Loading people from ", peoplePageURL
            self.allPeople = peoplePage.people
            
            self.meetingName = self.group.name
            if self.subgroup:
                self.meetingName += " - " + self.subgroup + " Subgroup"
        else:
            raise RuntimeError, "Bad source URL pattern: %s" % source
        

    colonPat = re.compile(r"""^([-a-zA-Z0-9_]+):(.*)$""")
    equalPat = re.compile(r"""^(=+) (.*)$""")

    def addChatEvent(self, chatEvent):

        self.events.append(SourceEvent(chatEvent))
        
        self.currentWhen = chatEvent.when

        m = self.equalPat.match(chatEvent.text)
        if m:
            cmd = ("", "TOPIC", "SUBTOPIC", "SUBSUBTOPIC",
                   "SUBSUBSUBTOPIC")[len(m.group(1))]
            post = m.group(2)
        else:
        
            m = self.colonPat.match(chatEvent.text)
            if m:
                pre = m.group(1)
                post = m.group(2)
            else:
                pre = ""
                post = chatEvent.text

            if pre == "http":
                pre = ""
                post = chatEvent.text

            cmd = pre.upper()
            post = post.strip()

        if cmd == "ACTION":
            newEvent = ActionItemAssigned(post)
        elif cmd == "PEOPLE":
            peoplePage = person.PageOfPeople(post)
            #print >>sys.stderr, "Loading people from ", post
            self.allPeople = peoplePage.people
            #for x in self.allPeople:
            #    print >>sys.stderr, "Person: ", x.name
            return
        elif cmd == "MEETING":
            self.meetingName = post
            return
        elif cmd == "OMIT":
            return
        elif cmd == "PRESENT":
            self.present = self.parsePeople(post)
            return
        elif cmd == "OBSERVERS":
            self.observers = self.parsePeople(post)
            return
        elif cmd == "REMOTE":
            self.remote = self.parsePeople(post)
            return
        elif cmd == "REGRETS":
            self.regrets = self.parsePeople(post)
            return
        elif cmd == "CHAIR":
            self.chair = self.parsePeople(post, self.active)
            return
        elif cmd == "SLIDE":
            newEvent = NextSlide(post)
        elif cmd == "ISSUE":
            newEvent = NewIssue(post)
        elif cmd == "PROPOSED" or cmd == "PROPOSAL":
            newEvent = ProposalMade(post)
        elif cmd == "STRAWPOLL":
            newEvent = StrawpollMade(post)
        elif cmd == "RESOLVED" or cmd == "RESOLUTION" or cmd == "ACCEPTED":
            newEvent = ResolutionMade(self, post)
        elif cmd == "SCRIBENICK" or cmd == "SCRIBE":  
            # record that they did some scribing, and how much...?
            newScribe = self.safe_match(post)
            if newScribe == self.activeScribe:
                return
            self.activeScribe = newScribe
            newEvent = ScribeChange(newScribe)
            if newScribe not in self.scribe:
                self.scribe.append(newScribe)
        elif cmd == "SUMMARY":
            newEvent = Summary(post)
        elif cmd == "TOPIC":
            newEvent = TopicChange(post)
        elif cmd == "SUBTOPIC":
            newEvent = TopicChange(post, 2)
        elif cmd == "SUBSUBTOPIC":
            newEvent = TopicChange(post, 3)
        elif cmd == "SUBSUBSUBTOPIC":
            newEvent = TopicChange(post, 4)
        else:
            who = self.safe_match(chatEvent.who)
            isScribe = ( who == self.activeScribe )
            if cmd == "":
                # no colon at all -- just someone typed something
                speaker = self.safe_match(chatEvent.who)
                newEvent = Typed(speaker, chatEvent.text, isScribe)
            else:
                # person:text
                speaker = self.safe_match(pre)
                thisScribe = who # safeLookup(allPeople, chatEvent.who, "(This IRC user who tried to transcribe some speech)")
                newEvent = Spoken(speaker, thisScribe, post, isScribe)

        if newEvent:

            if chatEvent.when:
                if self.lastEventTime:
                    gap = chatEvent.when - self.lastEventTime
                    if gap > 300 :
                        self.events.append(Gap(gap))
                self.lastEventTime = chatEvent.when

            newEvent.source = chatEvent
            newEvent.when = chatEvent.when
            newEvent.meeting = self
            self.events.append(newEvent)

    def old___date(self):
        seconds = self.events[0].when
        parts = time.localtime(seconds)
        return parts

    def parsePeople(self, text, listing=None):
        if listing==None:
            listing=self.allPeople
        result = []
        for entry in text.split(","):
            entry = entry.strip()
            if entry.endswith("(muted)"):
                entry = entry[0:-8]

            try:
                dummy = self.match(entry, self.bots)
                e = DontListBots(entry)
                self.events.append(e)
                return []
            except UnmatchedName:
                pass

            who = self.safe_match(entry, listing)
            result.append(who)
        return result

    @property
    def active(self):
        if self.present:
            listing = self.present + self.remote + self.observers + self.ircOnly
        else:
            listing = self.allPeople
        return listing
        

    def match(self, typed_orig, listing=None):
        if listing is None:
            listing = self.active + self.bots

        if typed_orig == "":
            raise UnmatchedName(typed_orig, listing)

        matches = set()
        typed = typed_orig.lower()

        if typed.endswith("_") or typed[-1:].isdigit():
            typed = typed[0:-1]
        if typed.endswith("_") or typed[-1:].isdigit():
            typed = typed[0:-1]

        for person in listing:
            if person.matches(typed):
                matches.add(person)

        if len(matches) == 1:
            return matches.pop()
        if len(matches) > 1:
            raise AmbiguousName(typed_orig, matches)
        if len(matches) < 1:
            raise UnmatchedName(typed_orig, listing)

    def safe_match(self, typed_orig, listing=None):
        try:
            return self.match(typed_orig, listing)
        except AmbiguousName, e:
            e.when = self.currentWhen
            self.events.append(e)
        except UnmatchedName, e:
            e.when = self.currentWhen
            self.events.append(e)

        # track these somehow?
        return person.Person("Unknown Person: %s" % typed_orig)

    def printForWiki(self):
        print "{{draftMinutes|date=%s}}" % time.strftime("%d %B %Y", self.date)
        print
        print "See also: [http://www.w3.org/%s-owl-irc IRC log]" % (time.strftime("%Y/%m/%d" , self.date))
        print
        print "__TOC__"
        print
        print '<div class="intro">'
        print
        print "; Present"
        print ": "+", ".join([x.toWiki() for x in self.present])
        print "; Regrets"
        print ": "+", ".join([x.toWiki() for x in self.regrets])
        if self.observers:
            print "; Observers"
            print ": "+", ".join([x.toWiki() for x in self.observers])
        print "; Chair"
        print ": "+", ".join([x.toWiki() for x in self.chair])
        print "; Scribe"
        print ": "+", ".join([x.toWiki() for x in self.scribe])
        print
        print "</div>"
        print " "
        print '<div class="meeting">'
        print
        for e in self.events:
            e.printForWiki()
            print
        print
        print "</div>"

    def toHTML(self, doc):
        datestr = time.strftime("%d %B %Y", self.date)
        doc.head << h.Raw('<meta http-equiv="Content-Type" content="text/html; charset=utf-8">')
        doc.head << h.title("%s Minutes, %s" % (self.meetingName, datestr))
        doc.head << h.script("/* text required here by some browsers */",src="http://www.w3.org/2009/CommonScribe/included.js", type="text/javascript")
        doc << h.h2(self.meetingName)
        doc << h.h2("Minutes of %s" % datestr)
        dl = h.dl(class_="meetingProps")
        doc << dl
        for field in ['Present', 'Remote', 'Regrets', 'Observers', 'Chair', 'Scribe']:
            value = getattr(self, field.lower())
            if value:
                dl << h.dt(field)
                dl << h.dd([h.span(x.toHTML()," ") for x in value])

        dl << h.dt("IRC Log")
        dl << h.dd(
                 h.a("Original", href="http://www.w3.org/%s-%s-irc" % (time.strftime("%Y/%m/%d" , self.date), self.group.channel)),
                 " and ",
                 h.a("Editable Wiki Version", href=self.source)
                 )

        dl << h.dt("Resolutions")
        dl << h.dd(self.ListResolutions())
        dl << h.dt("Topics")
        dl << h.dd(self.TOCHTML())

        self.WarnScribeErrors(doc)

        for e in self.events:
            if hasattr(e, "linkHere") and e.linkHere:
                ed = h.div(id_ = e.anchor)
                ed << h.span(" ",h.a("link here", href="#%s" % e.anchor),class_="permalink")
                ed << e.toHTML()
                doc << ed
            else:
                doc << e.toHTML()

        doc << h.br('')
        doc << h.p(h.em('Formatted by ', 
                        h.a('CommonScribe', 
                            href="http://www.w3.org/2009/CommonScribe/")),
                        ' ', source_button())
        # doc << h.script("commonscribe_begin();", type="text/javascript")


    def TOCHTML(self):
        buildSegments(self)
        d = h.div(class_="topics")
        self.rootSegment.toHTML(d)
        return d

    def WarnScribeErrors(self, doc):
        errors = 0
        for e in self.events:
            if isinstance(e, ScribeError):
                errors += 1
        if errors:
            div = h.div(class_="error")
            doc << div
            div << h.p("There are some format problems with the chatlog. Please correct them and reload this page.   They are labeled on this page in a red box, like this message.")
            div << h.p("It may be helpful to ", source_button())
            # doc.head << h.style(".source { display: block; }", type="text/css")

    def ListResolutions(self):
        #d = h.div(class_="resolutionsBox")
        d = h.ol(class_="resBox")
        #d << h.h3("Summary: Resolutions Accepted")
        count = 0
        for e in self.events:
            if isinstance(e, ResolutionMade):
                count += 1
                d << h.li(e.text, " ", h.a("link", href="#"+e.anchor))
                
        if count == 0:
            d = h.p("None.")
        return d


class MeetingEvent:

    def __init__(self):
        self.meeting = None    # set by the Meeting instance, when we're added

    def printForWiki(self):
        """
        {{Scribe | who=PROPOSED | what=Accept the previous minutes}}

        {{IRC | who=[[Peter Patel-Schneider]] | what=+1 to accept minutes}}

        +1
        """

        print "unserializable event: %s" % self.__class__

    def toHTML(self):
        return h.p("unserializable event: %s" % self.__class__, class_="error")

    @property
    def wikitext(self):
        return linktotracker(wikiQuote(self.text))

    @property
    def htext(self):
        tracker = self.meeting.group.trackerPrefix
        if tracker.endswith("/"):
            tracker = tracker[:-1]
        s = h.xstr(self.text)
        s = re.sub(r'''(http://[^ ]*)''', r'''<a href="\1">\1</a>''', s)
        s = re.sub(r'''(ISSUE|issue|Issue)(-| |)(\d+)''', r'''<a href="%s/issues/\3">ISSUE-\3</a>''' % tracker, s)
        s = re.sub(r'''(ACTION|action|Action)(-| |)(\d+)''', r'''<a href="%s/actions/\3">ACTION-\3</a>''' % tracker, s)
        return h.Raw(s)

    @property
    def xxxanchor(self):
        global anchorCount

        try:
            return self._anchor
        except AttributeError:
            pass
            
        if hasattr(self, "when"):
            if self.when:
                self._anchor = "t%d"%self.when
                return self._anchor
            
        anchorCount += 1
        self._anchor = "tx%d"%anchorCount
        return self._anchor

    def linkablePara(self, *a, **v):
        global anchorCount

        try:
            anchor = self.anchor
        except AttributeError:
            anchorCount += 1
            anchor = "line%04d" % anchorCount
            self.anchor = anchor

        aa = a + ( h.span(" ", h.a(h.Raw("&larr;"), href="#"+anchor),
                          class_="permalink")  ,)
        vv = v.copy()
        vv["id"] = anchor     # should we look to see if it's already set?
        return h.p(*aa, **vv)


class ActionItemAssigned(MeetingEvent):
    def __init__(self, text):
        self.text = text
    def printForWiki(self):
        print "<strong>ACTION:</strong> "+self.wikitext
    def toHTML(self):
        return self.linkablePara(h.strong("ACTION: "), self.htext)

class NextSlide(MeetingEvent):
    def __init__(self, text):
        self.text = text
    def printForWiki(self):
        print '=== Slide: "%s" ===' % self.wikitext

class NewIssue(MeetingEvent):
    def __init__(self, text):
        self.text = text
    def toHTML(self):
        return self.linkablePara(h.strong("ISSUE: "), self.htext)

class ProposalMade(MeetingEvent):
    def __init__(self, text):
        self.text = text
    def printForWiki(self):
        print "<strong>PROPOSED:</strong> "+self.wikitext
    def toHTML(self):
        return self.linkablePara(h.strong("PROPOSED:"), " ", self.htext)

class StrawpollMade(MeetingEvent):
    def __init__(self, text):
        self.text = text
    def printForWiki(self):
        print "<strong>PROPOSED:</strong> "+self.wikitext
    def toHTML(self):
        return self.linkablePara(h.strong("STRAWPOLL:"), " ", self.htext)

class ResolutionMade(MeetingEvent):
    def __init__(self, meeting, text):
        self.text = text
        meeting.resCount += 1
        self.anchor = "resolution_%d" % meeting.resCount
    def printForWiki(self):
        print '<strong style="color:red">RESOLVED:</strong> '+self.wikitext
    def toHTML(self):
        return self.linkablePara(h.strong("RESOLVED:", class_="resolution"), " ", self.htext,
                   id=self.anchor)
    
class TopicChange(MeetingEvent):
    def __init__(self, text, level=1):
        self.text = text
        self.newTopic = text
        self.level = level
        self.anchor = alnumEscape(text)
        self.segment = None

    def printForWiki(self):
        print "== "+self.wikitext+" =="

    def toHTML(self):
        func = [None, h.h3, h.h4, h.h5, h.h6][self.level]
        d = h.div(class_="meetingSegmentHeading meetingSegmentHeadingLevel_%d" % self.level)
        d << func(self.segment.position, ".  ", self.htext, id=self.anchor)
        if self.segment and self.segment.summary:
            d << h.p("Summary: ", self.segment.summary, class_="summaryInBody")
        return d
    
class Summary(MeetingEvent):
    def __init__(self, text):
        self.text = text

    def printForWiki(self):
        pass

    def toHTML(self):
        return h.div()
    
class ScribeChange(MeetingEvent):
    def __init__(self, text):
        self.text = text
    def printForWiki(self):
        print "(Scribe set to %s)" % self.wikitext
    def toHTML(self):
        return h.p("(Scribe set to ", self.text.toHTML(), ")")

class Gap(MeetingEvent):
    def __init__(self, gap):
        self.gap = gap
    def printForWiki(self):
        print "(No events recorded for %d minutes)" % (self.gap/60)
    def toHTML(self):
        return h.p("(No events recorded for %d minutes)" % (self.gap/60))
    
class SpeechEvent (MeetingEvent):
    pass
    
class Typed (SpeechEvent):
    def __init__(self, speaker, text, fromOfficialScribe):
        self.speaker = speaker
        self.text = text
        self.fromOfficialScribe = fromOfficialScribe
        self.isBot = speaker.isBot
    def printForWiki(self):
        if self.speaker == "Zakim":
            if self.text.find(', you wanted to') == -1:
                return self.bot()
        if self.speaker == 'RRSAgent':
            return self.bot()
        if self.speaker == 'trackbot-ng':
            return self.bot()
        if self.text.lower().startswith('zakim,'):
            return self.bot()
        if self.text.lower().startswith('rrsagent,'):
            return self.bot()
        if self.text.lower().startswith('trackbot-ng,'):
            return self.bot()
        
        if self.fromOfficialScribe:
            print self.wikitext
        else:
            print "{{IRC | who=%s | what=%s}}" % (
                self.speaker, self.wikitext)
    def bot(self):
        if self.speaker == "trackbot-ng" and self.text.startswith('Created'):
            print "{{IRC | who=%s | what=%s}}" % (
                self.speaker, self.wikitext)
        else:
            print ("<!-- {{BOT | who=%s | what=%s}} -->" % (
                self.speaker, self.wikitext))

    def toHTML(self):

        classXtra=""
        if self.isBot:
            if ((self.text.find(', you wanted to') == -1) and
                not self.text.startswith("Created")):
                classXtra = " bot"
        elif ( self.text.lower().startswith('zakim,') or
               self.text == "q+" or
               self.text == "q-" or
               self.text == "q?" or
               self.text.startswith("ack ") or
               self.text.lower().startswith('rrsagent,') or
               self.text.lower().startswith('trackbot,')):
            classXtra = " bot"

        if self.fromOfficialScribe:
            return self.linkablePara(self.htext, class_="scribenote"+classXtra)
        else:
            return self.linkablePara(self.speaker.toHTML(), ": ", self.htext, class_="irc"+classXtra)
                

class Spoken (SpeechEvent):
    def __init__(self, speaker, scribe, text, fromOfficialScribe):
        self.speaker = speaker
        self.scribe = scribe
        self.text = text
        self.fromOfficialScribe = fromOfficialScribe

    def printForWiki(self):
        """
        {{Scribe | who=PROPOSED | what=Accept the previous minutes}}

        {{IRC | who=[[Peter Patel-Schneider]] | what=+1 to accept minutes}}

        +1
        """
        if self.fromOfficialScribe:
            print "{{Scribe | who=%s | what=%s}}" % (
                self.speaker, self.wikitext)
        else:
            
            print "{{AsstScribe | asst=%s | who=%s | what=%s}}" % (
                self.scribe, self.speaker, (self.wikitext))

    def toHTML(self):
        if self.fromOfficialScribe:
            return self.linkablePara(self.speaker.toHTML(), ": ", self.htext, class_="scribe")
        else:
            if self.speaker.isBot:
                pass
            else:
                return self.linkablePara(self.speaker.toHTML(), ": ", self.htext, " [ Scribe Assist by ",
                           self.scribe.toHTML(), " ] ", class_="asst")


class SourceEvent (MeetingEvent):

    def __init__(self, source):
        self.source = source
        self.when = source.when

    def toHTML(self):
        #return h.pre(h.Raw(self.source.originalText), class_="source")
        return h.pre(self.source.originalText, class_="source")

def wikiQuote(str):
    return re.sub(r'''\|''', '&#124;', str) 

def linktotracker(str):
    str = re.sub(r'''(ISSUE|issue|Issue)(-| )(\d+)''', r'''{{Issue|\3}}''', str)
    str = re.sub(r'''(ACTION|action|Action)(-| )(\d+)''', r'''{{Action|\3}}''', str)
    return str

def alnumEscape(str):
    """
    Turn any string into an alphanumeric (plus _) string, by turning
    illegal chars in __hex_.  For the common case, we turn a single
    space (but only the first in a sequence of spaces) into a single
    underscore.  I believe this is a reverseable 1-1 mapping, but I
    could be wrong.
    
    >>> print alnumEscape("Hello")
    Hello
    >>> print alnumEscape("Hello World")
    Hello_World
    >>> print alnumEscape("Hello  World")
    Hello___20_World
    >>> print alnumEscape("Hello, World!")
    Hello__2c__World__21_
    >>> print alnumEscape("Hello,_World!")
    Hello__2c___5f_World__21_
    >>> print alnumEscape("Markus Krötzsch")
    Markus_Kr__c3___b6_tzsch
    
    """
    result = ""
    spaceRun = False
    for char in str:
        if char.isalnum():
            result += char
            spaceRun = False
        elif char == " ":
            if spaceRun:
                result += "__%x_"%ord(char)
            else:
                result += "_"
                spaceRun = True
        else:
            result += "__%x_"%ord(char)
    return result

xPat = re.compile(r"""__([abcdef0-9]+)_""")
def alnumUnescape(str):
    """

    >>> alnumUnescape(alnumEscape("Hello, World!"))
    'Hello, World!'
    >>> p = 'Markus Krötzsch'
    >>> p == alnumUnescape(alnumEscape(p))
    True
    
    """
    result = []
    delim = False
    for part in xPat.split(str):
        if delim:
            result.append(chr(int(part, 16)))
        else:
            result.append(part.replace("_", " "))
        delim = not delim
    return "".join(result)
        
class MeetingSegment:
    """
    Some day, a lot of Meeting should perhaps migrate down here.  For
    now, we just use it for the Table of Contents.
    """

    def __init__(self):
        self.summary = None
        self.level = None
        self.topicChange = None
        self.parent = None
        self.segments = []
        self.events = []

    @property
    def positionVector(self):

        if self.parent is None:
            return ()

        count = 0
        for child in self.parent.segments:
            count += 1
            if child is self:
                return self.parent.positionVector + (count,)

    @property
    def position(self):
        return ".".join([str(x) for x in self.positionVector])
    
    def toHTML(self, d=None):
        if d is None: d = h.div()

        if self.topicChange:
            d << h.p(h.a(self.topicChange.newTopic,
                         href="#"+self.topicChange.anchor))

        if self.summary:
            d << h.p(self.summary, class_="summary")
            
        if self.segments:
            subtree = h.ol()
            for child in self.segments:
                li = h.li()
                subtree << child.toHTML(li)
            d << subtree

        return d
    
            
def buildSegments(meeting):

    meeting.rootSegment = MeetingSegment()
    current = meeting.rootSegment
    current.level
    for event in meeting.events:
        if isinstance(event, Summary):
            current.summary = event.text
        elif isinstance(event, TopicChange):
            while event.level < current.level:
                current = current.parent
            save = current
            current = MeetingSegment()
            current.topicChange = event
            current.topicChange.segment = current
            current.level = event.level
            if event.level > save.level:
                current.parent = save
            else:
                current.parent = save.parent
            current.parent.segments.append(current)
        current.events.append(event)

class MeetingDatabase:
    """
    TODO
    
    Data about various meetings, and versions of minutes for each one.

    Store as .rdf file?   Or in SQL database?

    Used to generate forward <-> backward links, and Overview page(s)

    """

    def __init__(self):
        pass


def source_button():
    button = h.button("Show Interleaved IRC Text", onclick="show_source_lines();")
    return button

if __name__ == "__main__":
    import doctest, sys
    doctest.testmod(sys.modules[__name__])

