Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

## [0.6.7] - 2025-04-15

- **Fixed** Render preview sometimes failed to load when opening a .dot file with specials characters in content

## [0.6.6] - 2025-04-09

- **Added** Highlight dependencies in the preview graph (beta)
Expand Down
117 changes: 102 additions & 15 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,43 +45,130 @@ function isPathMatchingAnyPattern(
}

function sanitizeDotContent(dotContent: string): string {
// Verify that content looks like a DOT graph - Limit characters to avoid excessive backtracking
// Check that the content looks like a DOT graph
const dotGraphRegex = /^\s*(?:di)?graph\s+[\w"{}][^\n]{0,100}/i;
if (!dotGraphRegex.exec(dotContent.trim())) {
console.warn("Warning: Content doesn't appear to be a valid DOT graph");
}

// Add global graph attributes to improve edge rendering
// Add global attributes to improve edge rendering
if (dotContent.includes('digraph') && !dotContent.includes('splines=')) {
// Add graph attributes to optimize rendering, especially when multiple edges are close
// Utiliser une regex plus simple et plus sûre
dotContent = dotContent.replace(
/digraph\s+[\w"]+\s*\{/i,
match => `${match}\n // Graph attributes for better edge rendering\n graph [splines=polyline, overlap=false, nodesep=0.8, ranksep=1.0];\n edge [penwidth=1.5, arrowsize=0.8];\n node [shape=box, style=filled, fillcolor=aliceblue];\n`
);
}

// Sanitize HTML in label attributes
// Handle both single and double quoted node IDs that might contain apostrophes
dotContent = dotContent.replace(
/label\s*=\s*["']([^"']*)["']/gi,
(_match, labelContent) => {
const sanitized = sanitizeHtml(labelContent, {
/"([^"]*?)"/g,
(_match, nodeId) => {
// Replace apostrophes and other special characters in node IDs
const sanitized = nodeId
.replace(/'/g, ''')
return `"${sanitized}"`;
}
);

// Handle attributes with label properties - preserving newlines
dotContent = dotContent.replace(
/\[([^\]]*?)label\s*=\s*(?:"([^"]*)"|'([^']*)')([^\]]*?)\]/g,
(_match, beforeLabel, doubleQuotedContent, singleQuotedContent, afterLabel) => {
// Handle newlines in label content - use the non-undefined content
const labelContent = doubleQuotedContent !== undefined ? doubleQuotedContent : singleQuotedContent;

let sanitized = sanitizeHtml(labelContent, {
allowedTags: [],
allowedAttributes: {},
});
return `label="${sanitized}"`;

// Preserve valid newlines sequences
sanitized = sanitized
.replace(/'/g, ''') // Ensure apostrophes are HTML escaped
.replace(/"/g, '"') // Ensure quotes are HTML escaped

return `[${beforeLabel}label="${sanitized}"${afterLabel}]`;
}
);

// Fix common issues with node/edge definitions that can cause rendering problems
// Handle all other attributes - using a more direct approach to ensure all apostrophes are caught
dotContent = dotContent.replace(
/=\s*["']([^"']*?)["']/g,
(_match, attrContent) => {
// Handle newlines and other special characters
const sanitized = attrContent
.replace(/'/g, ''') // Apostrophes
.replace(/"/g, '"') // Double quotes
// Acute accents
.replace(/é/g, 'é')
.replace(/É/g, 'É')
.replace(/á/g, 'á')
.replace(/Á/g, 'Á')
.replace(/í/g, 'í')
.replace(/Í/g, 'Í')
.replace(/ó/g, 'ó')
.replace(/Ó/g, 'Ó')
.replace(/ú/g, 'ú')
.replace(/Ú/g, 'Ú')
.replace(/ý/g, 'ý')
.replace(/Ý/g, 'Ý')
// Grave accents
.replace(/è/g, 'è')
.replace(/È/g, 'È')
.replace(/à/g, 'à')
.replace(/À/g, 'À')
.replace(/ì/g, 'ì')
.replace(/Ì/g, 'Ì')
.replace(/ò/g, 'ò')
.replace(/Ò/g, 'Ò')
.replace(/ù/g, 'ù')
.replace(/Ù/g, 'Ù')
// Circumflex
.replace(/ê/g, 'ê')
.replace(/Ê/g, 'Ê')
.replace(/â/g, 'â')
.replace(/Â/g, 'Â')
.replace(/î/g, 'î')
.replace(/Î/g, 'Î')
.replace(/ô/g, 'ô')
.replace(/Ô/g, 'Ô')
.replace(/û/g, 'û')
.replace(/Û/g, 'Û')
// Diaeresis
.replace(/ë/g, 'ë')
.replace(/Ë/g, 'Ë')
.replace(/ä/g, 'ä')
.replace(/Ä/g, 'Ä')
.replace(/ï/g, 'ï')
.replace(/Ï/g, 'Ï')
.replace(/ö/g, 'ö')
.replace(/Ö/g, 'Ö')
.replace(/ü/g, 'ü')
.replace(/Ü/g, 'Ü')
.replace(/ÿ/g, 'ÿ')
// Other special characters
.replace(/ç/g, 'ç')
.replace(/Ç/g, 'Ç')
.replace(/ñ/g, 'ñ')
.replace(/Ñ/g, 'Ñ')
.replace(/œ/g, 'œ')
.replace(/Œ/g, 'Œ')
.replace(/æ/g, 'æ')
.replace(/Æ/g, 'Æ');

return `="${sanitized}"`;
}
);

// Fix common edge syntax issues
dotContent = dotContent
// Ensure proper spacing in edge definitions
// Ensure adequate spacing in edge definitions
.replace(/->(\S)/g, '-> $1')

// Fix issues with quotes within node attributes - Pas de backtracking possible
.replace(/label\s*=\s*"([^"]*)"/g, (match) => {
return match.replace(/\\"/g, '\\\\"');
})
// Additional fix for graph identifiers with spaces
.replace(/(di)?graph\s+(\w+\s+\w+)(\s*{)/gi, (_match, di, name, bracket) => {
return `${di || ''}graph "${name}"${bracket}`;
});

return dotContent;
}
Expand Down