diff --git a/README.md b/README.md index 605c0ba..26694de 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ This will ask you questions and create a config.json file in the app root direct Of course you can manually edit this file as well. Any config value set in config.json will override values set in ```realms/config/__init__.py``` -## Nginx Setup +### Nginx Setup sudo apt-get install -y nginx @@ -130,6 +130,23 @@ Reload Nginx sudo service nginx reload +### Mysql Setup + + sudo apt-get install -y mysql-server mysql-client libmysqlclient-dev + realms-wiki pip install python-memcached + +### MariaDB Setup + + sudo apt-get install -y mariadb-server mariadb-client libmariadbclient-dev + realms-wiki pip install MySQL-Python + +### Postgres + + sudo apt-get install -y libpq-dev postgresql postgresql-contrib postgresql-client + realms-wiki pip install psycopg2 + +_Don't forget to create your database._ + ## Running Current there are different ways. @@ -153,22 +170,19 @@ http://localhost:5000 ## Templating Realms uses handlebars partials to create templates. -Each page that you create is a potential partial. +Each page that you create can be imported as a partial. -Let's create the following partial: +This page imports and uses a partial: -http://localhost:5000/_create/my-partial + http://realms.io/_edit/hbs -
-
{{ heading }}
-
- {{ body }} -
-
+This page contains the content of the partial: -Don't forget to save it. + http://realms.io/_edit/example-tmpl + +I locked these pages to preserve them. +You may copy and paste into a new page to test. -Now ## Author Matthew Scragg diff --git a/VERSION b/VERSION new file mode 100644 index 0000000..9325c3c --- /dev/null +++ b/VERSION @@ -0,0 +1 @@ +0.3.0 \ No newline at end of file diff --git a/install.sh b/install.sh index 735f7c1..df8a398 100755 --- a/install.sh +++ b/install.sh @@ -23,7 +23,7 @@ sudo add-apt-repository -y ppa:chris-lea/node.js sudo apt-get update sudo apt-get install -y python build-essential git libpcre3-dev \ python-pip python-virtualenv python-dev pkg-config curl libxml2-dev libxslt1-dev zlib1g-dev \ -libffi-dev nodejs libyaml-dev +libffi-dev nodejs libyaml-dev libssl-dev # Default cache is memoization diff --git a/manage.py b/manage.py index 3356023..32c9b8b 100755 --- a/manage.py +++ b/manage.py @@ -1,8 +1,10 @@ from gevent import wsgi from realms import config, app, cli, db from realms.lib.util import random_string +from subprocess import call import click import json +import sys @cli.command() @@ -77,6 +79,37 @@ def setup_redis(**kw): conf[k.upper()] = v config.update(conf) + install_redis() + + +def get_pip(): + """ Get virtualenv path for pip + """ + return sys.prefix + '/bin/pip' + + +@cli.command() +@click.argument('cmd', nargs=-1) +def pip(cmd): + """ Execute pip commands for this virtualenv + """ + call(get_pip() + ' ' + ' '.join(cmd), shell=True) + + +def install_redis(): + call([get_pip(), 'install', 'redis']) + + +def install_mysql(): + call([get_pip(), 'install', 'MySQL-Python']) + + +def install_postgres(): + call([get_pip(), 'install', 'psycopg2']) + + +def install_memcached(): + call([get_pip(), 'install', 'python-memcached']) @click.command() @@ -139,5 +172,13 @@ def drop_db(): click.echo("Dropping all tables") db.drop_all() + +@cli.command() +def version(): + """ Output version + """ + with open('VERSION') as f: + return f.read().strip() + if __name__ == '__main__': cli() \ No newline at end of file diff --git a/realms/__init__.py b/realms/__init__.py index c3e6726..a002903 100644 --- a/realms/__init__.py +++ b/realms/__init__.py @@ -192,7 +192,8 @@ assets.register('main.js', 'js/html-sanitizer-minified.js', # don't minify? 'vendor/highlightjs/highlight.pack.js', 'vendor/parsleyjs/dist/parsley.js', - 'js/main.js') + 'js/hbs-helpers.js', + 'js/mdr.js') assets.register('main.css', 'vendor/bootswatch-dist/css/bootstrap.css', diff --git a/realms/config/__init__.py b/realms/config/__init__.py index ab0ca49..f3c0416 100644 --- a/realms/config/__init__.py +++ b/realms/config/__init__.py @@ -38,7 +38,11 @@ PORT = 5000 BASE_URL = 'http://localhost' SITE_TITLE = "Realms" +# https://pythonhosted.org/Flask-SQLAlchemy/config.html#connection-uri-format DB_URI = 'sqlite:///%s/wiki.db' % USER_HOME +# DB_URI = 'mysql://scott:tiger@localhost/mydatabase' +# DB_URI = 'postgresql://scott:tiger@localhost/mydatabase' +# DB_URI = 'oracle://scott:tiger@127.0.0.1:1521/sidname' CACHE_TYPE = 'simple' diff --git a/realms/lib/util.py b/realms/lib/util.py index bb8e97b..2dac2c1 100644 --- a/realms/lib/util.py +++ b/realms/lib/util.py @@ -91,6 +91,7 @@ def to_canonical(s): s = re.sub(r"\-\-+", "-", s) s = re.sub(r"[^a-zA-Z0-9\-]", "", s) s = s[:64] + s = s.lower() return s diff --git a/realms/modules/auth/models.py b/realms/modules/auth/models.py index 7cde5a4..ca1ab62 100644 --- a/realms/modules/auth/models.py +++ b/realms/modules/auth/models.py @@ -44,9 +44,9 @@ class AnonUser(AnonymousUserMixin): class User(Model, UserMixin): __tablename__ = 'users' id = db.Column(db.Integer, primary_key=True) - username = db.Column(db.String, unique=True) - email = db.Column(db.String, unique=True) - password = db.Column(db.String) + username = db.Column(db.String(128), unique=True) + email = db.Column(db.String(128), unique=True) + password = db.Column(db.String(60)) admin = False hidden_fields = ['password'] diff --git a/realms/modules/wiki/assets.py b/realms/modules/wiki/assets.py index 31cd839..8b2162d 100644 --- a/realms/modules/wiki/assets.py +++ b/realms/modules/wiki/assets.py @@ -7,4 +7,4 @@ assets.register('editor.js', 'vendor/ace-builds/src/mode-markdown.js', 'vendor/ace-builds/src/ext-keybinding_menu.js', 'vendor/keymaster/keymaster.js', - 'js/editor.js') + 'js/aced.js') diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py index 4be981b..e1247d0 100644 --- a/realms/modules/wiki/views.py +++ b/realms/modules/wiki/views.py @@ -36,7 +36,7 @@ def revert(): commit = request.form.get('commit') cname = to_canonical(name) - if cname.lower() in app.config.WIKI_LOCKED_PAGES: + if cname in app.config.WIKI_LOCKED_PAGES: flash("Page is locked") return redirect(url_for(app.config['ROOT_ENDPOINT'])) @@ -59,10 +59,10 @@ def edit(name): if request.method == 'POST': edit_cname = to_canonical(request.form['name']) - if edit_cname.lower() in app.config['WIKI_LOCKED_PAGES']: + if edit_cname in app.config['WIKI_LOCKED_PAGES']: return redirect(url_for(app.config['ROOT_ENDPOINT'])) - if edit_cname.lower() != cname.lower(): + if edit_cname != cname.lower(): g.current_wiki.rename_page(cname, edit_cname) g.current_wiki.write_page(edit_cname, diff --git a/realms/static/css/style.css b/realms/static/css/style.css index 7c3c64a..0709810 100644 --- a/realms/static/css/style.css +++ b/realms/static/css/style.css @@ -58,9 +58,9 @@ #app-wrap { top: 60px; - left: -5px; + left: 0; bottom: 0; - right: -5px; + right: 0; position: fixed; } @@ -186,9 +186,8 @@ a.label { right: 0; bottom: 0; left: 0; - padding: 45px 10px 10px 10px; + padding: 40px 10px 10px 10px; overflow: auto; - //word-break: break-word; cursor: default; } diff --git a/realms/static/js/aced.js b/realms/static/js/aced.js new file mode 100644 index 0000000..d4bb3ad --- /dev/null +++ b/realms/static/js/aced.js @@ -0,0 +1,430 @@ +function Aced(settings) { + var id, + options, + editor, + element, + preview, + previewWrapper, + profile, + autoInterval, + themes, + themeSelect, + loadedThemes = {}; + + settings = settings || {}; + + options = { + sanitize: true, + preview: null, + editor: null, + theme: 'idle_fingers', + themePath: '/static/vendor/ace-builds/src', + mode: 'markdown', + autoSave: true, + autoSaveInterval: 5000, + syncPreview: false, + keyMaster: false, + submit: function(data){ alert(data); }, + showButtonBar: false, + themeSelect: null, + submitBtn: null, + renderer: null, + info: null + }; + + themes = { + chrome: "Chrome", + clouds: "Clouds", + clouds_midnight: "Clouds Midnight", + cobalt: "Cobalt", + crimson_editor: "Crimson Editor", + dawn: "Dawn", + dreamweaver: "Dreamweaver", + eclipse: "Eclipse", + idle_fingers: "idleFingers", + kr_theme: "krTheme", + merbivore: "Merbivore", + merbivore_soft: "Merbivore Soft", + mono_industrial: "Mono Industrial", + monokai: "Monokai", + pastel_on_dark: "Pastel on Dark", + solarized_dark: "Solarized Dark", + solarized_light: "Solarized Light", + textmate: "TextMate", + tomorrow: "Tomorrow", + tomorrow_night: "Tomorrow Night", + tomorrow_night_blue: "Tomorrow Night Blue", + tomorrow_night_bright: "Tomorrow Night Bright", + tomorrow_night_eighties: "Tomorrow Night 80s", + twilight: "Twilight", + vibrant_ink: "Vibrant Ink" + }; + + function editorId() { + return "aced." + id; + } + + function infoKey() { + return editorId() + ".info"; + } + + function gc() { + // Clean up localstorage + store.forEach(function(key, val) { + var re = new RegExp("aced\.(.*?)\.info"); + var info = re.exec(key); + if (!info || !val.time) { + return; + } + + var id = info[1]; + + // Remove week+ old stuff + var now = new Date().getTime() / 1000; + + if (now > (val.time + 604800)) { + store.remove(key); + store.remove('aced.' + id); + } + }); + } + + function buildThemeSelect() { + var $sel = $(""); + $sel.append(''); + $.each(themes, function(k, v) { + $sel.append(""); + }); + return $("
").html($sel); + } + + function toJquery(o) { + return (typeof o == 'string') ? $("#" + o) : $(o); + } + + function initProfile() { + profile = {theme: ''}; + + try { + // Need to merge in any undefined/new properties from last release + // Meaning, if we add new features they may not have them in profile + profile = $.extend(true, profile, store.get('aced.profile')); + } catch (e) { } + } + + function updateProfile(obj) { + profile = $.extend(null, profile, obj); + store.set('profile', profile); + } + + function render(content) { + return (options.renderer) ? options.renderer(content) : content; + } + + function bindKeyboard() { + // CMD+s TO SAVE DOC + key('command+s, ctrl+s', function (e) { + submit(); + e.preventDefault(); + }); + + var saveCommand = { + name: "save", + bindKey: { + mac: "Command-S", + win: "Ctrl-S" + }, + exec: function () { + submit(); + } + }; + editor.commands.addCommand(saveCommand); + } + + function info(info) { + if (info) { + store.set(infoKey(), info); + } + return store.get(infoKey()); + } + + function val(val) { + // Alias func + if (val) { + editor.getSession().setValue(val); + } + return editor.getSession().getValue(); + } + + function discardDraft() { + stopAutoSave(); + store.remove(editorId()); + store.remove(infoKey()); + location.reload(); + } + + function save() { + store.set(editorId(), val()); + } + + function submit() { + store.remove(editorId()); + store.remove(editorId() + ".info"); + options.submit(val()); + } + + function autoSave() { + if (options.autoSave) { + autoInterval = setInterval(function () { + save(); + }, options.autoSaveInterval); + } else { + stopAutoSave(); + } + } + + function stopAutoSave() { + if (autoInterval){ + clearInterval(autoInterval) + } + } + + function renderPreview() { + if (!preview) { + return; + } + preview.html(render(val())); + $('pre code', preview).each(function(i, e) { + hljs.highlightBlock(e) + }); + } + + function getScrollHeight($prevFrame) { + // Different browsers attach the scrollHeight of a document to different + // elements, so handle that here. + if ($prevFrame[0].scrollHeight !== undefined) { + return $prevFrame[0].scrollHeight; + } else if ($prevFrame.find('html')[0].scrollHeight !== undefined && + $prevFrame.find('html')[0].scrollHeight !== 0) { + return $prevFrame.find('html')[0].scrollHeight; + } else { + return $prevFrame.find('body')[0].scrollHeight; + } + } + + function getPreviewWrapper(obj) { + // Attempts to get the wrapper for preview based on overflow prop + if (!obj) { + return; + } + if (obj.css('overflow') == 'auto' || obj.css('overflow') == 'scroll') { + return obj; + } else { + return getPreviewWrapper(obj.parent()); + } + } + + function syncPreview() { + + var editorScrollRange = (editor.getSession().getLength()); + + var previewScrollRange = (getScrollHeight(preview)); + + // Find how far along the editor is (0 means it is scrolled to the top, 1 + // means it is at the bottom). + var scrollFactor = editor.getFirstVisibleRow() / editorScrollRange; + + // Set the scroll position of the preview pane to match. jQuery will + // gracefully handle out-of-bounds values. + + previewWrapper.scrollTop(scrollFactor * previewScrollRange); + } + + function asyncLoad(filename, cb) { + (function (d, t) { + + var leScript = d.createElement(t) + , scripts = d.getElementsByTagName(t)[0]; + + leScript.async = 1; + leScript.src = filename; + scripts.parentNode.insertBefore(leScript, scripts); + + leScript.onload = function () { + cb && cb(); + } + + }(document, 'script')); + } + + function setTheme(theme) { + var cb = function(theme) { + editor.setTheme('ace/theme/'+theme); + updateProfile({theme: theme}); + }; + + if (loadedThemes[theme]) { + cb(theme); + } else { + asyncLoad(options.themePath + "/theme-" + theme + ".js", function () { + cb(theme); + loadedThemes[theme] = true; + }); + } + } + + function initSyncPreview() { + if (!preview || !options.syncPreview) return; + previewWrapper = getPreviewWrapper(preview); + window.onload = function () { + /** + * Bind synchronization of preview div to editor scroll and change + * of editor cursor position. + */ + editor.session.on('changeScrollTop', syncPreview); + editor.session.selection.on('changeCursor', syncPreview); + }; + } + + function initProps() { + // Id of editor + if (typeof settings == 'string') { + settings = { editor: settings }; + } + + if ('theme' in profile && profile['theme']) { + settings['theme'] = profile['theme']; + } + + if (settings['preview'] && !settings.hasOwnProperty('syncPreview')) { + settings['syncPreview'] = true; + } + + $.extend(options, settings); + + if (options.editor) { + element = toJquery(options.editor); + } + + $.each(options, function(k, v){ + if (element.data(k.toLowerCase())) { + options[k] = element.data(k.toLowerCase()); + } + }); + + if (options.themeSelect) { + themeSelect = toJquery(options.themeSelect); + } + + if (options.submitBtn) { + var submitBtn = toJquery(options.submitBtn); + submitBtn.click(function(){ + submit(); + }); + } + + if (options.preview) { + preview = toJquery(options.preview); + + // Enable sync unless set otherwise + if (!settings.hasOwnProperty('syncPreview')) { + options['syncPreview'] = true; + } + } + + if (!element.attr('id')) { + // No id, make one! + id = Math.random().toString(36).substring(7); + element.attr('id', id); + } else { + id = element.attr('id') + } + } + + function initEditor() { + editor = ace.edit(id); + setTheme(profile.theme || options.theme); + editor.getSession().setMode('ace/mode/' + options.mode); + if (store.get(editorId()) && store.get(editorId()) != val()) { + editor.getSession().setValue(store.get(editorId())); + } + editor.getSession().setUseWrapMode(true); + editor.getSession().setTabSize(2); + editor.getSession().setUseSoftTabs(true); + editor.setShowPrintMargin(false); + editor.renderer.setShowInvisibles(true); + editor.renderer.setShowGutter(false); + + if (options.showButtonBar) { + var $btnBar = $('
' + buildThemeSelect().html() + '
') + element.find('.ace_content').before($btnBar); + + $(".aced-save", $btnBar).click(function(){ + submit(); + }); + + if ($.fn.chosen) { + $('select', $btnBar).chosen().change(function(){ + setTheme($(this).val()); + }); + } + } + + if (options.keyMaster) { + bindKeyboard(); + } + + if (preview) { + editor.getSession().on('change', function (e) { + renderPreview(); + }); + renderPreview(); + } + + if (themeSelect) { + themeSelect + .find('li > a') + .bind('click', function (e) { + setTheme($(e.target).data('value')); + $(e.target).blur(); + return false; + }); + } + + if (options.info) { + // If no info exists, save it to storage + if (!store.get(infoKey())) { + store.set(infoKey(), options.info); + } else { + // Check info in storage against one passed in + // for possible changes in data that may have occurred + var info = store.get(infoKey()); + if (info['sha'] != options.info['sha'] && !info['ignore']) { + // Data has changed since start of draft + $(document).trigger('shaMismatch'); + } + } + } + + $(this).trigger('ready'); + } + + function init() { + gc(); + initProfile(); + initProps(); + initEditor(); + initSyncPreview(); + autoSave(); + } + + init(); + + return { + editor: editor, + submit: submit, + val: val, + discard: discardDraft, + info: info + }; +} \ No newline at end of file diff --git a/realms/static/js/collaboration/firepad.js b/realms/static/js/collaboration/firepad.js new file mode 100644 index 0000000..ed6cab9 --- /dev/null +++ b/realms/static/js/collaboration/firepad.js @@ -0,0 +1,52 @@ +// Helper to get hash from end of URL or generate a random one. +function getExampleRef() { + var ref = new Firebase('https://' + Config['FIREBASE_HOSTNAME']); + var hash = window.location.hash.replace(/^#fp-/, ''); + if (hash) { + ref = ref.child(hash); + } else { + ref = ref.push(); // generate unique location. + window.location = window.location + '#fp-' + ref.name(); // add it as a hash to the URL. + } + return ref; +} + +function initFirepad() { + var new_ = true; + if (window.location.hash.lastIndexOf('#fp-', 0) === 0) { + new_ = false; + } + var firepadRef = getExampleRef(); + var session = aced.editor.session; + var content; + + if (new_) { + content = session.getValue(); + } + + // Firepad wants an empty editor + session.setValue(''); + + //// Create Firepad. + var firepad = Firepad.fromACE(firepadRef, aced.editor, { + defaultText: content + }); + + firepad.on('ready', function() { + startCollaboration(); + }); + + $(document).on('end-collaboration', function() { + firepad.dispose(); + }); +} + +$(document).on('loading-collaboration', function() { + initFirepad(true); +}); + +$(function(){ + if (window.location.hash.lastIndexOf('#fp-', 0) === 0) { + loadingCollaboration(); + } +}); \ No newline at end of file diff --git a/realms/static/js/collaboration/main.js b/realms/static/js/collaboration/main.js new file mode 100644 index 0000000..c8f89a5 --- /dev/null +++ b/realms/static/js/collaboration/main.js @@ -0,0 +1,36 @@ +var $startCollaborationBtn = $('#start-collaboration'); +var $endCollaborationBtn = $('#end-collaboration'); +var $loadingCollaborationBtn = $('#loading-collaboration'); + +function loadingCollaboration() { + $endCollaborationBtn.hide(); + $startCollaborationBtn.hide(); + $loadingCollaborationBtn.show(); + $(document).trigger('loading-collaboration'); +} + +function startCollaboration() { + $loadingCollaborationBtn.hide(); + $startCollaborationBtn.hide(); + $endCollaborationBtn.show(); + $(document).trigger('start-collaboration'); +} + +function endCollaboration() { + $loadingCollaborationBtn.hide(); + $endCollaborationBtn.hide(); + $startCollaborationBtn.show(); + $(document).trigger('end-collaboration'); +} + +$(function() { + $startCollaborationBtn.click(function(e) { + loadingCollaboration(); + e.preventDefault(); + }); + $endCollaborationBtn.click(function(e) { + endCollaboration(); + e.preventDefault(); + + }); +}); diff --git a/realms/static/js/collaboration/togetherjs.js b/realms/static/js/collaboration/togetherjs.js new file mode 100644 index 0000000..f91440a --- /dev/null +++ b/realms/static/js/collaboration/togetherjs.js @@ -0,0 +1,28 @@ +$(document).on('loading-collaboration', function() { + TogetherJS(); +}); + +$(document).on('end-collaboration', function() { + TogetherJS(); +}); + +TogetherJSConfig_toolName = "Collaboration"; +TogetherJSConfig_suppressJoinConfirmation = true; + +if (User.is_authenticated) { + TogetherJSConfig_getUserName = function () { + return User.username; + }; + + TogetherJSConfig_getUserAvatar = function () { + return User.avatar; + }; +} + +TogetherJSConfig_on_ready = function () { + startCollaboration(); +}; + +TogetherJSConfig_on_close = function () { + //endCollaboration(); +}; \ No newline at end of file diff --git a/realms/static/js/editor.js b/realms/static/js/editor.js index d4bb3ad..1c3635d 100644 --- a/realms/static/js/editor.js +++ b/realms/static/js/editor.js @@ -1,430 +1,77 @@ -function Aced(settings) { - var id, - options, - editor, - element, - preview, - previewWrapper, - profile, - autoInterval, - themes, - themeSelect, - loadedThemes = {}; +var $entry_markdown_header = $("#entry-markdown-header"); +var $entry_preview_header = $("#entry-preview-header"); +var $entry_markdown = $(".entry-markdown"); +var $entry_preview = $(".entry-preview"); - settings = settings || {}; +// Tabs +$entry_markdown_header.click(function(){ + $entry_markdown.addClass('active'); + $entry_preview.removeClass('active'); +}); - options = { - sanitize: true, - preview: null, - editor: null, - theme: 'idle_fingers', - themePath: '/static/vendor/ace-builds/src', - mode: 'markdown', - autoSave: true, - autoSaveInterval: 5000, - syncPreview: false, - keyMaster: false, - submit: function(data){ alert(data); }, - showButtonBar: false, - themeSelect: null, - submitBtn: null, - renderer: null, - info: null - }; +$entry_preview_header.click(function(){ + $entry_preview.addClass('active'); + $entry_markdown.removeClass('active'); +}); - themes = { - chrome: "Chrome", - clouds: "Clouds", - clouds_midnight: "Clouds Midnight", - cobalt: "Cobalt", - crimson_editor: "Crimson Editor", - dawn: "Dawn", - dreamweaver: "Dreamweaver", - eclipse: "Eclipse", - idle_fingers: "idleFingers", - kr_theme: "krTheme", - merbivore: "Merbivore", - merbivore_soft: "Merbivore Soft", - mono_industrial: "Mono Industrial", - monokai: "Monokai", - pastel_on_dark: "Pastel on Dark", - solarized_dark: "Solarized Dark", - solarized_light: "Solarized Light", - textmate: "TextMate", - tomorrow: "Tomorrow", - tomorrow_night: "Tomorrow Night", - tomorrow_night_blue: "Tomorrow Night Blue", - tomorrow_night_bright: "Tomorrow Night Bright", - tomorrow_night_eighties: "Tomorrow Night 80s", - twilight: "Twilight", - vibrant_ink: "Vibrant Ink" - }; - - function editorId() { - return "aced." + id; - } - - function infoKey() { - return editorId() + ".info"; - } - - function gc() { - // Clean up localstorage - store.forEach(function(key, val) { - var re = new RegExp("aced\.(.*?)\.info"); - var info = re.exec(key); - if (!info || !val.time) { - return; - } - - var id = info[1]; - - // Remove week+ old stuff - var now = new Date().getTime() / 1000; - - if (now > (val.time + 604800)) { - store.remove(key); - store.remove('aced.' + id); - } - }); - } - - function buildThemeSelect() { - var $sel = $(""); - $sel.append(''); - $.each(themes, function(k, v) { - $sel.append(""); - }); - return $("
").html($sel); - } - - function toJquery(o) { - return (typeof o == 'string') ? $("#" + o) : $(o); - } - - function initProfile() { - profile = {theme: ''}; - - try { - // Need to merge in any undefined/new properties from last release - // Meaning, if we add new features they may not have them in profile - profile = $.extend(true, profile, store.get('aced.profile')); - } catch (e) { } - } - - function updateProfile(obj) { - profile = $.extend(null, profile, obj); - store.set('profile', profile); - } - - function render(content) { - return (options.renderer) ? options.renderer(content) : content; - } - - function bindKeyboard() { - // CMD+s TO SAVE DOC - key('command+s, ctrl+s', function (e) { - submit(); - e.preventDefault(); - }); - - var saveCommand = { - name: "save", - bindKey: { - mac: "Command-S", - win: "Ctrl-S" +$(document).on('shaMismatch', function() { + bootbox.dialog({ + title: "Page has changed", + message: "This page has changed and differs from your draft. What do you want to do?", + buttons: { + ignore: { + label: "Ignore", + className: "btn-default", + callback: function() { + var info = aced.info(); + info['ignore'] = true; + aced.info(info); + } }, - exec: function () { - submit(); - } - }; - editor.commands.addCommand(saveCommand); - } - - function info(info) { - if (info) { - store.set(infoKey(), info); - } - return store.get(infoKey()); - } - - function val(val) { - // Alias func - if (val) { - editor.getSession().setValue(val); - } - return editor.getSession().getValue(); - } - - function discardDraft() { - stopAutoSave(); - store.remove(editorId()); - store.remove(infoKey()); - location.reload(); - } - - function save() { - store.set(editorId(), val()); - } - - function submit() { - store.remove(editorId()); - store.remove(editorId() + ".info"); - options.submit(val()); - } - - function autoSave() { - if (options.autoSave) { - autoInterval = setInterval(function () { - save(); - }, options.autoSaveInterval); - } else { - stopAutoSave(); - } - } - - function stopAutoSave() { - if (autoInterval){ - clearInterval(autoInterval) - } - } - - function renderPreview() { - if (!preview) { - return; - } - preview.html(render(val())); - $('pre code', preview).each(function(i, e) { - hljs.highlightBlock(e) - }); - } - - function getScrollHeight($prevFrame) { - // Different browsers attach the scrollHeight of a document to different - // elements, so handle that here. - if ($prevFrame[0].scrollHeight !== undefined) { - return $prevFrame[0].scrollHeight; - } else if ($prevFrame.find('html')[0].scrollHeight !== undefined && - $prevFrame.find('html')[0].scrollHeight !== 0) { - return $prevFrame.find('html')[0].scrollHeight; - } else { - return $prevFrame.find('body')[0].scrollHeight; - } - } - - function getPreviewWrapper(obj) { - // Attempts to get the wrapper for preview based on overflow prop - if (!obj) { - return; - } - if (obj.css('overflow') == 'auto' || obj.css('overflow') == 'scroll') { - return obj; - } else { - return getPreviewWrapper(obj.parent()); - } - } - - function syncPreview() { - - var editorScrollRange = (editor.getSession().getLength()); - - var previewScrollRange = (getScrollHeight(preview)); - - // Find how far along the editor is (0 means it is scrolled to the top, 1 - // means it is at the bottom). - var scrollFactor = editor.getFirstVisibleRow() / editorScrollRange; - - // Set the scroll position of the preview pane to match. jQuery will - // gracefully handle out-of-bounds values. - - previewWrapper.scrollTop(scrollFactor * previewScrollRange); - } - - function asyncLoad(filename, cb) { - (function (d, t) { - - var leScript = d.createElement(t) - , scripts = d.getElementsByTagName(t)[0]; - - leScript.async = 1; - leScript.src = filename; - scripts.parentNode.insertBefore(leScript, scripts); - - leScript.onload = function () { - cb && cb(); - } - - }(document, 'script')); - } - - function setTheme(theme) { - var cb = function(theme) { - editor.setTheme('ace/theme/'+theme); - updateProfile({theme: theme}); - }; - - if (loadedThemes[theme]) { - cb(theme); - } else { - asyncLoad(options.themePath + "/theme-" + theme + ".js", function () { - cb(theme); - loadedThemes[theme] = true; - }); - } - } - - function initSyncPreview() { - if (!preview || !options.syncPreview) return; - previewWrapper = getPreviewWrapper(preview); - window.onload = function () { - /** - * Bind synchronization of preview div to editor scroll and change - * of editor cursor position. - */ - editor.session.on('changeScrollTop', syncPreview); - editor.session.selection.on('changeCursor', syncPreview); - }; - } - - function initProps() { - // Id of editor - if (typeof settings == 'string') { - settings = { editor: settings }; - } - - if ('theme' in profile && profile['theme']) { - settings['theme'] = profile['theme']; - } - - if (settings['preview'] && !settings.hasOwnProperty('syncPreview')) { - settings['syncPreview'] = true; - } - - $.extend(options, settings); - - if (options.editor) { - element = toJquery(options.editor); - } - - $.each(options, function(k, v){ - if (element.data(k.toLowerCase())) { - options[k] = element.data(k.toLowerCase()); - } - }); - - if (options.themeSelect) { - themeSelect = toJquery(options.themeSelect); - } - - if (options.submitBtn) { - var submitBtn = toJquery(options.submitBtn); - submitBtn.click(function(){ - submit(); - }); - } - - if (options.preview) { - preview = toJquery(options.preview); - - // Enable sync unless set otherwise - if (!settings.hasOwnProperty('syncPreview')) { - options['syncPreview'] = true; - } - } - - if (!element.attr('id')) { - // No id, make one! - id = Math.random().toString(36).substring(7); - element.attr('id', id); - } else { - id = element.attr('id') - } - } - - function initEditor() { - editor = ace.edit(id); - setTheme(profile.theme || options.theme); - editor.getSession().setMode('ace/mode/' + options.mode); - if (store.get(editorId()) && store.get(editorId()) != val()) { - editor.getSession().setValue(store.get(editorId())); - } - editor.getSession().setUseWrapMode(true); - editor.getSession().setTabSize(2); - editor.getSession().setUseSoftTabs(true); - editor.setShowPrintMargin(false); - editor.renderer.setShowInvisibles(true); - editor.renderer.setShowGutter(false); - - if (options.showButtonBar) { - var $btnBar = $('
' + buildThemeSelect().html() + '
') - element.find('.ace_content').before($btnBar); - - $(".aced-save", $btnBar).click(function(){ - submit(); - }); - - if ($.fn.chosen) { - $('select', $btnBar).chosen().change(function(){ - setTheme($(this).val()); - }); - } - } - - if (options.keyMaster) { - bindKeyboard(); - } - - if (preview) { - editor.getSession().on('change', function (e) { - renderPreview(); - }); - renderPreview(); - } - - if (themeSelect) { - themeSelect - .find('li > a') - .bind('click', function (e) { - setTheme($(e.target).data('value')); - $(e.target).blur(); - return false; - }); - } - - if (options.info) { - // If no info exists, save it to storage - if (!store.get(infoKey())) { - store.set(infoKey(), options.info); - } else { - // Check info in storage against one passed in - // for possible changes in data that may have occurred - var info = store.get(infoKey()); - if (info['sha'] != options.info['sha'] && !info['ignore']) { - // Data has changed since start of draft - $(document).trigger('shaMismatch'); + discard: { + label: "Discard Draft", + className: "btn-danger", + callback: function() { + aced.discard(); + } + }, + changes: { + label: "Show Diff", + className: "btn-primary", + callback: function() { + bootbox.alert("Draft diff not done! Sorry"); } } } + }) +}); - $(this).trigger('ready'); +$(function(){ + $("#discard-draft-btn").click(function() { + aced.discard(); + }); + + $(".entry-markdown .floatingheader").click(function(){ + aced.editor.focus(); + }); + + $("#delete-draft-btn").click(function() { + bootbox.alert("Not Done Yet! Sorry"); + }); +}); + +var aced = new Aced({ + editor: $('#entry-markdown-content').find('.editor').attr('id'), + renderer: function(md) { return MDR.convert(md) }, + info: Commit.info, + submit: function(content) { + var data = { + name: $("#page-name").val(), + message: $("#page-message").val(), + content: content + }; + $.post(window.location, data, function() { + location.href = Config['RELATIVE_PATH'] + '/' + data['name']; + }); } - - function init() { - gc(); - initProfile(); - initProps(); - initEditor(); - initSyncPreview(); - autoSave(); - } - - init(); - - return { - editor: editor, - submit: submit, - val: val, - discard: discardDraft, - info: info - }; -} \ No newline at end of file +}); \ No newline at end of file diff --git a/realms/static/js/hbs-helpers.js b/realms/static/js/hbs-helpers.js new file mode 100644 index 0000000..62840b2 --- /dev/null +++ b/realms/static/js/hbs-helpers.js @@ -0,0 +1,12 @@ +// Handlebar helpers +Handlebars.registerHelper('well', function(options) { + return '
' + options.fn(this) + '
'; +}); + +Handlebars.registerHelper('well-sm', function(options) { + return '
' + options.fn(this) + '
'; +}); + +Handlebars.registerHelper('well-lg', function(options) { + return '
' + options.fn(this) + '
'; +}); diff --git a/realms/static/js/main.js b/realms/static/js/mdr.js similarity index 93% rename from realms/static/js/main.js rename to realms/static/js/mdr.js index 4023f6d..a532560 100644 --- a/realms/static/js/main.js +++ b/realms/static/js/mdr.js @@ -1,12 +1,6 @@ -// Handlebar helpers -Handlebars.registerHelper('well', function(options) { - return '
' + options.fn(this) + '
'; -}); +// Init highlight JS +hljs.initHighlightingOnLoad(); -/* © 2013 j201 - * https://github.com/j201/meta-marked */ - -// Splits the given string into a meta section and a markdown section if a meta section is present, else returns null function splitInput(str) { if (str.slice(0, 3) !== '---') return; @@ -16,6 +10,10 @@ function splitInput(str) { return metaEnd && [str.slice(0, metaEnd.index), str.slice(matcher.lastIndex)]; } +/* © 2013 j201 + * https://github.com/j201/meta-marked */ + +// Splits the given string into a meta section and a markdown section if a meta section is present, else returns null var metaMarked = function(src, opt, callback) { if (Object.prototype.toString.call(src) !== '[object String]') throw new TypeError('First parameter must be a string.'); @@ -51,9 +49,6 @@ marked.setOptions({ smartypants: false }); -// Init highlight JS -hljs.initHighlightingOnLoad(); - // Markdown Renderer var MDR = { meta: null, @@ -100,10 +95,12 @@ var MDR = { try { var template = Handlebars.compile(this.md); this.md = template(this.meta); - } catch(e) {} + } catch(e) { + console.log(e); + } } }, hook: function() { } -}; +}; \ No newline at end of file diff --git a/realms/templates/layout.html b/realms/templates/layout.html index 586bf02..51c20c3 100644 --- a/realms/templates/layout.html +++ b/realms/templates/layout.html @@ -7,6 +7,7 @@ {{ config.SITE_TITLE }} + {% for bundle in g.assets['css'] %} @@ -14,6 +15,7 @@ {% endassets %} {% endfor %} + {% block css %}{% endblock %} @@ -88,6 +90,20 @@ {% block body %}{% endblock %}
+ + + {% for bundle in g.assets['js'] %} {% assets bundle %} {% if bundle == 'editor.js' %} @@ -97,6 +113,8 @@ {% endif %} {% endassets %} {% endfor %} + {% block js %}{% endblock %} + diff --git a/realms/templates/wiki/edit.html b/realms/templates/wiki/edit.html index 38e7d83..fbf2253 100644 --- a/realms/templates/wiki/edit.html +++ b/realms/templates/wiki/edit.html @@ -1,77 +1,10 @@ {% extends 'layout.html' %} {% block js %} + {% if partials %} + {% endif %} {% if config.get('COLLABORATION') == 'firepad' %} + - + {% endif %} - {% if config.get('COLLABORATION') == 'togetherjs' %} - - + {% if config.get('COLLABORATION') == 'togetherjs' %} + + {% endif %} {% endblock %} @@ -225,68 +46,89 @@
- +
- +
-
- -
-
+
+
Markdown -
-
-
{{ content }}
+
+
+
{{ content }}
+
- -
-
+
+
Preview -
-
-
-
-
- - +
+
+
+
+
diff --git a/setup.py b/setup.py index aa0f1fc..ac9c306 100644 --- a/setup.py +++ b/setup.py @@ -8,7 +8,8 @@ with open('README.md') as f: with open('requirements.txt') as f: required = f.read().splitlines() -VERSION = '0.2.2' +with open('VERSION') as f: + VERSION = f.read().strip() CLASSIFIERS = [ 'Intended Audience :: Developers',