#!/usr/bin/python
"""
pwyky - A Simple Wiki in Python.
Documentation:
')
self.write('\n')
self.write(self.wikiParse(content, level=0))
self.write('\n')
self.write('')
self.write('\n')
def quotElement(self, content, cite):
self.write('')
self.write('\n')
self.write('') # @@
self.write('\n')
self.write(self.wikiParse(content, level=0))
self.write('\n')
self.write('')
self.write('\n')
self.write('')
self.write('\n')
def paragraphElement(self, content):
self.write('') self.write(self.wikiParse(content)) self.write('
') self.write('\n') def wikiParse(self, s, level=None): if level is None: level = 1 # @@ use a proper parser, or catch the matches pos, result = 0, '' while pos < len(s): m = r_tag.match(s[pos:]) if m: span = m.span() result += self.tag(s[pos:pos+span[1]], level=level) pos += span[1] - span[0] else: m = r_emdash.match(s[pos:]) if m and (level > 0): # unicode must be explicit in
result += s[pos] + '—' # u'\u2014'.encode('utf-8')
pos += 3
elif (s[pos] == '{') and (s[pos+1:pos+2] != '{') and (level > 0):
if (s < 10): area = s[0:pos+10]
else: area = s[pos-10:pos+10]
msg = "The '{' must be escaped as '{{' in %r" % area
raise "WikiParseError", msg
elif (s[pos:pos+2] == '{{'): # d8uv bug "and (level > 0): "
result += '{'
pos += 2
elif s[pos] == '&':
result += '&'
pos += 1
elif s[pos] == '<':
result += '<'
pos += 1
else:
result += s[pos]
pos += 1
return result
def iriParse(self, uri):
r_unicode = re.compile(r'\{U\+([1-9A-F][0-9A-F]{1,5})\}')
def escape(m):
bytes = unichr(int(m.group(1), 16)).encode('utf-8')
return ''.join(['%%%02X' % ord(s) for s in bytes])
return r_unicode.sub(escape, uri)
def unicodeify(self, s):
if len(s) not in (2, 4, 6):
raise ValueError, 'Must be of length 2, 4, or 6'
for letter in 'abcdef':
if letter in s:
raise ValueError, 'Unicode escapes must be lower-case'
i = int(s.lstrip('0'), 16)
raw = [0x9, 0xA, 0xD] + list(xrange(0x20, 0x7E))
del raw[raw.index(0x2D)], raw[raw.index(0x5D)], raw[raw.index(0x7D)]
if i in raw: return chr(i) # printable - '-]}'
elif i > 0x10FFFF:
raise ValueError, 'Codepoint is out of range'
return '%s;' % s
def tag(self, s, level=None):
if level is None:
level = 1 # @@ { {U+..}?
s = s[1:-1] # @@ or s.strip('{}')
if s.startswith('U+'):
try: result = self.unicodeify(s[2:])
except ValueError: result = cgi.escape('{%s}' % s)
elif s == '$timenow':
result = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime())
elif s == '$datenow':
result = time.strftime('%Y-%m-%d', time.gmtime())
elif level < 1:
result = '{' + self.wikiParse('%s}' % s)
elif s.startswith('* '):
result = '%s' % s[2:]
elif s.startswith('#'):
i = s.find(' ')
href, title = s[:i], s[i+1:]
result = '%s' % (href, title)
elif not re.compile(r'[A-Za-z0-9_.-]').match(s):
result = cgi.escape('{%s}' % s)
else:
self.rawlinks.append(s)
words = s.split(' ')
words = [word.strip() for word in words if word.strip()]
if ('/' not in words[0]) and (':' not in words[0]): # @@!
wn = ''.join(words)
uri = './%s' % wn
if not self.exists(wn):
cls = ' class="nonexistent"'
else: cls = ''
else: uri, s, cls = words[0], ' '.join(words[1:]), ''
uri, s = cgi.escape(uri, quote=1), cgi.escape(s)
result = '%s' % (uri, cls, s)
return result
def wikiParse(s, getlinks=False):
output = StringIO()
parse = TextParser(write=output.write,
exists=lambda wn: os.path.exists(wn + '.html'))
parse(s)
output.flush()
output.seek(0)
if getlinks:
return output.read(), parse.rawlinks
return output.read()
class Wikify(HTMLParser):
def __init__(self, write=None):
HTMLParser.__init__(self)
if write is None:
self.write = sys.stdout.write
else: self.write = write
self.content = False
self.block = False
self.blockquote = False
self.anchor = False
self.current = None
def handle_starttag(self, tag, attrs):
self.current = tag
attrs = dict(attrs)
xhtmlxmlns = 'http://www.w3.org/1999/xhtml'
if (tag == 'html') and (attrs.get('xmlns') != xhtmlxmlns):
raise "ParseError", "document is not XHTML"
elif (tag == 'head') and (attrs.get('profile') != profile):
raise "ParseError", "document has incorrect profile"
elif (tag == 'div') and (attrs.get('class') == 'content'):
self.content = True
if self.content:
if tag in ('p', 'li', 'h1', 'h2', 'pre'):
self.block = True
if tag == 'li':
self.write('* ')
elif tag in ('h1', 'h2'):
self.write('@ ')
elif tag == 'pre' and not self.blockquote:
self.write('{{{')
elif tag == 'blockquote':
self.blockquote = attrs
self.write('[[[')
elif tag == 'strong':
self.write('{* ')
elif tag == 'a':
self.anchor = attrs
self.anchor['_'] = ''
def handle_endtag(self, tag):
self.current = None
if self.content:
if tag in ('p', 'li', 'h1', 'h2', 'pre'):
self.block = False
if tag in ('p', 'h1', 'h2'):
self.write('\n\n')
elif tag in ('ul', 'li'):
self.write('\n')
elif tag == 'pre' and not self.blockquote:
self.write('}}}\n\n')
elif tag == 'blockquote':
self.write(']]]')
cite = self.blockquote.get('cite', None)
if cite is not None:
self.write(' - %s' % cite)
self.write('\n\n')
self.blockquote = False
elif tag == 'a':
attrs, dual = self.anchor, True
uri, title = attrs.get('href', ''), attrs.get('_', '')
stuff = [w.strip() for w in title.split(' ') if w.strip()]
stitle = ''.join(stuff)
if uri.startswith('./'):
wn = uri[2:]
if r_name.match(wn):
if wn == stitle:
dual = False
if not dual: self.write('{%s}' % title)
else: self.write('{%s %s}' % (uri, title))
self.anchor = False
elif tag == 'strong':
self.write('}')
elif tag == 'div':
self.content = False
def handle_data(self, data):
if self.current in ('p', 'li', 'h1', 'h2', 'pre'): # d8uv, pre added
data = data.replace('{', '{{')
if self.content and self.block:
if not self.anchor:
self.write(data)
else: self.anchor['_'] += data
def handle_charref(self, name):
if self.content and self.block:
if name.startswith('x'):
result = '{U+%s}' % name.lstrip('x')
elif name == '8212':
result = '--'
else: raise "ParseError", "Unknown character reference: %s" % name
if not self.anchor:
self.write(result)
else: self.anchor['_'] += result
def handle_entityref(self, name):
if self.content and self.block:
entities = {'lt':'<', 'gt':'>', 'amp':'&', 'quot':'"'}
result = entities.get(name, '?')
if not self.anchor:
self.write(result)
else: self.anchor['_'] += result
def wikify(s):
output = StringIO()
parser = Wikify(write=output.write)
parser.feed(s) # @@ except?
output.flush()
output.seek(0)
return output.read()
def inboundLinks(wn):
if not os.path.exists('@links'): # @@ isDir?
os.mkdir('@links')
return [fn[9:-(len(wn)+1)] for fn in glob.glob('./@links/*%%%s' % wn)]
def outboundLinks(wn):
if not os.path.exists('@links'): # @@ isDir?
os.mkdir('@links')
return [fn[len(wn)+10:] for fn in glob.glob('./@links/%s%%*' % wn)]
def html(title, body):
s = '\n\n'
s += '\n'
s += '\n' % profile
s += '%s \n' % title
if '/' in os.environ.get('REQUEST_URI')[len(base)+1:]: # heh
s += '\n'
else: s += '\n'
s += '\n'
s += '\n'
s += body + '\n'
s += '\n'
s += '\n'
return s
def compile(wn, text, getlinks=False):
if getlinks: # @@ horky, but oh well
content, rawlinks = wikiParse(text, getlinks=getlinks)
else: content = wikiParse(text, getlinks=getlinks)
s = '\n\n' % wn
heading = None
if content.startswith('')
heading = content[:i][len('')]
content = '%s' % (heading, content[i:])
heading = content[:i][j+1:-len('
')]
else: s += '%s: %s
\n' % (shortname, wn)
s += '\n%s\n\n\n' % content
s += '%s. This is a pwyky site.'
if pedit:
s += ' ' % (nedit, wn)
s += 'Edit this document.'
s += ''
if (heading is not None) and (heading != wn):
title = '%s - %s' % (heading, wn)
else: title = wn
if getlinks:
return html(title, s), rawlinks
return html(title, s)
def rebuild(fn):
stat = os.stat(fn)
atime, mtime = stat.st_atime, stat.st_mtime
s = open(fn).read()
try: s = wikify(s)
except "ParseError", e:
s = '\n\n' % e
s += open(fn).read()
else: s = compile(fn[:-len('.html')], s)
open(fn, 'w').write(s)
try: os.utime(fn, (atime, mtime))
except OSError: pass
def get(wn):
if os.path.exists(wn + '.html'):
return open(wn + '.html').read()
else:
msg = '%s
\n' % wn
msg += '
This page does not yet exist.'
if pedit:
msg += ' Create it!' % (nedit, wn)
msg += '
\n'
return html('Create %s' % wn, msg)
def edit(wn):
if os.path.exists(wn + '.html'):
try: s = wikify(open(wn + '.html').read())
except "ParseError", e:
s = "Error: couldn't wikify source! (%s)\n" % e
else: s = ''
if wn == default:
wn = ''
return html('Editing %s' % (wn or default), '''
''' % (wn, cgi.escape(s)))
def info(wn):
if not os.path.exists(wn + '.html'):
return "Page doesn't exist: %s" % wn
results = []
for fn in glob.glob('*.html'):
fn = fn[:-len('.html')]
for title in outboundLinks(fn):
words = title.split(' ')
words = [word.strip() for word in words if word.strip()]
if ('/' not in words[0]) and (':' not in words[0]):
if ''.join(words) == wn: results.append(fn) # @@ break
r = ['* {../%s %s} ({../@info/%s @info})' % (f, f, f) for f in results]
try: content = wikify(open(wn + '.html').read())
except "ParseError":
content = open(wn + '.html').read()
t = os.stat(wn + '.html').st_mtime
lastmod = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(t))
s = 'Previous ' % wn s += 'version (%s).
\n' % plastmod s += '\n' else: s += '(This is the first version).
\n' s += 'This page has no inbound links.
\n' s += '%s. Nearby: home.\n' % owner return html('Information About %s' % wn, s) def meta(wn): if wn == 'about': pages = { 'about': 'This page.', 'archive': 'Create a .tar.gz of the whole site.', 'diff': 'Find differences between a page and its previous version.', 'grep': 'Grep (search) the text in the site.', 'names': 'Provide a list of all pages.', 'needed': 'List of pages that are linked to but not yet made.', 'todo': 'List of todo items in the site.', 'unlinked': 'Pages that are not linked to elsewhere.', 'updates': 'Shows the most recent changes.' }.items() pages.sort() pages = ['* {./%s %s} - %s' % (k, k, v) for k, v in pages] pagelist = wikiParse('\n'.join(pages)) s = 'This site is a pwyky installation (source); the pwyky homepage explains how to use it. The \n' s += '@meta directory contains tools for providing information \n' s += 'about this particular site.
\n' s += pagelist s += 'Contact the site owner for questions about this site.
\n' s += '%s. Nearby: home.\n' % owner return html("@meta: About This Site", s) elif wn == 'names': results = [fn[:-len('.html')] for fn in glob.glob('*.html')] results.sort() results = ['* {../%s %s}' % (fn, fn) for fn in results] s = 'There are %s pages in this site:
\n' % len(results) s += wikiParse('\n'.join(results)) s += '%s. Nearby: home.\n' % owner return html("@meta/names: Site Pages List", s) elif wn == 'archive': if not os.path.exists('.notar'): import tarfile tar = tarfile.open("archive.tar.gz", "w:gz") for fn in glob.glob('*.html'): tar.add(fn) tar.close() open('.notar', 'w').write('') return html(wn, '.tar.gz updated (../archive.tar.gz)') else: return html(wn, '.tar.gz not updated (../archive.tar.gz)') elif wn == 'updates': i = 100 # Number of results to show result = {} for fn in glob.glob('*.html'): t = os.stat(fn).st_mtime lastmod = time.strftime('%Y-%m-%d %H:%M:%S', time.gmtime(t)) while result.has_key(lastmod): lastmod += '_' result[lastmod] = fn[:-len('.html')] keys = result.keys() keys.sort() keys.reverse() keys = keys[:i] today = time.strftime('%Y-%m-%d', time.gmtime()) s = 'Today is %s. ' % today s += 'The %s most recent changes in this site are:
\n' % len(keys) s += 'Up to %s updates will be shown.
\n' % i s += '%s. Nearby: home.\n' % owner return html('%s - Updates: Recent Changes' % longname, s) elif wn == 'grep': s = '\n' if os.environ.get('REQUEST_METHOD') == 'POST': form = cgi.parse_qs(sys.stdin.read()).items() form = dict([(item[0], ''.join(item[1])) for item in form]) regexp = form.get('regexp', '') r_regexp = re.compile(regexp) results = {} for fn in glob.glob('*.html'): for line in open(fn).read().splitlines(): find = r_regexp.findall(line) if find: if results.has_key(fn): results[fn] += len(find) else: results[fn] = len(find) results = [(v, k) for k, v in results.items()] results.sort() results.reverse() results = [(k, v) for v, k in results][:10] # 10 results only! s += '%s has no previous versions.
\n' % diff else: from difflib import Differ before = open(diff + '.prev').read() after = open(diff + '.html').read() before = before.replace('. ', '. \n').splitlines() after = after.replace('. ', '. \n').splitlines() s += '\n'
for line in list(Differ().compare(before, after)):
if (line.startswith('+ ') or
line.startswith('- ') or
line.startswith('? ')):
s += '%s
' % cgi.escape(line)
else: s += '%s
' % cgi.escape(line)
s += '
List of pages that are linked to but not yet made:
\n" s += wikiParse(result or '[No such pages found].\n') s += '%s. Nearby: home.\n' return html("Needed Pages", s) elif wn == 'unlinked': result = '' for fn in glob.glob('*.html'): fn = fn[:-len('.html')] links = inboundLinks(fn) if len(links) == 0: result += '* {../%s %s} ({../@info/%s info})\n' % (fn, fn, fn) s = 'The following is a list of pages which aren't linked \n" s += 'to from any other page:
\n' s += wikiParse(result or '[No such pages found].\n') s += '%s. Nearby: home.\n' return html('Unlinked Pages', s) elif wn == 'rebuild': # @@ dark corner os.environ['REQUEST_URI'] = base + '/' for fn in glob.glob('*.html'): rebuild(fn) return html('Rebuilt Pages', 'All pages have been rebuilt.
\n') elif wn == 'todo': todo = '@@' r = '(?sm)%s[ \n].+?(?:\.(?=[ \n<])|\?(?=[ \n<])|.(?=<)|\n\n)' % todo r_regexp = re.compile(r) results = {} for fn in glob.glob('*.html'): for line in open(fn).read().splitlines(): find = r_regexp.findall(line) if find: if results.has_key(fn): results[fn] += find else: results[fn] = find results = results.items() results.sort() s = 'Welcome to this pwyky site. ' print 'Try: home page.
' elif action == 'get': if method == 'POST': post(wn) print get(wn) elif action == nedit: print edit(wn) elif action == 'info': print info(wn) elif action == 'meta': print meta(wn) else: print 'Unknown action: %s
' % action def run(argv=None): if argv is None: argv = sys.argv[1:] if argv: if argv[0] in ('-h', '--help'): print __doc__.lstrip() else: if argv[0] in ('-w', '--wikify'): func = wikify elif argv[0] in ('-p', '--parse'): func = wikiParse if len(argv) > 1: import urllib s = urllib.urlopen(argv[1]).read() else: s = sys.stdin.read() sys.stdout.write(func(s)) if __name__=='__main__': if os.environ.has_key('SCRIPT_NAME'): try: main() except: cgi.print_exception() else: run()