changeset 82:9afe4a1dbd1e

Refactoring of core wiki classes: - Use proper classes instead of dictionaries more often. - Simplified `Page`'s public API. - Page meta property values are now always stored in an array, even if there's only one occurence for the given key. - Updated unit-tests.
author Ludovic Chabant <ludovic@chabant.com>
date Tue, 19 Mar 2013 19:54:11 -0700
parents 05d0a7cd85e8
children 65f83a9b42f1
files tests/mock.py tests/test_page.py wikked/auth.py wikked/db.py wikked/formatter.py wikked/fs.py wikked/indexer.py wikked/page.py wikked/views.py wikked/wiki.py
diffstat 10 files changed, 240 insertions(+), 193 deletions(-) [+]
line wrap: on
line diff
--- a/tests/mock.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/tests/mock.py	Tue Mar 19 19:54:11 2013 -0700
@@ -5,7 +5,7 @@
 import logging
 import StringIO
 from wikked.page import Page
-from wikked.fs import PageNotFoundError
+from wikked.fs import PageInfo, PageNotFoundError
 from wikked.db import Database
 from wikked.indexer import WikiIndex
 from wikked.scm import SourceControl
@@ -132,12 +132,9 @@
     def _getPageInfo(self, node, with_content=False):
         path_split = os.path.splitext(node['path'])
         url = self.slugify(path_split[0])
-        info = {
-            'url': url,
-            'path': node['path']
-            }
+        info = PageInfo(url, node['path'])
         if with_content:
-            info['content'] = node['content']
+            info.content = node['content']
         return info
 
     def _getNode(self, path):
--- a/tests/test_page.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/tests/test_page.py	Tue Mar 19 19:54:11 2013 -0700
@@ -16,11 +16,11 @@
         page = Page(self.wiki, 'foo')
         self.assertEqual('foo', page.url)
         self.assertEqual('A test page.', page.raw_text)
-        self.assertEqual('A test page.', page.formatted_text)
+        self.assertEqual('A test page.', page._getFormattedText())
         self.assertEqual('foo', page.title)
         self.assertEqual('A test page.', page.text)
