diff --git a/README.md b/README.md index a4ca93a..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. @@ -150,6 +167,21 @@ Access from your browser http://localhost:5000 +## Templating + +Realms uses handlebars partials to create templates. +Each page that you create can be imported as a partial. + +This page imports and uses a partial: + + http://realms.io/_edit/hbs + +This page contains the content of the partial: + + http://realms.io/_edit/example-tmpl + +I locked these pages to preserve them. +You may copy and paste into a new page to test. ## Author 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/bower.json b/bower.json index d06f5a0..2c0846e 100644 --- a/bower.json +++ b/bower.json @@ -1,18 +1,19 @@ { - "name": "realms", - "version": "0.1.2", - "dependencies": { - "components-bootstrap": "~3.2.0", - "components-font-awesome": "~4.2.0", - "jquery": "~1.11.1", - "highlightjs": "~8.0.0", - "handlebars": "~2.0.0", - "keymaster": "madrobby/keymaster", - "ace-builds": "~1.1.6", - "parsleyjs": "~2.0.3", - "marked": "~0.3.2", - "js-yaml": "~3.2.1", - "localforage": "~0.9.2", - "bootswatch-dist": "3.2.0-flatly" - } + "name": "realms", + "version": "0.1.2", + "dependencies": { + "components-bootstrap": "~3.2.0", + "components-font-awesome": "~4.2.0", + "jquery": "~1.11.1", + "highlightjs": "~8.0.0", + "handlebars": "~2.0.0", + "keymaster": "madrobby/keymaster", + "ace-builds": "~1.1.6", + "parsleyjs": "~2.0.3", + "marked": "~0.3.2", + "js-yaml": "~3.2.1", + "store-js": "~1.3.16", + "bootswatch-dist": "3.2.0-flatly", + "bootbox": "4.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 f2cd42c..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' @@ -52,7 +56,6 @@ CACHE_REDIS_DB = '0' #CACHE_TYPE = 'memcached' CACHE_MEMCACHED_SERVERS = ['127.0.0.1:11211'] - # Get ReCaptcha Keys for your domain here: # https://www.google.com/recaptcha/admin#whyrecaptcha RECAPTCHA_ENABLE = False @@ -75,6 +78,12 @@ REGISTRATION_ENABLED = True # Used by Flask-Login LOGIN_DISABLED = ALLOW_ANON +# None, firepad, or togetherjs +COLLABORATION = 'togetherjs' + +# Required for firepad +FIREBASE_HOSTNAME = None + # Page names that can't be modified WIKI_LOCKED_PAGES = [] # Depreciated variable name 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 270bf61..8b2162d 100644 --- a/realms/modules/wiki/assets.py +++ b/realms/modules/wiki/assets.py @@ -1,9 +1,10 @@ from realms import assets assets.register('editor.js', - 'vendor/localforage/dist/localforage.js', + 'vendor/store-js/store.js', + 'vendor/bootbox/bootbox.js', 'vendor/ace-builds/src/ace.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/models.py b/realms/modules/wiki/models.py index affacd2..9d2c8e4 100644 --- a/realms/modules/wiki/models.py +++ b/realms/modules/wiki/models.py @@ -139,11 +139,11 @@ class Wiki(): return cached # commit = gittle.utils.git.commit_info(self.repo[sha]) - name = self.cname_to_filename(name).encode('latin-1') + filename = self.cname_to_filename(name).encode('latin-1') sha = sha.encode('latin-1') try: - data = self.gittle.get_commit_files(sha, paths=[name]).get(name) + data = self.gittle.get_commit_files(sha, paths=[filename]).get(filename) if not data: return None partials = {} @@ -153,6 +153,7 @@ class Wiki(): for partial_name in meta['import']: partials[partial_name] = self.get_page(partial_name) data['partials'] = partials + data['info'] = self.get_history(name, limit=1)[0] return data except KeyError: @@ -175,10 +176,10 @@ class Wiki(): new = self.get_page(name, sha=new_sha) return ghdiff.diff(old['data'], new['data']) - def get_history(self, name): + def get_history(self, name, limit=100): file_path = self.cname_to_filename(name) versions = [] - walker = self.repo.get_walker(paths=[file_path], max_entries=100) + walker = self.repo.get_walker(paths=[file_path], max_entries=limit) for entry in walker: change_type = None for change in entry.changes(): diff --git a/realms/modules/wiki/views.py b/realms/modules/wiki/views.py index 4eb9cb3..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, @@ -74,7 +74,12 @@ def edit(name): name = remove_ext(data['name']) content = data.get('data') g.assets['js'].append('editor.js') - return render_template('wiki/edit.html', name=name, content=content, sha=data.get('sha'), partials=data.get('partials')) + return render_template('wiki/edit.html', + name=name, + content=content, + info=data.get('info'), + sha=data.get('sha'), + partials=data.get('partials')) else: return redirect(url_for('wiki.create', name=cname)) @@ -110,7 +115,10 @@ def create(name): return redirect(url_for('wiki.edit', name=cname)) g.assets['js'].append('editor.js') - return render_template('wiki/edit.html', name=cname, content="") + return render_template('wiki/edit.html', + name=cname, + content="", + info={}) @blueprint.route("/", defaults={'name': 'home'}) diff --git a/realms/static/css/style.css b/realms/static/css/style.css index d337b4c..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; } @@ -188,7 +188,6 @@ a.label { left: 0; padding: 40px 10px 10px 10px; overflow: auto; - //word-break: break-word; cursor: default; } @@ -217,7 +216,7 @@ a.label { background-color: #eee; } -#editor { +.editor { margin-top: 40px; } @@ -226,7 +225,7 @@ a.label { padding: 3px; } - #editor { + .editor { margin-top: 0; } 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 db7d9ad..1c3635d 100644 --- a/realms/static/js/editor.js +++ b/realms/static/js/editor.js @@ -1,529 +1,77 @@ -/* - Source is modified version of http://dillinger.io/ - */ -$(function () { - - var url_prefix = ""; - var sha = $("#sha").text(); - var $theme = $('#theme-list'); - var $preview = $('#preview'); - var $autosave = $('#autosave'); - var $wordcount = $('#wordcount'); - var $wordcounter = $('#wordcounter'); - var $pagename = $("#page-name"); - - var $entry_markdown_header = $("#entry-markdown-header"); - var $entry_preview_header = $("#entry-preview-header"); - - // Tabs - $entry_markdown_header.click(function(){ - $("section.entry-markdown").addClass('active'); - $("section.entry-preview").removeClass('active'); - }); - - $entry_preview_header.click(function(){ - $("section.entry-preview").addClass('active'); - $("section.entry-markdown").removeClass('active'); - }); - - - var editor; - var autoInterval; - var profile = { - theme: 'ace/theme/idle_fingers', - currentMd: '', - autosave: { - enabled: true, - interval: 3000 // might be too aggressive; don't want to block UI for large saves. - }, - current_filename: $pagename.val() - }; - - // Feature detect ish - var dillinger = 'dillinger'; - var dillingerElem = document.createElement(dillinger); - var dillingerStyle = dillingerElem.style; - var domPrefixes = 'Webkit Moz O ms Khtml'.split(' '); - - /// UTILS ================= - - /** - * Utility method to async load a JavaScript file. - * - * @param {String} The name of the file to load - * @param {Function} Optional callback to be executed after the script loads. - * @return {void} - */ - 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')); - } - - /** - * Grab the user's profile from localStorage and stash in "profile" variable. - * - * @return {Void} - */ - function getUserProfile() { - localforage.getItem('profile', function(p) { - profile = $.extend(true, profile, p); - if (profile.filename != $pagename.val()) { - setEditorValue(""); - updateUserProfile({ filename: $pagename.val(), currentMd: "" }); - } else { - if (profile.currentMd) { - setEditorValue(profile.currentMd); - } - } - }); - } - - /** - * Update user's profile in localStorage by merging in current profile with passed in param. - * - * @param {Object} An object containg proper keys and values to be JSON.stringify'd - * @return {Void} - */ - function updateUserProfile(obj) { - localforage.clear(); - localforage.setItem('profile', $.extend(true, profile, obj)); - } - - /** - * Utility method to test if particular property is supported by the browser or not. - * Completely ripped from Modernizr with some mods. - * Thx, Modernizr team! - * - * @param {String} The property to test - * @return {Boolean} - */ - function prefixed(prop) { - return testPropsAll(prop, 'pfx') - } - - /** - * A generic CSS / DOM property test; if a browser supports - * a certain property, it won't return undefined for it. - * A supported CSS property returns empty string when its not yet set. - * - * @param {Object} A hash of properties to test - * @param {String} A prefix - * @return {Boolean} - */ - function testProps(props, prefixed) { - - for (var i in props) { - - if (dillingerStyle[ props[i] ] !== undefined) { - return prefixed === 'pfx' ? props[i] : true; - } - - } - return false - } - - /** - * Tests a list of DOM properties we want to check against. - * We specify literally ALL possible (known and/or likely) properties on - * the element including the non-vendor prefixed one, for forward- - * compatibility. - * - * @param {String} The name of the property - * @param {String} The prefix string - * @return {Boolean} - */ - function testPropsAll(prop, prefixed) { - - var ucProp = prop.charAt(0).toUpperCase() + prop.substr(1) - , props = (prop + ' ' + domPrefixes.join(ucProp + ' ') + ucProp).split(' '); - - return testProps(props, prefixed); - } - - /** - * Normalize the transitionEnd event across browsers. - * - * @return {String} - */ - function normalizeTransitionEnd() { - - var transEndEventNames = - { - 'WebkitTransition': 'webkitTransitionEnd', 'MozTransition': 'transitionend', 'OTransition': 'oTransitionEnd', 'msTransition': 'msTransitionEnd' // maybe? - , 'transition': 'transitionend' - }; - - return transEndEventNames[ prefixed('transition') ]; - } - - - /** - * Returns the full text of an element and all its children. - * The script recursively traverses all text nodes, and returns a - * concatenated string of all texts. - * - * Taken from - * http://stackoverflow.com/questions/2653670/innertext-textcontent-vs-retrieving-each-text-node - * - * @param node - * @return {int} - */ - function getTextInElement(node) { - if (node.nodeType === 3) { - return node.data; - } - - var txt = ''; - - if (node = node.firstChild) do { - txt += getTextInElement(node); - } while (node = node.nextSibling); - - return txt; - } - - /** - * Counts the words in a string - * - * @param string - * @return int - */ - function countWords(string) { - var words = string.replace(/W+/g, ' ').match(/\S+/g); - return words && words.length || 0; - } - - - /** - * Initialize application. - * - * @return {Void} - */ - function init() { - // Attach to jQuery support object for later use. - $.support.transitionEnd = normalizeTransitionEnd(); - - initAce(); - - getUserProfile(); - - initUi(); - - bindPreview(); - - bindNav(); - - bindKeyboard(); - - autoSave(); - } - - function initAce() { - editor = ace.edit("editor"); - editor.focus(); - editor.setOptions({ - enableBasicAutocompletion: true - }); - } - - function initUi() { - // Set proper theme value in theme dropdown - fetchTheme(profile.theme, function () { - $theme.find('li > a[data-value="' + profile.theme + '"]').addClass('selected'); - - editor.setBehavioursEnabled(true); - editor.getSession().setUseWrapMode(true); - editor.setShowPrintMargin(false); - editor.getSession().setTabSize(2); - editor.getSession().setUseSoftTabs(true); - editor.renderer.setShowInvisibles(true); - editor.renderer.setShowGutter(false); - editor.getSession().setMode('ace/mode/markdown'); - setEditorValue(profile.currentMd || editor.getSession().getValue()); - previewMd(); - }); - - - // Set text for dis/enable autosave / word counter - $autosave.html(profile.autosave.enabled ? ' Disable Autosave' : ' Enable Autosave'); - $wordcount.html(!profile.wordcount ? ' Disabled Word Count' : ' Enabled Word Count'); - - $('.dropdown-toggle').dropdown(); - } - - - function clearSelection() { - setEditorValue(""); - previewMd(); - } - - function saveFile(isManual) { - - updateUserProfile({currentMd: editor.getSession().getValue()}); - - if (isManual) { - updateUserProfile({ currentMd: "" }); - - var data = { - name: $pagename.val(), - message: $("#page-message").val(), - content: editor.getSession().getValue() - }; - $.post(window.location, data, function() { - location.href = url_prefix + '/' + data['name']; - }); - } - - } - - function autoSave() { - - if (profile.autosave.enabled) { - autoInterval = setInterval(function() { - saveFile(); - }, profile.autosave.interval); - - } else { - clearInterval(autoInterval) - } - - } - - $("#save-native").on('click', function() { - saveFile(true); - }); - - - function resetProfile() { - // For some reason, clear() is not working in Chrome. - localforage.clear(); - - // Let's turn off autosave - profile.autosave.enabled = false; - localforage.removeItem('profile', function() { - window.location.reload(); - }); - } - - function changeTheme(e) { - // check for same theme - var $target = $(e.target); - if ($target.attr('data-value') === profile.theme) { - return; - } - else { - // add/remove class - $theme.find('li > a.selected').removeClass('selected'); - $target.addClass('selected'); - // grabnew theme - var newTheme = $target.attr('data-value'); - $(e.target).blur(); - fetchTheme(newTheme, function () { - - }); - } - } - - function fetchTheme(th, cb) { - var name = th.split('/').pop(); - asyncLoad("/static/vendor/ace-builds/src/theme-" + name + ".js", function () { - editor.setTheme(th); - cb && cb(); - updateBg(name); - updateUserProfile({theme: th}); - }); - - } - - function updateBg(name) { - // document.body.style.backgroundColor = bgColors[name] - } - - function setEditorValue(str) { - editor.getSession().setValue(str); - } - - function previewMd() { - $preview.html(MDR.convert(editor.getSession().getValue(), true)); - } - - function updateFilename(str) { - // Check for string because it may be keyup event object - var f; - if (typeof str === 'string') { - f = str; - } else { - f = getCurrentFilenameFromField(); - } - updateUserProfile({ current_filename: f }); - } - - - function showHtml() { - - // TODO: UPDATE TO SUPPORT FILENAME NOT JUST A RANDOM FILENAME - - var unmd = editor.getSession().getValue(); - - function _doneHandler(jqXHR, data, response) { - // console.dir(resp) - var resp = JSON.parse(response.responseText); - $('#myModalBody').text(resp.data); - $('#myModal').modal(); - } - - function _failHandler() { - alert("Roh-roh. Something went wrong. :("); - } - - var config = { - type: 'POST', - data: "unmd=" + encodeURIComponent(unmd), - dataType: 'json', - url: '/factory/fetch_html_direct', - error: _failHandler, - success: _doneHandler - }; - - $.ajax(config) - - } - - function toggleAutoSave() { - $autosave.html(profile.autosave.enabled ? ' Disable Autosave' : ' Enable Autosave'); - updateUserProfile({autosave: {enabled: !profile.autosave.enabled }}); - autoSave(); - } - - function bindPreview() { - editor.getSession().on('change', function (e) { - previewMd(); - }); - } - - function bindNav() { - - $theme - .find('li > a') - .bind('click', function (e) { - changeTheme(e); - return false; - }); - - $('#clear') - .on('click', function () { - clearSelection(); - return false; - }); - - $("#autosave") - .on('click', function () { - toggleAutoSave(); - return false; - }); - - $('#reset') - .on('click', function () { - resetProfile(); - return false; - }); - - $('#cheat'). - on('click', function () { - window.open("https://github.com/adam-p/markdown-here/wiki/Markdown-Cheatsheet", "_blank"); - return false; - }); - - } // end bindNav() - - - function bindKeyboard() { - // CMD+s TO SAVE DOC - key('command+s, ctrl+s', function (e) { - saveFile(true); - e.preventDefault(); // so we don't save the web page - native browser functionality - }); - - var saveCommand = { - name: "save", - bindKey: { - mac: "Command-S", - win: "Ctrl-S" - }, - exec: function () { - saveFile(true); - } - }; - - editor.commands.addCommand(saveCommand); - } - - init(); +var $entry_markdown_header = $("#entry-markdown-header"); +var $entry_preview_header = $("#entry-preview-header"); +var $entry_markdown = $(".entry-markdown"); +var $entry_preview = $(".entry-preview"); + +// Tabs +$entry_markdown_header.click(function(){ + $entry_markdown.addClass('active'); + $entry_preview.removeClass('active'); }); +$entry_preview_header.click(function(){ + $entry_preview.addClass('active'); + $entry_markdown.removeClass('active'); +}); -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; +$(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); + } + }, + 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"); + } + } + } + }) +}); + +$(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 syncPreview() { - var $ed = window.ace.edit('editor'); - var $prev = $('#preview'); - - var editorScrollRange = ($ed.getSession().getLength()); - - var previewScrollRange = (getScrollHeight($prev)); - - // Find how far along the editor is (0 means it is scrolled to the top, 1 - // means it is at the bottom). - var scrollFactor = $ed.getFirstVisibleRow() / editorScrollRange; - - // Set the scroll position of the preview pane to match. jQuery will - // gracefully handle out-of-bounds values. - $prev.parent().scrollTop(scrollFactor * previewScrollRange); -} - -window.onload = function () { - var $loading = $('#loading'); - - if ($.support.transition) { - $loading - .bind($.support.transitionEnd, function () { - $('#main').removeClass('bye'); - $loading.remove(); - }) - .addClass('fade_slow'); - } else { - $('#main').removeClass('bye'); - $loading.remove(); - } - - /** - * Bind synchronization of preview div to editor scroll and change - * of editor cursor position. - */ - window.ace.edit('editor').session.on('changeScrollTop', syncPreview); - window.ace.edit('editor').session.selection.on('changeCursor', syncPreview); -}; \ 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/main.js deleted file mode 100644 index 3a8a80f..0000000 --- a/realms/static/js/main.js +++ /dev/null @@ -1,111 +0,0 @@ -// Handlebar helpers -Handlebars.registerHelper('well', function(options) { - return '
' + options.fn(this) + '
'; -}); - -/* © 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; - - var matcher = /\n(\.{3}|\-{3})/g; - var metaEnd = matcher.exec(str); - - return metaEnd && [str.slice(0, metaEnd.index), str.slice(matcher.lastIndex)]; -} - -var metaMarked = function(src, opt, callback) { - if (Object.prototype.toString.call(src) !== '[object String]') - throw new TypeError('First parameter must be a string.'); - - var mySplitInput = splitInput(src); - if (mySplitInput) { - var meta; - try { - meta = jsyaml.safeLoad(mySplitInput[0]); - } catch(e) { - meta = null; - } - return { - meta: meta, - md: mySplitInput[1] - }; - } else { - return { - meta: null, - md: src - } - } -}; - -marked.setOptions({ - renderer: new marked.Renderer(), - gfm: true, - tables: true, - breaks: false, - pedantic: false, - sanitize: false, - smartLists: true, - smartypants: false -}); - -// Init highlight JS -hljs.initHighlightingOnLoad(); - -// Markdown Renderer -var MDR = { - meta: null, - md: null, - sanitize: false, // Override - parse: function(md){ return marked(md); }, - convert: function(md, sanitize){ - if (this.sanitize !== null) { - sanitize = this.sanitize; - } - this.md = md; - this.processMeta(); - try { - var html = this.parse(this.md); - } catch(e) { - return this.md; - } - - if (sanitize) { - // Causes some problems with inline styles - html = html_sanitize(html, function(url) { - try { - var prot = decodeURIComponent(unescape(url)) - .replace(/[^\w:]/g, '') - .toLowerCase(); - } catch (e) { - return ''; - } - if (prot.indexOf('javascript:') === 0) { - return ''; - } - return prot; - }, function(id){ - return id; - }); - } - this.hook(); - return html; - }, - - processMeta: function() { - var doc = metaMarked(this.md); - this.md = doc.md; - this.meta = doc.meta; - if (this.meta) { - try { - var template = Handlebars.compile(this.md); - this.md = template(this.meta); - } catch(e) {} - } - }, - - hook: function() { - } -}; diff --git a/realms/static/js/mdr.js b/realms/static/js/mdr.js new file mode 100644 index 0000000..a532560 --- /dev/null +++ b/realms/static/js/mdr.js @@ -0,0 +1,106 @@ +// Init highlight JS +hljs.initHighlightingOnLoad(); + +function splitInput(str) { + if (str.slice(0, 3) !== '---') return; + + var matcher = /\n(\.{3}|\-{3})/g; + var metaEnd = matcher.exec(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.'); + + var mySplitInput = splitInput(src); + if (mySplitInput) { + var meta; + try { + meta = jsyaml.safeLoad(mySplitInput[0]); + } catch(e) { + meta = null; + } + return { + meta: meta, + md: mySplitInput[1] + }; + } else { + return { + meta: null, + md: src + } + } +}; + +marked.setOptions({ + renderer: new marked.Renderer(), + gfm: true, + tables: true, + breaks: false, + pedantic: false, + sanitize: false, + smartLists: true, + smartypants: false +}); + +// Markdown Renderer +var MDR = { + meta: null, + md: null, + sanitize: true, // Override + parse: function(md){ return marked(md); }, + convert: function(md, sanitize) { + if (this.sanitize !== null) { + sanitize = this.sanitize; + } + this.md = md; + this.processMeta(); + try { + var html = this.parse(this.md); + } catch(e) { + return this.md; + } + + if (sanitize) { + // Causes some problems with inline styles + html = html_sanitize(html, function(url) { + try { + var prot = decodeURIComponent(url.toString()); + } catch (e) { + return ''; + } + if (prot.indexOf('javascript:') === 0) { + return ''; + } + return prot; + }, function(id){ + return id; + }); + } + this.hook(); + return html; + }, + + processMeta: function() { + var doc = metaMarked(this.md); + this.md = doc.md; + this.meta = doc.meta; + if (this.meta) { + try { + var template = Handlebars.compile(this.md); + this.md = template(this.meta); + } 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 7590888..fbf2253 100644 --- a/realms/templates/wiki/edit.html +++ b/realms/templates/wiki/edit.html @@ -1,133 +1,169 @@ {% extends 'layout.html' %} {% block js %} - + + + {% if partials %} + + {% endif %} - TogetherJSConfig_getUserAvatar = function () { - return {{ current_user.avatar|tojson }}; - }; - {% endif %} + {% if config.get('COLLABORATION') %} + + {% endif %} + + {% if config.get('COLLABORATION') == 'firepad' %} + + + + + {% endif %} + + {% if config.get('COLLABORATION') == 'togetherjs' %} + + + {% endif %} - TogetherJSConfig_on_ready = function () { - MDR.sanitize = true; - $("#preview").html(''); - $("#start-togetherjs").addClass('btn-danger').html('End Collaboration').prop('disabled', false); - }; - TogetherJSConfig_on_close = function () { - MDR.sanitize = false; - $("#start-togetherjs").removeClass('btn-danger').html('Collaborate').prop('disabled', false); - }; - - {% endblock %} {% block body %}
- +
- +
-
-
+
- +
+
+ +
+ {% endif %} - - + + + + +
+ + {% if name in config['LOCKED'] %} {% else %} - + - + {% endif %}
+
-
-
+
+
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',