Merge lp:~thomir-deactivatedaccount/wikkid/wikkid-improvments into lp:wikkid

Proposed by Thomi Richards
Status: Merged
Merged at revision: 53
Proposed branch: lp:~thomir-deactivatedaccount/wikkid/wikkid-improvments
Merge into: lp:wikkid
Diff against target: 802 lines (+532/-24)
18 files modified
Hacking.txt (+2/-1)
ToDo.txt (+0/-2)
bin/wikkid-serve (+19/-5)
wikkid/app.py (+19/-8)
wikkid/context.py (+43/-0)
wikkid/dispatcher.py (+3/-2)
wikkid/fileutils.py (+58/-0)
wikkid/formatter/markdownformatter.py (+27/-0)
wikkid/formatter/registry.py (+7/-2)
wikkid/formatter/textileformatter.py (+26/-0)
wikkid/skin/default/missing-directory.html (+19/-0)
wikkid/skin/loader.py (+2/-0)
wikkid/tests/formatters/__init__.py (+2/-0)
wikkid/tests/formatters/test_markdown.py (+166/-0)
wikkid/tests/formatters/test_textile.py (+121/-0)
wikkid/view/base.py (+2/-1)
wikkid/view/missing.py (+11/-0)
wikkid/view/wiki.py (+5/-3)
To merge this branch: bzr merge lp:~thomir-deactivatedaccount/wikkid/wikkid-improvments
Reviewer Review Type Date Requested Status
Tim Penhey Pending
Review via email: mp+40250@code.launchpad.net

Description of the change

Several small changes:

 * Added wiki text formatters for markdown and textile. To use these, you must have the python-markdown and python-textile packages installed, respectively.

 * User can now specify which wiki text format they want to use by default when running wikkid-serve. The default is still 'rest'.

 * User can now specify which network interface to listen on. Default is still 'localhost'

 * When requesting a directory that does not exist, we now give a nice page, rather than an error.

To post a comment you must log in.
65. By Thomi Richards

Added unit tests for markdown formatter.

66. By Thomi Richards

Added unit tests for recently added textile formatter.

67. By Thomi Richards

Implemented several changes, as requested by Tim.

Mainly:
 * ExecutionContext is no longer a singleton, and is passed in to all base views.

 * ExecutionContext class is no longer a singleton. It now derives from the dict
   type, and provides attribute-based lookup as well.

 * Fixed indentation, module imports, docstrings and some other things.

68. By Thomi Richards

