perfectkb-yate
Version:
Yet Another Turtle Editor, forked from YASQE
581 lines (527 loc) • 18.1 kB
JavaScript
"use strict";
var CodeMirror = require("codemirror");
CodeMirror.defineMode("rdf11turtle", function(config, parserConfig) {
var indentUnit = config.indentUnit;
var grammar = require("./_tokenizer-table.js");
var ll1_table = grammar.table;
var IRI_REF = '<[^<>"`\|\{\}\^\\\x00-\x20]*>';
/*
* PN_CHARS_BASE =
* '[A-Z]|[a-z]|[\\u00C0-\\u00D6]|[\\u00D8-\\u00F6]|[\\u00F8-\\u02FF]|[\\u0370-\\u037D]|[\\u037F-\\u1FFF]|[\\u200C-\\u200D]|[\\u2070-\\u218F]|[\\u2C00-\\u2FEF]|[\\u3001-\\uD7FF]|[\\uF900-\\uFDCF]|[\\uFDF0-\\uFFFD]|[\\u10000-\\uEFFFF]';
*/
var PN_CHARS_BASE = "[A-Za-z\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02FF\\u0370-\\u037D\\u037F-\\u1FFF\\u200C-\\u200D\\u2070-\\u218F\\u2C00-\\u2FEF\\u3001-\\uD7FF\\uF900-\\uFDCF\\uFDF0-\\uFFFD]";
var PN_CHARS_U = PN_CHARS_BASE + "|_";
var PN_CHARS = "(" + PN_CHARS_U + "|-|[0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040])";
var VARNAME = "(" + PN_CHARS_U + "|[0-9])" + "(" + PN_CHARS_U + "|[0-9\\u00B7\\u0300-\\u036F\\u203F-\\u2040])*";
var VAR1 = "\\?" + VARNAME;
var VAR2 = "\\$" + VARNAME;
var PN_PREFIX = "(" + PN_CHARS_BASE + ")(((" + PN_CHARS + ")|\\.)*(" + PN_CHARS + "))?";
var HEX = "[0-9A-Fa-f]";
var PERCENT = "(%" + HEX + HEX + ")";
var PN_LOCAL_ESC = "(\\\\[_~\\.\\-!\\$&'\\(\\)\\*\\+,;=/\\?#@%])";
var PLX = "(" + PERCENT + "|" + PN_LOCAL_ESC + ")";
var PN_LOCAL = "(" +
PN_CHARS_U +
"|:|[0-9]|" +
PLX +
")((" +
PN_CHARS +
"|\\.|:|" +
PLX +
")*(" +
PN_CHARS +
"|:|" +
PLX +
"))?";
var BLANK_NODE_LABEL = "_:(" + PN_CHARS_U + "|[0-9])((" + PN_CHARS + "|\\.)*" + PN_CHARS + ")?";
var PNAME_NS = "(" + PN_PREFIX + ")?:";
var PNAME_LN = PNAME_NS + PN_LOCAL;
var LANGTAG = "@[a-zA-Z]+(-[a-zA-Z0-9]+)*";
var EXPONENT = "[eE][\\+-]?[0-9]+";
var INTEGER = "[-\\+]?[0-9]+";
var DECIMAL = "[-\\+]?(([0-9]+\\.[0-9]*)|(\\.[0-9]+))";
var DOUBLE = "[-\\+]?(([0-9]+\\.[0-9]*" + EXPONENT + ")|" + "(\\.[0-9]+" + EXPONENT + ")|" + "([0-9]+" + EXPONENT + "))";
var ECHAR = "\\\\[tbnrf\\\\\"']";
//IMPORTANT: this unicode rule is not in the official grammar.
//Reason: https://github.com/YASGUI/YASQE/issues/49
//unicode escape sequences (which the sparql spec considers part of the pre-processing of sparql queries)
//are marked as invalid. We have little choice (other than adding a layer of complixity) than to modify the grammar accordingly
//however, for now only allow these escape sequences in literals (where actually, this should be allows in e.g. prefixes as well)
var hex4 = HEX + "{4}";
var unicode = "(\\\\u" + hex4 + "|\\\\U00(10|0" + HEX + ")" + hex4 + ")";
var LINE_BREAK = "\n";
var STRING_LITERAL_SINGLE_QUOTE = "'(([^\\x27\\x5C\\x0A\\x0D])|" + ECHAR + "|" + unicode + ")*'";
var STRING_LITERAL_QUOTE = '"(([^\\x22\\x5C\\x0A\\x0D])|' + ECHAR + "|" + unicode + ')*"';
var STRING_LITERAL_LONG = {
SINGLE_QUOTE: {
CAT: "STRING_LITERAL_LONG_SINGLE_QUOTE",
QUOTES: "'''",
CONTENTS: "(('|'')?([^'\\\\]|" + ECHAR + "|" + unicode + "))*"
},
QUOTE: {
CAT: "STRING_LITERAL_LONG_QUOTE",
QUOTES: '"""',
CONTENTS: '(("|"")?([^"\\\\]|' + ECHAR + "|" + unicode + "))*"
}
};
for (var key in STRING_LITERAL_LONG) {
STRING_LITERAL_LONG[key].COMPLETE = STRING_LITERAL_LONG[key].QUOTES +
STRING_LITERAL_LONG[key].CONTENTS +
STRING_LITERAL_LONG[key].QUOTES;
}
// var STRING_LITERAL_LONG_QUOTES = {
// "STRING_LITERAL_LONG_QUOTES1": "'''",
// "STRING_LITERAL_LONG_QUOTES2": '"""',
// }
// var STRING_LITERAL_LONG_CONTENTS = {
// "STRING_LITERAL_LONG_QUOTES1": "(('|'')?([^'\\\\]|"+ECHAR+"|"+unicode+"))*",
// "STRING_LITERAL_LONG_QUOTES2": '(("|"")?([^"\\\\]|'+ECHAR+'|'+unicode+'))*'
// };
// var STRING_LITERAL_LONG1 = STRING_LITERAL_LONG['SINGLE'].QUOTES + STRING_LITERAL_LONG['SINGLE'].CONTENTS + STRING_LITERAL_LONG['SINGLE'].QUOTES;
// var STRING_LITERAL_LONG2 = STRING_LITERAL_LONG['DOUBLE'].QUOTES + STRING_LITERAL_LONG['DOUBLE'].CONTENTS + STRING_LITERAL_LONG['DOUBLE'].QUOTES;
// var stringLiteralLongContentTerminals = {};
// for (var key in STRING_LITERAL_LONG) {
// stringLiteralLongContentTerminals[key] = {
// name: key,
// regex:new RegExp("^"+STRING_LITERAL_LONG_CONTENTS[key]),
// style:"string"
// };
// }
//some regular expressions not used in regular terminals, because this is used accross lines
var stringLiteralLongRegex = {};
for (var key in STRING_LITERAL_LONG) {
stringLiteralLongRegex[key] = {
complete: {
name: "STRING_LITERAL_LONG_" + key,
regex: new RegExp("^" + STRING_LITERAL_LONG[key].COMPLETE),
style: "string"
},
contents: {
name: "STRING_LITERAL_LONG_" + key,
regex: new RegExp("^" + STRING_LITERAL_LONG[key].CONTENTS),
style: "string"
},
closing: {
name: "STRING_LITERAL_LONG_" + key,
regex: new RegExp("^" + STRING_LITERAL_LONG[key].CONTENTS + STRING_LITERAL_LONG[key].QUOTES),
style: "string"
},
quotes: {
name: "STRING_LITERAL_LONG_QUOTES_" + key,
regex: new RegExp("^" + STRING_LITERAL_LONG[key].QUOTES),
style: "string"
}
};
}
var WS = "[\\x20\\x09\\x0D\\x0A]";
// Careful! Code mirror feeds one line at a time with no \n
// ... but otherwise comment is terminated by \n
var COMMENT = "#([^\\n\\r]*[\\n\\r]|[^\\n\\r]*$)";
var WS_OR_COMMENT_STAR = "(" + WS + "|(" + COMMENT + "))*";
var NIL = "\\(" + WS_OR_COMMENT_STAR + "\\)";
var ANON = "\\[" + WS_OR_COMMENT_STAR + "\\]";
var terminals = [
{
name: "WS",
regex: new RegExp("^" + WS + "+"),
style: "ws"
},
{
name: "COMMENT",
regex: new RegExp("^" + COMMENT),
style: "comment"
},
{
name: "true",
regex: new RegExp("^true", "i"),
style: "keyword"
},
{
name: "false",
regex: new RegExp("^false", "i"),
style: "keyword"
},
{
name: "IRI_REF",
regex: new RegExp("^" + IRI_REF),
style: "variable-3"
},
{
name: "VAR1",
regex: new RegExp("^" + VAR1),
style: "atom"
},
{
name: "VAR2",
regex: new RegExp("^" + VAR2),
style: "atom"
},
{
name: "@prefix",
regex: new RegExp("^@prefix", "i"),
style: "keyword"
},
{
name: "@base",
regex: new RegExp("^@base", "i"),
style: "keyword"
},
{
name: "LANGTAG",
regex: new RegExp("^" + LANGTAG),
style: "meta"
},
{
name: "DOUBLE",
regex: new RegExp("^" + DOUBLE),
style: "number"
},
{
name: "DECIMAL",
regex: new RegExp("^" + DECIMAL),
style: "number"
},
{
name: "INTEGER",
regex: new RegExp("^" + INTEGER),
style: "number"
},
{
name: "STRING_LITERAL_SINGLE_QUOTE",
regex: new RegExp("^" + STRING_LITERAL_SINGLE_QUOTE),
style: "string"
},
{
name: "STRING_LITERAL_QUOTE",
regex: new RegExp("^" + STRING_LITERAL_QUOTE),
style: "string"
},
// Enclosed comments won't be highlighted
{
name: "NIL",
regex: new RegExp("^" + NIL),
style: "punc"
},
// Enclosed comments won't be highlighted
{
name: "ANON",
regex: new RegExp("^" + ANON),
style: "punc"
},
{
name: "PNAME_LN",
regex: new RegExp("^" + PNAME_LN),
style: "string-2"
},
{
name: "PNAME_NS",
regex: new RegExp("^" + PNAME_NS),
style: "string-2"
},
{
name: "BLANK_NODE_LABEL",
regex: new RegExp("^" + BLANK_NODE_LABEL),
style: "string-2"
}
];
function getPossibles(symbol) {
var possibles = [], possiblesOb = ll1_table[symbol];
if (possiblesOb != undefined) {
for (var property in possiblesOb) {
possibles.push(property.toString());
}
} else {
possibles.push(symbol);
}
return possibles;
}
function tokenBase(stream, state) {
function nextToken() {
var consumed = null;
if (state.inLiteral) {
var closingQuotes = false;
//multi-line literal. try to parse contents.
consumed = stream.match(stringLiteralLongRegex[state.inLiteral].contents.regex, true, false);
if (consumed && consumed[0].length == 0) {
//try seeing whether we can consume closing quotes, to avoid stopping
consumed = stream.match(stringLiteralLongRegex[state.inLiteral].closing.regex, true, false);
closingQuotes = true;
}
if (consumed && consumed[0].length > 0) {
//some string content here.
var returnObj = {
quotePos: closingQuotes ? "end" : "content",
cat: STRING_LITERAL_LONG[state.inLiteral].CAT,
style: stringLiteralLongRegex[state.inLiteral].complete.style,
text: consumed[0],
start: stream.start
};
if (closingQuotes) state.inLiteral = false;
return returnObj;
}
}
//Multiline literals
for (var quoteType in stringLiteralLongRegex) {
consumed = stream.match(stringLiteralLongRegex[quoteType].quotes.regex, true, false);
if (consumed) {
var quotePos;
if (state.inLiteral) {
//end of literal. everything is fine
state.inLiteral = false;
quotePos = "end";
} else {
state.inLiteral = quoteType;
quotePos = "start";
}
return {
cat: STRING_LITERAL_LONG[quoteType].CAT,
style: stringLiteralLongRegex[quoteType].quotes.style,
text: consumed[0],
quotePos: quotePos,
start: stream.start
};
}
}
// Tokens defined by individual regular expressions
for (var i = 0; i < terminals.length; ++i) {
consumed = stream.match(terminals[i].regex, true, false);
if (consumed) {
return {
cat: terminals[i].name,
style: terminals[i].style,
text: consumed[0],
start: stream.start
};
}
}
// Keywords
consumed = stream.match(grammar.keywords, true, false);
if (consumed)
return {
cat: stream.current().toUpperCase(),
style: "keyword",
text: consumed[0],
start: stream.start
};
// Punctuation
consumed = stream.match(grammar.punct, true, false);
if (consumed)
return {
cat: stream.current(),
style: "punc",
text: consumed[0],
start: stream.start
};
// Token is invalid
// better consume something anyway, or else we're stuck
consumed = stream.match(/^.[A-Za-z0-9]*/, true, false);
return {
cat: "<invalid_token>",
style: "error",
text: consumed[0],
start: stream.start
};
}
function recordFailurePos() {
// tokenOb.style= "sp-invalid";
var col = stream.column();
state.errorStartPos = col;
state.errorEndPos = col + tokenOb.text.length;
}
// Some fake non-terminals are just there to have side-effect on state
function setSideConditions(topSymbol) {
if (topSymbol === "prefixID" || topSymbol === "sparqlPrefix") {
state.inPrefixDecl = true;
} else {
state.inPrefixDecl = false;
}
if (topSymbol === "storeProperty") {
state.storeProperty = true;
}
}
// CodeMirror works with one line at a time,
// but newline should behave like whitespace
// - i.e. a definite break between tokens (for autocompleter)
if (stream.pos == 0) state.possibleCurrent = state.possibleNext;
var tokenOb = nextToken();
if (tokenOb.cat == "<invalid_token>") {
// set error state, and
if (state.OK == true) {
state.OK = false;
recordFailurePos();
}
state.complete = false;
// alert("Invalid:"+tokenOb.text);
return tokenOb.style;
}
if (tokenOb.cat == "WS" || tokenOb.cat == "COMMENT" || (tokenOb.quotePos && tokenOb.quotePos != "end")) {
state.possibleCurrent = state.possibleNext;
return tokenOb.style;
}
// Otherwise, run the parser until the token is digested
// or failure
var finished = false;
var topSymbol;
var token = tokenOb.cat;
if (!tokenOb.quotePos || tokenOb.quotePos == "end") {
// Incremental LL1 parse
while (state.stack.length > 0 && token && state.OK && !finished) {
topSymbol = state.stack.pop();
if (topSymbol === 'var' && tokenOb.text) state.variables[tokenOb.text] = tokenOb.text;
if (!ll1_table[topSymbol]) {
// Top symbol is a terminal
if (topSymbol == token) {
if (state.inPrefixDecl) {
if (topSymbol === "PNAME_NS" && tokenOb.text.length > 0) {
state.currentPnameNs = tokenOb.text.slice(0, -1);
} else if (state.currentPnameNs !== undefined && tokenOb.text.length > 2) {
state.prefixes[state.currentPnameNs] = tokenOb.text.slice(1, -1);
//reset current pname ns
state.currentPnameNs = undefined;
}
}
// Matching terminals
// - consume token from input stream
finished = true;
// Check whether $ (end of input token) is poss next
// for everything on stack
var allNillable = true;
for (var sp = state.stack.length; sp > 0; --sp) {
var item = ll1_table[state.stack[sp - 1]];
if (!item || !item["$"]) allNillable = false;
}
state.complete = allNillable;
if (state.storeProperty && token.cat != "punc") {
state.lastProperty = tokenOb.text;
state.storeProperty = false;
}
//check whether a used prefix is actually defined
if (!state.inPrefixDecl && (token === "PNAME_NS" || token === "PNAME_LN")) {
var colonIndex = tokenOb.text.indexOf(":");
if (colonIndex >= 0) {
var prefNs = tokenOb.text.slice(0, colonIndex);
//avoid warnings for missing bif prefixes (yuck, virtuoso-specific)
if (!state.prefixes[prefNs] && ["bif", "xsd", "sql"].indexOf(prefNs) < 0) {
state.OK = false;
recordFailurePos();
state.errorMsg = "Prefix '" + prefNs + "' is not defined";
}
}
}
} else {
state.OK = false;
state.complete = false;
recordFailurePos();
}
} else {
// topSymbol is nonterminal
// - see if there is an entry for topSymbol
// and nextToken in table
var nextSymbols = ll1_table[topSymbol][token];
if (nextSymbols !== undefined) {
// Match - copy RHS of rule to stack
for (var i = nextSymbols.length - 1; i >= 0; --i) {
state.stack.push(nextSymbols[i]);
}
// Peform any non-grammatical side-effects
setSideConditions(topSymbol);
} else {
// No match in table - fail
state.OK = false;
state.complete = false;
recordFailurePos();
state.stack.push(topSymbol); // Shove topSymbol back on stack
}
}
}
}
if (!finished && state.OK) {
state.OK = false;
state.complete = false;
recordFailurePos();
}
if (state.possibleCurrent.indexOf("a") >= 0) {
state.lastPredicateOffset = tokenOb.start;
}
state.possibleCurrent = state.possibleNext;
state.possibleNext = getPossibles(state.stack[state.stack.length - 1]);
return tokenOb.style;
}
var indentTop = {
"*[,, object]": 3,
"*[(,),object]": 3,
object: 2,
objectList: 2,
storeProperty: 2,
predicateObjectList: 1,
"?[verb,objectList]": 1
};
var indentTable = {
"}": 1,
"]": 1,
")": 1,
"{": -1,
"(": -1,
"[": -1
// "*[;,?[or([verbPath,verbSimple]),objectList]]": 1,
};
function indent(state, textAfter) {
//just avoid we don't indent multi-line literals
if (state.inLiteral) return 0;
if (state.stack.length && state.stack[state.stack.length - 1] == "?[verb,objectList]") {
//we are after a semi-colon. I.e., nicely align this line with predicate position of previous line
return state.lastPredicateOffset;
} else {
var n = 0; // indent level
var i = state.stack.length - 1;
if (/^[\}\]\)]/.test(textAfter)) {
// Skip stack items until after matching bracket
var closeBracket = textAfter.substr(0, 1);
for (; i >= 0; --i) {
if (state.stack[i] == closeBracket) {
--i;
break;
}
}
} else {
// Consider nullable non-terminals if at top of stack
var dn = indentTop[state.stack[i]];
if (dn) {
n += dn;
--i;
}
}
for (; i >= 0; --i) {
var dn = indentTable[state.stack[i]];
if (dn) {
n += dn;
}
}
return n * config.indentUnit;
}
}
return {
token: tokenBase,
startState: function(base) {
return {
tokenize: tokenBase,
OK: true,
complete: grammar.acceptEmpty,
errorStartPos: null,
errorEndPos: null,
possibleCurrent: getPossibles(grammar.startSymbol),
possibleNext: getPossibles(grammar.startSymbol),
storeProperty: false,
lastProperty: "",
inLiteral: false,
stack: [grammar.startSymbol],
lastPredicateOffset: config.indentUnit,
prefixes: {},
variables: {}
};
},
indent: indent,
electricChars: "}])"
};
});
CodeMirror.defineMIME("text/turtle", "rdf11turtle");