├── .babelrc ├── .gitignore ├── .travis.yml ├── README.md ├── example.gif ├── package.json ├── scripts ├── build-chrome ├── bundle-js └── styles ├── shells └── chrome │ ├── inject.js │ └── manifest.json └── src ├── js ├── api.js ├── dom.js └── live-preview.js └── styles └── live-preview.css /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015"] 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | dist 4 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - 'node' 4 | deploy: 5 | provider: script 6 | script: npm run release 7 | on: 8 | branch: master 9 | tags: true 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Octo Preview 2 | 3 | A Chrome extension that displays live previews of Markdown comments while you type. Works with Issues + Pull Requests. *Should* support GitHub Enterprise (*but it is currently untested*). 4 | 5 | ![animated example](example.gif) 6 | 7 | ## Where to Download 8 | 9 | - [Chrome](https://chrome.google.com/webstore/detail/octo-preview/elomekmlfonmdhmpmdfldcjgdoacjcba) 10 | 11 | ## Todo 12 | 13 | - Port to Firefox 14 | -------------------------------------------------------------------------------- /example.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DrewML/octo-preview/9a8906982a06e07bd0d7986c49728a5cdaf26877/example.gif -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "octo-preview", 3 | "description": "Live preview for markdown comments in PRs/Issues on GitHub", 4 | "scripts": { 5 | "build-chrome": "./scripts/build-chrome", 6 | "zip": "zip -jr chrome.zip dist/chrome/", 7 | "release": "npm run build-chrome && npm run zip && webstore upload --file chrome.zip --auto-publish" 8 | }, 9 | "author": "Andrew Levine", 10 | "devDependencies": { 11 | "chrome-webstore-upload-cli": "^0.2.0", 12 | "babel-core": "^6.3.15", 13 | "babel-preset-es2015": "^6.3.13", 14 | "babelify": "^7.2.0", 15 | "browserify": "^11.2.0" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /scripts/build-chrome: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CHROME_SHELL="shells/chrome" 4 | CHROME_DIST="dist/chrome" 5 | BUNDLE="./scripts/bundle-js" 6 | STYLES="./scripts/styles" 7 | 8 | rm -rf $CHROME_DIST && mkdir -p $CHROME_DIST 9 | $BUNDLE $CHROME_DIST/live-preview.js 10 | cp $CHROME_SHELL/* $CHROME_DIST 11 | $STYLES > $CHROME_DIST/live-preview.css 12 | -------------------------------------------------------------------------------- /scripts/bundle-js: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | BROWSERIFY="./node_modules/.bin/browserify" 4 | BUNDLE_ENTRY="src/js/live-preview.js" 5 | 6 | $BROWSERIFY -t babelify $BUNDLE_ENTRY -o $1 7 | -------------------------------------------------------------------------------- /scripts/styles: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | CSS_DIR="src/styles" 4 | 5 | cat $CSS_DIR/* 6 | -------------------------------------------------------------------------------- /shells/chrome/inject.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | window.addEventListener('message', msg => { 4 | if (!msg.data || !msg.data.installLivePreview) return; 5 | 6 | injectCSSFile('live-preview.css'); 7 | injectJSFile('live-preview.js'); 8 | }); 9 | 10 | function messageIfFound(window) { 11 | 'use strict'; 12 | 13 | try { 14 | // Determine if the page is truly a GitHub page. Use their 15 | // feature detection to detect them :) 16 | require('github/feature-detection'); 17 | window.postMessage({ installLivePreview: true }, '*'); 18 | } catch(err) {} 19 | } 20 | 21 | function runFnInPage(fn) { 22 | const script = document.createElement('script'); 23 | script.textContent = `;(${fn.toString()}(window))`; 24 | document.documentElement.appendChild(script); 25 | script.parentNode.removeChild(script); 26 | } 27 | 28 | function injectCSSFile(path) { 29 | const link = document.createElement('link'); 30 | link.type = 'text/css'; 31 | link.rel = 'stylesheet'; 32 | link.href = chrome.extension.getURL(path); 33 | document.body.appendChild(link); 34 | } 35 | 36 | function injectJSFile(path) { 37 | const inspectorScript = document.createElement('script'); 38 | inspectorScript.src = chrome.extension.getURL(path); 39 | document.documentElement.appendChild(inspectorScript); 40 | } 41 | 42 | runFnInPage(messageIfFound); 43 | -------------------------------------------------------------------------------- /shells/chrome/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Octo Preview", 3 | "version": "1.0.2", 4 | "description": "Live preview for markdown comments in PRs/Issues on GitHub", 5 | "manifest_version": 2, 6 | "content_scripts": [{ 7 | "matches": ["*://*/*"], 8 | "js": ["inject.js"], 9 | "run_at": "document_idle" 10 | }], 11 | "web_accessible_resources": [ 12 | "live-preview.js", 13 | "live-preview.css" 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /src/js/api.js: -------------------------------------------------------------------------------- 1 | const defaultText = 'Nothing to preview'; 2 | 3 | export function getMarkdownPreview(previewURI, text, authenticityToken) { 4 | if (!text) return Promise.resolve(defaultText); 5 | 6 | // Upgrade jQuery deferred to an ES6 Promise 7 | return Promise.resolve($.ajax({ 8 | method: 'POST', 9 | url: previewURI, 10 | data: { 11 | authenticity_token: authenticityToken, 12 | text 13 | } 14 | })); 15 | } 16 | -------------------------------------------------------------------------------- /src/js/dom.js: -------------------------------------------------------------------------------- 1 | const qs = document.querySelector.bind(document); 2 | 3 | export function getAuthenticityToken() { 4 | return qs('input[name="authenticity_token"]').value; 5 | } 6 | 7 | export function getComment(form) { 8 | return form.querySelector('.js-comment-field').value; 9 | } 10 | 11 | export function getPreviewUri() { 12 | return qs('.js-previewable-comment-form') 13 | .getAttribute('data-preview-url'); 14 | } 15 | 16 | export function throttleWhenTyping(cb, timeout) { 17 | let timer; 18 | 19 | function reset() { 20 | clearTimeout(timer); 21 | timer = null; 22 | } 23 | 24 | function success(form) { 25 | reset(); 26 | cb(form); 27 | } 28 | 29 | $(document).on('change keydown', '.js-comment-field', function() { 30 | if (timer) return; 31 | 32 | const form = $(this).closest('form').get(0); 33 | timer = setTimeout(() => success(form), timeout); 34 | }); 35 | } 36 | -------------------------------------------------------------------------------- /src/js/live-preview.js: -------------------------------------------------------------------------------- 1 | import { 2 | getPreviewUri, 3 | getAuthenticityToken, 4 | getComment, 5 | throttleWhenTyping 6 | } from './dom'; 7 | import {getMarkdownPreview} from './api'; 8 | 9 | 10 | // HACK: GitHub removed their jQuery global. 11 | // Short term fix: restore the global 12 | // Better fix (when time permits) just use direct DOM APIs 13 | window.$ = window.require('jquery'); 14 | 15 | throttleWhenTyping(form => { 16 | getMarkdownPreview( 17 | getPreviewUri(), 18 | getComment(form), 19 | getAuthenticityToken() 20 | ).then(markup => { 21 | form.querySelector('.js-preview-body').innerHTML = markup; 22 | }).catch(err => console.error(err.stack)); 23 | }, 800); 24 | -------------------------------------------------------------------------------- /src/styles/live-preview.css: -------------------------------------------------------------------------------- 1 | .preview-content { 2 | display: block !important; 3 | } 4 | 5 | .js-preview-tab { 6 | display: none !important; 7 | } 8 | 9 | .js-preview-body:before { 10 | content: "Preview"; 11 | font-weight: 600; 12 | color: #555; 13 | } 14 | --------------------------------------------------------------------------------