shared/analyzers/diagram.analyzer.ts
shared/analyzers/diagram.analyzer.ts is a file in GovLab Extension Host. 149 lines of code and 40 definitions.
import {
DIAGRAM_EDGE_CHARACTERS,
DIAGRAM_GROUP_KEYWORD,
DIAGRAM_HEADERS,
DIAGRAM_IDENTIFIER_CHARACTERS,
DIAGRAM_KEYWORDS,
DIAGRAM_SHAPE_OPENERS,
} from "../manifests/diagram.manifest.ts";
import type { ReservedNodeId } from "../../types/diagram.types.ts";
const LINE_END = "\n";
const SPACE = " ";
const QUOTE = '"';
const LABEL_BAR = "|";
const OPEN_CLOSE: ReadonlyMap<string, string> = new Map([
["[", "]"],
["(", ")"],
["{", "}"],
]);
interface Token {
readonly end: number;
readonly start: number;
readonly word: string;
}
const skipQuoted = function skipQuoted(line: string, from: number): number {
const close = line.indexOf(QUOTE, from + 1);
return close === -1 ? line.length : close + 1;
};
const skipBracket = function skipBracket(line: string, from: number, close: string): number {
let depth = 0;
let at = from;
while (at < line.length) {
const char = line.charAt(at);
if (char === QUOTE) {
at = skipQuoted(line, at);
continue;
}
if (OPEN_CLOSE.has(char)) {
depth += 1;
}
if (char === close) {
depth -= 1;
if (depth === 0) {
return at + 1;
}
}
at += 1;
}
return line.length;
};
const skipLabel = function skipLabel(line: string, from: number): number {
const close = line.indexOf(LABEL_BAR, from + 1);
return close === -1 ? line.length : close + 1;
};
const skipInlineLabel = function skipInlineLabel(line: string, from: number): number {
let at = from;
while (at < line.length) {
const char = line.charAt(at);
const next = line.charAt(at + 1);
if (DIAGRAM_EDGE_CHARACTERS.has(char) && DIAGRAM_EDGE_CHARACTERS.has(next)) {
return at;
}
at += 1;
}
return line.length;
};
const ARROW_HEAD = ">";
const LABEL_OPENER_LENGTH = 2;
const skipEdge = function skipEdge(line: string, from: number): number {
let at = from;
while (at < line.length && DIAGRAM_EDGE_CHARACTERS.has(line.charAt(at))) {
at += 1;
}
const run = line.slice(from, at);
if (run.endsWith(ARROW_HEAD) || run.length !== LABEL_OPENER_LENGTH) {
return at;
}
const rest = line.slice(at).trimStart();
const opensLabel = rest.length > 0 && DIAGRAM_IDENTIFIER_CHARACTERS.includes(rest.charAt(0));
if (!opensLabel) {
return at;
}
const labelEnd = skipInlineLabel(line, line.length - rest.length);
return labelEnd >= line.length ? labelEnd : skipEdge(line, labelEnd);
};
const readToken = function readToken(line: string, from: number): Token {
let at = from;
while (at < line.length && DIAGRAM_IDENTIFIER_CHARACTERS.includes(line.charAt(at))) {
at += 1;
}
return { end: at, start: from, word: line.slice(from, at) };
};
const groupIdEnd = function groupIdEnd(line: string, from: number): number {
const rest = line.slice(from).trimStart();
return readToken(line, line.length - rest.length).end;
};
const nodeTokensOf = function nodeTokensOf(line: string): Token[] {
const tokens: Token[] = [];
let at = 0;
while (at < line.length) {
const char = line.charAt(at);
const close = OPEN_CLOSE.get(char);
if (close !== undefined) {
at = skipBracket(line, at, close);
} else if (char === QUOTE) {
at = skipQuoted(line, at);
} else if (char === LABEL_BAR) {
at = skipLabel(line, at);
} else if (DIAGRAM_EDGE_CHARACTERS.has(char)) {
at = skipEdge(line, at);
} else if (DIAGRAM_IDENTIFIER_CHARACTERS.includes(char)) {
const token = readToken(line, at);
tokens.push(token);
at = token.word === DIAGRAM_GROUP_KEYWORD ? groupIdEnd(line, token.end) : token.end;
} else {
at += 1;
}
}
return tokens;
};
const nextChar = function nextChar(line: string, from: number): string {
return line.slice(from).trimStart().charAt(0);
};
const previousChar = function previousChar(line: string, before: number): string {
const head = line.slice(0, before).trimEnd();
return head.at(-1) ?? "";
};
const usedAsNode = function usedAsNode(line: string, token: Token): boolean {
const after = nextChar(line, token.end);
const before = previousChar(line, token.start);
const target = DIAGRAM_EDGE_CHARACTERS.has(before) || before === LABEL_BAR;
return DIAGRAM_SHAPE_OPENERS.has(after) || DIAGRAM_EDGE_CHARACTERS.has(after) || target;
};
export const reservedNodeIdsOf = function reservedNodeIdsOf(source: string): ReservedNodeId[] {
const found: ReservedNodeId[] = [];
source.split(LINE_END).forEach((raw, index) => {
const line = raw.trim();
for (const token of nodeTokensOf(line)) {
if (DIAGRAM_KEYWORDS.has(token.word) && usedAsNode(line, token)) {
found.push({ line: index + 1, word: token.word });
}
}
});
return found;
};
export const isDiagramSource = function isDiagramSource(source: string): boolean {
const [first = ""] = source.trimStart().split(LINE_END);
const [keyword = ""] = first.split(SPACE);
return DIAGRAM_HEADERS.has(keyword);
};