Hatta Devel

annotate hatta.py @ 724:22b21a40da4e

make the html generated by the code highlighter validate -- use spans instead of divs
author Radomir Dopieralski <devel@sheep.art.pl>
date Sun Jan 10 20:36:38 2010 +0100 (2010-01-10)
parents 3b5177cf4780
children ecb0716ced56
rev   line source
sheep@616 1 #!/usr/bin/env python
sheep@0 2 # -*- coding: utf-8 -*-
sheep@0 3
sheep@276 4 # @copyright: 2008-2009 Radomir Dopieralski <hatta@sheep.art.pl>
sheep@276 5 # @license: GNU GPL, see COPYING for details.
sheep@276 6
sheep@40 7 """
sheep@40 8 Hatta Wiki is a wiki engine designed to be used with Mercurial repositories.
sheep@40 9 It requires Mercurial and Werkzeug python modules.
sheep@40 10
sheep@190 11 Hatta's pages are just plain text files (and also images, binaries, etc.) in
sheep@190 12 some directory in your repository. For example, you can put it in your
sheep@190 13 project's "docs" directory to keep documentation. The files can be edited both
sheep@190 14 from the wiki or with a text editor -- in either case the changes committed to
sheep@190 15 the repository will appear in the recent changes and in page's history.
sheep@190 16
sheep@618 17 See hatta.py --help for usage.
sheep@40 18 """
sheep@40 19
sheep@123 20 import base64
sheep@6 21 import datetime
sheep@78 22 import difflib
sheep@163 23 import gettext
sheep@6 24 import itertools
sheep@6 25 import mimetypes
sheep@0 26 import os
sheep@0 27 import re
sheep@302 28 import sqlite3
sheep@453 29 import sys
sheep@0 30 import tempfile
sheep@302 31 import thread
sheep@630 32 import unicodedata
sheep@453 33
sheep@637 34
sheep@453 35 # Avoid WSGI errors, see http://mercurial.selenic.com/bts/issue1095
sheep@453 36 sys.stdout = sys.__stdout__
sheep@453 37 sys.stderr = sys.__stderr__
sheep@78 38
sheep@0 39 import werkzeug
sheep@632 40 import werkzeug.exceptions
sheep@632 41 import werkzeug.routing
sheep@271 42
sheep@598 43 try:
sheep@601 44 import Image
sheep@601 45 except ImportError:
sheep@601 46 Image = None
sheep@601 47
sheep@688 48 try:
sheep@688 49 import pygments
sheep@688 50 import pygments.util
sheep@688 51 import pygments.lexers
sheep@688 52 import pygments.formatters
sheep@688 53 import pygments.styles
sheep@688 54 except ImportError:
sheep@688 55 pygments = None
sheep@688 56
sheep@433 57 # Note: we have to set these before importing Mercurial
sheep@31 58 os.environ['HGENCODING'] = 'utf-8'
sheep@433 59 os.environ['HGMERGE'] = "internal:merge"
sheep@632 60
sheep@31 61 import mercurial.hg
sheep@31 62 import mercurial.ui
sheep@31 63 import mercurial.revlog
sheep@114 64 import mercurial.util
sheep@608 65 import mercurial.hgweb
sheep@2 66
sheep@541 67 __version__ = '1.3.3dev'
cezary@551 68 name = 'Hatta'
cezary@551 69 url = 'http://hatta-wiki.org/'
cezary@580 70 description = 'Wiki engine that lives in Mercurial repository.'
sheep@135 71
sheep@688 72 mimetypes.add_type('application/x-python', '.wsgi')
sheep@692 73 mimetypes.add_type('application/x-javascript', '.js')
sheep@713 74 mimetypes.add_type('text/x-rst', '.rst')
sheep@688 75
sheep@689 76
sheep@272 77 def external_link(addr):
sheep@375 78 """
sheep@375 79 Decide whether a link is absolute or internal.
sheep@375 80
sheep@375 81 >>> external_link('http://example.com')
sheep@375 82 True
sheep@375 83 >>> external_link('https://example.com')
sheep@375 84 True
sheep@375 85 >>> external_link('ftp://example.com')
sheep@375 86 True
sheep@375 87 >>> external_link('mailto:user@example.com')
sheep@375 88 True
sheep@375 89 >>> external_link('PageTitle')
sheep@375 90 False
sheep@375 91 >>> external_link(u'ąęśćUnicodePage')
sheep@375 92 False
sheep@375 93
sheep@375 94 """
sheep@272 95
sheep@277 96 return (addr.startswith('http://')
sheep@277 97 or addr.startswith('https://')
sheep@277 98 or addr.startswith('ftp://')
sheep@277 99 or addr.startswith('mailto:'))
sheep@272 100
sheep@272 101
sheep@111 102 class WikiConfig(object):
sheep@271 103 """
sheep@271 104 Responsible for reading and storing site configuration. Contains the
sheep@271 105 default settings.
sheep@375 106
sheep@375 107 >>> config = WikiConfig(port='2080')
sheep@457 108 >>> config.sanitize()
sheep@457 109 >>> config.get('port')
sheep@375 110 2080
sheep@271 111 """
sheep@271 112
cezary@548 113 default_filename = u'hatta.conf'
sheep@433 114
sheep@433 115 # Please see the bottom of the script for modifying these values.
sheep@111 116
sheep@434 117 def __init__(self, **kw):
sheep@457 118 self.config = dict(kw)
sheep@277 119 self.parse_environ()
sheep@294 120
sheep@294 121 def sanitize(self):
sheep@375 122 """
sheep@375 123 Convert options to their required types.
sheep@375 124 """
sheep@480 125
sheep@434 126 try:
sheep@452 127 self.config['port'] = int(self.get('port', 0))
sheep@452 128 except ValueError:
sheep@452 129 self.config['port'] = 8080
sheep@111 130
sheep@277 131 def parse_environ(self):
sheep@276 132 """Check the environment variables for options."""
sheep@276 133
sheep@115 134 prefix = 'HATTA_'
sheep@115 135 for key, value in os.environ.iteritems():
sheep@115 136 if key.startswith(prefix):
sheep@115 137 name = key[len(prefix):].lower()
sheep@434 138 self.config[name] = value
sheep@115 139
sheep@277 140 def parse_args(self):
sheep@276 141 """Check the commandline arguments for options."""
sheep@276 142
sheep@115 143 import optparse
sheep@479 144
sheep@434 145 self.options = []
sheep@115 146 parser = optparse.OptionParser()
sheep@434 147
sheep@434 148 def add(*args, **kw):
sheep@434 149 self.options.append(kw['dest'])
sheep@434 150 parser.add_option(*args, **kw)
sheep@434 151
sheep@704 152 add('-V', '--version', dest='show_version', default=False,
sheep@704 153 help='Display version and exit', action="store_true")
sheep@434 154 add('-d', '--pages-dir', dest='pages_path',
sheep@434 155 help='Store pages in DIR', metavar='DIR')
sheep@434 156 add('-t', '--cache-dir', dest='cache_path',
sheep@434 157 help='Store cache in DIR', metavar='DIR')
sheep@434 158 add('-i', '--interface', dest='interface',
sheep@434 159 help='Listen on interface INT', metavar='INT')
sheep@434 160 add('-p', '--port', dest='port', type='int',
sheep@434 161 help='Listen on port PORT', metavar='PORT')
sheep@434 162 add('-s', '--script-name', dest='script_name',
sheep@434 163 help='Override SCRIPT_NAME to NAME', metavar='NAME')
sheep@434 164 add('-n', '--site-name', dest='site_name',
sheep@434 165 help='Set the name of the site to NAME', metavar='NAME')
sheep@434 166 add('-m', '--front-page', dest='front_page',
sheep@434 167 help='Use PAGE as the front page', metavar='PAGE')
sheep@434 168 add('-e', '--encoding', dest='page_charset',
sheep@481 169 help='Use encoding ENC to read and write pages', metavar='ENC')
sheep@434 170 add('-c', '--config-file', dest='config_file',
sheep@434 171 help='Read configuration from FILE', metavar='FILE')
sheep@434 172 add('-l', '--language', dest='language',
sheep@434 173 help='Translate interface to LANG', metavar='LANG')
sheep@720 174 add('-r', '--read-only', dest='read_only',
sheep@434 175 help='Whether the wiki should be read-only', action="store_true")
sheep@526 176 add('-g', '--icon-page', dest='icon_page', metavar="PAGE",
sheep@526 177 help='Read icons graphics from PAGE.')
sheep@720 178 add('-w', '--hgweb', dest='hgweb',
sheep@608 179 help='Enable hgweb access to the repository', action="store_true")
sheep@720 180 add('-W', '--wiki-words', dest='wiki_words',
sheep@645 181 help='Enable WikiWord links', action="store_true")
sheep@720 182 add('-I', '--ignore-indent', dest='ignore_indent',
sheep@646 183 help='Treat indented lines as normal text', action="store_true")
sheep@719 184 add('-P', '--pygments-style', dest='pygments_style',
sheep@719 185 help='Use the STYLE pygments style for highlighting',
sheep@719 186 metavar='STYLE')
sheep@434 187
sheep@115 188 options, args = parser.parse_args()
sheep@434 189 for option, value in options.__dict__.iteritems():
sheep@434 190 if option in self.options:
sheep@434 191 if value is not None:
sheep@434 192 self.config[option] = value
sheep@117 193
sheep@308 194 def parse_files(self, files=None):
sheep@276 195 """Check the config files for options."""
sheep@276 196
sheep@308 197 import ConfigParser
sheep@434 198
sheep@308 199 if files is None:
cezary@551 200 files = [self.get('config_file', self.default_filename)]
sheep@308 201 parser = ConfigParser.SafeConfigParser()
sheep@308 202 parser.read(files)
sheep@308 203 for section in parser.sections():
sheep@308 204 for option, value in parser.items(section):
sheep@434 205 self.config[option] = value
sheep@434 206
cezary@548 207 def save_config(self, filename=None):
cezary@548 208 """Saves configuration to a given file."""
cezary@548 209 if filename is None:
cezary@548 210 filename = self.default_filename
cezary@548 211
cezary@548 212 import ConfigParser
cezary@548 213 parser = ConfigParser.RawConfigParser()
cezary@548 214 section = self.config['site_name']
cezary@548 215 parser.add_section(section)
cezary@548 216 for key, value in self.config.iteritems():
cezary@548 217 parser.set(section, str(key), str(value))
cezary@548 218
cezary@548 219 configfile = open(filename, 'wb')
cezary@548 220 try:
cezary@548 221 parser.write(configfile)
cezary@548 222 finally:
cezary@548 223 configfile.close()
cezary@548 224
sheep@480 225 def get(self, option, default_value=None):
sheep@457 226 """
sheep@457 227 Get the value of a config option or default if not set.
sheep@434 228
sheep@457 229 >>> config = WikiConfig(option=4)
sheep@457 230 >>> config.get("ziew", 3)
sheep@457 231 3
sheep@457 232 >>> config.get("ziew")
sheep@457 233 >>> config.get("ziew", "ziew")
sheep@457 234 'ziew'
sheep@457 235 >>> config.get("option")
sheep@457 236 4
sheep@457 237 """
sheep@457 238
sheep@480 239 return self.config.get(option, default_value)
sheep@480 240
sheep@480 241 def get_bool(self, option, default_value=False):
sheep@480 242 """
sheep@480 243 Like get, only convert the value to True or False.
sheep@480 244 """
sheep@480 245
sheep@480 246 value = self.get(option, default_value)
sheep@480 247 if value in (
sheep@480 248 1, True,
sheep@480 249 'True', 'true', 'TRUE',
sheep@480 250 '1',
sheep@480 251 'on', 'On', 'ON',
sheep@480 252 'yes', 'Yes', 'YES',
sheep@480 253 'enable', 'Enable', 'ENABLE',
sheep@480 254 'enabled', 'Enabled', 'ENABLED',
sheep@480 255 ):
sheep@480 256 return True
sheep@480 257 elif value in (
sheep@480 258 None, 0, False,
sheep@480 259 'False', 'false', 'FALSE',
sheep@480 260 '0',
sheep@480 261 'off', 'Off', 'OFF',
sheep@480 262 'no', 'No', 'NO',
sheep@480 263 'disable', 'Disable', 'DISABLE',
sheep@480 264 'disabled', 'Disabled', 'DISABLED',
sheep@480 265 ):
sheep@480 266 return False
sheep@480 267 else:
sheep@480 268 raise ValueError("expected boolean value")
sheep@115 269
cezary@548 270 def set(self, key, value):
cezary@548 271 self.config[key] = value
cezary@548 272
sheep@443 273
sheep@443 274 def locked_repo(func):
sheep@443 275 """A decorator for locking the repository when calling a method."""
sheep@443 276
sheep@443 277 def new_func(self, *args, **kwargs):
sheep@443 278 """Wrap the original function in locks."""
sheep@443 279
sheep@443 280 wlock = self.repo.wlock()
sheep@443 281 lock = self.repo.lock()
sheep@443 282 try:
sheep@443 283 func(self, *args, **kwargs)
sheep@443 284 finally:
sheep@444 285 lock.release()
sheep@444 286 wlock.release()
sheep@443 287
sheep@443 288 return new_func
sheep@443 289
sheep@0 290 class WikiStorage(object):
sheep@271 291 """
sheep@271 292 Provides means of storing wiki pages and keeping track of their
sheep@271 293 change history, using Mercurial repository as the storage method.
sheep@271 294 """
sheep@271 295
sheep@385 296 def __init__(self, path, charset=None):
sheep@271 297 """
sheep@271 298 Takes the path to the directory where the pages are to be kept.
sheep@271 299 If the directory doen't exist, it will be created. If it's inside
sheep@271 300 a Mercurial repository, that repository will be used, otherwise
sheep@271 301 a new repository will be created in it.
sheep@271 302 """
sheep@271 303
sheep@419 304 self.charset = charset or 'utf-8'
sheep@0 305 self.path = path
sheep@0 306 if not os.path.exists(self.path):
sheep@0 307 os.makedirs(self.path)
sheep@5 308 self.repo_path = self._find_repo_path(self.path)
sheep@432 309 try:
sheep@432 310 self.ui = mercurial.ui.ui(report_untrusted=False,
sheep@432 311 interactive=False, quiet=True)
sheep@432 312 except TypeError:
sheep@440 313 # Mercurial 1.3 changed the way we setup the ui object.
sheep@432 314 self.ui = mercurial.ui.ui()
sheep@439 315 self.ui.quiet = True
sheep@439 316 self.ui._report_untrusted = False
sheep@439 317 self.ui.setconfig('ui', 'interactive', False)
sheep@5 318 if self.repo_path is None:
sheep@5 319 self.repo_path = self.path
sheep@441 320 create = True
sheep@5 321 else:
sheep@441 322 create = False
sheep@5 323 self.repo_prefix = self.path[len(self.repo_path):].strip('/')
devel@723 324 self._repos = {}
devel@723 325 # Create the repository if needed.
devel@723 326 mercurial.hg.repository(self.ui, self.repo_path, create=create)
sheep@5 327
sheep@398 328 def reopen(self):
sheep@437 329 """Close and reopen the repo, to make sure we are up to date."""
sheep@404 330
devel@723 331 #self.repo = mercurial.hg.repository(self.ui, self.repo_path)
devel@723 332 self._repos = {}
devel@723 333
devel@723 334 @property
devel@723 335 def repo(self):
devel@723 336 """Keep one open repository per thread."""
devel@723 337
devel@723 338 thread_id = thread.get_ident()
devel@723 339 try:
devel@723 340 return self._repos[thread_id]
devel@723 341 except KeyError:
devel@723 342 repo = mercurial.hg.repository(self.ui, self.repo_path)
devel@723 343 self._repos[thread_id] = repo
devel@723 344 return repo
sheep@5 345
sheep@5 346 def _find_repo_path(self, path):
sheep@276 347 """Go up the directory tree looking for a repository."""
sheep@276 348
sheep@5 349 while not os.path.isdir(os.path.join(path, ".hg")):
sheep@5 350 old_path, path = path, os.path.dirname(path)
sheep@5 351 if path == old_path:
sheep@5 352 return None
sheep@5 353 return path
sheep@0 354
sheep@0 355 def _file_path(self, title):
sheep@711 356 title = unicode(title).strip()
sheep@708 357 return os.path.join(self.path,
sheep@711 358 werkzeug.url_quote(title, safe=''))
sheep@0 359
sheep@5 360 def _title_to_file(self, title):
sheep@711 361 title = unicode(title).strip()
sheep@277 362 return os.path.join(self.repo_prefix,
sheep@711 363 werkzeug.url_quote(title, safe=''))
sheep@277 364
sheep@5 365 def _file_to_title(self, filename):
sheep@446 366 assert filename.startswith(self.repo_prefix)
sheep@5 367 name = filename[len(self.repo_prefix):].strip('/')
sheep@5 368 return werkzeug.url_unquote(name)
sheep@5 369
sheep@0 370 def __contains__(self, title):
sheep@593 371 if title:
sheep@715 372 file_path = self._file_path(title)
sheep@715 373 return os.path.isfile(file_path) and not os.path.islink(file_path)
sheep@0 374
sheep@383 375 def __iter__(self):
sheep@383 376 return self.all_pages()
sheep@383 377
sheep@424 378 def merge_changes(self, changectx, repo_file, text, user, parent):
sheep@428 379 """Commits and merges conflicting changes in the repository."""
sheep@428 380
sheep@424 381 tip_node = changectx.node()
sheep@424 382 filectx = changectx[repo_file].filectx(parent)
sheep@424 383 parent_node = filectx.changectx().node()
sheep@447 384
sheep@424 385 self.repo.dirstate.setparents(parent_node)
sheep@438 386 node = self._commit([repo_file], text, user)
sheep@439 387
sheep@447 388 partial = lambda filename: repo_file == filename
sheep@424 389 try:
sheep@424 390 unresolved = mercurial.merge.update(self.repo, tip_node,
sheep@635 391 True, True, partial)
sheep@635 392 msg = _(u'merge of edit conflict')
sheep@424 393 except mercurial.util.Abort:
sheep@424 394 unresolved = 1, 1, 1, 1
sheep@635 395 msg = _(u'failed merge of edit conflict')
sheep@424 396 self.repo.dirstate.setparents(tip_node, node)
sheep@424 397 # Mercurial 1.1 and later need updating the merge state
sheep@424 398 try:
sheep@424 399 mercurial.merge.mergestate(self.repo).mark(repo_file, "r")
sheep@424 400 except (AttributeError, KeyError):
sheep@424 401 pass
sheep@429 402 return msg
sheep@424 403
sheep@443 404 @locked_repo
sheep@443 405 def save_file(self, title, file_name, author=u'', comment=u'', parent=None):
sheep@443 406 """Save an existing file as specified page."""
sheep@424 407
sheep@163 408 user = author.encode('utf-8') or _(u'anon').encode('utf-8')
sheep@163 409 text = comment.encode('utf-8') or _(u'comment').encode('utf-8')
sheep@5 410 repo_file = self._title_to_file(title)
sheep@5 411 file_path = self._file_path(title)
sheep@715 412 if os.path.islink(file_path):
sheep@715 413 raise werkzeug.exceptions.Forbidden(u"Can't edit symbolic links")
sheep@424 414 mercurial.util.rename(file_name, file_path)
sheep@454 415 changectx = self._changectx()
sheep@5 416 try:
sheep@424 417 filectx_tip = changectx[repo_file]
sheep@424 418 current_page_rev = filectx_tip.filerev()
sheep@424 419 except mercurial.revlog.LookupError:
sheep@424 420 self.repo.add([repo_file])
sheep@424 421 current_page_rev = -1
sheep@447 422 if parent is not None and current_page_rev != parent:
sheep@428 423 msg = self.merge_changes(changectx, repo_file, text, user, parent)
sheep@428 424 user = '<wiki>'
sheep@428 425 text = msg.encode('utf-8')
sheep@438 426 self._commit([repo_file], text, user)
sheep@438 427
sheep@438 428
sheep@438 429 def _commit(self, files, text, user):
sheep@438 430 try:
sheep@454 431 return self.repo.commit(files=files, text=text, user=user,
sheep@438 432 force=True, empty_ok=True)
sheep@438 433 except TypeError:
sheep@454 434 # Mercurial 1.3 doesn't accept empty_ok or files parameter
sheep@454 435 match = mercurial.match.exact(self.repo_path, '', list(files))
sheep@438 436 return self.repo.commit(match=match, text=text, user=user,
sheep@438 437 force=True)
sheep@424 438
sheep@0 439
sheep@385 440 def save_data(self, title, data, author=u'', comment=u'', parent=None):
sheep@385 441 """Save data as specified page."""
sheep@276 442
sheep@0 443 try:
sheep@201 444 temp_path = tempfile.mkdtemp(dir=self.path)
sheep@201 445 file_path = os.path.join(temp_path, 'saved')
sheep@201 446 f = open(file_path, "wb")
sheep@385 447 f.write(data)
sheep@6 448 f.close()
sheep@261 449 self.save_file(title, file_path, author, comment, parent)
sheep@0 450 finally:
sheep@0 451 try:
sheep@201 452 os.unlink(file_path)
sheep@201 453 except OSError:
sheep@201 454 pass
sheep@201 455 try:
sheep@201 456 os.rmdir(temp_path)
sheep@0 457 except OSError:
sheep@0 458 pass
sheep@0 459
sheep@385 460 def save_text(self, title, text, author=u'', comment=u'', parent=None):
sheep@386 461 """Save text as specified page, encoded to charset."""
sheep@385 462
sheep@385 463 data = text.encode(self.charset)
sheep@385 464 self.save_data(title, data, author, comment, parent)
sheep@385 465
sheep@385 466 def page_text(self, title):
sheep@385 467 """Read unicode text of a page."""
sheep@385 468
sheep@385 469 data = self.open_page(title).read()
sheep@385 470 text = unicode(data, self.charset, 'replace')
sheep@385 471 return text
sheep@385 472
sheep@386 473 def page_lines(self, page):
sheep@632 474 for data in page.xreadlines():
sheep@385 475 yield unicode(data, self.charset, 'replace')
sheep@385 476
sheep@443 477 @locked_repo
sheep@31 478 def delete_page(self, title, author=u'', comment=u''):
sheep@31 479 user = author.encode('utf-8') or 'anon'
sheep@31 480 text = comment.encode('utf-8') or 'deleted'
sheep@31 481 repo_file = self._title_to_file(title)
sheep@31 482 file_path = self._file_path(title)
sheep@715 483 if os.path.islink(file_path):
sheep@715 484 raise werkzeug.exceptions.Forbidden(u"Can't edit symbolic links")
sheep@31 485 try:
sheep@443 486 os.unlink(file_path)
sheep@443 487 except OSError:
sheep@443 488 pass
sheep@443 489 self.repo.remove([repo_file])
sheep@443 490 self._commit([repo_file], text, user)
sheep@31 491
sheep@0 492 def open_page(self, title):
sheep@632 493 """Open the page and return a file-like object with its contents."""
sheep@632 494
sheep@715 495 file_path = self._file_path(title)
sheep@715 496 if os.path.islink(file_path):
sheep@715 497 raise werkzeug.exceptions.Forbidden(u"Can't read symbolic links")
sheep@1 498 try:
sheep@715 499 return open(file_path, "rb")
sheep@1 500 except IOError:
sheep@1 501 raise werkzeug.exceptions.NotFound()
sheep@1 502
sheep@103 503 def page_file_meta(self, title):
sheep@276 504 """Get page's inode number, size and last modification time."""
sheep@276 505
sheep@117 506 try:
sheep@117 507 (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size,
sheep@117 508 st_atime, st_mtime, st_ctime) = os.stat(self._file_path(title))
sheep@117 509 except OSError:
sheep@117 510 return 0, 0, 0
sheep@104 511 return st_ino, st_size, st_mtime
sheep@99 512
sheep@37 513 def page_meta(self, title):
sheep@276 514 """Get page's revision, date, last editor and his edit comment."""
sheep@276 515
sheep@72 516 filectx_tip = self._find_filectx(title)
sheep@78 517 if filectx_tip is None:
sheep@117 518 raise werkzeug.exceptions.NotFound()
sheep@117 519 #return -1, None, u'', u''
sheep@72 520 rev = filectx_tip.filerev()
sheep@72 521 filectx = filectx_tip.filectx(rev)
sheep@37 522 date = datetime.datetime.fromtimestamp(filectx.date()[0])
sheep@37 523 author = unicode(filectx.user(), "utf-8",
sheep@37 524 'replace').split('<')[0].strip()
sheep@37 525 comment = unicode(filectx.description(), "utf-8", 'replace')
sheep@37 526 return rev, date, author, comment
sheep@37 527
sheep@91 528 def repo_revision(self):
sheep@632 529 """Give the latest revision of the repository."""
sheep@632 530
sheep@454 531 return self._changectx().rev()
sheep@91 532
sheep@1 533 def page_mime(self, title):
sheep@525 534 """
sheep@525 535 Guess page's mime type ased on corresponding file name.
sheep@525 536 Default ot text/x-wiki for files without an extension.
sheep@276 537
sheep@525 538 >>> page_mime('something.txt')
sheep@525 539 'text/plain'
sheep@525 540 >>> page_mime('SomePage')
sheep@525 541 'text/x-wiki'
sheep@525 542 >>> page_mime(u'ąęśUnicodePage')
sheep@525 543 'text/x-wiki'
sheep@525 544 >>> page_mime('image.png')
sheep@525 545 'image/png'
sheep@525 546 >>> page_mime('style.css')
sheep@525 547 'text/css'
sheep@525 548 >>> page_mime('archive.tar.gz')
sheep@525 549 'archive/gzip'
sheep@525 550 """
sheep@0 551
sheep@525 552 addr = self._file_path(title)
sheep@525 553 mime, encoding = mimetypes.guess_type(addr, strict=False)
sheep@525 554 if encoding:
sheep@525 555 mime = 'archive/%s' % encoding
sheep@525 556 if mime is None:
sheep@525 557 mime = 'text/x-wiki'
sheep@525 558 return mime
sheep@454 559
sheep@454 560 def _changectx(self):
sheep@454 561 """Get the changectx of the tip."""
sheep@632 562
sheep@454 563 try:
sheep@454 564 # This is for Mercurial 1.0
sheep@454 565 return self.repo.changectx()
sheep@454 566 except TypeError:
sheep@454 567 # Mercurial 1.3 (and possibly earlier) needs an argument
sheep@454 568 return self.repo.changectx('tip')
sheep@454 569
sheep@24 570 def _find_filectx(self, title):
sheep@276 571 """Find the last revision in which the file existed."""
sheep@276 572
sheep@24 573 repo_file = self._title_to_file(title)
sheep@454 574 changectx = self._changectx()
sheep@24 575 stack = [changectx]
sheep@24 576 while repo_file not in changectx:
sheep@24 577 if not stack:
sheep@24 578 return None
sheep@24 579 changectx = stack.pop()
sheep@24 580 for parent in changectx.parents():
sheep@24 581 if parent != changectx:
sheep@24 582 stack.append(parent)
sheep@24 583 return changectx[repo_file]
sheep@24 584
sheep@24 585 def page_history(self, title):
sheep@276 586 """Iterate over the page's history."""
sheep@276 587
sheep@24 588 filectx_tip = self._find_filectx(title)
sheep@24 589 if filectx_tip is None:
sheep@24 590 return
sheep@24 591 maxrev = filectx_tip.filerev()
sheep@24 592 minrev = 0
sheep@24 593 for rev in range(maxrev, minrev-1, -1):
sheep@24 594 filectx = filectx_tip.filectx(rev)
sheep@24 595 date = datetime.datetime.fromtimestamp(filectx.date()[0])
sheep@24 596 author = unicode(filectx.user(), "utf-8",
sheep@24 597 'replace').split('<')[0].strip()
sheep@24 598 comment = unicode(filectx.description(), "utf-8", 'replace')
sheep@24 599 yield rev, date, author, comment
sheep@24 600
sheep@24 601 def page_revision(self, title, rev):
sheep@632 602 """Get binary content of the specified revision of the page."""
sheep@276 603
sheep@24 604 filectx_tip = self._find_filectx(title)
sheep@24 605 if filectx_tip is None:
sheep@24 606 raise werkzeug.exceptions.NotFound()
sheep@24 607 try:
sheep@385 608 data = filectx_tip.filectx(rev).data()
sheep@24 609 except IndexError:
sheep@24 610 raise werkzeug.exceptions.NotFound()
sheep@385 611 return data
sheep@385 612
sheep@385 613 def revision_text(self, title, rev):
sheep@632 614 """Get unicode text of the specified revision of the page."""
sheep@632 615
sheep@385 616 data = self.page_revision(title, rev)
sheep@385 617 text = unicode(data, self.charset, 'replace')
sheep@385 618 return text
sheep@24 619
sheep@24 620 def history(self):
sheep@276 621 """Iterate over the history of entire wiki."""
sheep@276 622
sheep@454 623 changectx = self._changectx()
sheep@24 624 maxrev = changectx.rev()
sheep@24 625 minrev = 0
sheep@24 626 for wiki_rev in range(maxrev, minrev-1, -1):
sheep@24 627 change = self.repo.changectx(wiki_rev)
sheep@31 628 date = datetime.datetime.fromtimestamp(change.date()[0])
sheep@31 629 author = unicode(change.user(), "utf-8",
sheep@31 630 'replace').split('<')[0].strip()
sheep@31 631 comment = unicode(change.description(), "utf-8", 'replace')
sheep@24 632 for repo_file in change.files():
sheep@24 633 if repo_file.startswith(self.repo_prefix):
sheep@24 634 title = self._file_to_title(repo_file)
sheep@31 635 try:
sheep@31 636 rev = change[repo_file].filerev()
sheep@31 637 except mercurial.revlog.LookupError:
sheep@31 638 rev = -1
sheep@24 639 yield title, rev, date, author, comment
sheep@24 640
sheep@26 641 def all_pages(self):
sheep@276 642 """Iterate over the titles of all pages in the wiki."""
sheep@276 643
sheep@26 644 for filename in os.listdir(self.path):
sheep@26 645 if (os.path.isfile(os.path.join(self.path, filename))
sheep@26 646 and not filename.startswith('.')):
sheep@26 647 yield werkzeug.url_unquote(filename)
sheep@26 648
sheep@394 649 def changed_since(self, rev):
sheep@394 650 """Return all pages that changed since specified repository revision."""
sheep@394 651
sheep@468 652 try:
sheep@468 653 last = self.repo.lookup(int(rev))
sheep@468 654 except IndexError:
sheep@468 655 for page in self.all_pages():
sheep@468 656 yield page
sheep@546 657 return
sheep@394 658 current = self.repo.lookup('tip')
sheep@394 659 status = self.repo.status(current, last)
sheep@394 660 modified, added, removed, deleted, unknown, ignored, clean = status
sheep@394 661 for filename in modified+added+removed+deleted:
sheep@395 662 if filename.startswith(self.repo_prefix):
sheep@395 663 yield self._file_to_title(filename)
sheep@394 664
sheep@0 665 class WikiParser(object):
sheep@380 666 r"""
sheep@271 667 Responsible for generating HTML markup from the wiki markup.
sheep@271 668
sheep@271 669 The parser works on two levels. On the block level, it analyzes lines
sheep@271 670 of text and decides what kind of block element they belong to (block
sheep@271 671 elements include paragraphs, lists, headings, preformatted blocks).
sheep@271 672 Lines belonging to the same block are joined together, and a second
sheep@271 673 pass is made using regular expressions to parse line-level elements,
sheep@271 674 such as links, bold and italic text and smileys.
sheep@271 675
sheep@271 676 Some block-level elements, such as preformatted blocks, consume additional
sheep@323 677 lines from the input until they encounter the end-of-block marker, using
sheep@323 678 lines_until. Most block-level elements are just runs of marked up lines
sheep@323 679 though.
sheep@375 680
sheep@380 681
sheep@271 682 """
sheep@271 683
sheep@0 684 bullets_pat = ur"^\s*[*]+\s+"
sheep@0 685 heading_pat = ur"^\s*=+"
sheep@324 686 quote_pat = ur"^[>]+\s+"
sheep@646 687 block = {
sheep@646 688 # "name": (priority, ur"pattern"),
sheep@646 689 "bullets": (10, bullets_pat),
sheep@646 690 "code": (20, ur"^[{][{][{]+\s*$"),
sheep@646 691 "conflict": (30, ur"^<<<<<<< local\s*$"),
sheep@646 692 "empty": (40, ur"^\s*$"),
sheep@646 693 "heading": (50, heading_pat),
sheep@646 694 "indent": (60, ur"^[ \t]+"),
sheep@646 695 "macro":(70, ur"^<<\w+\s*$"),
sheep@646 696 "quote": (80, quote_pat),
sheep@646 697 "rule": (90, ur"^\s*---+\s*$"),
sheep@646 698 "syntax": (100, ur"^\{\{\{\#![\w+#.-]+\s*$"),
sheep@646 699 "table": (110, ur"^\|"),
sheep@646 700 }
sheep@277 701 image_pat = (ur"\{\{(?P<image_target>([^|}]|}[^|}])*)"
sheep@277 702 ur"(\|(?P<image_text>([^}]|}[^}])*))?}}")
sheep@0 703 smilies = {
sheep@0 704 r':)': "smile.png",
sheep@0 705 r':(': "frown.png",
sheep@0 706 r':P': "tongue.png",
sheep@0 707 r':D': "grin.png",
sheep@0 708 r';)': "wink.png",
sheep@0 709 }
sheep@0 710 punct = {
sheep@0 711 r'...': "&hellip;",
sheep@300 712 r'--': "&ndash;",
sheep@0 713 r'---': "&mdash;",
sheep@0 714 r'~': "&nbsp;",
sheep@0 715 r'\~': "~",
sheep@0 716 r'~~': "&sim;",
sheep@0 717 r'(C)': "&copy;",
sheep@0 718 r'-->': "&rarr;",
sheep@0 719 r'<--': "&larr;",
sheep@0 720 r'(R)': "&reg;",
sheep@0 721 r'(TM)': "&trade;",
sheep@0 722 r'%%': "&permil;",
sheep@0 723 r'``': "&ldquo;",
sheep@0 724 r"''": "&rdquo;",
sheep@0 725 r",,": "&bdquo;",
sheep@0 726 }
sheep@645 727 markup = {
sheep@645 728 # "name": (priority, ur"pattern"),
sheep@645 729 "bold": (10, ur"[*][*]"),
sheep@645 730 "code": (20, ur"[{][{][{](?P<code_text>([^}]|[^}][}]|[^}][}][}])"
sheep@633 731 ur"*[}]*)[}][}][}]"),
sheep@645 732 "free_link": (30, ur"""(http|https|ftp)://\S+[^\s.,:;!?()'"=+<>-]"""),
sheep@645 733 "italic": (40 , ur"//"),
sheep@645 734 "link": (50, ur"\[\[(?P<link_target>([^|\]]|\][^|\]])+)"
sheep@633 735 ur"(\|(?P<link_text>([^\]]|\][^\]])+))?\]\]"),
sheep@645 736 "image": (60, image_pat),
sheep@645 737 "linebreak": (70, ur"\\\\"),
sheep@645 738 "macro": (80, ur"[<][<](?P<macro_name>\w+)\s+"
sheep@633 739 ur"(?P<macro_text>([^>]|[^>][>])+)[>][>]"),
sheep@645 740 "mail": (90, ur"""(mailto:)?\S+@\S+(\.[^\s.,:;!?()'"/=+<>-]+)+"""),
sheep@645 741 "math": (100, ur"\$\$(?P<math_text>[^$]+)\$\$"),
sheep@645 742 "mono": (110, ur"##"),
sheep@645 743 "newline": (120, ur"\n"),
sheep@645 744 "punct": (130, ur'(^|\b|(?<=\s))(%s)((?=[\s.,:;!?)/&=+])|\b|$)' %
sheep@645 745 ur"|".join(re.escape(k) for k in punct)),
sheep@645 746 "table": (140, ur"=?\|=?"),
sheep@645 747 "text": (150, ur".+?"),
sheep@645 748 }
sheep@0 749
sheep@323 750
sheep@323 751 def __init__(self, lines, wiki_link, wiki_image,
sheep@526 752 wiki_syntax=None, wiki_math=None, smilies=None):
sheep@323 753 self.wiki_link = wiki_link
sheep@323 754 self.wiki_image = wiki_image
sheep@323 755 self.wiki_syntax = wiki_syntax
sheep@323 756 self.wiki_math = wiki_math
sheep@411 757 self.enumerated_lines = enumerate(lines)
sheep@526 758 if smilies is not None:
sheep@526 759 self.smilies = smilies
sheep@411 760 self.compile_patterns()
sheep@323 761 self.headings = {}
sheep@326 762 self.stack = []
sheep@349 763 self.line_no = 0
sheep@411 764
sheep@411 765 def compile_patterns(self):
sheep@411 766 self.quote_re = re.compile(self.quote_pat, re.U)
sheep@411 767 self.heading_re = re.compile(self.heading_pat, re.U)
sheep@411 768 self.bullets_re = re.compile(self.bullets_pat, re.U)
sheep@646 769 patterns = ((k, p) for (k, (x, p)) in
sheep@646 770 sorted(self.block.iteritems(), key=lambda x: x[1][0]))
sheep@646 771 self.block_re = re.compile(ur"|".join("(?P<%s>%s)" % pat
sheep@646 772 for pat in patterns), re.U)
sheep@411 773 self.code_close_re = re.compile(ur"^\}\}\}\s*$", re.U)
sheep@411 774 self.macro_close_re = re.compile(ur"^>>\s*$", re.U)
sheep@411 775 self.conflict_close_re = re.compile(ur"^>>>>>>> other\s*$", re.U)
sheep@411 776 self.conflict_sep_re = re.compile(ur"^=======\s*$", re.U)
sheep@411 777 self.image_re = re.compile(self.image_pat, re.U)
sheep@645 778 smileys = ur"|".join(re.escape(k) for k in self.smilies)
sheep@645 779 smiley_pat = (ur"(^|\b|(?<=\s))(?P<smiley_face>%s)"
sheep@645 780 ur"((?=[\s.,:;!?)/&=+-])|$)" % smileys)
sheep@645 781 self.markup['smiley'] = (125, smiley_pat)
sheep@645 782 patterns = ((k, p) for (k, (x, p)) in
sheep@645 783 sorted(self.markup.iteritems(), key=lambda x: x[1][0]))
sheep@645 784 self.markup_re = re.compile(ur"|".join("(?P<%s>%s)" % pat
sheep@645 785 for pat in patterns), re.U)
sheep@323 786
sheep@323 787 def __iter__(self):
sheep@323 788 return self.parse()
sheep@323 789
sheep@430 790 @classmethod
sheep@430 791 def extract_links(cls, text):
sheep@430 792 links = []
sheep@431 793 def link(addr, label=None, class_=None, image=None, alt=None, lineno=0):
sheep@709 794 addr = addr.strip()
sheep@430 795 if external_link(addr):
sheep@430 796 return u''
sheep@430 797 if '#' in addr:
sheep@430 798 addr, chunk = addr.split('#', 1)
sheep@430 799 if addr == u'':
sheep@430 800 return u''
sheep@430 801 links.append((addr, label))
sheep@430 802 return u''
sheep@430 803 lines = text.split('\n')
sheep@430 804 for part in cls(lines, link, link):
sheep@431 805 for ret in links:
sheep@431 806 yield ret
sheep@431 807 links[:] = []
sheep@430 808
sheep@323 809 def parse(self):
sheep@323 810 """Parse a list of lines of wiki markup, yielding HTML for it."""
sheep@323 811
sheep@411 812 self.headings = {}
sheep@411 813 self.stack = []
sheep@411 814 self.line_no = 0
sheep@411 815
sheep@349 816 def key(enumerated_line):
sheep@349 817 line_no, line = enumerated_line
sheep@323 818 match = self.block_re.match(line)
sheep@323 819 if match:
sheep@323 820 return match.lastgroup
sheep@323 821 return "paragraph"
sheep@323 822
sheep@349 823 for kind, block in itertools.groupby(self.enumerated_lines, key):
sheep@323 824 func = getattr(self, "_block_%s" % kind)
sheep@323 825 for part in func(block):
sheep@323 826 yield part
sheep@323 827
sheep@323 828 def parse_line(self, line):
sheep@376 829 """
sheep@376 830 Find all the line-level markup and return HTML for it.
sheep@376 831
sheep@376 832 """
sheep@323 833
sheep@419 834 for match in self.markup_re.finditer(line):
sheep@419 835 func = getattr(self, "_line_%s" % match.lastgroup)
sheep@419 836 yield func(match.groupdict())
sheep@323 837
sheep@0 838 def pop_to(self, stop):
sheep@0 839 """
sheep@0 840 Pop from the stack until the specified tag is encoutered.
sheep@0 841 Return string containing closing tags of everything popped.
sheep@0 842 """
sheep@0 843 tags = []
sheep@0 844 tag = None
sheep@0 845 try:
sheep@0 846 while tag != stop:
sheep@0 847 tag = self.stack.pop()
sheep@0 848 tags.append(tag)
sheep@0 849 except IndexError:
sheep@0 850 pass
sheep@0 851 return u"".join(u"</%s>" % tag for tag in tags)
sheep@0 852
sheep@320 853 def lines_until(self, close_re):
sheep@320 854 """Get lines from input until the closing markup is encountered."""
sheep@320 855
sheep@349 856 self.line_no, line = self.enumerated_lines.next()
sheep@320 857 while not close_re.match(line):
sheep@320 858 yield line.rstrip()
sheep@349 859 line_no, line = self.enumerated_lines.next()
sheep@320 860
sheep@376 861 # methods for the markup inside lines:
sheep@376 862
sheep@621 863 def _line_table(self, groups):
sheep@621 864 return groups["table"]
sheep@621 865
sheep@277 866 def _line_linebreak(self, groups):
sheep@0 867 return u'<br>'
sheep@0 868
sheep@277 869 def _line_smiley(self, groups):
sheep@0 870 smiley = groups["smiley_face"]
sheep@684 871 try:
sheep@684 872 url = self.smilies[smiley]
sheep@684 873 except KeyError:
sheep@684 874 url = ''
sheep@684 875 return self.wiki_image(url, smiley, class_="smiley")
sheep@0 876
sheep@277 877 def _line_bold(self, groups):
sheep@0 878 if 'b' in self.stack:
sheep@0 879 return self.pop_to('b')
sheep@0 880 else:
sheep@0 881 self.stack.append('b')
sheep@0 882 return u"<b>"
sheep@0 883
sheep@277 884 def _line_italic(self, groups):
sheep@0 885 if 'i' in self.stack:
sheep@0 886 return self.pop_to('i')
sheep@0 887 else:
sheep@0 888 self.stack.append('i')
sheep@0 889 return u"<i>"
sheep@0 890
sheep@634 891 def _line_mono(self, groups):
sheep@634 892 if 'tt' in self.stack:
sheep@634 893 return self.pop_to('tt')
sheep@634 894 else:
sheep@634 895 self.stack.append('tt')
sheep@634 896 return u"<tt>"
sheep@634 897
sheep@277 898 def _line_punct(self, groups):
sheep@0 899 text = groups["punct"]
sheep@0 900 return self.punct.get(text, text)
sheep@0 901
sheep@277 902 def _line_newline(self, groups):
sheep@0 903 return "\n"
sheep@0 904
sheep@277 905 def _line_text(self, groups):
sheep@0 906 return werkzeug.escape(groups["text"])
sheep@0 907
sheep@277 908 def _line_math(self, groups):
sheep@90 909 if self.wiki_math:
sheep@90 910 return self.wiki_math(groups["math_text"])
sheep@90 911 else:
sheep@90 912 return "<var>%s</var>" % werkzeug.escape(groups["math_text"])
sheep@0 913
sheep@277 914 def _line_code(self, groups):
sheep@0 915 return u'<code>%s</code>' % werkzeug.escape(groups["code_text"])
sheep@0 916
sheep@277 917 def _line_free_link(self, groups):
sheep@0 918 groups['link_target'] = groups['free_link']
sheep@277 919 return self._line_link(groups)
sheep@0 920
sheep@277 921 def _line_mail(self, groups):
sheep@202 922 addr = groups['mail']
sheep@202 923 groups['link_text'] = addr
sheep@202 924 if not addr.startswith(u'mailto:'):
sheep@202 925 addr = u'mailto:%s' % addr
sheep@202 926 groups['link_target'] = addr
sheep@277 927 return self._line_link(groups)
sheep@202 928
sheep@277 929 def _line_link(self, groups):
sheep@0 930 target = groups['link_target']
sheep@29 931 text = groups.get('link_text')
sheep@29 932 if not text:
sheep@29 933 text = target
sheep@29 934 if '#' in text:
sheep@29 935 text, chunk = text.split('#', 1)
sheep@0 936 match = self.image_re.match(text)
sheep@0 937 if match:
sheep@277 938 image = self._line_image(match.groupdict())
sheep@144 939 return self.wiki_link(target, text, image=image)
sheep@144 940 return self.wiki_link(target, text)
sheep@0 941
sheep@277 942 def _line_image(self, groups):
sheep@0 943 target = groups['image_target']
sheep@218 944 alt = groups.get('image_text')
sheep@218 945 if alt is None:
sheep@218 946 alt = target
sheep@0 947 return self.wiki_image(target, alt)
sheep@0 948
sheep@277 949 def _line_macro(self, groups):
sheep@0 950 name = groups['macro_name']
sheep@0 951 text = groups['macro_text'].strip()
sheep@277 952 return u'<span class="%s">%s</span>' % (
sheep@277 953 werkzeug.escape(name, quote=True),
sheep@0 954 werkzeug.escape(text))
sheep@0 955
sheep@326 956 # methods for the block (multiline) markup:
sheep@326 957
sheep@277 958 def _block_code(self, block):
sheep@349 959 for self.line_no, part in block:
sheep@320 960 inside = u"\n".join(self.lines_until(self.code_close_re))
sheep@357 961 yield werkzeug.html.pre(werkzeug.html(inside), class_="code",
sheep@357 962 id="line_%d" % self.line_no)
sheep@0 963
sheep@277 964 def _block_syntax(self, block):
sheep@349 965 for self.line_no, part in block:
sheep@27 966 syntax = part.lstrip('{#!').strip()
sheep@320 967 inside = u"\n".join(self.lines_until(self.code_close_re))
sheep@90 968 if self.wiki_syntax:
sheep@357 969 return self.wiki_syntax(inside, syntax=syntax,
sheep@357 970 line_no=self.line_no)
sheep@90 971 else:
sheep@357 972 return [werkzeug.html.div(werkzeug.html.pre(
sheep@357 973 werkzeug.html(inside), id="line_%d" % self.line_no),
sheep@357 974 class_="highlight")]
sheep@27 975
sheep@277 976 def _block_macro(self, block):
sheep@349 977 for self.line_no, part in block:
sheep@0 978 name = part.lstrip('<').strip()
sheep@320 979 inside = u"\n".join(self.lines_until(self.macro_close_re))
sheep@277 980 yield u'<div class="%s">%s</div>' % (
sheep@277 981 werkzeug.escape(name, quote=True),
sheep@0 982 werkzeug.escape(inside))
sheep@0 983
sheep@277 984 def _block_paragraph(self, block):
sheep@349 985 parts = []
sheep@351 986 first_line = None
sheep@349 987 for self.line_no, part in block:
sheep@351 988 if first_line is None:
sheep@351 989 first_line = self.line_no
sheep@349 990 parts.append(part)
sheep@349 991 text = u"".join(self.parse_line(u"".join(parts)))
sheep@357 992 yield werkzeug.html.p(text, self.pop_to(""), id="line_%d" % first_line)
sheep@0 993
sheep@277 994 def _block_indent(self, block):
sheep@349 995 parts = []
sheep@357 996 first_line = None
sheep@349 997 for self.line_no, part in block:
sheep@357 998 if first_line is None:
sheep@357 999 first_line = self.line_no
sheep@349 1000 parts.append(part.rstrip())
sheep@349 1001 text = u"\n".join(parts)
sheep@357 1002 yield werkzeug.html.pre(werkzeug.html(text), id="line_%d" % first_line)
sheep@0 1003
sheep@277 1004 def _block_table(self, block):
sheep@380 1005 first_line = None
sheep@137 1006 in_head = False
sheep@349 1007 for self.line_no, line in block:
sheep@380 1008 if first_line is None:
sheep@380 1009 first_line = self.line_no
sheep@380 1010 yield u'<table id="line_%d">' % first_line
sheep@137 1011 table_row = line.strip()
sheep@137 1012 is_header = table_row.startswith('|=') and table_row.endswith('=|')
sheep@137 1013 if not in_head and is_header:
sheep@137 1014 in_head = True
sheep@137 1015 yield '<thead>'
sheep@137 1016 elif in_head and not is_header:
sheep@137 1017 in_head = False
sheep@137 1018 yield '</thead>'
sheep@0 1019 yield '<tr>'
sheep@621 1020 in_cell = False
sheep@621 1021 in_th = False
sheep@621 1022
sheep@621 1023 for part in self.parse_line(table_row):
sheep@621 1024 if part in ('=|', '|', '=|=', '|='):
sheep@621 1025 if in_cell:
sheep@621 1026 if in_th:
sheep@621 1027 yield '</th>'
sheep@621 1028 else:
sheep@621 1029 yield '</td>'
sheep@621 1030 in_cell = False
sheep@621 1031 if part in ('=|=', '|='):
sheep@621 1032 in_th = True
sheep@621 1033 else:
sheep@621 1034 in_th = False
sheep@137 1035 else:
sheep@621 1036 if not in_cell:
sheep@621 1037 if in_th:
sheep@621 1038 yield '<th>'
sheep@621 1039 else:
sheep@621 1040 yield '<td>'
sheep@621 1041 in_cell = True
sheep@621 1042 yield part
sheep@621 1043 if in_cell:
sheep@621 1044 if in_th:
sheep@621 1045 yield '</th>'
sheep@621 1046 else:
sheep@621 1047 yield '</td>'
sheep@0 1048 yield '</tr>'
sheep@0 1049 yield u'</table>'
sheep@0 1050
sheep@277 1051 def _block_empty(self, block):
sheep@0 1052 yield u''
sheep@0 1053
sheep@277 1054 def _block_rule(self, block):
sheep@349 1055 for self.line_no, line in block:
sheep@349 1056 yield werkzeug.html.hr()
sheep@0 1057
sheep@277 1058 def _block_heading(self, block):
sheep@349 1059 for self.line_no, line in block:
sheep@0 1060 level = min(len(self.heading_re.match(line).group(0).strip()), 5)
sheep@197 1061 self.headings[level-1] = self.headings.get(level-1, 0)+1
sheep@277 1062 label = u"-".join(str(self.headings.get(i, 0))
sheep@277 1063 for i in range(level))
sheep@357 1064 yield werkzeug.html.a(name="head-%s" % label)
sheep@357 1065 yield u'<h%d id="line_%d">%s</h%d>' % (level, self.line_no,
sheep@357 1066 werkzeug.escape(line.strip("= \t\n\r\v")), level)
sheep@0 1067
sheep@277 1068 def _block_bullets(self, block):
sheep@0 1069 level = 0
sheep@365 1070 in_ul = False
sheep@349 1071 for self.line_no, line in block:
sheep@0 1072 nest = len(self.bullets_re.match(line).group(0).strip())
sheep@298 1073 while nest > level:
sheep@365 1074 if in_ul:
sheep@365 1075 yield '<li>'
sheep@357 1076 yield '<ul id="line_%d">' % self.line_no
sheep@365 1077 in_ul = True
sheep@0 1078 level += 1
sheep@298 1079 while nest < level:
sheep@325 1080 yield '</li></ul>'
sheep@365 1081 in_ul = False
sheep@0 1082 level -= 1
sheep@365 1083 if nest == level and not in_ul:
sheep@365 1084 yield '</li>'
sheep@0 1085 content = line.lstrip().lstrip('*').strip()
sheep@326 1086 yield '<li>%s%s' % (u"".join(self.parse_line(content)),
sheep@326 1087 self.pop_to(""))
sheep@365 1088 in_ul = False
sheep@326 1089 yield '</li></ul>'*level
sheep@0 1090
sheep@324 1091 def _block_quote(self, block):
sheep@324 1092 level = 0
sheep@334 1093 in_p = False
sheep@349 1094 for self.line_no, line in block:
sheep@324 1095 nest = len(self.quote_re.match(line).group(0).strip())
sheep@336 1096 if nest == level:
sheep@336 1097 yield u'\n'
sheep@324 1098 while nest > level:
sheep@334 1099 if in_p:
sheep@336 1100 yield '%s</p>' % self.pop_to("")
sheep@334 1101 in_p = False
sheep@334 1102 yield '<blockquote>'
sheep@324 1103 level += 1
sheep@324 1104 while nest < level:
sheep@334 1105 if in_p:
sheep@336 1106 yield '%s</p>' % self.pop_to("")
sheep@334 1107 in_p = False
sheep@334 1108 yield '</blockquote>'
sheep@324 1109 level -= 1
sheep@324 1110 content = line.lstrip().lstrip('>').strip()
sheep@334 1111 if not in_p:
sheep@357 1112 yield '<p id="line_%d">' % self.line_no
sheep@334 1113 in_p = True
sheep@336 1114 yield u"".join(self.parse_line(content))
sheep@334 1115 if in_p:
sheep@336 1116 yield '%s</p>' % self.pop_to("")
sheep@334 1117 yield '</blockquote>'*level
sheep@324 1118
sheep@368 1119 def _block_conflict(self, block):
sheep@369 1120 for self.line_no, part in block:
sheep@369 1121 yield u'<div class="conflict">'
sheep@369 1122 local = u"\n".join(self.lines_until(self.conflict_sep_re))
sheep@369 1123 yield werkzeug.html.pre(werkzeug.html(local),
sheep@369 1124 class_="local",
sheep@369 1125 id="line_%d" % self.line_no)
sheep@369 1126 other = u"\n".join(self.lines_until(self.conflict_close_re))
sheep@369 1127 yield werkzeug.html.pre(werkzeug.html(other),
sheep@369 1128 class_="other",
sheep@369 1129 id="line_%d" % self.line_no)
sheep@369 1130 yield u'</div>'
sheep@0 1131
sheep@645 1132
sheep@645 1133 class WikiWikiParser(WikiParser):
sheep@645 1134 """A version of WikiParser that recognizes WikiWord links."""
sheep@645 1135
sheep@647 1136 markup = dict(WikiParser.markup)
sheep@647 1137 camel_link = ur"\w+[%s]\w+" % re.escape(
sheep@647 1138 u''.join(unichr(i) for i in xrange(sys.maxunicode)
sheep@647 1139 if unicodedata.category(unichr(i))=='Lu'))
sheep@647 1140 markup["camel_link"] = (105, camel_link)
sheep@648 1141 markup["camel_nolink"] = (106, ur"[!~](?P<camel_text>%s)" % camel_link)
sheep@645 1142
sheep@645 1143 def _line_camel_link(self, groups):
sheep@645 1144 groups['link_target'] = groups['camel_link']
sheep@645 1145 return self._line_link(groups)
sheep@645 1146
sheep@645 1147 def _line_camel_nolink(self, groups):
sheep@645 1148 return werkzeug.escape(groups["camel_text"])
sheep@645 1149
sheep@645 1150
sheep@26 1151 class WikiSearch(object):
sheep@271 1152 """
sheep@271 1153 Responsible for indexing words and links, for fast searching and
sheep@271 1154 backlinks. Uses a cache directory to store the index files.
sheep@271 1155 """
sheep@271 1156
sheep@250 1157 word_pattern = re.compile(ur"""\w[-~&\w]+\w""", re.UNICODE)
sheep@419 1158 jword_pattern = re.compile(
sheep@419 1159 ur"""[ヲ-゚]+|[ぁ-ん~ー]+|[ァ-ヶ~ー]+|[0-9A-Za-z]+|"""
sheep@419 1160 ur"""[0-9A-Za-zΑ-Ωα-ωА-я]+|"""
sheep@419 1161 ur"""[^- !"#$%&'()*+,./:;<=>?@\[\\\]^_`{|}"""
sheep@419 1162 ur"""‾。「」、・ 、。,.・:;?!゛゜´`¨"""
sheep@419 1163 ur"""^ ̄_/〜‖|…‥‘’“”"""
sheep@419 1164 ur"""()〔〕[]{}〈〉《》「」『』【】+−±×÷"""
sheep@419 1165 ur"""=≠<>≦≧∞∴♂♀°′″℃¥$¢£"""
sheep@419 1166 ur"""%#&*@§☆★○●◎◇◆□■△▲▽▼※〒"""
sheep@419 1167 ur"""→←↑↓〓∈∋⊆⊇⊂⊃∪∩∧∨¬⇒⇔∠∃∠⊥"""
sheep@419 1168 ur"""⌒∂∇≡≒≪≫√∽∝∵∫∬ʼn♯♭♪†‡¶◾"""
sheep@419 1169 ur"""─│┌┐┘└├┬┤┴┼"""
sheep@419 1170 ur"""━┃┏┓┛┗┣┫┻╋"""
sheep@419 1171 ur"""┠┯┨┷┿┝┰┥┸╂"""
sheep@419 1172 ur"""ヲ-゚ぁ-ん~ーァ-ヶ"""
sheep@419 1173 ur"""0-9A-Za-z0-9A-Za-zΑ-Ωα-ωА-я]+""", re.UNICODE)
sheep@384 1174 _con = {}
sheep@165 1175
sheep@406 1176 def __init__(self, cache_path, lang, storage):
sheep@165 1177 self.path = cache_path
sheep@386 1178 self.storage = storage
sheep@285 1179 self.lang = lang
sheep@411 1180 if lang == "ja":
sheep@384 1181 self.split_text = self.split_japanese_text
sheep@285 1182 self.filename = os.path.join(cache_path, 'index.sqlite3')
sheep@285 1183 if not os.path.isdir(self.path):
sheep@285 1184 self.empty = True
sheep@285 1185 os.makedirs(self.path)
sheep@304 1186 elif not os.path.exists(self.filename):
sheep@303 1187 self.empty = True
sheep@285 1188 else:
sheep@285 1189 self.empty = False
sheep@302 1190 con = self.con # sqlite3.connect(self.filename)
sheep@463 1191 self.con.execute('CREATE TABLE IF NOT EXISTS titles '
sheep@463 1192 '(id INTEGER PRIMARY KEY, title VARCHAR);')
sheep@463 1193 self.con.execute('CREATE TABLE IF NOT EXISTS words '
sheep@463 1194 '(word VARCHAR, page INTEGER, count INTEGER);')
sheep@463 1195 self.con.execute('CREATE INDEX IF NOT EXISTS index1 '
sheep@463 1196 'ON words (page);')
sheep@463 1197 self.con.execute('CREATE INDEX IF NOT EXISTS index2 '
sheep@463 1198 'ON words (word);')
sheep@463 1199 self.con.execute('CREATE TABLE IF NOT EXISTS links '
sheep@463 1200 '(src INTEGER, target INTEGER, label VARCHAR, number INTEGER);')
sheep@384 1201 self.con.commit()
sheep@305 1202 self.stop_words_re = re.compile(u'^('+u'|'.join(re.escape(_(
sheep@252 1203 u"""am ii iii per po re a about above
sheep@165 1204 across after afterwards again against all almost alone along already also
sheep@165 1205 although always am among ain amongst amoungst amount an and another any aren
sheep@165 1206 anyhow anyone anything anyway anywhere are around as at back be became because
sheep@165 1207 become becomes becoming been before beforehand behind being below beside
sheep@165 1208 besides between beyond bill both but by can cannot cant con could couldnt
sheep@26 1209 describe detail do done down due during each eg eight either eleven else etc
sheep@26 1210 elsewhere empty enough even ever every everyone everything everywhere except
sheep@26 1211 few fifteen fifty fill find fire first five for former formerly forty found
sheep@26 1212 four from front full further get give go had has hasnt have he hence her here
sheep@26 1213 hereafter hereby herein hereupon hers herself him himself his how however
sheep@26 1214 hundred i ie if in inc indeed interest into is it its itself keep last latter
sheep@26 1215 latterly least isn less made many may me meanwhile might mill mine more
sheep@26 1216 moreover most mostly move much must my myself name namely neither never
sheep@26 1217 nevertheless next nine no nobody none noone nor not nothing now nowhere of off
sheep@26 1218 often on once one only onto or other others otherwise our ours ourselves out
sheep@26 1219 over own per perhaps please pre put rather re same see seem seemed seeming
sheep@26 1220 seems serious several she should show side since sincere six sixty so some
sheep@26 1221 somehow someone something sometime sometimes somewhere still such take ten than
sheep@26 1222 that the their theirs them themselves then thence there thereafter thereby
sheep@26 1223 therefore therein thereupon these they thick thin third this those though three
sheep@26 1224 through throughout thru thus to together too toward towards twelve twenty two
sheep@26 1225 un under ve until up upon us very via was wasn we well were what whatever when
sheep@26 1226 whence whenever where whereafter whereas whereby wherein whereupon wherever
sheep@26 1227 whether which while whither who whoever whole whom whose why will with within
sheep@305 1228 without would yet you your yours yourself yourselves""")).split())
sheep@305 1229 +ur')$|.*\d.*', re.U|re.I|re.X)
sheep@394 1230
sheep@395 1231
sheep@26 1232
sheep@302 1233 @property
sheep@302 1234 def con(self):
sheep@302 1235 """Keep one connection per thread."""
sheep@306 1236
sheep@302 1237 thread_id = thread.get_ident()
sheep@302 1238 try:
sheep@302 1239 return self._con[thread_id]
sheep@302 1240 except KeyError:
sheep@384 1241 connection = sqlite3.connect(self.filename)
sheep@394 1242 connection.isolation_level = None
sheep@384 1243 self._con[thread_id] = connection
sheep@384 1244 return connection
sheep@302 1245
sheep@399 1246 def split_text(self, text, stop=True):
sheep@306 1247 """Splits text into words, removing stop words"""
sheep@306 1248
sheep@26 1249 for match in self.word_pattern.finditer(text):
sheep@249 1250 word = match.group(0)
sheep@399 1251 if not (stop and self.stop_words_re.match(word)):
sheep@142 1252 yield word.lower()
sheep@26 1253
sheep@399 1254 def split_japanese_text(self, text, stop=True):
sheep@306 1255 """Splits text into words, including rules for Japanese"""
sheep@306 1256
sheep@285 1257 for match in self.word_pattern.finditer(text):
sheep@285 1258 word = match.group(0)
sheep@285 1259 got_japanese = False
sheep@411 1260 for m in self.jword_pattern.finditer(word):
sheep@411 1261 w = m.group(0)
sheep@285 1262 got_japanese = True
sheep@411 1263 if not (stop and self.stop_words_re.match(w)):
sheep@306 1264 yield w.lower()
sheep@399 1265 if not (got_japanese or stop and self.stop_words_re.match(word)):
sheep@285 1266 yield word.lower()
sheep@26 1267
sheep@26 1268 def count_words(self, words):
sheep@26 1269 count = {}
sheep@26 1270 for word in words:
sheep@384 1271 count[word] = count.get(word, 0)+1
sheep@26 1272 return count
sheep@26 1273
sheep@384 1274 def title_id(self, title, con):
sheep@462 1275 c = con.execute('SELECT id FROM titles WHERE title=?;', (title,))
sheep@285 1276 idents = c.fetchone()
sheep@384 1277 if idents is None:
sheep@462 1278 con.execute('INSERT INTO titles (title) VALUES (?);', (title,))
sheep@466 1279 c = con.execute('SELECT LAST_INSERT_ROWID();')
sheep@384 1280 idents = c.fetchone()
sheep@384 1281 return idents[0]
sheep@285 1282
sheep@388 1283 def update_words(self, title, text, cursor):
sheep@388 1284 title_id = self.title_id(title, cursor)
sheep@384 1285 words = self.count_words(self.split_text(text))
sheep@285 1286 title_words = self.count_words(self.split_text(title))
sheep@285 1287 for word, count in title_words.iteritems():
sheep@285 1288 words[word] = words.get(word, 0) + count
sheep@462 1289 cursor.execute('DELETE FROM words WHERE page=?;', (title_id,))
sheep@285 1290 for word, count in words.iteritems():
sheep@462 1291 cursor.execute('INSERT INTO words VALUES (?, ?, ?);',
sheep@285 1292 (word, title_id, count))
sheep@26 1293
sheep@388 1294 def update_links(self, title, links_and_labels, cursor):
sheep@388 1295 title_id = self.title_id(title, cursor)
sheep@463 1296 cursor.execute('DELETE FROM links WHERE src=?;', (title_id,))
sheep@285 1297 for number, (link, label) in enumerate(links_and_labels):
sheep@463 1298 cursor.execute('INSERT INTO links VALUES (?, ?, ?, ?);',
sheep@285 1299 (title_id, link, label, number))
sheep@147 1300
sheep@657 1301 def orphaned_pages(self):
sheep@658 1302 """Gives all pages with no links to them."""
sheep@658 1303
sheep@657 1304 con = self.con
sheep@657 1305 try:
sheep@657 1306 sql = ('SELECT title FROM titles '
sheep@657 1307 'WHERE NOT EXISTS '
sheep@657 1308 '(SELECT * FROM links WHERE target=title) '
sheep@657 1309 'ORDER BY title;')
sheep@657 1310 for (title,) in con.execute(sql):
sheep@712 1311 yield unicode(title)
sheep@657 1312 finally:
sheep@657 1313 con.commit()
sheep@657 1314
sheep@659 1315 def wanted_pages(self):
sheep@682 1316 """Gives all pages that are linked to, but don't exist, together with
sheep@682 1317 the number of links."""
sheep@659 1318
sheep@659 1319 con = self.con
sheep@659 1320 try:
sheep@682 1321 sql = ('SELECT COUNT(*), target FROM links '
sheep@659 1322 'WHERE NOT EXISTS '
sheep@659 1323 '(SELECT * FROM titles WHERE target=title) '
sheep@682 1324 'GROUP BY target ORDER BY -COUNT(*);')
sheep@705 1325 for (refs, db_title,) in con.execute(sql):
sheep@710 1326 title = unicode(db_title)
sheep@676 1327 if not external_link(title) and not title.startswith('+'):
sheep@682 1328 yield refs, title
sheep@659 1329 finally:
sheep@659 1330 con.commit()
sheep@659 1331
sheep@659 1332
sheep@30 1333 def page_backlinks(self, title):
sheep@658 1334 """Gives a list of pages linking to specified page."""
sheep@658 1335
sheep@302 1336 con = self.con # sqlite3.connect(self.filename)
sheep@384 1337 try:
sheep@466 1338 sql = ('SELECT DISTINCT(titles.title) '
sheep@463 1339 'FROM links, titles '
sheep@456 1340 'WHERE links.target=? AND titles.id=links.src '
sheep@463 1341 'ORDER BY titles.title;')
sheep@456 1342 for (backlink,) in con.execute(sql, (title,)):
sheep@712 1343 yield unicode(backlink)
sheep@384 1344 finally:
sheep@384 1345 con.commit()
sheep@30 1346
sheep@31 1347 def page_links(self, title):
sheep@658 1348 """Gives a list of links on specified page."""
sheep@658 1349
sheep@302 1350 con = self.con # sqlite3.connect(self.filename)
sheep@384 1351 try:
sheep@384 1352 title_id = self.title_id(title, con)
sheep@657 1353 sql = 'SELECT target FROM links WHERE src=? ORDER BY number;'
sheep@384 1354 for (link,) in con.execute(sql, (title_id,)):
sheep@712 1355 yield unicode(link)
sheep@384 1356 finally:
sheep@384 1357 con.commit()
sheep@276 1358
sheep@285 1359 def page_links_and_labels (self, title):
sheep@302 1360 con = self.con # sqlite3.connect(self.filename)
sheep@384 1361 try:
sheep@384 1362 title_id = self.title_id(title, con)
sheep@463 1363 sql = 'SELECT target, label FROM links WHERE src=? ORDER BY number;'
sheep@712 1364 for link, label in con.execute(sql, (title_id,)):
sheep@712 1365 yield unicode(link), unicode(label)
sheep@384 1366 finally:
sheep@384 1367 con.commit()
sheep@31 1368
sheep@26 1369 def find(self, words):
sheep@467 1370 """Iterator of all pages containing the words, and their scores."""
sheep@388 1371
sheep@464 1372 con = self.con
sheep@384 1373 try:
sheep@462 1374 ranks = []
sheep@462 1375 for word in words:
sheep@464 1376 # Calculate popularity of each word.
sheep@464 1377 sql = 'SELECT SUM(words.count) FROM words WHERE word LIKE ?;'
sheep@464 1378 rank = con.execute(sql, ('%%%s%%' % word,)).fetchone()[0]
sheep@464 1379 # If any rank is 0, there will be no results anyways
sheep@464 1380 if not rank:
sheep@464 1381 return
sheep@464 1382 ranks.append((rank, word))
sheep@462 1383 ranks.sort()
sheep@464 1384 # Start with the least popular word. Get all pages that contain it.
sheep@462 1385 first_rank, first = ranks[0]
sheep@463 1386 rest = ranks[1:]
sheep@464 1387 sql = ('SELECT words.page, titles.title, SUM(words.count) '
sheep@463 1388 'FROM words, titles '
sheep@464 1389 'WHERE word LIKE ? AND titles.id=words.page '
sheep@464 1390 'GROUP BY words.page;')
sheep@464 1391 first_counts = con.execute(sql, ('%%%s%%' % first,))
sheep@464 1392 # Check for the rest of words
sheep@464 1393 for title_id, title, first_count in first_counts:
sheep@464 1394 # Score for the first word
sheep@464 1395 score = float(first_count)/first_rank
sheep@463 1396 for rank, word in rest:
sheep@464 1397 sql = ('SELECT SUM(count) FROM words '
sheep@462 1398 'WHERE page=? AND word LIKE ?;')
sheep@464 1399 count = con.execute(sql,
sheep@464 1400 (title_id, '%%%s%%' % word)).fetchone()[0]
sheep@462 1401 if not count:
sheep@464 1402 # If page misses any of the words, its score is 0
sheep@464 1403 score = 0
sheep@462 1404 break
sheep@462 1405 score += float(count)/rank
sheep@464 1406 if score > 0:
sheep@712 1407 yield int(100*score), unicode(title)
sheep@384 1408 finally:
sheep@384 1409 con.commit()
sheep@285 1410
sheep@643 1411 def reindex_page(self, page, title, cursor, text=None):
sheep@388 1412 """Updates the content of the database, needs locks around."""
sheep@388 1413
sheep@386 1414 mime = self.storage.page_mime(title)
sheep@386 1415 if not mime.startswith('text/'):
sheep@388 1416 self.update_words(title, '', cursor=cursor)
sheep@386 1417 return
sheep@386 1418 if text is None:
sheep@394 1419 try:
sheep@394 1420 text = self.storage.page_text(title)
sheep@394 1421 except werkzeug.exceptions.NotFound:
sheep@394 1422 text = u''
sheep@685 1423 extract_links = getattr(page, 'extract_links', None)
sheep@685 1424 if extract_links is not None:
sheep@643 1425 links = extract_links(text)
sheep@388 1426 self.update_links(title, links, cursor=cursor)
sheep@388 1427 self.update_words(title, text, cursor=cursor)
sheep@386 1428
sheep@643 1429 def update_page(self, page, title, data=None, text=None):
sheep@388 1430 """Updates the index with new page content, for a single page."""
sheep@388 1431
sheep@388 1432 if text is None and data is not None:
sheep@388 1433 text = unicode(data, self.storage.charset, 'replace')
sheep@394 1434 cursor = self.con.cursor()
sheep@466 1435 cursor.execute('BEGIN IMMEDIATE TRANSACTION;')
sheep@388 1436 try:
sheep@394 1437 self.set_last_revision(self.storage.repo_revision())
sheep@643 1438 self.reindex_page(page, title, cursor, text)
sheep@466 1439 cursor.execute('COMMIT TRANSACTION;')
sheep@394 1440 except:
sheep@466 1441 cursor.execute('ROLLBACK;')
sheep@394 1442 raise
sheep@388 1443
sheep@648 1444 def reindex(self, wiki, request, pages):
sheep@388 1445 """Updates specified pages in bulk."""
sheep@388 1446
sheep@388 1447 cursor = self.con.cursor()
sheep@466 1448 cursor.execute('BEGIN IMMEDIATE TRANSACTION;')
sheep@388 1449 try:
sheep@388 1450 for title in pages:
sheep@643 1451 page = wiki.get_page(request, title)
sheep@643 1452 self.reindex_page(page, title, cursor)
sheep@466 1453 cursor.execute('COMMIT TRANSACTION;')
sheep@388 1454 self.empty = False
sheep@394 1455 except:
sheep@466 1456 cursor.execute('ROLLBACK;')
sheep@394 1457 raise
sheep@394 1458
sheep@394 1459 def set_last_revision(self, rev):
sheep@466 1460 """Store the last indexed repository revision."""
sheep@466 1461
sheep@466 1462 # We use % here because the sqlite3's substitiution doesn't work
sheep@466 1463 # We store revision 0 as 1, 1 as 2, etc. because 0 means "no revision"
sheep@466 1464 self.con.execute('PRAGMA USER_VERSION=%d;' % (int(rev+1),))
sheep@394 1465
sheep@394 1466 def get_last_revision(self):
sheep@466 1467 """Retrieve the last indexed repository revision."""
sheep@466 1468
sheep@394 1469 con = self.con
sheep@466 1470 c = con.execute('PRAGMA USER_VERSION;')
sheep@394 1471 rev = c.fetchone()[0]
sheep@466 1472 # -1 means "no revision", 1 means revision 0, 2 means revision 1, etc.
sheep@463 1473 return rev-1
sheep@26 1474
sheep@648 1475 def update(self, wiki, request):
sheep@395 1476 """Reindex al pages that changed since last indexing."""
sheep@395 1477
sheep@463 1478 last_rev = self.get_last_revision()
sheep@463 1479 if last_rev == -1:
sheep@463 1480 changed = self.storage.all_pages()
sheep@463 1481 else:
sheep@463 1482 changed = self.storage.changed_since(last_rev)
sheep@648 1483 self.reindex(wiki, request, changed)
sheep@463 1484 rev = self.storage.repo_revision()
sheep@463 1485 self.set_last_revision(rev)
sheep@395 1486
sheep@63 1487 class WikiResponse(werkzeug.BaseResponse, werkzeug.ETagResponseMixin,
sheep@63 1488 werkzeug.CommonResponseDescriptorsMixin):
sheep@271 1489 """A typical HTTP response class made out of Werkzeug's mixins."""
sheep@39 1490
sheep@490 1491 def make_conditional(self, request):
sheep@490 1492 ret = super(WikiResponse, self).make_conditional(request)
sheep@490 1493 # Remove all headers if it's 304, according to
sheep@490 1494 # http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.3.5
sheep@490 1495 if self.status.startswith('304'):
sheep@490 1496 self.response = []
sheep@529 1497 try:
sheep@529 1498 del self.content_type
sheep@529 1499 except AttributeError:
sheep@529 1500 pass
sheep@529 1501 try:
sheep@529 1502 del self.content_length
sheep@529 1503 except AttributeError:
sheep@529 1504 pass
sheep@529 1505 try:
sheep@529 1506 del self.headers['Content-length']
sheep@529 1507 except (KeyError, IndexError):
sheep@529 1508 pass
sheep@529 1509 try:
sheep@529 1510 del self.headers['Content-type']
sheep@529 1511 except (KeyError, IndexError):
sheep@529 1512 pass
sheep@490 1513 return ret
sheep@394 1514
sheep@418 1515 class WikiTempFile(object):
sheep@418 1516 """Wrap a file for uploading content."""
sheep@418 1517
sheep@418 1518 def __init__(self, tmppath):
sheep@418 1519 self.tmppath = tempfile.mkdtemp(dir=tmppath)
sheep@418 1520 self.tmpname = os.path.join(self.tmppath, 'saved')
sheep@418 1521 self.f = open(self.tmpname, "wb")
sheep@418 1522
sheep@418 1523 def read(self, *args, **kw):
sheep@418 1524 return self.f.read(*args, **kw)
sheep@418 1525
sheep@418 1526 def readlines(self, *args, **kw):
sheep@418 1527 return self.f.readlines(*args, **kw)
sheep@418 1528
sheep@418 1529 def write(self, *args, **kw):
sheep@418 1530 return self.f.write(*args, **kw)
sheep@418 1531
sheep@418 1532 def seek(self, *args, **kw):
sheep@418 1533 return self.f.seek(*args, **kw)
sheep@418 1534
sheep@418 1535 def truncate(self, *args, **kw):
sheep@418 1536 return self.f.truncate(*args, **kw)
sheep@418 1537
sheep@418 1538 def close(self, *args, **kw):
sheep@418 1539 ret = self.f.close(*args, **kw)
sheep@418 1540 try:
sheep@418 1541 os.unlink(self.tmpname)
sheep@418 1542 except OSError:
sheep@418 1543 pass
sheep@418 1544 try:
sheep@418 1545 os.rmdir(self.tmppath)
sheep@418 1546 except OSError:
sheep@418 1547 pass
sheep@418 1548 return ret
sheep@418 1549
sheep@418 1550
sheep@0 1551 class WikiRequest(werkzeug.BaseRequest, werkzeug.ETagRequestMixin):
sheep@271 1552 """
sheep@271 1553 A Werkzeug's request with additional functions for handling file
sheep@271 1554 uploads and wiki-specific link generation.
sheep@271 1555 """
sheep@381 1556
sheep@78 1557 charset = 'utf-8'
sheep@78 1558 encoding_errors = 'ignore'
sheep@381 1559
sheep@339 1560 def __init__(self, wiki, adapter, environ, **kw):
sheep@416 1561 werkzeug.BaseRequest.__init__(self, environ, shallow=False, **kw)
sheep@0 1562 self.wiki = wiki
sheep@0 1563 self.adapter = adapter
sheep@0 1564 self.tmpfiles = []
sheep@0 1565 self.tmppath = wiki.path
sheep@408 1566 # Whether to print the css for highlighting
sheep@408 1567 self.print_highlight_styles = True
sheep@0 1568
sheep@381 1569 def get_url(self, title=None, view=None, method='GET',
sheep@381 1570 external=False, **kw):
sheep@339 1571 if view is None:
sheep@339 1572 view = self.wiki.view
sheep@339 1573 if title is not None:
sheep@708 1574 kw['title'] = title.strip()
sheep@342 1575 return self.adapter.build(view, kw, method=method,
sheep@342 1576 force_external=external)
sheep@0 1577
sheep@0 1578 def get_download_url(self, title):
sheep@340 1579 return self.get_url(title, view=self.wiki.download)
sheep@0 1580
sheep@342 1581 def get_author(self):
sheep@342 1582 """Try to guess the author name. Use IP address as last resort."""
sheep@342 1583
sheep@612 1584 try:
sheep@612 1585 cookie = werkzeug.url_unquote(self.cookies.get("author", ""))
sheep@612 1586 except UnicodeError:
sheep@612 1587 cookie = None
sheep@612 1588 try:
sheep@612 1589 auth = werkzeug.url_unquote(self.environ.get('REMOTE_USER', ""))
sheep@612 1590 except UnicodeError:
sheep@612 1591 auth = None
sheep@612 1592 author = (self.form.get("author") or cookie or auth or self.remote_addr)
sheep@342 1593 return author
sheep@342 1594
sheep@416 1595 def _get_file_stream(self, total_content_length=None, content_type=None,
sheep@416 1596 filename=None, content_length=None):
sheep@342 1597 """Save all the POSTs to temporary files."""
sheep@342 1598
sheep@418 1599 temp_file = WikiTempFile(self.tmppath)
sheep@418 1600 self.tmpfiles.append(temp_file)
sheep@418 1601 return temp_file
sheep@342 1602
sheep@342 1603 def cleanup(self):
sheep@342 1604 """Clean up the temporary files created by POSTs."""
sheep@342 1605
sheep@418 1606 for temp_file in self.tmpfiles:
sheep@418 1607 temp_file.close()
sheep@418 1608 self.tmpfiles = []
sheep@342 1609
sheep@342 1610 class WikiPage(object):
sheep@342 1611 """Everything needed for rendering a page."""
sheep@342 1612
sheep@405 1613 def __init__(self, wiki, request, title, mime):
sheep@342 1614 self.request = request
sheep@342 1615 self.title = title
sheep@405 1616 self.mime = mime
sheep@406 1617 # for now we just use the globals from wiki object
sheep@342 1618 self.get_url = self.request.get_url
sheep@342 1619 self.get_download_url = self.request.get_download_url
sheep@406 1620 self.wiki = wiki
sheep@406 1621 self.storage = self.wiki.storage
sheep@406 1622 self.index = self.wiki.index
sheep@406 1623 self.config = self.wiki.config
sheep@342 1624
sheep@479 1625 def date_html(self, datetime):
sheep@479 1626 """
sheep@479 1627 Create HTML for a date, according to recommendation at
sheep@479 1628 http://microformats.org/wiki/date
sheep@479 1629 """
sheep@479 1630
sheep@479 1631 text = datetime.strftime('%Y-%m-%d %H:%M')
sheep@479 1632 # We are going for YYYY-MM-DDTHH:MM:SSZ
sheep@479 1633 title = datetime.strftime('%Y-%m-%dT%H:%M:%SZ')
sheep@479 1634 html = werkzeug.html.abbr(text, class_="date", title=title)
sheep@479 1635 return html
sheep@479 1636
sheep@434 1637
sheep@605 1638 def wiki_link(self, addr, label=None, class_=None, image=None, lineno=0):
sheep@276 1639 """Create HTML for a wiki link."""
sheep@276 1640
sheep@706 1641 addr = addr.strip()
sheep@604 1642 text = werkzeug.escape(label or addr)
sheep@604 1643 chunk = ''
sheep@605 1644 if class_ is not None:
sheep@605 1645 classes = [class_]
sheep@605 1646 else:
sheep@605 1647 classes = []
sheep@30 1648 if external_link(addr):
sheep@202 1649 if addr.startswith('mailto:'):
sheep@204 1650 class_ = 'external email'
sheep@202 1651 text = text.replace('@', '&#64;').replace('.', '&#46;')
sheep@202 1652 href = addr.replace('@', '%40').replace('.', '%2E')
sheep@202 1653 else:
sheep@605 1654 classes.append('external')
sheep@362 1655 href = werkzeug.escape(addr, quote=True)
sheep@29 1656 else:
sheep@202 1657 if '#' in addr:
sheep@202 1658 addr, chunk = addr.split('#', 1)
sheep@604 1659 chunk = '#'+chunk
sheep@604 1660 if addr.startswith('+'):
sheep@617 1661 href = '/'.join([self.request.script_root,
sheep@617 1662 '+'+werkzeug.escape(addr[1:], quote=True)])
sheep@605 1663 classes.append('special')
sheep@604 1664 elif addr == u'':
sheep@202 1665 href = chunk
sheep@605 1666 classes.append('anchor')
sheep@202 1667 else:
sheep@605 1668 classes.append('wiki')
sheep@341 1669 href = self.get_url(addr) + chunk
sheep@604 1670 if addr not in self.storage:
sheep@605 1671 classes.append('nonexistent')
sheep@605 1672 class_ = ' '.join(classes) or None
sheep@358 1673 return werkzeug.html.a(image or text, href=href, class_=class_,
sheep@604 1674 title=addr+chunk)
sheep@1 1675
sheep@431 1676 def wiki_image(self, addr, alt, class_='wiki', lineno=0):
sheep@276 1677 """Create HTML for a wiki image."""
sheep@276 1678
sheep@708 1679 addr = addr.strip()
sheep@341 1680 html = werkzeug.html
sheep@218 1681 chunk = ''
sheep@30 1682 if external_link(addr):
sheep@341 1683 return html.img(src=werkzeug.url_fix(addr), class_="external",
sheep@341 1684 alt=alt)
sheep@29 1685 if '#' in addr:
sheep@29 1686 addr, chunk = addr.split('#', 1)
sheep@218 1687 if addr == '':
sheep@341 1688 return html.a(name=chunk)
sheep@406 1689 if addr in self.storage:
sheep@406 1690 mime = self.storage.page_mime(addr)
sheep@214 1691 if mime.startswith('image/'):
sheep@341 1692 return html.img(src=self.get_download_url(addr), class_=class_,
sheep@341 1693 alt=alt)
sheep@214 1694 else:
sheep@341 1695 return html.img(href=self.get_download_url(addr), alt=alt)
sheep@1 1696 else:
sheep@341 1697 return html.a(html(alt), href=self.get_url(addr))
sheep@343 1698
sheep@343 1699 def search_form(self):
sheep@340 1700 html = werkzeug.html
sheep@340 1701 return html.form(html.div(html.input(name="q", class_="search"),
sheep@340 1702 html.input(class_="button", type_="submit", value=_(u'Search')),
sheep@340 1703 ), method="GET", class_="search",
sheep@342 1704 action=self.get_url(None, self.wiki.search))
sheep@340 1705
sheep@343 1706 def logo(self):
sheep@340 1707 html = werkzeug.html
sheep@434 1708 img = html.img(alt=u"[%s]" % self.wiki.front_page,
sheep@434 1709 src=self.get_download_url(self.wiki.logo_page))
sheep@434 1710 return html.a(img, class_='logo', href=self.get_url(self.wiki.front_page))
sheep@340 1711
sheep@343 1712 def menu(self):
sheep@606 1713 """Generate the menu items"""
sheep@606 1714
sheep@340 1715 html = werkzeug.html
sheep@434 1716 if self.wiki.menu_page in self.storage:
sheep@434 1717 items = self.index.page_links_and_labels(self.wiki.menu_page)
sheep@382 1718 else:
sheep@382 1719 items = [
sheep@434 1720 (self.wiki.front_page, self.wiki.front_page),
sheep@552 1721 ('+history', _(u'Recent changes')),
sheep@382 1722 ]
sheep@340 1723 for link, label in items:
sheep@345 1724 if link == self.title:
sheep@605 1725 class_="current"
sheep@340 1726 else:
sheep@605 1727 class_ = None
sheep@605 1728 yield self.wiki_link(link, label, class_=class_)
sheep@340 1729
sheep@343 1730 def header(self, special_title):
sheep@340 1731 html = werkzeug.html
sheep@434 1732 if self.wiki.logo_page in self.storage:
sheep@343 1733 yield self.logo()
sheep@343 1734 yield self.search_form()
sheep@382 1735 yield html.div(u" ".join(self.menu()), class_="menu")
sheep@343 1736 yield html.h1(html(special_title or self.title))
sheep@340 1737
sheep@672 1738 def html_header(self, special_title, edit_url):
sheep@672 1739 e = lambda x: werkzeug.escape(x, quote=True)
sheep@672 1740 h = werkzeug.html
sheep@672 1741 yield h.title(u'%s - %s' % (e(special_title or self.title),
sheep@672 1742 e(self.wiki.site_name)))
sheep@672 1743 yield h.link(rel="stylesheet", type_="text/css",
sheep@718 1744 href=self.get_url(None, self.wiki.pygments_css))
sheep@718 1745 yield h.link(rel="stylesheet", type_="text/css",
sheep@672 1746 href=self.get_url(None, self.wiki.style_css))
sheep@672 1747 if special_title:
sheep@672 1748 yield h.meta(name="robots", content="NOINDEX,NOFOLLOW")
sheep@672 1749 if edit_url:
sheep@672 1750 yield h.link(rel="alternate", type_="application/wiki",
sheep@672 1751 href=edit_url)
sheep@672 1752 yield h.link(rel="shortcut icon", type_="image/x-icon",
sheep@672 1753 href=self.get_url(None, self.wiki.favicon_ico))
sheep@672 1754 yield h.link(rel="alternate", type_="application/rss+xml",
sheep@672 1755 title=e("%s (RSS)" % self.wiki.site_name),
sheep@672 1756 href=self.get_url(None, self.wiki.rss))
sheep@672 1757 yield h.link(rel="alternate", type_="application/rss+xml",
sheep@672 1758 title=e("%s (ATOM)" % self.wiki.site_name),
sheep@672 1759 href=self.get_url(None, self.wiki.atom))
sheep@672 1760 yield h.script(type_="text/javascript",
sheep@672 1761 src=self.get_url(None, self.wiki.scripts_js))
sheep@672 1762
sheep@678 1763 def footer(self, special_title, edit_url):
sheep@672 1764 if special_title:
sheep@671 1765 footer_links = [
sheep@671 1766 (_(u'Changes'), 'changes',
sheep@671 1767 self.get_url(None, self.wiki.recent_changes)),
sheep@671 1768 (_(u'Index'), 'index',
sheep@671 1769 self.get_url(None, self.wiki.all_pages)),
sheep@671 1770 (_(u'Orphaned'), 'orphaned',
sheep@671 1771 self.get_url(None, self.wiki.orphaned)),
sheep@671 1772 (_(u'Wanted'), 'wanted',
sheep@671 1773 self.get_url(None, self.wiki.wanted)),
sheep@671 1774 ]
sheep@671 1775 else:
sheep@671 1776 footer_links = [
sheep@671 1777 (_(u'Edit'), 'edit', edit_url),
sheep@671 1778 (_(u'History'), 'history',
sheep@671 1779 self.get_url(self.title, self.wiki.history)),
sheep@671 1780 (_(u'Backlinks'), 'backlinks',
sheep@671 1781 self.get_url(self.title, self.wiki.backlinks))
sheep@671 1782 ]
sheep@671 1783 for label, class_, url in footer_links:
sheep@692 1784 if url:
sheep@692 1785 yield werkzeug.html.a(werkzeug.html(label), href=url,
sheep@692 1786 class_=class_)
sheep@692 1787 yield u'\n'
sheep@668 1788
sheep@678 1789 def render_content(self, content, special_title=None):
sheep@668 1790 """The main page template."""
sheep@668 1791
sheep@668 1792 edit_url = None
sheep@668 1793 if not special_title:
sheep@668 1794 try:
sheep@669 1795 self.wiki._check_lock(self.title)
sheep@668 1796 edit_url = self.get_url(self.title, self.wiki.edit)
sheep@668 1797 except werkzeug.exceptions.Forbidden:
sheep@668 1798 pass
sheep@668 1799
sheep@672 1800 yield u"""\
sheep@672 1801 <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
sheep@672 1802 "http://www.w3.org/TR/html4/strict.dtd">
sheep@672 1803 <html><head>\n"""
sheep@672 1804 for part in self.html_header(special_title, edit_url):
sheep@672 1805 yield part
sheep@672 1806 yield u'\n</head><body><div class="header">\n'
sheep@672 1807 for part in self.header(special_title):
sheep@672 1808 yield part
sheep@672 1809 yield u'\n</div><div class="content">\n'
sheep@668 1810 for part in content:
sheep@668 1811 yield part
sheep@678 1812 if not special_title or not self.title:
sheep@678 1813 yield u'\n<div class="footer">\n'
sheep@678 1814 for part in self.footer(special_title, edit_url):
sheep@678 1815 yield part
sheep@678 1816 yield u'</div>'
sheep@678 1817 yield u'</div></body></html>'
sheep@668 1818
sheep@668 1819
sheep@668 1820 def pages_list(self, pages, message=None, link=None, _class=None):
sheep@668 1821 """Generate the content of a page list page."""
sheep@668 1822
sheep@668 1823 yield werkzeug.html.p(werkzeug.escape(message) % {'link': link})
sheep@668 1824 yield u'<ul class="%s">' % werkzeug.escape(_class or 'pagelist')
sheep@668 1825 for title in pages:
sheep@668 1826 yield werkzeug.html.li(self.wiki_link(title))
sheep@668 1827 yield u'</ul>'
sheep@668 1828
sheep@668 1829 def history_list(self):
sheep@668 1830 """Generate the content of the history page."""
sheep@668 1831
sheep@686 1832 h = werkzeug.html
sheep@668 1833 max_rev = -1;
sheep@668 1834 title = self.title
sheep@668 1835 link = self.wiki_link(title)
sheep@686 1836 yield h.p(h(_(u'History of changes for %(link)s.')) % {'link': link})
sheep@668 1837 url = self.request.get_url(title, self.wiki.undo, method='POST')
sheep@668 1838 yield u'<form action="%s" method="POST"><ul class="history">' % url
sheep@668 1839 try:
sheep@669 1840 self.wiki._check_lock(title)
sheep@668 1841 read_only = False
sheep@668 1842 except werkzeug.exceptions.Forbidden:
sheep@668 1843 read_only = True
sheep@668 1844 for rev, date, author, comment in self.wiki.storage.page_history(title):
sheep@668 1845 if max_rev < rev:
sheep@668 1846 max_rev = rev
sheep@668 1847 if rev > 0:
sheep@668 1848 date_url = self.request.adapter.build(self.wiki.diff, {
sheep@668 1849 'title': title, 'from_rev': rev-1, 'to_rev': rev})
sheep@668 1850 else:
sheep@668 1851 date_url = self.request.adapter.build(self.wiki.revision, {
sheep@668 1852 'title': title, 'rev': rev})
sheep@673 1853 if read_only:
sheep@673 1854 button = u''
sheep@673 1855 else:
sheep@686 1856 button = h.input(type_="submit", name=str(rev),
sheep@688 1857 value=h(_(u'Undo')))
sheep@686 1858 yield h.li(h.a(self.date_html(date), href=date_url),
sheep@686 1859 button, ' . . . . ', h.i(self.wiki_link(author)),
sheep@686 1860 h.div(h(comment), class_="comment"))
sheep@686 1861 yield u'</ul>'
sheep@686 1862 yield h.input(type_="hidden", name="parent", value=max_rev)
sheep@686 1863 yield u'</form>'
sheep@668 1864
sheep@716 1865
sheep@668 1866 def dependencies(self):
sheep@668 1867 """Refresh the page when any of those pages was changed."""
sheep@668 1868
sheep@668 1869 dependencies = set()
sheep@716 1870 for title in [self.wiki.logo_page, self.wiki.menu_page]:
sheep@716 1871 if title not in self.storage:
sheep@716 1872 dependencies.add(werkzeug.url_quote(title))
sheep@716 1873 for title in [self.wiki.menu_page]:
sheep@716 1874 if title in self.storage:
sheep@716 1875 inode, size, mtime = self.storage.page_file_meta(title)
sheep@716 1876 etag = '%s/%d-%d' % (werkzeug.url_quote(title), inode, mtime)
sheep@716 1877 dependencies.add(etag)
sheep@668 1878 return dependencies
sheep@668 1879
sheep@668 1880 class WikiPageText(WikiPage):
sheep@668 1881 """Pages of mime type text/* use this for display."""
sheep@668 1882
sheep@703 1883 def content_iter(self, lines):
sheep@703 1884 yield '<pre>'
sheep@703 1885 for line in lines:
sheep@703 1886 yield werkzeug.html(line)
sheep@703 1887 yield '</pre>'
sheep@703 1888
sheep@668 1889 def view_content(self, lines=None):
sheep@703 1890 """Read the page content from storage or preview and return iterator."""
sheep@668 1891
sheep@668 1892 if lines is None:
sheep@703 1893 f = self.storage.open_page(self.title)
sheep@703 1894 lines = self.storage.page_lines(f)
sheep@703 1895 return self.content_iter(lines)
sheep@668 1896
sheep@668 1897 def editor_form(self, preview=None):
sheep@668 1898 """Generate the HTML for the editor."""
sheep@668 1899
sheep@668 1900 author = self.request.get_author()
sheep@668 1901 lines = []
sheep@668 1902 try:
sheep@668 1903 page_file = self.storage.open_page(self.title)
sheep@668 1904 lines = self.storage.page_lines(page_file)
sheep@668 1905 (rev, old_date, old_author,
sheep@668 1906 old_comment) = self.storage.page_meta(self.title)
sheep@668 1907 comment = _(u'modified')
sheep@668 1908 if old_author == author:
sheep@668 1909 comment = old_comment
sheep@668 1910 except werkzeug.exceptions.NotFound:
sheep@668 1911 comment = _(u'created')
sheep@668 1912 rev = -1
sheep@715 1913 except werkzeug.exceptions.Forbidden:
sheep@715 1914 yield werkzeug.html.p(werkzeug.html(_(u"Can't edit a symbolic link.")))
sheep@715 1915 return
sheep@668 1916 if preview:
sheep@668 1917 lines = preview
sheep@668 1918 comment = self.request.form.get('comment', comment)
sheep@668 1919 html = werkzeug.html
sheep@668 1920 yield u'<form action="" method="POST" class="editor"><div>'
sheep@668 1921 yield u'<textarea name="text" cols="80" rows="20" id="editortext">'
sheep@668 1922 for line in lines:
sheep@668 1923 yield werkzeug.escape(line)
sheep@668 1924 yield u"""</textarea>"""
sheep@668 1925 yield html.input(type_="hidden", name="parent", value=rev)
sheep@668 1926 yield html.label(html(_(u'Comment')), html.input(name="comment",
sheep@668 1927 value=comment), class_="comment")
sheep@668 1928 yield html.label(html(_(u'Author')), html.input(name="author",
sheep@668 1929 value=self.request.get_author()), class_="comment")
sheep@668 1930 yield html.div(
sheep@668 1931 html.input(type_="submit", name="save", value=_(u'Save')),
sheep@668 1932 html.input(type_="submit", name="preview", value=_(u'Preview')),
sheep@668 1933 html.input(type_="submit", name="cancel", value=_(u'Cancel')),
sheep@668 1934 class_="buttons")
sheep@668 1935 yield u'</div></form>'
sheep@668 1936 if preview:
sheep@668 1937 yield html.h1(html(_(u'Preview, not saved')), class_="preview")
sheep@668 1938 for part in self.view_content(preview):
sheep@668 1939 yield part
sheep@668 1940
sheep@717 1941 def diff_content(self, from_text, to_text, message=u''):
sheep@668 1942 """Generate the HTML markup for a diff."""
sheep@668 1943
sheep@668 1944 def infiniter(iterator):
sheep@668 1945 """Turn an iterator into an infinite one, padding it with None"""
sheep@668 1946
sheep@668 1947 for i in iterator:
sheep@668 1948 yield i
sheep@668 1949 while True:
sheep@668 1950 yield None
sheep@668 1951
sheep@717 1952 diff = difflib._mdiff(from_text.split('\n'), to_text.split('\n'))
sheep@668 1953 stack = []
sheep@668 1954 mark_re = re.compile('\0[-+^]([^\1\0]*)\1|([^\0\1])')
sheep@668 1955 yield message
sheep@668 1956 yield u'<pre class="diff">'
sheep@668 1957 for old_line, new_line, changed in diff:
sheep@668 1958 old_no, old_text = old_line
sheep@668 1959 new_no, new_text = new_line
sheep@668 1960 line_no = (new_no or old_no or 1)-1
sheep@668 1961 if changed:
sheep@668 1962 yield u'<div class="change" id="line_%d">' % line_no
sheep@668 1963 old_iter = infiniter(mark_re.finditer(old_text))
sheep@668 1964 new_iter = infiniter(mark_re.finditer(new_text))
sheep@668 1965 old = old_iter.next()
sheep@668 1966 new = new_iter.next()
sheep@668 1967 buff = u''
sheep@668 1968 while old or new:
sheep@668 1969 while old and old.group(1):
sheep@668 1970 if buff:
sheep@668 1971 yield werkzeug.escape(buff)
sheep@668 1972 buff = u''
sheep@668 1973 yield u'<del>%s</del>' % werkzeug.escape(old.group(1))
sheep@668 1974 old = old_iter.next()
sheep@668 1975 while new and new.group(1):
sheep@668 1976 if buff:
sheep@668 1977 yield werkzeug.escape(buff)
sheep@668 1978 buff = u''
sheep@668 1979 yield u'<ins>%s</ins>' % werkzeug.escape(new.group(1))
sheep@668 1980 new = new_iter.next()
sheep@668 1981 if new:
sheep@668 1982 buff += new.group(2)
sheep@668 1983 old = old_iter.next()
sheep@668 1984 new = new_iter.next()
sheep@668 1985 if buff:
sheep@668 1986 yield werkzeug.escape(buff)
sheep@668 1987 yield u'</div>'
sheep@668 1988 else:
sheep@668 1989 yield u'<div class="orig" id="line_%d">%s</div>' % (
sheep@668 1990 line_no, werkzeug.escape(old_text))
sheep@668 1991 yield u'</pre>'
sheep@668 1992
sheep@703 1993 class WikiPageColorText(WikiPageText):
sheep@703 1994 """Text pages, but displayed colorized with pygments"""
sheep@703 1995
sheep@703 1996 def view_content(self, lines=None):
sheep@703 1997 """Generate HTML for the content."""
sheep@703 1998
sheep@703 1999 if lines is None:
sheep@703 2000 text = self.storage.page_text(self.title)
sheep@703 2001 else:
sheep@703 2002 text = ''.join(lines)
sheep@703 2003 return self.highlight(text, mime=self.mime)
sheep@703 2004
sheep@703 2005 def highlight(self, text, mime=None, syntax=None, line_no=0):
sheep@703 2006 """Colorize the source code."""
sheep@703 2007
sheep@703 2008 if pygments is None:
sheep@703 2009 yield werkzeug.html.pre(werkzeug.html(text))
sheep@703 2010 return
sheep@703 2011
sheep@718 2012 formatter = pygments.formatters.HtmlFormatter()
sheep@703 2013 formatter.line_no = line_no
sheep@703 2014
sheep@703 2015 def wrapper(source, outfile):
sheep@703 2016 """Wrap each line of formatted output."""
sheep@703 2017
sheep@703 2018 yield 0, '<div class="highlight"><pre>'
sheep@703 2019 for lineno, line in source:
devel@724 2020 yield (lineno,
devel@724 2021 werkzeug.html.span(line, id_="line_%d" %
devel@724 2022 formatter.line_no))
sheep@703 2023 formatter.line_no += 1
sheep@703 2024 yield 0, '</pre></div>'
sheep@703 2025
sheep@703 2026 formatter.wrap = wrapper
sheep@703 2027 try:
sheep@703 2028 if mime:
sheep@703 2029 lexer = pygments.lexers.get_lexer_for_mimetype(mime)
sheep@703 2030 elif syntax:
sheep@703 2031 lexer = pygments.lexers.get_lexer_by_name(syntax)
sheep@703 2032 else:
sheep@703 2033 lexer = pygments.lexers.guess_lexer(text)
sheep@703 2034 except pygments.util.ClassNotFound:
sheep@703 2035 yield werkzeug.html.pre(werkzeug.html(text))
sheep@703 2036 return
sheep@703 2037 html = pygments.highlight(text, lexer, formatter)
sheep@703 2038 yield html
sheep@703 2039
sheep@703 2040 class WikiPageWiki(WikiPageColorText):
sheep@668 2041 """Pages of with wiki markup use this for display."""
sheep@668 2042
sheep@668 2043 def __init__(self, *args, **kw):
sheep@668 2044 super(WikiPageWiki, self).__init__(*args, **kw)
sheep@668 2045 if self.config.get_bool('wiki_words', False):
sheep@668 2046 self.parser = WikiWikiParser
sheep@668 2047 else:
sheep@668 2048 self.parser = WikiParser
sheep@668 2049 if self.config.get_bool('ignore_indent', False):
sheep@668 2050 try:
sheep@668 2051 del self.parser.block['indent']
sheep@668 2052 except KeyError:
sheep@668 2053 pass
sheep@668 2054
sheep@668 2055 def extract_links(self, text=None):
sheep@668 2056 """Extract all links from the page."""
sheep@668 2057
sheep@668 2058 if text is None:
sheep@668 2059 try:
sheep@668 2060 text = self.storage.page_text(self.title)
sheep@668 2061 except werkzeug.exceptions.NotFound:
sheep@668 2062 text = u''
sheep@668 2063 return self.parser.extract_links(text)
sheep@668 2064
sheep@668 2065 def view_content(self, lines=None):
sheep@668 2066 if lines is None:
sheep@668 2067 f = self.storage.open_page(self.title)
sheep@668 2068 lines = self.storage.page_lines(f)
sheep@668 2069 if self.wiki.icon_page and self.wiki.icon_page in self.storage:
sheep@668 2070 icons = self.index.page_links_and_labels(self.wiki.icon_page)
sheep@668 2071 smilies = dict((emo, link) for (link, emo) in icons)
sheep@668 2072 else:
sheep@668 2073 smilies = None
sheep@668 2074 content = self.parser(lines, self.wiki_link, self.wiki_image,
sheep@668 2075 self.highlight, self.wiki_math, smilies)
sheep@668 2076 return content
sheep@668 2077
sheep@668 2078 def wiki_math(self, math):
sheep@668 2079 math_url = self.config.get('math_url',
sheep@668 2080 'http://www.mathtran.org/cgi-bin/mathtran?tex=')
sheep@668 2081 if '%s' in math_url:
sheep@668 2082 url = math_url % werkzeug.url_quote(math)
sheep@668 2083 else:
sheep@668 2084 url = '%s%s' % (math_url, werkzeug.url_quote(math))
sheep@668 2085 label = werkzeug.escape(math, quote=True)
sheep@668 2086 return werkzeug.html.img(src=url, alt=label, class_="math")
sheep@668 2087
sheep@668 2088 def dependencies(self):
sheep@668 2089 dependencies = WikiPage.dependencies(self)
sheep@716 2090 for title in [self.wiki.icon_page]:
sheep@716 2091 if title in self.storage:
sheep@716 2092 inode, size, mtime = self.storage.page_file_meta(title)
sheep@716 2093 etag = '%s/%d-%d' % (werkzeug.url_quote(title), inode, mtime)
sheep@716 2094 dependencies.add(etag)
sheep@668 2095 for link in self.index.page_links(self.title):
sheep@668 2096 if link not in self.storage:
sheep@668 2097 dependencies.add(werkzeug.url_quote(link))
sheep@668 2098 return dependencies
sheep@668 2099
sheep@668 2100 class WikiPageFile(WikiPage):
sheep@668 2101 """Pages of all other mime types use this for display."""
sheep@668 2102
sheep@668 2103 def view_content(self, lines=None):
sheep@668 2104 if self.title not in self.storage:
sheep@668 2105 raise werkzeug.exceptions.NotFound()
sheep@668 2106 content = ['<p>Download <a href="%s">%s</a> as <i>%s</i>.</p>' %
sheep@668 2107 (self.request.get_download_url(self.title),
sheep@668 2108 werkzeug.escape(self.title), self.mime)]
sheep@668 2109 return content
sheep@668 2110
sheep@668 2111 def editor_form(self, preview=None):
sheep@668 2112 author = self.request.get_author()
sheep@668 2113 if self.title in self.storage:
sheep@668 2114 comment = _(u'changed')
sheep@668 2115 (rev, old_date, old_author,
sheep@668 2116 old_comment) = self.storage.page_meta(self.title)
sheep@668 2117 if old_author == author:
sheep@668 2118 comment = old_comment
sheep@668 2119 else:
sheep@668 2120 comment = _(u'uploaded')
sheep@668 2121 rev = -1
sheep@668 2122 html = werkzeug.html
sheep@702 2123 yield html.p(html(
sheep@668 2124 _(u"This is a binary file, it can't be edited on a wiki. "
sheep@668 2125 u"Please upload a new version instead.")))
sheep@668 2126 yield html.form(html.div(
sheep@668 2127 html.div(html.input(type_="file", name="data"), class_="upload"),
sheep@668 2128 html.input(type_="hidden", name="parent", value=rev),
sheep@668 2129 html.label(html(_(u'Comment')), html.input(name="comment",
sheep@668 2130 value=comment)),
sheep@668 2131 html.label(html(_(u'Author')), html.input(name="author",
sheep@668 2132 value=author)),
sheep@668 2133 html.div(html.input(type_="submit", name="save", value=_(u'Save')),
sheep@668 2134 html.input(type_="submit", name="cancel",
sheep@668 2135 value=_(u'Cancel')),
sheep@668 2136 class_="buttons")), action="", method="POST", class_="editor",
sheep@668 2137 enctype="multipart/form-data")
sheep@668 2138
sheep@668 2139 class WikiPageImage(WikiPageFile):
sheep@668 2140 """Pages of mime type image/* use this for display."""
sheep@668 2141
sheep@668 2142 render_file = '128x128.png'
sheep@668 2143
sheep@668 2144 def view_content(self, lines=None):
sheep@668 2145 if self.title not in self.storage:
sheep@668 2146 raise werkzeug.exceptions.NotFound()
sheep@668 2147 content = ['<img src="%s" alt="%s">'
sheep@668 2148 % (self.request.get_url(self.title, self.wiki.render),
sheep@668 2149 werkzeug.escape(self.title))]
sheep@668 2150 return content
sheep@668 2151
sheep@668 2152 def render_mime(self):
sheep@668 2153 """Give the filename and mime type of the rendered thumbnail."""
sheep@668 2154
sheep@668 2155 if not Image:
sheep@668 2156 raise NotImplementedError('No Image library available')
sheep@668 2157 return self.render_file, 'image/png'
sheep@668 2158
sheep@668 2159 def render_cache(self, cache_dir):
sheep@668 2160 """Render the thumbnail and save in the cache."""
sheep@668 2161
sheep@668 2162 if not Image:
sheep@668 2163 raise NotImplementedError('No Image library available')
sheep@668 2164 page_file = self.storage.open_page(self.title)
sheep@668 2165 cache_path = os.path.join(cache_dir, self.render_file)
sheep@668 2166 cache_file = open(cache_path, 'wb')
sheep@668 2167 try:
sheep@668 2168 im = Image.open(page_file)
sheep@668 2169 im = im.convert('RGBA')
sheep@668 2170 im.thumbnail((128, 128), Image.ANTIALIAS)
sheep@668 2171 im.save(cache_file,'PNG')
sheep@668 2172 except IOError:
sheep@668 2173 raise werkzeug.exceptions.UnsupportedMediaType('Image corrupted')
sheep@668 2174 cache_file.close()
sheep@668 2175 return cache_path
sheep@668 2176
sheep@668 2177 class WikiPageCSV(WikiPageFile):
sheep@668 2178 """Display class for type text/csv."""
sheep@668 2179
sheep@703 2180 def content_iter(self, lines=None):
sheep@668 2181 import csv
sheep@714 2182 # XXX Add preview support
sheep@668 2183 csv_file = self.storage.open_page(self.title)
sheep@668 2184 reader = csv.reader(csv_file)
sheep@668 2185 html_title = werkzeug.escape(self.title, quote=True)
sheep@668 2186 yield u'<table id="%s" class="csvfile">' % html_title
sheep@668 2187 try:
sheep@668 2188 for row in reader:
sheep@668 2189 yield u'<tr>%s</tr>' % (u''.join(u'<td>%s</td>' % cell
sheep@668 2190 for cell in row))
sheep@668 2191 except csv.Error, e:
sheep@668 2192 yield u'</table>'
sheep@668 2193 yield _(u'<p class="error">Error parsing CSV file %s on line %d: %s'
sheep@668 2194 % (html_title, reader.line_num, e))
sheep@668 2195 finally:
sheep@668 2196 csv_file.close()
sheep@668 2197 yield u'</table>'
sheep@668 2198
sheep@703 2199 def view_content(self, lines=None):
sheep@703 2200 if self.title not in self.storage:
sheep@703 2201 raise werkzeug.exceptions.NotFound()
sheep@703 2202 return self.content_iter(lines)
sheep@703 2203
sheep@713 2204 class WikiPageRST(WikiPageText):
sheep@713 2205 """
sheep@713 2206 Display ReStructured Text.
sheep@713 2207 """
sheep@713 2208
sheep@713 2209 def content_iter(self, lines):
sheep@713 2210 try:
sheep@713 2211 from docutils.core import publish_parts
sheep@713 2212 except ImportError:
sheep@713 2213 return super(WikiPageRST, self).content_iter(lines)
sheep@713 2214 text = ''.join(lines)
sheep@713 2215 SAFE_DOCUTILS = dict(file_insertion_enabled=False, raw_enabled=False)
sheep@713 2216 content = publish_parts(text, writer_name='html',
sheep@713 2217 settings_overrides=SAFE_DOCUTILS)['html_body']
sheep@713 2218 return [content]
sheep@713 2219
sheep@713 2220
sheep@694 2221 class WikiPageBugs(WikiPageText):
sheep@703 2222 """
sheep@703 2223 Display class for type text/x-bugs
sheep@703 2224 Parse the ISSUES file in (roughly) format used by ciss
sheep@703 2225 """
sheep@703 2226
sheep@703 2227 def content_iter(self, lines):
sheep@696 2228 last_lines = []
sheep@694 2229 in_header = False
sheep@697 2230 in_bug = False
sheep@694 2231 attributes = {}
sheep@697 2232 title = None
sheep@700 2233 for line_no, line in enumerate(lines):
sheep@696 2234 if last_lines and line.startswith('----'):
sheep@697 2235 title = ''.join(last_lines)
sheep@696 2236 last_lines = []
sheep@694 2237 in_header = True
sheep@694 2238 attributes = {}
sheep@694 2239 elif in_header and ':' in line:
sheep@694 2240 attribute, value = line.split(':', 1)
sheep@694 2241 attributes[attribute.strip()] = value.strip()
sheep@694 2242 else:
sheep@697 2243 if in_header:
sheep@697 2244 if in_bug:
sheep@697 2245 yield '</div>'
sheep@697 2246 tags = [tag.strip() for tag in
sheep@700 2247 attributes.get('tags', '').split()
sheep@697 2248 if tag.strip()]
sheep@700 2249 yield '<div id="line_%d">' % (line_no)
sheep@697 2250 in_bug = True
sheep@697 2251 if title:
sheep@697 2252 yield werkzeug.html.h2(werkzeug.html(title))
sheep@697 2253 if attributes:
sheep@697 2254 yield '<dl>'
sheep@697 2255 for attribute, value in attributes.iteritems():
sheep@697 2256 yield werkzeug.html.dt(werkzeug.html(attribute))
sheep@697 2257 yield werkzeug.html.dd(werkzeug.html(value))
sheep@697 2258 yield '</dl>'
sheep@697 2259 in_header = False
sheep@694 2260 if not line.strip():
sheep@696 2261 if last_lines:
sheep@699 2262 if last_lines[0][0] in ' \t':
sheep@698 2263 yield werkzeug.html.pre(werkzeug.html(
sheep@698 2264 ''.join(last_lines)))
sheep@698 2265 else:
sheep@698 2266 yield werkzeug.html.p(werkzeug.html(
sheep@698 2267 ''.join(last_lines)))
sheep@696 2268 last_lines = []
sheep@694 2269 else:
sheep@696 2270 last_lines.append(line)
sheep@696 2271 if last_lines:
sheep@699 2272 if last_lines[0][0] in ' \t':
sheep@698 2273 yield werkzeug.html.pre(werkzeug.html(
sheep@698 2274 ''.join(last_lines)))
sheep@698 2275 else:
sheep@698 2276 yield werkzeug.html.p(werkzeug.html(
sheep@698 2277 ''.join(last_lines)))
sheep@697 2278 if in_bug:
sheep@697 2279 yield '</div>'
sheep@694 2280
sheep@694 2281
sheep@668 2282 class WikiTitleConverter(werkzeug.routing.PathConverter):
sheep@668 2283 """Behaves like the path converter, except that it escapes slashes."""
sheep@668 2284
sheep@668 2285 def to_url(self, value):
sheep@706 2286 return werkzeug.url_quote(value.strip(), self.map.charset, safe="")
sheep@706 2287
sheep@679 2288 regex='([^+%]|%[^2]|%2[^Bb]).*'
sheep@679 2289
sheep@668 2290 class WikiAllConverter(werkzeug.routing.BaseConverter):
sheep@668 2291 """Matches everything."""
sheep@668 2292
sheep@668 2293 regex='.*'
sheep@668 2294
sheep@668 2295
sheep@668 2296 class Wiki(object):
sheep@668 2297 """
sheep@668 2298 The main class of the wiki, handling initialization of the whole
sheep@668 2299 application and most of the logic.
sheep@668 2300 """
sheep@668 2301 storage_class = WikiStorage
sheep@668 2302 index_class = WikiSearch
sheep@693 2303 filename_map = {
sheep@693 2304 'README': (WikiPageText, 'text/plain'),
sheep@694 2305 'ISSUES': (WikiPageBugs, 'text/x-bugs'),
sheep@694 2306 'ISSUES.txt': (WikiPageBugs, 'text/x-bugs'),
sheep@693 2307 'COPYING': (WikiPageText, 'text/plain'),
sheep@693 2308 'CHANGES': (WikiPageText, 'text/plain'),
sheep@693 2309 'MANIFEST': (WikiPageText, 'text/plain'),
sheep@693 2310 'favicon.ico': (WikiPageImage, 'image/x-icon'),
sheep@693 2311 }
sheep@668 2312 mime_map = {
sheep@721 2313 'text': WikiPageColorText,
sheep@722 2314 'application/x-javascript': WikiPageColorText,
sheep@722 2315 'application/x-python': WikiPageColorText,
sheep@668 2316 'text/csv': WikiPageCSV,
sheep@713 2317 'text/x-rst': WikiPageRST,
sheep@668 2318 'text/x-wiki': WikiPageWiki,
sheep@668 2319 'image': WikiPageImage,
sheep@668 2320 '': WikiPageFile,
sheep@668 2321 }
sheep@668 2322 icon = base64.b64decode(
sheep@668 2323 'iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAAABHNCSVQICAgIfAhki'
sheep@668 2324 'AAAAAlwSFlzAAAEnQAABJ0BfDRroQAAABl0RVh0U29mdHdhcmUAd3d3Lmlua3NjYXBlLm9yZ5vuPBo'
sheep@668 2325 'AAALWSURBVDiNbdNLaFxlFMDx//fd19x5JdNJm0lIImPaYm2MfSUggrssXBVaChUfi1JwpQtxK7gqu'
sheep@668 2326 'LMbQQQ3bipU0G3Rgg98DBpraWob00kzM6Z5TF7tdObm3vvd46K0TBo/OLtzfnychxIRut+Zo2/19vT'
sheep@668 2327 'kLxXze6biONbGJMRipL39MJyt33rvp+rVT7rzVTfw2vFzLxwcLf/V7oSq1W4hACIkIigUtnaoNecXG'
sheep@668 2328 '2u14T8blQRAd2v7yyN/RLFR6IRM1iedSeFnUvhpDydlI9ow0lcedG3348c1djeQz+WcThjgYZMgGBG'
sheep@668 2329 'SJMEYgzGGODLEoTBYGH4DeHcXoDSSzaRVogQjyaMwhtgYcoUco+Nl5qbnubFw7fr//uB2tXp78uj4c'
sheep@668 2330 '0YJsSTESUxsDCemjjH6YhnbtbA8xaVv7n/0uGZHDx48aH8+17iLJQrf9vCdFL7tkcn7/Pb7r8zdmWP'
sheep@668 2331 '2zqwopa7sAl4/cV4NlvrPbgch7aBN1vUIOw9ZWmmw2dqkb18fQSegOrOgfD9zahfQ37/3su+ljj1T6'
sheep@668 2332 'uCnAyxtoZVGa41tWSilULWfCZdaPD986MsjQxOHdwC9PdmT2tLk0oozpxfYf2SZwp4Iz1X4UZWBe1+'
sheep@668 2333 'z9+5X+OkiruWpYr744ZMmvjn5dvrwoVHLdRzWtobY2Kwx9soyz5ZXuV9fQ5pXCBabXKuXcBwbYwxYe'
sheep@668 2334 'kIppTXAF5VP2xutrVYmm8bzM1z9foSZik1z1SWMNLW1AtMrB/gnnMJxbSxbUV2a/QHQT8Y4c+vvC8V'
sheep@668 2335 'C74VCoZcodvnxux5Msg+THCSKHy2R48YgIb/crITrreZlEYl33MKrYycvvnx88p2BUkkpRyGSEBmDi'
sheep@668 2336 'WI6QcC95UUqM9PBzdqN99fbzc9EJNwBKKUoFw+8NDY8/sFQ/8CE57l5pZRdX6kHqxurW43mv98urM9'
sheep@668 2337 'fjJPouohE8NQ1dkEayAJ5wAe2gRawJSKmO/c/aERMn5m9/ksAAAAASUVORK5CYII=')
sheep@695 2338 scripts = r"""function hatta_dates(){var a=document.getElementsByTagName(
sheep@695 2339 'abbr');var p=function(i){return('00'+i).slice(-2)};for(var i=0;i<a.length;++i)
sheep@695 2340 {var n=a[i];if(n.className==='date'){var m=
sheep@695 2341 /^([0-9]{4})-([0-9]{2})-([0-9]{2})T([0-9]{2}):([0-9]{2}):([0-9]{2})Z$/.exec(
sheep@695 2342 n.getAttribute('title'));var d=new Date(Date.UTC(+m[1],+m[2]-1,+m[3],+m[4],
sheep@695 2343 +m[5],+m[6]));if(d){var b=-d.getTimezoneOffset()/60;if(b>=0){b="+"+b}
sheep@695 2344 n.textContent=""+d.getFullYear()+"-"+p(d.getMonth()+1)+"-"+p(d.getDate())+" "+
sheep@695 2345 p(d.getHours())+":"+p(d.getMinutes())+" GMT"+b}}}}function hatta_edit(){var b=
sheep@695 2346 document.getElementById('editortext');if(b){var c=0+
sheep@695 2347 document.location.hash.substring(1);var d=b.textContent.match(/(.*\n)/g);var
sheep@695 2348 f='';for(var i=0;i<d.length&&i<c;++i){f+=d[i]}b.focus();if(b.setSelectionRange)
sheep@695 2349 {b.setSelectionRange(f.length,f.length)}else if(b.createTextRange){var g=
sheep@695 2350 b.createTextRange();g.collapse(true);g.moveEnd('character',f.length);
sheep@695 2351 g.moveStart('character',f.length);g.select()}var h=document.createElement('pre'
sheep@695 2352 );b.parentNode.appendChild(h);var k=window.getComputedStyle(b,'');h.style.font=
sheep@695 2353 k.font;h.style.border=k.border;h.style.outline=k.outline;h.style.lineHeight=
sheep@695 2354 k.lineHeight;h.style.letterSpacing=k.letterSpacing;h.style.fontFamily=
sheep@695 2355 k.fontFamily;h.style.fontSize=k.fontSize;h.style.padding=0;h.style.overflow=
sheep@695 2356 'scroll';try{h.style.whiteSpace="-moz-pre-wrap"}catch(e){};try{
sheep@695 2357 h.style.whiteSpace="-o-pre-wrap"}catch(e){};try{h.style.whiteSpace="-pre-wrap"
sheep@695 2358 }catch(e){};try{h.style.whiteSpace="pre-wrap"}catch(e){};h.textContent=f;
sheep@695 2359 b.scrollTop=h.scrollHeight;h.parentNode.removeChild(h)}else{var l='';var m=
sheep@695 2360 document.getElementsByTagName('link');for(var i=0;i<m.length;++i){var n=m[i];
sheep@695 2361 if(n.getAttribute('type')==='application/wiki'){l=n.getAttribute('href')}}if(
devel@724 2362 l===''){return}var o=['p','h1','h2','h3','h4','h5','h6','pre','ul','div',
devel@724 2363 'span'];for(var j=0;j<o.length;++j){var m=document.getElementsByTagName(o[j]);
devel@724 2364 for(var i=0;i<m.length;++i){var n=m[i];if(n.id&&n.id.match(/^line_\d+$/)){
devel@724 2365 n.ondblclick=function(){var a=l+'#'+this.id.replace('line_','');
devel@724 2366 document.location.href=a}}}}}}
devel@724 2367 window.onload=function(){hatta_dates();hatta_edit()}"""
sheep@668 2368 style = """\
sheep@592 2369 html { background: #fff; color: #2e3436;
sheep@592 2370 font-family: sans-serif; font-size: 96% }
sheep@592 2371 body { margin: 1em auto; line-height: 1.3; width: 40em }
sheep@592 2372 a { color: #3465a4; text-decoration: none }
sheep@592 2373 a:hover { text-decoration: underline }
sheep@592 2374 a.wiki:visited { color: #204a87 }
sheep@642 2375 a.nonexistent, a.nonexistent:visited { color: #a40000; }
sheep@592 2376 a.external { color: #3465a4; text-decoration: underline }
sheep@592 2377 a.external:visited { color: #75507b }
sheep@592 2378 a img { border: none }
sheep@592 2379 img.math, img.smiley { vertical-align: middle }
sheep@592 2380 pre { font-size: 100%; white-space: pre-wrap; word-wrap: break-word;
sheep@592 2381 white-space: -moz-pre-wrap; white-space: -pre-wrap;
sheep@592 2382 white-space: -o-pre-wrap; line-height: 1.2; color: #555753 }
sheep@592 2383 div.conflict pre.local { background: #fcaf3e; margin-bottom: 0; color: 000}
sheep@592 2384 div.conflict pre.other { background: #ffdd66; margin-top: 0; color: 000; border-top: #d80 dashed 1px; }
sheep@592 2385 pre.diff div.orig { font-size: 75%; color: #babdb6 }
sheep@592 2386 b.highlight, pre.diff ins { font-weight: bold; background: #fcaf3e;
sheep@592 2387 color: #ce5c00; text-decoration: none }
sheep@592 2388 pre.diff del { background: #eeeeec; color: #888a85; text-decoration: none }
sheep@592 2389 pre.diff div.change { border-left: 2px solid #fcaf3e }
sheep@592 2390 div.footer { border-top: solid 1px #babdb6; text-align: right }
sheep@592 2391 h1, h2, h3, h4 { color: #babdb6; font-weight: normal; letter-spacing: 0.125em}
sheep@592 2392 div.buttons { text-align: center }
sheep@592 2393 input.button, div.buttons input { font-weight: bold; font-size: 100%;
sheep@592 2394 background: #eee; border: solid 1px #babdb6; margin: 0.25em; color: #888a85}
sheep@592 2395 .history input.button { font-size: 75% }
sheep@592 2396 .editor textarea { width: 100%; display: block; font-size: 100%;
sheep@592 2397 border: solid 1px #babdb6; }
sheep@592 2398 .editor label { display:block; text-align: right }
sheep@592 2399 .editor .upload { margin: 2em auto; text-align: center }
sheep@592 2400 form.search input.search, .editor label input { font-size: 100%;
sheep@592 2401 border: solid 1px #babdb6; margin: 0.125em 0 }
sheep@592 2402 .editor label.comment input { width: 32em }
sheep@592 2403 a.logo { float: left; display: block; margin: 0.25em }
sheep@592 2404 div.header h1 { margin: 0; }
sheep@592 2405 div.content { clear: left }
sheep@592 2406 form.search { margin:0; text-align: right; font-size: 80% }
sheep@592 2407 div.snippet { font-size: 80%; color: #888a85 }
sheep@592 2408 div.header div.menu { float: right; margin-top: 1.25em }
sheep@592 2409 div.header div.menu a.current { color: #000 }
sheep@592 2410 hr { background: transparent; border:none; height: 0;
sheep@592 2411 border-bottom: 1px solid #babdb6; clear: both }
sheep@592 2412 blockquote { border-left:.25em solid #ccc; padding-left:.5em; margin-left:0}
sheep@696 2413 abbr.date {border:none}
sheep@696 2414 dt {font-weight: bold; float: left; }
sheep@696 2415 dd {font-style: italic; }
sheep@696 2416 """
sheep@3 2417
sheep@327 2418 def __init__(self, config):
sheep@704 2419 if config.get_bool('show_version', False):
sheep@704 2420 sys.stdout.write("Hatta %s\n" % __version__)
sheep@704 2421 sys.exit()
sheep@196 2422 self.dead = False
sheep@111 2423 self.config = config
sheep@434 2424 self.language = config.get('language', None)
sheep@163 2425 global _
sheep@434 2426 if self.language is not None:
sheep@163 2427 try:
sheep@163 2428 _ = gettext.translation('hatta', 'locale',
sheep@434 2429 languages=[self.language]).ugettext
sheep@163 2430 except IOError:
sheep@163 2431 _ = gettext.translation('hatta', fallback=True,
sheep@434 2432 languages=[self.language]).ugettext
sheep@163 2433 else:
sheep@163 2434 _ = gettext.translation('hatta', fallback=True).ugettext
sheep@434 2435 self.path = os.path.abspath(config.get('pages_path', 'docs'))
sheep@434 2436 self.cache = os.path.abspath(config.get('cache_path', 'cache'))
sheep@434 2437 self.page_charset = config.get('page_charset', 'utf-8')
sheep@434 2438 self.menu_page = self.config.get('menu_page', u'Menu')
sheep@434 2439 self.front_page = self.config.get('front_page', u'Home')
sheep@434 2440 self.logo_page = self.config.get('logo_page', u'logo.png')
sheep@434 2441 self.locked_page = self.config.get('locked_page', u'Locked')
sheep@434 2442 self.site_name = self.config.get('site_name', u'Hatta Wiki')
sheep@480 2443 self.read_only = self.config.get_bool('read_only', False)
sheep@526 2444 self.icon_page = self.config.get('icon_page', None)
sheep@719 2445 self.pygments_style = self.config.get('pygments_style', 'tango')
sheep@434 2446
sheep@434 2447 self.storage = self.storage_class(self.path, self.page_charset)
sheep@32 2448 if not os.path.isdir(self.cache):
sheep@32 2449 os.makedirs(self.cache)
sheep@68 2450 reindex = True
sheep@68 2451 else:
sheep@68 2452 reindex = False
sheep@434 2453 self.index = self.index_class(self.cache, self.language, self.storage)
sheep@323 2454 R = werkzeug.routing.Rule
sheep@0 2455 self.url_map = werkzeug.routing.Map([
sheep@434 2456 R('/', defaults={'title': self.front_page},
sheep@322 2457 endpoint=self.view, methods=['GET', 'HEAD']),
sheep@552 2458 R('/+edit/<title:title>', endpoint=self.edit, methods=['GET']),
sheep@552 2459 R('/+edit/<title:title>', endpoint=self.save, methods=['POST']),
sheep@552 2460 R('/+undo/<title:title>', endpoint=self.undo, methods=['POST']),
sheep@552 2461 R('/+history/', endpoint=self.recent_changes,
sheep@322 2462 methods=['GET', 'HEAD']),
sheep@615 2463 R('/+history/<title:title>/<int:from_rev>:<int:to_rev>',
sheep@656 2464 endpoint=self.diff, methods=['GET', 'HEAD']),
sheep@552 2465 R('/+history/<title:title>/<int:rev>', endpoint=self.revision,
sheep@656 2466 methods=['GET', 'HEAD']),
sheep@615 2467 R('/+history/<title:title>', endpoint=self.history,
sheep@615 2468 methods=['GET', 'HEAD']),
sheep@656 2469 R('/+version/', endpoint=self.version, methods=['GET', 'HEAD']),
sheep@638 2470 R('/+version/<title:title>', endpoint=self.version,
sheep@656 2471 methods=['GET', 'HEAD']),
sheep@552 2472 R('/+download/<title:title>', endpoint=self.download,
sheep@322 2473 methods=['GET', 'HEAD']),
sheep@553 2474 R('/+render/<title:title>', endpoint=self.render,
sheep@553 2475 methods=['GET', 'HEAD']),
sheep@347 2476 R('/<title:title>', endpoint=self.view, methods=['GET', 'HEAD']),
sheep@552 2477 R('/+feed/rss', endpoint=self.rss, methods=['GET', 'HEAD']),
sheep@552 2478 R('/+feed/atom', endpoint=self.atom, methods=['GET', 'HEAD']),
sheep@656 2479 R('/+index', endpoint=self.all_pages, methods=['GET', 'HEAD']),
sheep@657 2480 R('/+orphaned', endpoint=self.orphaned, methods=['GET', 'HEAD']),
sheep@659 2481 R('/+wanted', endpoint=self.wanted, methods=['GET', 'HEAD']),
sheep@552 2482 R('/+search', endpoint=self.search, methods=['GET', 'POST']),
sheep@552 2483 R('/+search/<title:title>', endpoint=self.backlinks,
sheep@322 2484 methods=['GET', 'POST']),
sheep@322 2485 R('/off-with-his-head', endpoint=self.die, methods=['GET']),
sheep@608 2486 R('/+hg<all:path>', endpoint=self.hgweb, strict_slashes=False,
sheep@608 2487 methods=['GET', 'POST', 'HEAD']),
sheep@668 2488 # Pages with default content
sheep@668 2489 R('/favicon.ico', endpoint=self.favicon_ico,
sheep@668 2490 methods=['GET', 'HEAD']),
sheep@668 2491 R('/robots.txt', endpoint=self.robots_txt, methods=['GET', 'HEAD']),
sheep@668 2492 R('/+download/style.css', endpoint=self.style_css,
sheep@668 2493 methods=['GET', 'HEAD']),
sheep@718 2494 R('/+download/pygments.css', endpoint=self.pygments_css,
sheep@718 2495 methods=['GET', 'HEAD']),
sheep@669 2496 R('/+download/scripts.js', endpoint=self.scripts_js,
sheep@669 2497 methods=['GET', 'HEAD']),
sheep@608 2498 ], converters={'title':WikiTitleConverter, 'all':WikiAllConverter})
sheep@0 2499
sheep@405 2500 def get_page(self, request, title):
sheep@405 2501 """Creates a page object based on page's mime type"""
sheep@405 2502
sheep@405 2503 if title:
sheep@405 2504 try:
sheep@693 2505 page_class, mime = self.filename_map[title]
sheep@405 2506 except KeyError:
sheep@693 2507 mime = self.storage.page_mime(title)
sheep@693 2508 major, minor = mime.split('/', 1)
sheep@405 2509 try:
sheep@693 2510 page_class = self.mime_map[mime]
sheep@405 2511 except KeyError:
sheep@405 2512 try:
sheep@693 2513 plus_pos = minor.find('+')
sheep@693 2514 if plus_pos>0:
sheep@693 2515 minor_base = minor[plus_pos:]
sheep@693 2516 else:
sheep@693 2517 minor_base = ''
sheep@693 2518 base_mime = '/'.join([major, minor_base])
sheep@693 2519 page_class = self.mime_map[base_mime]
sheep@405 2520 except KeyError:
sheep@693 2521 try:
sheep@693 2522 page_class = self.mime_map[major]
sheep@693 2523 except KeyError:
sheep@693 2524 page_class = self.mime_map['']
sheep@405 2525 else:
sheep@405 2526 page_class = WikiPage
sheep@405 2527 mime = ''
sheep@405 2528 return page_class(self, request, title, mime)
sheep@405 2529
sheep@0 2530 def view(self, request, title):
sheep@405 2531 page = self.get_page(request, title)
sheep@117 2532 try:
sheep@405 2533 content = page.view_content()
sheep@117 2534 except werkzeug.exceptions.NotFound:
sheep@342 2535 url = request.get_url(title, self.edit, external=True)
sheep@343 2536 return werkzeug.routing.redirect(url, code=303)
sheep@344 2537 html = page.render_content(content)
sheep@405 2538 dependencies = page.dependencies()
sheep@343 2539 etag = '/(%s)' % u','.join(dependencies)
sheep@343 2540 return self.response(request, title, html, etag=etag)
sheep@92 2541
sheep@24 2542 def revision(self, request, title, rev):
sheep@385 2543 text = self.storage.revision_text(title, rev)
sheep@344 2544 link = werkzeug.html.a(werkzeug.html(title),
sheep@344 2545 href=request.get_url(title))
sheep@24 2546 content = [
sheep@344 2547 werkzeug.html.p(
sheep@344 2548 werkzeug.html(
sheep@277 2549 _(u'Content of revision %(rev)d of page %(title)s:'))
sheep@344 2550 % {'rev': rev, 'title': link }),
sheep@344 2551 werkzeug.html.pre(werkzeug.html(text)),
sheep@24 2552 ]
sheep@344 2553 special_title = _(u'Revision of "%(title)s"') % {'title': title}
sheep@405 2554 page = self.get_page(request, title)
sheep@344 2555 html = page.render_content(content, special_title)
sheep@113 2556 response = self.response(request, title, html, rev=rev, etag='/old')
sheep@24 2557 return response
sheep@24 2558
sheep@638 2559 def version(self, request, title=None):
sheep@638 2560 if title is None:
sheep@638 2561 version = self.storage.repo_revision()
sheep@638 2562 else:
sheep@638 2563 try:
sheep@638 2564 version, x, x, x = self.storage.page_history(title).next()
sheep@638 2565 except StopIteration:
sheep@638 2566 version = 0
sheep@638 2567 return werkzeug.Response('%d' % version, mimetype="text/plain")
sheep@638 2568
sheep@669 2569 def _check_lock(self, title):
sheep@669 2570 restricted_pages = [
sheep@669 2571 'scripts.js',
sheep@669 2572 'robots.txt',
sheep@669 2573 ]
sheep@434 2574 if self.read_only:
sheep@482 2575 raise werkzeug.exceptions.Forbidden(_("This site is read-only."))
sheep@669 2576 if title in restricted_pages:
sheep@669 2577 raise werkzeug.exceptions.Forbidden(_("""Can't edit this page.
sheep@669 2578 It can only be edited by the site admin directly on the disk."""))
sheep@669 2579 if title in self.index.page_links(self.locked_page):
sheep@669 2580 raise werkzeug.exceptions.Forbidden(_("This page is locked."))
sheep@31 2581
sheep@31 2582 def save(self, request, title):
sheep@669 2583 self._check_lock(title)
sheep@340 2584 url = request.get_url(title)
sheep@31 2585 if request.form.get('cancel'):
sheep@31 2586 if title not in self.storage:
sheep@434 2587 url = request.get_url(self.front_page)
sheep@92 2588 if request.form.get('preview'):
sheep@92 2589 text = request.form.get("text")
sheep@92 2590 if text is not None:
sheep@677 2591 lines = text.split('\n')
sheep@92 2592 else:
sheep@344 2593 lines = [werkzeug.html.p(werkzeug.html(
sheep@344 2594 _(u'No preview for binaries.')))]
sheep@113 2595 return self.edit(request, title, preview=lines)
sheep@31 2596 elif request.form.get('save'):
sheep@31 2597 comment = request.form.get("comment", "")
sheep@31 2598 author = request.get_author()
sheep@31 2599 text = request.form.get("text")
sheep@261 2600 try:
sheep@261 2601 parent = int(request.form.get("parent"))
RadomirDopieralski@312 2602 except (ValueError, TypeError):
sheep@261 2603 parent = None
sheep@398 2604 self.storage.reopen()
sheep@648 2605 self.index.update(self, request)
sheep@643 2606 page = self.get_page(request, title)
sheep@31 2607 if text is not None:
sheep@434 2608 if title == self.locked_page:
sheep@643 2609 for link, label in page.extract_links(text):
sheep@359 2610 if title == link:
sheep@359 2611 raise werkzeug.exceptions.Forbidden()
sheep@205 2612 if u'href="' in comment or u'http:' in comment:
sheep@150 2613 raise werkzeug.exceptions.Forbidden()
sheep@31 2614 if text.strip() == '':
sheep@31 2615 self.storage.delete_page(title, author, comment)
sheep@434 2616 url = request.get_url(self.front_page)
sheep@31 2617 else:
sheep@385 2618 self.storage.save_text(title, text, author, comment, parent)
sheep@31 2619 else:
sheep@216 2620 text = u''
sheep@216 2621 upload = request.files['data']
sheep@216 2622 f = upload.stream
sheep@240 2623 if f is not None and upload.filename is not None:
sheep@31 2624 try:
sheep@31 2625 self.storage.save_file(title, f.tmpname, author,
sheep@261 2626 comment, parent)
sheep@31 2627 except AttributeError:
sheep@385 2628 self.storage.save_data(title, f.read(), author,
sheep@261 2629 comment, parent)
sheep@216 2630 else:
sheep@216 2631 self.storage.delete_page(title, author, comment)
sheep@434 2632 url = request.get_url(self.front_page)
sheep@643 2633 self.index.update_page(page, title, text=text)
sheep@31 2634 response = werkzeug.routing.redirect(url, code=303)
sheep@31 2635 response.set_cookie('author',
sheep@31 2636 werkzeug.url_quote(request.get_author()),
sheep@31 2637 max_age=604800)
sheep@31 2638 return response
sheep@31 2639
sheep@92 2640 def edit(self, request, title, preview=None):
sheep@669 2641 self._check_lock(title)
sheep@641 2642 exists = title in self.storage
sheep@641 2643 if exists:
sheep@641 2644 self.storage.reopen()
sheep@405 2645 page = self.get_page(request, title)
sheep@405 2646 content = page.editor_form(preview)
sheep@344 2647 special_title = _(u'Editing "%(title)s"') % {'title': title}
sheep@678 2648 html = page.render_content(content, special_title)
sheep@641 2649 if not exists:
sheep@637 2650 response = werkzeug.Response(html, mimetype="text/html",
sheep@419 2651 status='404 Not found')
sheep@663 2652
sheep@663 2653 elif preview:
sheep@663 2654 response = werkzeug.Response(html, mimetype="text/html")
sheep@66 2655 else:
sheep@663 2656 response = self.response(request, title, html, '/edit')
sheep@637 2657 response.headers.add('Cache-Control', 'no-cache')
sheep@637 2658 return response
sheep@0 2659
sheep@130 2660 def atom(self, request):
sheep@130 2661 date_format = "%Y-%m-%dT%H:%M:%SZ"
sheep@130 2662 first_date = datetime.datetime.now()
sheep@130 2663 now = first_date.strftime(date_format)
sheep@130 2664 body = []
sheep@130 2665 first_title = u''
sheep@130 2666 count = 0
sheep@130 2667 unique_titles = {}
sheep@130 2668 for title, rev, date, author, comment in self.storage.history():
sheep@130 2669 if title in unique_titles:
sheep@130 2670 continue
sheep@130 2671 unique_titles[title] = True
sheep@130 2672 count += 1
sheep@130 2673 if count > 10:
sheep@130 2674 break
sheep@130 2675 if not first_title:
sheep@130 2676 first_title = title
sheep@130 2677 first_rev = rev
sheep@130 2678 first_date = date
sheep@130 2679 item = u"""<entry>
sheep@130 2680 <title>%(title)s</title>
sheep@130 2681 <link href="%(page_url)s" />
sheep@130 2682 <content>%(comment)s</content>
sheep@130 2683 <updated>%(date)s</updated>
sheep@130 2684 <author>
sheep@130 2685 <name>%(author)s</name>
sheep@130 2686 <uri>%(author_url)s</uri>
sheep@130 2687 </author>
sheep@130 2688 <id>%(url)s</id>
sheep@130 2689 </entry>""" % {
sheep@130 2690 'title': werkzeug.escape(title),
sheep@130 2691 'page_url': request.adapter.build(self.view, {'title': title},
sheep@130 2692 force_external=True),
sheep@130 2693 'comment': werkzeug.escape(comment),
sheep@130 2694 'date': date.strftime(date_format),
sheep@130 2695 'author': werkzeug.escape(author),
sheep@130 2696 'author_url': request.adapter.build(self.view,
sheep@130 2697 {'title': author},
sheep@130 2698 force_external=True),
sheep@130 2699 'url': request.adapter.build(self.revision,
sheep@130 2700 {'title': title, 'rev': rev},
sheep@130 2701 force_external=True),
sheep@130 2702 }
sheep@130 2703 body.append(item)
sheep@130 2704 content = u"""<?xml version="1.0" encoding="utf-8"?>
sheep@130 2705 <feed xmlns="http://www.w3.org/2005/Atom">
sheep@130 2706 <title>%(title)s</title>
sheep@130 2707 <link rel="self" href="%(atom)s"/>
sheep@130 2708 <link href="%(home)s"/>
sheep@130 2709 <id>%(home)s</id>
sheep@130 2710 <updated>%(date)s</updated>
sheep@130 2711 <logo>%(logo)s</logo>
sheep@130 2712 %(body)s
sheep@130 2713 </feed>""" % {
sheep@434 2714 'title': self.site_name,
sheep@130 2715 'home': request.adapter.build(self.view, force_external=True),
sheep@130 2716 'atom': request.adapter.build(self.atom, force_external=True),
sheep@130 2717 'date': first_date.strftime(date_format),
sheep@130 2718 'logo': request.adapter.build(self.download,
sheep@434 2719 {'title': self.logo_page},
sheep@130 2720 force_external=True),
sheep@130 2721 'body': u''.join(body),
sheep@130 2722 }
sheep@130 2723 response = self.response(request, 'atom', content, '/atom',
sheep@130 2724 'application/xml', first_rev, first_date)
sheep@130 2725 response.set_etag('/atom/%d' % self.storage.repo_revision())
sheep@130 2726 response.make_conditional(request)
sheep@130 2727 return response
sheep@129 2728
sheep@0 2729 def rss(self, request):
sheep@438 2730 """Serve an RSS feed of recent changes."""
sheep@438 2731
sheep@129 2732 first_date = datetime.datetime.now()
sheep@129 2733 now = first_date.strftime("%a, %d %b %Y %H:%M:%S GMT")
sheep@46 2734 rss_body = []
sheep@64 2735 first_title = u''
sheep@81 2736 count = 0
sheep@82 2737 unique_titles = {}
sheep@46 2738 for title, rev, date, author, comment in self.storage.history():
sheep@81 2739 if title in unique_titles:
sheep@81 2740 continue
sheep@81 2741 unique_titles[title] = True
sheep@81 2742 count += 1
sheep@81 2743 if count > 10:
sheep@81 2744 break
sheep@64 2745 if not first_title:
sheep@64 2746 first_title = title
sheep@64 2747 first_rev = rev
sheep@64 2748 first_date = date
sheep@277 2749 item = (u'<item><title>%s</title><link>%s</link>'
sheep@277 2750 u'<description>%s</description><pubDate>%s</pubDate>'
sheep@277 2751 u'<dc:creator>%s</dc:creator><guid>%s</guid></item>' % (
sheep@46 2752 werkzeug.escape(title),
sheep@254 2753 request.adapter.build(self.view, {'title': title},
sheep@254 2754 force_external=True),
sheep@46 2755 werkzeug.escape(comment),
sheep@46 2756 date.strftime("%a, %d %b %Y %H:%M:%S GMT"),
sheep@46 2757 werkzeug.escape(author),
sheep@46 2758 request.adapter.build(self.revision,
sheep@46 2759 {'title': title, 'rev': rev})
sheep@277 2760 ))
sheep@46 2761 rss_body.append(item)
sheep@129 2762 rss_head = u"""<?xml version="1.0" encoding="utf-8"?>
sheep@129 2763 <rss version="2.0"
sheep@129 2764 xmlns:dc="http://purl.org/dc/elements/1.1/"
sheep@129 2765 xmlns:atom="http://www.w3.org/2005/Atom"
sheep@129 2766 >
sheep@129 2767 <channel>
sheep@129 2768 <title>%s</title>
sheep@129 2769 <atom:link href="%s" rel="self" type="application/rss+xml" />
sheep@129 2770 <link>%s</link>
sheep@158 2771 <description>%s</description>
sheep@129 2772 <generator>Hatta Wiki</generator>
sheep@129 2773 <language>en</language>
sheep@129 2774 <lastBuildDate>%s</lastBuildDate>
sheep@129 2775
sheep@129 2776 """ % (
sheep@434 2777 werkzeug.escape(self.site_name),
sheep@129 2778 request.adapter.build(self.rss),
sheep@129 2779 request.adapter.build(self.recent_changes),
sheep@277 2780 werkzeug.escape(_(u'Track the most recent changes to the wiki '
sheep@277 2781 u'in this feed.')),
sheep@129 2782 first_date,
sheep@129 2783 )
sheep@46 2784 content = [rss_head]+rss_body+[u'</channel></rss>']
sheep@91 2785 response = self.response(request, 'rss', content, '/rss',
sheep@91 2786 'application/xml', first_rev, first_date)
sheep@130 2787 response.set_etag('/rss/%d' % self.storage.repo_revision())
sheep@91 2788 response.make_conditional(request)
sheep@91 2789 return response
sheep@0 2790
sheep@65 2791 def response(self, request, title, content, etag='', mime='text/html',
sheep@553 2792 rev=None, date=None, size=None):
sheep@438 2793 """Create a WikiResponse for a page."""
sheep@438 2794
sheep@69 2795 response = WikiResponse(content, mimetype=mime)
sheep@99 2796 if rev is None:
sheep@553 2797 inode, _size, mtime = self.storage.page_file_meta(title)
sheep@106 2798 response.set_etag(u'%s/%s/%d-%d' % (etag, werkzeug.url_quote(title),
sheep@277 2799 inode, mtime))
sheep@553 2800 if size == -1:
sheep@553 2801 size = _size
sheep@105 2802 else:
sheep@277 2803 response.set_etag(u'%s/%s/%s' % (etag, werkzeug.url_quote(title),
sheep@277 2804 rev))
sheep@553 2805 if size:
sheep@553 2806 response.content_length = size
sheep@63 2807 response.make_conditional(request)
sheep@63 2808 return response
sheep@63 2809
sheep@63 2810 def download(self, request, title):
sheep@438 2811 """Serve the raw content of a page."""
sheep@438 2812
sheep@1 2813 mime = self.storage.page_mime(title)
sheep@24 2814 if mime == 'text/x-wiki':
sheep@24 2815 mime = 'text/plain'
sheep@1 2816 f = self.storage.open_page(title)
sheep@553 2817 response = self.response(request, title, f, '/download', mime, size=-1)
sheep@553 2818 return response
sheep@553 2819
sheep@553 2820 def render(self, request, title):
sheep@553 2821 """Serve a thumbnail or otherwise rendered content."""
sheep@553 2822
sheep@601 2823 def file_time_and_size(file_path):
sheep@601 2824 """Get file's modification timestamp and its size."""
sheep@601 2825
sheep@601 2826 try:
sheep@601 2827 (st_mode, st_ino, st_dev, st_nlink, st_uid, st_gid, st_size,
sheep@601 2828 st_atime, st_mtime, st_ctime) = os.stat(file_path)
sheep@601 2829 except OSError:
sheep@601 2830 st_mtime = 0
sheep@601 2831 st_size = None
sheep@601 2832 return st_mtime, st_size
sheep@601 2833
sheep@601 2834 def rm_temp_dir(dir_path):
sheep@601 2835 """Delete the directory with subdirectories."""
sheep@601 2836
sheep@601 2837 for root, dirs, files in os.walk(dir_path, topdown=False):
sheep@601 2838 for name in files:
sheep@601 2839 try:
sheep@601 2840 os.remove(os.path.join(root, name))
sheep@601 2841 except OSError:
sheep@601 2842 pass
sheep@601 2843 for name in dirs:
sheep@601 2844 try:
sheep@601 2845 os.rmdir(os.path.join(root, name))
sheep@601 2846 except OSError:
sheep@601 2847 pass
sheep@601 2848 try:
sheep@601 2849 os.rmdir(dir_path)
sheep@601 2850 except OSError:
sheep@601 2851 pass
sheep@601 2852
sheep@553 2853 page = self.get_page(request, title)
sheep@553 2854 try:
sheep@601 2855 cache_filename, cache_mime = page.render_mime()
sheep@553 2856 render = page.render_cache
sheep@614 2857 except (AttributeError, NotImplementedError):
sheep@553 2858 return self.download(request, title)
sheep@601 2859
sheep@601 2860 cache_dir = os.path.join(self.cache, 'render',
sheep@601 2861 werkzeug.url_quote(title, safe=''))
sheep@601 2862 cache_file = os.path.join(cache_dir, cache_filename)
sheep@601 2863 page_inode, page_size, page_mtime = self.storage.page_file_meta(title)
sheep@601 2864 cache_mtime, cache_size = file_time_and_size(cache_file)
sheep@601 2865 if page_mtime > cache_mtime:
sheep@554 2866 if not os.path.exists(cache_dir):
sheep@554 2867 os.makedirs(cache_dir)
sheep@554 2868 try:
sheep@601 2869 temp_dir = tempfile.mkdtemp(dir=cache_dir)
sheep@601 2870 result_file = render(temp_dir)
sheep@601 2871 mercurial.util.rename(result_file, cache_file)
sheep@554 2872 finally:
sheep@601 2873 rm_temp_dir(temp_dir)
sheep@601 2874 f = open(cache_file)
sheep@601 2875 response = self.response(request, title, f, '/render', cache_mime,
sheep@601 2876 size=cache_size)
sheep@1 2877 return response
sheep@0 2878
sheep@31 2879 def undo(self, request, title):
sheep@438 2880 """Revert a change to a page."""
sheep@438 2881
sheep@669 2882 self._check_lock(title)
sheep@31 2883 rev = None
sheep@31 2884 for key in request.form:
sheep@31 2885 try:
sheep@31 2886 rev = int(key)
sheep@31 2887 except ValueError:
sheep@31 2888 pass
sheep@31 2889 author = request.get_author()
sheep@31 2890 if rev is not None:
RadomirDopieralski@312 2891 try:
RadomirDopieralski@312 2892 parent = int(request.form.get("parent"))
RadomirDopieralski@312 2893 except (ValueError, TypeError):
RadomirDopieralski@312 2894 parent = None
sheep@398 2895 self.storage.reopen()
sheep@648 2896 self.index.update(self, request)
sheep@31 2897 if rev == 0:
sheep@158 2898 comment = _(u'Delete page %(title)s') % {'title': title}
sheep@31 2899 data = ''
sheep@31 2900 self.storage.delete_page(title, author, comment)
sheep@31 2901 else:
sheep@158 2902 comment = _(u'Undo of change %(rev)d of page %(title)s') % {
sheep@158 2903 'rev': rev, 'title': title}
sheep@31 2904 data = self.storage.page_revision(title, rev-1)
sheep@385 2905 self.storage.save_data(title, data, author, comment, parent)
sheep@643 2906 page = self.get_page(request, title)
sheep@643 2907 self.index.update_page(page, title, data=data)
sheep@31 2908 url = request.adapter.build(self.history, {'title': title},
sheep@349 2909 method='GET', force_external=True)
sheep@31 2910 return werkzeug.redirect(url, 303)
sheep@31 2911
sheep@24 2912 def history(self, request, title):
sheep@438 2913 """Display history of changes of a page."""
sheep@438 2914
sheep@405 2915 page = self.get_page(request, title)
sheep@405 2916 content = page.render_content(page.history_list(),
sheep@344 2917 _(u'History of "%(title)s"') % {'title': title})
sheep@65 2918 response = self.response(request, title, content, '/history')
sheep@24 2919 return response
sheep@24 2920
sheep@606 2921
sheep@24 2922 def recent_changes(self, request):
sheep@438 2923 """Serve the recent changes page."""
sheep@438 2924
sheep@606 2925 def changes_list(page):
sheep@607 2926 """Generate the content of the recent changes page."""
sheep@607 2927
sheep@686 2928 h = werkzeug.html
sheep@436 2929 yield u'<ul>'
sheep@436 2930 last = {}
sheep@436 2931 lastrev = {}
sheep@436 2932 count = 0
sheep@436 2933 for title, rev, date, author, comment in self.storage.history():
sheep@436 2934 if (author, comment) == last.get(title, (None, None)):
sheep@436 2935 continue
sheep@436 2936 count += 1
sheep@436 2937 if count > 100:
sheep@436 2938 break
sheep@436 2939 if rev > 0:
sheep@606 2940 date_url = request.adapter.build(self.diff, {
sheep@436 2941 'title': title,
sheep@436 2942 'from_rev': rev-1,
sheep@436 2943 'to_rev': lastrev.get(title, rev)
sheep@436 2944 })
sheep@436 2945 elif rev == 0:
sheep@606 2946 date_url = request.adapter.build(self.revision, {
sheep@436 2947 'title': title, 'rev': rev})
sheep@436 2948 else:
sheep@606 2949 date_url = request.adapter.build(self.history, {
sheep@436 2950 'title': title})
sheep@436 2951 last[title] = author, comment
sheep@436 2952 lastrev[title] = rev
sheep@606 2953
sheep@686 2954 yield h.li(h.a(page.date_html(date), href=date_url), ' ',
sheep@686 2955 h.b(page.wiki_link(title)), u' . . . . ',
sheep@686 2956 h.i(page.wiki_link(author)),
sheep@686 2957 h.div(h(comment), class_="comment")
sheep@606 2958 )
sheep@436 2959 yield u'</ul>'
sheep@436 2960
sheep@678 2961 page = self.get_page(request, '')
sheep@606 2962 content = page.render_content(changes_list(page), _(u'Recent changes'))
sheep@490 2963 response = WikiResponse(content, mimetype='text/html')
sheep@405 2964 response.set_etag('/history/%d' % self.storage.repo_revision())
sheep@91 2965 response.make_conditional(request)
sheep@24 2966 return response
sheep@24 2967
sheep@24 2968 def diff(self, request, title, from_rev, to_rev):
sheep@607 2969 """Show the differences between specified revisions."""
sheep@438 2970
sheep@407 2971 page = self.get_page(request, title)
sheep@666 2972 build = request.adapter.build
sheep@666 2973 from_url = build(self.revision, {'title': title, 'rev': from_rev})
sheep@666 2974 to_url = build(self.revision, {'title': title, 'rev': to_rev})
sheep@666 2975 a = werkzeug.html.a
sheep@666 2976 links = {
sheep@666 2977 'link1': a(str(from_rev), href=from_url),
sheep@666 2978 'link2': a(str(to_rev), href=to_url),
sheep@666 2979 'link': a(werkzeug.html(title), href=request.get_url(title)),
sheep@666 2980 }
sheep@666 2981 message = werkzeug.html(_(
sheep@666 2982 u'Differences between revisions %(link1)s and %(link2)s '
sheep@666 2983 u'of page %(link)s.')) % links
sheep@717 2984 diff_content = getattr(page, 'diff_content', None)
sheep@717 2985 if diff_content:
sheep@717 2986 from_text = self.storage.revision_text(page.title, from_rev)
sheep@717 2987 to_text = self.storage.revision_text(page.title, to_rev)
sheep@717 2988 content = page.diff_content(from_text, to_text, message)
sheep@717 2989 else:
sheep@717 2990 content = [werkzeug.html.p(werkzeug.html(
sheep@717 2991 _(u"Diff not available for this kind of pages.")))]
sheep@436 2992 special_title = _(u'Diff for "%(title)s"') % {'title': title}
sheep@344 2993 html = page.render_content(content, special_title)
sheep@344 2994 response = werkzeug.Response(html, mimetype='text/html')
sheep@24 2995 return response
sheep@24 2996
sheep@607 2997
sheep@651 2998 def all_pages(self, request):
sheep@651 2999 """Show index of all pages in the wiki."""
sheep@651 3000
sheep@651 3001 page = self.get_page(request, '')
sheep@651 3002 all_pages = sorted(self.storage.all_pages())
sheep@651 3003 content = page.pages_list(all_pages, _(u'Index of all pages'))
sheep@651 3004 html = page.render_content(content, _(u'Page Index'))
sheep@652 3005 response = WikiResponse(html, mimetype='text/html')
sheep@652 3006 response.set_etag('/+index/%d' % self.storage.repo_revision())
sheep@652 3007 response.make_conditional(request)
sheep@652 3008 return response
sheep@651 3009
sheep@657 3010 def orphaned(self, request):
sheep@657 3011 """Show all pages that don't have backlinks."""
sheep@657 3012
sheep@657 3013 page = self.get_page(request, '')
sheep@657 3014 pages = self.index.orphaned_pages()
sheep@657 3015 content = page.pages_list(pages,
sheep@657 3016 _(u'List of pages with no links to them'))
sheep@657 3017 html = page.render_content(content, _(u'Orphaned pages'))
sheep@657 3018 response = WikiResponse(html, mimetype='text/html')
sheep@660 3019 response.set_etag('/+orphaned/%d' % self.storage.repo_revision())
sheep@657 3020 response.make_conditional(request)
sheep@657 3021 return response
sheep@657 3022
sheep@659 3023 def wanted(self, request):
sheep@659 3024 """Show all pages that don't exist yet, but are linked."""
sheep@659 3025
sheep@682 3026 def wanted_pages_list(page):
sheep@682 3027 """Generate the content of wanted pages page."""
sheep@682 3028
sheep@682 3029 h = werkzeug.html
sheep@682 3030 yield h.p(h(
sheep@682 3031 _(u"List of pages that are linked to, but don't exist yet.")))
sheep@682 3032 yield u'<ol class="wanted">'
sheep@682 3033 for refs, title in self.index.wanted_pages():
sheep@682 3034 url = page.get_url(title, self.backlinks)
sheep@682 3035 yield h.li(h.b(page.wiki_link(title)),
sheep@682 3036 h.i(u' (', h.a(h(_(u"%d references") % refs),
sheep@682 3037 href=url, class_="backlinks"), ')'))
sheep@682 3038 yield u'</ol>'
sheep@682 3039
sheep@659 3040 page = self.get_page(request, '')
sheep@682 3041 content = wanted_pages_list(page)
sheep@659 3042 html = page.render_content(content, _(u'Wanted pages'))
sheep@659 3043 response = WikiResponse(html, mimetype='text/html')
sheep@659 3044 response.set_etag('/+wanted/%d' % self.storage.repo_revision())
sheep@659 3045 response.make_conditional(request)
sheep@659 3046 return response
sheep@659 3047
sheep@26 3048 def search(self, request):
sheep@438 3049 """Serve the search results page."""
sheep@438 3050
sheep@436 3051 def search_snippet(title, words):
sheep@436 3052 """Extract a snippet of text for search results."""
sheep@436 3053
sheep@607 3054 try:
sheep@607 3055 text = self.storage.page_text(title)
sheep@607 3056 except werkzeug.exceptions.NotFound:
sheep@607 3057 return u''
sheep@607 3058 regexp = re.compile(u"|".join(re.escape(w) for w in words),
sheep@607 3059 re.U|re.I)
sheep@436 3060 match = regexp.search(text)
sheep@436 3061 if match is None:
sheep@436 3062 return u""
sheep@436 3063 position = match.start()
sheep@436 3064 min_pos = max(position - 60, 0)
sheep@436 3065 max_pos = min(position + 60, len(text))
sheep@436 3066 snippet = werkzeug.escape(text[min_pos:max_pos])
sheep@576 3067 highlighted = werkzeug.html.b(match.group(0), class_="highlight")
sheep@436 3068 html = regexp.sub(highlighted, snippet)
sheep@436 3069 return html
sheep@436 3070
sheep@712 3071 def page_search(words, page, request):
sheep@607 3072 """Display the search results."""
sheep@607 3073
sheep@681 3074 h = werkzeug.html
sheep@436 3075 self.storage.reopen()
sheep@648 3076 self.index.update(self, request)
sheep@436 3077 result = sorted(self.index.find(words), key=lambda x:-x[0])
sheep@681 3078 yield werkzeug.html.p(h(_(u'%d page(s) containing all words:')
sheep@681 3079 % len(result)))
sheep@681 3080 yield u'<ol class="search">'
sheep@681 3081 for number, (score, title) in enumerate(result):
sheep@712 3082 yield h.li(h.b(page.wiki_link(title)), u' ', h.i(str(score)),
sheep@681 3083 h.div(search_snippet(title, words),
sheep@681 3084 _class="snippet"),
sheep@681 3085 id_="search-%d" % (number+1))
sheep@681 3086 yield u'</ol>'
sheep@436 3087
sheep@400 3088 query = request.values.get('q', u'').strip()
sheep@607 3089 page = self.get_page(request, '')
sheep@400 3090 if not query:
sheep@661 3091 url = request.get_url(view=self.all_pages, external=True)
sheep@661 3092 return werkzeug.routing.redirect(url, code=303)
sheep@651 3093 words = tuple(self.index.split_text(query, stop=False))
sheep@651 3094 if not words:
sheep@651 3095 words = (query,)
sheep@651 3096 title = _(u'Searching for "%s"') % u" ".join(words)
sheep@651 3097 content = page_search(words, page, request)
sheep@344 3098 html = page.render_content(content, title)
sheep@65 3099 return WikiResponse(html, mimetype='text/html')
sheep@26 3100
sheep@30 3101 def backlinks(self, request, title):
sheep@435 3102 """Serve the page with backlinks."""
sheep@435 3103
sheep@402 3104 self.storage.reopen()
sheep@650 3105 self.index.update(self, request)
sheep@405 3106 page = self.get_page(request, title)
sheep@654 3107 message = _(u'Pages that contain a link to %(link)s.')
sheep@654 3108 link = page.wiki_link(title)
sheep@654 3109 pages = self.index.page_backlinks(title)
sheep@654 3110 content = page.pages_list(pages, message, link, _class='backlinks')
sheep@654 3111 html = page.render_content(content, _(u'Links to "%s"') % title)
sheep@490 3112 response = WikiResponse(html, mimetype='text/html')
sheep@652 3113 response.set_etag('/+search/%d' % self.storage.repo_revision())
sheep@170 3114 response.make_conditional(request)
sheep@65 3115 return response
sheep@30 3116
sheep@668 3117 def _serve_default(self, request, title, content, mime):
sheep@668 3118 """Some pages have their default content."""
sheep@668 3119
sheep@610 3120 if title in self.storage:
sheep@610 3121 return self.download(request, title)
sheep@668 3122 response = werkzeug.Response(content, mimetype=mime)
sheep@701 3123 response.set_etag('/%s/-1' % title)
sheep@701 3124 response.make_conditional(request)
sheep@668 3125 return response
sheep@668 3126
sheep@668 3127 def scripts_js(self, request):
sheep@668 3128 """Server the default scripts"""
sheep@668 3129
sheep@668 3130 return self._serve_default(request, 'scripts.js', self.scripts,
sheep@668 3131 'text/javascript')
sheep@668 3132
sheep@668 3133 def style_css(self, request):
sheep@668 3134 """Serve the default style"""
sheep@668 3135
sheep@668 3136 return self._serve_default(request, 'style.css', self.style,
sheep@668 3137 'text/css')
sheep@718 3138 def pygments_css(self, request):
sheep@718 3139 """Serve the default pygments style"""
sheep@718 3140
sheep@718 3141 if pygments is None:
sheep@718 3142 raise werkzeug.exceptions.NotFound()
sheep@718 3143
sheep@719 3144 pygments_style = self.pygments_style
sheep@718 3145 if pygments_style not in pygments.styles.STYLE_MAP:
sheep@718 3146 pygments_style = 'default'
sheep@718 3147 formatter = pygments.formatters.HtmlFormatter(style=pygments_style)
sheep@718 3148 style_defs = formatter.get_style_defs('.highlight')
sheep@718 3149 return self._serve_default(request, 'pygments.css', style_defs,
sheep@718 3150 'text/css')
sheep@668 3151
sheep@668 3152 def favicon_ico(self, request):
sheep@668 3153 """Serve the default favicon."""
sheep@668 3154
sheep@668 3155 return self._serve_default(request, 'favicon.ico', self.icon,
sheep@668 3156 'image/x-icon')
sheep@668 3157
sheep@668 3158 def robots_txt(self, request):
sheep@668 3159 """Serve the robots directives."""
sheep@668 3160
sheep@668 3161 robots = ('User-agent: *\r\n'
sheep@668 3162 'Disallow: /+*\r\n'
sheep@668 3163 'Disallow: /%2B*\r\n'
sheep@668 3164 'Disallow: /+edit\r\n'
sheep@668 3165 'Disallow: /+feed\r\n'
sheep@668 3166 'Disallow: /+history\r\n'
sheep@668 3167 'Disallow: /+search\r\n'
sheep@668 3168 'Disallow: /+hg\r\n'
sheep@668 3169 )
sheep@668 3170 return self._serve_default(request, 'robots.txt', robots,
sheep@668 3171 'text/plain')
sheep@1 3172
sheep@608 3173 def hgweb(self, request, path=None):
sheep@608 3174 """Serve the pages repository on the web like a normal hg repository."""
sheep@608 3175
sheep@608 3176 if not self.config.get_bool('hgweb', False):
sheep@608 3177 raise werkzeug.exceptions.Forbidden('Repository access disabled.')
sheep@608 3178 app = mercurial.hgweb.request.wsgiapplication(
sheep@609 3179 lambda: mercurial.hgweb.hgweb(self.storage.repo, self.site_name))
sheep@608 3180 def hg_app(env, start):
sheep@608 3181 env = request.environ
sheep@608 3182 prefix='/+hg'
sheep@608 3183 if env['PATH_INFO'].startswith(prefix):
sheep@608 3184 env["PATH_INFO"] = env["PATH_INFO"][len(prefix):]
sheep@608 3185 env["SCRIPT_NAME"] += prefix
sheep@608 3186 return app(env, start)
sheep@608 3187 return hg_app
sheep@608 3188
sheep@668 3189 def die(self, request):
sheep@668 3190 """Terminate the standalone server if invoked from localhost."""
sheep@590 3191
cezary@512 3192 if not request.remote_addr.startswith('127.'):
cezary@512 3193 raise werkzeug.exceptions.Forbidden()
sheep@195 3194 def agony():
sheep@195 3195 yield u'Oh dear!'
sheep@196 3196 self.dead = True
sheep@195 3197 return werkzeug.Response(agony(), mimetype='text/plain')
cezary@514 3198
sheep@0 3199 @werkzeug.responder
sheep@0 3200 def application(self, environ, start):
sheep@271 3201 """The main application loop."""
sheep@271 3202
sheep@0 3203 adapter = self.url_map.bind_to_environ(environ)
sheep@0 3204 request = WikiRequest(self, adapter, environ)
sheep@0 3205 try:
sheep@124 3206 try:
sheep@124 3207 endpoint, values = adapter.match()
sheep@124 3208 return endpoint(request, **values)
sheep@277 3209 except werkzeug.exceptions.HTTPException, err:
sheep@277 3210 return err
sheep@0 3211 finally:
sheep@0 3212 request.cleanup()
sheep@97 3213 del request
sheep@99 3214 del adapter
sheep@0 3215
sheep@613 3216 def read_config():
sheep@613 3217 """Read and parse the config."""
sheep@302 3218
sheep@112 3219 config = WikiConfig(
sheep@115 3220 # Here you can modify the configuration: uncomment and change the ones
sheep@115 3221 # you need. Note that it's better use environment variables or command
sheep@115 3222 # line switches.
sheep@115 3223
sheep@188 3224 # interface='',
sheep@188 3225 # port=8080,
sheep@188 3226 # pages_path = 'docs',
sheep@188 3227 # cache_path = 'cache',
sheep@188 3228 # front_page = 'Home',
sheep@188 3229 # site_name = 'Hatta Wiki',
sheep@188 3230 # page_charset = 'UTF-8',
sheep@112 3231 )
sheep@277 3232 config.parse_args()
sheep@308 3233 config.parse_files()
sheep@434 3234 # config.sanitize()
sheep@613 3235 return config
sheep@613 3236
sheep@613 3237 def application(env, start):
sheep@613 3238 """Detect that we are being run as WSGI application."""
sheep@613 3239
sheep@613 3240 global application
sheep@613 3241 config = read_config()
sheep@613 3242 script_dir = os.path.dirname(os.path.abspath(__file__))
sheep@687 3243 if config.get('pages_path') is None:
sheep@613 3244 config.set('pages_path', os.path.join(script_dir, 'docs'))
sheep@687 3245 if config.get('cache_path') is None:
sheep@613 3246 config.set('cache_path', os.path.join(script_dir, 'cache'))
sheep@613 3247 wiki = Wiki(config)
sheep@613 3248 application = wiki.application
sheep@613 3249 return application(env, start)
sheep@613 3250
sheep@644 3251 def main(config=None, wiki=None):
sheep@613 3252 """Start a standalone WSGI server."""
sheep@613 3253
sheep@644 3254 config = config or read_config()
sheep@644 3255 wiki = wiki or Wiki(config)
sheep@613 3256 app = wiki.application
sheep@613 3257
sheep@628 3258 host, port = (config.get('interface', '0.0.0.0'),
sheep@628 3259 int(config.get('port', 8080)))
sheep@217 3260 try:
sheep@415 3261 from cherrypy import wsgiserver
sheep@415 3262 except ImportError:
sheep@415 3263 try:
sheep@415 3264 from cherrypy import _cpwsgiserver as wsgiserver
sheep@415 3265 except ImportError:
sheep@613 3266 import wsgiref.simple_server
sheep@613 3267 server = wsgiref.simple_server.make_server(host, port, app)
sheep@415 3268 try:
sheep@415 3269 server.serve_forever()
sheep@415 3270 except KeyboardInterrupt:
sheep@415 3271 pass
sheep@415 3272 return
sheep@613 3273 apps = [('', app)]
sheep@435 3274 name = wiki.site_name
sheep@415 3275 server = wsgiserver.CherryPyWSGIServer((host, port), apps, server_name=name)
sheep@415 3276 try:
sheep@415 3277 server.start()
sheep@217 3278 except KeyboardInterrupt:
sheep@415 3279 server.stop()
sheep@187 3280
sheep@187 3281 if __name__ == "__main__":
sheep@187 3282 main()