├── .bin └── get-secrets ├── .gitignore ├── .jshintrc ├── .travis.yml ├── LICENSE ├── README.md ├── bower.json ├── client.src ├── base │ ├── collection-view.js │ ├── entities.js │ ├── item-view.js │ ├── layout-view.js │ ├── region.js │ ├── route.js │ └── router.js ├── core │ ├── app.js │ ├── assets │ │ ├── favicon.ico │ │ ├── img │ │ │ ├── bocoup.png │ │ │ └── jmeas.jpg │ │ └── styl │ │ │ ├── boxed-group.styl │ │ │ ├── button.styl │ │ │ ├── cursor.styl │ │ │ ├── entypo.styl │ │ │ ├── header.styl │ │ │ ├── index.styl │ │ │ ├── mixins.styl │ │ │ ├── notifications.styl │ │ │ ├── octicons.styl │ │ │ ├── scaffolding.styl │ │ │ ├── type.styl │ │ │ ├── util.styl │ │ │ └── variables.styl │ ├── entities │ │ └── user.js │ ├── index.js │ ├── router.js │ ├── services │ │ ├── auth.js │ │ ├── dev.js │ │ ├── env.js │ │ └── resource-cache.js │ └── views │ │ ├── footer-view │ │ ├── footer-view.hbs │ │ ├── footer-view.styl │ │ └── index.js │ │ ├── menu-views │ │ ├── auth-menu-view │ │ │ ├── index.js │ │ │ ├── menu-view.hbs │ │ │ └── menu-view.styl │ │ └── unauth-menu-view │ │ │ ├── index.js │ │ │ └── login-view.hbs │ │ ├── modal-wrapper │ │ ├── index.js │ │ ├── modal-wrapper.hbs │ │ └── modal-wrapper.styl │ │ ├── overlay │ │ ├── index.js │ │ └── overlay.styl │ │ ├── root-view │ │ └── index.js │ │ └── wrapper │ │ └── index.js ├── helpers │ ├── gistbook-helpers.js │ ├── github-api-helpers.js │ ├── handlebars │ │ └── scope-map.js │ ├── hbs-helpers.js │ ├── home-page.json │ └── scope-map.js ├── modules │ ├── about │ │ ├── about-route.js │ │ └── views │ │ │ └── about-view │ │ │ ├── about-view.hbs │ │ │ ├── about-view.styl │ │ │ └── index.js │ ├── contact │ │ ├── contact-route.js │ │ └── views │ │ │ └── contact-view │ │ │ ├── contact-view.hbs │ │ │ ├── contact-view.styl │ │ │ └── index.js │ ├── home │ │ └── home-route.js │ ├── logout │ │ └── logout-route.js │ ├── new │ │ └── new-route.js │ ├── profile │ │ ├── entities │ │ │ ├── gists.js │ │ │ ├── github-collection.js │ │ │ └── github-user.js │ │ ├── gistbook-route.js │ │ ├── profile-route.js │ │ └── views │ │ │ ├── gist-list │ │ │ └── index.js │ │ │ ├── gistbook-list-view │ │ │ ├── gistbook-list-view.hbs │ │ │ ├── gistbook-list-view.styl │ │ │ └── index.js │ │ │ ├── no-gists │ │ │ ├── index.js │ │ │ └── no-gists.hbs │ │ │ └── profile-view │ │ │ ├── index.js │ │ │ ├── profile-view.hbs │ │ │ └── profile-view.styl │ ├── settings │ │ ├── settings-route.js │ │ └── views │ │ │ ├── revoke-modal │ │ │ ├── index.js │ │ │ ├── revoke-modal.hbs │ │ │ └── revoke-modal.styl │ │ │ └── settings-view │ │ │ ├── index.js │ │ │ ├── settings-view.hbs │ │ │ └── settings-view.styl │ └── terms │ │ ├── terms-route.js │ │ └── views │ │ └── terms-view │ │ ├── index.js │ │ ├── terms-view.hbs │ │ └── terms.styl ├── shared │ ├── entities │ │ ├── gist.js │ │ └── gistbook.js │ └── views │ │ ├── error-views │ │ ├── error-page.styl │ │ ├── generic-error-view │ │ │ ├── generic-error-view.hbs │ │ │ └── index.js │ │ ├── not-found-view │ │ │ ├── index.js │ │ │ └── not-found-view.hbs │ │ └── rate-limit-view │ │ │ ├── index.js │ │ │ ├── rate-limit-view.hbs │ │ │ └── rate-limit-view.styl │ │ ├── existing-menu │ │ ├── existing-gist-menu.hbs │ │ ├── existing-gist-menu.styl │ │ └── index.js │ │ ├── gist-view │ │ ├── gist-view.hbs │ │ ├── gist-view.styl │ │ └── index.js │ │ ├── gistbook-view │ │ ├── gistbook-view.hbs │ │ ├── gistbook-view.styl │ │ ├── helpers │ │ │ ├── radio-helpers.js │ │ │ └── string-helpers.js │ │ ├── index.js │ │ └── views │ │ │ ├── ace-editor-view │ │ │ ├── ace-editor-view.hbs │ │ │ ├── ace-editor-view.styl │ │ │ └── index.js │ │ │ ├── output-view │ │ │ ├── index.js │ │ │ ├── output-view.hbs │ │ │ ├── output-view.styl │ │ │ ├── services │ │ │ │ ├── code-extractor.js │ │ │ │ ├── compiler.js │ │ │ │ └── module-bundler.js │ │ │ └── views │ │ │ │ ├── compile-error-view │ │ │ │ ├── compile-error-view.hbs │ │ │ │ ├── compile-error-view.styl │ │ │ │ └── index.js │ │ │ │ ├── document-view │ │ │ │ ├── document-view.hbs │ │ │ │ └── index.js │ │ │ │ └── iframe │ │ │ │ ├── iframe-view.styl │ │ │ │ └── index.js │ │ │ ├── sections │ │ │ ├── index.js │ │ │ └── sections.styl │ │ │ ├── text │ │ │ ├── display │ │ │ │ ├── display-text-view.styl │ │ │ │ └── index.js │ │ │ └── edit │ │ │ │ ├── edit-text-view.hbs │ │ │ │ ├── edit-text-view.styl │ │ │ │ └── index.js │ │ │ ├── title │ │ │ ├── display │ │ │ │ ├── display-title-view.hbs │ │ │ │ ├── display-title-view.styl │ │ │ │ └── index.js │ │ │ └── edit │ │ │ │ ├── edit-title-view.hbs │ │ │ │ ├── edit-title-view.styl │ │ │ │ └── index.js │ │ │ └── wrappers │ │ │ ├── controls-wrapper │ │ │ ├── controls-wrapper.hbs │ │ │ ├── controls-wrapper.styl │ │ │ └── index.js │ │ │ ├── display-wrapper │ │ │ ├── display-wrapper.hbs │ │ │ ├── display-wrapper.styl │ │ │ └── index.js │ │ │ └── edit-wrapper │ │ │ ├── edit-wrapper.hbs │ │ │ ├── edit-wrapper.styl │ │ │ └── index.js │ │ └── loading-view │ │ ├── index.js │ │ ├── loading-view.hbs │ │ └── loading-view.styl ├── shims │ ├── backbone-sync-shim.js │ ├── merge-options-shim.js │ ├── radio-shim.js │ ├── render-shim.js │ └── to-json-shim.js └── vendor │ └── state-router │ ├── route.js │ └── state-router.js ├── config └── _personal-access-token.json ├── deploy └── ansible │ ├── deploy.yml │ ├── group_vars │ └── all.yml │ ├── host_vars │ ├── 54.164.153.94 │ └── 54.173.215.88 │ ├── inventory │ ├── production │ └── staging │ ├── provision.yml │ └── roles │ ├── base │ └── tasks │ │ └── main.yml │ ├── deploy │ └── tasks │ │ └── main.yml │ ├── nginx │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── gistbook.conf │ ├── services │ ├── tasks │ │ └── main.yml │ └── templates │ │ └── gistbook.conf │ └── startup │ └── tasks │ └── main.yml ├── gruntfile.js ├── package.json └── server ├── app.js ├── helpers ├── auth-helpers.js ├── page-cache.js └── token-helpers.js ├── middleware ├── auth-callback.js ├── compile.js ├── env.js ├── login.js ├── logout.js ├── output.js ├── render.js └── verify.js └── views ├── index.hbs ├── layouts └── main.hbs └── partials ├── google-analytics.hbs ├── initial-data.hbs ├── livereload-config.hbs ├── mathjax-config.hbs └── semantic-ui-css.hbs /.bin/get-secrets: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const fs = require('fs'); 4 | const path = require('path'); 5 | 6 | const ssh = require('ssh2'); 7 | const userhome = require('userhome'); 8 | const bach = require('bach'); 9 | const chalk = require('chalk'); 10 | const mkdirp = require('mkdirp').sync; 11 | 12 | const LOCAL_SECRETS = './config/secrets'; 13 | const REMOTE_SECRETS = '/mnt/secrets'; 14 | const REMOTE_SERVER = 'nest.bocoup.com'; 15 | const USER = process.env.USER; 16 | const SSH_KEY = fs.readFileSync(userhome('.ssh/id_rsa')); 17 | 18 | console.log(chalk.green('getting secrets')); 19 | 20 | // todo: extract this into an npm module, sftp-get 21 | (function getSecrets(opts) { 22 | 23 | mkdirp(LOCAL_SECRETS); 24 | 25 | console.log('connecting to '+opts.username+'@'+REMOTE_SERVER+'...'); 26 | 27 | var remote = new ssh(); 28 | 29 | remote.on('error', function (err) { 30 | fail(err); 31 | }); 32 | 33 | remote.connect(opts); 34 | 35 | remote.on('ready', function () { 36 | remote.sftp(function (err, sftp) { 37 | if (err) { 38 | fail(err); 39 | } 40 | sftp.readdir(REMOTE_SECRETS, function(readdirErr, list) { 41 | if (readdirErr) { 42 | fail(readdirErr, 'unable to list files on remote machine'); 43 | } 44 | var resolveStreams = bach.parallel(list.map(function (entry) { 45 | return function() { 46 | var file = entry.filename; 47 | var remotePath = path.join(REMOTE_SECRETS, file); 48 | var localPath = path.join(LOCAL_SECRETS, file); 49 | var opts = {}; 50 | if (path.extname(file) == '.pem') { 51 | opts.mode = 0600; 52 | } 53 | console.log('downloading', file, 'to', localPath); 54 | return sftp.createReadStream(remotePath). 55 | pipe(fs.createWriteStream(path.resolve(localPath), opts)); 56 | }; 57 | })); 58 | resolveStreams(function (streamErr) { 59 | if (streamErr) { 60 | fail(streamErr); 61 | } 62 | console.log(chalk.green('downloads complete.')); 63 | remote.end(); 64 | }); 65 | }); 66 | }); 67 | }); 68 | }({ 69 | host: REMOTE_SERVER, 70 | username: process.argv[2]||USER, 71 | privateKey: SSH_KEY 72 | })); 73 | 74 | function fail(err, msg) { 75 | if (!msg) { 76 | msg = 'downloading secrets failed'; 77 | } 78 | console.log(chalk.red(msg)); 79 | if (process.env.DEBUG) { 80 | throw err; 81 | } 82 | process.exit(1); 83 | } 84 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | 5 | # Runtime data 6 | pids 7 | *.pid 8 | *.seed 9 | 10 | # Directory for instrumented libs generated by jscoverage/JSCover 11 | lib-cov 12 | 13 | # Coverage directory used by tools like istanbul 14 | coverage 15 | 16 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 17 | .grunt 18 | 19 | # Compiled binary addons (http://nodejs.org/api/addons.html) 20 | build/Release 21 | 22 | # Dependency directory 23 | # Deployed apps should consider commenting this line out: 24 | # see https://npmjs.org/doc/faq.html#Should-I-check-my-node_modules-folder-into-git 25 | node_modules 26 | 27 | tmp 28 | bower_components 29 | client.dev 30 | client.prod 31 | 32 | config/secrets 33 | config/personal-access-token.json 34 | config/client-info.json 35 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "camelcase" : false, 3 | "curly" : true, 4 | "eqeqeq" : true, 5 | "forin" : true, 6 | "immed" : true, 7 | "indent" : 2, 8 | "latedef" : true, 9 | "newcap" : true, 10 | "noarg" : true, 11 | "noempty" : true, 12 | "nonbsp" : true, 13 | "nonew" : true, 14 | "plusplus" : false, 15 | "quotmark" : "single", 16 | "undef" : true, 17 | "unused" : "vars", 18 | "strict" : false, 19 | "trailing" : true, 20 | "maxparams" : 4, 21 | "maxdepth" : 2, 22 | "maxstatements" : 15, 23 | "maxcomplexity" : 6, 24 | "maxlen" : 140, 25 | "expr" : true, 26 | 27 | "asi" : false, 28 | "boss" : false, 29 | "debug" : false, 30 | "eqnull" : false, 31 | "esnext" : true, 32 | "evil" : false, 33 | "expr" : false, 34 | "funcscope" : false, 35 | "globalstrict" : false, 36 | "iterator" : false, 37 | "lastsemic" : false, 38 | "laxbreak" : false, 39 | "laxcomma" : false, 40 | "loopfunc" : false, 41 | "maxerr" : 50, 42 | "multistr" : false, 43 | "notypeof" : false, 44 | "proto" : false, 45 | "scripturl" : false, 46 | "smarttabs" : false, 47 | "shadow" : false, 48 | "sub" : false, 49 | "supernew" : false, 50 | "validthis" : false, 51 | "noyield" : false, 52 | 53 | "browser" : true, 54 | "globals": { 55 | "console": true, 56 | "MathJax": true, 57 | "ace": true 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | before_script: 3 | - npm install -g grunt-cli 4 | node_js: 5 | - "0.10" 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2014 Gistbook 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Gistbook [![Travis build status](http://img.shields.io/travis/jmeas/gistbook.svg?style=flat)](https://travis-ci.org/jmeas/gistbook) 2 | 3 | This is the development repository for Gistbook, a web application that is no longer hosted. 4 | 5 | ### Installation 6 | 7 | Clone this repository. 8 | 9 | ```sh 10 | git clone https://github.com/jmeas/gistbook.git 11 | ``` 12 | 13 | Navigate into the root directory of the project and install the dependencies. 14 | 15 | ```sh 16 | cd gistbook && npm install 17 | ``` 18 | 19 | ### Developing locally 20 | 21 | If this is your first time setting up Gistbook locally, run `sudo npm run configure-hosts-local`. 22 | 23 | Next, create a personal access token for your Github account. You can generate one from 24 | your [Settings page](https://github.com/settings/applications). Make sure that it has `user` and `gist` access, 25 | otherwise it won't work. Copy the token down and place it in a file `config/personal-access-token.json`. There's 26 | an example file in the directory that shows you the format. 27 | 28 | To build and start the development version of the app, run `grunt work -f`. 29 | 30 | Once the app is built, you can access it at `http://gistbook.loc:3344`. 31 | 32 | _**Note:** Logging in through Github will only work on port 3344._ 33 | 34 | ### Deploying 35 | 36 | Gistbook is deployed to AWS using Ansible. Install it via: 37 | 38 | - All platforms: `pip install ansible` via [pip](http://pip.readthedocs.org/en/latest/installing.html) 39 | - OSX: `brew install ansible` via [homebrew](http://brew.sh/) 40 | - Linux: `apt-get/yum install ansible` 41 | 42 | Next, run `npm run get-secrets` if you haven't already. You'll only need to do this once. 43 | 44 | #### To Staging 45 | 46 | Run the `npm run deploy-staging` command from the root directory of the project. 47 | 48 | #### To Production 49 | 50 | Execute the `npm run deploy` command from the root directory of the project. Tag a new release 51 | on Github with the new version number. 52 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gistbook", 3 | "version": "0.2.1", 4 | "homepage": "https://github.com/Gistbook/gistbook", 5 | "authors": [ 6 | "Jmeas " 7 | ], 8 | "description": "A place to share code snippets on the web.", 9 | "keywords": [ 10 | "gist", 11 | "gistbook", 12 | "gists", 13 | "github", 14 | "javascript", 15 | "html", 16 | "css", 17 | "interactive", 18 | "snippets", 19 | "code", 20 | "social", 21 | "sharing" 22 | ], 23 | "license": "MIT", 24 | "ignore": [ 25 | "**/.*", 26 | "node_modules", 27 | "bower_components", 28 | "test" 29 | ], 30 | "dependencies": { 31 | "html5-reset": "~2.1.2", 32 | "nib": "~1.0.2", 33 | "octicons": "~2.1.1", 34 | "entypo": "*" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /client.src/base/collection-view.js: -------------------------------------------------------------------------------- 1 | // 2 | // CollectionView 3 | // 4 | 5 | import * as mn from 'marionette'; 6 | 7 | var CollectionView = mn.CollectionView.extend({}); 8 | 9 | export default CollectionView; 10 | -------------------------------------------------------------------------------- /client.src/base/entities.js: -------------------------------------------------------------------------------- 1 | // 2 | // BaseEntities 3 | // These are the base models and collections. They have been 4 | // updated to support resource caching via ETags, though 5 | // at the cost of much duplication of Backbone code. 6 | // 7 | 8 | import * as bb from 'backbone'; 9 | 10 | export var BaseModel = bb.Model.extend({}); 11 | export var BaseCollection = bb.Collection.extend({}); 12 | -------------------------------------------------------------------------------- /client.src/base/item-view.js: -------------------------------------------------------------------------------- 1 | // 2 | // ItemView 3 | // 4 | 5 | import * as mn from 'marionette'; 6 | 7 | var ItemView = mn.ItemView.extend({}); 8 | 9 | export default ItemView; 10 | -------------------------------------------------------------------------------- /client.src/base/layout-view.js: -------------------------------------------------------------------------------- 1 | // 2 | // LayoutView 3 | // 4 | 5 | import * as mn from 'marionette'; 6 | import Region from './region'; 7 | 8 | var LayoutView = mn.LayoutView.extend({ 9 | regionClass: Region 10 | }); 11 | 12 | export default LayoutView; 13 | -------------------------------------------------------------------------------- /client.src/base/region.js: -------------------------------------------------------------------------------- 1 | // 2 | // Region 3 | // 4 | 5 | import * as mn from 'marionette'; 6 | 7 | export default mn.Region.extend({}); 8 | -------------------------------------------------------------------------------- /client.src/base/route.js: -------------------------------------------------------------------------------- 1 | // 2 | // Base Route 3 | // The base route for this application 4 | // 5 | 6 | import StateRouter from '../vendor/state-router/state-router'; 7 | import rootView from '../core/views/root-view'; 8 | import loadingView from '../shared/views/loading-view'; 9 | import Error404View from '../shared/views/error-views/not-found-view'; 10 | import ErrorRateLimitView from '../shared/views/error-views/rate-limit-view'; 11 | import ErrorGenericView from '../shared/views/error-views/generic-error-view'; 12 | 13 | loadingView.render(); 14 | 15 | export default StateRouter.Route.extend({ 16 | constructor: function() { 17 | this.on({ 18 | enter: this._showLoadingView, 19 | error: this._showFetchErrorView 20 | }, this); 21 | StateRouter.Route.prototype.constructor.apply(this, arguments); 22 | }, 23 | 24 | // Append an overlay when the app is loading 25 | _showLoadingView: function() { 26 | // Share with Google analytics that the page transition has occurred 27 | if (window.ga) { window.ga('send', 'pageview'); } 28 | rootView.getRegion('container').$el.prepend(loadingView.$el); 29 | }, 30 | 31 | // If the error occurred when fetching, then we add that here 32 | _showFetchErrorView: function(e) { 33 | var errorViewOptions = {}; 34 | var statusCode = e ? e.status : undefined; 35 | var rateLimit = e ? e.getResponseHeader('X-RateLimit-Remaining') : undefined; 36 | if (!statusCode) { return; } 37 | var ErrorView = ErrorGenericView; 38 | if (statusCode === 404) { 39 | ErrorView = Error404View; 40 | } else if (statusCode === 403 && !rateLimit) { 41 | ErrorView = ErrorRateLimitView; 42 | errorViewOptions.resetTimestamp = e.getResponseHeader('X-RateLimit-Reset'); 43 | } 44 | rootView.getRegion('container').show(new ErrorView(errorViewOptions)); 45 | } 46 | }); 47 | -------------------------------------------------------------------------------- /client.src/base/router.js: -------------------------------------------------------------------------------- 1 | // 2 | // BaseRouter 3 | // Configures auth for our StateRouter 4 | // 5 | 6 | import * as Radio from 'radio'; 7 | import StateRouter from 'vendor/state-router/state-router'; 8 | 9 | export default StateRouter.extend({ 10 | initialize() { 11 | Radio.comply('router', 'navigate', route => { 12 | this.navigate(route, {trigger:true}); 13 | }); 14 | this.on('unauthorized', this._navigateToLogin, this); 15 | }, 16 | 17 | authorize(routeData) { 18 | var newRoute = routeData.linked; 19 | return !(newRoute.authorize && !Radio.request('auth', 'authorized')); 20 | }, 21 | 22 | _navigateToLogin() { 23 | this.navigate('login', {trigger:true}); 24 | } 25 | }); 26 | -------------------------------------------------------------------------------- /client.src/core/app.js: -------------------------------------------------------------------------------- 1 | // 2 | // app 3 | // Define our app, which is just a thin container for 4 | // everything else. 5 | // 6 | 7 | import * as bb from 'backbone'; 8 | import * as mn from 'marionette'; 9 | import * as Intercept from 'backbone.intercept'; 10 | import env from './services/env'; 11 | import auth from './services/auth'; 12 | import user from './entities/user'; 13 | import router from './router'; 14 | import overlay from './views/overlay'; 15 | import rootView from './views/root-view'; 16 | import modalWrapper from './views/modal-wrapper'; 17 | 18 | // Create the app 19 | var app = new mn.Application(); 20 | 21 | // Attach all of the things 22 | app.env = env; 23 | app.auth = auth; 24 | app.user = user; 25 | app.router = router; 26 | app.overlay = overlay; 27 | app.rootView = rootView; 28 | app.modalWrapper = modalWrapper; 29 | 30 | app.VERSION = window._initData.VERSION; 31 | 32 | // Attach it to the window for debugging 33 | window.app = app; 34 | 35 | // Once the app is ready, start history and interception 36 | app.on('start', () => { 37 | bb.history.start({pushState: true}); 38 | Intercept.start(); 39 | }); 40 | 41 | export default app; 42 | -------------------------------------------------------------------------------- /client.src/core/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/gistbook/2ee81e6fa7aee1c75ef83b06535898867a1403c9/client.src/core/assets/favicon.ico -------------------------------------------------------------------------------- /client.src/core/assets/img/bocoup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/gistbook/2ee81e6fa7aee1c75ef83b06535898867a1403c9/client.src/core/assets/img/bocoup.png -------------------------------------------------------------------------------- /client.src/core/assets/img/jmeas.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jamesplease/gistbook/2ee81e6fa7aee1c75ef83b06535898867a1403c9/client.src/core/assets/img/jmeas.jpg -------------------------------------------------------------------------------- /client.src/core/assets/styl/boxed-group.styl: -------------------------------------------------------------------------------- 1 | .boxed-group 2 | width 100% 3 | background #fff 4 | margin 0 auto 5 | border-radius 3px 6 | mediumShadow() 7 | 8 | .boxed-group-header 9 | background #9699bc 10 | border-bottom 1px solid #747796 11 | height 55px 12 | font-size 16px 13 | line-height 55px 14 | font-weight 200 15 | color #fff 16 | font-size 19px 17 | padding 0 30px 18 | border-radius 3px 3px 0 0 19 | 20 | .boxed-group-body 21 | color $mediumTextColor 22 | padding 10px 20px 20px 23 | 24 | p 25 | margin 0 10px 13px 26 | 27 | &:first-child 28 | margin-top 15px 29 | 30 | &:last-child 31 | margin 0 32 | 33 | button 34 | margin-left 10px 35 | 36 | ul.bullet 37 | list-style-type disc 38 | margin-left 30px 39 | 40 | li 41 | color $textColor 42 | font-weight bold 43 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/button.styl: -------------------------------------------------------------------------------- 1 | .compound-button 2 | display flex 3 | 4 | button, .button 5 | margin-right 0 6 | border-radius 3px 0 0 3px 7 | 8 | .button-count 9 | font-size 13px 10 | font-weight bold 11 | border-radius 0 3px 3px 0 12 | border 0 13 | line-height 26px 14 | height 28px 15 | padding 0 7px 16 | display block 17 | 18 | button, .button 19 | height 30px 20 | text-decoration none 21 | font-size 15px 22 | font-family Arial, sans-serif 23 | color lighten($purple, 38) 24 | border-radius 2px 25 | border none 26 | border-bottom 2px solid darken($purple, 10) 27 | display inline-block 28 | outline none 29 | box-sizing border-box 30 | background-color $purple 31 | padding 1px 12px 32 | 33 | &:hover 34 | background-color darken($purple, 10) 35 | 36 | &:disabled 37 | color #f1f1f1 38 | background-color #929292 39 | border-color #929292 40 | 41 | &.blue 42 | color lighten($teal, 55) 43 | background-color $teal 44 | border-bottom-color darken($teal, 10) 45 | 46 | &:hover, &:active 47 | background-color darken($teal, 10) 48 | 49 | &.danger 50 | color lighten($danger, 50) 51 | background-color $danger 52 | border-bottom-color darken($danger, 10) 53 | 54 | &:hover, &:active 55 | background-color darken($danger, 10) 56 | 57 | &.dark 58 | background-color $darkBlue 59 | color lighten($darkBlue, 50) 60 | border-bottom-color darken($darkBlue, 10) 61 | 62 | &:hover, &:active 63 | background-color darken($darkBlue, 10) 64 | 65 | &.large 66 | height 48px 67 | padding 0 15px 68 | font-size 19px 69 | 70 | .octicon 71 | font-size 19px 72 | 73 | &:hover, &:active 74 | padding 0 14px 75 | 76 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/cursor.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * Cursor 3 | * Better default cursors 4 | * 5 | */ 6 | 7 | html, 8 | body, 9 | button[disabled], 10 | input[disabled] 11 | cursor default 12 | 13 | code 14 | cursor text 15 | 16 | a, 17 | label, 18 | button, 19 | input[type="radio"], 20 | input[type="submit"], 21 | input[type="checkbox"] 22 | cursor pointer 23 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/header.styl: -------------------------------------------------------------------------------- 1 | header 2 | margin-bottom 35px 3 | smallShadow() 4 | 5 | h1 6 | font-size 21px 7 | font-weight bold 8 | 9 | .octicon 10 | font-size 24px 11 | 12 | a:hover 13 | color $textColor 14 | 15 | .pre-release 16 | font-size 14px 17 | color $betaBlue 18 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/index.styl: -------------------------------------------------------------------------------- 1 | // External libs 2 | @import 'html5-reset/assets/css/reset.css' 3 | @import 'octicons' 4 | @import 'entypo' 5 | 6 | @import 'scaffolding' 7 | 8 | @import 'cursor' 9 | @import 'type' 10 | @import 'util' 11 | @import 'button' 12 | 13 | @import 'header' 14 | @import 'boxed-group' 15 | @import 'notifications' 16 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/mixins.styl: -------------------------------------------------------------------------------- 1 | smallShadow() 2 | box-shadow 0 1px 3px rgba(0,0,0,.15) 3 | 4 | mediumShadow() 5 | box-shadow 0 1px 3px rgba(0,0,0,.25) 6 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/notifications.styl: -------------------------------------------------------------------------------- 1 | .notification 2 | border 1px solid #ddd 3 | background #f4f4f4 4 | padding 10px 5 | border-radius 3px 6 | color $mediumTextColor 7 | 8 | &.notification-info 9 | color $stateInfoText 10 | border-color $stateInfoBorder 11 | background $stateInfoBg 12 | 13 | &.notification-danger 14 | color $stateDangerText 15 | border-color $stateDangerBorder 16 | background $stateDangerBg 17 | 18 | &.notification-success 19 | color $stateSuccessText 20 | border-color $stateSuccessBorder 21 | background $stateSuccessBg 22 | 23 | &.notification-warning 24 | color $stateWarningText 25 | border-color $stateWarningBorder 26 | background $stateWarningBg 27 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/octicons.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * I had to modify octicons myself 3 | * so that it put the fonts in the correct 4 | * location 5 | * 6 | */ 7 | 8 | @font-face { 9 | font-family: 'octicons'; 10 | src: url('/fonts/octicons.eot?#iefix') format('embedded-opentype'), 11 | url('/fonts/octicons.woff') format('woff'), 12 | url('/fonts/octicons.ttf') format('truetype'), 13 | url('/fonts/octicons.svg#octicons') format('svg'); 14 | font-weight: normal; 15 | font-style: normal; 16 | } 17 | 18 | /* 19 | 20 | .octicon is optimized for 16px. 21 | .mega-octicon is optimized for 32px but can be used larger. 22 | 23 | */ 24 | .octicon { 25 | font: normal normal 16px octicons; 26 | line-height: 1; 27 | display: inline-block; 28 | text-decoration: none; 29 | -webkit-font-smoothing: antialiased; 30 | -moz-osx-font-smoothing: grayscale; 31 | -webkit-user-select: none; 32 | -moz-user-select: none; 33 | -ms-user-select: none; 34 | user-select: none; 35 | } 36 | .mega-octicon { 37 | font: normal normal 32px octicons; 38 | line-height: 1; 39 | display: inline-block; 40 | text-decoration: none; 41 | -webkit-font-smoothing: antialiased; 42 | -moz-osx-font-smoothing: grayscale; 43 | -webkit-user-select: none; 44 | -moz-user-select: none; 45 | -ms-user-select: none; 46 | user-select: none; 47 | } 48 | 49 | .octicon-alert:before { content: '\f02d'} /*  */ 50 | .octicon-alignment-align:before { content: '\f08a'} /*  */ 51 | .octicon-alignment-aligned-to:before { content: '\f08e'} /*  */ 52 | .octicon-alignment-unalign:before { content: '\f08b'} /*  */ 53 | .octicon-arrow-down:before { content: '\f03f'} /*  */ 54 | .octicon-arrow-left:before { content: '\f040'} /*  */ 55 | .octicon-arrow-right:before { content: '\f03e'} /*  */ 56 | .octicon-arrow-small-down:before { content: '\f0a0'} /*  */ 57 | .octicon-arrow-small-left:before { content: '\f0a1'} /*  */ 58 | .octicon-arrow-small-right:before { content: '\f071'} /*  */ 59 | .octicon-arrow-small-up:before { content: '\f09f'} /*  */ 60 | .octicon-arrow-up:before { content: '\f03d'} /*  */ 61 | .octicon-beer:before { content: '\f069'} /*  */ 62 | .octicon-book:before { content: '\f007'} /*  */ 63 | .octicon-bookmark:before { content: '\f07b'} /*  */ 64 | .octicon-briefcase:before { content: '\f0d3'} /*  */ 65 | .octicon-broadcast:before { content: '\f048'} /*  */ 66 | .octicon-browser:before { content: '\f0c5'} /*  */ 67 | .octicon-bug:before { content: '\f091'} /*  */ 68 | .octicon-calendar:before { content: '\f068'} /*  */ 69 | .octicon-check:before { content: '\f03a'} /*  */ 70 | .octicon-checklist:before { content: '\f076'} /*  */ 71 | .octicon-chevron-down:before { content: '\f0a3'} /*  */ 72 | .octicon-chevron-left:before { content: '\f0a4'} /*  */ 73 | .octicon-chevron-right:before { content: '\f078'} /*  */ 74 | .octicon-chevron-up:before { content: '\f0a2'} /*  */ 75 | .octicon-circle-slash:before { content: '\f084'} /*  */ 76 | .octicon-circuit-board:before { content: '\f0d6'} /*  */ 77 | .octicon-clippy:before { content: '\f035'} /*  */ 78 | .octicon-clock:before { content: '\f046'} /*  */ 79 | .octicon-cloud-download:before { content: '\f00b'} /*  */ 80 | .octicon-cloud-upload:before { content: '\f00c'} /*  */ 81 | .octicon-code:before { content: '\f05f'} /*  */ 82 | .octicon-color-mode:before { content: '\f065'} /*  */ 83 | .octicon-comment-add:before, 84 | .octicon-comment:before { content: '\f02b'} /*  */ 85 | .octicon-comment-discussion:before { content: '\f04f'} /*  */ 86 | .octicon-credit-card:before { content: '\f045'} /*  */ 87 | .octicon-dash:before { content: '\f0ca'} /*  */ 88 | .octicon-dashboard:before { content: '\f07d'} /*  */ 89 | .octicon-database:before { content: '\f096'} /*  */ 90 | .octicon-device-camera:before { content: '\f056'} /*  */ 91 | .octicon-device-camera-video:before { content: '\f057'} /*  */ 92 | .octicon-device-desktop:before { content: '\f27c'} /*  */ 93 | .octicon-device-mobile:before { content: '\f038'} /*  */ 94 | .octicon-diff:before { content: '\f04d'} /*  */ 95 | .octicon-diff-added:before { content: '\f06b'} /*  */ 96 | .octicon-diff-ignored:before { content: '\f099'} /*  */ 97 | .octicon-diff-modified:before { content: '\f06d'} /*  */ 98 | .octicon-diff-removed:before { content: '\f06c'} /*  */ 99 | .octicon-diff-renamed:before { content: '\f06e'} /*  */ 100 | .octicon-ellipsis:before { content: '\f09a'} /*  */ 101 | .octicon-eye-unwatch:before, 102 | .octicon-eye-watch:before, 103 | .octicon-eye:before { content: '\f04e'} /*  */ 104 | .octicon-file-binary:before { content: '\f094'} /*  */ 105 | .octicon-file-code:before { content: '\f010'} /*  */ 106 | .octicon-file-directory:before { content: '\f016'} /*  */ 107 | .octicon-file-media:before { content: '\f012'} /*  */ 108 | .octicon-file-pdf:before { content: '\f014'} /*  */ 109 | .octicon-file-submodule:before { content: '\f017'} /*  */ 110 | .octicon-file-symlink-directory:before { content: '\f0b1'} /*  */ 111 | .octicon-file-symlink-file:before { content: '\f0b0'} /*  */ 112 | .octicon-file-text:before { content: '\f011'} /*  */ 113 | .octicon-file-zip:before { content: '\f013'} /*  */ 114 | .octicon-flame:before { content: '\f0d2'} /*  */ 115 | .octicon-fold:before { content: '\f0cc'} /*  */ 116 | .octicon-gear:before { content: '\f02f'} /*  */ 117 | .octicon-gift:before { content: '\f042'} /*  */ 118 | .octicon-gist:before { content: '\f00e'} /*  */ 119 | .octicon-gist-secret:before { content: '\f08c'} /*  */ 120 | .octicon-git-branch-create:before, 121 | .octicon-git-branch-delete:before, 122 | .octicon-git-branch:before { content: '\f020'} /*  */ 123 | .octicon-git-commit:before { content: '\f01f'} /*  */ 124 | .octicon-git-compare:before { content: '\f0ac'} /*  */ 125 | .octicon-git-merge:before { content: '\f023'} /*  */ 126 | .octicon-git-pull-request-abandoned:before, 127 | .octicon-git-pull-request:before { content: '\f009'} /*  */ 128 | .octicon-globe:before { content: '\f0b6'} /*  */ 129 | .octicon-graph:before { content: '\f043'} /*  */ 130 | .octicon-heart:before { content: '\2665'} /* ♥ */ 131 | .octicon-history:before { content: '\f07e'} /*  */ 132 | .octicon-home:before { content: '\f08d'} /*  */ 133 | .octicon-horizontal-rule:before { content: '\f070'} /*  */ 134 | .octicon-hourglass:before { content: '\f09e'} /*  */ 135 | .octicon-hubot:before { content: '\f09d'} /*  */ 136 | .octicon-inbox:before { content: '\f0cf'} /*  */ 137 | .octicon-info:before { content: '\f059'} /*  */ 138 | .octicon-issue-closed:before { content: '\f028'} /*  */ 139 | .octicon-issue-opened:before { content: '\f026'} /*  */ 140 | .octicon-issue-reopened:before { content: '\f027'} /*  */ 141 | .octicon-jersey:before { content: '\f019'} /*  */ 142 | .octicon-jump-down:before { content: '\f072'} /*  */ 143 | .octicon-jump-left:before { content: '\f0a5'} /*  */ 144 | .octicon-jump-right:before { content: '\f0a6'} /*  */ 145 | .octicon-jump-up:before { content: '\f073'} /*  */ 146 | .octicon-key:before { content: '\f049'} /*  */ 147 | .octicon-keyboard:before { content: '\f00d'} /*  */ 148 | .octicon-law:before { content: '\f0d8'} /* */ 149 | .octicon-light-bulb:before { content: '\f000'} /*  */ 150 | .octicon-link:before { content: '\f05c'} /*  */ 151 | .octicon-link-external:before { content: '\f07f'} /*  */ 152 | .octicon-list-ordered:before { content: '\f062'} /*  */ 153 | .octicon-list-unordered:before { content: '\f061'} /*  */ 154 | .octicon-location:before { content: '\f060'} /*  */ 155 | .octicon-gist-private:before, 156 | .octicon-mirror-private:before, 157 | .octicon-git-fork-private:before, 158 | .octicon-lock:before { content: '\f06a'} /*  */ 159 | .octicon-logo-github:before { content: '\f092'} /*  */ 160 | .octicon-mail:before { content: '\f03b'} /*  */ 161 | .octicon-mail-read:before { content: '\f03c'} /*  */ 162 | .octicon-mail-reply:before { content: '\f051'} /*  */ 163 | .octicon-mark-github:before { content: '\f00a'} /*  */ 164 | .octicon-markdown:before { content: '\f0c9'} /*  */ 165 | .octicon-megaphone:before { content: '\f077'} /*  */ 166 | .octicon-mention:before { content: '\f0be'} /*  */ 167 | .octicon-microscope:before { content: '\f089'} /*  */ 168 | .octicon-milestone:before { content: '\f075'} /*  */ 169 | .octicon-mirror-public:before, 170 | .octicon-mirror:before { content: '\f024'} /*  */ 171 | .octicon-mortar-board:before { content: '\f0d7'} /* */ 172 | .octicon-move-down:before { content: '\f0a8'} /*  */ 173 | .octicon-move-left:before { content: '\f074'} /*  */ 174 | .octicon-move-right:before { content: '\f0a9'} /*  */ 175 | .octicon-move-up:before { content: '\f0a7'} /*  */ 176 | .octicon-mute:before { content: '\f080'} /*  */ 177 | .octicon-no-newline:before { content: '\f09c'} /*  */ 178 | .octicon-octoface:before { content: '\f008'} /*  */ 179 | .octicon-organization:before { content: '\f037'} /*  */ 180 | .octicon-package:before { content: '\f0c4'} /*  */ 181 | .octicon-paintcan:before { content: '\f0d1'} /*  */ 182 | .octicon-pencil:before { content: '\f058'} /*  */ 183 | .octicon-person-add:before, 184 | .octicon-person-follow:before, 185 | .octicon-person:before { content: '\f018'} /*  */ 186 | .octicon-pin:before { content: '\f041'} /*  */ 187 | .octicon-playback-fast-forward:before { content: '\f0bd'} /*  */ 188 | .octicon-playback-pause:before { content: '\f0bb'} /*  */ 189 | .octicon-playback-play:before { content: '\f0bf'} /*  */ 190 | .octicon-playback-rewind:before { content: '\f0bc'} /*  */ 191 | .octicon-plug:before { content: '\f0d4'} /*  */ 192 | .octicon-repo-create:before, 193 | .octicon-gist-new:before, 194 | .octicon-file-directory-create:before, 195 | .octicon-file-add:before, 196 | .octicon-plus:before { content: '\f05d'} /*  */ 197 | .octicon-podium:before { content: '\f0af'} /*  */ 198 | .octicon-primitive-dot:before { content: '\f052'} /*  */ 199 | .octicon-primitive-square:before { content: '\f053'} /*  */ 200 | .octicon-pulse:before { content: '\f085'} /*  */ 201 | .octicon-puzzle:before { content: '\f0c0'} /*  */ 202 | .octicon-question:before { content: '\f02c'} /*  */ 203 | .octicon-quote:before { content: '\f063'} /*  */ 204 | .octicon-radio-tower:before { content: '\f030'} /*  */ 205 | .octicon-repo-delete:before, 206 | .octicon-repo:before { content: '\f001'} /*  */ 207 | .octicon-repo-clone:before { content: '\f04c'} /*  */ 208 | .octicon-repo-force-push:before { content: '\f04a'} /*  */ 209 | .octicon-gist-fork:before, 210 | .octicon-repo-forked:before { content: '\f002'} /*  */ 211 | .octicon-repo-pull:before { content: '\f006'} /*  */ 212 | .octicon-repo-push:before { content: '\f005'} /*  */ 213 | .octicon-rocket:before { content: '\f033'} /*  */ 214 | .octicon-rss:before { content: '\f034'} /*  */ 215 | .octicon-ruby:before { content: '\f047'} /*  */ 216 | .octicon-screen-full:before { content: '\f066'} /*  */ 217 | .octicon-screen-normal:before { content: '\f067'} /*  */ 218 | .octicon-search-save:before, 219 | .octicon-search:before { content: '\f02e'} /*  */ 220 | .octicon-server:before { content: '\f097'} /*  */ 221 | .octicon-settings:before { content: '\f07c'} /*  */ 222 | .octicon-log-in:before, 223 | .octicon-sign-in:before { content: '\f036'} /*  */ 224 | .octicon-log-out:before, 225 | .octicon-sign-out:before { content: '\f032'} /*  */ 226 | .octicon-split:before { content: '\f0c6'} /*  */ 227 | .octicon-squirrel:before { content: '\f0b2'} /*  */ 228 | .octicon-star-add:before, 229 | .octicon-star-delete:before, 230 | .octicon-star:before { content: '\f02a'} /*  */ 231 | .octicon-steps:before { content: '\f0c7'} /*  */ 232 | .octicon-stop:before { content: '\f08f'} /*  */ 233 | .octicon-repo-sync:before, 234 | .octicon-sync:before { content: '\f087'} /*  */ 235 | .octicon-tag-remove:before, 236 | .octicon-tag-add:before, 237 | .octicon-tag:before { content: '\f015'} /*  */ 238 | .octicon-telescope:before { content: '\f088'} /*  */ 239 | .octicon-terminal:before { content: '\f0c8'} /*  */ 240 | .octicon-three-bars:before { content: '\f05e'} /*  */ 241 | .octicon-tools:before { content: '\f031'} /*  */ 242 | .octicon-trashcan:before { content: '\f0d0'} /*  */ 243 | .octicon-triangle-down:before { content: '\f05b'} /*  */ 244 | .octicon-triangle-left:before { content: '\f044'} /*  */ 245 | .octicon-triangle-right:before { content: '\f05a'} /*  */ 246 | .octicon-triangle-up:before { content: '\f0aa'} /*  */ 247 | .octicon-unfold:before { content: '\f039'} /*  */ 248 | .octicon-unmute:before { content: '\f0ba'} /*  */ 249 | .octicon-versions:before { content: '\f064'} /*  */ 250 | .octicon-remove-close:before, 251 | .octicon-x:before { content: '\f081'} /*  */ 252 | .octicon-zap:before { content: '\26A1'} /* ⚡ */ 253 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/scaffolding.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * scaffolding 3 | * Sets up the scaffolding for the app, which is 4 | * mainly just a simple flexbox layout 5 | * 6 | * 7 | */ 8 | 9 | html 10 | height 100% 11 | 12 | * 13 | box-sizing border-box 14 | 15 | // We use flex box to give us the 'sticky' footer 16 | body 17 | min-height 100% 18 | min-width $appWidth + 40px 19 | display flex 20 | flex-direction column 21 | background $tan 22 | 23 | main 24 | position relative 25 | flex 1 26 | 27 | header 28 | height 50px 29 | line-height 50px 30 | background #fff 31 | 32 | // This gives us the scaffolding of the stuff within the header. 33 | div 34 | display flex 35 | div.menu 36 | flex 1 37 | justify-content flex-end 38 | 39 | footer 40 | width $appWidth 41 | height 70px 42 | line-height 70px 43 | margin 40px auto 0 44 | border-top 1px solid $darkGrey 45 | 46 | // The header, main, and footer span the whole 47 | // width so that the borders span the whole width. 48 | // Each of them has a child div that we place the 49 | // contents in, which keep the size reasonable 50 | header, main, footer 51 | > div 52 | width $appWidth 53 | margin 0 auto 54 | 55 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/type.styl: -------------------------------------------------------------------------------- 1 | html 2 | font-size 14px 3 | line-height 1.3em 4 | font-family Helvetica, Arial, sans-serif 5 | color #333 6 | 7 | // More often than not I *don't* want 8 | // my links to be blue and underlined 9 | a 10 | color $textColor 11 | text-decoration none 12 | 13 | &:hover 14 | color $hoverLinkColor 15 | 16 | b, em 17 | font-weight bold 18 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/util.styl: -------------------------------------------------------------------------------- 1 | .center 2 | margin 0 auto 3 | 4 | .hide 5 | display none !important 6 | -------------------------------------------------------------------------------- /client.src/core/assets/styl/variables.styl: -------------------------------------------------------------------------------- 1 | $appWidth = 800px 2 | 3 | $tan = #e8e5df; 4 | $darkGrey = #c8c8c8; 5 | 6 | $borderColor = #e5e5e5 7 | $lightBorderColor = #f4f4f4 8 | $lightBackground = #f6f6f6 9 | 10 | $veryLightTextColor = #ccc 11 | $lightTextColor = #aaa 12 | $darkTextColor = #333 13 | $mediumTextColor = #666 14 | $textColor = #000 15 | $betaBlue = #5597d0 16 | 17 | $hoverLinkColor = #4183c4 18 | 19 | $purple = #b086bb 20 | $teal = #77acc0 21 | $danger = #a65d5d 22 | $darkBlue = #686c96 23 | 24 | $stateSuccessText = #3c763d 25 | $stateSuccessBg = #dff0d8 26 | $stateSuccessBorder = darken(spin($stateSuccessBg, -10deg), 5%) 27 | 28 | $stateInfoText = #31708f 29 | $stateInfoBg = #d9edf7 30 | $stateInfoBorder = darken(spin($stateInfoBg, -10deg), 7%) 31 | 32 | $stateWarningText = #8a6d3b 33 | $stateWarningBg = #fcf8e3 34 | $stateWarningBorder = darken(spin($stateWarningBg, -10deg), 5%) 35 | 36 | $stateDangerText = #a94442 37 | $stateDangerBg = #f2dede 38 | $stateDangerBorder = darken(spin($stateDangerBg, -10deg), 5%) 39 | -------------------------------------------------------------------------------- /client.src/core/entities/user.js: -------------------------------------------------------------------------------- 1 | // 2 | // GistbookUser 3 | // An object that represents the user of Gistbook 4 | // 5 | 6 | import * as bb from 'backbone'; 7 | import * as Radio from 'radio'; 8 | 9 | var userChannel = Radio.channel('user'); 10 | var authChannel = Radio.channel('auth'); 11 | 12 | var User = bb.Model.extend({ 13 | initialize() { 14 | this._configureEvents(); 15 | }, 16 | 17 | _configureEvents() { 18 | userChannel.reply('user', this); 19 | this.listenTo(authChannel, 'logout', this.clear); 20 | } 21 | }); 22 | 23 | var user = new User(window._initData.user); 24 | 25 | export default user; 26 | -------------------------------------------------------------------------------- /client.src/core/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // index 3 | // Bootstrap & start our application 4 | // 5 | 6 | // Load our shims. In general, 'shims' are things that directly modify 7 | // Backbone or Marionette. These shims set this app up to be 8 | // in a Marionette v3-like state. I cannot suggest you use them 9 | // unless you're absolutely sure you know what you're doing! 10 | import 'shims/radio-shim'; 11 | import 'shims/backbone-sync-shim'; 12 | import 'shims/render-shim'; 13 | import 'shims/to-json-shim'; 14 | import 'shims/merge-options-shim'; 15 | 16 | // Load and configure emojify 17 | import * as emojify from 'emojify.js'; 18 | emojify.setConfig({ 19 | ignore_emoticons: true, 20 | img_dir: '/img/emoji' 21 | }); 22 | 23 | // Load our Handlebars helpers 24 | import '../helpers/hbs-helpers'; 25 | 26 | // Setup dev environment 27 | import dev from './services/dev'; 28 | dev.start(); 29 | 30 | // Start our resource cache 31 | import './services/resource-cache'; 32 | 33 | // Load & start our app 34 | import app from './app'; 35 | app.start(); 36 | -------------------------------------------------------------------------------- /client.src/core/router.js: -------------------------------------------------------------------------------- 1 | // 2 | // router 3 | // The router loads up our Routes. Our Routes specify which 4 | // features are activated whenever the URL changes. 5 | // 6 | 7 | import Router from 'base/router'; 8 | import HomeRoute from 'modules/home/home-route'; 9 | import AboutRoute from 'modules/about/about-route'; 10 | import TermsRoute from 'modules/terms/terms-route'; 11 | import NewGistbookRoute from 'modules/new/new-route'; 12 | import LogoutRoute from 'modules/logout/logout-route'; 13 | import ContactRoute from 'modules/contact/contact-route'; 14 | import ProfileRoute from 'modules/profile/profile-route'; 15 | import GistbookRoute from 'modules/profile/gistbook-route'; 16 | import SettingsRoute from 'modules/settings/settings-route'; 17 | 18 | var GistbookRouter = Router.extend({ 19 | routes: { 20 | '': new HomeRoute(), 21 | 'new': new NewGistbookRoute(), 22 | 'about': new AboutRoute(), 23 | 'terms': new TermsRoute(), 24 | 'logout': new LogoutRoute(), 25 | 'contact': new ContactRoute(), 26 | 'settings': new SettingsRoute(), 27 | ':username': new ProfileRoute(), 28 | ':username/:gistbookId': new GistbookRoute() 29 | } 30 | }); 31 | 32 | export default new GistbookRouter(); 33 | -------------------------------------------------------------------------------- /client.src/core/services/auth.js: -------------------------------------------------------------------------------- 1 | // 2 | // auth 3 | // Manages anything and everything related to 4 | // authorization 5 | // 6 | 7 | import * as $ from 'jquery'; 8 | import * as bb from 'backbone'; 9 | import * as Radio from 'radio'; 10 | import * as cookies from 'cookies-js'; 11 | 12 | var COOKIE_NAME = 'token'; 13 | 14 | var Auth = bb.Model.extend({ 15 | defaults: { 16 | token: '', 17 | authorized: false 18 | }, 19 | 20 | initialize() { 21 | this.channel = Radio.channel('auth'); 22 | this.determineLogin(); 23 | this.configureEvents(); 24 | this._configureAjax(); 25 | }, 26 | 27 | // Log us out by destroying the token 28 | logout() { 29 | cookies.expire(COOKIE_NAME); 30 | this.set('authorized', false); 31 | this.set('token', ''); 32 | this.channel.trigger('logout'); 33 | }, 34 | 35 | // Determine if we're authorized based on the cookie 36 | determineLogin() { 37 | var token = cookies.get(COOKIE_NAME); 38 | 39 | // Set the status of our authorization 40 | if (token) { 41 | this.set('authorized', true); 42 | this.set('token', token); 43 | } else { 44 | this.set('authorized', false); 45 | } 46 | 47 | // Trigger it on the channel, and register it as a request 48 | this.trigger('authorize', { 49 | token: this.get('token'), 50 | authorized: this.get('authorized') 51 | }); 52 | }, 53 | 54 | // Register our events on the channel 55 | configureEvents() { 56 | this.channel.reply('authorized', () => { 57 | return this.get('authorized'); 58 | }); 59 | this.channel.reply('token', () => { 60 | return this.get('token'); 61 | }); 62 | this.channel.comply('logout', this.logout, this); 63 | }, 64 | 65 | // Include our token in every subsequent request 66 | _configureAjax() { 67 | var self = this; 68 | $.ajaxSetup({ 69 | beforeSend(jqXHR, settings) { 70 | if (self.get('authorized')) { 71 | jqXHR.setRequestHeader('Authorization', 'token ' + self.get('token')); 72 | } 73 | return true; 74 | } 75 | }); 76 | } 77 | }); 78 | 79 | export default new Auth(); 80 | -------------------------------------------------------------------------------- /client.src/core/services/dev.js: -------------------------------------------------------------------------------- 1 | // 2 | // dev 3 | // Things that we want to activate in dev mode only 4 | // 5 | 6 | import * as bb from 'backbone'; 7 | import * as mn from 'marionette'; 8 | import * as Radio from 'radio'; 9 | import env from './env'; 10 | 11 | export default { 12 | start() { 13 | if (env !== 'development') { return; } 14 | 15 | // Configure for the Marionette Inspector 16 | if (window.__agent && window.__agent.start) { 17 | window.__agent.start(bb, mn); 18 | } 19 | 20 | // Turn on DEBUG mode for Radio 21 | Radio.DEBUG = true; 22 | } 23 | }; 24 | -------------------------------------------------------------------------------- /client.src/core/services/env.js: -------------------------------------------------------------------------------- 1 | // 2 | // env 3 | // The app environment – one of `dev` or `prod` 4 | // 5 | 6 | import * as Radio from 'radio'; 7 | 8 | var envChannel = Radio.channel('env'); 9 | var env = window._initData.env; 10 | 11 | envChannel.reply({ 12 | env: env, 13 | dev: env === 'development', 14 | production: env === 'production', 15 | staging: env === 'staging' 16 | }); 17 | 18 | export default env; 19 | -------------------------------------------------------------------------------- /client.src/core/services/resource-cache.js: -------------------------------------------------------------------------------- 1 | // 2 | // Resource Cache 3 | // Caches API resources by ETag 4 | // 5 | 6 | import * as bb from 'backbone'; 7 | 8 | var cache = {}; 9 | 10 | // Configure jQuery to handle resources that haven't been 11 | // modified since the last request 12 | bb.$.ajaxSetup({ 13 | ifModified: true 14 | }); 15 | 16 | // Override Backbone.ajax to save & load cached resources 17 | bb.ajax = function(options, ...args) { 18 | var success = options.success; 19 | options.success = function(resp, textStatus, jqXHR) { 20 | var ETag = jqXHR.getResponseHeader('ETag'); 21 | if (textStatus === 'notmodified') { 22 | resp = cache[ETag]; 23 | } else if (ETag) { 24 | cache[ETag] = resp; 25 | } 26 | if (success) { 27 | success(resp, textStatus, jqXHR); 28 | } 29 | }; 30 | args.unshift(options); 31 | return bb.$.ajax.apply(bb.$, args); 32 | }; 33 | -------------------------------------------------------------------------------- /client.src/core/views/footer-view/footer-view.hbs: -------------------------------------------------------------------------------- 1 | 2 | © Gistbook 2014 3 | 4 | 5 | 10 | -------------------------------------------------------------------------------- /client.src/core/views/footer-view/footer-view.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * footer 3 | * Style the footer's text and contents in here 4 | * To adjust how the footer fits in with the rest of the 5 | * pieces of the app, see the scaffolding file 6 | * 7 | */ 8 | 9 | footer 10 | color $lightTextColor 11 | font-size 12px 12 | position relative 13 | 14 | div 15 | display flex 16 | 17 | .copyright 18 | flex 1 19 | 20 | .mega-octicon 21 | color #bbb 22 | position absolute 23 | left 47% 24 | top 23px 25 | font-size 25px 26 | 27 | ul 28 | display flex 29 | align-self flex-end 30 | 31 | li 32 | margin-left 35px 33 | 34 | a 35 | color $lightTextColor 36 | 37 | &:hover 38 | color $lightTextColor 39 | text-decoration underline 40 | -------------------------------------------------------------------------------- /client.src/core/views/footer-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // FooterView 3 | // 4 | 5 | import LayoutView from 'base/layout-view'; 6 | 7 | export default LayoutView.extend({ 8 | template: 'footerView' 9 | }); 10 | -------------------------------------------------------------------------------- /client.src/core/views/menu-views/auth-menu-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // MenuView 3 | // The menu that's displayed once you're logged in 4 | // 5 | 6 | import ItemView from 'base/item-view'; 7 | 8 | export default ItemView.extend({ 9 | tagName: 'nav', 10 | template: 'menuView', 11 | 12 | ui: { 13 | $menuToggle: '.octicon-triangle-down', 14 | $menuItem: '.dropdown-menu a', 15 | $dropdown: '.dropdown-menu', 16 | $dropdownMask: '.dropdown-mask' 17 | }, 18 | 19 | events: { 20 | 'click @ui.$menuItem': 'hideMenu', 21 | 'click @ui.$dropdownMask': 'hideMenu', 22 | 'click @ui.$menuToggle': 'toggleMenu' 23 | }, 24 | 25 | toggleMenu() { 26 | this._modifyClass('toggle'); 27 | }, 28 | 29 | hideMenu() { 30 | this._modifyClass('remove'); 31 | }, 32 | 33 | _modifyClass(methodName) { 34 | this.ui.$dropdownMask[methodName + 'Class']('show'); 35 | this.ui.$dropdown[methodName + 'Class']('show'); 36 | this.ui.$menuToggle[methodName + 'Class']('active'); 37 | } 38 | }); 39 | -------------------------------------------------------------------------------- /client.src/core/views/menu-views/auth-menu-view/menu-view.hbs: -------------------------------------------------------------------------------- 1 | 33 | 34 | -------------------------------------------------------------------------------- /client.src/core/views/menu-views/auth-menu-view/menu-view.styl: -------------------------------------------------------------------------------- 1 | .menu nav 2 | > ul 3 | list-style none 4 | display flex 5 | 6 | .menu-icon 7 | cursor pointer 8 | color $mediumTextColor 9 | 10 | > ul > li 11 | position relative 12 | margin-right 3px 13 | 14 | a 15 | display inline 16 | 17 | &:last-child 18 | margin-right 0 19 | 20 | .octicon-triangle-down 21 | color #aaa 22 | padding 3px 23 | cursor pointer 24 | 25 | &:hover, 26 | &.active 27 | color #333 28 | 29 | .new-button 30 | margin-right 30px 31 | 32 | .profile-link 33 | 34 | a 35 | font-weight bold 36 | 37 | img 38 | width 28px 39 | height 28px 40 | border-radius 14px 41 | vertical-align middle 42 | margin-right 2px 43 | 44 | .dropdown-mask 45 | display none 46 | z-index 19000 47 | position fixed 48 | height 100% 49 | width 100% 50 | left 0 51 | top 0 52 | 53 | &.show 54 | display block 55 | 56 | .dropdown-menu 57 | display none 58 | z-index 20000 59 | background #fff 60 | border-radius 3px 61 | position absolute 62 | bottom -80px 63 | right 0px 64 | border 1px solid $borderColor 65 | width 170px 66 | padding 5px 0 67 | box-shadow 1px 2px 3px rgba(0, 0, 0, .15); 68 | 69 | &.show 70 | display block 71 | 72 | li 73 | height 40px 74 | line-height 40px 75 | display block 76 | 77 | &:hover 78 | background $lightBackground 79 | 80 | a 81 | color #333 82 | box-sizing border-box 83 | padding-left 15px 84 | display block 85 | height 100% 86 | width 100% 87 | line-height 40px 88 | 89 | -------------------------------------------------------------------------------- /client.src/core/views/menu-views/unauth-menu-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // LoginView 3 | // A view that displays our login button in the 4 | // header 5 | // 6 | 7 | import * as bb from 'backbone'; 8 | import ItemView from 'base/item-view'; 9 | 10 | export default ItemView.extend({ 11 | template: 'loginView', 12 | 13 | events: { 14 | 'click a': 'onClickLogin' 15 | }, 16 | 17 | // Save the current page in sessionStorage 18 | onClickLogin() { 19 | if (!bb.history.fragment) { return; } 20 | sessionStorage.setItem('cachedFragment', bb.history.fragment); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /client.src/core/views/menu-views/unauth-menu-view/login-view.hbs: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | -------------------------------------------------------------------------------- /client.src/core/views/modal-wrapper/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // ModalWrapper 3 | // Provides the wrapping element for views you wish to 4 | // display as modals. Also manages the screen overlay. 5 | // 6 | 7 | import Wrapper from '../wrapper'; 8 | import * as Radio from 'radio'; 9 | 10 | var modalChannel = Radio.channel('modal'); 11 | var overlayChannel = Radio.channel('overlay'); 12 | 13 | var ModalWrapper = Wrapper.extend({ 14 | template: 'modalWrapper', 15 | 16 | className: 'modal-wrapper', 17 | 18 | ui: { 19 | close: '.close' 20 | }, 21 | 22 | events: { 23 | 'click @ui.close': 'destroyView' 24 | }, 25 | 26 | region: '.modal-window', 27 | 28 | initialize() { 29 | this.listenTo(overlayChannel, 'click', this.destroyView); 30 | modalChannel.comply({ 31 | show: this.showView, 32 | hide: this.destroyView 33 | }, this); 34 | }, 35 | 36 | onShowView(view) { 37 | overlayChannel.command('show'); 38 | this.$el.addClass('show'); 39 | }, 40 | 41 | onBeforeDestroyView() { 42 | overlayChannel.command('hide'); 43 | this.$el.removeClass('show'); 44 | } 45 | }); 46 | 47 | var modalWrapper = new ModalWrapper(); 48 | modalWrapper.render(); 49 | 50 | var body = document.getElementsByTagName('body')[0]; 51 | body.appendChild(modalWrapper.el); 52 | 53 | export default modalWrapper; 54 | -------------------------------------------------------------------------------- /client.src/core/views/modal-wrapper/modal-wrapper.hbs: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /client.src/core/views/modal-wrapper/modal-wrapper.styl: -------------------------------------------------------------------------------- 1 | .modal-wrapper 2 | display none 3 | overflow hidden 4 | width 100% 5 | height 100% 6 | top 0 7 | left 0 8 | 9 | &.show 10 | display block 11 | 12 | .modal-window 13 | position fixed 14 | top 15% 15 | left 0 16 | right 0 17 | z-index 20000 18 | background #fff 19 | border-radius 5px 20 | width 450px 21 | min-height 130px 22 | border 1px solid $borderColor 23 | box-shadow 1px 2px 15px rgba(0,0,0,.35) 24 | margin 0 auto 25 | -------------------------------------------------------------------------------- /client.src/core/views/overlay/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Overlay 3 | // A simple Backbone View that can be used to mask your application 4 | // The overlay z-index is 10000, so anything below that is masked 5 | // 6 | 7 | import * as Radio from 'radio'; 8 | import ItemView from 'base/item-view'; 9 | 10 | var overlayChannel = Radio.channel('overlay'); 11 | 12 | var Overlay = ItemView.extend({ 13 | className: 'overlay', 14 | 15 | template: false, 16 | 17 | initialize() { 18 | overlayChannel.comply({ 19 | show: this.show, 20 | hide: this.hide 21 | }, this); 22 | }, 23 | 24 | triggers: { 25 | click: 'click' 26 | }, 27 | 28 | onClick() { 29 | overlayChannel.trigger('click'); 30 | }, 31 | 32 | show() { 33 | this.$el.addClass('visible'); 34 | }, 35 | 36 | hide() { 37 | this.$el.removeClass('visible'); 38 | } 39 | }); 40 | var overlay = new Overlay(); 41 | 42 | var body = document.getElementsByTagName('body')[0]; 43 | body.appendChild(overlay.el); 44 | 45 | export default overlay; 46 | -------------------------------------------------------------------------------- /client.src/core/views/overlay/overlay.styl: -------------------------------------------------------------------------------- 1 | .overlay 2 | z-index 10000 3 | position fixed 4 | left 0 5 | top 0 6 | width 100% 7 | height 100% 8 | display none 9 | 10 | &.visible 11 | display block 12 | -------------------------------------------------------------------------------- /client.src/core/views/root-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // RootView 3 | // 4 | 5 | import * as bb from 'backbone'; 6 | import * as Radio from 'radio'; 7 | import LayoutView from 'base/layout-view'; 8 | import FooterView from '../footer-view'; 9 | import AuthMenuView from '../menu-views/auth-menu-view'; 10 | import UnauthMenuView from '../menu-views/unauth-menu-view'; 11 | 12 | var authChannel = Radio.channel('auth'); 13 | 14 | var RootView = LayoutView.extend({ 15 | el: bb.$('body'), 16 | 17 | regions: { 18 | header: '.menu', 19 | container: 'main > div', 20 | footer: 'footer' 21 | }, 22 | 23 | initialize() { 24 | this.getRegion('footer').show(new FooterView()); 25 | this.showMenu(); 26 | this.listenTo(authChannel, 'logout', this.showMenu); 27 | var containerRegion = this.getRegion('container'); 28 | Radio.comply('rootView', 'showIn:container', containerRegion.show, containerRegion); 29 | }, 30 | 31 | showMenu() { 32 | var auth = authChannel.request('authorized'); 33 | var model = auth ? Radio.request('user', 'user') : undefined; 34 | var View = auth ? AuthMenuView : UnauthMenuView; 35 | this.getRegion('header').show(new View({model: model})); 36 | } 37 | }); 38 | 39 | export default new RootView(); 40 | -------------------------------------------------------------------------------- /client.src/core/views/wrapper/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Wrapper 3 | // A LayoutView that only supports a single child, typically 4 | // because it 'wraps' that child to provide additional 5 | // functionality. This is a handy class to make the API simpler 6 | // 7 | 8 | import * as mn from 'marionette'; 9 | import LayoutView from 'base/layout-view'; 10 | 11 | export default LayoutView.extend({ 12 | wrapperOptions: ['region'], 13 | 14 | constructor(options) { 15 | LayoutView.prototype.constructor.apply(this, arguments); 16 | mn.mergeOptions(this, options, this.wrapperOptions); 17 | this._createRegions(); 18 | this._setEvents(); 19 | }, 20 | 21 | _createRegions() { 22 | this.addRegion('wrapRegion', this.region); 23 | }, 24 | 25 | _setEvents() { 26 | this.on('before:show', this.showWrappedView, this); 27 | }, 28 | 29 | // Show the view in the region 30 | showView(view) { 31 | this.triggerMethod('before:show:view', view); 32 | this.getRegion('wrapRegion').show(view); 33 | this.triggerMethod('show:view', view); 34 | }, 35 | 36 | // Destroy the view in the region 37 | destroyView() { 38 | var currentView = this.getRegion('wrapRegion').currentView; 39 | if (!currentView) { return false; } 40 | this.triggerMethod('before:destroy:view', currentView); 41 | this.getRegion('wrapRegion').empty(); 42 | this.triggerMethod('destroy:view', currentView); 43 | } 44 | }); 45 | 46 | -------------------------------------------------------------------------------- /client.src/helpers/gistbook-helpers.js: -------------------------------------------------------------------------------- 1 | // 2 | // gistbookHelpers 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import * as homePageGistbook from './home-page.json'; 7 | 8 | var authChannel = Radio.channel('auth'); 9 | var userChannel = Radio.channel('user'); 10 | 11 | var gistbookHelpers = { 12 | 13 | // Determine if `gist` is a Gistbook 14 | isGistbook(gist) { 15 | var files = gist.files; 16 | 17 | // Gistbooks are always single JSON file 18 | if (!files && files.length !== 1) { return false; } 19 | 20 | // They're always stored as `gistbook.json` 21 | return !!files['gistbook.json']; 22 | }, 23 | 24 | getHomePage() { 25 | return homePageGistbook; 26 | }, 27 | 28 | // Returns a new, empty Gistbook – like magic! 29 | newGistbook() { 30 | var author = 'Anonymous'; 31 | if (authChannel.request('authorized')) { 32 | author = userChannel.request('user').get('login'); 33 | } 34 | return { 35 | title: 'Anonymous Gistbook', 36 | author: author, 37 | pages: [gistbookHelpers.createPage()], 38 | public: true 39 | }; 40 | }, 41 | 42 | // Create an empty Gistbook page 43 | createPage() { 44 | return { 45 | pageName: '', 46 | sections: [ 47 | { 48 | type: 'text', 49 | source: 'This is a new Gistbook.' 50 | }, 51 | { 52 | type: 'html', 53 | source: '' 54 | }, 55 | { 56 | type: 'css', 57 | source: '' 58 | }, 59 | { 60 | type: 'javascript', 61 | source: '' 62 | } 63 | ] 64 | }; 65 | }, 66 | 67 | // Takes in a Github Gist, get back a Gistbook Javascript object 68 | gistbookFromGist(gist) { 69 | return gistbookHelpers.parseGistfile(gist.get('files')['gistbook.json']); 70 | }, 71 | 72 | // Parses the contents of a gistbook.json file 73 | parseGistfile(file) { 74 | return JSON.parse(file.content); 75 | } 76 | }; 77 | 78 | export default gistbookHelpers; 79 | -------------------------------------------------------------------------------- /client.src/helpers/github-api-helpers.js: -------------------------------------------------------------------------------- 1 | // 2 | // githubApiHelpers 3 | // 4 | 5 | export default { 6 | url: 'https://api.github.com', 7 | version: 'v3', 8 | acceptHeader: {Accept: 'application/vnd.github.v3+json'} 9 | }; 10 | -------------------------------------------------------------------------------- /client.src/helpers/handlebars/scope-map.js: -------------------------------------------------------------------------------- 1 | // 2 | // Scope map helper 3 | // 4 | 5 | import * as hbs from 'handlebars'; 6 | import scopeMap from '../scope-map'; 7 | 8 | hbs.registerHelper('scopeMap', scopeName => { 9 | return scopeMap[scopeName] || scopeName; 10 | }); 11 | -------------------------------------------------------------------------------- /client.src/helpers/hbs-helpers.js: -------------------------------------------------------------------------------- 1 | // 2 | // Handlebars Helpers 3 | // Configure our handlebars helpers 4 | // 5 | 6 | import './handlebars/scope-map'; 7 | -------------------------------------------------------------------------------- /client.src/helpers/home-page.json: -------------------------------------------------------------------------------- 1 | {"title":"Welcome to Gistbook!","author":"Anonymous","pages":[{"pageName":"","sections":[{"type":"text","source":"## Welcome!\n\nGistbook is a place where you can write short entries about programming, math, and other technical subjects. Each book is made up of a series of blocks.\n\nText blocks, like the one you're reading now, support [Github-flavored Markdown](https://help.github.com/articles/github-flavored-markdown/) and [LaTeX](http://en.wikipedia.org/wiki/LaTeX), so you can **format text** and write equations such as \\(x = 3 + 5\\). It also supports display math, as in\n\n$$\\int_a^b{x^2\\mathrm{d}x}$$\n\nAnd, of course, no Gistbook would be complete without Emoji. :+1:\n\nIn addition to text, you can write code, too. Gistbook supports `html`, `css`, and `javascript`. \n\n\n"},{"type":"html","source":"\n
\n Start making Gistbooks today!\n
"},{"type":"css","source":"/* Give the document some style */\ndiv {\n padding: 20px;\n border: 1px solid #eee;\n border-radius: 4px;\n}\n\ndiv:first-child {\n border-bottom: 0;\n}"},{"type":"javascript","source":"// This code will execute when you hit Run\nconsole.log('You just ran this Gistbook');"},{"type":"text","source":"You can even require in Node modules.\n"},{"type":"javascript","source":"// Every node module is available to you in your Gistbooks\nvar math = require('mathjs');\nvar $ = require('jquery');\n\nfunction norm(x, y) {\n return math.sqrt(math.square(x) + math.square(y));\n}\n\n$('.output').text(norm(3, 4));"},{"type":"html","source":"
\n The norm that we calculated is: \n
"},{"type":"text","source":"To see this Gistbook in action, compile it by clicking the **run** button below :point_down:\n\nWant to make your own Gistbook? [**Click here**](/new).\n\n"}]}],"public":true} 2 | -------------------------------------------------------------------------------- /client.src/helpers/scope-map.js: -------------------------------------------------------------------------------- 1 | // 2 | // scopeMapHelpers 3 | // Maps Github's internal scope values to 4 | // user-friendly strings. I only mapped 5 | // the scopes that Gistbook is most likely 6 | // to use. 7 | // 8 | 9 | export default { 10 | user: 'User information', 11 | 'user:email': 'Email address', 12 | public_repo: 'Public repositories', 13 | repo: 'All repositories', 14 | gist: 'Gists' 15 | }; 16 | -------------------------------------------------------------------------------- /client.src/modules/about/about-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // AboutRoute 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import Route from 'base/route'; 7 | import AboutView from './views/about-view'; 8 | 9 | export default Route.extend({ 10 | show() { 11 | Radio.command('rootView', 'showIn:container', new AboutView()); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /client.src/modules/about/views/about-view/about-view.hbs: -------------------------------------------------------------------------------- 1 |

2 | Gistbook is the easiest way to create interactive 3 |
4 | code snippets in the browser. 5 |

6 |
7 | 16 | 25 |
26 |
27 |

28 | The source of Gistbook is available under the MIT license. 29 |

30 | 31 | 34 | 35 |
36 | -------------------------------------------------------------------------------- /client.src/modules/about/views/about-view/about-view.styl: -------------------------------------------------------------------------------- 1 | .about 2 | text-align center 3 | font-size 24px 4 | font-weight 200 5 | line-height 33px 6 | margin 20px 0 50px 7 | 8 | hr 9 | border-color $borderColor 10 | margin-bottom 60px 11 | 12 | .open-source 13 | text-align center 14 | color #444 15 | 16 | p 17 | margin-bottom 10px 18 | 19 | .about-container 20 | width 84% 21 | margin 0 auto 40px 22 | display flex 23 | border 1px solid #c3c3c3 24 | border-radius 3px 25 | 26 | > div:first-child 27 | border-right 1px solid #c3c3c3 28 | 29 | .creator, .sponsor 30 | padding 30px 0 31 | flex .5 32 | text-align center 33 | 34 | span, a 35 | font-size 15px 36 | font-weight 200 37 | 38 | img 39 | display inline-block 40 | margin-bottom 15px 41 | 42 | .creator img 43 | border-radius 50% 44 | -------------------------------------------------------------------------------- /client.src/modules/about/views/about-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // AboutView 3 | // 4 | 5 | import ItemView from 'base/item-view'; 6 | 7 | export default ItemView.extend({ 8 | template: 'aboutView' 9 | }); 10 | -------------------------------------------------------------------------------- /client.src/modules/contact/contact-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // ContactRoute 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import Route from 'base/route'; 7 | import ContactView from './views/contact-view'; 8 | 9 | export default Route.extend({ 10 | show() { 11 | Radio.command('rootView', 'showIn:container', new ContactView()); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /client.src/modules/contact/views/contact-view/contact-view.hbs: -------------------------------------------------------------------------------- 1 |

2 | Contact Us 3 |

4 |

5 | We love receiving messages. Get in touch if you find any issues, have feature requests, or just want to say hey. 6 |

7 |
8 |
9 | 10 |
11 |
12 |

Github Issues

13 |

14 | Our Github issue tracker is a great place to file bug reports or to request new features. 15 |

16 | 17 | 20 | 21 |
22 |
23 |
24 |
25 | 26 |
27 |
28 |

Email

29 |

30 | For any other questions or messages, send us an email at hello@gistbook.io. 31 |

32 | 33 | 36 | 37 |
38 |
39 | -------------------------------------------------------------------------------- /client.src/modules/contact/views/contact-view/contact-view.styl: -------------------------------------------------------------------------------- 1 | .contact-us 2 | width 75% 3 | 4 | h1, h2 5 | color #333 6 | 7 | h1 8 | font-size 20px 9 | font-weight 200 10 | margin-bottom 15px 11 | 12 | h2 13 | font-size 15px 14 | font-weight bold 15 | margin 0 0 8px 16 | 17 | p 18 | font-size 13px 19 | margin-bottom 10px 20 | 21 | a 22 | color #4183c4 23 | 24 | &:hover 25 | text-decoration underline 26 | 27 | .contact-github-issues, 28 | .contact-email 29 | display flex 30 | border-style solid 31 | border-color #c3c3c3 32 | padding 20px 33 | 34 | > div:first-child 35 | margin-right 20px 36 | display flex 37 | align-items center 38 | justify-content center 39 | 40 | .contact-github-issues 41 | border-width 1px 42 | border-radius 3px 3px 0 0 43 | 44 | .contact-email 45 | border-width 0 1px 1px 1px 46 | border-radius 0 0 3px 3px 47 | -------------------------------------------------------------------------------- /client.src/modules/contact/views/contact-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // ContactView 3 | // 4 | 5 | import ItemView from 'base/item-view'; 6 | 7 | export default ItemView.extend({ 8 | className: 'contact-us', 9 | template: 'contactView' 10 | }); 11 | -------------------------------------------------------------------------------- /client.src/modules/home/home-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // HomeRoute 3 | // Home is a kitchen sink Gistbook that demonstrates 4 | // all of the features of the app 5 | // 6 | 7 | import * as Radio from 'radio'; 8 | import Route from 'base/route'; 9 | import Gist from 'shared/entities/gist'; 10 | import GistView from 'shared/views/gist-view'; 11 | 12 | export default Route.extend({ 13 | 14 | // We check to see if the user has a cached uri fragment in session storage. 15 | // If they do, then that means they logged in from some other page. If that's the case, 16 | // then we want to redirect them to that page. 17 | redirect() { 18 | var cachedUriFragment = sessionStorage.getItem('cachedFragment'); 19 | if (!cachedUriFragment) { return; } 20 | sessionStorage.removeItem('cachedFragment'); 21 | return cachedUriFragment; 22 | }, 23 | 24 | show() { 25 | var gistView = new GistView({ 26 | model: new Gist(), 27 | ownGistbook: false, 28 | homePage: true 29 | }); 30 | Radio.command('rootView', 'showIn:container', gistView); 31 | } 32 | }); 33 | -------------------------------------------------------------------------------- /client.src/modules/logout/logout-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // LogoutRoute 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import Route from 'base/route'; 7 | 8 | var authChannel = Radio.channel('auth'); 9 | 10 | export default Route.extend({ 11 | redirect() { 12 | authChannel.command('logout'); 13 | return ''; 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /client.src/modules/new/new-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // NewRoute 3 | // The /new route is where we make a Gistbook. 4 | // 5 | 6 | import * as Radio from 'radio'; 7 | import Route from 'base/route'; 8 | import Gist from 'shared/entities/gist'; 9 | import GistView from 'shared/views/gist-view'; 10 | 11 | export default Route.extend({ 12 | show() { 13 | var gistView = new GistView({ 14 | model: new Gist(), 15 | newGist: true 16 | }); 17 | Radio.command('rootView', 'showIn:container', gistView); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client.src/modules/profile/entities/gists.js: -------------------------------------------------------------------------------- 1 | // 2 | // Gists 3 | // A Collection of a user's Gists that store Gistbooks 4 | // 5 | 6 | import * as _ from 'underscore'; 7 | import Gist from 'shared/entities/gist'; 8 | import GithubCollection from './github-collection'; 9 | import gistbookHelpers from 'helpers/gistbook-helpers'; 10 | 11 | export default GithubCollection.extend({ 12 | constructor(options) { 13 | options = options || {}; 14 | this.collectionUrl = options.collectionUrl || this.collectionUrl; 15 | GithubCollection.prototype.constructor.apply(this, arguments); 16 | }, 17 | 18 | model: Gist, 19 | 20 | // By default we attempt to get the authenticated user's gistbooks 21 | collectionUrl: '/gists', 22 | 23 | // Only some gists are gistbooks, so we need to filter those out 24 | parse(gists) { 25 | return _.filter(gists, gistbookHelpers.isGistbook); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /client.src/modules/profile/entities/github-collection.js: -------------------------------------------------------------------------------- 1 | // 2 | // GithubCollection 3 | // A collection that represents a Github API resource 4 | // 5 | 6 | import * as _ from 'underscore'; 7 | import { BaseCollection } from 'base/entities'; 8 | import githubApiHelpers from 'helpers/github-api-helpers'; 9 | 10 | export default BaseCollection.extend({ 11 | urlRoot: githubApiHelpers.url, 12 | 13 | // This is the property that we set on a per-collection basis 14 | collectionUrl: '', 15 | 16 | url() { 17 | return this.urlRoot + _.result(this, 'collectionUrl'); 18 | } 19 | }); 20 | -------------------------------------------------------------------------------- /client.src/modules/profile/entities/github-user.js: -------------------------------------------------------------------------------- 1 | // 2 | // GithubUser 3 | // A model representing a Github User 4 | // 5 | 6 | import * as bb from 'backbone'; 7 | import githubApiHelpers from 'helpers/github-api-helpers'; 8 | 9 | export default bb.Model.extend({ 10 | urlRoot() { 11 | return githubApiHelpers.url + '/users'; 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /client.src/modules/profile/gistbook-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // GistbookRoute 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import Route from 'base/route'; 7 | import GistView from 'shared/views/gist-view'; 8 | import Gist from 'shared/entities/gist'; 9 | 10 | export default Route.extend({ 11 | fetch(urlData) { 12 | this.gistbook = new Gist({ 13 | id: urlData.params.gistbookId 14 | }); 15 | return this.gistbook.fetch({ cache: false }); 16 | }, 17 | 18 | show(data, urlData) { 19 | var username = urlData.params.username; 20 | var user = Radio.request('user', 'user'); 21 | var view = new GistView({ 22 | model: this.gistbook, 23 | ownGistbook: user.get('login') === username 24 | }); 25 | Radio.command('rootView', 'showIn:container', view); 26 | } 27 | }); 28 | -------------------------------------------------------------------------------- /client.src/modules/profile/profile-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileRoute 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import Route from 'base/route'; 7 | import ProfileView from './views/profile-view'; 8 | import Gists from './entities/gists'; 9 | import GithubUser from './entities/github-user'; 10 | 11 | export default Route.extend({ 12 | fetch(urlData) { 13 | var user = Radio.request('user', 'user'); 14 | var isSelf = urlData.params.username === user.get('login'); 15 | 16 | var dataOptions = {}; 17 | if (!isSelf) { 18 | dataOptions.collectionUrl = '/users/' + urlData.params.username + '/gists'; 19 | } 20 | 21 | var Gistbooks = Gists.extend(dataOptions); 22 | this.gistbooks = new Gistbooks(); 23 | 24 | this.githubUser = this._getUser(isSelf, urlData); 25 | 26 | var fetchedData = [this.gistbooks.fetch({ cache: false })]; 27 | 28 | if (!isSelf) { 29 | fetchedData.push(this.githubUser.fetch({ cache: false })); 30 | } 31 | 32 | return Promise.all(fetchedData); 33 | }, 34 | 35 | show(data, urlData) { 36 | var user = Radio.request('user', 'user'); 37 | var username = urlData.params.username; 38 | var profileView = new ProfileView({ 39 | model: this.githubUser, 40 | collection: this.gistbooks, 41 | isSelf: user.get('login') === username 42 | }); 43 | Radio.command('rootView', 'showIn:container', profileView); 44 | }, 45 | 46 | _getUser(isSelf, urlData) { 47 | if (isSelf) { 48 | return Radio.request('user', 'user'); 49 | } else { 50 | return new GithubUser({ 51 | id: urlData.params.username 52 | }); 53 | } 54 | } 55 | }); 56 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/gist-list/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // GistList 3 | // 4 | 5 | import CollectionView from 'base/collection-view'; 6 | import GistbookListView from '../gistbook-list-view'; 7 | 8 | export default CollectionView.extend({ 9 | initialize(options) { 10 | this.model = options.model; 11 | this.isSelf = options.isSelf; 12 | }, 13 | 14 | tagName: 'ul', 15 | 16 | childView: GistbookListView, 17 | 18 | childViewOptions() { 19 | return { 20 | user: this.model, 21 | isSelf: this.isSelf 22 | }; 23 | }, 24 | }); 25 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/gistbook-list-view/gistbook-list-view.hbs: -------------------------------------------------------------------------------- 1 |
  • 2 |
    3 |
    4 | 5 | {{ description }} 6 |
    7 | {{#unless public }} 8 |
    9 | SECRET 10 |
    11 | {{/unless }} 12 |
    13 | 18 |
  • 19 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/gistbook-list-view/gistbook-list-view.styl: -------------------------------------------------------------------------------- 1 | .gistbook-list-view 2 | 3 | .gistbook-title 4 | margin-right 30px 5 | display flex 6 | 7 | .title-container 8 | white-space nowrap 9 | overflow hidden 10 | text-overflow ellipsis 11 | 12 | .secret-container 13 | margin 0 15px 0 5px 14 | 15 | .secret 16 | font-size 11px 17 | text-transform uppercase 18 | color #a1882b 19 | background-color #f8eec7 20 | border-radius 3px 21 | padding 3px 6px 2px 22 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/gistbook-list-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // GistbookListView 3 | // 4 | 5 | import ItemView from 'base/item-view'; 6 | 7 | export default ItemView.extend({ 8 | initialize(options) { 9 | this.user = options.user; 10 | this.isSelf = options.isSelf; 11 | }, 12 | 13 | className: 'gistbook-list-view', 14 | 15 | template: 'gistbookListView', 16 | 17 | ui: { 18 | delete: '.octicon-trashcan' 19 | }, 20 | 21 | events: { 22 | 'click @ui.delete': 'onDelete' 23 | }, 24 | 25 | onDelete() { 26 | if (window.confirm('Are you sure you want to delete this Gistbook?')) { 27 | this.model.destroy(); 28 | } 29 | }, 30 | 31 | templateHelpers() { 32 | return { 33 | username: this.user.escape('login') 34 | }; 35 | }, 36 | 37 | onRender() { 38 | if (this.isSelf) { 39 | this.ui.delete.removeClass('hide'); 40 | } 41 | } 42 | }); 43 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/no-gists/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // NoGistsView 3 | // Displayed when a user doesn't have any Gistbooks 4 | // 5 | 6 | import ItemView from 'base/item-view'; 7 | 8 | export default ItemView.extend({ 9 | className: 'notification notification-info', 10 | template: 'noGists' 11 | }); 12 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/no-gists/no-gists.hbs: -------------------------------------------------------------------------------- 1 | Aw shucks, it looks like {{ login }} hasn't created any Gistbooks yet! 2 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/profile-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // ProfileView 3 | // 4 | 5 | import * as mn from 'marionette'; 6 | import LayoutView from 'base/layout-view'; 7 | import NoGistsView from '../no-gists'; 8 | import GistList from '../gist-list'; 9 | 10 | export default LayoutView.extend({ 11 | profileViewOptions: ['isSelf'], 12 | 13 | initialize(options) { 14 | mn.mergeOptions(this, options, this.profileViewOptions); 15 | this.configureEvents(); 16 | }, 17 | 18 | className: 'profile-view', 19 | 20 | template: 'profileView', 21 | 22 | ui: { 23 | $count: '.gistbook-count > span' 24 | }, 25 | 26 | regions: { 27 | gistsContainer: '.boxed-group-body' 28 | }, 29 | 30 | onBeforeShow() { 31 | this.getRegion('gistsContainer').show(this.getChildView()); 32 | }, 33 | 34 | getChildView() { 35 | var childViewOptions = this.getChildViewOptions(); 36 | var ChildView = this.noGists() ? NoGistsView : GistList; 37 | 38 | return new ChildView(childViewOptions); 39 | }, 40 | 41 | getChildViewOptions() { 42 | return this.noGists() ? { 43 | model: this.model 44 | } : { 45 | model: this.model, 46 | collection: this.collection, 47 | isSelf: this.isSelf 48 | }; 49 | }, 50 | 51 | noGists() { 52 | return !this.collection.length; 53 | }, 54 | 55 | templateHelpers() { 56 | return { 57 | gistbooks: this.collection, 58 | gistbooksLabel: this.collection.length === 1 ? 'Gistbook' : 'Gistbooks' 59 | }; 60 | }, 61 | 62 | configureEvents() { 63 | this.listenTo(this.collection, 'add remove reset', this.onCollectionChange); 64 | }, 65 | 66 | onCollectionChange() { 67 | this.ui.$count.text(this.collection.length); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/profile-view/profile-view.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | Gistbooks 4 |
    5 |
    6 |
    7 |
    8 | {{ name }} 9 |
    10 | {{ name }} 11 | {{ login }} 12 |
    13 |
    14 | {{ gistbooks.length }} {{ gistbooksLabel }} 15 |
    16 | 17 | 21 | 22 |
    23 | -------------------------------------------------------------------------------- /client.src/modules/profile/views/profile-view/profile-view.styl: -------------------------------------------------------------------------------- 1 | .profile-view 2 | width 800px 3 | margin 0 auto 4 | display flex 5 | 6 | .profile-side-bar 7 | width 250px 8 | background #fff 9 | padding 30px 10 | border-radius 3px 11 | mediumShadow() 12 | text-align center 13 | 14 | img 15 | width 120px 16 | border-radius 60px 17 | margin-bottom 10px 18 | 19 | .profile-names 20 | padding-bottom 20px 21 | margin-bottom 20px 22 | border-bottom 1px solid $borderColor 23 | 24 | span 25 | display block 26 | 27 | &.full-name 28 | font-weight bold 29 | font-size 23px 30 | margin-bottom 10px 31 | &.username 32 | font-size 20px 33 | font-weight 200 34 | color $mediumTextColor 35 | 36 | .gistbook-count 37 | padding-bottom 20px 38 | margin-bottom 20px 39 | border-bottom 1px solid $borderColor 40 | color $lightTextColor 41 | 42 | span 43 | font-weight bold 44 | font-size 28px 45 | color $textColor 46 | 47 | .profile-list 48 | flex 1 49 | margin-right 30px 50 | 51 | .gistbook-title 52 | flex 1 53 | 54 | .notification 55 | margin-top 20px 56 | 57 | .gistbook-list 58 | height 40px 59 | line-height 40px 60 | display flex 61 | border-bottom 1px solid $borderColor 62 | padding 0 10px 63 | 64 | ul 65 | align-self flex-end 66 | 67 | a 68 | font-weight bold 69 | color $hoverLinkColor 70 | 71 | .octicon 72 | color $lightTextColor 73 | 74 | .octicon-trashcan 75 | cursor pointer 76 | -------------------------------------------------------------------------------- /client.src/modules/settings/settings-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsRoute 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import Route from 'base/route'; 7 | import SettingsView from './views/settings-view'; 8 | 9 | var user = Radio.request('user', 'user'); 10 | 11 | export default Route.extend({ 12 | 13 | // Redirect us to the home page if we're unauthorized 14 | // This is the only page that requires that you be authorized 15 | redirect() { 16 | return !Radio.request('auth', 'authorized') ? '' : false; 17 | }, 18 | 19 | show() { 20 | Radio.command('rootView', 'showIn:container', new SettingsView({ model: user})); 21 | } 22 | }); 23 | -------------------------------------------------------------------------------- /client.src/modules/settings/views/revoke-modal/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // RevokeModal 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import ItemView from 'base/item-view'; 7 | 8 | var modalChannel = Radio.channel('modal'); 9 | 10 | export default ItemView.extend({ 11 | template: 'revokeModal', 12 | 13 | ui: { 14 | confirm: 'button' 15 | }, 16 | 17 | events: { 18 | 'click @ui.confirm': 'onConfirm' 19 | }, 20 | 21 | onConfirm() { 22 | modalChannel.command('hide'); 23 | } 24 | }); 25 | -------------------------------------------------------------------------------- /client.src/modules/settings/views/revoke-modal/revoke-modal.hbs: -------------------------------------------------------------------------------- 1 | 5 |
    6 | Read this or unforeseen consequences might happen! 7 |
    8 | 17 | 18 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /client.src/modules/settings/views/revoke-modal/revoke-modal.styl: -------------------------------------------------------------------------------- 1 | .modal-title 2 | margin 20px 3 | font-size 20px 4 | display flex 5 | justify-content center 6 | align-items center 7 | 8 | .modal-title-text 9 | flex 1 10 | 11 | .close 12 | color $lightTextColor 13 | cursor pointer 14 | align-self flex-end 15 | 16 | .modal-body 17 | padding 10px 20px 18 | 19 | p 20 | margin-bottom 10px 21 | 22 | &:last-child 23 | margin 0 24 | 25 | a 26 | color $hoverLinkColor 27 | 28 | .modal-button 29 | margin 20px 20px 10px 30 | width 410px 31 | 32 | .modal .notification 33 | border-radius 0 34 | border-left 0 35 | border-right 0 36 | padding-left 20px 37 | padding-right 20px 38 | -------------------------------------------------------------------------------- /client.src/modules/settings/views/settings-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // SettingsView 3 | // 4 | 5 | import * as _ from 'underscore'; 6 | import * as Radio from 'radio'; 7 | import ItemView from 'base/item-view'; 8 | import RevokeModalView from '../revoke-modal'; 9 | import scopeMap from 'helpers/scope-map'; 10 | 11 | export default ItemView.extend({ 12 | template: 'settingsView', 13 | 14 | className: 'settings', 15 | 16 | ui: { 17 | revoke: 'button.danger' 18 | }, 19 | 20 | events: { 21 | 'click @ui.revoke': 'onRevoke' 22 | }, 23 | 24 | templateHelpers: { 25 | 26 | // Attempts to find a user-friendly version of the scope name 27 | mapScope(scopeName) { 28 | return _.result(scopeMap, scopeName) || scopeName; 29 | } 30 | }, 31 | 32 | onRevoke() { 33 | Radio.command('modal', 'show', new RevokeModalView()); 34 | } 35 | }); 36 | -------------------------------------------------------------------------------- /client.src/modules/settings/views/settings-view/settings-view.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 | Github Authorizations 4 |
    5 |
    6 |

    7 | You have granted Gistbook access to certain areas of your Github account. 8 |

    9 |

    10 | Gistbook can access your: 11 |

    12 |

    13 |

      14 | {{#each scopes }} 15 |
    • {{ scopeMap this }}
    • 16 | {{/each}} 17 |
    18 |

    19 |

    20 | 23 |

    24 |
    25 |
    26 | -------------------------------------------------------------------------------- /client.src/modules/settings/views/settings-view/settings-view.styl: -------------------------------------------------------------------------------- 1 | .settings 2 | 3 | ul 4 | margin-bottom 20px 5 | -------------------------------------------------------------------------------- /client.src/modules/terms/terms-route.js: -------------------------------------------------------------------------------- 1 | // 2 | // TermsRoute 3 | // 4 | 5 | import * as Radio from 'radio'; 6 | import Route from 'base/route'; 7 | import TermsView from './views/terms-view'; 8 | 9 | export default Route.extend({ 10 | show() { 11 | Radio.command('rootView', 'showIn:container', new TermsView()); 12 | } 13 | }); 14 | -------------------------------------------------------------------------------- /client.src/modules/terms/views/terms-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // TermsView 3 | // 4 | 5 | import ItemView from 'base/item-view'; 6 | 7 | export default ItemView.extend({ 8 | className: 'terms-of-service', 9 | template: 'termsView' 10 | }); 11 | -------------------------------------------------------------------------------- /client.src/modules/terms/views/terms-view/terms-view.hbs: -------------------------------------------------------------------------------- 1 |

    2 | Gistbook Terms of Service 3 |

    4 |

    5 | By using our services ("Services"), you are agreeing to these terms. 6 |

    7 |

    8 | Your Github Account 9 |

    10 |

    11 | Gistbook is accessed through your Github account. You are free to revoke access to your Github account at any time. It is not considered revoking access to make a request through email. To revoke access you must login to Gistbook (or Github) and use the interface on the Account Settings page. 12 |

    13 |

    14 | Use of Our Services 15 |

    16 |

    17 | Using our Services is at your sole risk. Do not misuse the Service. Modifications made to the Service's source code, which is freely available, must not be affiliated with the Service. 18 |

    19 |

    20 | Our Service displays some content that is not Gistbook's. This content is the sole responsibility of the entity that makes it available. By using this Service you are taking the risk of being exposed to such materials. 21 |

    22 |

    23 | We may suspend or prevent you from accessing or using the Services at any time. 24 |

    25 |

    26 | Changing or Terminating Our Service 27 |

    28 |

    29 | Gistbook is a constantly changing Service. We may add and remove functionality, or suspend and stop the Service altogether. 30 |

    31 |

    32 | Your Content in the Services 33 |

    34 |

    35 | Gistbook allows you to create and manage content that can be stored on Github as well as Gistbook. Gistbook claims no ownership of content that you create. 36 |

    37 |

    38 | Content stored by Github is subject to Github's terms of service. 39 |

    40 |

    41 | You are responsible for all content that is posted under your Account (even if it was posted by someone who has access to your account). 42 |

    43 |

    44 | We hold the right to remove any content that is available via this Service. 45 |

    46 |

    47 | Liability for our Services 48 |

    49 |

    50 | When permitted by law, Gistbook shall not be responsible for direct, indirect, incidental, special, consequential or exemplary damages resulting from your use of the Service. This covers, but is not limited to, any data or information stored in Gistbook as well as data and content stored on your Github account. 51 |

    52 |

    53 | Business Use of Our Services 54 |

    55 |

    56 | If you are using these Services on behalf of a business, that business also accepts these terms. It shall defend Gistbook from any third-party that alleges that your content or use of this Service in violation of this agreement infringes or misappropriates the intellectual property rights of a third-party or violates applicable law. 57 |

    58 |

    59 | About these Terms 60 |

    61 |

    62 | We may modify these terms at any time, or add additional terms that apply to the Service. We encourage you to read the terms regularly. We will post notifications of changes to the terms on this page. 63 |

    64 | -------------------------------------------------------------------------------- /client.src/modules/terms/views/terms-view/terms.styl: -------------------------------------------------------------------------------- 1 | .terms-of-service 2 | width 75% 3 | 4 | h1, h2 5 | color #333 6 | 7 | h1 8 | font-size 20px 9 | font-weight 200 10 | margin-bottom 15px 11 | 12 | h2 13 | font-size 15px 14 | font-weight bold 15 | margin 30px 0 8px 16 | 17 | p 18 | font-size 13px 19 | margin-bottom 10px 20 | -------------------------------------------------------------------------------- /client.src/shared/entities/gist.js: -------------------------------------------------------------------------------- 1 | // 2 | // Gist 3 | // A model representing a new Github Gist that can 4 | // store a Gistbook. 5 | // 6 | 7 | import * as _ from 'underscore'; 8 | import * as Radio from 'radio'; 9 | import { BaseModel } from 'base/entities'; 10 | import githubApiHelpers from 'helpers/github-api-helpers'; 11 | 12 | export default BaseModel.extend({ 13 | defaults() { 14 | return { 15 | description: 'Anonymous Gistbook', 16 | owner: { 17 | login: this._getLoginName() 18 | }, 19 | files: { 20 | 'gistbook.json': { 21 | content: '{}' 22 | } 23 | } 24 | }; 25 | }, 26 | 27 | parse(data) { 28 | var defaults = _.result(this, 'defaults'); 29 | data.description = data.description ? data.description : defaults.description; 30 | return data; 31 | }, 32 | 33 | _getLoginName() { 34 | return Radio.request('user', 'user').get('login') || 'Anonymous'; 35 | }, 36 | 37 | urlRoot() { 38 | return githubApiHelpers.url + '/gists'; 39 | } 40 | }); 41 | -------------------------------------------------------------------------------- /client.src/shared/entities/gistbook.js: -------------------------------------------------------------------------------- 1 | // 2 | // Gistbook 3 | // A model representing a new Gistbook 4 | // 5 | 6 | import * as _ from 'underscore'; 7 | import { BaseModel, BaseCollection } from 'base/entities'; 8 | 9 | export default BaseModel.extend({ 10 | initialize() { 11 | this._dirty = false; 12 | this.on('change', this._markDirty, this); 13 | this.listenTo(this.get('pages'), 'change', this._markDirty); 14 | }, 15 | 16 | // Create a deeply nested model structure 17 | // Gistbooks have a collection of pages 18 | // Pages have a collection of sections 19 | parse(data) { 20 | data = _.clone(data); 21 | 22 | // Create our nested pages 23 | data.pages = new BaseCollection(data.pages); 24 | 25 | // Loop through each page... 26 | data.pages.each(page => { 27 | 28 | // ...to create our nested sections 29 | page.set({ 30 | sections: new BaseCollection(page.get('sections')) 31 | }, {silent: true}); 32 | 33 | // And forward all events from our sections to our pages 34 | page.listenTo(page.get('sections'), 'all', function() { 35 | page.trigger.apply(page, arguments); 36 | }); 37 | }); 38 | return data; 39 | }, 40 | 41 | isDirty() { 42 | return !!this._dirty; 43 | }, 44 | 45 | _markDirty() { 46 | this._dirty = true; 47 | }, 48 | }); 49 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/error-page.styl: -------------------------------------------------------------------------------- 1 | .error-page 2 | margin-top 60px 3 | text-align center 4 | 5 | h1 6 | color $lightTextColor 7 | font-size 40px 8 | font-weight 200 9 | margin-bottom 30px 10 | 11 | p 12 | font-size 21px 13 | line-height 1.3em 14 | 15 | &.standalone 16 | margin 30px 0 17 | 18 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/generic-error-view/generic-error-view.hbs: -------------------------------------------------------------------------------- 1 |

    :(

    2 |

    Uh oh. An unknown error occurred. Try reloading the page.

    3 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/generic-error-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Generic Error View 3 | // If the error isn't one of the ones we specifically 4 | // target, then we display this view 5 | // 6 | 7 | import * as mn from 'marionette'; 8 | 9 | export default mn.ItemView.extend({ 10 | template: 'genericErrorView', 11 | className: 'generic-error error-page' 12 | }); 13 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/not-found-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Not Found View 3 | // Displayed in the main region whenever there's a 4 | // 404 error response from the server. Ruh roh! 5 | // 6 | 7 | import * as mn from 'marionette'; 8 | 9 | export default mn.ItemView.extend({ 10 | template: 'notFoundView', 11 | className: 'not-found error-page' 12 | }); 13 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/not-found-view/not-found-view.hbs: -------------------------------------------------------------------------------- 1 |

    404

    2 |

    Sorry, we couldn't find that page :(

    3 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/rate-limit-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Rate Limit View 3 | // When you've run into the rate limit, you will 4 | // see this view 5 | // 6 | 7 | import * as _ from 'underscore'; 8 | import * as Radio from 'radio'; 9 | import * as mn from 'marionette'; 10 | 11 | export default mn.ItemView.extend({ 12 | initialize: function(options) { 13 | mn.mergeOptions(this, options, this.errorRateLimitOptions); 14 | }, 15 | 16 | errorRateLimitOptions: ['resetTimestamp'], 17 | 18 | template: 'rateLimitView', 19 | 20 | className: 'error-rate-limit error-page', 21 | 22 | ui: { 23 | $minutesLeft: '.minutes-left', 24 | $secondsLeft: '.seconds-left' 25 | }, 26 | 27 | templateHelpers: function() { 28 | this.now = Math.floor(Date.now() / 1000); 29 | var diff = this.resetTimestamp - this.now; 30 | return { 31 | minutesLeft: this._minutesLeft(diff), 32 | secondsLeft: this._secondsLeft(diff), 33 | loggedIn: Radio.request('auth', 'authorized') 34 | }; 35 | }, 36 | 37 | // The following two methods determine the # of 38 | // minutes & seconds in a unix timestamp. In this 39 | // case, the timestamp is a difference, and what we 40 | // get is how much longer you need to wait 41 | _minutesLeft: function(diff) { 42 | return Math.floor(diff / 60); 43 | }, 44 | 45 | _secondsLeft: function(diff) { 46 | return diff % 60; 47 | }, 48 | 49 | updateTime: function(diff) { 50 | this.ui.$minutesLeft.text(this._minutesLeft(diff)); 51 | this.ui.$secondsLeft.text(this._secondsLeft(diff)); 52 | }, 53 | 54 | scheduleUpdate: function(diff) { 55 | diff--; 56 | this.updateTime(diff); 57 | if (diff) { 58 | this.scheduleUpdate(diff); 59 | } else { 60 | this.reload(); 61 | } 62 | }, 63 | 64 | reload: function() { 65 | window.document.location.reload(); 66 | }, 67 | 68 | // Ensure that scheduleUpdate is only called once per second, 69 | // then call it 70 | onRender: function() { 71 | this.scheduleUpdate = _.debounce(this.scheduleUpdate, 1000); 72 | this.scheduleUpdate(this.resetTimestamp - this.now); 73 | } 74 | }); 75 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/rate-limit-view/rate-limit-view.hbs: -------------------------------------------------------------------------------- 1 |

    :(

    2 |

    You've reached Github's rate limit for this hour.

    3 | {{#unless loggedIn }} 4 |

    To increase your limit, you should sign in.

    5 |

    6 | 7 | 11 | 12 |

    13 | {{/unless }} 14 |

    15 | Your rate limit will reset in 16 | 17 | {{ minutesLeft }} minutes, 18 | {{ secondsLeft }} seconds 19 | . 20 |

    21 | -------------------------------------------------------------------------------- /client.src/shared/views/error-views/rate-limit-view/rate-limit-view.styl: -------------------------------------------------------------------------------- 1 | .error-rate-limit 2 | .time-left, 3 | .time-left span 4 | font-weight bold 5 | -------------------------------------------------------------------------------- /client.src/shared/views/existing-menu/existing-gist-menu.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 6 | 10 |
    11 |
    12 |
    13 | 17 | 18 | 0 19 | 20 |
    21 |
    22 | 23 | -------------------------------------------------------------------------------- /client.src/shared/views/existing-menu/existing-gist-menu.styl: -------------------------------------------------------------------------------- 1 | .gist-menu-container-view 2 | display flex 3 | 4 | .left-menu-buttons, 5 | .right-menu-buttons 6 | display flex 7 | 8 | .right-menu-buttons 9 | flex 1 10 | justify-content flex-end 11 | 12 | .compound-fork-gist 13 | align-self flex-end 14 | -------------------------------------------------------------------------------- /client.src/shared/views/existing-menu/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // ExistingMenu 3 | // The menu to be displayed for a new Gistbook 4 | // 5 | 6 | import * as mn from 'marionette'; 7 | import ItemView from 'base/item-view'; 8 | import * as Radio from 'radio'; 9 | 10 | export default ItemView.extend({ 11 | existingMenuOptions: ['newGist', 'ownGistbook'], 12 | 13 | template: 'existingGistMenu', 14 | 15 | className: 'gist-menu-container-view', 16 | 17 | ui: { 18 | save: '.save-gist', 19 | delete: '.delete-gist', 20 | fork: '.fork-gist', 21 | forkCount: '.fork-count', 22 | forkContainer: '.compound-fork-gist' 23 | }, 24 | 25 | triggers: { 26 | 'click @ui.save': 'click:save', 27 | 'click @ui.fork': 'click:fork' 28 | }, 29 | 30 | events: { 31 | 'click @ui.delete': 'onClickDelete' 32 | }, 33 | 34 | initialize(options) { 35 | mn.mergeOptions(this, options, this.existingMenuOptions); 36 | this.on({ 37 | 'save error:save': this._resetSaveBtn, 38 | 'delete error:delete': this._resetDeleteBtn, 39 | 'error:fork': this._resetForkBtn 40 | }, this); 41 | }, 42 | 43 | onRender() { 44 | if (this.newGist) { 45 | this.ui.save.removeClass('hide'); 46 | return; 47 | } 48 | 49 | if (this.ownGistbook) { 50 | this.ui.save.removeClass('hide'); 51 | this.ui.delete.removeClass('hide'); 52 | } else if (Radio.request('auth', 'authorized')) { 53 | this.ui.forkContainer.removeClass('hide'); 54 | this.ui.forkCount.text(this.model.get('forks').length); 55 | } 56 | }, 57 | 58 | onClickSave() { 59 | this._cachedSaveHtml = this.ui.save.html(); 60 | this.ui.save 61 | .width(this.ui.save.width()) 62 | .html('Saving...') 63 | .prop('disabled', true); 64 | }, 65 | 66 | onClickDelete() { 67 | if (window.confirm('Are you sure you want to delete this Gist?')) { 68 | this._cachedDeleteHtml = this.ui.delete.html(); 69 | this.ui.delete 70 | .html('Deleting...') 71 | .prop('disabled', true); 72 | this.trigger('click:delete'); 73 | } 74 | }, 75 | 76 | onClickFork() { 77 | this._cachedForkHtml = this.ui.fork.html(); 78 | this.ui.fork 79 | .html('Forking...') 80 | .prop('disabled', true); 81 | }, 82 | 83 | _resetSaveBtn() { 84 | this.ui.save 85 | .width('auto') 86 | .html(this._cachedSaveHtml) 87 | .prop('disabled', false); 88 | this._cachedSaveHtml = null; 89 | }, 90 | 91 | _resetDeleteBtn() { 92 | this.ui.delete 93 | .html(this._cachedDeleteHtml) 94 | .prop('disabled', false); 95 | this._cachedDeleteHtml = null; 96 | }, 97 | 98 | _resetForkBtn() { 99 | this.ui.fork 100 | .html(this._cachedForkHtml) 101 | .prop('disabled', false); 102 | this._cachedForkHtml = null; 103 | } 104 | }); 105 | -------------------------------------------------------------------------------- /client.src/shared/views/gist-view/gist-view.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 | -------------------------------------------------------------------------------- /client.src/shared/views/gist-view/gist-view.styl: -------------------------------------------------------------------------------- 1 | .home 2 | width 800px 3 | margin 0 auto 4 | 5 | .gist-header 6 | display flex 7 | margin-bottom 35px 8 | 9 | .gist-menu-container 10 | flex 1 11 | 12 | .compound-button 13 | margin-right 10px 14 | &:last-child 15 | margin-right 0 16 | 17 | button, .button 18 | margin-right 0 19 | 20 | button, .button 21 | margin-right 10px 22 | 23 | &:last-child 24 | margin-right 0 25 | -------------------------------------------------------------------------------- /client.src/shared/views/gist-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // GistView 3 | // Displays a Github Gist that contains a Gistbook 4 | // 5 | 6 | import * as _ from 'underscore'; 7 | import * as bb from 'backbone'; 8 | import * as mn from 'marionette'; 9 | import LayoutView from 'base/layout-view'; 10 | import * as Radio from 'radio'; 11 | import githubApiHelpers from 'helpers/github-api-helpers'; 12 | import GistbookView from '../gistbook-view'; 13 | import Gist from '../../entities/gist'; 14 | import Gistbook from '../../entities/gistbook'; 15 | import ExistingMenu from '../existing-menu'; 16 | import gistbookHelpers from 'helpers/gistbook-helpers'; 17 | 18 | var routerChannel = Radio.channel('router'); 19 | 20 | export default LayoutView.extend({ 21 | gistViewOptions: ['newGist', 'ownGistbook', 'homePage'], 22 | 23 | className: 'home', 24 | 25 | template: 'gistView', 26 | 27 | ui: { 28 | gistHeader: '.gist-header' 29 | }, 30 | 31 | regions: { 32 | gistbookContainer: '.gistbook-container', 33 | menuContainer: '.gist-menu-container' 34 | }, 35 | 36 | initialize(options) { 37 | mn.mergeOptions(this, options, this.gistViewOptions); 38 | }, 39 | 40 | // Show a new Gistbook 41 | onBeforeShow() { 42 | if (this._showHeader()) { 43 | this.ui.gistHeader.addClass('hide'); 44 | } else { 45 | this.getRegion('menuContainer').show(this.createMenu()); 46 | this.registerMenuListeners(); 47 | } 48 | this.getRegion('gistbookContainer').show(this.createNewGistbook()); 49 | 50 | }, 51 | 52 | _showHeader() { 53 | return (!Radio.request('auth', 'authorized') && !this.newGist) || this.homePage; 54 | }, 55 | 56 | registerMenuListeners() { 57 | this.listenTo(this.menu, { 58 | 'click:save': this._sync, 59 | 'click:fork': this._fork 60 | }); 61 | this.listenToOnce(this.menu, 'click:delete', this._delete); 62 | }, 63 | 64 | // Syncs the 'nested' data structure with the parent 65 | _sync() { 66 | var sections = this.gistbookView.sections; 67 | this.gistbook.pages[0].sections = _.filter(sections.toJSON(), section => { 68 | return !/^\s*$/.test(section.source); 69 | }); 70 | this.gistbook.title = this.gistbookModel.get('title'); 71 | this._setGistbook(); 72 | this._saveGist({newGist:false}); 73 | }, 74 | 75 | _fork() { 76 | var baseUrl = githubApiHelpers.url + '/gists'; 77 | var oldDescription = this.model.get('description'); 78 | 79 | // First, we fork the old Gist 80 | bb.$.post(baseUrl + '/' + this.model.get('id') + '/forks') 81 | 82 | // Now we need to update the Gist's description 83 | .then(resp => { 84 | return bb.$.ajax({ 85 | type: 'PATCH', 86 | url: baseUrl + '/' + resp.id, 87 | data: JSON.stringify({description: oldDescription, files: {}}) 88 | }); 89 | }) 90 | 91 | // Lastly, we redirect to the newly created fork 92 | .then(this._onForkComplete) 93 | .fail(() => { 94 | this.menu.trigger('error:fork'); 95 | }); 96 | }, 97 | 98 | _onForkComplete(resp) { 99 | var currentUser = Radio.request('user', 'user').get('login'); 100 | Radio.command('router', 'navigate', currentUser + '/' + resp.id); 101 | }, 102 | 103 | _delete() { 104 | this.model.destroy() 105 | .then(() => { 106 | this.menu.trigger('delete'); 107 | routerChannel.command('navigate', Radio.request('user', 'user').get('login')); 108 | }) 109 | .catch(() => { 110 | this.menu.trigger('error:delete'); 111 | }); 112 | }, 113 | 114 | // Update the gist with the current gistbook 115 | _setGistbook() { 116 | this.model.set('description', this.gistbook.title); 117 | this.model.set('files', { 118 | 'gistbook.json': { 119 | content: JSON.stringify(this.gistbook) 120 | } 121 | }); 122 | }, 123 | 124 | // The attributes to send to the server when saved 125 | saveAttrs: ['description', 'files'], 126 | 127 | // Save the gist to Github 128 | _saveGist() { 129 | var attrs = { 130 | description: this.model.get('description'), 131 | files: this.model.get('files'), 132 | public: true 133 | }; 134 | var isNew = this.isNew(); 135 | var saveOptions = isNew ? undefined : {patch:true}; 136 | this.model.save(attrs, saveOptions) 137 | .then(gistData => { 138 | this.menu.trigger('save'); 139 | if (isNew) { 140 | var username = gistData.owner ? gistData.owner.login : 'anonymous'; 141 | var url = '/' + username + '/' + gistData.id; 142 | Radio.command('router', 'navigate', url); 143 | } 144 | }) 145 | .catch(() => { 146 | this.menu.trigger('error:save'); 147 | console.log('Gist not saved', arguments); 148 | }); 149 | }, 150 | 151 | createMenu() { 152 | var menuOptions = { 153 | model: this.model, 154 | newGist: this.newGist, 155 | ownGistbook: this.ownGistbook 156 | }; 157 | this.menu = new ExistingMenu(menuOptions); 158 | return this.menu; 159 | }, 160 | 161 | createNewGistbook() { 162 | this.gistbookView = new GistbookView({ 163 | model: this.getGistbookModel(), 164 | newGist: this.newGist, 165 | ownGistbook: this.ownGistbook 166 | }); 167 | return this.gistbookView; 168 | }, 169 | 170 | // We're only rendering a single page of a Gistbook for now. Thus, 171 | // we first convert our Gist to a Gistbook, then we grab the first page. 172 | getGistbookModel() { 173 | var gistbookData = this.getGistbookData(); 174 | this.gistbookModel = new Gistbook(gistbookData, {parse: true}); 175 | return this.gistbookModel; 176 | }, 177 | 178 | getGistbookData() { 179 | var gist = !this.isNew() ? this.model : new Gist(); 180 | return this._convertGistToGistbook(gist); 181 | }, 182 | 183 | _convertGistToGistbook(gist) { 184 | var gistbook; 185 | if (this.homePage) { 186 | gistbook = gistbookHelpers.getHomePage(); 187 | } else if (!this.isNew()) { 188 | gistbook = gistbookHelpers.gistbookFromGist(gist); 189 | } else { 190 | gistbook = gistbookHelpers.newGistbook(); 191 | } 192 | this.gistbook = gistbook; 193 | return this.gistbook; 194 | }, 195 | 196 | // Determine whether this is a new Gist or an existing one 197 | isNew() { 198 | return !this.model.get('id'); 199 | }, 200 | 201 | // Whether or not the gist has been saved 202 | isSaved: function() { 203 | return !this.gistbookModel.isDirty(); 204 | } 205 | }); 206 | 207 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/gistbook-view.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 |
    5 |
    6 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/gistbook-view.styl: -------------------------------------------------------------------------------- 1 | .gistbook 2 | mediumShadow() 3 | border none 4 | border-radius 4px 5 | width 100% 6 | margin 0 auto 7 | line-height 1.6 8 | background #fff 9 | overflow hidden 10 | 11 | a i 12 | text-decoration none 13 | 14 | .gistbook-header 15 | padding 12px 30px 16 | background #9699bc 17 | border-bottom 1px solid #747796 18 | 19 | .gistbook-output-wrapper 20 | border-top 1px solid #ddd 21 | padding 20px 22 | position relative 23 | 24 | .gistbook-output 25 | background #fff 26 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/helpers/radio-helpers.js: -------------------------------------------------------------------------------- 1 | // 2 | // radioHelpersHelpers 3 | // 4 | 5 | import * as _ from 'underscore'; 6 | import * as Radio from 'radio'; 7 | 8 | var radioHelpers = { 9 | 10 | // Returns a unique channel 11 | uniqueChannel() { 12 | return Radio.channel(_.uniqueId('_channel-')); 13 | }, 14 | 15 | // Generate a unique channel name for an object 16 | // (like a model or collection) 17 | objChannelName(obj) { 18 | obj._uniqueId = obj._uniqueId || _.uniqueId(); 19 | return '_obj-' + obj._uniqueId; 20 | }, 21 | 22 | // Get the channel from an obj 23 | objChannel(obj) { 24 | return Radio.channel(radioHelpers.objChannelName(obj)); 25 | } 26 | }; 27 | 28 | export default radioHelpers; 29 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/helpers/string-helpers.js: -------------------------------------------------------------------------------- 1 | // 2 | // stringHelpers 3 | // 4 | 5 | export default { 6 | capitalize(string) { 7 | return string.charAt(0).toUpperCase() + string.slice(1); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // GistbookView 3 | // A LayoutView that renders a Gistbook 4 | // 5 | 6 | import * as mn from 'marionette'; 7 | import LayoutView from 'base/layout-view'; 8 | import SectionsView from './views/sections'; 9 | import OutputView from './views/output-view'; 10 | import DisplayTitleView from './views/title/display'; 11 | import EditTitleView from './views/title/edit'; 12 | import radioHelpers from './helpers/radio-helpers'; 13 | 14 | export default LayoutView.extend({ 15 | gistbookViewOptions: ['newGist', 'ownGistbook'], 16 | 17 | initialize(options) { 18 | mn.mergeOptions(this, options, this.gistbookViewOptions); 19 | }, 20 | 21 | template: 'gistbookView', 22 | 23 | ui: { 24 | container: '.gistbook-container', 25 | header: '.gistbook-header', 26 | output: '.gistbook-output' 27 | }, 28 | 29 | regions: { 30 | sectionsContainer: '.gistbook-body', 31 | header: '.gistbook-header', 32 | output: '.gistbook-output' 33 | }, 34 | 35 | className: 'gistbook', 36 | 37 | onBeforeShow() { 38 | this.getRegion('sectionsContainer').show(this._createSectionsView()); 39 | this.getRegion('output').show(new OutputView({ 40 | sections: this.sections 41 | })); 42 | this._showDisplayTitle(); 43 | }, 44 | 45 | _showDisplayTitle() { 46 | var displayHeader = this._createDisplayHeaderView(); 47 | this.getRegion('header').show(displayHeader); 48 | this.listenToOnce(displayHeader, 'edit', this._showEditTitle); 49 | }, 50 | 51 | _showEditTitle() { 52 | var editTitleView = this._createEditHeaderView(); 53 | this.getRegion('header').show(editTitleView); 54 | this.listenToOnce(editTitleView, 'save cancel', this._showDisplayTitle); 55 | }, 56 | 57 | onBeforeDestroy() { 58 | this.pageChannel.reset(); 59 | delete this.sections; 60 | }, 61 | 62 | _createDisplayHeaderView() { 63 | return new DisplayTitleView({ 64 | model: this.model, 65 | editable: this.ownGistbook 66 | }); 67 | }, 68 | 69 | _createEditHeaderView() { 70 | return new EditTitleView({ 71 | model: this.model 72 | }); 73 | }, 74 | 75 | _createSectionsView() { 76 | this.pageChannel = radioHelpers.objChannel(this.model); 77 | return new SectionsView({ 78 | collection: this._createSectionsCollection(), 79 | pageChannel: this.pageChannel, 80 | newGist: this.newGist, 81 | ownGistbook: this.ownGistbook 82 | }); 83 | }, 84 | 85 | _createSectionsCollection() { 86 | this.sections = this.model.get('pages').at(0).get('sections'); 87 | return this.sections; 88 | } 89 | }); 90 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/ace-editor-view/ace-editor-view.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    {{ source }}
    3 |
    4 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/ace-editor-view/ace-editor-view.styl: -------------------------------------------------------------------------------- 1 | .ace-editor 2 | 3 | .ace-wrapper:after 4 | position absolute 5 | top 0 6 | right 0 7 | display block 8 | content 'Code' 9 | color #bbb 10 | font-weight 600 11 | letter-spacing 1px 12 | font-size 13px 13 | border 1px solid #d3d3d3 14 | border-top 0 15 | border-right 0 16 | border-radius 0 0 0 3px 17 | padding 4px 13px 3px 18 | background #fff 19 | 20 | &.ace-editor-javascript .ace-wrapper:after 21 | content 'Javascript' 22 | &.ace-editor-html .ace-wrapper:after 23 | content 'HTML' 24 | &.ace-editor-css .ace-wrapper:after 25 | content 'CSS' 26 | 27 | &.read-only .ace_content 28 | cursor default 29 | 30 | 31 | .ace-wrapper 32 | border 1px solid #d3d3d3 33 | border-radius 3px 34 | padding 8px 5px 35 | position relative 36 | background #f5f5f5 37 | box-shadow inset 0 1px 3px rgba(0,0,0,.1) 38 | 39 | .ace-tomorrow 40 | background inherit 41 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/ace-editor-view/index.js: -------------------------------------------------------------------------------- 1 | /* jshint maxstatements: 20 */ 2 | 3 | // 4 | // aceEditor 5 | // A view for the ace editor, which is used 6 | // when editing code 7 | // 8 | 9 | import * as mn from 'marionette'; 10 | import * as Radio from 'radio'; 11 | import ItemView from 'base/item-view'; 12 | 13 | var aceChannel = Radio.channel('ace'); 14 | 15 | export default ItemView.extend({ 16 | template: 'aceEditorView', 17 | 18 | // Defaults for the Ace Editor 19 | readOnly: false, 20 | tabSize: 2, 21 | softTabs: true, 22 | highlightActiveLine: false, 23 | theme: 'tomorrow', 24 | mode: 'javascript', 25 | minLines: 5, 26 | maxLines: 20, 27 | hideCursor: false, 28 | showGutter: false, 29 | 30 | aceEditorViewOptions: [ 31 | 'readOnly', 32 | 'tabSize', 33 | 'softTabs', 34 | 'highlightActiveLine', 35 | 'theme', 36 | 'mode', 37 | 'minLines', 38 | 'maxLines', 39 | 'hideCursor', 40 | 'showGutter' 41 | ], 42 | 43 | ui: { 44 | aceContainer: '.ace-editor' 45 | }, 46 | 47 | events: { 48 | 'focusout': 'onBlur', 49 | 'keypress': 'onKeypress', 50 | }, 51 | 52 | onBlur() { 53 | this.update(); 54 | }, 55 | 56 | onKeypress(e) { 57 | var code = (e.keyCode ? e.keyCode : e.which); 58 | if (code !== 13 || !e.shiftKey) { 59 | return; 60 | } 61 | e.preventDefault(); 62 | this.editor.blur(); 63 | }, 64 | 65 | value() { 66 | return this.editor.getSession().getValue(); 67 | }, 68 | 69 | update() { 70 | this.model.set('source', this.value()); 71 | }, 72 | 73 | // Merge the options 74 | initialize(options) { 75 | mn.mergeOptions(this, options, this.aceEditorViewOptions); 76 | this.listenTo(aceChannel, { 77 | dragStart: this._onDragStart, 78 | dragEnd: this._onDragEnd 79 | }); 80 | }, 81 | 82 | // These two methods are here because the $fontMetrics element 83 | // is absolutely positioned, which shifts the ghost image 84 | // that appears during drag and drop interface. Ideally this 85 | // won't always be the case. Related issue: ace#2240 86 | // https://github.com/ajaxorg/ace/issues/2240 87 | _onDragStart() { 88 | this.editor.renderer.$fontMetrics.el.style.display = 'none'; 89 | }, 90 | _onDragEnd() { 91 | this.editor.renderer.$fontMetrics.el.style.display = 'block'; 92 | }, 93 | 94 | // Create the editor and configure it 95 | onRender() { 96 | this.editor = ace.edit(this.ui.aceContainer[0]); 97 | this._configureEditor(); 98 | if (this.readOnly) { 99 | this.$el.addClass('read-only'); 100 | } 101 | }, 102 | 103 | // Clean up the editor before we close the view down 104 | onBeforeDestroy() { 105 | this.editor.destroy(); 106 | }, 107 | 108 | // Configure the editor based on our options 109 | _configureEditor() { 110 | var themePath = this._getThemePath(this.theme); 111 | var modePath = this._getModePath(this.mode); 112 | 113 | var session = this.editor.getSession(); 114 | var renderer = this.editor.renderer; 115 | 116 | this.editor.setHighlightActiveLine(this.highlightActiveLine); 117 | this.editor.getSession().setMode(modePath); 118 | this.editor.setTheme(themePath); 119 | this.editor.setShowPrintMargin(false); 120 | this.editor.setOption('maxLines', this.maxLines); 121 | this.editor.setOption('minLines', this.minLines); 122 | 123 | this.editor.setReadOnly(this.readOnly); 124 | session.setTabSize(this.tabSize); 125 | session.setUseSoftTabs(this.softTabs); 126 | renderer.setShowGutter(this.showGutter); 127 | 128 | if (this.hideCursor) { 129 | renderer.$cursorLayer.element.style.opacity = 0; 130 | } 131 | }, 132 | 133 | // Where ace stores its themes 134 | _getThemePath(themeName) { 135 | return 'ace/theme/' + themeName; 136 | }, 137 | 138 | // Where ace stores modes 139 | _getModePath(modeName) { 140 | return 'ace/mode/' + modeName; 141 | } 142 | }); 143 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // compileView 3 | // Manages compiling a Gistbook Page 4 | // 5 | 6 | import LayoutView from 'base/layout-view'; 7 | import * as Spinner from 'spin.js'; 8 | import CodeExtractor from './services/code-extractor'; 9 | import moduleBundler from './services/module-bundler'; 10 | import Compiler from './services/compiler'; 11 | import ErrorView from './views/compile-error-view'; 12 | 13 | export default LayoutView.extend({ 14 | initialize(options) { 15 | this.sections = options.sections; 16 | this.createManagers(); 17 | this.configureListeners(); 18 | }, 19 | 20 | regions: { 21 | output: '.gistbook-output-container' 22 | }, 23 | 24 | template: 'outputView', 25 | 26 | className: 'gistbook-compile-view', 27 | 28 | ui: { 29 | compile: 'button', 30 | spinnerContainer: '.spinner-container' 31 | }, 32 | 33 | events: { 34 | 'click @ui.compile': 'onClickCompile' 35 | }, 36 | 37 | spinnerOptions: { 38 | color: '#777', 39 | lines: 7, 40 | length: 3, 41 | width: 3, 42 | radius: 4 43 | }, 44 | 45 | configureListeners() { 46 | this.listenTo(this.compiler, { 47 | compile: this.showCompiledView, 48 | 'error:compile': this.showErrorView 49 | }); 50 | }, 51 | 52 | onClickCompile() { 53 | this._compiling = true; 54 | this.ui.compile.prop('disabled', true); 55 | this._showSpinner(); 56 | var code = this.codeExtractor.getCode(); 57 | this.listenToOnce(moduleBundler, 'retrieve', js => { 58 | code.javascript = js; 59 | this.compiler.compile(code); 60 | }); 61 | moduleBundler.getBundle(code.javascript); 62 | }, 63 | 64 | _showSpinner() { 65 | if (!this._compiling) { return; } 66 | this.ui.spinnerContainer.addClass('show'); 67 | }, 68 | 69 | _resetBtn() { 70 | this.ui.compile.prop('disabled', false); 71 | this.ui.spinnerContainer.removeClass('show'); 72 | }, 73 | 74 | onRender() { 75 | new Spinner(this.spinnerOptions).spin(this.ui.spinnerContainer[0]); 76 | }, 77 | 78 | showCompiledView(iFrameView) { 79 | this._compiling = false; 80 | this._resetBtn(); 81 | this.getRegion('output').show(iFrameView); 82 | }, 83 | 84 | showErrorView() { 85 | this._compiling = false; 86 | this._resetBtn(); 87 | this.getRegion('output').show(new ErrorView()); 88 | }, 89 | 90 | createManagers() { 91 | this.codeExtractor = new CodeExtractor({ 92 | sections: this.sections 93 | }); 94 | 95 | this.compiler = new Compiler(); 96 | } 97 | }); 98 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/output-view.hbs: -------------------------------------------------------------------------------- 1 |
    2 | Output 3 |
    4 | 8 |
    9 |
    10 |
    Click Run to execute this Gistbook's code.
    11 |
    12 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/output-view.styl: -------------------------------------------------------------------------------- 1 | .gistbook-compile-view 2 | 3 | .gistbook-output-title 4 | padding 10px 10px 5 | display flex 6 | align-items center 7 | 8 | > span 9 | flex 1 10 | font-weight bold 11 | color #aaa 12 | letter-spacing 1px 13 | text-transform uppercase 14 | font-size 13px 15 | 16 | .spinner-container 17 | opacity 0 18 | height 28px 19 | width 28px 20 | margin-right 1px 21 | align-self flex-end 22 | position relative 23 | transition opacity .05s ease-in-out 24 | 25 | &.show 26 | opacity 1 27 | transition-duration .2s 28 | 29 | button 30 | align-self flex-end 31 | 32 | .gistbook-output-container 33 | padding 10px 34 | 35 | .notice 36 | border-radius 2px 37 | padding 30px 0 38 | color #6b8296 39 | background #d4dfe9 40 | text-align center 41 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/services/code-extractor.js: -------------------------------------------------------------------------------- 1 | // 2 | // codeExtractor 3 | // A gistbook page stores a collection of Models for each type of 4 | // code: HTML, JS, and CSS. 5 | // This manager has methods to loop through the page to get all 6 | // of that data in a form that we can use for sending to the server. 7 | // 8 | 9 | import * as _ from 'underscore'; 10 | import * as mn from 'marionette'; 11 | 12 | export default mn.Object.extend({ 13 | codeManagerOptions: ['sections', 'join'], 14 | 15 | // The character used to join separate blocks of source code 16 | join: '\n', 17 | 18 | initialize(options) { 19 | mn.mergeOptions(this, options, this.codeManagerOptions); 20 | }, 21 | 22 | // Return a hash of the three source codes. 23 | getCode() { 24 | return { 25 | html: this.getHtml(), 26 | css: this.getCss(), 27 | javascript: this.getJavascript() 28 | }; 29 | }, 30 | 31 | // Retrieve the concatenated HTML from the Gistbook 32 | getHtml() { 33 | var htmlCollection = this.getSectionsOfType('html'); 34 | return this.concatenate(htmlCollection); 35 | }, 36 | 37 | // Retrieve the concatenated CSS from the Gistbook 38 | getCss() { 39 | var cssCollection = this.getSectionsOfType('css'); 40 | return this.concatenate(cssCollection); 41 | }, 42 | 43 | // Retrieve the concatenated Javascript from the Gistbook 44 | getJavascript() { 45 | var jsCollection = this.getSectionsOfType('javascript'); 46 | return this.concatenate(jsCollection); 47 | }, 48 | 49 | // Filter the collection by a particular type. Type should be 50 | // one of 'html', 'css', or 'javascript'. 51 | getSectionsOfType(type) { 52 | return this.sections.where({type: type}); 53 | }, 54 | 55 | // Returns the concatenated source of a subcollection. 56 | concatenate(subcollection) { 57 | var result = ''; 58 | var join = this.join; 59 | var source; 60 | var length = subcollection.length - 1; 61 | _.each(subcollection, (model, index) => { 62 | source = model.get('source'); 63 | result += source ? source : ''; 64 | result += index !== length ? join : ''; 65 | }); 66 | return result; 67 | } 68 | }); 69 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/services/compiler.js: -------------------------------------------------------------------------------- 1 | // 2 | // compiler 3 | // Builds our output iFrame view. 4 | // 5 | 6 | import * as $ from 'jquery'; 7 | import * as _ from 'underscore'; 8 | import * as bb from 'backbone'; 9 | import * as mn from 'marionette'; 10 | import DocumentView from '../views/document-view'; 11 | import IFrameView from '../views/iframe'; 12 | 13 | export default mn.Object.extend({ 14 | url: '/compile', 15 | 16 | compile(code) { 17 | 18 | // Create our document view. This is ultimately what 19 | // we load into the iFrame. 20 | var documentView = this.createDocumentView(code); 21 | 22 | // Render it, 'cause, you know, we need it to be rendered 23 | documentView.render(); 24 | 25 | // Serialize it so we can send it off to the server 26 | var serializedView = this.serializeDocumentView(documentView); 27 | 28 | // Finally, post to the server. 29 | this.post(serializedView) 30 | .then(_.bind(this.onPostSuccess, this)) 31 | .catch(_.bind(this.onPostError, this)); 32 | }, 33 | 34 | post(serializedView) { 35 | return Promise.resolve($.post(this.url, serializedView)); 36 | }, 37 | 38 | // If the server responds that we've succeeded, then we 39 | // create the iFrame and share that the compile was a success. 40 | onPostSuccess(res) { 41 | var iFrameView = this.createIFrameView(res.token); 42 | this.trigger('compile', iFrameView); 43 | }, 44 | 45 | // When the server responds that we errored, we emit it as an event. 46 | onPostError(err) { 47 | this.trigger('error:compile', err); 48 | }, 49 | 50 | createDocumentView(code) { 51 | return new DocumentView({ 52 | model: new bb.Model(code) 53 | }); 54 | }, 55 | 56 | createIFrameView(token) { 57 | var model = new bb.Model({ 58 | token: token 59 | }); 60 | return new IFrameView({ 61 | model: model 62 | }); 63 | }, 64 | 65 | serializeDocumentView(documentView) { 66 | return { 67 | html: documentView.toString() 68 | }; 69 | } 70 | }); 71 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/services/module-bundler.js: -------------------------------------------------------------------------------- 1 | // 2 | // moduleBundler 3 | // Adds support for 'require' calls in Gistbooks 4 | // 5 | 6 | import * as _ from 'underscore'; 7 | import * as bb from 'backbone'; 8 | import * as mn from 'marionette'; 9 | import * as detective from 'detective'; 10 | import * as createCache from 'browser-module-cache'; 11 | 12 | var cache = createCache({ 13 | name: 'browser-module-cache', 14 | inMemory: false 15 | }); 16 | 17 | var ModuleBundler = mn.Object.extend({ 18 | getBundle(src) { 19 | var modules = detective(src); 20 | 21 | if (modules.length === 0) { 22 | this.trigger('retrieve', src); 23 | return; 24 | } 25 | 26 | var allBundles = ''; 27 | var downloads = []; 28 | 29 | cache.get((err, cached) => { 30 | if (err) { throw new Error(err); } 31 | 32 | _.each(modules, module => { 33 | if (cached[module]) { 34 | allBundles += cached[module].bundle; 35 | } else { 36 | downloads.push(module); 37 | } 38 | }); 39 | 40 | // If everything is cached, then we return 41 | // all of the cached source 42 | if (!downloads.length) { 43 | this.trigger('retrieve', allBundles + src); 44 | return; 45 | } 46 | 47 | var dependencies = {}; 48 | _.each(downloads, module => { 49 | dependencies[module] = 'latest'; 50 | }); 51 | 52 | var body = { 53 | options: { debug: true }, 54 | dependencies: dependencies 55 | }; 56 | 57 | return bb.$.post('https://wzrd.bocoup.com/multi', JSON.stringify(body)).then(data => { 58 | _.each(data, datum => { 59 | allBundles += datum.bundle; 60 | }); 61 | cache.put(data, () => { 62 | this.trigger('retrieve', allBundles + src); 63 | }); 64 | }); 65 | }); 66 | } 67 | }); 68 | 69 | export default new ModuleBundler(); 70 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/views/compile-error-view/compile-error-view.hbs: -------------------------------------------------------------------------------- 1 | 2 | The Gistbook failed to compile. 3 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/views/compile-error-view/compile-error-view.styl: -------------------------------------------------------------------------------- 1 | .error-view 2 | 3 | .octicon 4 | color $stateDangerText 5 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/views/compile-error-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // CompileErrorView 3 | // 4 | 5 | import ItemView from 'base/item-view'; 6 | 7 | export default ItemView.extend({ 8 | template: 'compileErrorView', 9 | className: 'notification notification-danger error-view' 10 | }); 11 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/views/document-view/document-view.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | Gistbook 4 | 7 | 8 | 9 | {{{ html }}} 10 | 13 | 14 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/views/document-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // documentView 3 | // The HTML document that we build with the user's input. It is injected 4 | // into the iFrame view to render the output. 5 | // 6 | 7 | import ItemView from 'base/item-view'; 8 | 9 | export default ItemView.extend({ 10 | template: 'documentView', 11 | 12 | tagName: 'html', 13 | 14 | // I'm assuming English for now. In the future, this will 15 | // likely become customizable. 16 | attributes: { 17 | lang: 'en' 18 | }, 19 | 20 | // The DOCTYPE declaration for this document 21 | doctype: '', 22 | 23 | newline: '\n', 24 | 25 | // Serialize our element to a string, including the doctype. 26 | // This is what we send over to the server, and it's ultimately 27 | // what the server returns back to us for our iFrame source. 28 | toString() { 29 | return this.doctype + this.newline + this.el.outerHTML; 30 | } 31 | }); 32 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/views/iframe/iframe-view.styl: -------------------------------------------------------------------------------- 1 | .gistbook-iframe 2 | width 100% 3 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/output-view/views/iframe/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // iframe 3 | // The iFrame that renders the user's code. 4 | // 5 | 6 | import ItemView from 'base/item-view'; 7 | 8 | export default ItemView.extend({ 9 | tagName: 'iframe', 10 | 11 | template: false, 12 | 13 | attributes: { 14 | sandbox: 'allow-scripts allow-popups allow-pointer-lock' 15 | }, 16 | 17 | onRender() { 18 | this.$el.attr('src', this._generateSrc()); 19 | }, 20 | 21 | _generateSrc() { 22 | return '/output/' + this.model.get('token'); 23 | }, 24 | 25 | className: 'gistbook-iframe' 26 | }); 27 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/sections/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Sections 3 | // A CollectionView that renders all of the Gistbook's sections 4 | // 5 | 6 | import * as Sortable from 'sortable'; 7 | import * as _ from 'underscore'; 8 | import * as mn from 'marionette'; 9 | import * as Radio from 'radio'; 10 | import CollectionView from 'base/collection-view'; 11 | import DisplayTextView from '../text/display'; 12 | import ControlsWrapper from '../wrappers/controls-wrapper'; 13 | import AceEditorView from '../ace-editor-view'; 14 | import stringHelpers from '../../helpers/string-helpers'; 15 | import radioHelpers from '../../helpers/radio-helpers'; 16 | 17 | var aceChannel = Radio.channel('ace'); 18 | 19 | export default CollectionView.extend({ 20 | sectionsOptions: ['newGist', 'ownGistbook'], 21 | 22 | tagName: 'ul', 23 | 24 | initialize(options) { 25 | mn.mergeOptions(this, options, this.sectionsOptions); 26 | this.pageChannel = options.pageChannel; 27 | this._renderedBefore = false; 28 | }, 29 | 30 | // This is a magical method that determines which factory we should use 31 | // based on the type of the gistbook section and whether we're authorized 32 | // or not 33 | getChildView(model) { 34 | var factoryMethod = this._factoryMethodName(model.get('type')); 35 | return this[factoryMethod](model); 36 | }, 37 | 38 | onAttach() { 39 | var self = this; 40 | this._sortable = new Sortable(this.el, { 41 | setData: false, 42 | handle: '.gistblock-move', 43 | draggable: '.controls-wrapper-view', 44 | ghostClass: 'gistblock-placeholder', 45 | onStart() { 46 | aceChannel.trigger('dragStart'); 47 | }, 48 | onEnd() { 49 | aceChannel.trigger('dragEnd'); 50 | self._resortByDom(); 51 | } 52 | }); 53 | }, 54 | 55 | // Silently update the collection based on the new DOM indices 56 | _resortByDom() { 57 | var newCollection = {}; 58 | var newArray = []; 59 | var index, $children = this.$el.children(); 60 | 61 | this.children.each((view, i) => { 62 | index = $children.index(view.el); 63 | newCollection[index] = view.model; 64 | newArray = _.sortBy(newCollection, (key, i) => { 65 | return i; 66 | }); 67 | view._index = index; 68 | }, this); 69 | 70 | this.collection.reset(newArray, {silent: true}); 71 | }, 72 | 73 | // Make it sortable if we're authorized 74 | onRender() { 75 | this.$el.addClass('gistbook-'+this._style().toLowerCase()); 76 | this._renderedBefore = true; 77 | }, 78 | 79 | _style() { 80 | return this.ownGistbook || this.newGist ? 'Edit' : 'Display'; 81 | }, 82 | 83 | _factoryMethodName(viewType) { 84 | var style = this._style(); 85 | return '_create' + style + stringHelpers.capitalize(viewType) + 'View'; 86 | }, 87 | 88 | _createEditTextView(model) { 89 | this._registerDisplayView(model, DisplayTextView); 90 | var initialMode = this.initialRender ? 'edit' : 'display'; 91 | this.childViewOptions = { 92 | initialMode: initialMode 93 | }; 94 | return ControlsWrapper; 95 | }, 96 | 97 | _createDisplayTextView(model) { 98 | this.childViewOptions = {}; 99 | return DisplayTextView.extend({ 100 | tagName: 'li', 101 | className: 'gistblock gistblock-text gistbook-section-display' 102 | }); 103 | }, 104 | 105 | _createEditJavascriptView(model) { 106 | var CustomAceEditor = AceEditorView.extend({ 107 | className: 'ace-editor ace-editor-javascript' 108 | }); 109 | this._registerDisplayView(model, CustomAceEditor); 110 | this.childViewOptions = { 111 | cache: false, 112 | editOptions: { 113 | edit: false, 114 | move: true, 115 | delete: true 116 | } 117 | }; 118 | return ControlsWrapper; 119 | }, 120 | 121 | _createDisplayJavascriptView(model) { 122 | this.childViewOptions = { 123 | readOnly: true, 124 | hideCursor: true, 125 | }; 126 | return AceEditorView.extend({ 127 | className: 'ace-editor ace-editor-javascript gistbook-section-display', 128 | tagName: 'li' 129 | }); 130 | }, 131 | 132 | _createEditHtmlView(model) { 133 | var CustomAceEditor = AceEditorView.extend({ 134 | className: 'ace-editor ace-editor-html' 135 | }); 136 | this._registerDisplayView(model, CustomAceEditor); 137 | this.childViewOptions = { 138 | cache: false, 139 | mode: 'html', 140 | editOptions: { 141 | edit: false, 142 | move: true, 143 | delete: true 144 | } 145 | }; 146 | return ControlsWrapper; 147 | }, 148 | 149 | _createDisplayHtmlView(model) { 150 | this.childViewOptions = { 151 | readOnly: true, 152 | hideCursor: true, 153 | mode: 'html' 154 | }; 155 | return AceEditorView.extend({ 156 | className: 'ace-editor ace-editor-html gistbook-section-display', 157 | tagName: 'li' 158 | }); 159 | }, 160 | 161 | _createEditCssView(model) { 162 | var CustomAceEditor = AceEditorView.extend({ 163 | className: 'ace-editor ace-editor-css' 164 | }); 165 | this._registerDisplayView(model, CustomAceEditor); 166 | this.childViewOptions = { 167 | cache: false, 168 | mode: 'css', 169 | editOptions: { 170 | edit: false, 171 | move: true, 172 | delete: true 173 | } 174 | }; 175 | return ControlsWrapper; 176 | }, 177 | 178 | _createDisplayCssView(model) { 179 | this.childViewOptions = { 180 | readOnly: true, 181 | hideCursor: true, 182 | mode: 'css' 183 | }; 184 | return AceEditorView.extend({ 185 | className: 'ace-editor ace-editor-css gistbook-section-display', 186 | tagName: 'li' 187 | }); 188 | }, 189 | 190 | // Register the DisplayView for a particular Gistblock on that block's Channel 191 | _registerDisplayView(model, ViewClass) { 192 | radioHelpers.objChannel(model).reply('displayView', model => { 193 | var options = _.extend({}, this.childViewOptions, {model:model}); 194 | return new ViewClass(options); 195 | }); 196 | } 197 | }); 198 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/sections/sections.styl: -------------------------------------------------------------------------------- 1 | .gistbook-edit 2 | padding-top 3px 3 | padding-bottom 15px 4 | 5 | .gistbook-display 6 | margin-top 20px 7 | 8 | > li 9 | margin 0 30px 30px 10 | 11 | .gistblock-placeholder 12 | opacity .2 13 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/text/display/display-text-view.styl: -------------------------------------------------------------------------------- 1 | /* 2 | * emoji 3 | */ 4 | 5 | .gistblock .emoji 6 | width 20px 7 | 8 | /* 9 | * Mathjax 10 | */ 11 | 12 | .gistblock 13 | .MathJax_Display 14 | margin 0 15 | 16 | /* 17 | * Markdown 18 | */ 19 | 20 | .gistblock-markdown, 21 | .gistblock-text { 22 | a { 23 | color: #4183C4; } 24 | a.absent { 25 | color: #cc0000; } 26 | a.anchor { 27 | display: block; 28 | padding-left: 30px; 29 | margin-left: -30px; 30 | cursor: pointer; 31 | position: absolute; 32 | top: 0; 33 | left: 0; 34 | bottom: 0; } 35 | 36 | h1, h2, h3, h4, h5, h6 { 37 | margin: 20px 0 10px; 38 | padding: 0; 39 | font-weight: bold; 40 | -webkit-font-smoothing: antialiased; 41 | position: relative; 42 | &:first-child { 43 | margin-top: 0; 44 | } 45 | &:last-child { 46 | margin-bottom: 0; 47 | } 48 | } 49 | 50 | h1:hover a.anchor, h2:hover a.anchor, h3:hover a.anchor, h4:hover a.anchor, h5:hover a.anchor, h6:hover a.anchor { 51 | background: url("../../images/modules/styleguide/para.png") no-repeat 10px center; 52 | text-decoration: none; } 53 | 54 | h1 tt, h1 code { 55 | font-size: inherit; } 56 | 57 | h2 tt, h2 code { 58 | font-size: inherit; } 59 | 60 | h3 tt, h3 code { 61 | font-size: inherit; } 62 | 63 | h4 tt, h4 code { 64 | font-size: inherit; } 65 | 66 | h5 tt, h5 code { 67 | font-size: inherit; } 68 | 69 | h6 tt, h6 code { 70 | font-size: inherit; } 71 | 72 | h1 { 73 | font-size: 28px; 74 | color: black; } 75 | 76 | h2 { 77 | font-size: 24px; 78 | border-bottom: 1px solid #cccccc; 79 | color: black; } 80 | 81 | h3 { 82 | font-size: 18px; } 83 | 84 | h4 { 85 | font-size: 16px; } 86 | 87 | h5 { 88 | font-size: 14px; } 89 | 90 | h6 { 91 | color: #777777; 92 | font-size: 14px; } 93 | 94 | p, blockquote, ul, ol, dl, li, table, pre { 95 | margin: 15px 0; 96 | &:first-child { 97 | margin-top: 0; 98 | } 99 | &:last-child { 100 | margin-bottom: 0; 101 | } 102 | } 103 | 104 | hr { 105 | background: transparent url("../../images/modules/pulls/dirty-shade.png") repeat-x 0 0; 106 | border: 0 none; 107 | color: #cccccc; 108 | height: 4px; 109 | padding: 0; } 110 | 111 | body > h2:first-child { 112 | margin-top: 0; 113 | padding-top: 0; } 114 | body > h1:first-child { 115 | margin-top: 0; 116 | padding-top: 0; } 117 | body > h1:first-child + h2 { 118 | margin-top: 0; 119 | padding-top: 0; } 120 | body > h3:first-child, body > h4:first-child, body > h5:first-child, body > h6:first-child { 121 | margin-top: 0; 122 | padding-top: 0; } 123 | 124 | a:first-child h1, a:first-child h2, a:first-child h3, a:first-child h4, a:first-child h5, a:first-child h6 { 125 | margin-top: 0; 126 | padding-top: 0; } 127 | 128 | h1 p, h2 p, h3 p, h4 p, h5 p, h6 p { 129 | margin-top: 0; } 130 | 131 | li p.first { 132 | display: inline-block; } 133 | 134 | ul, ol { 135 | padding-left: 30px; } 136 | 137 | ul :first-child, ol :first-child { 138 | margin-top: 0; } 139 | 140 | ul :last-child, ol :last-child { 141 | margin-bottom: 0; } 142 | 143 | dl { 144 | padding: 0; } 145 | dl dt { 146 | font-size: 14px; 147 | font-weight: bold; 148 | font-style: italic; 149 | padding: 0; 150 | margin: 15px 0 5px; } 151 | dl dt:first-child { 152 | padding: 0; } 153 | dl dt > :first-child { 154 | margin-top: 0; } 155 | dl dt > :last-child { 156 | margin-bottom: 0; } 157 | dl dd { 158 | margin: 0 0 15px; 159 | padding: 0 15px; } 160 | dl dd > :first-child { 161 | margin-top: 0; } 162 | dl dd > :last-child { 163 | margin-bottom: 0; } 164 | 165 | blockquote { 166 | border-left: 4px solid #dddddd; 167 | padding: 0 15px; 168 | color: #777777; } 169 | blockquote > :first-child { 170 | margin-top: 0; } 171 | blockquote > :last-child { 172 | margin-bottom: 0; } 173 | 174 | table { 175 | padding: 0; } 176 | table tr { 177 | border-top: 1px solid #cccccc; 178 | background-color: white; 179 | margin: 0; 180 | padding: 0; } 181 | table tr:nth-child(2n) { 182 | background-color: #f8f8f8; } 183 | table tr th { 184 | font-weight: bold; 185 | border: 1px solid #cccccc; 186 | text-align: left; 187 | margin: 0; 188 | padding: 6px 13px; } 189 | table tr td { 190 | border: 1px solid #cccccc; 191 | text-align: left; 192 | margin: 0; 193 | padding: 6px 13px; } 194 | table tr th :first-child, table tr td :first-child { 195 | margin-top: 0; } 196 | table tr th :last-child, table tr td :last-child { 197 | margin-bottom: 0; } 198 | 199 | img { 200 | max-width: 100%; } 201 | 202 | span.frame { 203 | display: block; 204 | overflow: hidden; } 205 | span.frame > span { 206 | border: 1px solid #dddddd; 207 | display: block; 208 | float: left; 209 | overflow: hidden; 210 | margin: 13px 0 0; 211 | padding: 7px; 212 | width: auto; } 213 | span.frame span img { 214 | display: block; 215 | float: left; } 216 | span.frame span span { 217 | clear: both; 218 | color: #333333; 219 | display: block; 220 | padding: 5px 0 0; } 221 | span.align-center { 222 | display: block; 223 | overflow: hidden; 224 | clear: both; } 225 | span.align-center > span { 226 | display: block; 227 | overflow: hidden; 228 | margin: 13px auto 0; 229 | text-align: center; } 230 | span.align-center span img { 231 | margin: 0 auto; 232 | text-align: center; } 233 | span.align-right { 234 | display: block; 235 | overflow: hidden; 236 | clear: both; } 237 | span.align-right > span { 238 | display: block; 239 | overflow: hidden; 240 | margin: 13px 0 0; 241 | text-align: right; } 242 | span.align-right span img { 243 | margin: 0; 244 | text-align: right; } 245 | span.float-left { 246 | display: block; 247 | margin-right: 13px; 248 | overflow: hidden; 249 | float: left; } 250 | span.float-left span { 251 | margin: 13px 0 0; } 252 | span.float-right { 253 | display: block; 254 | margin-left: 13px; 255 | overflow: hidden; 256 | float: right; } 257 | span.float-right > span { 258 | display: block; 259 | overflow: hidden; 260 | margin: 13px auto 0; 261 | text-align: right; } 262 | 263 | code, tt { 264 | margin: 0 2px; 265 | padding: 0 5px; 266 | white-space: nowrap; 267 | border: 1px solid #eaeaea; 268 | background-color: #f8f8f8; 269 | border-radius: 3px; } 270 | 271 | pre code { 272 | margin: 0; 273 | padding: 0; 274 | white-space: pre; 275 | border: none; 276 | background: transparent; } 277 | 278 | .highlight pre { 279 | background-color: #f8f8f8; 280 | border: 1px solid #cccccc; 281 | font-size: 13px; 282 | line-height: 19px; 283 | overflow: auto; 284 | padding: 6px 10px; 285 | border-radius: 3px; } 286 | 287 | pre { 288 | background-color: #f8f8f8; 289 | border: 1px solid #cccccc; 290 | font-size: 13px; 291 | line-height: 19px; 292 | overflow: auto; 293 | padding: 6px 10px; 294 | border-radius: 3px; } 295 | pre code, pre tt { 296 | background-color: transparent; 297 | border: none; } 298 | } 299 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/text/display/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // DisplayTextView 3 | // Displays text, first formatted with Markdown, and then Latex. 4 | // 5 | 6 | import * as _ from 'underscore'; 7 | import ItemView from 'base/item-view'; 8 | import * as marked from 'marked'; 9 | import * as emojify from 'emojify.js'; 10 | 11 | export default ItemView.extend({ 12 | template: false, 13 | 14 | // They're not always displayed as sole gistblocks, such as 15 | // when they're rendered in an edit view. For those 16 | // times, the CSS is overwritten. 17 | className: 'gistblock gistblock-text', 18 | 19 | // After render, check if the user has inputted any text. If so, 20 | // pass it along to be rendered by Mathjax and Marked. 21 | onRender() { 22 | var text = this.model.get('source'); 23 | 24 | if (!text) { return; } 25 | 26 | this.$el.html(text); 27 | this._parseText(text); 28 | }, 29 | 30 | // Parse the text of the element as Mathjax, and then pass it along to Marked. 31 | _parseText(text) { 32 | MathJax.Hub.Queue(['Typeset', MathJax.Hub, this.$el[0]]); 33 | MathJax.Hub.Queue(_.bind(this._parseMarked, this)); 34 | }, 35 | 36 | // Parse the element's HTML as Markdown, then set the element's text. 37 | _parseMarked() { 38 | var $el = this.$el; 39 | marked($el.html(), (err, content) => { 40 | $el.html(content); 41 | this._parseEmoji(); 42 | }); 43 | }, 44 | 45 | _parseEmoji() { 46 | emojify.run(this.el); 47 | } 48 | }); 49 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/text/edit/edit-text-view.hbs: -------------------------------------------------------------------------------- 1 | {{{ source }}} 2 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/text/edit/edit-text-view.styl: -------------------------------------------------------------------------------- 1 | .gistbook-textarea 2 | display block 3 | resize vertical 4 | width 100% 5 | min-height 100px 6 | padding 10px 7 | border-radius 3px 8 | border 1px solid #ccc 9 | background #f5f5f5 10 | box-shadow inset 0 1px 3px rgba(0,0,0,.1) 11 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/text/edit/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // textEditView 3 | // Modify text based on a textarea 4 | // 5 | 6 | import ItemView from 'base/item-view'; 7 | 8 | export default ItemView.extend({ 9 | template: 'editTextView', 10 | 11 | tagName: 'textarea', 12 | 13 | className: 'gistbook-textarea', 14 | 15 | // Get the value of the element, 16 | // stripping line breaks from the 17 | // start and end 18 | value() { 19 | return this.el.value.replace(/^\s+|\s+$/g, ''); 20 | } 21 | }); 22 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/title/display/display-title-view.hbs: -------------------------------------------------------------------------------- 1 |

    2 | {{ title }} 3 |

    4 | 5 | 6 | 7 | Edit title 8 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/title/display/display-title-view.styl: -------------------------------------------------------------------------------- 1 | .display-title-view 2 | font-size 14px 3 | color #fff 4 | 5 | h1 6 | font-size 19px 7 | font-weight 200 8 | cursor default 9 | display inline-block 10 | max-width 540px 11 | white-space nowrap 12 | overflow hidden 13 | text-overflow ellipsis 14 | vertical-align bottom 15 | 16 | .gistbook-edit-text 17 | font-size 15px 18 | display none 19 | position relative 20 | font-weight 500 21 | top -2px 22 | 23 | .gistbook-title-edit, .separator 24 | color #4f568d 25 | text-decoration none 26 | 27 | &.editable .gistbook-edit-text 28 | display inline-block 29 | 30 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/title/display/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // Display Title View 3 | // Shows the Gistbook's title, and the text to edit it 4 | // 5 | 6 | import * as mn from 'marionette'; 7 | import ItemView from 'base/item-view'; 8 | 9 | export default ItemView.extend({ 10 | template: 'displayTitleView', 11 | 12 | className: 'display-title-view', 13 | 14 | ui: { 15 | edit: '.gistbook-edit-text', 16 | editTitle: '.gistbook-title-edit' 17 | }, 18 | 19 | triggers: { 20 | 'click @ui.editTitle': 'edit' 21 | }, 22 | 23 | editable: false, 24 | 25 | displayTitleViewOptions: ['editable'], 26 | 27 | initialize(options) { 28 | mn.mergeOptions(this, options, this.displayTitleViewOptions); 29 | this._setClass(); 30 | }, 31 | 32 | // Sets whether the view is editable or not. 33 | _setClass() { 34 | this.$el.toggleClass('editable', this.editable); 35 | } 36 | }); 37 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/title/edit/edit-title-view.hbs: -------------------------------------------------------------------------------- 1 | 2 | or Cancel 3 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/title/edit/edit-title-view.styl: -------------------------------------------------------------------------------- 1 | .edit-title-view 2 | color #4f568d 3 | font-weight 200 4 | 5 | a 6 | color #4f568d 7 | text-decoration underline 8 | 9 | button 10 | font-size 13px 11 | 12 | input 13 | color #fff 14 | font-weight 200 15 | background transparent 16 | padding 4px 7px 17 | width 400px 18 | font-size 19px 19 | border-radius 3px 20 | border 1px solid $darkBlue 21 | 22 | &:focus 23 | outline 0 24 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/title/edit/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // EditHeaderView 3 | // The edit view for the title. 4 | // 5 | 6 | import ItemView from 'base/item-view'; 7 | 8 | export default ItemView.extend({ 9 | template: 'editTitleView', 10 | 11 | className: 'edit-title-view', 12 | 13 | ui: { 14 | input: 'input', 15 | save: 'button', 16 | cancel: 'a' 17 | }, 18 | 19 | events: { 20 | 'keypress input': 'onKeypress' 21 | }, 22 | 23 | triggers: { 24 | 'click @ui.save': 'save', 25 | 'click @ui.cancel': 'cancel' 26 | }, 27 | 28 | onSave() { 29 | var newTitle = this.ui.input.val(); 30 | this.model.set('title', newTitle); 31 | }, 32 | 33 | onKeypress(e) { 34 | if (e.keyCode !== 13) { 35 | return; 36 | } 37 | this.triggerMethod('save'); 38 | } 39 | }); 40 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/controls-wrapper/controls-wrapper.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 3 | 9 |
    10 |
    11 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/controls-wrapper/controls-wrapper.styl: -------------------------------------------------------------------------------- 1 | .controls-wrapper-view 2 | 3 | .gistbook-add-row 4 | cursor pointer 5 | position relative 6 | background #f3f3f3 7 | color #999 8 | height 30px 9 | width 35px 10 | line-height 30px 11 | text-align center 12 | margin-bottom 3px 13 | 14 | &:hover, &.active 15 | color #666 16 | background #eee 17 | 18 | .add-section-menu 19 | display none 20 | z-index 20000 21 | width 150px 22 | padding 5px 0 23 | position absolute 24 | background #fff 25 | border 1px solid #ddd 26 | border-radius 2px 27 | box-shadow 0 2px 3px rgba(0,0,0,.2) 28 | right -150px 29 | top 0 30 | 31 | a 32 | padding 2px 0 33 | display block 34 | 35 | &:hover 36 | background #f3f3f3 37 | color #000 38 | 39 | &.visible 40 | display block 41 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/controls-wrapper/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // controlsWrapper 3 | // If the user is logged in, then this wraps every view. 4 | // It also manages shared cached data between the Display 5 | // and Edit modes. 6 | // 7 | 8 | import * as bb from 'backbone'; 9 | import * as mn from 'marionette'; 10 | import * as Radio from 'radio'; 11 | import LayoutView from 'base/layout-view'; 12 | import DisplayWrapper from '../display-wrapper'; 13 | import EditWrapper from '../edit-wrapper'; 14 | import radioHelpers from '../../../helpers/radio-helpers'; 15 | 16 | var overlayChannel = Radio.channel('overlay'); 17 | 18 | export default LayoutView.extend({ 19 | template: 'controlsWrapper', 20 | 21 | className: 'controls-wrapper-view', 22 | 23 | tagName: 'li', 24 | 25 | initialMode: 'display', 26 | 27 | editOptions: { 28 | edit: true, 29 | delete: true, 30 | move: true 31 | }, 32 | 33 | controlsWrapperOptions: [ 34 | 'cache', 35 | 'editOptions', 36 | 'initialMode', 37 | 'gistbookChannel' 38 | ], 39 | 40 | regions: { 41 | wrapper: '.gistblock-wrapper' 42 | }, 43 | 44 | ui: { 45 | addRow: '.gistbook-add-row', 46 | addSectionMenu: '.add-section-menu', 47 | addText: '.gistbook-add-text', 48 | addJavascript: '.gistbook-add-js', 49 | addCss: '.gistbook-add-css', 50 | addHtml: '.gistbook-add-html' 51 | }, 52 | 53 | triggers: { 54 | 'click @ui.addRow': 'click:add:row', 55 | 'click @ui.addText': 'add:text', 56 | 'click @ui.addHtml': 'add:html', 57 | 'click @ui.addCss': 'add:css', 58 | 'click @ui.addJavascript': 'add:javascript' 59 | }, 60 | 61 | // Sets our options, binds callback context, and creates 62 | // a cached model for users to mess around with 63 | initialize(options) { 64 | this.gistSections = this.model.collection; 65 | mn.mergeOptions(this, options, this.controlsWrapperOptions); 66 | this._createCache(); 67 | }, 68 | 69 | hideAddOptions() { 70 | this.ui.addRow.removeClass('active'); 71 | overlayChannel.command('hide'); 72 | this.ui.addSectionMenu.removeClass('visible'); 73 | }, 74 | 75 | onClickAddRow() { 76 | this.ui.addRow.addClass('active'); 77 | this.listenToOnce(overlayChannel, 'click', this.hideAddOptions); 78 | overlayChannel.command('show'); 79 | this.ui.addSectionMenu.addClass('visible'); 80 | }, 81 | 82 | onEdit() { 83 | this.showActive(); 84 | }, 85 | 86 | // Refactor to use the channel 87 | onDelete() { 88 | this.model.collection.remove(this.model); 89 | }, 90 | 91 | // When the user cancels editing, first reset the cache to match 92 | // the saved state. Then, show the preview 93 | onCancel() { 94 | this._resetCache(); 95 | this.showDisplay(); 96 | }, 97 | 98 | onAddText() { 99 | this._addSection('text'); 100 | }, 101 | 102 | onAddHtml() { 103 | this._addSection('html'); 104 | }, 105 | 106 | onAddCss() { 107 | this._addSection('css'); 108 | }, 109 | 110 | onAddJavascript() { 111 | this._addSection('javascript'); 112 | }, 113 | 114 | _addSection(type) { 115 | var index = this.gistSections.indexOf(this.model); 116 | var newSection = this._createNewSection(type); 117 | this.gistSections.add(newSection, {at: index}); 118 | this.hideAddOptions(); 119 | }, 120 | 121 | showDisplay() { 122 | if (this.currentView) { this.stopListening(this.currentView); } 123 | var displayWrapper = this.getDisplayWrapper(); 124 | this.getRegion('wrapper').show(displayWrapper); 125 | this.currentView = displayWrapper; 126 | this._configurePreviewListeners(); 127 | }, 128 | 129 | showActive() { 130 | if (this.currentView) { this.stopListening(this.currentView); } 131 | var editWrapper = this.getEditWrapper(); 132 | this.getRegion('wrapper').show(editWrapper); 133 | this.currentView = editWrapper; 134 | this._configureEditListeners(); 135 | }, 136 | 137 | // When the user updates, first update the cache with the value 138 | // from the AceEditor. Then persist those changes to the actual model. 139 | // Finally, take them to the preview view. 140 | onUpdate() { 141 | this._updateCache(); 142 | this._saveCache(); 143 | this.showDisplay(); 144 | }, 145 | 146 | // Determine which view to show 147 | onRender() { 148 | if (this.initialMode === 'edit') { 149 | this.showActive(); 150 | } else { 151 | this.showDisplay(); 152 | } 153 | }, 154 | 155 | getDisplayWrapper() { 156 | return new DisplayWrapper({ 157 | mode: this.mode, 158 | editOptions: this.editOptions, 159 | model: this.getModel(), 160 | blockChannel: radioHelpers.objChannel(this.model) 161 | }); 162 | }, 163 | 164 | getModel() { 165 | var model; 166 | if (this.cache) { 167 | this.cachedModel = model = new bb.Model( 168 | this.model.toJSON() 169 | ); 170 | } else { 171 | model = this.model; 172 | } 173 | return model; 174 | }, 175 | 176 | getEditWrapper() { 177 | return new EditWrapper({ 178 | model: this.cachedModel, 179 | blockChannel: radioHelpers.objChannel(this.model) 180 | }); 181 | }, 182 | 183 | // Store a cached model on the view. The user can manipulate 184 | // this all they want. If they save the block we will persist 185 | // it to the model 186 | _createCache() { 187 | this.cachedModel = new bb.Model( 188 | this.model.toJSON() 189 | ); 190 | }, 191 | 192 | // Call this when the user hits update and wants to save 193 | // their changes to the model 194 | _saveCache() { 195 | this.model.set(this.cachedModel.toJSON()); 196 | }, 197 | 198 | // If the user changes the cache and wants to reset it, 199 | // call this 200 | _resetCache() { 201 | this.cachedModel.set(this.model.toJSON()); 202 | }, 203 | 204 | _createNewSection(type) { 205 | return new bb.Model({ 206 | type: type, 207 | source: '' 208 | }); 209 | }, 210 | 211 | // Update the cache with the latest content of the text editor. Only 212 | // makes sense to be called when the currentView is the cache 213 | _updateCache() { 214 | var cachedSource = this.getRegion('wrapper').currentView.cache; 215 | this.cachedModel.set('source', cachedSource); 216 | }, 217 | 218 | _configureEditListeners() { 219 | this.listenTo(this.currentView, { 220 | cancel: this.onCancel, 221 | update: this.onUpdate, 222 | updateCache: this._updateCache 223 | }); 224 | }, 225 | 226 | _configurePreviewListeners() { 227 | this.listenTo(this.currentView, { 228 | edit: this.onEdit, 229 | delete: this.onDelete 230 | }); 231 | } 232 | }); 233 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/display-wrapper/display-wrapper.hbs: -------------------------------------------------------------------------------- 1 |
    2 | 13 |
    14 |
    15 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/display-wrapper/display-wrapper.styl: -------------------------------------------------------------------------------- 1 | .gistblock-menu 2 | display flex 3 | padding-right 40px 4 | 5 | &:hover 6 | 7 | .gistbook-menu-container ul 8 | visibility visible 9 | 10 | .gistblock-content 11 | flex 1 12 | 13 | .gistbook-menu-container 14 | width 35px 15 | margin-right 5px 16 | margin-bottom 3px 17 | 18 | ul 19 | visibility hidden 20 | background #eee 21 | 22 | li 23 | cursor pointer 24 | display none 25 | color #666 26 | height 30px 27 | width 35px 28 | line-height 30px 29 | text-align center 30 | 31 | &:hover 32 | opacity 1 33 | 34 | &.active-option 35 | display block 36 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/display-wrapper/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // displayWrapper 3 | // This provides our edit menu when you're logged in 4 | // 5 | 6 | import * as _ from 'underscore'; 7 | import * as mn from 'marionette'; 8 | import LayoutView from 'base/layout-view'; 9 | 10 | export default LayoutView.extend({ 11 | template: 'displayWrapper', 12 | 13 | className: 'gistblock-menu', 14 | 15 | menuWrapperOptions: [ 16 | 'blockChannel', 17 | 'editOptions' 18 | ], 19 | 20 | editOptions: { 21 | edit: true, 22 | delete: true, 23 | move: true 24 | }, 25 | 26 | // Where to render the view 27 | regions: { 28 | content: '.gistblock-content' 29 | }, 30 | 31 | // Where to put the view, and the 3 menu options 32 | ui: { 33 | content: '.gistblock-content', 34 | edit: '.gistblock-edit', 35 | move: '.gistblock-move', 36 | delete: '.gistblock-delete' 37 | }, 38 | 39 | // Respond to clicks; the parent view is listening 40 | triggers: { 41 | 'click @ui.edit': 'edit', 42 | 'click @ui.move': 'move', 43 | 'click @ui.delete': 'delete' 44 | }, 45 | 46 | // Store our options on the object itself 47 | initialize(options) { 48 | mn.mergeOptions(this, options, this.menuWrapperOptions); 49 | }, 50 | 51 | createDisplayView() { 52 | return this.blockChannel.request('displayView', this.model); 53 | }, 54 | 55 | // Show the inert view after rendering 56 | onRender() { 57 | this._showMenu(); 58 | var region = this.getRegion('content'); 59 | this.displayView = this.createDisplayView(); 60 | region.show(this.displayView); 61 | }, 62 | 63 | // Show or hide each menu item based on options 64 | _showMenu() { 65 | _.each(this.editOptions, (val, key) => { 66 | this.ui[key].toggleClass('active-option', val); 67 | }); 68 | } 69 | }); 70 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/edit-wrapper/edit-wrapper.hbs: -------------------------------------------------------------------------------- 1 | 5 |
    6 |
    7 | 8 | 9 |
    10 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/edit-wrapper/edit-wrapper.styl: -------------------------------------------------------------------------------- 1 | .gistblock-editor 2 | margin 0 40px 3 | 4 | .gistblock-editor ul 5 | border-bottom 1px solid #ccc 6 | margin-bottom 15px 7 | 8 | li 9 | display inline-block 10 | 11 | a 12 | position relative 13 | bottom -1px 14 | display block 15 | padding 8px 16px 6px 16 | text-decoration none 17 | color #999 18 | 19 | &.active-tab 20 | color #000 21 | padding 8px 15px 6px 22 | border 1px solid #ccc 23 | border-bottom 0 24 | border-radius 4px 4px 0 0 25 | background #fff 26 | 27 | .gistblock-editor .gistblock-content 28 | margin-bottom 15px 29 | 30 | .gistblock-editor button 31 | width: 80px 32 | 33 | .gistblock-editor .button-container 34 | text-align right 35 | 36 | .gistblock-content > .gistblock 37 | margin-top 5px 38 | 39 | .gistblock-content > .gistblock-javascript 40 | margin-top 10px 41 | 42 | .gistblock-editor .gistblock 43 | padding-bottom 10px 44 | border-bottom 1px solid #ddd 45 | -------------------------------------------------------------------------------- /client.src/shared/views/gistbook-view/views/wrappers/edit-wrapper/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // editWrapper 3 | // A wrapper for an editable View; it provides the controls to toggle 4 | // between source/preview, and the buttons to cancel/save the changes. 5 | // 6 | 7 | import * as _ from 'underscore'; 8 | import * as mn from 'marionette'; 9 | import LayoutView from 'base/layout-view'; 10 | import EditTextView from '../../text/edit'; 11 | 12 | export default LayoutView.extend({ 13 | template: 'editWrapper', 14 | 15 | className: 'gistblock-editor', 16 | 17 | tagName: 'li', 18 | 19 | // Default values for options 20 | defaults: { 21 | // What the tab says that shows the source 22 | sourceTabText: 'Write', 23 | PreviewView: undefined, 24 | blockChannel: undefined 25 | }, 26 | 27 | sourceTabText: 'Write', 28 | 29 | editWrapperOptions: [ 30 | 'sourceTabText', 31 | 'PreviewView', 32 | 'blockChannel' 33 | ], 34 | 35 | // Store our options on the object itself. 36 | // Also set the initial mode to be code. 37 | initialize(options) { 38 | mn.mergeOptions(this, options, this.editWrapperOptions); 39 | this.cache = this.model.toJSON(); 40 | this.mode = 'write'; 41 | }, 42 | 43 | serializeData() { 44 | var data = LayoutView.prototype.serializeData.call(this); 45 | data.sourceTabText = this.sourceTabText; 46 | return data; 47 | }, 48 | 49 | // Where to put the view, and the 3 menu options 50 | ui: { 51 | content: '.gistblock-content', 52 | write: '.gistblock-write', 53 | preview: '.gistblock-preview', 54 | cancel: '.gistblock-cancel', 55 | update: '.gistblock-update' 56 | }, 57 | 58 | // Respond to clicks; the parent view is listening 59 | triggers: { 60 | 'click @ui.write': 'write', 61 | 'click @ui.preview': 'preview', 62 | 'click @ui.cancel': 'cancel', 63 | 'click @ui.update': 'update' 64 | }, 65 | 66 | // On preview, update the cache with the changes in the Ace Editor 67 | // Then, show the preview state 68 | onPreview() { 69 | if (this.mode === 'preview') { 70 | return; 71 | } 72 | this._setCache(); 73 | this.transitionUiToPreview(); 74 | this.triggerMethod('updateCache'); 75 | this.showPreview(); 76 | this.mode = 'preview'; 77 | }, 78 | 79 | onWrite() { 80 | if (this.mode === 'write') { 81 | return; 82 | } 83 | this.transitionUiToCode(); 84 | this.mode = 'write'; 85 | this.showEditor(); 86 | }, 87 | 88 | // Update the cache when the user clicks update, 89 | // only if you're in code mode 90 | onUpdate() { 91 | if (this.mode === 'preview') { 92 | return; 93 | } 94 | this._setCache(); 95 | }, 96 | 97 | transitionUiToPreview() { 98 | this.ui.write.removeClass('active-tab'); 99 | this.ui.preview.addClass('active-tab'); 100 | }, 101 | 102 | transitionUiToCode() { 103 | this.ui.preview.removeClass('active-tab'); 104 | this.ui.write.addClass('active-tab'); 105 | }, 106 | 107 | getEditTextView() { 108 | return new EditTextView({ 109 | model: this.model 110 | }); 111 | }, 112 | 113 | getPreviewView() { 114 | return this.blockChannel.request('displayView', this.model); 115 | }, 116 | 117 | // Show the Ace Editor in our region; also set our cache 118 | showEditor() { 119 | var textEditorView = this.getEditTextView(); 120 | var region = this.getRegion('content'); 121 | region.show(textEditorView); 122 | this._setCache(); 123 | }, 124 | 125 | // The preview is just an inert math view 126 | showPreview() { 127 | this._setCache(); 128 | var region = this.getRegion('content'); 129 | var previewView = this.getPreviewView(); 130 | region.show(previewView); 131 | }, 132 | 133 | // Where to render the inert view 134 | regions: { 135 | content: '.gistblock-content' 136 | }, 137 | 138 | // Show the editor view on the first render 139 | onRender() { 140 | this._showMenu(); 141 | this.showEditor(); 142 | }, 143 | 144 | // Set the cache from the value in the currentView 145 | _setCache() { 146 | var region = this.getRegion('content'); 147 | this.cache = region.currentView.value(); 148 | }, 149 | 150 | // Show or hide each menu item based on options 151 | _showMenu() { 152 | _.each(this.editOptions, (val, key) => { 153 | this.ui[key].toggleClass('active-option', val); 154 | }); 155 | } 156 | }); 157 | -------------------------------------------------------------------------------- /client.src/shared/views/loading-view/index.js: -------------------------------------------------------------------------------- 1 | // 2 | // LoadingView 3 | // Displayed inside of the main region when the app is 4 | // transitioning; dims the contents of the region. 5 | // 6 | 7 | import ItemView from 'base/item-view'; 8 | 9 | var LoadingView = ItemView.extend({ 10 | template: 'loadingView', 11 | className: 'loading-view' 12 | }); 13 | 14 | export default new LoadingView(); 15 | -------------------------------------------------------------------------------- /client.src/shared/views/loading-view/loading-view.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |
    4 | -------------------------------------------------------------------------------- /client.src/shared/views/loading-view/loading-view.styl: -------------------------------------------------------------------------------- 1 | .loading-view 2 | position absolute 3 | z-index 10000 4 | top 0 5 | left 0 6 | background $tan 7 | opacity .7 8 | height 100% 9 | width 100% 10 | 11 | .loader-container 12 | position relative 13 | height 40px 14 | -------------------------------------------------------------------------------- /client.src/shims/backbone-sync-shim.js: -------------------------------------------------------------------------------- 1 | // 2 | // Sync Shim 3 | // Backbone.Sync returns a jQuery Deferred. We wrap 4 | // it to return an A+ promise. 5 | // 6 | 7 | import * as bb from 'backbone'; 8 | 9 | var originalSync = bb.sync; 10 | bb.sync = function() { 11 | return Promise.resolve(originalSync.apply(this, arguments)); 12 | }; 13 | -------------------------------------------------------------------------------- /client.src/shims/merge-options-shim.js: -------------------------------------------------------------------------------- 1 | // 2 | // mergeOptionsShim 3 | // this.getOption is a terrible pattern. This keeps one from 4 | // having to use that. 5 | // 6 | 7 | import * as mn from 'marionette'; 8 | import * as _ from 'underscore'; 9 | 10 | mn.mergeOptions = function(target, options, mergeOptions) { 11 | if (!options) { return; } 12 | _.extend(target, _.pick(options, mergeOptions)); 13 | }; 14 | -------------------------------------------------------------------------------- /client.src/shims/radio-shim.js: -------------------------------------------------------------------------------- 1 | // 2 | // Radio Shim 3 | // We're loading Radio in place of Wreqr in this 4 | // app, but Marionette's Application will attempt 5 | // to create a channel with Wreqr's API. This 6 | // overrides that method to be a noop instead. 7 | // 8 | 9 | import * as mn from 'marionette'; 10 | 11 | mn.Application.prototype._initChannel = function() {}; 12 | -------------------------------------------------------------------------------- /client.src/shims/render-shim.js: -------------------------------------------------------------------------------- 1 | // 2 | // render 3 | // Overrides the render to support string-based templates 4 | // pulled from the `templates` module, which are the 5 | // pre-compiled JST templates. 6 | // 7 | // 8 | 9 | import * as mn from 'marionette'; 10 | import * as templates from 'templates'; 11 | 12 | mn.Renderer.render = function(templateName, data) { 13 | var err, templateFunc = templates[templateName]; 14 | 15 | if (typeof templateName !== 'string') { 16 | err = 'Templates must be specified by name in Gistbook'; 17 | } else if (!templateFunc) { 18 | err = 'Cannot render the view because the template was not found.'; 19 | } 20 | 21 | if (err) { throw new Error(err); } 22 | return templateFunc(data); 23 | }; 24 | -------------------------------------------------------------------------------- /client.src/shims/to-json-shim.js: -------------------------------------------------------------------------------- 1 | // 2 | // toJsonShim 3 | // Marionette v2.x Views use toJSON for serialization, which isn't 4 | // the intended use of that method. This resolves that problem. 5 | // 6 | 7 | import * as _ from 'underscore'; 8 | import * as mn from 'marionette'; 9 | 10 | mn.View.prototype.serializeModel = function(model) { 11 | model = model || this.model; 12 | return _.clone(model.attributes); 13 | }; 14 | 15 | mn.ItemView.prototype.serializeCollection = function(collection) { 16 | return collection.map(function(model){ return this.serializeModel(model); }, this); 17 | }; 18 | -------------------------------------------------------------------------------- /client.src/vendor/state-router/route.js: -------------------------------------------------------------------------------- 1 | // 2 | // Route 3 | // An object that manages transitioning 4 | // the app's state when a URI is matched 5 | // 6 | 7 | import * as mn from 'marionette'; 8 | 9 | export default mn.Object.extend({ 10 | show() {}, 11 | onError(e) { 12 | if (!console || !console.assert) { return; } 13 | console.assert(false, e); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /client.src/vendor/state-router/state-router.js: -------------------------------------------------------------------------------- 1 | /* jshint maxstatements: 30, maxcomplexity: 7 */ 2 | 3 | import * as _ from 'underscore'; 4 | import * as BaseRouter from 'backbone.base-router'; 5 | import Route from './route'; 6 | 7 | var StateRouter = BaseRouter.extend({ 8 | onNavigate(routeData) { 9 | var newRoute = routeData.linked; 10 | 11 | if (!(newRoute instanceof Route)) { 12 | throw new Error('A Route object must be associated with each route.'); 13 | } 14 | 15 | var redirect = _.result(newRoute, 'redirect'); 16 | if (_.isString(redirect)) { 17 | this.navigate(redirect, {trigger:true}); 18 | newRoute.triggerMethod('redirect', routeData); 19 | this.trigger('redirect', routeData); 20 | return; 21 | } 22 | 23 | if (this.authenticate && !this.authenticate(routeData)) { 24 | newRoute.triggerMethod('unauthorized', routeData); 25 | this.trigger('unauthorized', routeData); 26 | return; 27 | } 28 | 29 | if (this.currentRoute) { 30 | this.currentRoute.triggerMethod('exit', routeData); 31 | } 32 | this.currentRoute = newRoute; 33 | newRoute.triggerMethod('enter', routeData); 34 | 35 | if (!newRoute.fetch) { 36 | newRoute.show(undefined, routeData); 37 | newRoute.triggerMethod('show', routeData); 38 | } else { 39 | 40 | // Wait for the data to come back, then 41 | // show the view if the route is still active. 42 | Promise.resolve(newRoute.fetch(routeData)) 43 | .then(data => { 44 | newRoute.triggerMethod('fetch', routeData, data); 45 | if (newRoute !== this.currentRoute) { return; } 46 | newRoute.show(data, routeData); 47 | newRoute.triggerMethod('show', routeData); 48 | }) 49 | .catch(e => { 50 | if (newRoute !== this.currentRoute) { return; } 51 | newRoute.triggerMethod('error', e, routeData); 52 | }); 53 | } 54 | } 55 | }); 56 | 57 | StateRouter.Route = Route; 58 | 59 | export default StateRouter; 60 | -------------------------------------------------------------------------------- /config/_personal-access-token.json: -------------------------------------------------------------------------------- 1 | { 2 | "token": "YOUR_TOKEN_HERE" 3 | } 4 | -------------------------------------------------------------------------------- /deploy/ansible/deploy.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # deploy code and restart services 3 | 4 | - hosts: all 5 | remote_user: ubuntu 6 | sudo: yes 7 | roles: 8 | - deploy 9 | - startup 10 | -------------------------------------------------------------------------------- /deploy/ansible/group_vars/all.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # defaults for all groups 3 | 4 | app_base_path: "/mnt/gistbook/" 5 | default_user: "ubuntu" 6 | ansible_ssh_private_key_file: "{{ lookup('env', 'PWD') }}/config/secrets/Gistbook.pem" 7 | -------------------------------------------------------------------------------- /deploy/ansible/host_vars/54.164.153.94: -------------------------------------------------------------------------------- 1 | --- 2 | # configuration for production 3 | 4 | app_fqdn: "gistbook.io" 5 | node_env: "production" 6 | -------------------------------------------------------------------------------- /deploy/ansible/host_vars/54.173.215.88: -------------------------------------------------------------------------------- 1 | --- 2 | # configuration for staging 3 | 4 | app_fqdn: "staging.gistbook.io" 5 | node_env: "staging" 6 | -------------------------------------------------------------------------------- /deploy/ansible/inventory/production: -------------------------------------------------------------------------------- 1 | [appservers] 2 | 54.164.153.94 3 | -------------------------------------------------------------------------------- /deploy/ansible/inventory/staging: -------------------------------------------------------------------------------- 1 | [appservers] 2 | 54.173.215.88 3 | -------------------------------------------------------------------------------- /deploy/ansible/provision.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # provision machine and deploy code 3 | 4 | - hosts: all 5 | remote_user: ubuntu 6 | sudo: yes 7 | roles: 8 | - base 9 | - nginx 10 | - services 11 | - deploy 12 | - startup 13 | -------------------------------------------------------------------------------- /deploy/ansible/roles/base/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # configure base machine 3 | 4 | - name: make sure apt list is updated 5 | apt: update_cache=yes 6 | 7 | - name: make sure nginx is installed 8 | apt: name=nginx state=present 9 | 10 | - name: make sure node ppa is available 11 | apt_repository: repo='ppa:chris-lea/node.js' 12 | 13 | - name: make sure apt list is updated 14 | apt: update_cache=yes 15 | 16 | - name: make sure nodejs is installed 17 | apt: name=nodejs state=present 18 | 19 | - name: make sure the latest npm is installed 20 | command: npm install -g npm 21 | -------------------------------------------------------------------------------- /deploy/ansible/roles/deploy/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # deploy it 3 | 4 | - name: Make sure the base directory exsists 5 | file: path={{app_base_path}} state=directory 6 | 7 | - name: Make sure the front end code is on the box 8 | synchronize: src=../../client.prod dest={{app_base_path}} 9 | 10 | - name: Make sure the server code is on the box 11 | synchronize: src=../../server dest={{app_base_path}} 12 | 13 | - name: Make sure a project config directory exists 14 | file: path={{app_base_path}}/config state=directory 15 | 16 | - name: Make sure package.json is on the box 17 | copy: src=../../package.json dest={{app_base_path}} backup=no 18 | 19 | - name: Make sure NPM install has been run 20 | npm: path={{app_base_path}} 21 | 22 | - name: Make sure the config code is on the box 23 | synchronize: src=../../config/client-info.json dest={{app_base_path}}/config/client-info.json 24 | 25 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # configure nginx 3 | 4 | - name: make sure nginx has a configuration file 5 | template: src=gistbook.conf dest=/etc/nginx/conf.d/gistbook.conf backup=no 6 | 7 | - name: restart nginx 8 | service: name=nginx state=restarted 9 | -------------------------------------------------------------------------------- /deploy/ansible/roles/nginx/templates/gistbook.conf: -------------------------------------------------------------------------------- 1 | server { 2 | listen 80; 3 | listen 443 default_server ssl; 4 | server_name {{ app_fqdn }} www.{{ app_fqdn }}; 5 | client_max_body_size 5M; 6 | 7 | location / { 8 | proxy_pass http://localhost:3344; 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /deploy/ansible/roles/services/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # configure services 3 | 4 | - name: Make sure the gistbook daemon is on the box 5 | template: src=gistbook.conf dest=/etc/init/gistbook.conf backup=no 6 | -------------------------------------------------------------------------------- /deploy/ansible/roles/services/templates/gistbook.conf: -------------------------------------------------------------------------------- 1 | #!upstart 2 | # 3 | # Copy me to /etc/init/ 4 | # This installs a daemon as a system level call and ensures the process is consistently restarted on error. 5 | # Manual start, stop, and restart respected. 6 | # 7 | 8 | description "Daemon for nodejs gistbook server" 9 | 10 | start on startup 11 | stop on shutdown 12 | respawn 13 | 14 | env NODE_ENV={{node_env}} 15 | script 16 | /usr/bin/node {{app_base_path}}server/app.js >> /var/log/gistbook 2>&1 17 | end script 18 | -------------------------------------------------------------------------------- /deploy/ansible/roles/startup/tasks/main.yml: -------------------------------------------------------------------------------- 1 | --- 2 | # start it up. 3 | 4 | - name: Make sure the node app server is running 5 | service: name=gistbook state=restarted 6 | -------------------------------------------------------------------------------- /gruntfile.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var webpack = require('webpack'); 4 | 5 | module.exports = function(grunt) { 6 | require('load-grunt-tasks')(grunt); 7 | 8 | grunt.initConfig({ 9 | 10 | // Specify where you'd like to keep your files 11 | app: { 12 | server: 'server', 13 | src : 'client.src', 14 | dev : 'client.dev', 15 | prod : 'client.prod', 16 | tmp : 'tmp', 17 | bower : 'bower_components' 18 | }, 19 | 20 | clean: { 21 | dev : ['tmp', '<%= app.dev %>'], 22 | prod: ['tmp', '<%= app.prod %>'] 23 | }, 24 | 25 | jshint: { 26 | options: { 27 | jshintrc : '.jshintrc' 28 | }, 29 | source: { 30 | src: ['<%= app.src %>/**/*.js'] 31 | } 32 | }, 33 | 34 | handlebars: { 35 | templates: { 36 | options: { 37 | prettify: true, 38 | 39 | // This converts our file names into camelCase names. 40 | // For instance, some-name.okay.ext => someNameOkay 41 | processName: function(filename) { 42 | var basename = path.basename( filename, path.extname(filename) ); 43 | return basename.replace(/[-\.]([a-z0-9])/g, function (g) { return g[1].toUpperCase(); }); 44 | } 45 | }, 46 | files: { 47 | '<%= app.tmp %>/templates.js': ['<%= app.src %>/**/*.hbs'] 48 | } 49 | } 50 | }, 51 | 52 | stylus: { 53 | options: { 54 | 'include css': true, 55 | import: ['variables', 'nib', 'mixins'], 56 | paths: ['bower_components'] 57 | }, 58 | dev: { 59 | src: ['<%= app.src %>/core/assets/styl/index.styl', '<%= app.src %>/core/views/**/*.styl', '<%= app.src %>/modules/**/*.styl', '<%= app.src %>/vendor/**/*.styl', '<%= app.src %>/shared/**/*.styl'], 60 | dest: '<%= app.dev %>/style.css' 61 | }, 62 | prod: { 63 | src: ['<%= app.src %>/core/assets/styl/index.styl', '<%= app.src %>/core/views/**/*.styl', '<%= app.src %>/modules/**/*.styl', '<%= app.src %>/vendor/**/*.styl', '<%= app.src %>/shared/**/*.styl'], 64 | dest: '<%= app.prod %>/style.css' 65 | }, 66 | }, 67 | 68 | cssmin: { 69 | prod: { 70 | src: '<%= stylus.prod.dest %>', 71 | dest: '<%= stylus.prod.dest %>' 72 | } 73 | }, 74 | 75 | // Only minify images for production. Just copy for dev. 76 | imagemin: { 77 | prod: { 78 | expand: true, 79 | cwd: '<%= app.src %>/core/assets', 80 | src: ['img/**/*.{png,jpg}'], 81 | dest: '<%= app.prod %>' 82 | } 83 | }, 84 | 85 | copy: { 86 | favicon_dev: { 87 | src: '<%= app.src %>/core/assets/favicon.ico', 88 | dest: '<%= app.dev %>/favicon.ico' 89 | }, 90 | favicon_prod: { 91 | src: '<%= app.src %>/core/assets/favicon.ico', 92 | dest: '<%= app.prod %>/favicon.ico' 93 | }, 94 | images: { 95 | expand: true, 96 | flatten: true, 97 | src: '<%= app.src %>/core/assets/img/**/*', 98 | dest: '<%= app.dev %>/img' 99 | }, 100 | emoji_dev: { 101 | expand: true, 102 | flatten: true, 103 | src: 'node_modules/emojify.js/images/**/*', 104 | dest: '<%= app.dev %>/img/emoji' 105 | }, 106 | emoji_prod: { 107 | expand: true, 108 | flatten: true, 109 | src: 'node_modules/emojify.js/images/**/*', 110 | dest: '<%= app.prod %>/img/emoji' 111 | }, 112 | fonts_dev: { 113 | expand: true, 114 | flatten: true, 115 | src: ['<%= app.bower %>/octicons/octicons/*.{ttf,eot,svg,woff}', '<%= app.bower %>/entypo/font/*.{ttf,eot,svg,woff}'], 116 | dest: '<%= app.dev %>/fonts' 117 | }, 118 | fonts_prod: { 119 | expand: true, 120 | flatten: true, 121 | src: ['<%= app.bower %>/octicons/octicons/*.{ttf,eot,svg,ttf,woff}', '<%= app.bower %>/entypo/font/*.{ttf,eot,svg,woff}'], 122 | dest: '<%= app.prod %>/fonts' 123 | } 124 | }, 125 | 126 | watch: { 127 | 128 | // Refresh the watch task when the Gruntfile changes 129 | grunt: { 130 | files: ['gruntfile.js'] 131 | }, 132 | styles: { 133 | files: ['<%= app.src %>/**/*.styl'], 134 | tasks: ['stylus:dev'] 135 | }, 136 | images: { 137 | files: ['<%= app.src %>/img/**/*', '!<%= app.src %>/img/sprite/*'], 138 | tasks: ['copy:images'] 139 | }, 140 | fonts: { 141 | files: ['<%= app.src %>/fonts/**/*'], 142 | tasks: ['copy:fonts'] 143 | }, 144 | hbs: { 145 | files: ['<%= app.src %>/**/*.hbs'], 146 | tasks: ['handlebars', 'webpack:dev'] 147 | }, 148 | 149 | // Refresh the browser when clientside code change 150 | assets: { 151 | files: ['<%= app.dev %>/**/*', '!<%= app.dev %>/img/emoji/**'], 152 | options: { 153 | spawn: false, 154 | livereload: 35729 155 | } 156 | }, 157 | 158 | // Restart the express app & refresh browser when 159 | // server-side code changes 160 | express: { 161 | options: { 162 | spawn: false, 163 | livereload: 35729 164 | }, 165 | files: '<%= app.server %>/**/*', 166 | tasks: ['express:dev'] 167 | } 168 | }, 169 | 170 | express: { 171 | options: { 172 | script: '<%= app.server %>/app.js', 173 | output: 'Gistbook is listening on port 3344' 174 | }, 175 | dev: { 176 | options: { 177 | node_env: 'development', 178 | } 179 | } 180 | }, 181 | 182 | webpack: { 183 | options: { 184 | entry: './<%= app.src %>/core/index.js', 185 | module: { 186 | loaders: [ 187 | {test: /templates/, loader: 'imports?Handlebars=handlebars/dist/handlebars.runtime.js!exports?this.JST'}, 188 | {test: /\.js$/, exclude: /node_modules/, loader: '6to5-loader'}, 189 | {test: /\.json$/, loader: 'json-loader'} 190 | ] 191 | }, 192 | resolve: { 193 | alias: { 194 | handlebars: 'handlebars/dist/handlebars.runtime.js', 195 | marionette: 'backbone.marionette', 196 | 'backbone.wreqr': 'backbone.radio', 197 | radio: 'backbone.radio', 198 | _: 'underscore' 199 | }, 200 | modulesDirectories: ['node_modules', '<%= app.tmp %>', '<%= app.src %>'] 201 | }, 202 | cache: true, 203 | watch: true 204 | }, 205 | dev: { 206 | output: { 207 | path: './<%= app.dev %>/', 208 | filename: 'script.js', 209 | pathinfo: true 210 | }, 211 | devtool: 'eval-source-map', 212 | debug: true 213 | }, 214 | prod: { 215 | output: { 216 | path: './client.prod/', 217 | filename: 'script.js' 218 | } 219 | } 220 | }, 221 | 222 | uglify: { 223 | prod: { 224 | src: '<%= app.prod %>/script.js', 225 | dest: '<%= app.prod %>/script.js' 226 | } 227 | } 228 | }); 229 | 230 | grunt.registerTask('default', 'An alias of test', ['test']); 231 | 232 | grunt.registerTask('test', 'Runs unit tests', ['jshint']); 233 | 234 | grunt.registerTask('build', 'Build the application', function(target) { 235 | target = target || 'dev'; 236 | var images = (target === 'dev') ? 'copy:images' : 'imagemin'; 237 | 238 | var taskArray = [ 239 | 'clean:'+target, 240 | 'handlebars', 241 | 'webpack:'+target, 242 | 'copy:favicon_'+target, 243 | 'copy:emoji_'+target, 244 | 'copy:fonts_'+target, 245 | images, 246 | 'stylus:'+target 247 | ]; 248 | 249 | if (target === 'prod') { 250 | taskArray.push('cssmin:prod', 'uglify:prod'); 251 | } 252 | 253 | grunt.task.run(taskArray); 254 | }); 255 | 256 | grunt.registerTask('work', 'Develop the app', ['jshint', 'build:dev', 'express:dev', 'watch']); 257 | 258 | // For running the site on a production server 259 | grunt.registerTask('deploy', 'Deploy the app', ['jshint', 'build:prod']); 260 | 261 | }; 262 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "gistbook", 3 | "version": "0.2.1", 4 | "description": "A place to share code snippets on the web.", 5 | "main": "index.js", 6 | "scripts": { 7 | "postinstall": "bower install", 8 | "test": "grunt test", 9 | "get-secrets": "./.bin/get-secrets", 10 | "configure-hosts-local": "hostile set 127.0.0.1 gistbook.loc", 11 | "provision": "grunt build:prod && ansible-playbook -i deploy/ansible/inventory/production deploy/ansible/provision.yml", 12 | "provision-staging": "grunt build:prod && ansible-playbook -i deploy/ansible/inventory/staging deploy/ansible/provision.yml", 13 | "deploy": "grunt build:prod && ansible-playbook -i deploy/ansible/inventory/production deploy/ansible/deploy.yml", 14 | "deploy-staging": "grunt build:prod && ansible-playbook -i deploy/ansible/inventory/staging deploy/ansible/deploy.yml" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "https://github.com/Gistbook/gistbook.git" 19 | }, 20 | "keywords": [ 21 | "gist", 22 | "gistbook", 23 | "gists", 24 | "github", 25 | "javascript", 26 | "html", 27 | "css", 28 | "interactive", 29 | "snippets", 30 | "code", 31 | "social", 32 | "sharing" 33 | ], 34 | "author": "Jmeas", 35 | "license": "MIT", 36 | "bugs": { 37 | "url": "https://github.com/Gistbook/gistbook/issues" 38 | }, 39 | "homepage": "https://github.com/Gistbook/gistbook", 40 | "devDependencies": { 41 | "6to5": "^1.10.10", 42 | "6to5-loader": "^0.2.3", 43 | "bach": "^0.4.0", 44 | "bower": "^1.3.12", 45 | "chalk": "^0.5.1", 46 | "exports-loader": "^0.6.2", 47 | "grunt": "~0.4.2", 48 | "grunt-contrib-clean": "~0.5.0", 49 | "grunt-contrib-copy": "~0.5.0", 50 | "grunt-contrib-cssmin": "^0.10.0", 51 | "grunt-contrib-imagemin": "~0.5.0", 52 | "grunt-contrib-jshint": "git://github.com/jmeas/grunt-contrib-jshint.git#hi-es6-modules", 53 | "grunt-contrib-stylus": "^0.15.1", 54 | "grunt-contrib-uglify": "~0.3.2", 55 | "grunt-contrib-watch": "~0.6.0", 56 | "grunt-express-server": "~0.4.13", 57 | "grunt-webpack": "^1.0.5", 58 | "hostile": "^1.0.0", 59 | "imports-loader": "^0.6.2", 60 | "load-grunt-tasks": "~0.4.0", 61 | "mkdirp": "^0.5.0", 62 | "ssh2": "^0.3.6", 63 | "userhome": "^1.0.0" 64 | }, 65 | "dependencies": { 66 | "backbone": "^1.1.2", 67 | "backbone.babysitter": "^0.1.1", 68 | "backbone.base-router": "^0.4.1", 69 | "backbone.intercept": "^0.3.0", 70 | "backbone.marionette": "^2.3.0-pre", 71 | "backbone.radio": "^0.6.0", 72 | "body-parser": "^1.7.0", 73 | "browser-module-cache": "^0.1.3", 74 | "cookies": "^0.4.1", 75 | "cookies-js": "^0.4.0", 76 | "detective": "^4.0.0", 77 | "emojify.js": "^0.9.5", 78 | "express": "^4.0.0", 79 | "express-handlebars": "^1.1.0", 80 | "express-routebuilder": "^2.0.0", 81 | "grunt-contrib-handlebars": "^0.9.1", 82 | "handlebars": "^2.0.0", 83 | "jquery": "^2.1.0", 84 | "json-loader": "^0.5.1", 85 | "marked": "^0.3.2", 86 | "octonode": "^0.6.2", 87 | "semantic-ui": "^1.1.2", 88 | "sortable": "git://github.com/jmeas/Sortable.git#optional-set-text", 89 | "spin.js": "^2.0.1", 90 | "underscore": "1.7.0" 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /server/app.js: -------------------------------------------------------------------------------- 1 | // Load dependencies 2 | const path = require('path'); 3 | const express = require('express'); 4 | const routeBuilder = require('express-routebuilder'); 5 | const exphbs = require('express-handlebars'); 6 | const bodyParser = require('body-parser'); 7 | 8 | // Paths and variables 9 | const ENV = process.env.NODE_ENV; 10 | const BASE_DIR = __dirname; 11 | const BASE_PATH = path.normalize(BASE_DIR + '/..'); 12 | const ASSETS_DIR = ENV === 'development' ? 'client.dev' : 'client.prod'; 13 | const ASSETS_PATH = BASE_PATH + '/' + ASSETS_DIR; 14 | const MANIFEST = require('../package.json'); 15 | const VERSION = MANIFEST.version; 16 | 17 | const VIEWS_DIR = path.join(BASE_DIR, 'views'); 18 | 19 | // Github authentication will only respond to this port 20 | // during development. Only change the port if you 21 | // don't need to develop with authentication. 22 | const PORT = 3344; 23 | 24 | // Start the app 25 | var app = express(); 26 | 27 | // Static files 28 | app.use(express.static(ASSETS_PATH)); 29 | 30 | // The maximum size of a request 31 | const BODY_LIMIT = '5mb'; 32 | 33 | // Parse JSON bodies 34 | app.use(bodyParser.json({ limit: BODY_LIMIT })); 35 | app.use(bodyParser.urlencoded({ limit: BODY_LIMIT, extended: true })); 36 | 37 | // Templates 38 | const hbsOptions = { 39 | extname: '.hbs', 40 | layoutsDir: VIEWS_DIR + '/layouts', 41 | partialsDir: VIEWS_DIR + '/partials', 42 | defaultLayout: 'main' 43 | }; 44 | app.set('view engine', '.hbs'); 45 | app.set('views', VIEWS_DIR); 46 | app.engine('.hbs', exphbs(hbsOptions)); 47 | 48 | app.locals.VERSION = VERSION; 49 | 50 | var routes = { 51 | all: { 52 | '*': require('./middleware/env') 53 | }, 54 | get: { 55 | '/login': require('./middleware/login'), 56 | '/logout': require('./middleware/logout'), 57 | '/github/auth': require('./middleware/auth-callback'), 58 | '/output/:outputId': require('./middleware/output'), 59 | '*': [ 60 | require('./middleware/verify'), 61 | require('./middleware/render') 62 | ] 63 | }, 64 | post: { 65 | '/compile': require('./middleware/compile') 66 | }, 67 | param: { 68 | outputId: function (req, res, next, outputId) { 69 | req.outputId = outputId; 70 | next(); 71 | } 72 | } 73 | }; 74 | 75 | var router = express.Router(); 76 | routeBuilder(router, routes); 77 | app.use(router); 78 | 79 | // Start the app 80 | app.listen(PORT); 81 | console.log('Gistbook is listening on port ' + PORT + '.'); 82 | -------------------------------------------------------------------------------- /server/helpers/auth-helpers.js: -------------------------------------------------------------------------------- 1 | // 2 | // authHelpers 3 | // Useful things for authentication 4 | // 5 | 6 | var ENV = process.env.NODE_ENV || 'development'; 7 | var github = require('octonode'); 8 | 9 | var authHelpers = { 10 | url: function() { 11 | var clientInfo = require('../../config/client-info.json')[ENV]; 12 | return github.auth.config({ 13 | id: clientInfo.clientId, 14 | secret: clientInfo.clientSecret 15 | }).login(['gist']); 16 | } 17 | }; 18 | 19 | module.exports = authHelpers; 20 | -------------------------------------------------------------------------------- /server/helpers/page-cache.js: -------------------------------------------------------------------------------- 1 | // 2 | // pageCache 3 | // Caches our pages 4 | // 5 | 6 | // Our cache 7 | var cache = {}; 8 | 9 | module.exports = { 10 | set: function(uniqueId, html) { 11 | cache[uniqueId] = html; 12 | }, 13 | 14 | // If the object exists in the cache, 15 | // delete it from the cache and then 16 | // return it. This ensures that each request 17 | // is done a single time 18 | get: function(uniqueId) { 19 | if (cache[uniqueId]) { 20 | var obj = cache[uniqueId]; 21 | delete cache[uniqueId]; 22 | return obj; 23 | } 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /server/helpers/token-helpers.js: -------------------------------------------------------------------------------- 1 | // 2 | // tokenHelpers 3 | // Useful functions for working with our token. To use them, 4 | // pass in the instance of cookies. 5 | // 6 | 7 | // How many years til the token expires 8 | const TOKEN_EXPIRATION = 1; 9 | 10 | var tokenHelpers = { 11 | 12 | // Destroy the auth token 13 | destroyToken: function(cookies) { 14 | var now = new Date(); 15 | var thePast = new Date(); 16 | thePast.setYear(now.getFullYear() - 1); 17 | 18 | var destroyOptions = { 19 | expires: thePast, 20 | httpOnly: false, 21 | overwrite: true 22 | }; 23 | 24 | cookies.set('token', '', destroyOptions); 25 | }, 26 | 27 | setToken: function(cookies, token) { 28 | var now = new Date(); 29 | var oneYr = new Date(); 30 | oneYr.setYear(now.getFullYear() + TOKEN_EXPIRATION); 31 | 32 | var cookieOptions = { 33 | expires: oneYr, 34 | httpOnly: false, 35 | overwrite: true 36 | }; 37 | 38 | cookies.set('token', token, cookieOptions); 39 | } 40 | }; 41 | 42 | module.exports = tokenHelpers; 43 | -------------------------------------------------------------------------------- /server/middleware/auth-callback.js: -------------------------------------------------------------------------------- 1 | // 2 | // authCallback 3 | // This is the endpoint that Github redirects us to after 4 | // we attempt a login. All that it does is forward you along 5 | // to the base route. 6 | // 7 | 8 | // Get the URL for the Github login 9 | var github = require('octonode'); 10 | var Cookies = require('cookies'); 11 | var tokenHelpers = require('../helpers/token-helpers'); 12 | 13 | // Redirect us to the Github login URL 14 | var authCallback = function(req, res, next) { 15 | var cookies = new Cookies(req, res); 16 | 17 | github.auth.login(req.query.code, function(err, token) { 18 | // If we got a token, save it to a cookie 19 | if (token) { 20 | tokenHelpers.setToken(cookies, token); 21 | } 22 | // Then redirect the user 23 | res.writeHead(301, {'Content-Type': 'text/plain', 'Location': '/'}); 24 | res.end(); 25 | }); 26 | }; 27 | 28 | module.exports = authCallback; 29 | -------------------------------------------------------------------------------- /server/middleware/compile.js: -------------------------------------------------------------------------------- 1 | // 2 | // compile 3 | // Returns a URL to access the HTML that you send over 4 | // 5 | 6 | var cache = require('../helpers/page-cache'); 7 | 8 | var token = function() { 9 | return Math.random().toString(36).slice(2); 10 | }; 11 | 12 | module.exports = function(req, res) { 13 | var randomToken = token(); 14 | cache.set(randomToken, req.body.html); 15 | res.send({ 16 | token: randomToken 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /server/middleware/env.js: -------------------------------------------------------------------------------- 1 | // 2 | // env 3 | // Passes the env along to the client 4 | // 5 | 6 | var env = function(req, res, next) { 7 | res.locals.env = process.env.NODE_ENV; 8 | res.locals.devMode = res.locals.env === 'development'; 9 | next(); 10 | }; 11 | 12 | module.exports = env; 13 | -------------------------------------------------------------------------------- /server/middleware/login.js: -------------------------------------------------------------------------------- 1 | // 2 | // login 3 | // A simple endpoint that redirects to Github 4 | // to request authorization 5 | // 6 | 7 | // Get the URL for the Github login 8 | var authHelpers = require('../helpers/auth-helpers'); 9 | var Cookies = require('cookies'); 10 | var tokenHelpers = require('../helpers/token-helpers'); 11 | 12 | var login = function(req, res, next) { 13 | var redirectUrl = '/'; 14 | 15 | // If we're not in development mode, then we redirect to Github to authenticate 16 | if (res.locals.env !== 'development') { 17 | redirectUrl = authHelpers.url(); 18 | } 19 | 20 | // Otherwise, we use hardcoded user data as the token and redirect home 21 | else { 22 | var token; 23 | var cookies = new Cookies(req, res); 24 | try { 25 | token = require('../../config/personal-access-token.json').token; 26 | } catch(e) { 27 | console.log('Please make a personal access token.'); 28 | console.log('For more info, read: https://github.com/jmeas/gistbook#developing-locally'); 29 | } 30 | tokenHelpers.setToken(cookies, token); 31 | } 32 | res.writeHead(301, {'Content-Type': 'text/plain', 'Location': redirectUrl}); 33 | res.end(); 34 | }; 35 | 36 | module.exports = login; 37 | -------------------------------------------------------------------------------- /server/middleware/logout.js: -------------------------------------------------------------------------------- 1 | // 2 | // logout 3 | // This bit of middleware destroys our cookie before redirecting 4 | // the user to the login page 5 | // 6 | 7 | var tokenHelpers = require('../helpers/token-helpers'); 8 | var Cookies = require('cookies'); 9 | 10 | // Destroys our token, then redirects us home 11 | var logout = function(req, res, next) { 12 | 13 | // Create an instance of cookies 14 | var cookies = new Cookies(req, res); 15 | 16 | // Destroy the token. It's no problem if it doesn't exist 17 | tokenHelpers.destroyToken(cookies); 18 | 19 | // Finally, redirect the user and conclude our response 20 | res.writeHead(301, {'Content-Type': 'text/plain', 'Location': '/'}); 21 | res.end(); 22 | }; 23 | 24 | module.exports = logout; 25 | -------------------------------------------------------------------------------- /server/middleware/output.js: -------------------------------------------------------------------------------- 1 | // 2 | // output 3 | // Returns a cached Gistbook 4 | // 5 | 6 | var cache = require('../helpers/page-cache'); 7 | 8 | module.exports = function(req, res) { 9 | var token = req.outputId; 10 | var cached = cache.get(token); 11 | res.send(cached); 12 | }; 13 | -------------------------------------------------------------------------------- /server/middleware/render.js: -------------------------------------------------------------------------------- 1 | // 2 | // render 3 | // Renders our template and returns it. 4 | // Always the last middleware to be called. 5 | // 6 | 7 | var render = function(req, res) { 8 | return res.render('index'); 9 | }; 10 | 11 | module.exports = render; 12 | -------------------------------------------------------------------------------- /server/middleware/verify.js: -------------------------------------------------------------------------------- 1 | // 2 | // verify 3 | // Verifies that our login token still works 4 | // 5 | 6 | var github = require('octonode'); 7 | var Cookies = require('cookies'); 8 | var tokenHelpers = require('../helpers/token-helpers'); 9 | 10 | var verify = function(req, res, next) { 11 | var cookies = new Cookies(req, res); 12 | var token = cookies.get('token'); 13 | 14 | res.locals.authorized = false; 15 | 16 | // If we have no token, move along 17 | if (!token) { 18 | next(); 19 | } 20 | 21 | // Otherise we do have the token and should verify it 22 | else { 23 | var client = github.client(token); 24 | req.client = client; 25 | 26 | client.get('/user', {}, function(err, status, body, header) { 27 | if (err) { 28 | tokenHelpers.destroyToken(cookies); 29 | } 30 | else { 31 | 32 | // Check to see if gists are included in our scopes 33 | var scopes = header['x-oauth-scopes'].split(', '); 34 | // If it's not there, destroy the cookie 35 | if (scopes.indexOf('gist') == -1) { 36 | tokenHelpers.destroyToken(cookies); 37 | } else { 38 | req.authorized = true; 39 | res.locals.authorized = true; 40 | 41 | // Gather useful data from the response 42 | body.scopes = scopes; 43 | res.locals.user = JSON.stringify(body); 44 | res.locals.rateMax = header['x-ratelimit-limit']; 45 | res.locals.rateLeft = header['x-ratelimit-remaining']; 46 | res.locals.rateReset = header['x-ratelimit-reset']; 47 | } 48 | } 49 | next(); 50 | }); 51 | } 52 | }; 53 | 54 | module.exports = verify; 55 | -------------------------------------------------------------------------------- /server/views/index.hbs: -------------------------------------------------------------------------------- 1 |
    2 |
    3 |

    4 | 5 | 6 | Gistbook 7 | Alpha 8 | 9 |

    10 | 11 |
    12 |
    13 |
    14 |
    15 |
    16 | 17 | -------------------------------------------------------------------------------- /server/views/layouts/main.hbs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Gistbook 7 | 8 | 9 | {{> semantic-ui-css }} 10 | {{> livereload-config }} 11 | 12 | 13 | {{{ body }}} 14 | {{> initial-data }} 15 | {{> mathjax-config }} 16 | 17 | 18 | 19 | {{> google-analytics }} 20 | 21 | 22 | -------------------------------------------------------------------------------- /server/views/partials/google-analytics.hbs: -------------------------------------------------------------------------------- 1 | {{#unless devMode}} 2 | 10 | {{/unless}} 11 | -------------------------------------------------------------------------------- /server/views/partials/initial-data.hbs: -------------------------------------------------------------------------------- 1 | 18 | -------------------------------------------------------------------------------- /server/views/partials/livereload-config.hbs: -------------------------------------------------------------------------------- 1 | {{#if devMode}} 2 | 3 | {{/if}} 4 | -------------------------------------------------------------------------------- /server/views/partials/mathjax-config.hbs: -------------------------------------------------------------------------------- 1 | 15 | -------------------------------------------------------------------------------- /server/views/partials/semantic-ui-css.hbs: -------------------------------------------------------------------------------- 1 | 2 | --------------------------------------------------------------------------------