| 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'...': "…",
|
|
sheep@300
|
712 r'--': "–",
|
|
sheep@0
|
713 r'---': "—",
|
|
sheep@0
|
714 r'~': " ",
|
|
sheep@0
|
715 r'\~': "~",
|
|
sheep@0
|
716 r'~~': "∼",
|
|
sheep@0
|
717 r'(C)': "©",
|
|
sheep@0
|
718 r'-->': "→",
|
|
sheep@0
|
719 r'<--': "←",
|
|
sheep@0
|
720 r'(R)': "®",
|
|
sheep@0
|
721 r'(TM)': "™",
|
|
sheep@0
|
722 r'%%': "‰",
|
|
sheep@0
|
723 r'``': "“",
|
|
sheep@0
|
724 r"''": "”",
|
|
sheep@0
|
725 r",,": "„",
|
|
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('@', '@').replace('.', '.')
|
|
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()
|