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)