176 lines
4.8 KiB
TypeScript
176 lines
4.8 KiB
TypeScript
import {
|
|
getMermaidErrorLineNumber,
|
|
getMermaidInactiveParticipant,
|
|
isMermaidAutoFixableError,
|
|
isMermaidParseSyntaxError,
|
|
} from "./mermaidError";
|
|
|
|
const getErrorLineIndex = (message: string, sourceText: string) => {
|
|
const lineNumber = getMermaidErrorLineNumber(message, sourceText);
|
|
if (lineNumber == null) {
|
|
return null;
|
|
}
|
|
return lineNumber - 1;
|
|
};
|
|
|
|
const replaceLineAt = (
|
|
lines: string[],
|
|
index: number,
|
|
transform: (line: string) => string,
|
|
) => {
|
|
if (index < 0 || index >= lines.length) {
|
|
return null;
|
|
}
|
|
const nextLine = transform(lines[index]);
|
|
if (nextLine === lines[index]) {
|
|
return null;
|
|
}
|
|
const nextLines = [...lines];
|
|
nextLines[index] = nextLine;
|
|
return nextLines.join("\n");
|
|
};
|
|
|
|
const stripTrailingTokenAfterShape = (line: string) => {
|
|
const alphaTailMatch = line.match(
|
|
/^(.*(?:\[[^\]]*]|\([^)]*\)|\{[^}]*}|"(?:[^"]*)"|'(?:[^']*)'))([A-Za-z]+)\s*$/,
|
|
);
|
|
if (alphaTailMatch) {
|
|
return alphaTailMatch[1];
|
|
}
|
|
|
|
const punctuationTailMatch = line.match(
|
|
/^(.*(?:\[[^\]]*]|\([^)]*\)|\{[^}]*}|"(?:[^"]*)"|'(?:[^']*)'))([,;:])\s*$/,
|
|
);
|
|
if (punctuationTailMatch) {
|
|
return punctuationTailMatch[1];
|
|
}
|
|
|
|
return line;
|
|
};
|
|
|
|
const removeExtraArrowheadAfterEdgeLabel = (line: string) => {
|
|
// Common typo in generated Mermaid: `-->|label|> Target` (extra `>`).
|
|
// Convert it to `-->|label| Target`.
|
|
return line.replace(/(\|[^|\n]+\|)\s*>\s*(?=[A-Za-z0-9_("[{'`])/g, "$1 ");
|
|
};
|
|
|
|
const escapeRegExp = (value: string) =>
|
|
value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
|
|
const removeLastDeactivateForParticipant = (
|
|
sourceText: string,
|
|
participant: string,
|
|
) => {
|
|
const pattern = new RegExp(
|
|
`^\\s*deactivate\\s+${escapeRegExp(participant)}(?:\\s+%%.*)?\\s*$`,
|
|
);
|
|
const lines = sourceText.split(/\r?\n/);
|
|
|
|
for (let index = lines.length - 1; index >= 0; index--) {
|
|
if (pattern.test(lines[index])) {
|
|
return lines.filter((_, lineIndex) => lineIndex !== index).join("\n");
|
|
}
|
|
}
|
|
|
|
return null;
|
|
};
|
|
|
|
const removeAllDeactivateForParticipant = (
|
|
sourceText: string,
|
|
participant: string,
|
|
) => {
|
|
const pattern = new RegExp(
|
|
`^\\s*deactivate\\s+${escapeRegExp(participant)}(?:\\s+%%.*)?\\s*$`,
|
|
);
|
|
const lines = sourceText.split(/\r?\n/);
|
|
let removedAny = false;
|
|
const remainingLines = lines.filter((line) => {
|
|
if (!pattern.test(line)) {
|
|
return true;
|
|
}
|
|
removedAny = true;
|
|
return false;
|
|
});
|
|
|
|
return removedAny ? remainingLines.join("\n") : null;
|
|
};
|
|
|
|
const appendMissingEnds = (sourceText: string) => {
|
|
const subgraphCount = (sourceText.match(/^\s*subgraph\b/gm) || []).length;
|
|
const endCount = (sourceText.match(/^\s*end\s*$/gm) || []).length;
|
|
const missingCount = subgraphCount - endCount;
|
|
|
|
if (missingCount <= 0) {
|
|
return null;
|
|
}
|
|
|
|
const endings = Array.from({ length: missingCount }, () => "end").join("\n");
|
|
return `${sourceText.trimEnd()}\n${endings}`;
|
|
};
|
|
|
|
const normalizeSmartQuotes = (sourceText: string) =>
|
|
sourceText.replace(/[“”]/g, '"').replace(/[‘’]/g, "'");
|
|
|
|
export const getMermaidAutoFixCandidates = (
|
|
sourceText: string,
|
|
errorMessage: string,
|
|
) => {
|
|
if (!isMermaidAutoFixableError(errorMessage) || !sourceText.trim()) {
|
|
return [];
|
|
}
|
|
|
|
const candidates: string[] = [];
|
|
const seen = new Set<string>();
|
|
const addCandidate = (candidate: string | null) => {
|
|
if (!candidate || candidate === sourceText || seen.has(candidate)) {
|
|
return;
|
|
}
|
|
seen.add(candidate);
|
|
candidates.push(candidate);
|
|
};
|
|
|
|
const inactiveParticipant = getMermaidInactiveParticipant(errorMessage);
|
|
if (inactiveParticipant) {
|
|
addCandidate(
|
|
removeLastDeactivateForParticipant(sourceText, inactiveParticipant),
|
|
);
|
|
// Fallback for repeated invalid inactivations in one diagram.
|
|
addCandidate(
|
|
removeAllDeactivateForParticipant(sourceText, inactiveParticipant),
|
|
);
|
|
}
|
|
|
|
if (isMermaidParseSyntaxError(errorMessage)) {
|
|
const lines = sourceText.split(/\r?\n/);
|
|
const errorLineIndex = getErrorLineIndex(errorMessage, sourceText);
|
|
const lineIndexesToTry =
|
|
errorLineIndex == null
|
|
? []
|
|
: [errorLineIndex, errorLineIndex - 1, errorLineIndex + 1];
|
|
|
|
for (const lineIndex of lineIndexesToTry) {
|
|
addCandidate(
|
|
replaceLineAt(lines, lineIndex, (line) =>
|
|
stripTrailingTokenAfterShape(line),
|
|
),
|
|
);
|
|
addCandidate(
|
|
replaceLineAt(lines, lineIndex, (line) =>
|
|
removeExtraArrowheadAfterEdgeLabel(line),
|
|
),
|
|
);
|
|
}
|
|
|
|
// Also try full-text replacement so repeated occurrences on other lines
|
|
// are fixed together in a single candidate.
|
|
addCandidate(removeExtraArrowheadAfterEdgeLabel(sourceText));
|
|
|
|
addCandidate(appendMissingEnds(sourceText));
|
|
|
|
const normalizedQuotes = normalizeSmartQuotes(sourceText);
|
|
addCandidate(normalizedQuotes === sourceText ? null : normalizedQuotes);
|
|
}
|
|
|
|
return candidates;
|
|
};
|