-        self.assertEqual({}, page.local_meta)
-        self.assertEqual([], page.local_links)
+        self.assertEqual({}, page._getLocalMeta())
+        self.assertEqual([], page._getLocalLinks())
 
     def testPageMeta(self):
         self.wiki = self._getWikiFromStructure({
@@ -29,11 +29,11 @@
         page = Page(self.wiki, 'foo')
         self.assertEqual('foo', page.url)
         self.assertEqual("A page with simple meta.\n{{bar: baz}}\n{{is_test: }}", page.raw_text)
-        self.assertEqual('A page with simple meta.\n\n', page.formatted_text)
+        self.assertEqual('A page with simple meta.\n\n', page._getFormattedText())
         self.assertEqual('foo', page.title)
         self.assertEqual('A page with simple meta.\n\n', page.text)
-        self.assertEqual({'bar': 'baz', 'is_test': True}, page.local_meta)
-        self.assertEqual([], page.local_links)
+        self.assertEqual({'bar': ['baz'], 'is_test': True}, page._getLocalMeta())
+        self.assertEqual([], page._getLocalLinks())
 
     def testPageTitleMeta(self):
         self.wiki = self._getWikiFromStructure({
@@ -42,11 +42,11 @@
         page = Page(self.wiki, 'test_title')
         self.assertEqual('test_title', page.url)
         self.assertEqual("A page with a custom title.\n{{title: TEST-TITLE}}", page.raw_text)
-        self.assertEqual('A page with a custom title.\n', page.formatted_text)
+        self.assertEqual('A page with a custom title.\n', page._getFormattedText())
         self.assertEqual('TEST-TITLE', page.title)
         self.assertEqual('A page with a custom title.\n', page.text)
-        self.assertEqual({'title': 'TEST-TITLE'}, page.local_meta)
-        self.assertEqual([], page.local_links)
+        self.assertEqual({'title': ['TEST-TITLE']}, page._getLocalMeta())
+        self.assertEqual([], page._getLocalLinks())
 
     def testPageOutLinks(self):
         self.wiki = self._getWikiFromStructure({
@@ -61,8 +61,8 @@
                 "Follow a link to the %s. Or to %s." % (
                     format_link('Sandbox', 'sandbox'),
                     format_link('this page', 'other-sandbox', True)),
-                page.formatted_text)
-        self.assertEqual(set(['sandbox', 'other-sandbox']), set(page.local_links))
+                page.text)
+        self.assertEqual(set(['sandbox', 'other-sandbox']), set(page._getLocalLinks()))
 
     def testPageRelativeOutLinks(self):
         self.wiki = self._getWikiFromStructure({
@@ -74,20 +74,20 @@
                 }
             })
         first = Page(self.wiki, 'first')
-        self.assertEqual(['first-sibling'], first.local_links)
+        self.assertEqual(['first-sibling'], first._getLocalLinks())
         first2 = Page(self.wiki, 'first-sibling')
-        self.assertEqual(['first', 'sub_dir/second'], first2.local_links)
+        self.assertEqual(['first', 'sub_dir/second'], first2._getLocalLinks())
         second = Page(self.wiki, 'sub_dir/second')
-        self.assertEqual(['first', 'sub_dir/second-sibling'], second.local_links)
+        self.assertEqual(['first', 'sub_dir/second-sibling'], second._getLocalLinks())
         second2 = Page(self.wiki, 'sub_dir/second-sibling')
-        self.assertEqual(['sub_dir/second'], second2.local_links)
+        self.assertEqual(['sub_dir/second'], second2._getLocalLinks())
 
     def testGenericUrl(self):
         self.wiki = self._getWikiFromStructure({
             'foo.txt': "URL: [[url:/blah/boo/image.png]]"
             })
         foo = Page(self.wiki, 'foo')
-        self.assertEqual("URL: /files/blah/boo/image.png", foo.formatted_text)
+        self.assertEqual("URL: /files/blah/boo/image.png", foo._getFormattedText())
 
     def testPageInclude(self):
         self.wiki = self._getWikiFromStructure({
@@ -95,10 +95,10 @@
             'Trans Desc.txt': "BLAH\n"
             })
         foo = Page(self.wiki, 'foo')
-        self.assertEqual({'include': 'trans-desc'}, foo.local_meta)
+        self.assertEqual({'include': ['trans-desc']}, foo._getLocalMeta())
         self.assertEqual(
                 "A test page.\n%s" % format_include('trans-desc'),
-                foo.formatted_text)
+                foo._getFormattedText())
         self.assertEqual("A test page.\nBLAH\n\n", foo.text)
 
     def testPageIncludeWithMeta(self):
@@ -107,16 +107,16 @@
             'Trans Desc.txt': "BLAH: [[Somewhere]]\n{{bar: 42}}\n{{__secret: love}}\n{{+given: hope}}"
             })
         foo = Page(self.wiki, 'foo')
-        self.assertEqual([], foo.local_links)
-        self.assertEqual({'include': 'trans-desc'}, foo.local_meta)
+        self.assertEqual([], foo._getLocalLinks())
+        self.assertEqual({'include': ['trans-desc']}, foo._getLocalMeta())
         self.assertEqual(
                 "A test page.\n%s" % format_include('trans-desc'),
-                foo.formatted_text)
+                foo._getFormattedText())
         self.assertEqual(
                 "A test page.\nBLAH: %s\n\n\n\n" % format_link('Somewhere', 'somewhere', True),
                 foo.text)
