Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
  • 5 commits
  • 16 files changed
  • 0 comments
  • 1 contributor
11 README.markdown
Source Rendered
... ... @@ -1,11 +1,12 @@
1 1 # Python in Chrome
2 2
3   -This demos shows how you can run Python as a Native Client module inside Google Chrome.
  3 +This is a step-by-step guide to building a Chrome Web App using [Native Client](https://developers.google.com/native-client/) (NaCl) to compile the Python interpreter.
4 4
5   -<img src="http://willmoffat.github.com/SaltySnake/pages/images/screenshot.png" alt="screenshot" />
  5 +*To install the example app please visit the [project homepage](http://willmoffat.github.com/SaltySnake/).*
6 6
7   -## Installing the Python Editor as a Chrome Web App
  7 +## Building a Chrome Python app
8 8
9   -See `apps/editor`
  9 +* Compile Python using the NaCl toolchain. _Optional_. See `py_patch/`.
  10 +* Write a C++ NaCl module that provides an interface to the embedded Python. _Optional_. See `jspy/'.
  11 +* Write a JavaScript Chrome App or Extension using the NaCl module. See `apps/editor`.
10 12
11   -TODO: link to sample apps on Chrome Web Store.
4 apps/editor/html/editor.html
@@ -11,10 +11,6 @@
11 11 <textarea id="output" readonly></textarea>
12 12 <div id="tip">Press Ctrl-Enter to execute the python code.</div>
13 13
14   - <embed id="jspy" width="0" height="0" src="/jspy/jspy.nmf" type="application/x-nacl" />
15   -
16   -
17   -
18 14 <script src="ace/ace-uncompressed.js"></script>
19 15 <script src="ace/theme-eclipse-uncompressed.js"></script>
20 16 <script src="ace/mode-python-uncompressed.js"></script>
6 apps/editor/html/editor.js
@@ -25,7 +25,11 @@ var jspy = (function() {
25 25
26 26 function init() {
27 27 if (DEBUG) console.log('JSPY.init');
28   - module = document.getElementById('jspy');
  28 + module = document.createElement('embed');
  29 + module.src = '/jspy/jspy.nmf';
  30 + module.type = 'application/x-nacl';
  31 + document.body.appendChild(module);
  32 +
29 33 module.addEventListener('load', function() {
30 34 if (DEBUG) console.log('JSPY.load');
31 35 // TODO: grey out Run until module is ready.
1  apps/udacity_ext/Makefile
... ... @@ -0,0 +1 @@
  1 +include ../common_makefile
24 apps/udacity_ext/README.markdown
Source Rendered
... ... @@ -0,0 +1,24 @@
  1 +TODO
  2 +
  3 +* support timeout
  4 +* syntax checking
  5 +
  6 +* try autorun
  7 +
  8 +* package as ext and put on chrome web store
  9 +* add py edit (to post on hacker news?)
  10 +* screencast
  11 +
  12 +Maybe
  13 +
  14 +* Putscroll bars around editor. (rather than right of page and bottom of code).
  15 +* BUG: grab new codemirror if editor changes
  16 +* ?run on contract if dirty? (to save)
  17 +* date + time to run
  18 +
  19 +DONE
  20 +* shift-enter
  21 +* remove autorun
  22 +* empty response -> no output
  23 +* line numbers for error - reset before run
  24 +* better layout
54 apps/udacity_ext/background.js
... ... @@ -0,0 +1,54 @@
  1 +//HACK: some way to reload module?
  2 +
  3 +var jspy;
  4 +
  5 +var DEBUG = false;
  6 +
  7 +var waiting_callback;
  8 +
  9 +function initModule(callback) {
  10 + if (DEBUG) console.log('JsPy init');
  11 + if (jspy) {
  12 + jspy.parentNode.removeChild(jspy);
  13 + }
  14 + jspy = document.createElement('embed');
  15 + jspy.src = '/jspy/jspy.nmf';
  16 + jspy.type = 'application/x-nacl';
  17 + document.body.appendChild(jspy);
  18 + waiting_callback = function() { console.error('Should never be called'); };
  19 + jspy.addEventListener('load', function() { initModuleMessageHandler(callback); });
  20 +}
  21 +
  22 +function initModuleMessageHandler(initCallback) {
  23 + if (DEBUG) console.log('JsPy Module loaded');
  24 + waiting_callback = null;
  25 + jspy.addEventListener('message', messageHandler);
  26 + initCallback();
  27 +}
  28 +
  29 +function messageHandler(e) {
  30 + if (DEBUG) console.log('From JsPy:', e.data);
  31 + if (!waiting_callback) {
  32 + console.error('No waiting callback');
  33 + } else {
  34 + waiting_callback(e.data);
  35 + waiting_callback = null;
  36 + }
  37 +}
  38 +
  39 +function handleContentScriptMessage(msg, sender, sendResponse) {
  40 + if (DEBUG) console.log('To JsPy:', msg);
  41 + if (msg === 'init') {
  42 + initModule(sendResponse);
  43 + return;
  44 + }
  45 + if (waiting_callback) {
  46 + sendResponse('stderr:JSPY error: outstanding callback');
  47 + return;
  48 + }
  49 + waiting_callback = sendResponse;
  50 + jspy.postMessage(msg);
  51 +}
  52 +
  53 +
  54 +chrome.extension.onRequest.addListener(handleContentScriptMessage);
BIN  apps/udacity_ext/images/arrow_contract.png
BIN  apps/udacity_ext/images/arrow_expand.png
BIN  apps/udacity_ext/images/icon_128.png
BIN  apps/udacity_ext/images/icon_16.png
17 apps/udacity_ext/manifest.json
... ... @@ -0,0 +1,17 @@
  1 +{
  2 + "name": "Udacity CS101 Helper",
  3 + "description": "Expand the Udacity code editor to full screen and run Python super fast in Chrome.",
  4 + "version": "0.1",
  5 + "background": {
  6 + "scripts": ["background.js"]
  7 + },
  8 + "content_scripts": [{
  9 + "matches": ["http://www.udacity.com/*"],
  10 + "run_at": "document_idle",
  11 + "js": ["ss_content_script.js"]
  12 + }],
  13 + "icons": {
  14 + "16": "images/icon_16.png",
  15 + "128": "images/icon_128.png"
  16 + }
  17 +}
46 apps/udacity_ext/page.css
... ... @@ -0,0 +1,46 @@
  1 +/* Page - ContentScript communication element. */
  2 +#ssDataNode { display:none; }
  3 +
  4 +/* In expanded mode only the editor is visible. */
  5 +body.ssExpanded>* { display:none; }
  6 +body.ssExpanded>.CodeMirror { display:block; }
  7 +
  8 +/* Better editor style for expanded mode. */
  9 +body.ssExpanded .CodeMirror {
  10 + height: 100%;
  11 + border: none;
  12 +}
  13 +body.ssExpanded .CodeMirror-scroll {
  14 + height: auto;
  15 + overflow-y: hidden;
  16 + overflow-x: auto;
  17 + width: 100%;
  18 + border-right: solid 1px #bbb;
  19 +}
  20 +
  21 +.ssErrorLine { color: red; font-weight:bold; }
  22 +
  23 +/* Show in expanded mode only. */
  24 +.ssExpandedOnly { display: none; }
  25 +body.ssExpanded .ssExpandedOnly { display:block; }
  26 +
  27 +/* Show in contracted mode only. */
  28 +body.ssExpanded .ssContractedOnly { display: none; }
  29 +
  30 +#ssButtonExpand {position:absolute; top:0; right:16px; }
  31 +#ssButtonContract {position:fixed; top:0; right:16px; }
  32 +
  33 +#ssColRight {
  34 + position: fixed;
  35 + top: 0; right: 0;
  36 + width: 42%; height: 100%;
  37 +}
  38 +
  39 +.ssFloating { z-index:30000; cursor:pointer; opacity:0.8; background-color:white; }
  40 +
  41 +#ssButtonRun { cursor:pointer; position:fixed; top:11px; right:65px; }
  42 +
  43 +#ssOutput { width:100%; height:100%; overflow:auto; padding-top:3.3em; border:none; }
  44 +
  45 +.ssError { color:red; }
  46 +
11 apps/udacity_ext/page.html
... ... @@ -0,0 +1,11 @@
  1 +<!-- TODO: credit http://findicons.com/pack/1688/web_blog for images -->
  2 +<img id="ssButtonExpand" class="ssContractedOnly ssFloating" title="Enable Fullscreen (Escape)" src="BASEURL/images/arrow_expand.png"/>
  3 +
  4 +<div id="ssColRight" class="ssExpandedOnly">
  5 + <img id="ssButtonContract" class="ssFloating" title="Close Fullscreen (Escape)" src="BASEURL/images/arrow_contract.png"/>
  6 + <button id="ssButtonRun" class="orange-button" title="To execute your code press (Shift or Ctrl) + Enter">Run</button>
  7 + <textarea id="ssOutput" disabled>Run to see results</textarea>
  8 +</div>
  9 +
  10 +<!-- Hidden element used for page/contentscript communication. -->
  11 +<pre id="ssDataNode"/>
20 apps/udacity_ext/page.js
... ... @@ -0,0 +1,20 @@
  1 +var SS = {markers:[]};
  2 +
  3 +SS.getAssignment = function() { return App.current_nugget_controller.get("currentAssignment"); };
  4 +SS.getEditor = function() { return SS.getAssignment().get("codeEditor"); };
  5 +
  6 +SS.focusEditor = function() { SS.getEditor().focus(); };
  7 +
  8 +SS.run = function() {
  9 + var cm = SS.getEditor();
  10 + while (SS.markers.length) { var m = SS.markers.pop(); cm.clearMarker(m); }
  11 + var val = cm.getValue();
  12 + document.getElementById("ssDataNode").textContent = val;
  13 +};
  14 +
  15 +SS.markLine = function(lineNum) {
  16 + var cm = SS.getEditor();
  17 + cm.setCursor(lineNum, 10000);
  18 + SS.markers.push(cm.setMarker(lineNum, "", "ssErrorLine"));
  19 +};
  20 +
240 apps/udacity_ext/ss_content_script.js
... ... @@ -0,0 +1,240 @@
  1 +var DEBUG = false;
  2 +
  3 +var dom = {};
  4 +
  5 +var currentlyExpanded = false;
  6 +
  7 +var MSG_EMPTY_RESULT = 'Empty result. Did you forget to use "print"?';
  8 +
  9 +function setError(lineNum, msg) {
  10 + // Not using msg.
  11 + var script = 'SS.markLine(' + lineNum + ');';
  12 + injectScript(script);
  13 +}
  14 +
  15 +function parseError(text) {
  16 + var lines = text.split('\n');
  17 + // Throw away last newline.
  18 + lines.pop();
  19 + var msg = lines.pop();
  20 + var match = null;
  21 + while (!match && lines.length) {
  22 + var r = /\bline (\d+)\b/.exec(lines.pop());
  23 + if (r) {
  24 + var line_num = parseInt(r[1]) - 1;
  25 + setError(line_num, msg);
  26 + return;
  27 + }
  28 + }
  29 + console.error('Could not parse', text);
  30 +}
  31 +
  32 +function showOutput(result) {
  33 + var out;
  34 + var className = '';
  35 + if (result.stderr) {
  36 + className = 'ssError';
  37 + out = result.stderr;
  38 + parseError(result.stderr);
  39 + } else {
  40 + out = result.stdout || MSG_EMPTY_RESULT;
  41 + }
  42 + var el = document.getElementById('ssOutput');
  43 + el.textContent = out;
  44 + el.className = className;
  45 +}
  46 +
  47 +function elide(str) {
  48 + if (str.length<80) return str;
  49 + return str.slice(0,80) + '...';
  50 +}
  51 +
  52 +var PyRunner = (function() {
  53 + var state = {
  54 + running: false,
  55 + stopping: false
  56 + };
  57 +
  58 + var decodeResponse = function(response) {
  59 + var i = response.indexOf(':');
  60 + var header = response.slice(0,i);
  61 + var data = response.slice(i+1);
  62 + var result = {};
  63 + result[header] = data;
  64 + return result;;
  65 + };
  66 +
  67 + var send = function(cmd, data, callback) {
  68 + var msg = cmd;
  69 + if (data) {
  70 + msg += ':' + data;
  71 + }
  72 + if (DEBUG) console.log('JSPY.send:', elide(msg));
  73 + chrome.extension.sendRequest(msg, callback);
  74 + };
  75 +
  76 + var init = function(callback) {
  77 + send('init', null, callback);
  78 + };
  79 +
  80 + var run = function(pyCode, callback) {
  81 + if (state.running) {
  82 + stop(function() { run(pyCode, callback); });
  83 + }
  84 + state.running = true;
  85 + var wrapper = function(r) {
  86 + state.running = false;
  87 + callback(decodeResponse(r));
  88 + };
  89 + send('run', pyCode, wrapper);
  90 + };
  91 +
  92 + var stop = function(callback) {
  93 + if (DEBUG) console.log('JSPY: stop');
  94 + if (state.stopping) {
  95 + console.error('Already stopping!');
  96 + return;
  97 + }
  98 + state.stopping = true;
  99 + var stop_cb = function(response) {
  100 + if (DEBUG) console.log('JSPY stopped', response);
  101 + // There is a bug in the JsPy module that causes the next 'run' command to fail.
  102 + // so send a dummy program. TODO: fix this!
  103 + // Once this has (failed to) execute, JsPy is back to normal.
  104 + var dummyCode = 'True';
  105 + var stop_cb2 = function() {
  106 + state.stopping = false;
  107 + callback();
  108 + };
  109 + send('run', dummyCode, stop_cb2);
  110 + };
  111 + send('stop', null, stop_cb);
  112 + };
  113 +
  114 + return { init:init, run:run, stop:stop };
  115 +})();
  116 +
  117 +
  118 +function make(tagname, opt_parent, opt_props) {
  119 + var el = document.createElement(tagname);
  120 + for (k in opt_props) {
  121 + el[k] = opt_props[k];
  122 + }
  123 + if (opt_parent) {
  124 + opt_parent.appendChild(el);
  125 + }
  126 + return el;
  127 +}
  128 +
  129 +
  130 +function doExpandEditor() {
  131 + currentlyExpanded = true;
  132 + dom.editorOriginalParent = dom.editor.parentNode;
  133 + dom.editorNextSibling = dom.editor.nextSibling;
  134 + document.body.appendChild(dom.editor);
  135 +
  136 + injectScript('SS.focusEditor()');
  137 +
  138 + document.body.className += ' ssExpanded';
  139 +
  140 + PyRunner.init(function() { console.log('SS JsPy loaded.'); });
  141 +}
  142 +
  143 +function doContractEditor() {
  144 + currentlyExpanded = false;
  145 + dom.editorOriginalParent.insertBefore(dom.editor, dom.editorNextSibling);
  146 + document.body.className = document.body.className.replace(' ssExpanded', '');
  147 +}
  148 +
  149 +function keyHandler(e) {
  150 + // Escape toggles
  151 + if (e.which === 27) {
  152 + currentlyExpanded ? doContractEditor() : doExpandEditor();
  153 + }
  154 +
  155 + // No other keys are handled when in contracted mode.
  156 + if (!currentlyExpanded) return;
  157 +
  158 + // Cmd/Ctrl-Enter runs.
  159 + if (e.which === 13 && (e.metaKey || e.ctrlKey || e.shiftKey)) {
  160 + doRun();
  161 + e.preventDefault();
  162 + }
  163 +
  164 +}
  165 +
  166 +function doRun() {
  167 + showOutput({stdout:'Running...'});
  168 + injectScript('SS.run();' );
  169 + setTimeout(sendToApp, 1);
  170 +}
  171 +
  172 +function sendToApp() {
  173 + var pyCode = document.getElementById('ssDataNode').textContent;
  174 + PyRunner.run(pyCode, showOutput);
  175 +}
  176 +
  177 +
  178 +function loadFile(filename) {
  179 + var xhr = new XMLHttpRequest();
  180 + xhr.open('GET', chrome.extension.getURL(filename), false);
  181 + xhr.send(null);
  182 + if (xhr.status !== 200) {
  183 + console.error('Failed to load ' + filename, xhr);
  184 + }
  185 + return xhr.responseText;
  186 +}
  187 +
  188 +function modifyEditor(editor) {
  189 + dom.editor = editor;
  190 +
  191 + injectCss( loadFile('page.css' ));
  192 + injectHtml( loadFile('page.html'), editor);
  193 + injectScript(loadFile('page.js' ), true);
  194 +
  195 + document.getElementById('ssButtonRun' ).addEventListener('click', doRun, false);
  196 + document.getElementById('ssButtonExpand' ).addEventListener('click', doExpandEditor, false);
  197 + document.getElementById('ssButtonContract').addEventListener('click', doContractEditor, false);
  198 + document.addEventListener('keydown', keyHandler, false);
  199 +}
  200 +
  201 +function watchForEditor() {
  202 + var editor = document.querySelector('.CodeMirror');
  203 + if (editor && editor !== dom.editor) {
  204 + modifyEditor(editor);
  205 + }
  206 + if (DEBUG) console.log('.');
  207 + window.setTimeout(watchForEditor, 500);
  208 +}
  209 +
  210 +function init() {
  211 + if (DEBUG) console.log('SS:init');
  212 + if (DEBUG) window.onerror = null; // Kill the Udacity error supressor.
  213 + watchForEditor();
  214 +}
  215 +
  216 +init();
  217 +
  218 +function injectScript(code, keep) {
  219 + var script = make('script', document.head, {textContent:code});
  220 + if (!keep) {
  221 + setTimeout(function() { document.head.removeChild(script); }, 100);
  222 + }
  223 +}
  224 +
  225 +function injectCss(css) {
  226 + make('style', document.head, {textContent:css});
  227 +}
  228 +
  229 +function injectHtml(html, parent) {
  230 + var BASEURL = chrome.extension.getURL('');
  231 + html = html.replace(/BASEURL/g, BASEURL);
  232 +
  233 + var dummy = make('div', null, {innerHTML:html});
  234 +
  235 + var fragment = document.createDocumentFragment();
  236 + while(dummy.firstChild) {
  237 + fragment.appendChild(dummy.firstChild);
  238 + }
  239 + parent.appendChild(fragment);
  240 +}
1  jspy/README.markdown
Source Rendered
@@ -42,6 +42,7 @@ In a Chrome extensions page:
42 42 var data = 'print "Hello world!"';
43 43 jspy.postMessage(cmd + ':' + data);
44 44 </script>
  45 +```
45 46
46 47 See `apps/` for real examples.
47 48

No commit comments for this range

Something went wrong with that request. Please try again.