diff --git a/demo/kitchen-sink/doclist.js b/demo/kitchen-sink/doclist.js index b0198238..a4848d50 100644 --- a/demo/kitchen-sink/doclist.js +++ b/demo/kitchen-sink/doclist.js @@ -169,13 +169,44 @@ function loadDoc(name, callback) { }); } +function saveDoc(name, callback) { + var doc = fileCache[name] || name; + if (!doc || !doc.session) + return callback("Unknown document: " + name); + + var path = doc.path; + var parts = path.split("/"); + if (parts[0] == "docs") + path = "demo/kitchen-sink/" + path; + else if (parts[0] == "ace") + path = "lib/" + path; + + upload(path, doc.session.getValue(), callback); +} + +function upload(url, data, callback) { + url = net.qualifyURL(url); + if (!/https?:/.test(url)) + return callback(new Error("Unsupported url scheme")); + var xhr = new XMLHttpRequest(); + xhr.open("PUT", url, true); + xhr.onreadystatechange = function () { + if (xhr.readyState === 4) { + callback(!/^2../.test(xhr.status)); + } + }; + xhr.send(data); +}; + + module.exports = { fileCache: fileCache, docs: sort(prepareDocList(docs)), ownSource: prepareDocList(ownSource), hugeDocs: prepareDocList(hugeDocs), initDoc: initDoc, - loadDoc: loadDoc + loadDoc: loadDoc, + saveDoc: saveDoc, }; module.exports.all = { "Mode Examples": module.exports.docs, diff --git a/lib/ace/mode/abap_highlight_rules.js b/lib/ace/mode/abap_highlight_rules.js index 6b232c90..bab1e504 100644 --- a/lib/ace/mode/abap_highlight_rules.js +++ b/lib/ace/mode/abap_highlight_rules.js @@ -126,7 +126,7 @@ var AbapHighlightRules = function() { {token : "string", regex : "`", next : "start"}, {defaultToken : "string"} ] - } + }; }; oop.inherits(AbapHighlightRules, TextHighlightRules); diff --git a/static.js b/static.js index 4f7e7248..a711715e 100755 --- a/static.js +++ b/static.js @@ -10,37 +10,100 @@ var http = require("http") // compatibility with node 0.6 if (!fs.exists) - fs.exists = path.exists; + fs.exists = path.exists; -http.createServer(function(request, response) { +var allowSave = process.argv.indexOf("--allow-save") != -1; - var uri = url.parse(request.url).pathname - , filename = path.join(process.cwd(), uri); - - fs.exists(filename, function(exists) { - if(!exists) { - response.writeHead(404, {"Content-Type": "text/plain"}); - response.write("404 Not Found\n"); - response.end(); - return; +http.createServer(function(req, res) { + var uri = url.parse(req.url).pathname + , filename = path.join(process.cwd(), uri); + + if (req.method == "PUT") { + if (!allowSave) + return error(res, 404, "Saving not allowed pass --allow-save to enable"); + save(req, res, filename); } - if (fs.statSync(filename).isDirectory()) filename += '/index.html'; + fs.exists(filename, function(exists) { + if (!exists) + return error(res, 404, "404 Not Found\n"); - fs.readFile(filename, "binary", function(err, file) { - if(err) { - response.writeHead(500, {"Content-Type": "text/plain"}); - response.write(err + "\n"); - response.end(); - return; - } + if (fs.statSync(filename).isDirectory()) { + var files = fs.readdirSync(filename); + res.writeHead(200, {"Content-Type": "text/html"}); + + files.push(".", ".."); + var html = files.map(function(name) { + var href = uri + "/" + name; + href = href.replace(/[\/\\]+/g, "/").replace(/\/$/g, ""); + if (fs.statSync(filename + "/" + name + "/").isDirectory()) + href += "/"; + return "" + name + "
"; + }); - var contentType = mime.lookup(filename) || "text/plain"; - response.writeHead(200, {"Content-Type": contentType}); - response.write(file, "binary"); - response.end(); + res._hasBody && res.write(html.join("")); + res.end(); + return; + } + + fs.readFile(filename, "binary", function(err, file) { + if (err) { + res.writeHead(500, { "Content-Type": "text/plain" }); + res.write(err + "\n"); + res.end(); + return; + } + + var contentType = mime.lookup(filename) || "text/plain"; + res.writeHead(200, { "Content-Type": contentType }); + res.write(file, "binary"); + res.end(); + }); }); - }); }).listen(port, ip); -console.log("http://localhost:" + port); +function error(res, status, message, error) { + console.error(error || message); + res.writeHead(status, { "Content-Type": "text/plain" }); + res.write(message); + res.end(); +} + +function save(req, res, filePath) { + var data = ""; + req.on("data", function(chunk) { + data += chunk; + }); + req.on("error", function() { + error(res, 404, "Could't save file"); + }); + req.on("end", function() { + try { + fs.writeFileSync(filePath, data); + } + catch (e) { + return error(res, 404, "Could't save file", e); + } + res.statusCode = 200; + res.end("OK"); + }); +} + +function getLocalIps() { + var os = require("os"); + + var interfaces = os.networkInterfaces ? os.networkInterfaces() : {}; + var addresses = []; + for (var k in interfaces) { + for (var k2 in interfaces[k]) { + var address = interfaces[k][k2]; + if (address.family === "IPv4" && !address.internal) { + addresses.push(address.address); + } + } + } + return addresses; +} + +console.log("http://" + (ip == "0.0.0.0" ? getLocalIps()[0] : ip) + ":" + port); + diff --git a/static.py b/static.py index eb864596..7a2faf91 100755 --- a/static.py +++ b/static.py @@ -32,7 +32,9 @@ import time import string import sys from os import path, stat, getcwd +from fnmatch import fnmatch from wsgiref import util +from wsgiref.validate import validator from wsgiref.headers import Headers from wsgiref.simple_server import make_server from optparse import OptionParser @@ -84,6 +86,8 @@ class Cling(object): not_modified = StatusApp('304 Not Modified', "") moved_permanently = StatusApp('301 Moved Permanently') method_not_allowed = StatusApp('405 Method Not Allowed') + success_no_content = StatusApp('204 No Content', "") + server_error = StatusApp('500 Internal Server Error') def __init__(self, root, **kw): """Just set the root and any other attribs passes via **kw.""" @@ -93,9 +97,6 @@ class Cling(object): def __call__(self, environ, start_response): """Respond to a request when called in the usual WSGI way.""" - if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): - headers = [('Allow', 'GET, HEAD')] - return self.method_not_allowed(environ, start_response, headers) path_info = environ.get('PATH_INFO', '') full_path = self._full_path(path_info) if not self._is_under_root(full_path): @@ -109,6 +110,23 @@ class Cling(object): return self.moved_permanently(environ, start_response, headers) else: full_path = self._full_path(path_info + self.index_file) + try: + sz = int(environ['CONTENT_LENGTH']) + except: + sz = 0 + if environ['REQUEST_METHOD'] == 'PUT' and sz > 0: + for putglob in self.puttable: + if fnmatch(path_info, putglob): + data = environ['wsgi.input'].read(sz) + try: + with open(full_path, "wb") as f: f.write(data) + return self.success_no_content(environ, start_response) + except: + print sys.exc_info()[1] + return self.server_error(environ, start_response) + if environ['REQUEST_METHOD'] not in ('GET', 'HEAD'): + headers = [('Allow', 'GET, HEAD')] + return self.method_not_allowed(environ, start_response, headers) content_type = self._guess_type(full_path) try: etag, last_modified = self._conditions(full_path, environ) @@ -191,52 +209,90 @@ def cling_wrap(package_name, dir_name, **kw): def command(): - parser = OptionParser(usage="%prog DIR [HOST][:][PORT]", - version="static 0.3.6") + usage = "%prog [--help] [-d DIR] [-l [HOST][:PORT]] [-p GLOB[,GLOB...]]" + parser = OptionParser(usage=usage, version="static 0.3.6") + parser.add_option("-d", "--dir", dest="rootdir", default=".", + help="Root directory to serve. Defaults to '.' .", metavar="DIR") + parser.add_option("-l", "--listen", dest="listen", default="127.0.0.1:8888", + help="Listen on this interface (given by its hostname or IP) and port."+ + " HOST defaults to 127.0.0.1. PORT defaults to 8888. "+ + "Leave HOST empty to listen on all interfaces (INSECURE!).", + metavar="[HOST][:PORT]") + parser.add_option("-p", "--puttable", dest="puttable", default="", + help="Comma or space-separated list of request paths for which to"+ + " permit PUT requests. Each path is a glob pattern that may "+ + "contain wildcard characters '*' and/or '?'. "+ + "'*' matches any sequence of characters, including the empty"+ + " string. '?' matches exactly 1 arbitrary character. "+ + "NOTE: Both '*' and '?' match slashes and dots. "+ + "I.e. --puttable=* makes every file under DIR writable!", + metavar="GLOB[,GLOB...]") + parser.add_option("--validate", dest="validate", action="store_true", + default=False, + help="Enable HTTP validation. You don't need this unless "+ + "you're developing static.py itself.") + options, args = parser.parse_args() - if len(args) in (1, 2): - if len(args) == 2: - parts = args[1].split(":") - if len(parts) == 1: - host = parts[0] - port = None - elif len(parts) == 2: - host, port = parts - else: - sys.exit("Invalid host:port specification.") - elif len(args) == 1: - host, port = None, None - if not host: - host = '0.0.0.0' - if not port: - port = 8888 - try: - port = int(port) - except: - sys.exit("Invalid host:port specification.") - app = Cling(args[0]) - try: - make_server(host, port, app).serve_forever() - except KeyboardInterrupt, ki: - print "Cio, baby!" - except: - sys.exit("Problem initializing server.") - else: + if len(args) > 0: parser.print_help(sys.stderr) sys.exit(1) + parts = options.listen.split(":") + if len(parts) == 1: + try: # if the the listen argument consists only of a port number + port = int(parts[0]) + host = None + except: # could not parse as port number => must be a host IP or name + host = parts[0] + port = None + elif len(parts) == 2: + host, port = parts + else: + sys.exit("Invalid host:port specification.") -def test(): - from wsgiref.validate import validator - app = Cling(getcwd()) + if not host: + host = '0.0.0.0' + if not port: + port = 8888 try: - print "Serving " + getcwd() + " to http://localhost:8888" - make_server('0.0.0.0', 8888, validator(app)).serve_forever() + port = int(port) + if port <= 0 or port > 65535: raise ValueError + except: + sys.exit("Invalid host:port specification.") + + puttable = set(path.abspath(p) for p in + options.puttable.replace(","," ").split()) + if puttable and host not in ('127.0.0.1', 'localhost'): + print("Permitting PUT access for non-localhost connections may be unwise.") + + options.rootdir = path.abspath(options.rootdir) + + for p in puttable: + if not p.startswith(options.rootdir): + sys.exit("puttable path '%s' not under root '%s'" % (p, options.rootdir)) + + # cut off root prefix from puttable paths + puttable = set(p[len(options.rootdir):] for p in puttable) + + app = Cling(options.rootdir, puttable=puttable) + + if options.validate: + app = validator(app) + + try: + print "Serving %s to http://%s:%d" % (options.rootdir, host, port) + if puttable: + print("The following paths (relative to server root) may be "+ + "OVERWRITTEN via HTTP PUT.") + for p in puttable: + print p + make_server(host, port, app).serve_forever() except KeyboardInterrupt, ki: - print "" print "Ciao, baby!" + except: + sys.exit("Problem initializing server: %s" % sys.exc_info()[1]) if __name__ == '__main__': - test() + command() diff --git a/tool/mode_creator.html b/tool/mode_creator.html index e2db56af..259ba64e 100644 --- a/tool/mode_creator.html +++ b/tool/mode_creator.html @@ -19,7 +19,7 @@ border-bottom: solid 1px; } .separator-h { - padding: 0 20px; + padding: 0 10px; } #closeBtn { background: rgba(245, 146, 146, 0.5); @@ -59,6 +59,7 @@ + @@ -70,6 +71,7 @@ + diff --git a/tool/mode_creator.js b/tool/mode_creator.js index 6b1122cd..e79c54c6 100644 --- a/tool/mode_creator.js +++ b/tool/mode_creator.js @@ -53,6 +53,8 @@ util.bindDropdown("doc", function(value) { doclist.loadDoc(value, function(session) { if (session) { editor2.setSession(session); + session.getUndoManager().markClean(); + updateSaveButtonState(null, editor2); } }); }); @@ -60,6 +62,7 @@ util.bindDropdown("doc", function(value) { var modeEl = document.getElementById("modeEl"); util.fillDropdown(modeEl, modelist.modes); var modeSessions = {}; + util.bindDropdown(modeEl, function(value) { if (modeSessions[value]) { editor1.setSession(modeSessions[value]); @@ -68,14 +71,18 @@ util.bindDropdown(modeEl, function(value) { } var hp = "./lib/ace/mode/" + value + "_highlight_rules.js"; net.get(hp, function(text) { - text = util.stripLeadingComments(text); - var session = new EditSession(text); session.setUndoManager(new UndoManager()); + modeSessions[value] = session; - session.setMode("ace/mode/javascript"); + session.setMode("ace/mode/javascript", function() { + if (session.getLine(0).match(/^\s*\//)) + session.toggleFoldWidget(0); // fold licence comment + }); editor1.setSession(modeSessions[value]); + session.getUndoManager().markClean(); + updateSaveButtonState(null, editor1); schedule(); }); }); @@ -85,6 +92,50 @@ document.getElementById("syncToMode").onclick = function() { docEl.onchange(); run(); }; + +editor1.saveButton = document.getElementById("saveButton1"); +editor2.saveButton = document.getElementById("saveButton2"); +editor1.saveButton.editor = editor1; +editor2.saveButton.editor = editor2; + +editor1.saveButton.onclick = function() { + doclist.saveDoc({ + path: "./lib/ace/mode/" + modeEl.value + "_highlight_rules.js", + session: editor1.session + }, function(err) { + handleSaveResult(err, editor1); + }); +}; +editor1.commands.bindKey({ + win: "Ctrl-S", mac: "Cmd-s" +}, editor1.saveButton.onclick); +editor2.saveButton.onclick = function() { + doclist.saveDoc(docEl.value, function(err) { + handleSaveResult(err, editor2); + }); +}; +editor2.commands.bindKey({ + win: "Ctrl-S", mac: "Cmd-s" +}, editor2.saveButton.onclick); +function updateSaveButtonState(e, editor){ + editor.saveButton.disabled = editor.session.getUndoManager().isClean(); +} +editor1.on("input", updateSaveButtonState); +editor2.on("input", updateSaveButtonState); + +function handleSaveResult(err, editor) { + if (err) { + return log( + "Write access to this file is disabled.\n"+ + "To enable saving your changes to disk, clone the Ace repository\n"+ + "and run the included web server with the --allow-write option\n"+ + "`node static.js --allow-write` or `static.py --puttable=*`" + ); + } + editor.session.getUndoManager().markClean(); + updateSaveButtonState(null, editor); +} + document.getElementById("perfTest").onclick = function() { var lines = editor2.session.doc.getAllLines(); if (!lines.length)