-        self.assertEqual(['somewhere'], foo.all_links)
-        self.assertEqual({'bar': '42', 'given': 'hope', 'include': 'trans-desc'}, foo.all_meta)
+        self.assertEqual(['somewhere'], foo.links)
+        self.assertEqual({'bar': ['42'], 'given': ['hope'], 'include': ['trans-desc']}, foo.meta)
 
     def testPageIncludeWithTemplating(self):
         self.wiki = self._getWikiFromStructure({
@@ -126,7 +126,7 @@
         foo = Page(self.wiki, 'foo')
         self.assertEqual(
             "A test page.\n%s" % format_include('greeting', 'name=Dave|what=drink'),
-            foo.formatted_text)
+            foo._getFormattedText())
         self.assertEqual("A test page.\nHello Dave, would you like a drink?\n", foo.text)
 
     def testGivenOnlyInclude(self):
@@ -138,7 +138,7 @@
         tpl1 = Page(self.wiki, 'template-1')
         self.assertEqual(
                 "TEMPLATE!\n%s" % format_include('template-2', mod='+'),
-                tpl1.formatted_text)
+                tpl1._getFormattedText())
         self.assertEqual("TEMPLATE!\n\n", tpl1.text)
         base = Page(self.wiki, 'base')
         self.assertEqual("The base page.\nTEMPLATE!\nMORE TEMPLATE!\n\n", base.text)
@@ -155,16 +155,16 @@
             })
         base = Page(self.wiki, 'base')
         self.assertEqual({
-            'foo': 'bar', 
+            'foo': ['bar'], 
             'category': ['blah', 'yolo']
-            }, base.all_meta)
+            }, base.meta)
         tpl1 = Page(self.wiki, 'template-1')
         self.assertEqual({
-            'foo': 'bar',
-            '+category': 'blah',
-            '+include': 'template-2',
-            '__secret': 'ssh'
-            }, tpl1.all_meta)
+            'foo': ['bar'],
+            '+category': ['blah'],
+            '+include': ['template-2'],
+            '__secret': ['ssh']
+            }, tpl1.meta)
         self.assertEqual(
                 "\n\n%s\n\n" % format_include('template-2'),
                 tpl1.text)
--- a/wikked/auth.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/auth.py	Tue Mar 19 19:54:11 2013 -0700
@@ -56,8 +56,8 @@
         if (self._permissions[meta_name] is not None and
                 username not in self._permissions[meta_name]):
             return False
-        if meta_name in page.all_meta:
-            allowed = [r.strip() for r in re.split(r'[ ,;]', page.all_meta[meta_name])]
+        if meta_name in page.meta:
+            allowed = [r.strip() for r in re.split(r'[ ,;]', page.meta[meta_name][0])]
             if username is None:
                 return 'anonymous' in allowed
             else:
--- a/wikked/db.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/db.py	Tue Mar 19 19:54:11 2013 -0700
@@ -64,6 +64,19 @@
         raise NotImplementedError()
 
 
+
+class SQLitePageInfo(object):
+    def __init__(self, row):
+        self.url = row['url']
+        self.path = row['path']
+        self.time = row['time']
+        self.title = row['title']
+        self.raw_text = row['raw_text']
+        self.formatted_text = row['formatted_text']
+        self.links = []
+        self.meta = {}
+
+
 class SQLiteDatabase(Database):
     """ A database cache based on SQLite.
     """
@@ -286,10 +299,10 @@
             (time, url, path, title, raw_text, formatted_text)
             VALUES (?, ?, ?, ?, ?, ?)''',
             (now, page.url, page.path, page.title,
-                page.raw_text, page.formatted_text))
+                page.raw_text, page._getFormattedText()))
         page_id = c.lastrowid
 
-        for name, value in page.local_meta.iteritems():
+        for name, value in page._getLocalMeta().iteritems():
             if isinstance(value, bool):
                 value = ""
             if isinstance(value, types.StringTypes):
@@ -302,7 +315,7 @@
                         (page_id, name, value) VALUES (?, ?, ?)''',
                         (page_id, name, v))
 
-        for link_url in page.local_links:
+        for link_url in page._getLocalLinks():
             c.execute('''INSERT INTO links
                 (source, target) VALUES (?, ?)''',
                 (page.url, link_url))
@@ -317,21 +330,12 @@
         c.execute('''DELETE FROM links WHERE source=?''', (row['url'],))
 
     def _getPage(self, row, c):
