156 changes: 156 additions & 0 deletions mode/sieve/sieve.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
/*
* See LICENSE in this directory for the license under which this code
* is released.
*/

CodeMirror.defineMode("sieve", function(config) {
function words(str) {
var obj = {}, words = str.split(" ");
for (var i = 0; i < words.length; ++i) obj[words[i]] = true;
return obj;
}

var keywords = words("if elsif else stop require");
var atoms = words("true false not");
var indentUnit = config.indentUnit;

function tokenBase(stream, state) {

var ch = stream.next();
if (ch == "/" && stream.eat("*")) {
state.tokenize = tokenCComment;
return tokenCComment(stream, state);
}

if (ch === '#') {
stream.skipToEnd();
return "comment";
}

if (ch == "\"") {
state.tokenize = tokenString(ch);
return state.tokenize(stream, state);
}

if (ch === "{")
{
state._indent++;
return null;
}

if (ch === "}")
{
state._indent--;
return null;
}

if (/[{}\(\),;]/.test(ch))
return null;

// 1*DIGIT "K" / "M" / "G"
if (/\d/.test(ch)) {
stream.eatWhile(/[\d]/);
stream.eat(/[KkMmGg]/);
return "number";
}

// ":" (ALPHA / "_") *(ALPHA / DIGIT / "_")
if (ch == ":") {
stream.eatWhile(/[a-zA-Z_]/);
stream.eatWhile(/[a-zA-Z0-9_]/);

return "operator";
}

stream.eatWhile(/[\w\$_]/);
var cur = stream.current();

// "text:" *(SP / HTAB) (hash-comment / CRLF)
// *(multiline-literal / multiline-dotstart)
// "." CRLF
if ((cur == "text") && stream.eat(":"))
{
state.tokenize = tokenMultiLineString;
return "string";
}

if (keywords.propertyIsEnumerable(cur))
return "keyword";

if (atoms.propertyIsEnumerable(cur))
return "atom";
}

function tokenMultiLineString(stream, state)
{
state._multiLineString = true;
// the first line is special it may contain a comment
if (!stream.sol()) {
stream.eatSpace();

if (stream.peek() == "#") {
stream.skipToEnd();
return "comment";
}

stream.skipToEnd();
return "string";
}

if ((stream.next() == ".") && (stream.eol()))
{
state._multiLineString = false;
state.tokenize = tokenBase;
}

return "string";
}

function tokenCComment(stream, state) {
var maybeEnd = false, ch;
while ((ch = stream.next()) != null) {
if (maybeEnd && ch == "/") {
state.tokenize = tokenBase;
break;
}
maybeEnd = (ch == "*");
}
return "comment";
}

function tokenString(quote) {
return function(stream, state) {
var escaped = false, ch;
while ((ch = stream.next()) != null) {
if (ch == quote && !escaped)
break;
escaped = !escaped && ch == "\\";
}
if (!escaped) state.tokenize = tokenBase;
return "string";
};
}

return {
startState: function(base) {
return {tokenize: tokenBase,
baseIndent: base || 0,
_indent: 0};
},

token: function(stream, state) {
if (stream.eatSpace())
return null;

return (state.tokenize || tokenBase)(stream, state);;
},

indent: function(state, textAfter) {
return state.baseIndent + state._indent * indentUnit;
},

electricChars: "}"
};
});

CodeMirror.defineMIME("application/sieve", "sieve");
1 change: 1 addition & 0 deletions mode/smalltalk/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: Smalltalk mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
4 changes: 2 additions & 2 deletions mode/smalltalk/smalltalk.js
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ CodeMirror.defineMode('smalltalk', function(config, modeConfig) {

} else if (/\d/.test(aChar)) {
stream.eatWhile(/[\w\d]/);
token.name = 'number'
token.name = 'number';

} else if (/[\w_]/.test(aChar)) {
stream.eatWhile(/[\w\d_]/);
Expand Down Expand Up @@ -100,7 +100,7 @@ CodeMirror.defineMode('smalltalk', function(config, modeConfig) {
}

return token;
}
};

return {
startState: function() {
Expand Down
1 change: 1 addition & 0 deletions mode/smarty/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: Smarty mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
4 changes: 2 additions & 2 deletions mode/smarty/smarty.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CodeMirror.defineMode("smarty", function(config, parserConfig) {
operatorChars: /[+\-*&%=<>!?]/,
validIdentifier: /[a-zA-Z0-9\_]/,
stringChar: /[\'\"]/
}
};
var leftDelim = (typeof config.mode.leftDelimiter != 'undefined') ? config.mode.leftDelimiter : "{";
var rightDelim = (typeof config.mode.rightDelimiter != 'undefined') ? config.mode.rightDelimiter : "}";
function ret(style, lst) { last = lst; return style; }
Expand Down Expand Up @@ -142,7 +142,7 @@ CodeMirror.defineMode("smarty", function(config, parserConfig) {
return style;
},
electricChars: ""
}
};
});

CodeMirror.defineMIME("text/x-smarty", "smarty");
1 change: 1 addition & 0 deletions mode/sparql/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: SPARQL mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
1 change: 1 addition & 0 deletions mode/stex/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: sTeX mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion mode/stex/stex.js
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ CodeMirror.defineMode("stex", function(cmCfg, modeCfg)
};
this.closeBracket = function(content) {
};
}
};
}

