dot-language-support
Version:
Parser and language service for graphviz (dot) files
1,789 lines (1,783 loc) • 104 kB
JavaScript
import * as lst from "vscode-languageserver-types";
import { Range, TextEdit } from "vscode-languageserver-types";
//#region src/core.ts
function createMapFromTemplate(template) {
const map = /* @__PURE__ */ new Map();
for (const key in template) if (key in template) map.set(key, template[key]);
return map;
}
//#endregion
//#region src/types.ts
const errorSource = {
Scan: 1,
Parse: 2,
Check: 4
};
const parseError = {
ExpectationFailed: 0,
TrailingData: 1,
FailedListParsing: 2
};
const scanError = {
ExpectationFailed: 0,
Unterminated: 1
};
const checkError = {
InvalidEdgeOperation: 0,
InvalidShapeName: 1
};
const diagnosticCategory = {
Error: 1,
Warning: 2,
Message: 3,
Suggestion: 4
};
const syntaxKind = {
Unknown: 0,
EndOfFileToken: 1,
NewLineTrivia: 2,
WhitespaceTrivia: 3,
HashCommentTrivia: 4,
SingleLineCommentTrivia: 5,
MultiLineCommentTrivia: 6,
CommaToken: 7,
SemicolonToken: 8,
PlusToken: 9,
OpenBraceToken: 10,
CloseBraceToken: 11,
OpenBracketToken: 12,
CloseBracketToken: 13,
ColonToken: 14,
EqualsToken: 15,
LessThan: 16,
GreaterThan: 17,
CompassNorthToken: 18,
CompassNorthEastToken: 19,
CompassEastToken: 20,
CompassSouthEastToken: 21,
CompassSouthToken: 22,
CompassSouthWestToken: 23,
CompassWestToken: 24,
CompassNorthWestToken: 25,
CompassCenterToken: 26,
UnderscoreToken: 27,
StringLiteral: 28,
HtmlIdentifier: 29,
TextIdentifier: 30,
QuotedTextIdentifier: 31,
NumericIdentifier: 32,
GraphKeyword: 33,
DigraphKeyword: 34,
NodeKeyword: 35,
EdgeKeyword: 36,
SubgraphKeyword: 37,
StrictKeyword: 38,
DirectedEdgeOp: 39,
UndirectedEdgeOp: 40,
DirectedGraph: 41,
UndirectedGraph: 42,
NodeStatement: 43,
EdgeStatement: 44,
AttributeStatement: 45,
IdEqualsIdStatement: 46,
SubGraph: 47,
SubGraphStatement: 48,
EdgeRhs: 49,
AttributeContainer: 50,
Assignment: 51,
NormalPortDeclaration: 52,
CompassPortDeclaration: 53,
NodeId: 54,
Count: 55,
FirstNode: 41,
CompassBegin: 18,
CompassEnd: 27,
LastKeyword: 38
};
/** reverse lookup because we cannot make syntaxKind an enum */
const syntaxKindNames = {
0: "Unknown",
1: "EndOfFileToken",
2: "NewLineTrivia",
3: "WhitespaceTrivia",
4: "HashCommentTrivia",
5: "SingleLineCommentTrivia",
6: "MultiLineCommentTrivia",
7: "CommaToken",
8: "SemicolonToken",
9: "PlusToken",
10: "OpenBraceToken",
11: "CloseBraceToken",
12: "OpenBracketToken",
13: "CloseBracketToken",
14: "ColonToken",
15: "EqualsToken",
16: "LessThan",
17: "GreaterThan",
18: "CompassNorthToken",
19: "CompassNorthEastToken",
20: "CompassEastToken",
21: "CompassSouthEastToken",
22: "CompassSouthToken",
23: "CompassSouthWestToken",
24: "CompassWestToken",
25: "CompassNorthWestToken",
26: "CompassCenterToken",
27: "UnderscoreToken",
28: "StringLiteral",
29: "HtmlIdentifier",
30: "TextIdentifier",
31: "QuotedTextIdentifier",
32: "NumericIdentifier",
33: "GraphKeyword",
34: "DigraphKeyword",
35: "NodeKeyword",
36: "EdgeKeyword",
37: "SubgraphKeyword",
38: "StrictKeyword",
39: "DirectedEdgeOp",
40: "UndirectedEdgeOp",
41: "DirectedGraph",
42: "UndirectedGraph",
43: "NodeStatement",
44: "EdgeStatement",
45: "AttributeStatement",
46: "IdEqualsIdStatement",
47: "SubGraph",
48: "SubGraphStatement",
49: "EdgeRhs",
50: "AttributeContainer",
51: "Assignment",
52: "NormalPortDeclaration",
53: "CompassPortDeclaration",
54: "NodeId",
55: "Count"
};
const syntaxNodeFlags = {
None: 0,
ContainsErrors: 2,
Synthesized: 4
};
const graphContext = {
None: 0,
Strict: 2,
Directed: 4,
Undirected: 8
};
const tokenFlags = {
None: 0,
Unterminated: 2,
PrecedingLineBreak: 4
};
const characterCodes = {
nullCharacter: 0,
maxAsciiCharacter: 127,
lineFeed: 10,
carriageReturn: 13,
lineSeparator: 8232,
paragraphSeparator: 8233,
nextLine: 133,
space: 32,
nonBreakingSpace: 160,
enQuad: 8192,
emQuad: 8193,
enSpace: 8194,
emSpace: 8195,
threePerEmSpace: 8196,
fourPerEmSpace: 8197,
sixPerEmSpace: 8198,
figureSpace: 8199,
punctuationSpace: 8200,
thinSpace: 8201,
hairSpace: 8202,
zeroWidthSpace: 8203,
narrowNoBreakSpace: 8239,
ideographicSpace: 12288,
mathematicalSpace: 8287,
ogham: 5760,
_: 95,
$: 36,
_0: 48,
_1: 49,
_2: 50,
_3: 51,
_4: 52,
_5: 53,
_6: 54,
_7: 55,
_8: 56,
_9: 57,
a: 97,
b: 98,
c: 99,
d: 100,
e: 101,
f: 102,
g: 103,
h: 104,
i: 105,
j: 106,
k: 107,
l: 108,
m: 109,
n: 110,
o: 111,
p: 112,
q: 113,
r: 114,
s: 115,
t: 116,
u: 117,
v: 118,
w: 119,
x: 120,
y: 121,
z: 122,
A: 65,
B: 66,
C: 67,
D: 68,
E: 69,
F: 70,
G: 71,
H: 72,
I: 73,
J: 74,
K: 75,
L: 76,
M: 77,
N: 78,
O: 79,
P: 80,
Q: 81,
R: 82,
S: 83,
T: 84,
U: 85,
V: 86,
W: 87,
X: 88,
Y: 89,
Z: 90,
ampersand: 38,
asterisk: 42,
at: 64,
backslash: 92,
backtick: 96,
bar: 124,
caret: 94,
closeBrace: 125,
closeBracket: 93,
closeParen: 41,
colon: 58,
comma: 44,
dot: 46,
doubleQuote: 34,
equals: 61,
exclamation: 33,
greaterThan: 62,
hash: 35,
lessThan: 60,
minus: 45,
openBrace: 123,
openBracket: 91,
openParen: 40,
percent: 37,
plus: 43,
question: 63,
semicolon: 59,
singleQuote: 39,
slash: 47,
tilde: 126,
backspace: 8,
formFeed: 12,
byteOrderMark: 65279,
tab: 9,
verticalTab: 11
};
//#endregion
//#region src/service/util.ts
function getStart(sourceFile, node) {
return getTokenPosOfNode(sourceFile, node);
}
function getTokenPosOfNode(sourceFile, node) {
if (nodeIsMissing(node)) return node.pos;
return skipTrivia(sourceFile.content, node.pos);
}
function nodeIsMissing(node) {
return node === void 0 ? true : node.pos === node.end && node.pos >= 0 && node.kind !== syntaxKind.EndOfFileToken;
}
function syntaxNodesToRanges(doc, sourceFile, nodes) {
return nodes.map((node) => syntaxNodeToRange(doc, sourceFile, node));
}
function syntaxNodeToRange(doc, sourceFile, node) {
const start = getStart(sourceFile, node);
return {
start: doc.positionAt(start),
end: doc.positionAt(node.end)
};
}
function escapeIdentifierText(text) {
if (text === "") return quote("");
if (text.includes("\"") || text.includes("\n")) return quote(text.replace(/"/, "\\\"").replace(/\n/, "\\\n"));
if (!isIdentifierStart(text.charCodeAt(0)) || text.includes(" ")) return quote(text);
return text;
}
const quote = (s) => `"${s}"`;
function assertNever(v) {
throw new Error(`Should not have reached this. Value: ${v ?? ""}`);
}
//#endregion
//#region src/scanner.ts
const textToToken = createMapFromTemplate({
digraph: syntaxKind.DigraphKeyword,
graph: syntaxKind.GraphKeyword,
edge: syntaxKind.EdgeKeyword,
node: syntaxKind.NodeKeyword,
strict: syntaxKind.StrictKeyword,
subgraph: syntaxKind.SubgraphKeyword,
n: syntaxKind.CompassNorthToken,
ne: syntaxKind.CompassNorthEastToken,
e: syntaxKind.CompassEastToken,
se: syntaxKind.CompassSouthEastToken,
s: syntaxKind.CompassSouthToken,
sw: syntaxKind.CompassSouthWestToken,
w: syntaxKind.CompassWestToken,
nw: syntaxKind.CompassNorthWestToken,
c: syntaxKind.CompassCenterToken,
"+": syntaxKind.PlusToken,
"=": syntaxKind.EqualsToken,
"->": syntaxKind.DirectedEdgeOp,
"--": syntaxKind.UndirectedEdgeOp,
"{": syntaxKind.OpenBraceToken,
"}": syntaxKind.CloseBraceToken,
"[": syntaxKind.OpenBracketToken,
"]": syntaxKind.CloseBracketToken,
";": syntaxKind.SemicolonToken,
":": syntaxKind.ColonToken,
_: syntaxKind.UnderscoreToken,
",": syntaxKind.CommaToken,
"<": syntaxKind.LessThan,
">": syntaxKind.GreaterThan
});
function makeReverseMap(source) {
const result = /* @__PURE__ */ new Map();
source.forEach((value, key) => {
result.set(value, key);
});
return result;
}
const tokenToText = makeReverseMap(textToToken);
function getTokenAsText(token) {
return tokenToText.get(token);
}
function getTextAsToken(token) {
return textToToken.get(token);
}
var DefaultScanner = class {
end;
pos;
startPos;
tokenPos;
token;
tokenValue;
tokenFlags;
isUnterminated;
text;
onError;
setText(newText, start = 0, length) {
this.text = newText || "";
this.end = length === void 0 ? this.text.length : start + length;
this.#setTextPos(start || 0);
}
setErrorCallback(cb) {
this.onError = cb;
}
#setTextPos(textPos) {
console.assert(textPos >= 0);
this.pos = textPos;
this.startPos = textPos;
this.tokenPos = textPos;
this.token = syntaxKind.Unknown;
this.tokenValue = void 0;
this.tokenFlags = tokenFlags.None;
}
scan(skipTrivia$1 = true) {
this.startPos = this.pos;
this.tokenFlags = tokenFlags.None;
this.isUnterminated = false;
while (true) {
this.tokenPos = this.pos;
if (this.pos >= this.end) return this.token = syntaxKind.EndOfFileToken;
let ch = this.text.charCodeAt(this.pos);
switch (ch) {
case characterCodes.lineFeed:
case characterCodes.carriageReturn:
this.tokenFlags |= tokenFlags.PrecedingLineBreak;
if (skipTrivia$1) {
this.pos++;
continue;
}
if (ch === characterCodes.carriageReturn && this.pos + 1 < this.end && this.text.charCodeAt(this.pos + 1) === characterCodes.lineFeed) this.pos += 2;
else this.pos++;
return this.token = syntaxKind.NewLineTrivia;
case characterCodes.tab:
case characterCodes.verticalTab:
case characterCodes.formFeed:
case characterCodes.space:
if (skipTrivia$1) {
this.pos++;
continue;
}
while (this.pos < this.end && this.#isWhiteSpaceSingleLine(this.text.charCodeAt(this.pos))) this.pos++;
return this.token = syntaxKind.WhitespaceTrivia;
case characterCodes.hash: {
const content = this.#scanHashCommentTrivia(skipTrivia$1);
if (skipTrivia$1) continue;
this.tokenValue = content;
return this.token = syntaxKind.HashCommentTrivia;
}
case characterCodes.slash:
if (this.pos + 1 < this.end) switch (this.text.charCodeAt(this.pos + 1)) {
case characterCodes.slash: {
const commentContent = this.#scanSingleLineCommentTrivia(skipTrivia$1);
if (skipTrivia$1) continue;
this.tokenValue = commentContent;
return this.token = syntaxKind.SingleLineCommentTrivia;
}
case characterCodes.asterisk: {
const commentContent = this.#scanMultiLineCommentTrivia(skipTrivia$1);
if (skipTrivia$1) continue;
this.tokenValue = commentContent;
return this.token = syntaxKind.MultiLineCommentTrivia;
}
}
this.#error("Unexpected \"/\". Did you mean to start a comment like \"/*\" or \"//\"? If you wanted to use it as an identifier, put it in double quotes.", scanError.ExpectationFailed);
++this.pos;
break;
case characterCodes.openBrace:
this.pos++;
return this.token = syntaxKind.OpenBraceToken;
case characterCodes.closeBrace:
this.pos++;
return this.token = syntaxKind.CloseBraceToken;
case characterCodes.openBracket:
this.pos++;
return this.token = syntaxKind.OpenBracketToken;
case characterCodes.closeBracket:
this.pos++;
return this.token = syntaxKind.CloseBracketToken;
case characterCodes.plus:
this.pos++;
return this.token = syntaxKind.PlusToken;
case characterCodes.equals:
this.pos++;
return this.token = syntaxKind.EqualsToken;
case characterCodes._0:
case characterCodes._1:
case characterCodes._2:
case characterCodes._3:
case characterCodes._4:
case characterCodes._5:
case characterCodes._6:
case characterCodes._7:
case characterCodes._8:
case characterCodes._9:
case characterCodes.dot:
this.tokenValue = this.#scanNumber();
return this.token = syntaxKind.NumericIdentifier;
case characterCodes.minus:
switch (this.text.charCodeAt(this.pos + 1)) {
case characterCodes.minus:
this.pos += 2;
return this.token = syntaxKind.UndirectedEdgeOp;
case characterCodes.greaterThan:
this.pos += 2;
return this.token = syntaxKind.DirectedEdgeOp;
case characterCodes._0:
case characterCodes._1:
case characterCodes._2:
case characterCodes._3:
case characterCodes._4:
case characterCodes._5:
case characterCodes._6:
case characterCodes._7:
case characterCodes._8:
case characterCodes._9:
case characterCodes.dot:
this.tokenValue = this.#scanNumber();
return this.token = syntaxKind.NumericIdentifier;
default: {
const chr = this.text.charAt(this.pos + 1);
this.#error(`Unexpected "${chr}". Did you mean to define an edge? Depending on the type of graph you are defining, use "->" or "--".`, scanError.ExpectationFailed);
break;
}
}
this.pos++;
break;
case characterCodes._:
this.pos++;
return this.token = syntaxKind.UnderscoreToken;
case characterCodes.semicolon:
this.pos++;
return this.token = syntaxKind.SemicolonToken;
case characterCodes.colon:
this.pos++;
return this.token = syntaxKind.ColonToken;
case characterCodes.comma:
this.pos++;
return this.token = syntaxKind.CommaToken;
case characterCodes.lessThan:
this.tokenValue = this.#scanHtml();
return this.token = syntaxKind.HtmlIdentifier;
case characterCodes.doubleQuote:
this.tokenValue = this.#scanString();
return this.token = syntaxKind.StringLiteral;
default: {
if (isIdentifierStart(ch)) {
this.pos++;
while (this.pos < this.end && isIdentifierPart(ch = this.text.charCodeAt(this.pos))) this.pos++;
const value = this.text.substring(this.tokenPos, this.pos);
this.tokenValue = value;
return this.token = this.#getIdentifierToken(value);
}
if (this.#isWhiteSpaceSingleLine(ch)) {
this.pos++;
continue;
}
const chr = this.text.charAt(this.pos);
this.#error(`Unexpected "${chr}". Did you mean to start an identifier? Node names cannot start with "${chr}".`, scanError.ExpectationFailed);
this.pos++;
break;
}
}
}
}
#error(message, sub, category = diagnosticCategory.Error, errPos = this.pos, length = 0) {
const cb = this.onError;
if (cb) {
const posSave = this.pos;
this.pos = errPos;
cb(message, category, sub, length);
this.pos = posSave;
}
}
#isWhiteSpaceSingleLine(ch) {
return ch === characterCodes.space || ch === characterCodes.tab || ch === characterCodes.verticalTab || ch === characterCodes.formFeed || ch === characterCodes.nonBreakingSpace || ch === characterCodes.nextLine || ch === characterCodes.ogham || ch >= characterCodes.enQuad && ch <= characterCodes.zeroWidthSpace || ch === characterCodes.narrowNoBreakSpace || ch === characterCodes.mathematicalSpace || ch === characterCodes.ideographicSpace || ch === characterCodes.byteOrderMark;
}
#isAtMultiLineCommentEnd(pos) {
return pos + 1 < this.end && this.text.charCodeAt(pos) === characterCodes.asterisk && this.text.charCodeAt(pos + 1) === characterCodes.slash;
}
#scanHashCommentTrivia(skip) {
++this.pos;
const start = this.pos;
while (this.pos < this.end && !isLineBreak(this.text.charCodeAt(this.pos))) this.pos++;
return skip ? void 0 : this.text.substring(start, this.pos);
}
#scanSingleLineCommentTrivia(skip) {
this.pos += 2;
const start = this.pos;
while (this.pos < this.end && !isLineBreak(this.text.charCodeAt(this.pos))) this.pos++;
return skip ? void 0 : this.text.substring(start, this.pos);
}
#scanMultiLineCommentTrivia(skip) {
this.pos += 2;
const start = this.pos;
while (this.pos < this.end && !this.#isAtMultiLineCommentEnd(this.pos)) this.pos++;
const commentEnd = this.pos;
if (this.#isAtMultiLineCommentEnd(this.pos)) this.pos += 2;
return skip ? void 0 : this.text.substring(start, commentEnd);
}
#scanHtml() {
this.pos++;
let result = "";
const start = this.pos;
let subTagsLevel = 0;
while (true) {
if (this.pos >= this.end) {
result += this.text.substring(start, this.pos);
this.tokenFlags |= tokenFlags.Unterminated;
this.isUnterminated = true;
this.#error("Unterminated html literal", scanError.Unterminated);
break;
}
const ch = this.text.charCodeAt(this.pos);
if (ch === characterCodes.lessThan) {
++subTagsLevel;
this.pos++;
continue;
}
if (ch === characterCodes.greaterThan) {
this.pos++;
console.assert(subTagsLevel >= 0);
if (subTagsLevel === 0) {
result += this.text.substring(start, this.pos);
break;
}
--subTagsLevel;
continue;
}
this.pos++;
}
return result;
}
#scanString(allowEscapes = true) {
const quote$1 = this.text.charCodeAt(this.pos);
this.pos++;
let result = "";
const start = this.pos;
let hasBackslash = false;
while (true) {
if (this.pos >= this.end) {
result += this.text.substring(start, this.pos);
this.tokenFlags |= tokenFlags.Unterminated;
this.isUnterminated = true;
this.#error("Unterminated string", scanError.Unterminated);
break;
}
const ch = this.text.charCodeAt(this.pos);
if (ch === characterCodes.backslash) hasBackslash = true;
else if (hasBackslash) hasBackslash = false;
else {
if (ch === quote$1) {
result += this.text.substring(start, this.pos);
this.pos++;
break;
}
if (isLineBreak(ch)) {
result += this.text.substring(start, this.pos);
this.tokenFlags |= tokenFlags.Unterminated;
this.isUnterminated = true;
this.#error("Unterminated string", scanError.Unterminated);
break;
}
}
this.pos++;
}
return result.replace(/\\"/g, "\"").replace(/\\(\r?\n)/g, "$1");
}
#scanNumber() {
let result = "";
let hadDot = false;
let hadMinus = false;
const start = this.pos;
while (true) {
switch (this.text.charCodeAt(this.pos)) {
case characterCodes._0:
case characterCodes._1:
case characterCodes._2:
case characterCodes._3:
case characterCodes._4:
case characterCodes._5:
case characterCodes._6:
case characterCodes._7:
case characterCodes._8:
case characterCodes._9: break;
case characterCodes.dot:
if (hadDot) {
result += this.text.substring(start, this.pos);
return result;
}
hadDot = true;
hadMinus = true;
break;
case characterCodes.minus:
if (this.pos !== start || hadMinus) {
result += this.text.substring(start, this.pos);
return result;
}
hadMinus = true;
break;
default:
result += this.text.substring(start, this.pos);
return result;
}
++this.pos;
}
}
#getIdentifierToken(tokenValue) {
const len = tokenValue.length;
if (len >= 4 && len <= 8) {
const ch = tokenValue.charCodeAt(0);
if (ch >= characterCodes.a && ch <= characterCodes.z || ch >= characterCodes.A && ch <= characterCodes.Z) {
const lowerCaseToken = tokenValue.toLowerCase();
const t = textToToken.get(lowerCaseToken);
if (t !== void 0) {
this.token = t;
return t;
}
}
}
return this.token = syntaxKind.TextIdentifier;
}
lookAhead(callback) {
return this.#speculationHelper(callback, true);
}
tryScan(callback) {
return this.#speculationHelper(callback, false);
}
#speculationHelper(callback, isLookahead) {
const savePos = this.pos;
const saveStartPos = this.startPos;
const saveTokenPos = this.tokenPos;
const saveToken = this.token;
const saveTokenValue = this.tokenValue;
const saveTokenFlags = this.tokenFlags;
const result = callback();
if (!result || isLookahead) {
this.pos = savePos;
this.startPos = saveStartPos;
this.tokenPos = saveTokenPos;
this.token = saveToken;
this.tokenValue = saveTokenValue;
this.tokenFlags = saveTokenFlags;
}
return result;
}
};
function isIdentifierStart(ch) {
return ch >= characterCodes.A && ch <= characterCodes.Z || ch >= characterCodes.a && ch <= characterCodes.z || ch >= characterCodes._0 && ch <= characterCodes._9 || ch === characterCodes._ || ch === characterCodes.lessThan || ch === characterCodes.doubleQuote;
}
function isIdentifierPart(ch) {
return ch >= characterCodes.A && ch <= characterCodes.Z || ch >= characterCodes.a && ch <= characterCodes.z || ch >= characterCodes._0 && ch <= characterCodes._9 || ch === characterCodes.$ || ch === characterCodes._ || ch > characterCodes.maxAsciiCharacter;
}
function skipTrivia(text, pos) {
while (true) {
switch (text.charCodeAt(pos)) {
case characterCodes.carriageReturn:
if (text.charCodeAt(pos + 1) === characterCodes.lineFeed) ++pos;
continue;
case characterCodes.lineFeed:
case characterCodes.tab:
case characterCodes.verticalTab:
case characterCodes.formFeed:
case characterCodes.space:
++pos;
continue;
case characterCodes.hash:
++pos;
while (pos < text.length) {
if (isLineBreak(text.charCodeAt(pos))) break;
++pos;
}
continue;
case characterCodes.slash:
if (pos + 1 < text.length) switch (text.charCodeAt(pos + 1)) {
case characterCodes.slash:
pos += 2;
while (pos < text.length) {
if (isLineBreak(text.charCodeAt(pos))) break;
++pos;
}
continue;
case characterCodes.asterisk:
pos += 2;
while (pos < text.length) {
if (text.charCodeAt(pos) === characterCodes.asterisk && text.charCodeAt(pos + 1) === characterCodes.slash) {
pos += 2;
break;
}
++pos;
}
continue;
}
break;
}
return pos;
}
}
function isLineBreak(ch) {
return ch === characterCodes.lineFeed || ch === characterCodes.carriageReturn;
}
//#endregion
//#region src/parser.ts
const parsingContext = {
None: 0,
StatementList: 1,
AttributeContainerList: 2,
AssignmentList: 3,
EdgeRhsList: 4,
QuotedTextIdentifierConcatenation: 5,
Count: 6
};
var Parser = class {
currentToken = syntaxKind.Unknown;
nodeCount;
identifiers;
identifierCount = 0;
sourceText;
scanner = new DefaultScanner();
currentNodeHasError;
currentContext;
diagnostics;
constructor() {
this.#resetState();
}
#resetState() {
this.sourceText = "";
this.scanner.setText(this.sourceText);
this.scanner.setErrorCallback(this.#scanError.bind(this));
this.identifierCount = 0;
this.identifiers = /* @__PURE__ */ new Set();
this.nodeCount = 0;
this.diagnostics = [];
this.currentNodeHasError = false;
this.currentContext = parsingContext.None;
}
#nextToken() {
this.currentToken = this.scanner.scan(true);
return this.currentToken;
}
#token() {
return this.currentToken;
}
#getLinesFromFile(sourceText) {
return sourceText.split(/\r?\n/);
}
parse(sourceText) {
this.sourceText = sourceText;
this.scanner.setText(this.sourceText);
this.#nextToken();
let graph;
if (this.#token() !== syntaxKind.EndOfFileToken) {
graph = this.#parseGraph();
if (this.#token() !== syntaxKind.EndOfFileToken) this.#parseErrorAtPosition(this.scanner.tokenPos, this.scanner.text.length - 1, "Content after the end of a graph declaration is invalid.", {
source: errorSource.Parse,
sub: parseError.TrailingData
});
}
const result = {
content: this.sourceText,
graph,
identifiers: this.identifiers,
diagnostics: this.diagnostics
};
this.#resetState();
return result;
}
#parseGraph() {
const strictToken = this.#parseOptionalToken(syntaxKind.StrictKeyword);
const keyword = this.#parseExpectedTokenOneOf(syntaxKind.DigraphKeyword, [syntaxKind.DigraphKeyword, syntaxKind.GraphKeyword]);
const kind = keyword === void 0 || keyword.kind === syntaxKind.DigraphKeyword ? syntaxKind.DirectedGraph : syntaxKind.UndirectedGraph;
const graphStart = strictToken ? strictToken.pos : keyword.pos;
const node = this.#createNode(kind, graphStart);
node.strict = strictToken;
node.keyword = keyword;
node.id = this.#isIdentifier() ? this.#parseIdentifier() : void 0;
this.#parseExpectedToken(syntaxKind.OpenBraceToken);
node.statements = this.#parseList(parsingContext.StatementList, () => this.#parseStatement());
this.#parseExpectedToken(syntaxKind.CloseBraceToken);
return this.#finishNode(node);
}
#parseIdentifier() {
let result;
const escapedIdTexts = [];
switch (this.#token()) {
case syntaxKind.TextIdentifier:
result = this.#parseTextIdentifier();
escapedIdTexts.push(result.text);
break;
case syntaxKind.StringLiteral:
result = this.#parseQuotedTextIdentifierConcatenation();
escapedIdTexts.push(...result.values.map((v) => v.text));
break;
case syntaxKind.HtmlIdentifier:
result = this.#parseHtmlIdentifier();
escapedIdTexts.push(result.htmlContent);
break;
case syntaxKind.NumericIdentifier:
result = this.#parseNumericIdentifier();
escapedIdTexts.push(result.text);
break;
default:
this.#reportExpectedError([syntaxKind.TextIdentifier]);
result = this.#createMissingNode(syntaxKind.TextIdentifier);
break;
}
for (const i of escapedIdTexts) this.#registerIdentifier(i);
return result;
}
#registerIdentifier(id) {
this.identifierCount++;
if (!this.identifiers.has(id)) this.identifiers.add(id);
}
#parseTextIdentifier() {
const node = this.#createNode(syntaxKind.TextIdentifier);
const text = this.scanner.tokenValue;
this.#nextToken();
if (text === void 0) throw "Satisfy type checker";
node.text = text;
return this.#finishNode(node);
}
#parseQuotedTextIdentifierConcatenation() {
const node = this.#createNode(syntaxKind.QuotedTextIdentifier);
node.values = this.#parseList(parsingContext.QuotedTextIdentifierConcatenation, () => this.#parseQuotedTextIdentifier(), true);
return this.#finishNode(node);
}
#parseQuotedTextIdentifier() {
const node = this.#createNode(syntaxKind.StringLiteral);
if (this.#token() === syntaxKind.PlusToken) this.#nextToken();
const text = this.scanner.tokenValue;
this.#nextToken();
if (text === void 0) throw "Satisfy type checker";
node.text = text;
return this.#finishNode(node);
}
#isQuotedStringFollowing() {
this.#nextToken();
return this.#token() === syntaxKind.StringLiteral;
}
#parseHtmlIdentifier() {
const node = this.#createNode(syntaxKind.HtmlIdentifier);
const text = this.scanner.tokenValue;
this.#nextToken();
if (text === void 0) throw "Satisfy type checker";
node.htmlContent = text;
return this.#finishNode(node);
}
#parseNumericIdentifier() {
const node = this.#createNode(syntaxKind.NumericIdentifier);
const text = this.scanner.tokenValue;
this.#nextToken();
if (text === void 0) throw "Satisfy type checker";
node.text = text;
node.value = Number(text);
return this.#finishNode(node);
}
#parseStatement() {
switch (this.#token()) {
case syntaxKind.GraphKeyword:
case syntaxKind.NodeKeyword:
case syntaxKind.EdgeKeyword: return this.#parseAttributeStatement();
case syntaxKind.OpenBraceToken:
case syntaxKind.SubgraphKeyword: {
const subgraph = this.#parseSubGraph();
if (this.#token() === syntaxKind.SemicolonToken) {
const subgraphStatement$1 = this.#createNode(syntaxKind.SubGraphStatement, subgraph.pos);
subgraphStatement$1.subgraph = subgraph;
subgraphStatement$1.terminator = this.#parseExpectedToken(syntaxKind.SemicolonToken);
return this.#finishNode(subgraphStatement$1);
}
if (this.#isEdgeOp()) return this.#parseEdgeStatement(subgraph);
const subgraphStatement = this.#createNode(syntaxKind.SubGraphStatement, subgraph.pos);
subgraphStatement.subgraph = subgraph;
return this.#finishNode(subgraphStatement);
}
default: {
if (!this.#isIdentifier) debugger;
if (this.#lookAhead(() => this.#isIdEqualsIdStatement())) return this.#parseIdEqualsIdStatement();
const ns = this.#parseNodeStatement();
if (ns.terminator !== void 0 || ns.attributes.length !== 0) return ns;
if (this.#isEdgeOp()) return this.#parseEdgeStatement(ns.id);
return ns;
}
}
}
#parseAttributeStatement() {
switch (this.#token()) {
case syntaxKind.GraphKeyword:
case syntaxKind.NodeKeyword:
case syntaxKind.EdgeKeyword: {
const node = this.#createNode(syntaxKind.AttributeStatement);
node.subject = this.#parseTokenNode();
if (this.#token() === syntaxKind.OpenBracketToken) node.attributes = this.#parseList(parsingContext.AttributeContainerList, () => this.#parseAttributeContainer());
else {
this.#reportExpectedError([syntaxKind.OpenBracketToken]);
const missingStatement = this.#createMissingNode(syntaxKind.AttributeStatement);
missingStatement.attributes = this.#createNodeArray([this.#createMissingNode(syntaxKind.AttributeContainer)], this.scanner.tokenPos, this.scanner.tokenPos);
}
node.terminator = this.#parseOptionalToken(syntaxKind.SemicolonToken);
return this.#finishNode(node);
}
default: throw "This should never happen";
}
}
#parseAttributeContainer() {
if (this.#token() !== syntaxKind.OpenBracketToken) debugger;
const node = this.#createNode(syntaxKind.AttributeContainer);
node.openBracket = this.#parseExpectedToken(syntaxKind.OpenBracketToken);
if (this.#isIdentifier() && this.#lookAhead(() => this.#isAssignmentStart())) node.assignments = this.#parseList(parsingContext.AssignmentList, () => this.#parseAssignment());
else node.assignments = this.#createEmptyArray();
node.closeBracket = this.#parseExpectedToken(syntaxKind.CloseBracketToken);
return this.#finishNode(node);
}
#isAssignmentStart() {
if (!this.#isIdentifier) debugger;
this.#nextToken();
return this.#token() === syntaxKind.EqualsToken;
}
#parseIdEqualsIdStatement() {
if (!this.#isIdentifier) debugger;
const leftIdentifier = this.#parseIdentifier();
const node = this.#createNode(syntaxKind.IdEqualsIdStatement, leftIdentifier.pos);
node.leftId = leftIdentifier;
if (this.#token() !== syntaxKind.EqualsToken) debugger;
this.#parseExpectedToken(syntaxKind.EqualsToken);
node.rightId = this.#parseIdentifier();
node.terminator = this.#parseOptionalToken(syntaxKind.SemicolonToken);
return this.#finishNode(node);
}
#isIdEqualsIdStatement() {
if (!this.#isIdentifier) debugger;
this.#nextToken();
return this.#token() === syntaxKind.EqualsToken;
}
#parseNodeStatement() {
if (!this.#isIdentifier) debugger;
const node = this.#createNode(syntaxKind.NodeStatement);
node.id = this.#parseNodeId();
if (this.#token() === syntaxKind.OpenBracketToken) node.attributes = this.#parseList(parsingContext.AttributeContainerList, () => this.#parseAttributeContainer());
else node.attributes = this.#createEmptyArray();
node.terminator = this.#parseOptionalToken(syntaxKind.SemicolonToken);
return this.#finishNode(node);
}
#parseEdgeStatement(precedingItem) {
console.assert(precedingItem.kind === syntaxKind.SubGraph || precedingItem.kind === syntaxKind.NodeId);
console.assert(precedingItem.pos !== void 0);
if (!this.#isEdgeOp()) debugger;
const node = this.#createNode(syntaxKind.EdgeStatement, precedingItem.pos);
node.source = precedingItem;
node.rhs = this.#parseList(parsingContext.EdgeRhsList, () => this.#parseEdgeRhs());
if (this.#token() === syntaxKind.OpenBracketToken) node.attributes = this.#parseList(parsingContext.AttributeContainerList, () => this.#parseAttributeContainer());
else node.attributes = this.#createEmptyArray();
node.terminator = this.#parseOptionalToken(syntaxKind.SemicolonToken);
return this.#finishNode(node);
}
#parseEdgeRhs() {
const node = this.#createNode(syntaxKind.EdgeRhs);
node.operation = this.#parseExpectedTokenOneOf(syntaxKind.DirectedEdgeOp, [syntaxKind.DirectedEdgeOp, syntaxKind.UndirectedEdgeOp]);
switch (this.#token()) {
case syntaxKind.SubgraphKeyword:
case syntaxKind.OpenBraceToken:
node.target = this.#parseSubGraph();
break;
default:
node.target = this.#parseNodeId();
break;
}
return this.#finishNode(node);
}
#createMissingNode(kind) {
const result = this.#createNode(kind);
if (isIdentifierNode(result)) switch (result.kind) {
case syntaxKind.QuotedTextIdentifier: {
const literal = this.#createNode(syntaxKind.StringLiteral);
literal.text = "";
result.values = this.#createNodeArray([literal], result.pos, result.pos);
break;
}
case syntaxKind.HtmlIdentifier:
result.htmlContent = "";
break;
case syntaxKind.TextIdentifier:
case syntaxKind.NumericIdentifier:
result.text = "";
break;
}
return this.#finishNode(result);
}
#parseAssignment() {
if (!this.#isIdentifier) debugger;
const node = this.#createNode(syntaxKind.Assignment);
node.leftId = this.#parseIdentifier();
this.#parseExpectedToken(syntaxKind.EqualsToken);
node.rightId = this.#parseIdentifier();
let terminator = this.#parseOptionalToken(syntaxKind.CommaToken);
if (terminator === void 0) terminator = this.#parseOptionalToken(syntaxKind.SemicolonToken);
node.terminator = terminator;
return this.#finishNode(node);
}
#parseSubGraph() {
console.assert(this.#token() === syntaxKind.SubgraphKeyword || this.#token() === syntaxKind.OpenBraceToken);
const subGraph = this.#parseOptionalToken(syntaxKind.SubgraphKeyword);
const subGraphStart = subGraph !== void 0 ? subGraph.pos : void 0;
const node = this.#createNode(syntaxKind.SubGraph, subGraphStart);
node.id = subGraph !== void 0 && this.#isIdentifier() ? this.#parseIdentifier() : void 0;
this.#parseExpectedToken(syntaxKind.OpenBraceToken);
node.statements = this.#parseList(parsingContext.StatementList, () => this.#parseStatement());
this.#parseExpectedToken(syntaxKind.CloseBraceToken);
return this.#finishNode(node);
}
#parseNodeId() {
if (!this.#isIdentifier) debugger;
const node = this.#createNode(syntaxKind.NodeId);
node.id = this.#parseIdentifier();
node.port = this.#token() === syntaxKind.ColonToken ? this.#parsePortDeclaration() : void 0;
return this.#finishNode(node);
}
#parseCompassPortDeclaration() {
console.assert(this.#token() === syntaxKind.ColonToken);
const node = this.#createNode(syntaxKind.CompassPortDeclaration);
node.colon = this.#parseTokenNode();
node.compassPt = this.#parseTokenNode();
return this.#finishNode(node);
}
#parseNormalPortDeclaration() {
console.assert(this.#token() === syntaxKind.ColonToken);
const node = this.#createNode(syntaxKind.NormalPortDeclaration);
node.colon = this.#parseTokenNode();
node.id = this.#parseIdentifier();
node.compassPt = this.#token() === syntaxKind.ColonToken ? this.#parseCompassPortDeclaration() : void 0;
return this.#finishNode(node);
}
#parsePortDeclaration() {
console.assert(this.#token() === syntaxKind.ColonToken);
if (this.#lookAhead(() => this.#isCompassPort())) return this.#parseCompassPortDeclaration();
return this.#parseNormalPortDeclaration();
}
#isCompassPort() {
console.assert(this.#token() === syntaxKind.ColonToken);
if (this.#token() !== syntaxKind.ColonToken) return false;
this.#nextToken();
return this.#isCompassPortKind(this.#token());
}
#parseList(context, parseElement, atLeastOne = false) {
const saveParsingContext = this.currentContext;
this.currentContext |= 1 << context;
let isListTerminated = atLeastOne ? false : this.#isListTerminator(context);
const startPos = this.scanner.startPos;
const elements = [];
while (!isListTerminated) {
if (this.#isListElement(context, false)) {
const element = parseElement();
elements.push(element);
isListTerminated = this.#isListTerminator(context);
continue;
}
if (this.#abortListParsing(context)) break;
}
this.currentContext = saveParsingContext;
return this.#createNodeArray(elements, startPos);
}
#getContextParseError(context) {
switch (context) {
case parsingContext.StatementList: return "Assignment, node definition, graph/node/edge attribute or edge definition expected.";
case parsingContext.AssignmentList: return "Assignment expected.";
case parsingContext.EdgeRhsList: return "Edge operation expected.";
case parsingContext.QuotedTextIdentifierConcatenation: return "Quoted identifier expected";
case parsingContext.AttributeContainerList: return "Attribute marker expected.";
case parsingContext.None: return "Wat, no parsing context";
case parsingContext.Count: return "Wat, 'Count' parsing context";
default: return assertNever(context);
}
}
#isInSomeParsingContext() {
for (let ctx = 0; ctx < parsingContext.Count; ctx++) if (this.currentContext & 1 << ctx) {
if (this.#isListElement(ctx, true) || this.#isListTerminator(ctx)) return true;
}
return false;
}
#abortListParsing(context) {
this.#parseErrorAtCurrentToken(this.#getContextParseError(context), parseError.FailedListParsing);
if (this.#isInSomeParsingContext()) return true;
this.#nextToken();
return false;
}
#isListElement(context, _inErrorRecovery) {
switch (context) {
case parsingContext.AssignmentList: return this.#isIdentifier();
case parsingContext.AttributeContainerList: return this.#token() === syntaxKind.OpenBracketToken;
case parsingContext.EdgeRhsList: return this.#token() === syntaxKind.DirectedEdgeOp || this.#token() === syntaxKind.UndirectedEdgeOp;
case parsingContext.QuotedTextIdentifierConcatenation: return this.#token() === syntaxKind.StringLiteral || this.#token() === syntaxKind.PlusToken;
case parsingContext.StatementList: return this.#isIdentifier() || this.#token() === syntaxKind.SubgraphKeyword || this.#token() === syntaxKind.OpenBraceToken || this.#token() === syntaxKind.GraphKeyword || this.#token() === syntaxKind.EdgeKeyword || this.#token() === syntaxKind.NodeKeyword;
default: throw "This should never happen";
}
}
#isListTerminator(context) {
const token = this.#token();
if (token === syntaxKind.EndOfFileToken) return true;
switch (context) {
case parsingContext.StatementList: return token === syntaxKind.CloseBraceToken;
case parsingContext.AttributeContainerList: return token !== syntaxKind.OpenBracketToken;
case parsingContext.AssignmentList: return token === syntaxKind.CloseBracketToken;
case parsingContext.EdgeRhsList: return token !== syntaxKind.DirectedEdgeOp && token !== syntaxKind.UndirectedEdgeOp;
case parsingContext.QuotedTextIdentifierConcatenation: return token !== syntaxKind.PlusToken;
default: throw "Unsupported parsing context";
}
}
#createEmptyArray() {
const startPos = this.scanner.startPos;
return this.#createNodeArray([], startPos);
}
#finishNode(node, end) {
node.end = end === void 0 ? this.scanner.startPos : end;
if (this.currentNodeHasError) {
this.currentNodeHasError = false;
node.flags |= syntaxNodeFlags.ContainsErrors;
}
return node;
}
#createNode(kind, pos) {
this.nodeCount++;
const p = pos !== void 0 && pos >= 0 ? pos : this.scanner.startPos;
if (isNodeKind(kind) || kind === syntaxKind.Unknown) return newNode(kind, p, p);
return isIdentifier(kind) ? newIdentifier(kind, p, p) : newToken(kind, p, p);
}
#createNodeArray(elements, pos, end) {
const length = elements.length;
const array = length >= 1 && length <= 4 ? elements.slice() : elements;
array.pos = pos;
array.end = end === void 0 ? this.scanner.startPos : end;
return array;
}
#parseTokenNode() {
const node = this.#createNode(this.#token());
this.#nextToken();
return this.#finishNode(node);
}
#getLastError(diagnostics) {
return diagnostics && diagnostics.length > 0 ? diagnostics[diagnostics.length - 1] : void 0;
}
#parseErrorAtPosition(start, end, message, code) {
const ds = this.diagnostics;
const lastError = this.#getLastError(ds);
if (!lastError || start !== lastError.start) ds.push({
category: diagnosticCategory.Error,
start,
end,
message,
code
});
this.currentNodeHasError = true;
}
#parseErrorAtCurrentToken(message, sub) {
const error = {
source: errorSource.Parse,
sub
};
return this.#parseErrorAtPosition(this.scanner.tokenPos, this.scanner.pos, message, error);
}
#scanError(message, _category, sub, length) {
const errorPos = this.scanner.pos;
const err = {
source: errorSource.Scan,
sub
};
this.#parseErrorAtPosition(errorPos, errorPos + length, message, err);
}
#reportExpectedError(expectedKinds) {
const found = this.#isIdentifier() ? "identifier" : this.#token() === syntaxKind.EndOfFileToken ? "end of file" : `"${getTokenAsText(this.#token())}"`;
const expected = expectedKinds.map((k) => {
if (isIdentifier(k)) return "identifier";
if (k === syntaxKind.EndOfFileToken) return "end of file";
return `"${getTokenAsText(k)}"`;
});
const lastExpected = expected.pop();
const expectedJoined = expected.join(", ");
const msg = expected.length > 0 ? `Expected ${expectedJoined} or ${lastExpected} but found ${found} instead.` : `Expected ${lastExpected} but found ${found} instead.`;
this.#parseErrorAtCurrentToken(msg, parseError.ExpectationFailed);
}
#parseExpectedOneOf(...kinds) {
if (kinds.length < 2) {
console.assert(false);
debugger;
}
for (const kind of kinds) if (this.#token() === kind) {
this.#nextToken();
return true;
}
this.#reportExpectedError(kinds);
return false;
}
#parseExpectedTokenOneOf(fallback, kinds) {
if (kinds.length < 2) {
console.assert(false);
debugger;
}
for (const kind of kinds) if (this.#token() === kind) {
const node = this.#createNode(this.#token());
this.#nextToken();
return this.#finishNode(node);
}
this.#reportExpectedError(kinds);
return this.#createMissingNode(fallback);
}
#parseExpectedToken(kind) {
const tokenNode = this.#parseOptionalToken(kind);
if (tokenNode !== void 0) return tokenNode;
this.#reportExpectedError([kind]);
return this.#createMissingNode(kind);
}
#parseExpected(kind) {
const res = this.#parseOptional(kind);
if (!res) this.#reportExpectedError([kind]);
return res;
}
#parseOptionalToken(t) {
if (this.#token() === t) return this.#parseTokenNode();
}
#parseOptional(t) {
if (this.#token() === t) {
this.#nextToken();
return true;
}
return false;
}
#isEdgeOp() {
switch (this.#token()) {
case syntaxKind.DirectedEdgeOp:
case syntaxKind.UndirectedEdgeOp: return true;
default: return false;
}
}
#isIdentifier() {
switch (this.#token()) {
case syntaxKind.TextIdentifier:
case syntaxKind.NumericIdentifier:
case syntaxKind.StringLiteral:
case syntaxKind.HtmlIdentifier: return true;
default: return false;
}
}
#isCompassPortKind(kind) {
return kind >= syntaxKind.CompassCenterToken && kind <= syntaxKind.CompassEnd;
}
#speculationHelper(callback, isLookAhead) {
const saveToken = this.#token();
const saveDiagnosticsLength = this.diagnostics.length;
const result = isLookAhead ? this.scanner.lookAhead(callback) : this.scanner.tryScan(callback);
if (!result || isLookAhead) {
this.currentToken = saveToken;
this.diagnostics.length = saveDiagnosticsLength;
}
return result;
}
/** Invokes the provided callback then unconditionally restores the parser to the state it
* was in immediately prior to invoking the callback. The result of invoking the callback
* is returned from this function.
*/
#lookAhead(callback) {
return this.#speculationHelper(callback, true);
}
/** Invokes the provided callback. If the callback returns something falsy, then it restores
* the parser to the state it was in immediately prior to invoking the callback. If the
* callback returns something truthy, then the parser state is not rolled back. The result
* of invoking the callback is returned from this function.
*/
#tryParse(callback) {
return this.#speculationHelper(callback, false);
}
};
function newNode(kind, pos, end) {
return {
kind,
flags: syntaxNodeFlags.None,
end,
pos,
parent: void 0
};
}
const newIdentifier = newNode;
const newToken = newNode;
function isNodeKind(kind) {
return kind >= syntaxKind.FirstNode;
}
function isIdentifier(kind) {
return kind === syntaxKind.HtmlIdentifier || kind === syntaxKind.NumericIdentifier || kind === syntaxKind.TextIdentifier || kind === syntaxKind.QuotedTextIdentifier;
}
function isIdentifierNode(node) {
return isIdentifier(node.kind);
}
//#endregion
//#region src/service/languageFacts.ts
const shapes = Object.freeze([
"box",
"polygon",
"ellipse",
"oval",
"circle",
"point",
"egg",
"triangle",
"plaintext",
"plain",
"diamond",
"trapezium",
"parallelogram",
"house",
"pentagon",
"hexagon",
"septagon",
"octagon",
"doublecircle",
"doubleoctagon",
"tripleoctagon",
"invtriangle",
"invtrapezium",
"invhouse",
"Mdiamond",
"Msquare",
"Mcircle",
"record",
"rect",
"rectangle",
"square",
"star",
"none",
"underline",
"cylinder",
"note",
"tab",
"folder",
"box3d",
"component",
"promoter",
"cds",
"terminator",
"utr",
"primersite",
"restrictionsite",
"fivepoverhang",
"threepoverhang",
"noverhang",
"assembly",
"signature",
"insulator",
"ribosite",
"rnastab",
"proteasesite",
"proteinstab",
"rpromoter",
"rarrow",
"larrow",
"lpromoter"
]);
const edgeAttributes = Object.freeze([
"URL",
"arrowhead",
"arrowsize",
"arrowtail",
"color",
"colorscheme",
"comment",
"constraint",
"decorate",
"dir",
"edgeURL",
"edgehref",
"edgetarget",
"edgetooltip",
"fillcolor",
"fontcolor",
"fontname",
"fontsize",
"headURL",
"head_lp",
"headclip",
"headhref",
"headlabel",
"headport",
"headtarget",
"headtooltip",
"href",
"id",
"label",
"labelURL",
"labelangle",
"labeldistance",
"labelfloat",
"labelfontcolor",
"labelfontname",
"labelfontsize",
"labelhref",
"labeltarget",
"labeltooltip",
"layer",
"len",
"lhead",
"lp",
"ltail",
"minlen",
"nojustify",
"penwidth",
"pos",
"samehead",
"sametail",
"showboxes",
"style",
"tailURL",
"tail_lp",
"tailclip",
"tailhref",
"taillabel",
"tailport",
"tailtarget",
"tailtooltip",
"target",
"tooltip",
"weight",
"xlabel",
"xlp"
]);
const nodeAttributes = Object.freeze([
"URL",
"area",
"color",
"colorscheme",
"comment",
"distortion",
"fillcolor",
"fixedsize",
"fontcolor",
"fontname",
"fontsize",
"gradientangle",
"group",
"height",
"href",
"id",
"image",
"imagepos",
"imagescale",
"label",
"labelloc",
"layer",
"margin",
"nojustify",
"ordering",
"orientation",
"penwidth",
"peripheries",
"pin",
"pos",
"rects",
"regular",
"root",
"samplepoints",
"shape",
"shapefile",
"showboxes",
"sides",
"skew",
"sortv",
"style",
"target",
"tooltip",
"vertices",
"width",
"xlabel",
"xlp",
"z"
]);
const graphAttributes = Object.freeze([
"Damping",
"K",
"URL",
"_background",
"bb",
"bgcolor",
"center",
"charset",
"clusterrank",
"colorscheme",
"comment",
"compound",
"concentrate",
"defaultdist",
"dim",
"dimen",
"diredgeconstraints",
"dpi",
"epsilon",
"esep",
"fontcolor",
"fontname",
"fontnames",
"fontpath",
"fontsize",
"forcelabels",
"gradientangle",
"href",
"id",
"imagepath",
"inputscale",
"label",
"label_scheme",
"labeljust",
"labelloc",
"landscape",
"layerlistsep",
"layers",
"layerselect",
"layersep",
"layout",
"levels",
"levelsgap",
"lheight",
"lp",
"lwidth",
"margin",
"maxiter",
"mclimit",
"mindist",
"mode",
"model",
"mosek",
"newrank",
"nodesep",
"nojustify",
"normalize",
"notranslate",
"nslimit",
"nslimit1",
"ordering",
"orientation",
"outputorder",
"overlap",
"overlap_scaling",
"overlap_shrink",
"pack",
"packmode",
"pad",
"page",
"pagedir",
"quadtree",
"quantum",
"rankdir",
"ranksep",
"ratio",
"remincross",
"repulsiveforce",
"resolution",
"root",
"rotate",
"rotation",
"scale",
"searchsize",
"sep",
"showboxes",
"size",
"smoothing",
"sortv",
"splines",
"start",
"style",
"stylesheet",
"target",
"truecolor",
"viewport",
"voro_margin",
"xdotversion"
]);
const clusterAttributes = Object.freeze([
"K",
"URL",
"area",
"bgcolor",
"color",
"colorscheme",
"fillcolor",
"fontcolor",
"fontname",
"fontsize",
"gradientangle",
"href",
"id",
"label",
"labeljust",
"labelloc",
"layer",
"lheight",
"lp",
"lwidth",
"margin",
"nojustify",
"pencolor",
"penwidth",
"peripheries",
"sortv",
"style",
"target",
"tooltip"
]);
const attributes = Array.from(new Set([
...nodeAttributes,
...edgeAttributes,
...graphAttributes,
...clusterAttributes
])).sort();
/**
* We only support the X11 color scheme
*/
const colors = Object.freeze({
aliceblue: "#f0f8ff",
antiquewhite: "#faebd7",
antiquewhite1: "#ffefdb",
antiquewhite2: "#eedfcc",
antiquewhite3: "#cdc0b0",
antiquewhite4: "#8b8378",
aqua: "#00ffff",
aquamarine: "#7fffd4",
aquamarine1: "#7fffd4",
aquamarine2: "#76eec6",
aquamarine3: "#66cdaa",
aquamarine4: "#458b74",
azure: "#f0ffff",
azure1: "#f0ffff",
azure2: "#e0eeee",
azure3: "#c1cdcd",
azure4: "#838b8b",
beige: "#f5f5dc",
bisque: "#ffe4c4",
bisque1: "#ffe4c4",
bisque2: "#eed5b7",
bisque3: "#cdb79e",
bisque4: "#8b7d6b",
black: "#000000",
blanchedalmond: "#ffebcd",
blue: "#0000ff",
blue1: "#0000ff",
blue2: "#0000ee",
blue3: "#0000cd",
blue4: "#00008b",
blueviolet: "#8a2be2",
brown: "#a52a2a",
brown1: "#ff4040",
brown2: "#ee3b3b",
brown3: "#cd3333",
brown4: "#8b2323",
burlywood: "#deb887",
burlywood1: "#ffd39b",
burlywood2: "#eec591",
burlywood3: "#cdaa7d",
burlywood4: "#8b7355",
cadetblue: "#5f9ea0",
cadetblue1: "#98f5ff",
cadetblue2: "#8ee5ee",
cadetblue3: "#7ac5cd",
cadetblue4: "#53868b",
chartreuse: "#7fff00",
chartreuse1: "#7fff00",
chartreuse2: "#76ee00",
chartreuse3: "#66cd00",
chartreuse4: "#458b00",
chocolate: "#d2691e",
chocolate1: "#ff7f24",
chocolate2: "#ee7621",
chocolate3: "#cd661d",
chocolate4: "#8b4513",
coral: "#ff7f50",
coral1: "#ff7256",
coral2: "#ee6a50",
coral3: "#cd5b45",
coral4: "#8b3e2f",
cornflowerblue: "#6495ed",
cornsilk: "#fff8dc",
cornsilk1: "#fff8dc",
cornsilk2: "#eee8cd",
cornsilk3: "#cdc8b1",
cornsilk4: "#8b8878",
crimson: "#dc143c",
cyan: "#00ffff",
cyan1: "#00ffff",
cyan2: "#00eeee",
cyan3: "#00cdcd",
cyan4: "#008b8b",
darkblue: "#00008b",
darkcyan: "#008b8b",
darkgoldenrod: "#b8860b",
darkgoldenrod1: "#ffb90f",
darkgoldenrod2: "#eead0e",
darkgoldenrod3: "#cd950c",
darkgoldenrod4: "#8b6508",
darkgray: "#a9a9a9