-        db_page = {
-            'url': row['url'],
-            'path': row['path'],
-            'time': row['time'],
-            'title': row['title'],
-            'content': row['raw_text'],
-            'formatted': row['formatted_text'],
-            'links': [],
-            'meta': {}
-            }
+        db_page = SQLitePageInfo(row)
 
         c.execute('''SELECT target FROM links
             WHERE source=?''', (row['url'],))
         for r in c.fetchall():
-            db_page['links'].append(r['target'])
+            db_page.links.append(r['target'])
 
         c.execute('''SELECT page_id, name, value
             FROM meta WHERE page_id=?''', (row['id'],))
@@ -340,11 +344,9 @@
             if value == '':
                 value = True
             name = r['name']
-            if name not in db_page['meta']:
-                db_page['meta'][name] = value
-            elif db_page['meta'][name] is list:
-                db_page['meta'][name].append(value)
+            if name not in db_page.meta:
+                db_page.meta[name] = [value]
             else:
-                db_page['meta'][name] = [db_page['meta'][name], value]
+                db_page.meta[name].append(value)
 
         return db_page
--- a/wikked/formatter.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/formatter.py	Tue Mar 19 19:54:11 2013 -0700
@@ -1,7 +1,6 @@
 import os
 import os.path
 import re
-import types
 import pystache
 
 
@@ -132,9 +131,7 @@
             # Then, set the value on the meta dictionary, or add it to
             # other existing meta values with the same key.
             if meta_name not in ctx.meta:
-                ctx.meta[meta_name] = coerced_meta_value
-            elif isinstance(ctx.meta[meta_name], types.StringTypes):
-                ctx.meta[meta_name] = [ctx.meta[meta_name], coerced_meta_value]
+                ctx.meta[meta_name] = [coerced_meta_value]
             else:
                 ctx.meta[meta_name].append(coerced_meta_value)
 
@@ -235,12 +232,15 @@
     def _formatWikiLink(self, ctx, display, url):
         abs_url = ctx.getAbsoluteUrl(url)
         ctx.out_links.append(abs_url)
+        return '<a class="wiki-link" data-wiki-url="%s">%s</a>' % (abs_url, display)
 
-        css_class = 'wiki-link'
-        if not self.wiki.pageExists(abs_url, from_db=False):
-            css_class += ' missing'
-        return '<a class="%s" data-wiki-url="%s">%s</a>' % (css_class, abs_url, display)
-
+    @staticmethod
+    def parseWikiLinks(text):
+        urls = []
+        pattern = r"<a class=\"[^\"]*\" data-wiki-url=\"(?P<url>[^\"]+)\">"
+        for m in re.finditer(pattern, text):
+            urls.append(str(m.group('url')))
+        return urls
 
 class ResolveContext(object):
     """ The context for resolving page queries. """
@@ -266,8 +266,8 @@
         self.meta = {}
         self.out_links = []
         if page:
-            self.meta = dict(page.local_meta)
-            self.out_links = list(page.local_links)
+            self.meta = dict(page._getLocalMeta())
+            self.out_links = list(page._getLocalLinks())
 
     def add(self, other):
         self.out_links += other.out_links
@@ -280,10 +280,8 @@
 
             if key not in self.meta:
                 self.meta[key] = val
-            elif self.meta[key] is list:
+            else:
                 self.meta[key].append(val)
-            else:
-                self.meta[key] = [self.meta[key], val]
 
 
 class PageResolver(object):
@@ -308,7 +306,23 @@
         return self.page.wiki
 
     def run(self):
-        def repl(m):
+        if not self.ctx:
+            self.ctx = ResolveContext(self.page.url)
+
+        # Resolve link states.
+        def repl1(m):
+            url = str(m.group('url'))
+            if self.wiki.pageExists(url):
+                return str(m.group())
+            return '<a class="wiki-link missing" data-wiki-url="%s">' % url
+        
+        formatted_text = re.sub(
+                r'<a class="wiki-link" data-wiki-url="(?P<url>[^"]+)">',
+                repl1,
+                self.page._getFormattedText())
+
+        # Resolve queries, includes, etc.
+        def repl2(m):
             meta_name = str(m.group('name'))
             meta_value = str(m.group('value'))
             meta_opts = {}
