ink-docstrap
Version:
[](https://nodei.co/npm/ink-docstrap/)
784 lines (644 loc) • 24.3 kB
JavaScript
(function(sunlight, undefined){
if (sunlight === undefined || sunlight["registerLanguage"] === undefined) {
throw "Include sunlight.js before including language files";
}
var primitives = ["int", "bool", "double", "float", "char", "byte", "sbyte", "uint", "long", "ulong", "char", "decimal", "short", "ushort"],
//things that are allowed inside a generic type definition
acceptableKeywords = primitives.concat(["in", "out", "string", "object"]);
function createNamedIdentFunction(func) {
var typeDefinitionRegex = /^T([A-Z0-9]\w*)?$/;
return function(context) {
return !typeDefinitionRegex.test(context.tokens[context.index].value) && func(context);
};
}
sunlight.registerLanguage("csharp", {
keywords: primitives.concat([
//this is also contextual (must be first thing in the file or something), but it's never used so i don't really care
"extern alias",
//class qualifiers
"public", "private", "protected", "internal", "static", "sealed", "abstract", "partial",
//method qualifiers
"virtual", "override", "new", "implicit", "explicit", "extern", "override", "operator",
//member qualifiers
"const", "readonly", "volatile",
//types
"class", "interface", "enum", "struct", "event", "delegate",
//literals
"null", "true", "false",
//aliases
"string", "object", "void",
//looping
"for", "foreach", "do", "while",
//scoping
"fixed", "unchecked", "using", "lock", "namespace", "checked", "unsafe",
//flow control
"if", "else", "try", "catch", "finally", "break", "continue", "goto", "case", "throw", "return", "switch", "yield return", "yield break",
//parameter qualifiers
"in", "out", "ref", "params",
//type comparison
"as", "is", "typeof",
//other
"this", "sizeof", "stackalloc", "var", "default",
//contextual keywords
//property stuff
//get, set and value are handled by customParseRules below
//linq
"from", "select", "where", "groupby", "orderby"
]),
customParseRules: [
//xml doc comments
function(context) {
var metaName = "xmlDocCommentMeta", //tags and the "///" starting token
contentName = "xmlDocCommentContent", //actual comments (words and stuff)
tokens,
peek,
current = { line: 0, column: 0, value: "", name: null };
if (context.reader.current() !== "/" || context.reader.peek(2) !== "//") {
return null;
}
tokens = [context.createToken(metaName, "///", context.reader.getLine(), context.reader.getColumn())];
context.reader.read(2);
while ((peek = context.reader.peek()) !== context.reader.EOF) {
if (peek === "<" && current.name !== metaName) {
//push the current token
if (current.value !== "") {
tokens.push(context.createToken(current.name, current.value, current.line, current.column));
}
//amd create a token for the tag
current.line = context.reader.getLine();
current.column = context.reader.getColumn();
current.name = metaName;
current.value = context.reader.read();
continue;
}
if (peek === ">" && current.name === metaName) {
//close the tag
current.value += context.reader.read();
tokens.push(context.createToken(current.name, current.value, current.line, current.column));
current.name = null;
current.value = "";
continue;
}
if (peek === "\n") {
break;
}
if (current.name === null) {
current.name = contentName;
current.line = context.reader.getLine();
current.column = context.reader.getColumn();
}
current.value += context.reader.read();
}
if (current.name === contentName) {
tokens.push(context.createToken(current.name, current.value, current.line, current.column));
}
return tokens.length > 0 ? tokens : null;
},
//get/set contextual keyword
function(context) {
var rule,
count,
peek,
allGoodYo = false,
line = context.reader.getLine(),
column = context.reader.getColumn(),
value;
if (!/^(get|set)\b/.test(context.reader.current() + context.reader.peek(3))) {
return null;
}
rule = sunlight.util.createProceduralRule(context.count() - 1, -1, [
{ token: "punctuation", values: ["}", "{", ";"] },
sunlight.util.whitespace,
{ token: "keyword", values: ["public", "private", "protected", "internal"], optional: true }
]);
if (!rule(context.getAllTokens())) {
return null;
}
//now we need to look ahead and verify that the next non-sunlight.util.whitespace token is "{" or ";"
count = "get".length;
peek = context.reader.peek(count);
while (peek.length === count) {
if (!/\s$/.test(peek)) {
if (!/[\{;]$/.test(peek)) {
return null;
}
allGoodYo = true;
break;
}
peek = context.reader.peek(++count);
}
if (!allGoodYo) {
return null;
}
value = context.reader.current() + context.reader.read(2); //we already read the first letter
return context.createToken("keyword", value, line, column);
},
//value contextual keyword
function(context) {
var count,
peek,
allGoodYo,
token,
index,
bracketCount,
line = context.reader.getLine(),
column = context.reader.getColumn(),
peekPlus1;
if (!/^value\b/.test(context.reader.current() + context.reader.peek(5))) {
return null;
}
//comes after "set" but not after the closing "}" (we'll have to count them to make sure scoping is correct)
//can't be on the left side of an assignment
//first check equals because that's easy
count = "value".length;
peek = context.reader.peek(count);
while (peek.length === count) {
if (!/\s$/.test(peek)) {
peekPlus1 = context.reader.peek(count + 1);
if (peek.charAt(peek.length - 1) === "=" && peekPlus1.charAt(peekPlus1.length - 1) !== "=") {
//"value" is on the left side of an assignment, so this is not the droid we're looking for
return null;
}
allGoodYo = true;
break;
}
peek = context.reader.peek(++count);
}
if (!allGoodYo) {
//EOF FTL
return null;
}
//now go backward until we run into a "set" keyword, keeping track of all brackets along the way
index = context.count() -1;
bracketCount = [0, 0]; //open, close
tokenLoop: while ((token = context.token(index--)) !== undefined) {
if (token.name === "punctuation") {
if (token.value === "{") {
bracketCount[0]++;
} else if (token.value === "}") {
bracketCount[1]++;
}
} else if (token.name === "keyword") {
switch (token.value) {
case "set":
//yay!
break tokenLoop;
case "class":
case "public":
case "private":
case "protected":
case "internal":
//easiest way to detect we're out of scope so we can stop looking
return null;
}
}
}
if (token === undefined) {
//EOF FTL
return null;
}
//examine the bracket count and make sure we're in the correct scope
if (bracketCount[1] >= bracketCount[0]) {
//nope
return null;
}
context.reader.read(4); //already read the "v" in "value"
return context.createToken("keyword", "value", line, column);
}
],
scopes: {
string: [ ["\"", "\"", sunlight.util.escapeSequences.concat(["\\\""])], ["@\"", "\"", ["\"\""]] ],
"char": [ ["'", "'", ["\\'", "\\\\"]] ],
comment: [ ["//", "\n", null, true], ["/*", "*/"] ],
pragma: [ ["#", "\n", null, true] ]
},
identFirstLetter: /[A-Za-z_@]/,
identAfterFirstLetter: /\w/,
namedIdentRules: {
custom: [
//extends/implements/type constraints
createNamedIdentFunction(function(context) {
var index = context.index,
token,
foundColon = false,
nextToken;
//between ":" and "{" but not case statements
//look backward for a ":" not preceded by a "case"
while ((token = context.tokens[--index]) !== undefined) {
if (token.name === "punctuation" && token.value === "{") {
return false;
}
if (token.name === "keyword" && token.value === "case") {
return false;
}
if (token.name === "keyword" && (token.value === "class" || token.value === "where")) {
//the "class" keyword for classes
//or the "where" keyword for generic methods/classes with type constraints
//if "class" is used as a type constraint, then ignore it
nextToken = context.tokens[index + 1].name === "default" ? context.tokens[index + 2] : context.tokens[index + 1];
if (nextToken.name === "punctuation" && nextToken.value === ",") {
continue;
}
break;
}
if (token.name === "operator" && token.value === ":") {
//make sure there isn't a case preceding it
foundColon = true;
}
}
if (!foundColon) {
return false;
}
return true;
}),
//generic definitions/params between "<" and ">"
createNamedIdentFunction(function(context) {
//between < and > and preceded by an ident and not preceded by "class"
var index = context.index,
token,
foundIdent = false,
bracketCountLeft = [0, 0];
//look for "<" preceded by an ident but not "class"
//if we run into ">" before "," or "<" then it's a big fail
while ((token = context.tokens[--index]) !== undefined) {
if (token.name === "keyword" && token.value === "class") {
//this must be a generic class type definition, e.g. Foo<T>, and we don't want to color the "T"
return false;
}
if (token.name === "operator") {
switch (token.value) {
case "<":
case "<<":
bracketCountLeft[0] += token.value.length;
continue;
case ">":
case ">>":
if (bracketCountLeft[0] === 0) {
return false;
}
bracketCountLeft[1] += token.value.length;
continue;
}
break;
}
if (
(token.name === "keyword" && sunlight.util.contains(acceptableKeywords, token.value))
|| token.name === "default"
|| (token.name === "punctuation" && token.value === ",")
) {
continue;
}
if (token.name === "ident") {
foundIdent = true;
continue;
}
//anything else means we're no longer in a generic definition
break;
}
if (!foundIdent || bracketCountLeft[0] === 0) {
//not inside a generic definition
return false;
}
//now look forward to make sure the generic definition is closed
//this avoids false positives like "foo < bar"
index = context.index;
while ((token = context.tokens[++index]) !== undefined) {
if (token.name === "operator" && (token.value === ">" || token.value === ">>")) {
return true;
}
if (
(token.name === "keyword" && sunlight.util.contains(acceptableKeywords, token.value))
|| (token.name === "operator" && sunlight.util.contains(["<", "<<", ">", ">>"], token.value))
|| (token.name === "punctuation" && token.value === ",")
|| token.name === "ident"
|| token.name === "default"
) {
continue;
}
return false;
}
return false;
}),
//generic declarations and return values (ident preceding a generic definition)
//this finds "Foo" in "Foo<Bar> foo"
createNamedIdentFunction(function(context) {
var token = sunlight.util.getPreviousNonWsToken(context.tokens, context.index),
index,
bracketCount;
//if it's preceded by an ident or a primitive/alias keyword then it's no good (i.e. a generic method definition like "public void Foo<T>")
//also a big fail if it is preceded by a ., i.e. a generic method invocation like container.Resolve()
if (token !== undefined) {
if (
token.name === "ident"
|| (token.name === "keyword" && sunlight.util.contains(primitives.concat(["string", "object", "void"]), token.value))
|| (token.name === "operator" && token.value === ".")
) {
return false;
}
}
//needs to be immediately followed by <, then by idents, acceptable keywords and ",", and then closed by >, then immediately followed by an ident
token = sunlight.util.getNextNonWsToken(context.tokens, context.index);
if (!token || token.name !== "operator" || token.value !== "<") {
return false;
}
index = context.index;
bracketCount = [0, 0]; //open (<), close (>)
while ((token = context.tokens[++index]) !== undefined) {
if (token.name === "operator") {
switch (token.value) {
case "<":
bracketCount[0]++;
break;
case "<<":
bracketCount[0] += 2;
break;
case ">":
bracketCount[1]++;
break;
case ">>":
bracketCount[1] += 2;
break;
default:
return false;
}
//if bracket counts match, get the f out
if (bracketCount[0] === bracketCount[1]) {
break;
}
continue;
}
if (
token.name === "default"
|| token.name === "ident"
|| (token.name === "keyword" && sunlight.util.contains(acceptableKeywords, token.value))
|| (token.name === "punctuation" && token.value === ",")
) {
continue;
}
return false;
}
//verify bracket count
if (bracketCount[0] !== bracketCount[1]) {
//mismatched generics, could be something scary
return false;
}
//next token should be optional whitespace followed by an ident
token = context.tokens[++index];
if (!token || (token.name !== "default" && token.name !== "ident")) {
return false;
}
if (token.name === "default") {
token = context.tokens[++index];
if (!token || token.name !== "ident") {
return false;
}
}
return true;
}),
//using aliases, e.g. "Foo" in "using Foo = System.Linq.Enumerable;"
function(context) {
//previous non-ws token must be "using" and next non-ws token must be "="
var prevToken = sunlight.util.getPreviousNonWsToken(context.tokens, context.index),
nextToken;
if (!prevToken || prevToken.name !== "keyword" || prevToken.value !== "using") {
return false;
}
nextToken = sunlight.util.getNextNonWsToken(context.tokens, context.index);
if (!nextToken || nextToken.name !== "operator" || nextToken.value !== "=") {
return false;
}
return true;
},
//attributes (friggin' attributes...)
createNamedIdentFunction(function(context) {
var token = sunlight.util.getNextNonWsToken(context.tokens, context.index),
index,
bracketCount,
foundComma = false,
indexOfLastBracket;
//if the next token is an equals sign, this is a named parameter (or something else not inside of an attribute)
if (token && token.name === "operator" && (token.value === "=" || token.value === ".")) {
return false;
}
//this is annoyingly complicated
//we need to verify that we're between [], but not in something like "new string[foo]" for instance
//first, verify that we're inside an opening bracket
index = context.index;
bracketCount = [0, 0];
foundComma = false;
while ((token = context.tokens[--index]) !== undefined) {
if (token.name === "punctuation") {
if (token.value === "[") {
bracketCount[0]++;
continue;
}
if (token.value === "]") {
bracketCount[1]++;
continue;
}
if (token.value === ",") {
foundComma = true;
}
//exit rules
if (token.value === "{" || token.value === "}" || token.value === ";") {
break;
}
}
}
if (bracketCount[0] === 0 || bracketCount[0] === bracketCount[1]) {
//if no brackets were found OR...
//all the found brackets are closed, so this ident is actually outside of the brackets
//duh.
return false;
}
//next, verify we're inside a closing bracket
index = context.index;
indexOfLastBracket = -1;
while ((token = context.tokens[++index]) !== undefined) {
if (token.name === "punctuation") {
if (token.value === "[") {
bracketCount[0]++;
continue;
}
if (token.value === "]") {
indexOfLastBracket = index;
bracketCount[1]++;
continue;
}
//some early exit rules
if (token.value === "{" || token.value === "}" || token.value === ";") {
break;
}
}
}
if (indexOfLastBracket < 0 || bracketCount[0] !== bracketCount[1]) {
return false;
}
//next token after the last closing bracket should be either a keyword or an ident
token = sunlight.util.getNextNonWsToken(context.tokens, indexOfLastBracket);
if (token && (token.name === "keyword" || token.name === "ident")) {
return true;
}
return false;
}),
//fully qualified type names
createNamedIdentFunction(function(context) {
//next token is not "."
var nextToken = sunlight.util.getNextNonWsToken(context.tokens, context.index),
token,
index,
previous;
if (nextToken && nextToken.name === "operator" && nextToken.value === ".") {
return false;
}
//go backward and make sure that there are only idents and dots before the new keyword
//"previous" is used to make sure that method declarations like "public new Object Value()..." are treated correctly
index = context.index;
previous = context.tokens[index];
while ((token = context.tokens[--index]) !== undefined) {
if (token.name === "keyword" && (token.value === "new" || token.value === "is")) {
return true;
}
if (token.name === "default") {
continue;
}
if (token.name === "ident") {
if (previous && previous.name === "ident") {
return false;
}
previous = token;
continue;
}
if (token.name === "operator" && token.value === ".") {
if (previous && previous.name !== "ident") {
return false;
}
previous = token;
continue;
}
break;
}
return false;
}),
//casting
function() {
var precedes = [
[sunlight.util.whitespace, { token: "punctuation", values: [")"] }, sunlight.util.whitespace, { token: "ident" }],
[sunlight.util.whitespace, { token: "punctuation", values: [")"] }, sunlight.util.whitespace, { token: "keyword", values: ["this"] }]
];
return createNamedIdentFunction(function(context) {
var token,
index,
prevToken,
precedesIsSatisfied = function(tokens) {
for (var i = 0; i < precedes.length; i++) {
if (sunlight.util.createProceduralRule(context.index + 1, 1, precedes[i], false)(tokens)) {
return true;
}
}
return false;
}(context.tokens);
if (!precedesIsSatisfied) {
return false;
}
//make sure the previous tokens are "(" and then not a keyword
//this'll make sure that things like "if (foo) doSomething();" won't color "foo"
index = context.index;
while (token = context.tokens[--index]) {
if (token.name === "punctuation" && token.value === "(") {
prevToken = sunlight.util.getPreviousNonWsToken(context.tokens, index);
if (prevToken && prevToken.name === "keyword") {
return false;
}
return true;
}
}
return false;
});
}(),
//using alias type names, e.g. "Foo" in "using Bar = My.Namespace.Foo;"
function(context) {
var nextToken = sunlight.util.getNextNonWsToken(context.tokens, context.index),
token,
index;
if (!nextToken || nextToken.name !== "punctuation" || nextToken.value !== ";") {
return false;
}
//should be preceded by idents and dots, and then "using ="
index = context.index;
while (token = context.tokens[--index]) {
if (token.name !== "ident" && token.name !== "default" && (token.name !== "operator" || token.value !== ".")) {
//should be an equals sign, and then an ident and then "using"
if (token.name !== "operator" || token.value !== "=") {
return false;
}
return sunlight.util.createProceduralRule(
index - 1,
-1,
[{ token: "keyword", values: ["using"] }, { token: "default" }, { token: "ident" }, sunlight.util.whitespace]
)(context.tokens);
}
}
return false;
},
//can't use the follows/precedes utilities since we need to verify that it doesn't match the type definition naming convention
createNamedIdentFunction(function(context) {
var i,
follows = [
//method/property return values
//special method parameters
//new: public new Foo Method() { } and new Foo();
//class/interface/event/struct/delegate names
[{ token: "keyword", values: ["class", "interface", "event", "struct", "enum", "delegate", "public", "private", "protected", "internal", "static", "virtual", "sealed", "params"] }, sunlight.util.whitespace],
//typeof/default
[{ token: "keyword", values: ["typeof", "default"] }, sunlight.util.whitespace, { token: "punctuation", values: ["("] }, sunlight.util.whitespace ],
//casting using "as"
[{ token: "keyword", values: ["as"] }, sunlight.util.whitespace ]
],
precedes = [
//arrays
[sunlight.util.whitespace, { token: "punctuation", values: ["["] }, sunlight.util.whitespace, { token: "punctuation", values: ["]"] }], //in method parameters
//assignment: Object o = new object();
//method parameters: public int Foo(Foo foo, Bar b, Object o) { }
[{ token: "default" }, { token: "ident" }]
];
for (i = 0; i < follows.length; i++) {
if (sunlight.util.createProceduralRule(context.index - 1, -1, follows[i], false)(context.tokens)) {
return true;
}
}
for (i = 0; i < precedes.length; i++) {
if (sunlight.util.createProceduralRule(context.index + 1, 1, precedes[i], false)(context.tokens)) {
return true;
}
}
return false;
})
]
},
operators: [
//arithmetic
"++", "+=", "+",
"--", "-=", "-",
"*=", "*",
"/=", "/",
"%=", "%",
//boolean
"&&", "||",
//bitwise
"|=", "|",
"&=", "&",
"^=", "^",
">>=", ">>",
"<<=", "<<",
//inequality
"<=", "<",
">=", ">",
"==", "!=",
//unary
"!", "~",
//other
"??", "?", "::", ":", ".", "=>", "="
]
});
}(this["Sunlight"]));