('rect[fill="white"]');
// Background rectangle of the lower compartment created by the ``
// HTML element in dot. It was transformed into a rounded rect by `fixNodeBackground`.
// Since dot doesn't round the coners of ` | ` background,
// we have to clip it ourselves.
const compartment = node.querySelector('rect[fill="green"]');
// Make sure we provide traceability with IDs also for the border.
const border = node.querySelector('rect[stroke="black"]');
if (container === null || compartment === null || border === null) {
return;
}
const copyOfContainer = container.cloneNode() as SVGRectElement;
const clipPath = document.createElementNS(SVG_NS, 'clipPath');
const clipId = `${node.id},,clip`;
clipPath.setAttribute('id', clipId);
clipPath.appendChild(copyOfContainer);
node.appendChild(clipPath);
compartment.setAttribute('clip-path', `url(#${clipId})`);
// Enlarge the compartment to completely cover the background.
modifyAttribute(compartment, 'y', -5);
modifyAttribute(compartment, 'x', -5);
modifyAttribute(compartment, 'width', 10);
const isEmpty = node.classList.contains('node-empty');
// Make sure that empty nodes are fully filled.
modifyAttribute(compartment, 'height', isEmpty ? 10 : 5);
if (node.classList.contains('node-equalsSelf-UNKNOWN')) {
addShadow(node, container, 6);
}
container.id = `${node.id},container`;
compartment.id = `${node.id},compartment`;
border.id = `${node.id},border`;
}
function createRect(
{ x, y, width, height }: BBox,
original: SVGElement,
): SVGRectElement {
const rect = document.createElementNS(SVG_NS, 'rect');
rect.setAttribute('fill', original.getAttribute('fill') ?? '');
rect.setAttribute('stroke', original.getAttribute('stroke') ?? '');
rect.setAttribute('x', String(x));
rect.setAttribute('y', String(y));
rect.setAttribute('width', String(width));
rect.setAttribute('height', String(height));
return rect;
}
function optimizeNodeShapes(node: SVGGElement) {
node.querySelectorAll('path').forEach((path) => {
const bbox = parsePathBBox(path);
const rect = createRect(bbox, path);
rect.setAttribute('rx', '12');
rect.setAttribute('ry', '12');
path.parentNode?.replaceChild(rect, path);
});
node.querySelectorAll('polygon').forEach((polygon) => {
const bbox = parsePolygonBBox(polygon);
if (bbox.height === 0) {
const polyline = document.createElementNS(SVG_NS, 'polyline');
polyline.setAttribute('stroke', polygon.getAttribute('stroke') ?? '');
polyline.setAttribute(
'points',
`${bbox.x},${bbox.y} ${bbox.x + bbox.width},${bbox.y}`,
);
polygon.parentNode?.replaceChild(polyline, polygon);
} else {
const rect = createRect(bbox, polygon);
polygon.parentNode?.replaceChild(rect, polygon);
}
});
clipCompartmentBackground(node);
}
function hrefToClass(node: SVGGElement) {
node.querySelectorAll('a').forEach((a) => {
if (a.parentNode === null) {
return;
}
const href = a.getAttribute('href') ?? a.getAttributeNS(XLINK_NS, 'href');
if (href === 'undefined' || !href?.startsWith('#')) {
return;
}
while (a.lastChild !== null) {
const child = a.lastChild;
a.removeChild(child);
if (child.nodeType === Node.ELEMENT_NODE) {
const element = child as Element;
element.classList.add('label', `label-${href.replace('#', '')}`);
a.after(child);
}
}
a.parentNode.removeChild(a);
});
}
function replaceImages(node: SVGGElement) {
node.querySelectorAll('image').forEach((image) => {
const href =
image.getAttribute('href') ?? image.getAttributeNS(XLINK_NS, 'href');
if (href === 'undefined' || !href?.startsWith('#')) {
return;
}
const width = image.getAttribute('width')?.replace('px', '') ?? '';
const height = image.getAttribute('height')?.replace('px', '') ?? '';
const foreign = document.createElementNS(SVG_NS, 'foreignObject');
foreign.setAttribute('x', image.getAttribute('x') ?? '');
foreign.setAttribute('y', image.getAttribute('y') ?? '');
foreign.setAttribute('width', width);
foreign.setAttribute('height', height);
const div = document.createElement('div');
div.classList.add('icon', `icon-${href.replace('#', '')}`);
foreign.appendChild(div);
const sibling = image.nextElementSibling;
// Since dot doesn't respect the `id` attribute on table cells with a single image,
// compute the ID based on the ID of the next element (the label).
if (
sibling !== null &&
sibling.tagName.toLowerCase() === 'g' &&
sibling.id !== ''
) {
foreign.id = `${sibling.id},icon`;
}
image.parentNode?.replaceChild(foreign, image);
});
}
function markerColorToClass(svg: SVGSVGElement) {
svg.querySelectorAll('.node [stroke="black"]').forEach((node) => {
node.removeAttribute('stroke');
node.classList.add('node-outline');
});
svg.querySelectorAll('.node [fill="green"]').forEach((node) => {
node.removeAttribute('fill');
node.classList.add('node-header');
});
svg.querySelectorAll('.node [fill="white"]').forEach((node) => {
node.removeAttribute('fill');
node.classList.add('node-bg');
});
svg.querySelectorAll('.edge [stroke="black"]').forEach((node) => {
node.removeAttribute('stroke');
node.classList.add('edge-line');
});
svg.querySelectorAll('.edge [fill="black"]').forEach((node) => {
node.removeAttribute('fill');
node.classList.add('edge-arrow');
});
svg.querySelectorAll('[font-family]').forEach((node) => {
node.removeAttribute('font-family');
});
}
export default function postProcessSvg(svg: SVGSVGElement) {
// svg
// .querySelectorAll('title')
// .forEach((title) => title.parentElement?.removeChild(title));
svg.querySelectorAll('g.node').forEach((node) => {
optimizeNodeShapes(node);
hrefToClass(node);
replaceImages(node);
});
// Increase padding to fit box shadows for multi-objects.
const viewBox = [
svg.viewBox.baseVal.x - 6,
svg.viewBox.baseVal.y - 6,
svg.viewBox.baseVal.width + 12,
svg.viewBox.baseVal.height + 12,
];
svg.setAttribute('viewBox', viewBox.join(' '));
markerColorToClass(svg);
}
|