var plugins = new Array();
Expand Down
1 change: 1 addition & 0 deletions mode/stex/test.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: sTeX mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
1 change: 1 addition & 0 deletions mode/tiddlywiki/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: TiddlyWiki mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
1 change: 1 addition & 0 deletions mode/tiki/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<meta charset="utf-8">
<title>CodeMirror: Tiki wiki mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
8 changes: 4 additions & 4 deletions mode/tiki/tiki.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ CodeMirror.defineMode('tiki', function(config, parserConfig) {
function inLine(style, terminator) {
return function(stream, state) {
while(!stream.eol()) {
stream.next()
stream.next();
}
state.tokenize = inText;
return style;
Expand All @@ -37,9 +37,9 @@ CodeMirror.defineMode('tiki', function(config, parserConfig) {
//non start of line
switch (ch) { //switch is generally much faster than if, so it is used here
case "{": //plugin
type = stream.eat("/") ? "closeTag" : "openTag";
var type = stream.eat("/") ? "closeTag" : "openTag";
stream.eatSpace();
tagName = "";
var tagName = "";
var c;
while ((c = stream.eat(/[^\s\u00a0=\"\'\/?(}]/))) tagName += c;
state.tokenize = inPlugin;
Expand Down Expand Up @@ -251,7 +251,7 @@ CodeMirror.defineMode('tiki', function(config, parserConfig) {
if (err) setStyle = "error";
if (type == "endPlugin") return cont();
return pass();
}
};
}

function attributes(type) {
Expand Down
1 change: 1 addition & 0 deletions mode/vb/index.html
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: VB.NET mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
1 change: 1 addition & 0 deletions mode/vbscript/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: VBScript mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
1 change: 1 addition & 0 deletions mode/velocity/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: Velocity mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion mode/velocity/velocity.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ CodeMirror.defineMode("velocity", function(config) {
return obj;
}

var indentUnit = config.indentUnit
var indentUnit = config.indentUnit;
var keywords = parseWords("#end #else #break #stop #[[ #]] " +
"#{end} #{else} #{break} #{stop}");
var functions = parseWords("#if #elseif #foreach #set #include #parse #macro #define #evaluate " +
Expand Down
1 change: 1 addition & 0 deletions mode/verilog/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: Verilog mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
2 changes: 1 addition & 1 deletion mode/verilog/verilog.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ CodeMirror.defineMode("verilog", function(config, parserConfig) {
}
if (/[\[\]{}\(\),;\:\.]/.test(ch)) {
curPunc = ch;
return null
return null;
}
if (/[\d']/.test(ch)) {
stream.eatWhile(/[\w\.']/);
Expand Down
1 change: 1 addition & 0 deletions mode/xml/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: XML mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
4 changes: 2 additions & 2 deletions mode/xml/xml.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
},
doNotIndent: {"pre": true},
allowUnquoted: true,
allowMissing: false
allowMissing: true
} : {
autoSelfClosers: {},
implicitlyClosed: {},
Expand Down Expand Up @@ -229,7 +229,7 @@ CodeMirror.defineMode("xml", function(config, parserConfig) {
if (type == "endTag") { popContext(); return cont(); }
setStyle = "error";
return cont(arguments.callee);
}
};
}
function maybePopContext(nextTagName) {
var parentTagName;
Expand Down
5 changes: 3 additions & 2 deletions mode/xquery/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
*/
-->
<head>
<title>CodeMirror 2: JavaScript mode</title>
<meta charset="utf-8">
<title>CodeMirror: XQuery mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="http://codemirror.net/lib/codemirror.js"></script>
<script src="xquery.js"></script>
Expand All @@ -41,7 +42,7 @@
</style>
</head>
<body>
<h1>CodeMirror 2: XQuery mode</h1>
<h1>CodeMirror: XQuery mode</h1>

<div class="cm-s-default">
<textarea id="code" name="code">
Expand Down
11 changes: 7 additions & 4 deletions mode/xquery/xquery.js
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ CodeMirror.defineMode("xquery", function(config, parserConfig) {
'preceding-sibling','processing-instruction','ref','return','returns','satisfies','schema','schema-element',
'self','some','sortby','stable','text','then','to','treat','typeswitch','union','variable','version','where',
'xquery', 'empty-sequence'];
for(var i=0, l=basic.length; i < l; i++) { kwObj[basic[i]] = kw(basic[i])};
for(var i=0, l=basic.length; i < l; i++) { kwObj[basic[i]] = kw(basic[i]);};

// a list of types. For each add a property to kwObj with the value of
// {type: "atom", style: "atom"}
Expand Down Expand Up @@ -191,7 +191,7 @@ CodeMirror.defineMode("xquery", function(config, parserConfig) {
if(!known) stream.eatWhile(/[\w\$_-]/);

// gobble a colon in the case that is a lib func type call fn:doc
var foundColon = stream.eat(":")
var foundColon = stream.eat(":");

// if there's not a second colon, gobble another word. Otherwise, it's probably an axis specifier
// which should get matched as a keyword
Expand Down Expand Up @@ -325,7 +325,7 @@ CodeMirror.defineMode("xquery", function(config, parserConfig) {
state.tokenize = tokenBase;
}
return ret("tag", "tag");
}
};
}

// tokenizer for XML attributes
Expand Down Expand Up @@ -365,6 +365,7 @@ CodeMirror.defineMode("xquery", function(config, parserConfig) {

// handle comments, including nested
function tokenXMLComment(stream, state) {
var ch;
while (ch = stream.next()) {
if (ch == "-" && stream.match("->", true)) {
state.tokenize = tokenBase;
Expand All @@ -376,6 +377,7 @@ CodeMirror.defineMode("xquery", function(config, parserConfig) {

// handle CDATA
function tokenCDATA(stream, state) {
var ch;
while (ch = stream.next()) {
if (ch == "]" && stream.match("]", true)) {
state.tokenize = tokenBase;
Expand All @@ -386,6 +388,7 @@ CodeMirror.defineMode("xquery", function(config, parserConfig) {

// handle preprocessing instructions
function tokenPreProcessing(stream, state) {
var ch;
while (ch = stream.next()) {
if (ch == "?" && stream.match(">", true)) {
state.tokenize = tokenBase;
Expand Down Expand Up @@ -422,7 +425,7 @@ CodeMirror.defineMode("xquery", function(config, parserConfig) {

function popStateStack(state) {
var popped = state.stack.pop();
var reinstateTokenize = state.stack.length && state.stack[state.stack.length-1].tokenize
var reinstateTokenize = state.stack.length && state.stack[state.stack.length-1].tokenize;
state.tokenize = reinstateTokenize || tokenBase;
}

Expand Down
1 change: 1 addition & 0 deletions mode/yaml/index.html
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<!doctype html>
<html>
<head>
<meta charset="utf-8">
<title>CodeMirror: YAML mode</title>
<link rel="stylesheet" href="../../lib/codemirror.css">
<script src="../../lib/codemirror.js"></script>
Expand Down
36 changes: 14 additions & 22 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,29 +1,21 @@
{
"name": "CodeMirror",
"version":"2.32.0",
"version":"2.33.0",
"main": "codemirror.js",
"description": "In-browser code editing made bearable",
"licenses": [
{
"type": "MIT",
"url": "http://codemirror.net/LICENSE"
}
],
"directories": {
"lib": "./lib"
},
"bugs": "http://github.com/marijnh/CodeMirror2/issues",
"licenses": [{"type": "MIT",
"url": "http://codemirror.net/LICENSE"}],
"directories": {"lib": "./lib"},
"scripts": {"test": "node ./test/run.js"},
"devDependencies": {"node-static": "0.6.0"},
"bugs": "http://github.com/marijnh/CodeMirror/issues",
"keywords": ["JavaScript", "CodeMirror", "Editor"],
"homepage": "http://codemirror.net",
"maintainers":[ {
"name": "Marijn Haverbeke",
"email": "marijnh@gmail.com",
"web": "http://codemirror.net"
}],
"repositories": [
{
"type": "git",
"url": "https://github.com/marijnh/CodeMirror2.git"
}
]
"maintainers":[{"name": "Marijn Haverbeke",
"email": "marijnh@gmail.com",
"web": "http://marijnhaverbeke.nl"}],
"repositories": [{"type": "git",
"url": "http://marijnhaverbeke.nl/git/codemirror"},
{"type": "git",
"url": "https://github.com/marijnh/CodeMirror.git"}]
}
47 changes: 29 additions & 18 deletions test/driver.js
Original file line number Diff line number Diff line change
@@ -1,42 +1,53 @@
var tests = [], runOnly = null;
var tests = [], debug = null;

function Failure(why) {this.message = why;}

function test(name, run) {tests.push({name: name, func: run}); return name;}
function testCM(name, run, opts) {
function test(name, run, expectedFail) {
tests.push({name: name, func: run, expectedFail: expectedFail});
return name;
}
function testCM(name, run, opts, expectedFail) {
return test(name, function() {
var place = document.getElementById("testground"), cm = CodeMirror(place, opts);
if (debug) place.style.visibility = "";
try {run(cm);}
finally {place.removeChild(cm.getWrapperElement());}
});
finally {if (!debug) place.removeChild(cm.getWrapperElement());}
}, expectedFail);
}

function runTests(callback) {
function step(i) {
if (i == tests.length) return callback("done");
var test = tests[i];
if (runOnly != null && runOnly != test.name) return step(i + 1);
try {test.func(); callback("ok", test.name);}
catch(e) {
if (e instanceof Failure)
callback("fail", test.name, e.message);
else
callback("error", test.name, e.toString());
var test = tests[i], expFail = test.expectedFail;
if (debug != null && debug != test.name) return step(i + 1);
try {
test.func();
if (expFail) callback("fail", test.name, "was expected to fail, but succeeded");
else callback("ok", test.name);
} catch(e) {
if (expFail) callback("expected", test.name);
else if (e instanceof Failure) callback("fail", test.name, e.message);
else callback("error", test.name, e.toString());
}
setTimeout(function(){step(i + 1);}, 20);
}
step(0);
}

function label(str, msg) {
if (msg) return str + " (" + msg + ")";
return str;
}
function eq(a, b, msg) {
if (a != b) throw new Failure(a + " != " + b + (msg ? " (" + msg + ")" : ""));
if (a != b) throw new Failure(label(a + " != " + b, msg));
}
function eqPos(a, b, msg) {
function str(p) { return "{line:" + p.line + ",ch:" + p.ch + "}"; }
if (a == b) return;
if (a == null || b == null) throw new Failure("comparing point to null");
eq(a.line, b.line, msg);
eq(a.ch, b.ch, msg);
if (a == null) throw new Failure(label("comparing null to " + str(b)));
if (b == null) throw new Failure(label("comparing " + str(a) + " to null"));
if (a.line != b.line || a.ch != b.ch) throw new Failure(label(str(a) + " != " + str(b), msg));
}
function is(a, msg) {
if (!a) throw new Failure("assertion failed" + (msg ? " (" + msg + ")" : ""));
if (!a) throw new Failure(label("assertion failed", msg));
}
11 changes: 9 additions & 2 deletions test/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
<link rel="stylesheet" href="../lib/codemirror.css">
<script src="../lib/codemirror.js"></script>
<script src="../mode/javascript/javascript.js"></script>
<script src="../mode/xml/xml.js"></script>

<style type="text/css">
.ok {color: #090;}
Expand Down Expand Up @@ -33,17 +34,23 @@ <h1>CodeMirror: Test Suite</h1>
runTests(displayTest);
};

function esc(str) {
return str.replace(/[<&]/, function(ch) { return ch == "<" ? "&lt;" : "&amp;"; });
}

var output = document.getElementById("output"), progress = document.getElementById("progress");
var count = 0, failed = 0, bad = "";
function displayTest(type, name, msg) {
if (type != "done") ++count;
progress.style.width = (count * (progress.parentNode.clientWidth - 8) / tests.length) + "px";
progress.innerHTML = "Ran " + count + (count < tests.length ? " of " + tests.length : "") + " tests";
if (type == "ok") {
output.innerHTML = bad + "<span class=ok>Test '" + CodeMirror.htmlEscape(name) + "' succeeded</span>";
output.innerHTML = bad + "<span class=ok>Test '" + esc(name) + "' succeeded</span>";
} else if (type == "expected") {
output.innerHTML = bad + "<span class=ok>Test '" + esc(name) + "' failed as expected</span>";
} else if (type == "error" || type == "fail") {
++failed;
bad += CodeMirror.htmlEscape(name) + ": <span class=" + type + ">" + CodeMirror.htmlEscape(msg) + "</span>\n";
bad += esc(name) + ": <span class=" + type + ">" + esc(msg) + "</span><br>";
output.innerHTML = bad;
} else if (type == "done") {
output.innerHTML = bad + (failed ? "<span class=fail>" + failed + " failure" + (failed > 1 ? "s" : "") + "</span>"
Expand Down
120 changes: 120 additions & 0 deletions test/lint/lint.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
/*
Simple linter, based on UglifyJS's [1] parse-js module
All of the existing linters either cramp my style or have huge
dependencies (Closure). So here's a very simple, non-invasive one
that only spots
- missing semicolons and trailing commas
- variables or properties that are reserved words
- assigning to a variable you didn't declare
[1]: https://github.com/mishoo/UglifyJS/
*/

var fs = require("fs"), parse_js = require("./parse-js").parse;

var reserved = {};
"break case catch continue debugger default delete do else false finally for function if in\
instanceof new null return switch throw true try typeof var void while with abstract enum\
int short boolean export interface static byte extends long super char final native\
synchonized class float package throws const goto private transient implements protected\
volatile double import public const".split(" ").forEach(function(word) { reserved[word] = true; });

function checkVariable(scope, name, pos) {
while (scope) {
if (scope.cur.hasOwnProperty(name)) return;
scope = scope.prev;
}
fail("Accidental global: " + name, pos);
}
function checkProperty(name, pos) {
if (reserved.hasOwnProperty(name)) {
fail("Using a keyword or reserved word as a property: " + name, pos);
}
}

function walk(ast, scope) {
var tp = ast[0];
if (typeof tp != "string") tp = tp.name;
function sub(ast) { if (ast) walk(ast, scope); }
function subn(array) { if (array) array.forEach(sub); }
if (tp == "block" || tp == "splice" || tp == "toplevel" || tp == "array") {
subn(ast[1]);
} else if (tp == "var" || tp == "const") {
ast[1].forEach(function(def) { scope.cur[def[0]] = true; if (def[1]) sub(def[1]); });
} else if (tp == "try") {
subn(ast[1]);
if (ast[2]) { scope.cur[ast[2][0]] = true; subn(ast[2][1]); }
subn(ast[3]);
} else if (tp == "throw" || tp == "return" || tp == "dot" || tp == "stat") {
sub(ast[1]);
} else if (tp == "dot") {
sub(ast[1]);
checkProperty(ast[2], ast[0]);
} else if (tp == "new" || tp == "call") {
sub(ast[1]); subn(ast[2]);
} else if (tp == "switch") {
sub(ast[1]);
ast[2].forEach(function(part) { sub(part[0]); subn(part[1]); });
} else if (tp == "conditional" || tp == "if" || tp == "for" || tp == "for-in") {
sub(ast[1]); sub(ast[2]); sub(ast[3]); sub(ast[4]);
} else if (tp == "assign") {
if (ast[2][0].name == "name") checkVariable(scope, ast[2][1], ast[2][0]);
sub(ast[2]); sub(ast[3]);
} else if (tp == "function" || tp == "defun") {
if (tp == "defun") scope.cur[ast[1]] = true;
var nscope = {prev: scope, cur: {}};
ast[2].forEach(function(arg) { nscope.cur[arg] = true; });
ast[3].forEach(function(ast) { walk(ast, nscope); });
} else if (tp == "while" || tp == "do" || tp == "sub" || tp == "with") {
sub(ast[1]); sub(ast[2]);
} else if (tp == "binary" || tp == "unary-prefix" || tp == "unary-postfix" || tp == "label") {
if (/\+\+|--/.test(ast[1]) && ast[2][0].name == "name") checkVariable(scope, ast[2][1], ast[2][0]);
sub(ast[2]); sub(ast[3]);
} else if (tp == "object") {
ast[1].forEach(function(prop) {
if (prop.type != "string") checkProperty(prop[0], ast[0]);
sub(prop[1]); sub(prop[2]);
});
} else if (tp == "seq") {
subn(ast.slice(1));
} else if (tp == "name") {
if (reserved.hasOwnProperty(ast[1]) && !/^(?:null|true|false)$/.test(ast[1]))
fail("Using reserved word as variable name: " + ast[1], ast[0]);
}
}

var failed = false, curFile;
function fail(msg, pos) {
if (typeof pos == "object") pos = pos.start.line + 1;
console.log(curFile + ": " + msg + (typeof pos == "number" ? " (" + pos + ")" : ""));
failed = true;
}

function checkFile(fileName) {
curFile = fileName.match(/[^\/+]*\.js$/)[0];
var file = fs.readFileSync(fileName, "utf8");
var badChar = file.match(/[\x00-\x08\x0b\x0c\x0e-\x19\uFEFF]/);
if (badChar) fail("Undesirable character " + badChar[0].charCodeAt(0) + " at position " + badChar.index);
if (/^#!/.test(file)) file = file.slice(file.indexOf("\n") + 1);
try {
var parsed = parse_js(file, true, true);
} catch(e) {
fail(e.message, e.line);
return;
}
walk(parsed, {prev: null, cur: {}});
}

function checkDir(dir) {
fs.readdirSync(dir).forEach(function(file) {
var fname = dir + "/" + file;
if (/\.js$/.test(file)) checkFile(fname);
else if (fs.lstatSync(fname).isDirectory()) checkDir(fname);
});
}

exports.checkDir = checkDir;
exports.checkFile = checkFile;
exports.success = function() { return !failed; };
1,372 changes: 1,372 additions & 0 deletions test/lint/parse-js.js

Large diffs are not rendered by default.

30 changes: 30 additions & 0 deletions test/phantom_driver.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
var page = require('webpage').create();

page.open("http://localhost:3000/test/index.html", function (status) {
if (status != "success") {
console.log("page couldn't be loaded successfully");
phantom.exit(1);
}
waitFor(function () {
return page.evaluate(function () {
var output = document.getElementById('output');
if (!output) { return false; }
return (/(\d+ failures?|all passed)$/i).test(output.innerText);
});
}, function () {
var failed = page.evaluate(function () { return window.failed; });
var output = page.evaluate(function () {
return document.getElementById('output').innerText;
});
console.log(output);
phantom.exit(failed > 0 ? 1 : 0);
});
});

function waitFor (test, cb) {
if (test()) {
cb();
} else {
setTimeout(function () { waitFor(test, cb); }, 250);
}
}
32 changes: 32 additions & 0 deletions test/run.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#!/usr/bin/env node

var lint = require("./lint/lint");

lint.checkDir("mode");
lint.checkDir("lib");

var ok = lint.success();

var files = new (require('node-static').Server)('.');

var server = require('http').createServer(function (req, res) {
req.addListener('end', function () {
files.serve(req, res);
});
}).addListener('error', function (err) {
throw err;
}).listen(3000, function () {
var child_process = require('child_process');
child_process.exec("which phantomjs", function (err) {
if (err) {
console.error("PhantomJS is not installed. Download from http://phantomjs.org");
process.exit(1);
}
var cmd = 'phantomjs test/phantom_driver.js';
child_process.exec(cmd, function (err, stdout) {
server.close();
console.log(stdout);
process.exit(err || !ok ? 1 : 0);
});
});
});
191 changes: 189 additions & 2 deletions test/test.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ function byClassName(elt, cls) {
return found;
}

var ie_lt8 = /MSIE [1-7]\b/.test(navigator.userAgent);

test("fromTextArea", function() {
var te = document.getElementById("code");
te.value = "CONTENT";
Expand Down Expand Up @@ -100,6 +102,10 @@ testCM("indent", function(cm) {
cm.setOption("indentUnit", 8);
cm.indentLine(1);
eq(cm.getLine(1), "\tblah();");
cm.setOption("indentUnit", 10);
cm.setOption("tabSize", 4);
cm.indentLine(1);
eq(cm.getLine(1), "\t\t blah();");
}, {value: "if (x) {\nblah();\n}", indentUnit: 3, indentWithTabs: true, tabSize: 8});

test("defaults", function() {
Expand Down Expand Up @@ -322,7 +328,7 @@ testCM("scrollSnap", function(cm) {
cm.setCursor({line: 100, ch: 180});
cm.setCursor({line: 199, ch: 0});
info = cm.getScrollInfo();
is(info.x == 0 && info.y == info.height - 100, "scrolled clean to bottom");
is(info.x == 0 && info.y > info.height - 100, "scrolled clean to bottom");
});

testCM("selectionPos", function(cm) {
Expand Down Expand Up @@ -357,7 +363,7 @@ testCM("selectionPos", function(cm) {
}
}
is(sawTop && sawBottom && sawMiddle, "all parts");
});
}, null, ie_lt8);

testCM("restoreHistory", function(cm) {
cm.setValue("abc\ndef");
Expand Down Expand Up @@ -464,4 +470,185 @@ testCM("wrappingAndResizing", function(cm) {
var coords = cm.charCoords(pos);
eqPos(pos, cm.coordsChar({x: coords.x + 2, y: coords.y + 2}));
});
}, null, ie_lt8);

testCM("measureEndOfLine", function(cm) {
cm.setSize(null, "auto");
var inner = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild;
var w = 20, lh = inner.offsetHeight;
for (var step = 10;; w += step) {
cm.setSize(w);
if (inner.offsetHeight < 2.5 * lh) {
if (step == 10) { w -= 10; step = 1; }
else { break; }
}
}
cm.setValue(cm.getValue() + "\n\n");
var endPos = cm.charCoords({line: 0, ch: 18}, "local");
is(endPos.y > lh * .8, "not at top");
is(endPos.x > w - 20, "not at right");
endPos = cm.charCoords({line: 0, ch: 18});
eqPos(cm.coordsChar({x: endPos.x, y: endPos.y + 2}), {line: 0, ch: 18});
}, {mode: "text/html", value: "<!-- foo barrr -->", lineWrapping: true}, ie_lt8);

testCM("scrollVerticallyAndHorizontally", function(cm) {
cm.setSize(100, 100);
addDoc(cm, 40, 40);
cm.setCursor(39);
var wrap = cm.getWrapperElement(), bar = byClassName(wrap, "CodeMirror-scrollbar")[0];
is(bar.offsetHeight < wrap.offsetHeight, "vertical scrollbar limited by horizontal one");
var cursorBox = byClassName(wrap, "CodeMirror-cursor")[0].getBoundingClientRect();
var editorBox = wrap.getBoundingClientRect();
is(cursorBox.bottom < editorBox.top + cm.getScrollerElement().clientHeight,
"bottom line visible");
}, {gutter: true});

testCM("moveV stuck", function(cm) {
var lines = byClassName(cm.getWrapperElement(), "CodeMirror-lines")[0].firstChild, h0 = lines.offsetHeight;
var val = "fooooooooooooooooooooooooo baaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaar\n";
cm.setValue(val);
for (var w = 50;; w += 5) {
cm.setSize(w);
if (lines.offsetHeight <= 3 * h0) break;
}
cm.setCursor({line: 0, ch: val.length - 1});
cm.moveV(-1, "line");
eqPos(cm.getCursor(), {line: 0, ch: 26});
}, {lineWrapping: true}, ie_lt8);

testCM("clickTab", function(cm) {
var p0 = cm.charCoords({line: 0, ch: 0}), p1 = cm.charCoords({line: 0, ch: 1});
eqPos(cm.coordsChar({x: p0.x + 5, y: p0.y + 5}), {line: 0, ch: 0});
eqPos(cm.coordsChar({x: p1.x - 5, y: p1.y + 5}), {line: 0, ch: 1});
}, {value: "\t\n\n", lineWrapping: true, tabSize: 8});

testCM("verticalScroll", function(cm) {
cm.setSize(100, 200);
cm.setValue("foo\nbar\nbaz\n");
var sc = cm.getScrollerElement(), baseWidth = sc.scrollWidth;
cm.setLine(0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah");
is(sc.scrollWidth > baseWidth, "scrollbar present");
cm.setLine(0, "foo");
eq(sc.scrollWidth, baseWidth, "scrollbar gone");
cm.setLine(0, "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaah");
cm.setLine(1, "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbh");
is(sc.scrollWidth > baseWidth, "present again");
var curWidth = sc.scrollWidth;
cm.setLine(0, "foo");
is(sc.scrollWidth < curWidth, "scrollbar smaller");
is(sc.scrollWidth > baseWidth, "but still present");
});

testCM("extraKeys", function(cm) {
var outcome;
function fakeKey(expected, code, props) {
if (typeof code == "string") code = code.charCodeAt(0);
var e = {type: "keydown", keyCode: code, preventDefault: function(){}, stopPropagation: function(){}};
if (props) for (var n in props) e[n] = props[n];
outcome = null;
cm.triggerOnKeyDown(e);
eq(outcome, expected);
}
CodeMirror.commands.testCommand = function() {outcome = "tc";};
CodeMirror.commands.goTestCommand = function() {outcome = "gtc";};
cm.setOption("extraKeys", {"Shift-X": function() {outcome = "sx";},
"X": function() {outcome = "x";},
"Ctrl-Alt-U": function() {outcome = "cau";},
"End": "testCommand",
"Home": "goTestCommand",
"Tab": false});
fakeKey(null, "U");
fakeKey("cau", "U", {ctrlKey: true, altKey: true});
fakeKey(null, "U", {shiftKey: true, ctrlKey: true, altKey: true});
fakeKey("x", "X");
fakeKey("sx", "X", {shiftKey: true});
fakeKey("tc", 35);
fakeKey(null, 35, {shiftKey: true});
fakeKey("gtc", 36);
fakeKey("gtc", 36, {shiftKey: true});
fakeKey(null, 9);
});

testCM("wordMovementCommands", function(cm) {
cm.execCommand("goWordLeft");
eqPos(cm.getCursor(), {line: 0, ch: 0});
cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
eqPos(cm.getCursor(), {line: 0, ch: 7});
cm.execCommand("goWordLeft");
eqPos(cm.getCursor(), {line: 0, ch: 5});
cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
eqPos(cm.getCursor(), {line: 0, ch: 12});
cm.execCommand("goWordLeft");
eqPos(cm.getCursor(), {line: 0, ch: 9});
cm.execCommand("goWordRight"); cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
eqPos(cm.getCursor(), {line: 1, ch: 1});
cm.execCommand("goWordRight");
eqPos(cm.getCursor(), {line: 1, ch: 9});
cm.execCommand("goWordRight");
eqPos(cm.getCursor(), {line: 1, ch: 13});
cm.execCommand("goWordRight"); cm.execCommand("goWordRight");
eqPos(cm.getCursor(), {line: 2, ch: 0});
}, {value: "this is (the) firstline.\na foo12\u00e9\u00f8\u00d7bar\n"});

testCM("charMovementCommands", function(cm) {
cm.execCommand("goCharLeft"); cm.execCommand("goColumnLeft");
eqPos(cm.getCursor(), {line: 0, ch: 0});
cm.execCommand("goCharRight"); cm.execCommand("goCharRight");
eqPos(cm.getCursor(), {line: 0, ch: 2});
cm.setCursor({line: 1, ch: 0});
cm.execCommand("goColumnLeft");
eqPos(cm.getCursor(), {line: 1, ch: 0});
cm.execCommand("goCharLeft");
eqPos(cm.getCursor(), {line: 0, ch: 5});
cm.execCommand("goColumnRight");
eqPos(cm.getCursor(), {line: 0, ch: 5});
cm.execCommand("goCharRight");
eqPos(cm.getCursor(), {line: 1, ch: 0});
cm.execCommand("goLineEnd");
eqPos(cm.getCursor(), {line: 1, ch: 5});
cm.execCommand("goLineStartSmart");
eqPos(cm.getCursor(), {line: 1, ch: 1});
cm.execCommand("goLineStartSmart");
eqPos(cm.getCursor(), {line: 1, ch: 0});
cm.setCursor({line: 2, ch: 0});
cm.execCommand("goCharRight"); cm.execCommand("goColumnRight");
eqPos(cm.getCursor(), {line: 2, ch: 0});
}, {value: "line1\n ine2\n"});

testCM("verticalMovementCommands", function(cm) {
cm.execCommand("goLineUp");
eqPos(cm.getCursor(), {line: 0, ch: 0});
cm.execCommand("goLineDown");
eqPos(cm.getCursor(), {line: 1, ch: 0});
cm.setCursor({line: 1, ch: 12});
cm.execCommand("goLineDown");
eqPos(cm.getCursor(), {line: 2, ch: 5});
cm.execCommand("goLineDown");
eqPos(cm.getCursor(), {line: 3, ch: 0});
cm.execCommand("goLineUp");
eqPos(cm.getCursor(), {line: 2, ch: 5});
cm.execCommand("goLineUp");
eqPos(cm.getCursor(), {line: 1, ch: 12});
cm.execCommand("goPageDown");
eqPos(cm.getCursor(), {line: 5, ch: 0});
cm.execCommand("goPageDown"); cm.execCommand("goLineDown");
eqPos(cm.getCursor(), {line: 5, ch: 0});
cm.execCommand("goPageUp");
eqPos(cm.getCursor(), {line: 0, ch: 0});
}, {value: "line1\nlong long line2\nline3\n\nline5\n"});

testCM("verticalMovementCommandsWrapping", function(cm) {
cm.setSize(120);
cm.setCursor({line: 0, ch: 5});
cm.execCommand("goLineDown");
eq(cm.getCursor().line, 0);
is(cm.getCursor().ch > 5, "moved beyond wrap");
for (var i = 0; ; ++i) {
is(i < 20, "no endless loop");
cm.execCommand("goLineDown");
var cur = cm.getCursor();
if (cur.line == 1) eq(cur.ch, 5);
if (cur.line == 2) { eq(cur.ch, 1); break; }
}
}, {value: "a very long line that wraps around somehow so that we can test cursor movement\nshortone\nk",
lineWrapping: true});