Mercurial > wikked
changeset 11:aa6951805e1a
New features and bug fixes:
- Extracted navigation and footer parts into their own model/view.
- Added search.
- Better typography styles.
- Fixed some bugs in the Handlebars helpers.
author | Ludovic Chabant <ludovic@chabant.com> |
---|---|
date | Sat, 29 Dec 2012 18:21:44 -0800 |
parents | 6ac0b74a57f7 |
children | ff0058feccf5 |
files | wikked/indexer.py wikked/static/css/wikked.less wikked/static/js/wikked.js wikked/static/tpl/diff-page.html wikked/static/tpl/edit-page.html wikked/static/tpl/footer.html wikked/static/tpl/history-page.html wikked/static/tpl/inlinks-page.html wikked/static/tpl/nav.html wikked/static/tpl/read-page.html wikked/static/tpl/revision-page.html wikked/static/tpl/search-results.html wikked/templates/index.html wikked/views.py wikked/wiki.py |
diffstat | 15 files changed, 652 insertions(+), 379 deletions(-) [+] |
line wrap: on
line diff
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/indexer.py Sat Dec 29 18:21:44 2012 -0800 @@ -0,0 +1,98 @@ +import os +import os.path +import logging +from whoosh.index import create_in, open_dir +from whoosh.fields import Schema, ID, KEYWORD, TEXT, STORED +from whoosh.qparser import QueryParser + + +class WikiIndex(object): + def __init__(self, store_dir, logger=None): + self.store_dir = store_dir + self.logger = logger + if logger is None: + self.logger = logging.getLogger('wikked.index') + + def update(self, pages): + raise NotImplementedError() + + def search(self, query): + raise NotImplementedError() + + +class WhooshWikiIndex(WikiIndex): + def __init__(self, store_dir, logger=None): + WikiIndex.__init__(self, store_dir, logger) + if not os.path.isdir(store_dir): + os.makedirs(store_dir) + schema = Schema( + url=ID(stored=True), + title=TEXT(stored=True), + content=TEXT, + path=STORED, + time=STORED + ) + self.ix = create_in(store_dir, schema) + else: + self.ix = open_dir(store_dir) + + def update(self, pages): + to_reindex = set() + already_indexed = set() + + with self.ix.searcher() as searcher: + writer = self.ix.writer() + + for fields in searcher.all_stored_fields(): + indexed_url = fields['url'] + indexed_path = fields['path'] + indexed_time = fields['time'] + + if not os.path.isfile(indexed_path): + # File was deleted. + writer.delete_by_term('url', indexed_url) + else: + already_indexed.add(indexed_path) + if os.path.getmtime(indexed_path) > fields['time']: + # File as changed since last index. + writer.delete_by_term('url', indexed_url) + 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: + self._indexPage(writer, page) + + writer.commit() + + def search(self, query): + with self.ix.searcher() as searcher: + title_qp = QueryParser("title", self.ix.schema).parse(query) + content_qp = QueryParser("content", self.ix.schema).parse(query) + comp_query = title_qp | content_qp + results = searcher.search(comp_query) + + page_infos = [] + for hit in results: + page_info = { + 'title': hit['title'], + 'url': hit['url'] + } + page_info['title_highlights'] = hit.highlights('title') + with open(hit['path']) as f: + content = unicode(f.read()) + page_info['content_highlights'] = hit.highlights('content', text=content) + page_infos.append(page_info) + return page_infos + + def _indexPage(self, writer, page): + self.logger.debug("Indexing: %s" % page.url) + writer.add_document( + url=unicode(page.url), + title=unicode(page.title), + content=unicode(page.raw_text), + path=page._meta['path'], + time=os.path.getmtime(page._meta['path']) + ) +
--- a/wikked/static/css/wikked.less Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/css/wikked.less Sat Dec 29 18:21:44 2012 -0800 @@ -83,6 +83,9 @@ sup, sub { line-height: 0; } +li { + line-height: @baseLineHeight; +} // Global classes a.wiki-link { @@ -99,17 +102,16 @@ .decorator { text-transform: uppercase; font-weight: lighter; - font-size: 0.8em; + font-size: 0.7em; letter-spacing: 0.1em; color: @colorBlueDark; -} -.rev_id { - font-family: monospace; - font-size: 0.8em; - color: @colorCode; + .rev_id { + font-family: monospace; + color: @colorCode; + } } -.wrapper { +.container { nav, div.meta { color: @colorNavLink; font-size: @smallFontSize; @@ -141,6 +143,10 @@ } } +form.search { + display: inline-block; + margin: 0; +} form.page-edit { textarea { height: 10em;
--- a/wikked/static/js/wikked.js Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/js/wikked.js Sat Dec 29 18:21:44 2012 -0800 @@ -18,6 +18,7 @@ loadedTemplates: {}, get: function(name, callback) { if (name in this.loadedTemplates) { + console.log('Returning cached template "{0}".'.format(name)); callback(this.loadedTemplates[name]); } else { var $loader = this; @@ -37,6 +38,9 @@ * Handlebars helper: reverse iterator. */ Handlebars.registerHelper('eachr', function(context, options) { + if (context === undefined) { + return ''; + } data = undefined; if (options.data) { data = Handlebars.createFrame(options.data); @@ -62,6 +66,12 @@ } return options.inverse(this); }); +Handlebars.registerHelper('ifneq', function(context, options) { + if (context != options.hash.to) { + return options.fn(this); + } + return options.inverse(this); +}); //-------------------------------------------------------------// @@ -103,8 +113,46 @@ /** * Wiki page models. */ + var NavigationModel = Backbone.Model.extend({ + idAttribute: 'path', + defaults: function() { + return { + path: "main-page", + action: "read" + }; + }, + initialize: function() { + this.on('change:path', function(model, path) { + model._onChangePath(path); + }); + this._onChangePath(this.get('path')); + return this; + }, + _onChangePath: function(path) { + this.set('url_home', '/#/read/main-page'); + this.set('url_read', '/#/read/' + path); + this.set('url_edit', '/#/edit/' + path); + this.set('url_hist', '/#/changes/' + path); + this.set('url_search', '/search'); + } + }); + + var FooterModel = Backbone.Model.extend({ + defaults: function() { + return { + url_extras: [ { name: 'Home', url: '/' } ] + }; + }, + addExtraUrl: function(name, url, index) { + if (index === undefined) { + this.get('url_extras').push({ name: name, url: url }); + } else { + this.get('url_extras').splice(index, 0, { name: name, url: url }); + } + } + }); + var PageModel = Backbone.Model.extend({ - urlRoot: '/api/read/', idAttribute: 'path', defaults: function() { return { @@ -120,6 +168,7 @@ }); this._onChangePath(this.get('path')); this._onChangeText(''); + return this; }, url: function() { var base = _.result(this, 'urlRoot') || _.result(this.collection, 'url') || urlError(); @@ -137,14 +186,6 @@ return meta[key]; }, _onChangePath: function(path) { - this.set('url_home', '/#/read/main-page'); - this.set('url_read', '/#/read/' + path); - this.set('url_edit', '/#/edit/' + path); - this.set('url_hist', '/#/changes/' + path); - this.set('url_rev', '/#/revision/' + path); - this.set('url_diffc', '/#/diff/c/' + path); - this.set('url_diffr', '/#/diff/r/' + path); - this.set('url_inlinks', '/#/inlinks/' + path); }, _onChangeText: function(text) { this.set('content', new Handlebars.SafeString(text)); @@ -154,22 +195,59 @@ var PageStateModel = PageModel.extend({ urlRoot: '/api/state/' }); - - var PageSourceModel = PageModel.extend({ - urlRoot: '/api/raw/' + + var MasterPageModel = PageModel.extend({ + initialize: function() { + this.nav = new NavigationModel({ id: this.id }); + this.footer = new FooterModel(); + MasterPageModel.__super__.initialize.apply(this, arguments); + if (this.action !== undefined) { + this.nav.set('action', this.action); + this.footer.set('action', this.action); + } + return this; + }, + _onChangePath: function(path) { + MasterPageModel.__super__._onChangePath.apply(this, arguments); + this.nav.set('path', path); + } }); - var PageEditModel = PageModel.extend({ - urlRoot: '/api/edit/' + var PageReadModel = MasterPageModel.extend({ + urlRoot: '/api/read/', + action: 'read', + _onChangePath: function(path) { + PageReadModel.__super__._onChangePath.apply(this, arguments); + this.footer.addExtraUrl('Pages Linking Here', '/#/inlinks/' + path, 1); + this.footer.addExtraUrl('JSON', '/api/read/' + path); + } + }); + + var PageSourceModel = MasterPageModel.extend({ + urlRoot: '/api/raw/', + action: 'source' }); - var PageHistoryModel = PageModel.extend({ - urlRoot: '/api/history/' + var PageEditModel = MasterPageModel.extend({ + urlRoot: '/api/edit/', + action: 'edit' }); - var PageRevisionModel = PageModel.extend({ + var PageHistoryModel = MasterPageModel.extend({ + urlRoot: '/api/history/', + action: 'history', + _onChangePath: function(path) { + PageHistoryModel.__super__._onChangePath.apply(this, arguments); + this.set('url_rev', '/#/revision/' + path); + this.set('url_diffc', '/#/diff/c/' + path); + this.set('url_diffr', '/#/diff/r/' + path); + } + }); + + var PageRevisionModel = MasterPageModel.extend({ urlRoot: '/api/revision/', idAttribute: 'path_and_rev', + action: 'revision', defaults: function() { return { path: "main-page", @@ -177,6 +255,7 @@ }; }, initialize: function() { + PageRevisionModel.__super__.initialize.apply(this, arguments); this.on('change:path', function(model, path) { model._onChangePathOrRev(path, model.get('rev')); }); @@ -184,7 +263,7 @@ model._onChangePathOrRev(model.get('path'), rev); }); this._onChangePathOrRev(this.get('path'), this.get('rev')); - PageRevisionModel.__super__.initialize.call(this); + return this; }, _onChangePathOrRev: function(path, rev) { this.set('path_and_rev', path + '/' + rev); @@ -195,9 +274,10 @@ } }); - var PageDiffModel = PageModel.extend({ + var PageDiffModel = MasterPageModel.extend({ urlRoot: '/api/diff/', idAttribute: 'path_and_revs', + action: 'diff', defaults: function() { return { path: "main-page", @@ -206,6 +286,7 @@ }; }, initialize: function() { + PageDiffModel.__super__.initialize.apply(this, arguments); this.on('change:path', function(model, path) { model._onChangePathOrRevs(path, model.get('rev')); }); @@ -216,7 +297,7 @@ model._onChangePathOrRevs(model.get('path'), model.get('rev1'), rev2); }); this._onChangePathOrRevs(this.get('path'), this.get('rev1'), this.get('rev2')); - PageRevisionModel.__super__.initialize.call(this); + return this; }, _onChangePathOrRevs: function(path, rev1, rev2) { this.set('path_and_revs', path + '/' + rev1 + '/' + rev2); @@ -234,52 +315,130 @@ } }); - var IncomingLinksModel = PageModel.extend({ - urlRoot: '/api/inlinks/' + var IncomingLinksModel = MasterPageModel.extend({ + urlRoot: '/api/inlinks/', + action: 'inlinks' + }); + + var WikiSearchModel = MasterPageModel.extend({ + urlRoot: '/api/search/', + action: 'search', + title: function() { + return 'Search'; + }, + execute: function(query) { + var $model = this; + $.getJSON('/api/search', { q: query }) + .success(function (data) { + $model.set('hits', data.hits); + }) + .error(function() { + alert("Error searching..."); + }); + } }); /** * Wiki page views. */ - var PageReadView = Backbone.View.extend({ - tagName: "div", + var PageView = Backbone.View.extend({ + tagName: 'div', + className: 'wrapper', initialize: function() { + PageView.__super__.initialize.apply(this, arguments); var $view = this; - var model = new PageModel({ path: this.id }); - model.fetch({ - success: function(model, response, options) { - TemplateLoader.get('read-page', function(src) { - var template_data = model.toJSON(); - var template = Handlebars.compile(src); - $view.$el.html(template(template_data)); - $('a.wiki-link[data-wiki-url]').each(function(i, el) { - var jel = $(el); - if (jel.hasClass('missing')) - jel.attr('href', '/#/edit/' + jel.attr('data-wiki-url')); - else - jel.attr('href', '/#/read/' + jel.attr('data-wiki-url')); - }); - document.title = model.title(); - }); - }, - error: function(model, xhr, options) { - TemplateLoader.get('404', function(src) { - var template_data = model.toJSON(); - var template = Handlebars.compile(src); - $view.$el.html(template(template_data)); - }); + this.model.on("change", function() { $view.render(); }); + return this; + }, + render: function(view) { + if (this.templateName !== undefined) { + this.renderTemplate(this.templateName, this.renderCallback); + } + this.renderTitle(this.titleFormat); + return this; + }, + renderTemplate: function(tpl_name, callback) { + var $view = this; + TemplateLoader.get(tpl_name, function(src) { + var template = Handlebars.compile(src); + $view.$el.html(template($view.model.toJSON())); + if (callback !== undefined) { + callback.call($view, $view, $view.model); } }); + }, + renderTitle: function(formatter) { + var title = this.model.title(); + if (formatter !== undefined) { + title = formatter.call(this, title); + } + document.title = title; + } + }); + _.extend(PageView, Backbone.Events); + + var NavigationView = PageView.extend({ + templateName: 'nav', + initialize: function() { + NavigationView.__super__.initialize.apply(this, arguments); + this.render(); + return this; + }, + render: function() { + this.renderTemplate('nav'); + }, + postRender: function() { + var $view = this; + this.$('#search').submit(function() { + app.navigate('/search/' + $(this.q).val(), { trigger: true }); + return false; + }); + } + }); + + var FooterView = PageView.extend({ + templateName: 'footer', + initialize: function() { + FooterView.__super__.initialize.apply(this, arguments); + this.render(); + return this; + }, + render: function() { + this.renderTemplate('footer'); + }, + postRender: function() { + } + }); + + var MasterPageView = PageView.extend({ + initialize: function() { + MasterPageView.__super__.initialize.apply(this, arguments); + this.nav = new NavigationView({ model: this.model.nav }); + this.footer = new FooterView({ model: this.model.footer }); + this.render(); + return this; + }, + renderCallback: function(view, model) { + this.nav.$el.prependTo(this.$el); + this.nav.postRender(); + this.footer.$el.appendTo(this.$el); + this.footer.postRender(); + } + }); + + var PageReadView = MasterPageView.extend({ + templateName: 'read-page', + initialize: function() { + PageReadView.__super__.initialize.apply(this, arguments); // Also get the current state, and show a warning // if the page is new or modified. - var stateModel = new PageStateModel({ id: this.id }); + var stateModel = new PageStateModel({ path: this.model.get('path') }); stateModel.fetch({ success: function(model, response, options) { if (model.get('state') == 'new' || model.get('state') == 'modified') { TemplateLoader.get('state-warning', function(src) { - var template_data = model.toJSON(); var template = Handlebars.compile(src); - var warning = $(template(template_data)); + var warning = $(template(model.toJSON())); warning.css('display', 'none'); warning.prependTo($('#app')); warning.slideDown(); @@ -292,34 +451,35 @@ } }); return this; + }, + renderCallback: function(view, model) { + PageReadView.__super__.renderCallback.apply(this, arguments); + // Replace all wiki links with proper hyperlinks using the JS app's + // URL scheme. + this.$('a.wiki-link[data-wiki-url]').each(function(i) { + var jel = $(this); + if (jel.hasClass('missing')) + jel.attr('href', '/#/edit/' + jel.attr('data-wiki-url')); + else + jel.attr('href', '/#/read/' + jel.attr('data-wiki-url')); + }); } }); - var PageEditView = Backbone.View.extend({ - initialize: function() { - var $view = this; - var model = new PageEditModel({ path: this.id }); - model.fetch({ - success: function(model, response, options) { - TemplateLoader.get('edit-page', function(src) { - var template_data = model.toJSON(); - var template = Handlebars.compile(src); - $view.$el.html(template(template_data)); - document.title = 'Editing: ' + model.title(); - - $('#page-edit').submit(function() { - $view._submitText(this, model.get('path')); - return false; - }); - }); - }, - error: function(model, xhr, options) { - } + var PageEditView = MasterPageView.extend({ + templateName: 'edit-page', + renderCallback: function(view, model) { + PageEditView.__super__.renderCallback.apply(this, arguments); + this.$('#page-edit').submit(function() { + view._submitText(this, model.get('path')); + return false; }); - return this; + }, + titleFormat: function(title) { + return 'Editing: ' + title; }, _submitText: function(form, path) { - $.post('/api/edit/' + path, $(form).serialize()) + $.post('/api/edit/' + path, this.$(form).serialize()) .success(function(data) { app.navigate('/read/' + path, { trigger: true }); }) @@ -329,28 +489,17 @@ } }); - var PageHistoryView = Backbone.View.extend({ - initialize: function() { - var $view = this; - var model = new PageHistoryModel({ path: this.id }); - model.fetch({ - success: function(model, response, options) { - TemplateLoader.get('history-page', function(src) { - var template_data = model.toJSON(); - var template = Handlebars.compile(src); - $view.$el.html(template(template_data)); - document.title = 'Changes: ' + model.title(); - - $('#diff-page').submit(function() { - $view._triggerDiff(this, model.get('path')); - return false; - }); - }); - }, - error: function() { - } + var PageHistoryView = MasterPageView.extend({ + templateName: 'history-page', + renderCallback: function(view, model) { + PageHistoryView.__super__.renderCallback.apply(this, arguments); + this.$('#diff-page').submit(function() { + view._triggerDiff(this, model.get('path')); + return false; }); - return this; + }, + titleFormat: function(title) { + return 'History: ' + title; }, _triggerDiff: function(form, path) { var rev1 = $('input[name=rev1]:checked', form).val(); @@ -359,60 +508,31 @@ } }); - var PageRevisionView = Backbone.View.extend({ - initialize: function() { - var $view = this; - var model = new PageRevisionModel({ path: this.id, rev: this.options.rev }); - model.fetch({ - success: function(model, response, options) { - TemplateLoader.get('revision-page', function(src) { - var template_data = model.toJSON(); - var template = Handlebars.compile(src); - $view.$el.html(template(template_data)); - document.title = model.title() + ' [' + model.get('rev') + ']'; - }); - } - }); + var PageRevisionView = MasterPageView.extend({ + templateName: 'revision-page', + titleFormat: function(title) { + return title + ' [' + this.model.get('rev') + ']'; } }); - var PageDiffView = Backbone.View.extend({ - initialize: function() { - var $view = this; - var model = new PageDiffModel({ path: this.id, rev1: this.options.rev1, rev2: this.options.rev2 }); - model.fetch({ - success: function(model, response, options) { - TemplateLoader.get('diff-page', function(src) { - var template_data = model.toJSON(); - var template = Handlebars.compile(src); - $view.$el.html(template(template_data)); - document.title = model.title() + ' [' + model.get('rev1') + '-' + model.get('rev2') + ']'; - }); - } - }); + var PageDiffView = MasterPageView.extend({ + templateName: 'diff-page', + titleFormat: function(title) { + return title + ' [' + this.model.get('rev1') + '-' + this.model.get('rev2') + ']'; } }); - var IncomingLinksView = Backbone.View.extend({ - initialize: function() { - var $view = this; - var model = new IncomingLinksModel({ path: this.id }); - model.fetch({ - success: function(model, response, options) { - TemplateLoader.get('inlinks-page', function(src) { - var template_data = model.toJSON(); - var template = Handlebars.compile(src); - $view.$el.html(template(template_data)); - document.title = 'Incoming Links: ' + model.title(); - }); - }, - error: function() { - } - }); - return this; + var IncomingLinksView = MasterPageView.extend({ + templateName: 'inlinks-page', + titleFormat: function(title) { + return 'Incoming Links: ' + title; } }); + var WikiSearchView = MasterPageView.extend({ + templateName: 'search-results' + }); + /** * Main URL router. */ @@ -425,38 +545,93 @@ 'inlinks/*path': "showIncomingLinks", 'revision/*path/:rev': "readPageRevision", 'diff/c/*path/:rev': "showDiffWithPrevious", - 'diff/r/*path/:rev1/:rev2':"showDiff" + 'diff/r/*path/:rev1/:rev2':"showDiff", + 'search/:query': "showSearchResults" }, readPage: function(path) { - var page_view = new PageReadView({ id: path, el: $('#app') }); + var view = new PageReadView({ + el: $('#app'), + model: new PageReadModel({ path: path }) + }); + view.model.fetch(); this.navigate('/read/' + path); }, readMainPage: function() { this.readPage('main-page'); }, editPage: function(path) { - var edit_view = new PageEditView({ id: path, el: $('#app') }); + var view = new PageEditView({ + el: $('#app'), + model: new PageEditModel({ path: path }) + }); + view.model.fetch(); this.navigate('/edit/' + path); }, showPageHistory: function(path) { - var changes_view = new PageHistoryView({ id: path, el: $('#app') }); + var view = new PageHistoryView({ + el: $('#app'), + model: new PageHistoryModel({ path: path }) + }); + view.model.fetch(); this.navigate('/changes/' + path); }, showIncomingLinks: function(path) { - var in_view = new IncomingLinksView({ id: path, el: $('#app') }); + var view = new IncomingLinksView({ + el: $('#app'), + model: new IncomingLinksModel({ path: path }) + }); + view.model.fetch(); this.navigate('/inlinks/' + path); }, readPageRevision: function(path, rev) { - var rev_view = new PageRevisionView({ id: path, rev: rev, el: $('#app') }); + var view = new PageRevisionView({ + el: $('#app'), + rev: rev, + model: new PageRevisionModel({ path: path, rev: rev }) + }); + view.model.fetch(); this.navigate('/revision/' + path + '/' + rev); }, showDiffWithPrevious: function(path, rev) { - var diff_view = new PageDiffView({ id: path, rev1: rev, el: $('#app') }); + var view = new PageDiffView({ + el: $('#app'), + rev1: rev, + model: new PageDiffModel({ path: path, rev1: rev }) + }); + view.model.fetch(); this.navigate('/diff/c/' + path + '/' + rev); }, showDiff: function(path, rev1, rev2) { - var diff_view = new PageDiffView({ id: path, rev1: rev1, rev2: rev2, el: $('#app') }); + var view = new PageDiffView({ + el: $('#app'), + rev1: rev1, + rev2: rev2, + model: new PageDiffModel({ path: path, rev1: rev1, rev2: rev2 }) + }); + view.model.fetch(); this.navigate('/diff/r/' + path + '/' + rev1 + '/' + rev2); + }, + showSearchResults: function(query) { + if (query === '') { + query = this.getQueryVariable('q'); + } + var view = new WikiSearchView({ + el: $('#app'), + model: new WikiSearchModel() + }); + view.model.execute(query); + this.navigate('/search/' + query); + }, + getQueryVariable: function(variable) { + var query = window.location.search.substring(1); + var vars = query.split("&"); + for (var i = 0; i < vars.length; i++) { + var pair = vars[i].split("="); + if (pair[0] == variable) { + return unescape(pair[1]); + } + } + return false; } });
--- a/wikked/static/tpl/diff-page.html Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/tpl/diff-page.html Sat Dec 29 18:21:44 2012 -0800 @@ -1,29 +1,15 @@ -<div class="wrapper"> - <nav class="row"> - <div class="span12"> - <a href="{{url_read}}">Read</a> - <a href="{{url_hist}}">History</a> - </div> - </nav> - <article class="row"> - <div class="page span12"> - <h1> - {{meta.title}} - <span class="decorator">Diff - {{#if disp_rev2}} - <span class="rev_id">{{disp_rev1}}</span> to <span class="rev_id">{{disp_rev2}}</span> - {{else}} - change <span class="rev_id">{{disp_rev1}}</span> - {{/if}} - </span> - </h1> - <pre><code>{{{diff}}}</code></pre> - </div> - </article> - <div class="row meta"> - <div class="span12"> - <a href="{{url_inlinks}}">Pages Linking Here</a> - <!-- TODO: last modified, etc. --> - </div> +<article class="row"> + <div class="page span12"> + <h1> + {{meta.title}} + <span class="decorator">Diff: + {{#if disp_rev2}} + <span class="rev_id">{{disp_rev1}}</span> to <span class="rev_id">{{disp_rev2}}</span> + {{else}} + change <span class="rev_id">{{disp_rev1}}</span> + {{/if}} + </span> + </h1> + <pre><code>{{{diff}}}</code></pre> </div> -</div> +</article>
--- a/wikked/static/tpl/edit-page.html Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/tpl/edit-page.html Sat Dec 29 18:21:44 2012 -0800 @@ -1,94 +1,87 @@ -<div class="wrapper"> - <nav class="row"> - <div> - <a href="{{url_read}}">Read</a> - </div> - </nav> - <article class="row"> - <div class="page span12"> - <h1>{{meta.title}} <span class="decorator">Editing</span></h1> - <form id="page-edit" class="page-edit row"> - <div class="span12"> - <div id="wmd-button-bar"></div> - </div> - <div class="span12"> - <div class="wmd-input-wrapper"> - <textarea id="wmd-input" name="text" placeholder="Your page's contents go here...">{{content}}</textarea> - <div class="wmd-input-grip"></div> - </div> +<article class="row"> + <div class="page span12"> + <h1>{{meta.title}} <span class="decorator">Editing</span></h1> + <form id="page-edit" class="page-edit row"> + <div class="span12"> + <div id="wmd-button-bar"></div> + </div> + <div class="span12"> + <div class="wmd-input-wrapper"> + <textarea id="wmd-input" name="text" placeholder="Your page's contents go here...">{{content}}</textarea> + <div class="wmd-input-grip"></div> </div> - <div class="span12"> - <div class="wmd-preview-wrapper"> - <h3><a class="btn"><i class="icon-minus"></i></a> Preview</h3> - <div id="wmd-preview" class="wmd-preview"></div> - </div> - </div> - <div class="span12"> - <div class="controls commit-meta"> - <div class="control-group input-prepend"> - <label for="author" class="control-label add-on">Author: </label> - <input type="text" name="author" class="" placeholder="{{commit_meta.author}}"></input> - </div> - <div class="control-group input-prepend"> - <label for="message" class="control-label add-on">Change Description: </label> - <input type="text" name="message" class="input-xxlarge" placeholder="{{commit_meta.desc}}"></input> - </div> - </div> - </div> - <div class="span12"> - <button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> Save</button> - <a href="{{url_read}}" class="btn">Cancel</a> + </div> + <div class="span12"> + <div class="wmd-preview-wrapper"> + <h3><a class="btn"><i class="icon-minus"></i></a> Preview</h3> + <div id="wmd-preview" class="wmd-preview"></div> </div> - </form> - </div> - </article> - <script type="text/javascript" src="/js/pagedown/Markdown.Converter.js"></script> - <script type="text/javascript" src="/js/pagedown/Markdown.Sanitizer.js"></script> - <script type="text/javascript" src="/js/pagedown/Markdown.Editor.js"></script> - <script type="text/javascript"> - (function() { - var formatter = _.extend(this.Wikked.PageFormatter, {}); - var converter = new Markdown.Converter(); - converter.hooks.chain("preConversion", function(text) { - return formatter.formatText(text); - }); - - //var help = function () { alert("Do you need help?"); } - //var options = { - // helpButton: { handler: help }, - // strings: { quoteexample: "whatever you're quoting, put it right here" } - //}; - var editor = new Markdown.Editor(converter); //TODO: pass options - editor.run(); - - var editor_control = $('textarea#wmd-input'); - editor_control.outerWidth($('.wmd-input-wrapper').innerWidth()); + </div> + <div class="span12"> + <div class="controls commit-meta"> + <div class="control-group input-prepend"> + <label for="author" class="control-label add-on">Author: </label> + <input type="text" name="author" class="" placeholder="{{commit_meta.author}}"></input> + </div> + <div class="control-group input-prepend"> + <label for="message" class="control-label add-on">Change Description: </label> + <input type="text" name="message" class="input-xxlarge" placeholder="{{commit_meta.desc}}"></input> + </div> + </div> + </div> + <div class="span12"> + <button type="submit" class="btn btn-primary"><i class="icon-ok icon-white"></i> Save</button> + <a href="{{url_read}}" class="btn">Cancel</a> + </div> + </form> + </div> +</article> +<script type="text/javascript" src="/js/pagedown/Markdown.Converter.js"></script> +<script type="text/javascript" src="/js/pagedown/Markdown.Sanitizer.js"></script> +<script type="text/javascript" src="/js/pagedown/Markdown.Editor.js"></script> +<script type="text/javascript"> + (function() { + var formatter = _.extend(this.Wikked.PageFormatter, {}); + var converter = new Markdown.Converter(); + converter.hooks.chain("preConversion", function(text) { + return formatter.formatText(text); + }); + + //var help = function () { alert("Do you need help?"); } + //var options = { + // helpButton: { handler: help }, + // strings: { quoteexample: "whatever you're quoting, put it right here" } + //}; + var editor = new Markdown.Editor(converter); //TODO: pass options + editor.run(); - var last_pageY; - $(".wmd-input-grip") - .mousedown(function(e) { + var editor_control = $('textarea#wmd-input'); + editor_control.outerWidth($('.wmd-input-wrapper').innerWidth()); + + var last_pageY; + $(".wmd-input-grip") + .mousedown(function(e) { + last_pageY = e.pageY; + $('body') + .on('mousemove.wikked.editor_resize', function(e) { + editor_control.height(editor_control.height() + e.pageY - last_pageY); last_pageY = e.pageY; - $('body') - .on('mousemove.wikked.editor_resize', function(e) { - editor_control.height(editor_control.height() + e.pageY - last_pageY); - last_pageY = e.pageY; - }) - .on('mouseup.wikked.editor_resize mouseleave.wikked.editor_resize', function(e) { - $('body').off('.wikked.editor_resize'); - }); + }) + .on('mouseup.wikked.editor_resize mouseleave.wikked.editor_resize', function(e) { + $('body').off('.wikked.editor_resize'); }); - $('.wmd-preview-wrapper>h3>a').on('click', function(e) { - $('#wmd-preview').fadeToggle(function() { - var icon = $('.wmd-preview-wrapper>h3>a i'); - if (icon.hasClass('icon-minus')) { - icon.removeClass('icon-minus'); - icon.addClass('icon-plus'); - } else { - icon.removeClass('icon-plus'); - icon.addClass('icon-minus'); - } - }); - }); - })(); - </script> -</div> + }); + $('.wmd-preview-wrapper>h3>a').on('click', function(e) { + $('#wmd-preview').fadeToggle(function() { + var icon = $('.wmd-preview-wrapper>h3>a i'); + if (icon.hasClass('icon-minus')) { + icon.removeClass('icon-minus'); + icon.addClass('icon-plus'); + } else { + icon.removeClass('icon-plus'); + icon.addClass('icon-minus'); + } + }); + }); + })(); +</script>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/static/tpl/footer.html Sat Dec 29 18:21:44 2012 -0800 @@ -0,0 +1,8 @@ +<div class="row meta"> + <div class="span12"> + {{#each url_extras}} + <a href="{{url}}">{{name}}</a> + {{/each}} + <!-- TODO: last modified, etc. --> + </div> +</div>
--- a/wikked/static/tpl/history-page.html Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/tpl/history-page.html Sat Dec 29 18:21:44 2012 -0800 @@ -1,43 +1,34 @@ -<div class="wrapper"> - <nav class="row"> - <div class="span12"> - <a href="{{url_read}}">Read</a> - </div> - </nav> - <article class="row"> - <div class="page span12"> - <h1>{{meta.title}} <span class="decorator">History</span></h1> - <p>Here's the revision log for <a href="{{url_read}}">{{meta.title}}</a>.</p> - <form id="diff-page"> - <table class="table table-hover"> - <thead> - <tr> - <th>Rev.</th> - <th>Date</th> - <th>Author</th> - <th>Comment</th> - <th><button id="diff-revs" class="btn btn-primary">Show Diff.</button></th> - </tr> - </thead> - <tbody> - {{#eachr history}} - <tr> - <td><a href="{{../url_rev}}/{{rev_hash}}">{{index}}</a></td> - <td>{{timestamp}}</td> - <td>{{author}}</td> - <td>{{description}}</td> - <td> - <input type="radio" name="rev1" value="{{rev_hash}}" {{#ifeq @index to=0 }}checked="true" {{/ifeq}}/> - <input type="radio" name="rev2" value="{{rev_hash}}" {{#ifeq @index to=1 }}checked="true" {{/ifeq}}/> - <small><a href="{{../url_diffc}}/{{rev_hash}}">with previous</a></small> - </td> - </tr> - {{/eachr}} - </tbody> - </table> - </form> - </div> - </article> - <div class="row meta"> +<article class="row"> + <div class="page span12"> + <h1>{{meta.title}} <span class="decorator">History</span></h1> + <p>Here's the revision log for <a href="{{url_read}}">{{meta.title}}</a>.</p> + <form id="diff-page"> + <table class="table table-hover"> + <thead> + <tr> + <th>Rev.</th> + <th>Date</th> + <th>Author</th> + <th>Comment</th> + <th><button id="diff-revs" class="btn btn-primary">Show Diff.</button></th> + </tr> + </thead> + <tbody> + {{#eachr history}} + <tr> + <td><a href="{{../url_rev}}/{{rev_hash}}">{{index}}</a></td> + <td>{{timestamp}}</td> + <td>{{author}}</td> + <td>{{description}}</td> + <td> + <input type="radio" name="rev1" value="{{rev_hash}}" {{#ifeq @index to=0 }}checked="true" {{/ifeq}}/> + <input type="radio" name="rev2" value="{{rev_hash}}" {{#ifeq @index to=1 }}checked="true" {{/ifeq}}/> + <small><a href="{{../url_diffc}}/{{rev_hash}}">with previous</a></small> + </td> + </tr> + {{/eachr}} + </tbody> + </table> + </form> </div> -</div> +</article>
--- a/wikked/static/tpl/inlinks-page.html Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/tpl/inlinks-page.html Sat Dec 29 18:21:44 2012 -0800 @@ -1,24 +1,17 @@ -<div class="wrapper"> - <nav class="row"> - <div class="span12"> - <a href="{{url_read}}">Read</a> - </div> - </nav> - <article class="row"> - <div class="page span12"> - <h1>{{meta.title}} <span class="decorator">Incoming Links</span></h1> - <p>The following pages link to <a href="{{url_read}}">{{meta.title}}</a>:</p> - <ul> - {{#each in_links}} - <li> - {{#if missing}} - <a class="wiki-link missing" href="/#/edit/{{url}}">{{url}}</a> - {{else}} - <a class="wiki-link" href="/#/read/{{url}}">{{meta.title}}</a> - {{/if}} - </li> - {{/each}} - </ul> - </div> - </article> -</div> +<article class="row"> + <div class="page span12"> + <h1>{{meta.title}} <span class="decorator">Incoming Links</span></h1> + <p>The following pages link to <a href="{{url_read}}">{{meta.title}}</a>:</p> + <ul> + {{#each in_links}} + <li> + {{#if missing}} + <a class="wiki-link missing" href="/#/edit/{{url}}">{{url}}</a> + {{else}} + <a class="wiki-link" href="/#/read/{{url}}">{{meta.title}}</a> + {{/if}} + </li> + {{/each}} + </ul> + </div> +</article>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/static/tpl/nav.html Sat Dec 29 18:21:44 2012 -0800 @@ -0,0 +1,11 @@ +<nav class="row"> + <div class="span12"> + {{#ifneq action to='read'}}<a href="{{url_read}}">Read</a>{{/ifneq}} + {{#ifneq action to='edit'}}<a href="{{url_edit}}">Edit</a>{{/ifneq}} + {{#ifneq action to='history'}}<a href="{{url_hist}}">History</a>{{/ifneq}} + <form id="search" class="search"> + <input type="text" name="q" class="input-medium search-query" placeholder="Search..."> + <button type="submit" class="btn">Search</button> + </form> + </div> +</nav>
--- a/wikked/static/tpl/read-page.html Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/tpl/read-page.html Sat Dec 29 18:21:44 2012 -0800 @@ -1,22 +1,6 @@ -<div class="wrapper"> - <nav class="row"> - <div class="span12"> - <a href="{{url_home}}">Home</a> - <a href="{{url_edit}}">Edit</a> - <a href="{{url_hist}}">History</a> - <span>Search</span> - </div> - </nav> - <article class="row"> - <div class="page span12"> - <h1>{{meta.title}}</h1> - {{content}} - </div> - </article> - <div class="row meta"> - <div class="span12"> - <a href="{{url_inlinks}}">Pages Linking Here</a> - <!-- TODO: last modified, etc. --> - </div> +<article class="row"> + <div class="page span12"> + <h1>{{meta.title}}</h1> + {{content}} </div> -</div> +</article>
--- a/wikked/static/tpl/revision-page.html Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/static/tpl/revision-page.html Sat Dec 29 18:21:44 2012 -0800 @@ -1,20 +1,6 @@ -<div class="wrapper"> - <nav class="row"> - <div class="span12"> - <a href="{{url_read}}">Read</a> - <a href="{{url_hist}}">History</a> - </div> - </nav> - <article class="row"> - <div class="page span12"> - <h1>{{meta.title}} <span class="decorator">Revision <span class="rev_id">{{disp_rev}}</span></span></h1> - <pre><code>{{text}}</code></pre> - </div> - </article> - <div class="meta row"> - <div class="span12"> - <a href="{{url_inlinks}}">Pages Linking Here</a> - <!-- TODO: last modified, etc. --> - </div> +<article class="row"> + <div class="page span12"> + <h1>{{meta.title}} <span class="decorator">Revision: <span class="rev_id">{{disp_rev}}</span></span></h1> + <pre><code>{{text}}</code></pre> </div> -</div> +</article>
--- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/wikked/static/tpl/search-results.html Sat Dec 29 18:21:44 2012 -0800 @@ -0,0 +1,10 @@ +<article class="row"> + <div class="page span12"> + <h1>Search Results</h1> + <ul class="search-results"> + {{#each hits}} + <li><a href="/#/read/{{url}}">{{title}}</a></li> + {{/each}} + </ul> + </div> +</article>
--- a/wikked/templates/index.html Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/templates/index.html Sat Dec 29 18:21:44 2012 -0800 @@ -8,7 +8,8 @@ <link rel="stylesheet" type="text/css" href="/jquery-ui/css/smoothness/jquery-ui-1.9.2.min.css" /> </head> <body> - <div id="app" class="container">{% block app %}{% endblock %}</div> + <div id="app" class="container"> + </div> <script src="/js/jquery-1.8.3.min.js"></script> <script src="/js/underscore-min.js"></script> <script src="/js/backbone-min.js"></script>
--- a/wikked/views.py Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/views.py Sat Dec 29 18:21:44 2012 -0800 @@ -35,6 +35,16 @@ return render_template('index.html', cache_bust=('?%d' % time.time())) +@app.route('/read/<path:url>') +def read(): + return render_template('index.html', cache_bust=('?%d' % time.time())) + + +@app.route('/search') +def search(): + return render_template('index.html', cache_bust=('?%d' % time.time())) + + @app.route('/api/list') def api_list_all_pages(): return list_pages(None) @@ -84,7 +94,7 @@ formatter = get_formatter_by_name('html') diff = highlight(diff, lexer, formatter) if rev2 is None: - meta = dict(page.all_meta, change=rev) + meta = dict(page.all_meta, change=rev1) else: meta = dict(page.all_meta, rev1=rev1, rev2=rev2) result = { 'path': url, 'meta': meta, 'diff': diff } @@ -207,3 +217,10 @@ result = { 'url': url, 'meta': page.all_meta, 'history': hist_data } return jsonify(result) +@app.route('/api/search') +def api_search(): + query = request.args.get('q') + hits = wiki.index.search(query) + result = { 'query': query, 'hits': hits } + return jsonify(result) +
--- a/wikked/wiki.py Sat Dec 22 22:33:11 2012 -0800 +++ b/wikked/wiki.py Sat Dec 29 18:21:44 2012 -0800 @@ -8,6 +8,7 @@ from fs import FileSystem from cache import Cache from scm import MercurialSourceControl +from indexer import WhooshWikiIndex class FormatterNotFound(Exception): @@ -73,7 +74,7 @@ return text def _formatWikiLink(self, ctx, display, url): - slug = re.sub(r'[^A-Za-z0-9_\.\-\(\)/]+', '-', url.lower()) + slug = Page.title_to_url(url) ctx.out_links.append(slug) css_class = 'wiki-link' @@ -188,6 +189,10 @@ self.wiki.logger.debug("Updated cached %s for page '%s'." % (cache_key, self.url)) self.wiki.cache.write(cache_key, data) + @staticmethod + def title_to_url(title): + return re.sub(r'[^A-Za-z0-9_\.\-\(\)/]+', '-', title.lower()) + class Wiki(object): def __init__(self, root=None, logger=None): @@ -202,17 +207,23 @@ self.fs = FileSystem(root) self.scm = MercurialSourceControl(root, self.logger) self.cache = None #Cache(os.path.join(root, '.cache')) + self.index = WhooshWikiIndex(os.path.join(root, '.index'), logger=self.logger) if self.cache is not None: self.fs.excluded.append(self.cache.cache_dir) if self.scm is not None: self.fs.excluded += self.scm.getSpecialDirs() + if self.index is not None: + self.fs.excluded.append(self.index.store_dir) self.formatters = { markdown.markdown: [ 'md', 'mdown', 'markdown' ], self._passthrough: [ 'txt', 'text', 'html' ] } + if self.index is not None: + self.index.update(self.getPages()) + @property def root(self): return self.fs.root @@ -249,6 +260,9 @@ } self.scm.commit([ path ], commit_meta) + if self.index is not None: + self.index.update([ self.getPage(url) ]) + def pageExists(self, url): return self.fs.pageExists(url)