Files
2026-09-16 05:31:04 +05:00

176 lines
4.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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;
};