ink-docstrap
Version:
[](https://nodei.co/npm/ink-docstrap/)
478 lines (398 loc) • 14.6 kB
JavaScript
(function(sunlight, undefined){
if (sunlight === undefined || sunlight["registerLanguage"] === undefined) {
throw "Include sunlight.js before including language files";
}
sunlight.registerLanguage("ruby", {
//http://www.ruby-doc.org/docs/keywords/1.9/
keywords: [
"BEGIN","END","__ENCODING__","__END__","__FILE__","__LINE__","alias","and","begin","break","case",
"class","def","defined?","do","else","elsif","end","ensure","false","for","if","in","module","next",
"nil","not","or","redo","rescue","retry","return","self","super","then","true","undef","unless",
"until","when","while","yield"
],
customTokens: {
//http://www.ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/function.html
"function": {
values: [
"Array","Float","Integer","String","at_exit","autoload","binding","caller","catch","chop!","chop",
"chomp!","chomp","eval","exec","exit!","exit","fail","fork","format","gets","global_variables",
"gsub!","gsub","iterator?","lambda","load","local_variables","loop","open","p","print","printf","proc",
"putc","puts","raise","rand","readline","readlines","require","select","sleep","split","sprintf","srand",
"sub!","sub","syscall","system","test","trace_var","trap","untrace_var"
],
boundary: "\\W"
},
specialOperator: {
values: ["defined?", "eql?", "equal?"],
boundary: "\\W"
}
},
customParseRules: [
//regex literal, same as javascript
function(context) {
var isValid,
regexLiteral = "/",
line = context.reader.getLine(),
column = context.reader.getColumn(),
peek2,
next;
if (context.reader.current() !== "/") {
return null;
}
isValid = function() {
var previousNonWsToken = context.token(context.count() - 1),
previousToken = null;
if (context.defaultData.text !== "") {
previousToken = context.createToken("default", context.defaultData.text);
}
if (!previousToken) {
previousToken = previousNonWsToken;
}
//first token of the string
if (previousToken === undefined) {
return true;
}
//since Ruby doesn't have statement terminators, if the previous token was whitespace and contained a newline, then we're good
if (previousToken.name === "default" && previousToken.value.indexOf("\n") > -1) {
return true;
}
if (sunlight.util.contains(["keyword", "ident", "number"], previousNonWsToken.name)) {
return false;
}
if (previousNonWsToken.name === "punctuation" && !sunlight.util.contains(["(", "{", "[", ","], previousNonWsToken.value)) {
return false;
}
return true;
}();
if (!isValid) {
return null;
}
while (context.reader.peek() !== context.reader.EOF) {
peek2 = context.reader.peek(2);
if (peek2 === "\\/" || peek2 === "\\\\") {
//escaped backslash or escaped forward slash
regexLiteral += context.reader.read(2);
continue;
}
regexLiteral += (next = context.reader.read());
if (next === "/") {
break;
}
}
//read the regex modifiers
//only "x", "i", "o" and "m" are allowed, but for the sake of simplicity we'll just say any alphabetical character is valid
while (context.reader.peek() !== context.reader.EOF) {
if (!/[A-Za-z]/.test(context.reader.peek())) {
break;
}
regexLiteral += context.reader.read();
}
return context.createToken("regexLiteral", regexLiteral, line, column);
},
//symbols
function(context) {
var token,
index = context.count(),
parenCount = 0,
count = index - 1,
prevToken,
symbol;
//this is goofy, because it needs to recognize things like "foo = true ? :true :not_true"
//and detect that :not_true is not a symbol
if (context.reader.current() !== ":" || !/[a-zA-Z_]/.test(context.reader.peek())) {
return null;
}
//basically look backward until a line break not preceded by an operator or a comma
while (token = context.token(--index)) {
if (token.name === "operator") {
if (parenCount === 0) {
if (token.value === "?" && index < count) {
//this is a ternary operator, not a symbol
return null;
} else if (token.value === ":") {
break;
}
}
} else if (token.name === "punctuation") {
switch (token.value) {
case "(":
parenCount--;
break;
case ")":
parenCount++;
break;
}
} else if (token.name === "default" && /\n/.test(token.value)) {
prevToken = context.token(index - 1);
if (prevToken && (prevToken.name === "operator" || (prevToken.name === "punctuation" && prevToken.value === ","))) {
//line continuation
continue;
}
break;
}
}
//read the symbol
symbol = /^:\w+/.exec(context.reader.substring())[0];
token = context.createToken("symbol", symbol, context.reader.getLine(), context.reader.getColumn());
context.reader.read(symbol.length - 1); //already read the ":"
return token;
},
//heredoc declaration
//heredocs can be stacked and delimited, so this is a bit complicated
//we keep track of the heredoc declarations in context.items.heredocQueue, and then use them later in the heredoc custom parse rule below
function(context) {
var prevToken,
line = context.reader.getLine(),
column = context.reader.getColumn(),
value = "<<",
ident = "",
current,
delimiter = "",
peek,
peek2;
if (context.reader.current() !== "<" || !/<[\w'"`-]/.test(context.reader.peek(2))) {
return null;
}
//cannot be preceded by an a number or a string
prevToken = context.token(context.count() - 1);
if (prevToken && sunlight.util.contains(["number", "string"], prevToken.name)) {
return null;
}
//there are still cases where heredocs are falsely detected, because it would require performing
//static analysis
//e.g. foo <<a
//if foo is an object that has the "<<" method defined, then it will perform a left shift
//if foo is a function that takes a string argument, it will interpret it as a heredoc
//so, we just force you to have whitespace between << and the rhs operand in these ambiguous cases
//can be between quotes (double, single or back) or not, or preceded by a hyphen
context.reader.read(2);
current = context.reader.current();
if (current === "-") {
context.reader.read();
value += current;
ident += current;
current = context.reader.current();
}
if (sunlight.util.contains(["\"", "'", "`"], current)) {
delimiter = current;
} else {
ident += current;
}
value += current;
while ((peek = context.reader.peek()) !== context.reader.EOF) {
if (peek === "\n" || (delimiter === "" && /\W/.test(peek))) {
break;
}
if (peek === "\\") {
peek2 = context.reader.peek(2);
if (delimiter !== "" && sunlight.util.contains(["\\" + delimiter, "\\\\"], peek2)) {
value += peek2;
ident += context.reader.read(2);
continue;
}
}
value += context.reader.read();
if (delimiter !== "" && peek === delimiter) {
break;
}
ident += peek;
}
context.items.heredocQueue.push(ident);
return context.createToken("heredocDeclaration", value, line, column);
},
//heredoc
function(context) {
var tokens = [],
declaration,
line,
column,
value = context.reader.current(),
ignoreWhitespace = false,
regex,
match;
if (context.items.heredocQueue.length === 0) {
return null;
}
//there must have been at least one line break since the heredoc declaration(s)
if (context.defaultData.text.replace(/[^\n]/g, "").length === 0) {
return null;
}
//we're confirmed to be in the heredoc body, so read until all of the heredoc declarations have been satisfied
while (context.items.heredocQueue.length > 0 && context.reader.peek() !== context.reader.EOF) {
declaration = context.items.heredocQueue.shift();
if (declaration.charAt(0) === "-") {
declaration = declaration.substring(1);
ignoreWhitespace = true;
}
line = context.reader.getLine(), column = context.reader.getColumn();
//read until "\n{declaration}\n"
//unless the declaration is prefixed with "-", then we don't care about preceding whitespace, but it must be on its own line
//e.g. \n[ \t]*{declaration}\n
regex = new RegExp("^\\n" + (ignoreWhitespace ? "[ \\t]*" : "") + sunlight.util.regexEscape(declaration) + "\\n");
while (context.reader.peek() !== context.reader.EOF) {
match = regex.exec(context.reader.peekSubstring());
if (match !== null) {
value += context.reader.read(match[0].length);
break;
}
value += context.reader.read();
}
tokens.push(context.createToken("heredoc", value, line, column));
value = "";
}
return tokens.length > 0 ? tokens : null;
},
//raw string/regex
//http://www.ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html#string
//http://www.ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html#regexp
function(context) {
var value = "%",
readCount = 1,
isRegex = false,
peek,
line = context.reader.getLine(),
column = context.reader.getColumn(),
delimiter;
//begin with % or %q or %Q with a non-alphanumeric delimiter (opening bracket/paren are closed by corresponding closing bracket/paren)
if (context.reader.current() !== "%") {
return null;
}
peek = context.reader.peek();
if (peek === "q" || peek === "Q" || peek === "r") {
readCount++;
if (peek === "r") {
isRegex = true;
}
}
if (/[A-Za-z0-9=]$/.test(context.reader.peek(readCount))) {
//potential % or %= operator (how does ruby differentiate between "%=" and "%=string="?)
return null;
}
value += context.reader.read(readCount);
delimiter = value.charAt(value.length - 1);
switch (delimiter) {
case "(":
delimiter = ")";
break;
case "[":
delimiter = "]";
break;
case "{":
delimiter = "}";
break;
}
//read until the delimiter
while ((peek = context.reader.peek()) !== context.reader.EOF) {
if (peek === "\\" && sunlight.util.contains(["\\" + delimiter, "\\\\"], context.reader.peek(2))) {
//escape sequence
value += context.reader.read(2);
continue;
}
value += context.reader.read();
if (peek === delimiter) {
break;
}
}
if (isRegex) {
//read potential regex modifiers
while (context.reader.peek() !== context.reader.EOF) {
if (!/[A-Za-z]/.test(context.reader.peek())) {
break;
}
value += context.reader.read();
}
}
return context.createToken(isRegex ? "regexLiteral" : "rawString", value, line, column);
},
//doc comments
//http://www.ruby-doc.org/docs/ruby-doc-bundle/Manual/man-1.4/syntax.html#embed_doc
function(context) {
var value = "=begin",
line = context.reader.getLine(),
column = context.reader.getColumn(),
foundEnd = false,
peek;
//these begin on with a line that starts with "=begin" and end with a line that starts with "=end"
//apparently stuff on the same line as "=end" is also part of the comment
if (!context.reader.isSol() || context.reader.current() !== "=" || context.reader.peek(5) !== "begin") {
return null;
}
context.reader.read(5);
//read until "\n=end" and then everything until the end of that line
while ((peek = context.reader.peek()) !== context.reader.EOF) {
if (!foundEnd && context.reader.peek(5) === "\n=end") {
foundEnd = true;
value += context.reader.read(5);
continue;
}
if (foundEnd && peek === "\n") {
break;
}
value += context.reader.read();
}
return context.createToken("docComment", value, line, column);
}
],
scopes: {
string: [ ["\"", "\"", sunlight.util.escapeSequences.concat(["\\\""])], ["'", "'", ["\\\'", "\\\\"]] ],
comment: [ ["#", "\n", null, true] ],
subshellCommand: [ ["`", "`", ["\\`"]] ],
globalVariable: [ ["$", { length: 1, regex: /[\W]/ }, null, true] ],
instanceVariable: [ ["@", { length: 1, regex: /[\W]/ }, null, true] ]
},
identFirstLetter: /[A-Za-z_]/,
identAfterFirstLetter: /\w/,
namedIdentRules: {
follows: [
//class names
//function names
[{ token: "keyword", values: ["class", "def"] }, sunlight.util.whitespace],
//extended classes
[
{ token: "keyword", values: ["class"] },
sunlight.util.whitespace,
{ token: "ident" },
sunlight.util.whitespace,
{ token: "operator", values: ["<", "<<"] },
sunlight.util.whitespace
]
],
precedes: [
//static variable access
[sunlight.util.whitespace, { token: "operator", values: ["::"] }],
//new-ing a class
[
sunlight.util.whitespace,
{ token: "operator", values: ["."] },
sunlight.util.whitespace,
{ token: "ident", values: ["new"] },
sunlight.util.whitespace,
{ token: "punctuation", values: ["("] }
]
]
},
operators: [
"?",
"...", "..", ".",
"::", ":",
"[]",
"+=", "+",
"-=", "-",
"**=", "*=", "**", "*",
"/=", "/",
"%=", "%",
"&&=", "&=", "&&", "&",
"||=", "|=", "||", "|",
"^=", "^",
"~",
"\\", //line continuation
"<=>", "<<=", "<<", "<=", "<",
">>=", ">>", ">=", ">",
"!~", "!=", "!",
"=>", "===", "==", "=~", "="
],
contextItems: {
heredocQueue: []
}
});
}(this["Sunlight"]));