Added simple unicode tests for markdown and textile formatters.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Hacking.txt'
2--- Hacking.txt 2010-07-12 19:02:14 +0000
3+++ Hacking.txt 2010-11-12 09:13:57 +0000
4@@ -29,7 +29,8 @@
5 Currently there is the Bazaar based one, and a simplistic filestore that
6 just uses some in-memory dictionaries.
7 * **wikkid.formatter**: The different formatters used to render the text files
8- as HTML. Currently there are formatters for Creold, ReST, and Pygments.
9+ as HTML. Currently there are formatters for Creold, ReST, textile, markdown
10+ and Pygments.
11 * **wikkid.interface**: All the zope interface definitions are in this module.
12 * **wikkid.model**: The model objects representing the different things that
13 you end up hitting through file traversal, like missing pages, directories,
14
15=== modified file 'ToDo.txt'
16--- ToDo.txt 2010-07-12 18:34:03 +0000
17+++ ToDo.txt 2010-11-12 09:13:57 +0000
18@@ -7,8 +7,6 @@
19
20 * Add in the ability to move pages
21
22-* Add markdown and textile formatters
23-
24 * Lock pages as read-only (see also merging)
25 Is this really locked, or is it a restricted ACL?
26
27
28=== modified file 'bin/wikkid-serve'
29--- bin/wikkid-serve 2010-06-13 11:12:22 +0000
30+++ bin/wikkid-serve 2010-11-12 09:13:57 +0000
31@@ -1,4 +1,5 @@
32 #! /usr/bin/python
33+# -*- coding: utf-8 -*-
34 #
35 # Copyright (C) 2010 Wikkid Developers.
36 #
37@@ -23,6 +24,7 @@
38 from wikkid.app import WikkidApp
39 from wikkid.filestore.bzr import FileStore
40 from wikkid.user.bzr import LocalBazaarUserMiddleware
41+from wikkid.context import ExecutionContext
42
43
44 def setup_logging():
45@@ -39,11 +41,23 @@
46 def main(args):
47
48 usage = "Usage: %prog [options] <wiki-branch>"
49- parser = optparse.OptionParser(
50- usage=usage, description="Run a Wikkid Wiki server.", version=version)
51+ parser = optparse.OptionParser(usage=usage,
52+ description="Run a Wikkid Wiki server.",
53+ version=version)
54+ parser.add_option('--host', type='string', default='localhost',
55+ help='The interface to listen on. Defaults to \'localhost\'')
56 parser.add_option('--port', type='int', default=8080,
57 help='The port to listen on. Defaults to 8080.')
58+ parser.add_option('--default-format', type='string', default='rest',
59+ dest='default_formatter',
60+ help='Specify the default formatter to use. Defaults to \'rest\'')
61 options, args = parser.parse_args(sys.argv[1:])
62+
63+ # create execution context object, and add processed command line args to it
64+ ec = ExecutionContext()
65+ # kinda ugly, but the only way to do it.
66+ ec.add_context(vars(options))
67+
68 if len(args) == 0:
69 print "No branch location specified.\n"
70 parser.print_usage()
71@@ -61,11 +75,11 @@
72 logger.info('Using: %s', working_tree)
73 filestore = FileStore(working_tree)
74
75- app = WikkidApp(filestore)
76+ app = WikkidApp(filestore, execution_context=ec)
77 app = LocalBazaarUserMiddleware(app, working_tree.branch)
78 from wsgiref.simple_server import make_server
79- httpd = make_server('localhost', options.port, app)
80- logger.info('Serving on http://localhost:%s', options.port)
81+ httpd = make_server(options.host, options.port, app)
82+ logger.info('Serving on http://%s:%s', options.host, options.port)
83 try:
84 httpd.serve_forever()
85 except KeyboardInterrupt:
86
87=== modified file 'wikkid/app.py'
88--- wikkid/app.py 2010-07-12 18:21:42 +0000
89+++ wikkid/app.py 2010-11-12 09:13:57 +0000
90@@ -1,3 +1,4 @@
91+# -*- coding: utf-8 -*-
92 #
93 # Copyright (C) 2010 Wikkid Developers.
94 #
95@@ -19,18 +20,26 @@
96 from wikkid.model.factory import ResourceFactory
97 from wikkid.skin.loader import Skin
98 from wikkid.view.urls import parse_url
99+from wikkid.fileutils import FileIterable
100+from wikkid.context import ExecutionContext
101
102
103 def serve_file(filename):
104 if os.path.exists(filename):
105 basename = urlutils.basename(filename)
106 content_type = mimetypes.guess_type(basename)[0]
107- f = open(filename, 'rb')
108- try:
109- return Response(
110- f.read(), content_type=content_type)
111- finally:
112- f.close()
113+
114+ res = Response(content_type=content_type, conditional_response=True)
115+ res.app_iter = FileIterable(filename)
116+ res.content_length = os.path.getsize(filename)
117+ res.last_modified = os.path.getmtime(filename)
118+ # Todo: is this the best value for the etag?
119+ # perhaps md5 would be a better alternative
120+ res.etag = '%s-%s-%s' % (os.path.getmtime(filename),
121+ os.path.getsize(filename),
122+ hash(filename))
123+ return res
124+
125 else:
126 return HTTPNotFound()
127
128@@ -38,7 +47,9 @@
129 class WikkidApp(object):
130 """The main wikkid application."""
131
132- def __init__(self, filestore, skin_name=None):
133+ def __init__(self, filestore, skin_name=None,
134+ execution_context = ExecutionContext()):
135+ self.execution_context = execution_context
136 self.filestore = filestore
137 self.resource_factory = ResourceFactory(self.filestore)
138 # Need to load the initial templates for the skin.
139@@ -73,7 +84,7 @@
140 resource_path, action = parse_url(path)
141 model = self.resource_factory.get_resource_at_path(resource_path)
142 try:
143- view = get_view(model, action, request)
144+ view = get_view(model, action, request, self.execution_context)
145 response = view.render(self.skin)
146 except HTTPException, e:
147 response = e
148
149=== added file 'wikkid/context.py'
150--- wikkid/context.py 1970-01-01 00:00:00 +0000
151+++ wikkid/context.py 2010-11-12 09:13:57 +0000
152@@ -0,0 +1,43 @@
153+# -*- coding: utf-8 -*-
154+#
155+# Copyright (C) 2010 Wikkid Developers.
156+#
157+# This software is licensed under the GNU Affero General Public License
158+# version 3 (see the file LICENSE).
159+
160+"""A means of storing execution context."""
161+
162+
163+class ExecutionContext(dict):
164+ """Store run-time execution context data.
165+
166+ To use this class, create an instance of it, and either set key,value
167+ pairs just like you would a dictionary, or call add_context(dict),
168+ passing in a dictionary of name,value pairs that you want to _append_
169+ to the execution context.
170+
171+ To retrieve values, you can either use the classic dictionary methods,
172+ or access options as attribute. So, this:
173+
174+ ec['someOption']
175+
176+ is equivilent to this:
177+
178+ ec.someOption
179+ """
180+
181+ def add_context(self, contextDict):
182+ if type(contextDict) is not dict:
183+ raise TypeError, "Context must be a dictionary type."
184+ self.update(contextDict)
185+
186+ def __getattr__(self, name):
187+ if self.has_key(name):
188+ return self[name]
189+ else:
190+ raise AttributeError, "ExecutionContext instance has no attribute '%s'" % (name)
191+
192+ def __setattr__(self, name, value):
193+ self[name] = value
194+
195+
196
197=== modified file 'wikkid/dispatcher.py'
198--- wikkid/dispatcher.py 2010-06-13 11:41:41 +0000
199+++ wikkid/dispatcher.py 2010-11-12 09:13:57 +0000
200@@ -17,12 +17,13 @@
201
202 from zope.interface import providedBy
203
204+from wikkid.context import ExecutionContext
205
206 # The view registry needs to map an Interface and a name to a class.
207 _VIEW_REGISTRY = {}
208
209
210-def get_view(obj, view_name, request):
211+def get_view(obj, view_name, request, ec=ExecutionContext()):
212 """Get the most relevant view for the object for the specified name.
213
214 Iterate through the provided interfaces of the object and look in the view
215@@ -32,7 +33,7 @@
216 for interface in interfaces:
217 try:
218 klass = _VIEW_REGISTRY[(interface, view_name)]
219- instance = klass(obj, request)
220+ instance = klass(obj, request, ec)
221 instance.initialize()
222 return instance
223 except KeyError:
224
225=== added file 'wikkid/fileutils.py'
226--- wikkid/fileutils.py 1970-01-01 00:00:00 +0000
227+++ wikkid/fileutils.py 2010-11-12 09:13:57 +0000
228@@ -0,0 +1,58 @@
229+ #
230+# Copyright (C) 2010 Wikkid Developers.
231+#
232+# This software is licensed under the GNU Affero General Public License
233+# version 3 (see the file LICENSE).
234+
235+"""A method of iterating over file contents
236+
237+This stops us reading the entire file into memory when we serve it.
238+"""
239+
240+
241+class FileIterable(object):
242+ """An iterable file-like object."""
243+ def __init__(self, filename, start=None, stop=None):
244+ self.filename = filename
245+ self.start = start
246+ self.stop = stop
247+
248+ def __iter__(self):
249+ return FileIterator(self.filename, self.start, self.stop)
250+
251+ def app_iter_range(self, start, stop):
252+ return self.__class__(self.filename, start, stop)
253+
254+class FileIterator(object):
255+ """Iterate over a file.
256+
257+ FileIterable provides a simple file iterator, optionally allowing the
258+ user to specify start and end ranges for the file.
259+ """
260+ chunk_size = 4096
261+ def __init__(self, filename, start, stop):
262+ self.filename = filename
263+ self.fileobj = open(self.filename, 'rb')
264+ if start:
265+ self.fileobj.seek(start)
266+ if stop is not None:
267+ self.length = stop - start
268+ else:
269+ self.length = None
270+
271+ def __iter__(self):
272+ return self
273+
274+ def next(self):
275+ if self.length is not None and self.length <= 0:
276+ raise StopIteration
277+ chunk = self.fileobj.read(self.chunk_size)
278+ if not chunk:
279+ raise StopIteration
280+ if self.length is not None:
281+ self.length -= len(chunk)
282+ if self.length < 0:
283+ # Chop off the extra:
284+ chunk = chunk[:self.length]
285+ return chunk
286+
287
288=== added file 'wikkid/formatter/markdownformatter.py'
289--- wikkid/formatter/markdownformatter.py 1970-01-01 00:00:00 +0000
290+++ wikkid/formatter/markdownformatter.py 2010-11-12 09:13:57 +0000
291@@ -0,0 +1,27 @@
292+# -*- coding: utf-8 -*-
293+#
294+# Copyright (C) 2010 Wikkid Developers.
295+#
296+# This software is licensed under the GNU Affero General Public License
297+# version 3 (see the file LICENSE).
298+
299+"""A text to html formatter using markdown."""
300+
301+import cgi
302+import markdown
303+from zope.interface import implements
304+
305+from wikkid.interface.formatter import ITextFormatter
306+
307+
308+class MarkdownFormatter(object):
309+ """Format source files as HTML using markdown."""
310+
311+ implements(ITextFormatter)
312+
313+ def format(self, filename, text):
314+ """Format the text.
315+ """
316+ md = markdown.Markdown(safe_mode='replace')
317+ return md.convert(text)
318+
319
320=== modified file 'wikkid/formatter/registry.py'
321--- wikkid/formatter/registry.py 2010-06-09 11:29:38 +0000
322+++ wikkid/formatter/registry.py 2010-11-12 09:13:57 +0000
323@@ -1,3 +1,4 @@
324+# -*- coding: utf-8 -*-
325 #
326 # Copyright (C) 2010 Wikkid Developers
327 #
328@@ -11,6 +12,8 @@
329 from wikkid.formatter.creoleformatter import CreoleFormatter
330 from wikkid.formatter.pygmentsformatter import PygmentsFormatter
331 from wikkid.formatter.restformatter import RestructuredTextFormatter
332+from wikkid.formatter.markdownformatter import MarkdownFormatter
333+from wikkid.formatter.textileformatter import TextileFormatter
334
335
336 class FormatterRegistry(object):
337@@ -18,9 +21,11 @@
338
339 def __init__(self):
340 self._formatters = {
341+ 'creole': CreoleFormatter(),
342+ 'markdown': MarkdownFormatter(),
343+ 'pygments': PygmentsFormatter(),
344 'rest': RestructuredTextFormatter(),
345- 'creole': CreoleFormatter(),
346- 'pygments': PygmentsFormatter()
347+ 'textile': TextileFormatter(),
348 }
349
350 def __getitem__(self, formatter):
351
352=== added file 'wikkid/formatter/textileformatter.py'
353--- wikkid/formatter/textileformatter.py 1970-01-01 00:00:00 +0000
354+++ wikkid/formatter/textileformatter.py 2010-11-12 09:13:57 +0000
355@@ -0,0 +1,26 @@
356+# -*- coding: utf-8 -*-
357+#
358+# Copyright (C) 2010 Wikkid Developers.
359+#
360+# This software is licensed under the GNU Affero General Public License
361+# version 3 (see the file LICENSE).
362+
363+"""A text to html formatter using textile."""
364+
365+
366+from textile import textile
367+from zope.interface import implements
368+
369+from wikkid.interface.formatter import ITextFormatter
370+
371+
372+class TextileFormatter(object):
373+ """Format source files as HTML using textile."""
374+
375+ implements(ITextFormatter)
376+
377+ def format(self, filename, text):
378+ """Format the text.
379+ """
380+ return textile(text)
381+
382
383=== added file 'wikkid/skin/default/missing-directory.html'
384--- wikkid/skin/default/missing-directory.html 1970-01-01 00:00:00 +0000
385+++ wikkid/skin/default/missing-directory.html 2010-11-12 09:13:57 +0000
386@@ -0,0 +1,19 @@
387+{% extends "base.html" %}
388+{% block title %}{{ view.title }}{% endblock %}
389+{% block content %}
390+
391+<p>The directory you are looking for does not exist.<p>
392+
393+<p><a href="{{ canonical_url(context.root_resource, 'listing') }}">
394+ Browse the wiki content
395+</a></p>
396+
397+{% endblock %}
398+{% block commands %}
399+{% if view.user %}
400+<a href="{{ canonical_url(context, 'edit') }}">Edit</a>
401+{% endif %}
402+{% if context.get_dir_name %}
403+<a class="command" href="{{ canonical_url(context.root_resource, 'listing') }}">Browse</a>
404+{% endif %}
405+{% endblock %}
406
407=== modified file 'wikkid/skin/loader.py'
408--- wikkid/skin/loader.py 2010-05-12 10:39:13 +0000
409+++ wikkid/skin/loader.py 2010-11-12 09:13:57 +0000
410@@ -1,3 +1,4 @@
411+# -*- coding: utf-8 -*-
412 #
413 # Copyright (C) 2010 Wikkid Developers.
414 #
415@@ -36,6 +37,7 @@
416 'edit_page': self.env.get_template('edit.html'),
417 'view_directory': self.env.get_template('directory_listing.html'),
418 'missing': self.env.get_template('missing-page.html'),
419+ 'missing-dir' : self.env.get_template('missing-directory.html')
420 }
421 module_location = urlutils.dirname(__file__)
422 self.dir_name = urlutils.joinpath(module_location, skin_name)
423
424=== modified file 'wikkid/tests/formatters/__init__.py'
425--- wikkid/tests/formatters/__init__.py 2010-05-16 02:40:12 +0000
426+++ wikkid/tests/formatters/__init__.py 2010-11-12 09:13:57 +0000
427@@ -15,6 +15,8 @@
428 'registry',
429 'rest',
430 'creole',
431+ 'markdown',
432+ 'textile',
433 ]
434 module_names = ['wikkid.tests.formatters.test_' + name for name in names]
435 loader = unittest.TestLoader()
436
437=== added file 'wikkid/tests/formatters/test_markdown.py'
438--- wikkid/tests/formatters/test_markdown.py 1970-01-01 00:00:00 +0000
439+++ wikkid/tests/formatters/test_markdown.py 2010-11-12 09:13:57 +0000
440@@ -0,0 +1,166 @@
441+# -*- coding: utf-8 -*-
442+# Copyright (C) 2010 Wikkid Developers.
443+#
444+# This software is licensed under the GNU Affero General Public License
445+# version 3 (see the file LICENSE).
446+
447+"""Tests for the wikkid.formatter.markdownformatter module."""
448+
449+from textwrap import dedent
450+from BeautifulSoup import BeautifulSoup
451+
452+from wikkid.formatter.markdownformatter import MarkdownFormatter
453+from wikkid.tests import TestCase
454+
455+
456+class TestCreoleFormatter(TestCase):
457+ """Tests for the creole formatter."""
458+
459+ def setUp(self):
460+ TestCase.setUp(self)
461+ self.formatter = MarkdownFormatter()
462+
463+ def test_simple_headings(self):
464+ # A simple heading and a paragraph.
465+ text = dedent("""\
466+ Heading 1
467+ =========
468+
469+ Heading 2
470+ ---------
471+
472+ Simple sentence.""")
473+ result = self.formatter.format('filename', text)
474+ soup = BeautifulSoup(result)
475+ self.assertEqual('Heading 1', soup.h1.string)
476+ self.assertEqual('Heading 2', soup.h2.string)
477+ # we don't care about whitespace on <p> tags, and markdown inserts
478+ # newlines.
479+ self.assertEqual('Simple sentence.', soup.p.string.strip())
480+
481+ def test_detailed_headings(self):
482+ text = dedent("""\
483+ # Heading 1
484+
485+ ## Heading 2
486+
487+ ### Heading 3
488+
489+ #### Heading 4
490+
491+ ##### Heading 5
492+
493+ ###### Heading 6""")
494+ result = self.formatter.format('filename', text)
495+ soup = BeautifulSoup(result)
496+ self.assertEqual('Heading 1', soup.h1.string)
497+ self.assertEqual('Heading 2', soup.h2.string)
498+ self.assertEqual('Heading 3', soup.h3.string)
499+ self.assertEqual('Heading 4', soup.h4.string)
500+ self.assertEqual('Heading 5', soup.h5.string)
501+ self.assertEqual('Heading 6', soup.h6.string)
502+
503+
504+ def test_inline_link(self):
505+ # A paragraph containing a wiki word.
506+ text = "A link to the [FrontPage](http://127.0.0.1) helps."
507+ result = self.formatter.format('filename', text)
508+ soup = BeautifulSoup(result)
509+ self.assertEqual('http://127.0.0.1', soup.a['href'])
510+
511+ def test_reference_link(self):
512+ # A paragraph containing a wiki word.
513+ text = dedent("""\
514+ This is [an example][id] reference-style link.
515+
516+ [id]: http://127.0.0.1
517+ """)
518+ result = self.formatter.format('filename', text)
519+ soup = BeautifulSoup(result)
520+ self.assertEqual('http://127.0.0.1', soup.a['href'])
521+
522+ def test_emphasis(self):
523+ texts = ('We can have *emphasis* and **strong** as well!',
524+ 'We can have _emphasis_ and __strong__ as well!')
525+ for text in texts:
526+ result = self.formatter.format('filename', text)
527+ soup = BeautifulSoup(result)
528+ self.assertEqual('emphasis', soup.em.string)
529+ self.assertEqual('strong', soup.strong.string)
530+
531+ def test_blockquote(self):
532+ text = dedent('''\
533+ > This is a block quoted paragraph
534+ > that spans multiple lines.
535+ ''')
536+ result = self.formatter.format('filename', text)
537+ soup = BeautifulSoup(result)
538+ self.assertTrue(soup.blockquote is not None)
539+
540+ def test_lists(self):
541+ text = dedent('''\
542+ Some Text.
543+
544+ * UL 1
545+ * UL 2
546+ * UL 3
547+
548+ Some More Text!
549+
550+ 1. OL 1
551+ 2. OL 2
552+ 7. OL 3
553+ ''')
554+ result = self.formatter.format('filename', text)
555+ soup = BeautifulSoup(result)
556+
557+ self.assertTrue(soup.ul)
558+ ulNodes = soup.ul.findAll('li')
559+ for i in range(3):
560+ self.assertEqual('UL %d' % (i+1), ulNodes[i].string.strip())
561+
562+ self.assertTrue(soup.ol)
563+ olNodes = soup.ol.findAll('li')
564+ for i in range(3):
565+ self.assertEqual('OL %d' % (i+1), olNodes[i].string.strip())
566+
567+ def test_code_blocks(self):
568+ text = dedent('''\
569+ Some Normal Text.
570+
571+ Some Code inside pre tags
572+
573+ More Normal text.
574+ ''')
575+ result = self.formatter.format('filename', text)
576+ soup = BeautifulSoup(result)
577+ self.assertEqual(soup.pre.code.string.strip(), 'Some Code inside pre tags')
578+
579+ def test_hr(self):
580+ # test different HR types:
581+ texts = ('* * *',
582+ '***',
583+ '*********',
584+ '- - - -',
585+ '------------------')
586+ for text in texts:
587+ result = self.formatter.format('filename', text)
588+ soup = BeautifulSoup(result)
589+ self.assertTrue(soup.hr is not None)
590+
591+ def test_code(self):
592+ text = 'use the `printf()` function'
593+ result = self.formatter.format('filename', text)
594+ soup = BeautifulSoup(result)
595+ self.assertEqual('printf()', soup.code.string)
596+
597+ def test_unicode(self):
598+ # I have no idea what this says. Taken from:
599+ # http://www.humancomp.org/unichtm/calblur8.htm
600+ # It's simplified chinese, in utf-8, apparently
601+ text = u'个专为语文教学而设计的电脑软件'
602+ result = self.formatter.format('format', text)
603+ soup = BeautifulSoup(result)
604+ self.assertEqual(soup.p.string.strip(), text)
605+
606+
607
608=== added file 'wikkid/tests/formatters/test_textile.py'
609--- wikkid/tests/formatters/test_textile.py 1970-01-01 00:00:00 +0000
610+++ wikkid/tests/formatters/test_textile.py 2010-11-12 09:13:57 +0000
611@@ -0,0 +1,121 @@
612+# -*- coding: utf-8 -*-
613+# Copyright (C) 2010 Wikkid Developers.
614+#
615+# This software is licensed under the GNU Affero General Public License
616+# version 3 (see the file LICENSE).
617+
618+"""Tests for the wikkid.formatter.markdownformatter module."""
619+
620+from textwrap import dedent
621+from BeautifulSoup import BeautifulSoup
622+
623+from wikkid.formatter.textileformatter import TextileFormatter
624+from wikkid.tests import TestCase
625+
626+
627+class TestTextileFormatter(TestCase):
628+ """Tests for the creole formatter."""
629+
630+ def setUp(self):
631+ TestCase.setUp(self)
632+ self.formatter = TextileFormatter()
633+
634+ def test_detailed_headings(self):
635+ text = dedent("""\
636+ h1. Heading 1
637+
638+ h2. Heading 2
639+
640+ h3. Heading 3
641+
642+ h4. Heading 4
643+
644+ h5. Heading 5
645+
646+ h6. Heading 6""")
647+ result = self.formatter.format('filename', text)
648+ soup = BeautifulSoup(result)
649+ self.assertEqual('Heading 1', soup.h1.string)
650+ self.assertEqual('Heading 2', soup.h2.string)
651+ self.assertEqual('Heading 3', soup.h3.string)
652+ self.assertEqual('Heading 4', soup.h4.string)
653+ self.assertEqual('Heading 5', soup.h5.string)
654+ self.assertEqual('Heading 6', soup.h6.string)
655+
656+
657+ def test_inline_link(self):
658+ # A paragraph containing a wiki word.
659+ text = 'A link to the "FrontPage":http://127.0.0.1 helps.'
660+ result = self.formatter.format('filename', text)
661+ soup = BeautifulSoup(result)
662+ self.assertEqual('http://127.0.0.1', soup.a['href'])
663+
664+ def test_emphasis(self):
665+ text = 'We can have _emphasis_ and *strong* as well!'
666+ result = self.formatter.format('filename', text)
667+ soup = BeautifulSoup(result)
668+ self.assertEqual('emphasis', soup.em.string)
669+ self.assertEqual('strong', soup.strong.string)
670+
671+ def test_blockquote(self):
672+ text = dedent('''\
673+ Some Text
674+
675+ bq. This is a block quoted paragraph
676+ that spans multiple lines.
677+
678+ Some more text
679+ ''')
680+ result = self.formatter.format('filename', text)
681+ soup = BeautifulSoup(result)
682+ self.assertTrue(soup.blockquote is not None)
683+
684+ def test_lists(self):
685+ text = dedent('''\
686+ Some Text.
687+
688+ * UL 1
689+ * UL 2
690+ * UL 3
691+
692+ Some More Text!
693+
694+ # OL 1
695+ # OL 2
696+ # OL 3
697+ ''')
698+ result = self.formatter.format('filename', text)
699+ soup = BeautifulSoup(result)
700+
701+ self.assertTrue(soup.ul)
702+ ulNodes = soup.ul.findAll('li')
703+ for i in range(3):
704+ self.assertEqual('UL %d' % (i+1), ulNodes[i].string.strip())
705+
706+ self.assertTrue(soup.ol)
707+ olNodes = soup.ol.findAll('li')
708+ for i in range(3):
709+ self.assertEqual('OL %d' % (i+1), olNodes[i].string.strip())
710+
711+ def test_code_blocks(self):
712+ text = dedent('''\
713+ Some Normal Text.
714+
715+ @Some Code inside pre tags@
716+
717+ More Normal text.
718+ ''')
719+ result = self.formatter.format('filename', text)
720+ soup = BeautifulSoup(result)
721+ self.assertEqual(soup.code.string.strip(), 'Some Code inside pre tags')
722+
723+ def test_unicode(self):
724+ # I have no idea what this says. Taken from:
725+ # http://www.humancomp.org/unichtm/calblur8.htm
726+ # It's simplified chinese, in utf-8, apparently
727+ text = u'个专为语文教学而设计的电脑软件'
728+ result = self.formatter.format('format', text)
729+ soup = BeautifulSoup(result)
730+ self.assertEqual(soup.p.string.strip(), text)
731+
732+
733
734=== modified file 'wikkid/view/base.py'
735--- wikkid/view/base.py 2010-06-23 10:57:14 +0000
736+++ wikkid/view/base.py 2010-11-12 09:13:57 +0000
737@@ -45,7 +45,8 @@
738
739 __metaclass__ = BaseViewMetaClass
740
741- def __init__(self, context, request):
742+ def __init__(self, context, request, execution_context):
743+ self.execution_context = execution_context
744 self.context = context
745 self.request = request
746 if request is not None:
747
748=== modified file 'wikkid/view/missing.py'
749--- wikkid/view/missing.py 2010-05-19 10:00:43 +0000
750+++ wikkid/view/missing.py 2010-11-12 09:13:57 +0000
751@@ -1,3 +1,4 @@
752+# -*- coding: utf-8 -*-
753 #
754 # Copyright (C) 2010 Wikkid Developers
755 #
756@@ -24,6 +25,16 @@
757 def content(self):
758 '%s Not found' % self.path
759
760+class MissingDirectory(BaseView):
761+ #"""A wiki Directory that does not exist."""
762+
763+ for_interface = IMissingResource
764+ name = 'listing'
765+ template = 'missing-dir'
766+
767+ @property
768+ def content(self):
769+ '%s Not found' % self.path
770
771 class NewWikiPage(BaseEditView):
772 """Show the edit page with no existing content."""
773
774=== modified file 'wikkid/view/wiki.py'
775--- wikkid/view/wiki.py 2010-06-17 10:12:31 +0000
776+++ wikkid/view/wiki.py 2010-11-12 09:13:57 +0000
777@@ -1,3 +1,4 @@
778+# -*- coding: utf-8 -*-
779 #
780 # Copyright (C) 2010 Wikkid Developers
781 #
782@@ -11,6 +12,7 @@
783 from wikkid.formatter.registry import get_wiki_formatter
784 from wikkid.interface.resource import IWikiTextFile
785 from wikkid.view.base import BaseView
786+from wikkid.context import ExecutionContext
787
788
789 class WikiPage(BaseView):
790@@ -25,9 +27,9 @@
791 def content(self):
792 bytes = self.context.get_bytes()
793 # Check the first line of the content to see if it specifies a
794- # formatter. The default is currently ReST, but we should have it
795- # configurable shortly.
796- content, formatter = get_wiki_formatter(bytes, 'rest')
797+ # formatter. We get the default formatter from the execution context:
798+ content, formatter = get_wiki_formatter(bytes,
799+ self.execution_context.get('default_formatter','rest'))
800 return formatter.format(self.context.base_name, content)
801
802 def _render(self, skin):

Subscribers

People subscribed via source and target branches