@@ -326,16 +340,15 @@
                 return self._runInclude(meta_opts, meta_value)
             return ''
 
-        if not self.ctx:
-            self.ctx = ResolveContext(self.page.url)
-
         self.output = ResolveOutput(self.page)
-        self.output.text = re.sub(r'^<div class="wiki-(?P<name>[a-z]+)"'
+        self.output.text = re.sub(
+                r'^<div class="wiki-(?P<name>[a-z]+)"'
                 r'(?P<opts>( data-wiki-([a-z]+)="([^"]+)")*)'
                 r'>(?P<value>.*)</div>$',
-                repl,
-                self.page.formatted_text,
+                repl2,
+                formatted_text,
                 flags=re.MULTILINE)
+
         return self.output
 
     def _runInclude(self, opts, args):
@@ -412,7 +425,7 @@
                     'url': p.url,
                     'title': p.title
                     }
-            tokens.update(p.local_meta)
+            tokens.update(p._getLocalMeta())
             text += self._renderTemplate(
                     self._valueOrPageText(parameters['__item']),
                     tokens)
@@ -434,7 +447,7 @@
             # meta properties.
             meta_keys.append('+' + name)
         for key in meta_keys:
-            actual = page.local_meta.get(key)
+            actual = page._getLocalMeta().get(key)
             if (actual is not None and
                     ((type(actual) is list and value in actual) or
                     (actual == value))):
@@ -450,7 +463,7 @@
         else:
             include_meta_keys.append('__include')
         for key in include_meta_keys:
-            i = page.local_meta.get(key)
+            i = page._getLocalMeta().get(key)
             if i is not None:
                 if (type(i) is list):
                     include_meta_values += i
--- a/wikked/fs.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/fs.py	Tue Mar 19 19:54:11 2013 -0700
@@ -13,6 +13,17 @@
     pass
 
 
+class PageInfo(object):
+    def __init__(self, url, path, content=None):
+        self.url = url
+        self.path = path
+        self.content = content
+
+    @property
+    def has_content(self):
+        return self.content is not None
+
+
 class FileSystem(object):
     """ A class responsible for mapping page URLs to
         file-system paths, and for scanning the file-system
@@ -59,11 +70,7 @@
         path = self.getPhysicalPagePath(url)
         with codecs.open(path, 'r', encoding='utf-8') as f:
             content = f.read()
-        return {
-                'url': url,
-                'path': path,
-                'content': content
-                }
+        return PageInfo(url, path, content)
 
     def setPage(self, path, content):
         with codecs.open(path, 'w', encoding='utf-8') as f:
@@ -98,10 +105,7 @@
             if i > 0:
                 url += '/'
             url += self.slugify(part)
-        return {
-                'url': url,
-                'path': path
-                }
+        return PageInfo(url, path, None)
 
     def _getPhysicalPath(self, url, is_file):
         if string.find(url, '..') >= 0:
--- a/wikked/indexer.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/indexer.py	Tue Mar 19 19:54:11 2013 -0700
@@ -44,7 +44,6 @@
         self.ix = create_in(self.store_dir, schema=self._getSchema())
         writer = self.ix.writer()
         for page in pages:
-            page._ensureMeta()
             self._indexPage(writer, page)
         writer.commit()
 
@@ -72,9 +71,7 @@
                         to_reindex.add(indexed_path)
 
             for page in pages:
-                page._ensureMeta()
-                page_path = page._meta['path']
-                if page_path in to_reindex or page_path not in already_indexed:
+                if page.path in to_reindex or page.path not in already_indexed:
                     self._indexPage(writer, page)
 
             writer.commit()
@@ -116,8 +113,8 @@
             url=unicode(page.url),
             title=unicode(page.title),
             content=unicode(page.raw_text),
-            path=page._meta['path'],
-            time=os.path.getmtime(page._meta['path'])
+            path=page.path,
+            time=os.path.getmtime(page.path)
             )
 
     def _unindexPage(self, writer, url):
--- a/wikked/page.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/page.py	Tue Mar 19 19:54:11 2013 -0700
@@ -7,62 +7,59 @@
 from formatter import PageFormatter, FormattingContext, PageResolver, CircularIncludeError
 
 
+class PageData(object):
+    def __init__(self):
+        self.path = None
+        self.title = None
+        self.raw_text = None
+        self.formatted_text = None
+        self.text = None
+        self.local_meta = {}
+        self.local_links = []
+        self.ext_meta = {}
+        self.ext_links = []
+        self.has_extended_data = False
+
+
 class Page(object):
     """ A wiki page.
     """
     def __init__(self, wiki, url):
         self.wiki = wiki
         self.url = url
-        self._meta = None
-        self._ext_meta = None
+        self._data = None
 
     @property
     def path(self):
-        self._ensureMeta()
-        return self._meta['path']
+        self._ensureData()
+        return self._data.path
 
     @property
     def title(self):
-        self._ensureMeta()
-        return self._meta['title']
+        self._ensureData()
+        return self._data.title
 
     @property
     def raw_text(self):
-        self._ensureMeta()
-        return self._meta['content']
-
-    @property
-    def formatted_text(self):
-        self._ensureMeta()
-        return self._meta['formatted']
+        self._ensureData()
+        return self._data.raw_text
 
     @property
     def text(self):
-        self._ensureExtendedMeta()
-        return self._ext_meta['text']
-
-    @property
-    def local_meta(self):
-        self._ensureMeta()
-        return self._meta['meta']
+        self._ensureExtendedData()
+        return self._data.text
 
     @property
-    def local_links(self):
-        self._ensureMeta()
-        return self._meta['links']
+    def meta(self):
+        self._ensureExtendedData()
+        return self._data.ext_meta
 
     @property
-    def all_meta(self):
-        self._ensureExtendedMeta()
-        return self._ext_meta['meta']
+    def links(self):
+        self._ensureExtendedData()
+        return self._data.ext_links
 
-    @property
-    def all_links(self):
-        self._ensureExtendedMeta()
-        return self._ext_meta['links']
-
-    @property
-    def in_links(self):
+    def getIncomingLinks(self):
         return self.wiki.db.getLinksTo(self.url)
 
     def getHistory(self):
@@ -77,55 +74,71 @@
     def getDiff(self, rev1, rev2):
         return self.wiki.scm.diff(self.path, rev1, rev2)
 
-    def _ensureMeta(self):
-        if self._meta is not None:
+    def _getFormattedText(self):
+        self._ensureData()
+        return self._data.formatted_text
+
+    def _getLocalMeta(self):
+        self._ensureData()
+        return self._data.local_meta
+
+    def _getLocalLinks(self):
+        self._ensureData()
+        return self._data.local_links
+
+    def _ensureData(self):
+        if self._data is not None:
             return
 
-        self._meta = self._loadCachedMeta()
-        if self._meta is not None:
+        self._data = self._loadCachedData()
+        if self._data is not None:
             return
 
-        self._meta = self._loadOriginalMeta()
-        self._saveCachedMeta(self._meta)
+        self._data = self._loadOriginalData()
+        self._saveCachedData(self._data)
 
-    def _loadCachedMeta(self):
+    def _loadCachedData(self):
         return None
 
-    def _saveCachedMeta(self, meta):
+    def _saveCachedData(self, meta):
         pass
 
-    def _loadOriginalMeta(self):
+    def _loadOriginalData(self):
+        data = PageData()
+
         # Get info from the file-system.
-        meta = self.wiki.fs.getPage(self.url)
+        page_info = self.wiki.fs.getPage(self.url)
+        data.path = page_info.path
+        data.raw_text = page_info.content
 
         # Format the page and get the meta properties.
-        filename = os.path.basename(meta['path'])
+        filename = os.path.basename(data.path)
         filename_split = os.path.splitext(filename)
         extension = filename_split[1].lstrip('.')
         ctx = FormattingContext(self.url, extension, slugify=Page.title_to_url)
         f = PageFormatter(self.wiki)
-        meta['formatted'] = f.formatText(ctx, meta['content'])
-        meta['meta'] = ctx.meta
-        meta['links'] = ctx.out_links
+        data.formatted_text = f.formatText(ctx, data.raw_text)
+        data.local_meta = ctx.meta
+        data.local_links = ctx.out_links
 
         # Add some common meta.
-        meta['title'] = re.sub(r'\-', ' ', filename_split[0])
-        if 'title' in meta['meta']:
-            meta['title'] = meta['meta']['title']
+        data.title = re.sub(r'\-', ' ', filename_split[0])
+        if 'title' in data.local_meta:
+            data.title = data.local_meta['title'][0]
 
-        return meta
+        return data
 
-    def _ensureExtendedMeta(self):
-        if self._ext_meta is not None:
+    def _ensureExtendedData(self):
+        if self._data is not None and self._data.has_extended_data:
             return
 
+        self._ensureData()
         try:
             r = PageResolver(self)
             out = r.run()
-            self._ext_meta = {}
-            self._ext_meta['text'] = out.text
-            self._ext_meta['meta'] = out.meta
-            self._ext_meta['links'] = out.out_links
+            self._data.text = out.text
+            self._data.ext_meta = out.meta
+            self._data.ext_links = out.out_links
         except CircularIncludeError as cie:
             template_path = os.path.join(
                     os.path.dirname(__file__),
@@ -134,14 +147,10 @@
                     )
             with open(template_path, 'r') as f:
                 template = pystache.compile(f.read())
-            self._ext_meta = {
-                    'text': template({
-                        'message': str(cie),
-                        'url_trail': cie.url_trail
-                        }),
-                    'meta': {},
-                    'links': []
-                    }
+            self._data.text = template({
+                    'message': str(cie),
+                    'url_trail': cie.url_trail
+                    })
 
     @staticmethod
     def title_to_url(title):
@@ -174,7 +183,7 @@
             raise Exception("The wiki doesn't have a database.")
         self.auto_update = wiki.config.get('wiki', 'auto_update')
 
-    def _loadCachedMeta(self):
+    def _loadCachedData(self):
         if self.wiki.db is None:
             return None
         db_page = self.wiki.db.getPage(self.url)
@@ -182,21 +191,19 @@
             return None
         if self.auto_update:
             path_time = datetime.datetime.fromtimestamp(
-                os.path.getmtime(db_page['path']))
-            if path_time >= db_page['time']:
+                os.path.getmtime(db_page.path))
+            if path_time >= db_page.time:
                 return None
-        meta = {
-                'url': self.url,
-                'path': db_page['path'],
-                'content': db_page['content'],
-                'formatted': db_page['formatted'],
-                'meta': db_page['meta'],
-                'title': db_page['title'],
-                'links': db_page['links']
-                }
-        return meta
+        data = PageData()
+        data.path = db_page.path
+        data.title = db_page.title
+        data.raw_text = db_page.raw_text
+        data.formatted_text = db_page.formatted_text
+        data.local_meta = db_page.meta
+        data.local_links = db_page.links
+        return data
 
-    def _saveCachedMeta(self, meta):
+    def _saveCachedData(self, meta):
         if self.wiki.db is not None:
             self.wiki.logger.debug(
                 "Updated database cache for page '%s'." % self.url)
--- a/wikked/views.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/views.py	Tue Mar 19 19:54:11 2013 -0700
@@ -8,6 +8,7 @@
 from web import app, login_manager
 from wiki import Page
 from fs import PageNotFoundError
+from formatter import PageFormatter
 import scm
 
 
@@ -23,7 +24,6 @@
 def get_page_or_none(url):
     try:
         page = g.wiki.getPage(url)
-        page._ensureMeta()
         return page
     except PageNotFoundError:
         return None
@@ -50,9 +50,9 @@
 
 def get_page_meta(page, local_only=False):
     if local_only:
-        meta = dict(page.local_meta)
+        meta = dict(page._getLocalMeta())
     else:
-        meta = dict(page.all_meta)
+        meta = dict(page.meta)
     meta['title'] = page.title
     meta['url'] = page.url
     for name in COERCE_META:
@@ -78,11 +78,14 @@
                 f_info = g.wiki.fs.getPageInfo(f['path'])
                 if f_info is None:
                     continue
-                page = g.wiki.getPage(f_info['url'])
-                if not is_page_readable(page):
-                    continue
+                page = g.wiki.getPage(f_info.url)
+                try:
+                    if not is_page_readable(page):
+                        continue
+                except PageNotFoundError:
+                    pass
                 rev_data['pages'].append({
-                    'url': f_info['url'],
+                    'url': f_info.url,
                     'action': scm.ACTION_NAMES[f['action']]
                     })
             if len(rev_data['pages']) > 0:
@@ -189,7 +192,7 @@
 def api_get_outgoing_links(url):
     page = get_page_or_404(url, CHECK_FOR_READ)
     links = []
-    for link in page.out_links:
+    for link in page.links:
         other = get_page_or_none(link)
         if other is not None:
             links.append({
@@ -199,7 +202,7 @@
         else:
             links.append({'url': link, 'missing': True})
 
-    result = {'meta': get_page_meta(page, True), 'out_links': links}
+    result = {'meta': get_page_meta(page), 'out_links': links}
     return make_auth_response(result)
 
 
@@ -207,7 +210,7 @@
 def api_get_incoming_links(url):
     page = get_page_or_404(url, CHECK_FOR_READ)
     links = []
-    for link in page.in_links:
+    for link in page.getIncomingLinks():
         other = get_page_or_none(link)
         if other is not None and is_page_readable(other):
             links.append({
@@ -217,7 +220,7 @@
         else:
             links.append({'url': link, 'missing': True})
 
-    result = {'meta': get_page_meta(page, True), 'in_links': links}
+    result = {'meta': get_page_meta(page), 'in_links': links}
     return make_auth_response(result)
 
 
@@ -303,10 +306,34 @@
 
 @app.route('/api/orphans')
 def api_special_orphans():
+    run_queries = request.args.get('run_queries')
+
     orphans = []
-    for page in filter(is_page_readable, g.wiki.getPages()):
-        if len(page.in_links) == 0:
-            orphans.append({'path': page.url, 'meta': get_page_meta(page, True)})
+    pages_with_queries = []
+    for page in g.wiki.getPages():
+        try:
+            if not is_page_readable(page):
+                continue
+            if len(page.getIncomingLinks()) == 0:
+                orphans.append({'path': page.url, 'meta': get_page_meta(page)})
+        except Exception as e:
+            app.logger.error("Error while inspecting page: %s" % page.url)
+            app.logger.error("   %s" % e)
+            continue
+        if run_queries:
+            page_queries = page._getLocalMeta().get('query')
+            if page_queries is not None:
+                pages_with_queries.append(page)
+
+    if run_queries:
+        app.logger.debug("Running queries for %d pages." % len(pages_with_queries))
+        links_to_remove = set()
+        for page in pages_with_queries:
+            links = PageFormatter.parseWikiLinks(page.text)
+            links_to_remove |= set(links)
+        app.logger.debug( links_to_remove)
+        orphans = [o for o in orphans if o['path'] not in links_to_remove]
+        
     result = {'orphans': orphans}
     return make_auth_response(result)
 
@@ -324,7 +351,7 @@
     page = get_page_or_404(url, CHECK_FOR_READ)
     history = page.getHistory()
     hist_data = get_history_data(history)
-    result = {'url': url, 'meta': get_page_meta(page, True), 'history': hist_data}
+    result = {'url': url, 'meta': get_page_meta(page), 'history': hist_data}
     return make_auth_response(result)
 
 
--- a/wikked/wiki.py	Wed Feb 27 22:55:11 2013 -0800
+++ b/wikked/wiki.py	Tue Mar 19 19:54:11 2013 -0700
@@ -147,7 +147,7 @@
                 yield url
         else:
             for info in self.fs.getPageInfos(subdir):
-                yield info['url']
+                yield info.url
 
     def getPages(self, subdir=None, from_db=None, factory=None):
         """ Gets all the pages in the wiki, or in the given sub-directory.