Skip to content

Commit c8978b2

Browse files
committed
fix: improve zenuml print rendering, sizing, and syntax resilience
1 parent 2b938d0 commit c8978b2

File tree

6 files changed

+451
-184
lines changed

6 files changed

+451
-184
lines changed
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
---
2+
'mermaid': patch
3+
'@mermaid-js/mermaid-zenuml': patch
4+
---
5+
6+
fix: update @zenuml/core to v3.46.11 with native SVG renderer
7+
8+
- Fix vertical lifelines disappearing when printing (#6004)
9+
- Fix SVG dimensions exceeding container boundaries (#7266)
10+
- Fix invalid ZenUML syntax freezing the editor (#7154)

packages/mermaid-zenuml/package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
],
3434
"license": "MIT",
3535
"dependencies": {
36-
"@zenuml/core": "^3.41.6"
36+
"@zenuml/core": "^3.47.0"
3737
},
3838
"devDependencies": {
3939
"mermaid": "workspace:^"
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Override @zenuml/core types for nodenext module resolution.
2+
// The package lacks "type": "module" so TS treats it as CJS,
3+
// rejecting named imports. This declaration fixes that.
4+
declare module '@zenuml/core' {
5+
export interface RenderOptions {
6+
theme?: 'theme-default' | 'theme-mermaid';
7+
}
8+
9+
export interface RenderResult {
10+
svg: string;
11+
innerSvg: string;
12+
width: number;
13+
height: number;
14+
viewBox: string;
15+
}
16+
17+
export function renderToSvg(code: string, options?: RenderOptions): RenderResult;
18+
}
Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,98 @@
1+
import { vi } from 'vitest';
2+
import { calculateSvgSizeAttrs } from './zenumlRenderer.js';
3+
4+
vi.mock('@zenuml/core', () => ({
5+
renderToSvg: vi.fn((code: string) => ({
6+
innerSvg: `<text>${code.trim()}</text>`,
7+
width: 400,
8+
height: 300,
9+
viewBox: '0 0 400 300',
10+
})),
11+
}));
12+
13+
vi.mock('./mermaidUtils.js', () => ({
14+
log: { info: vi.fn(), error: vi.fn(), debug: vi.fn(), warn: vi.fn() },
15+
getConfig: vi.fn(() => ({
16+
securityLevel: 'loose',
17+
sequence: { useMaxWidth: true },
18+
})),
19+
}));
20+
21+
describe('calculateSvgSizeAttrs', function () {
22+
it('should return responsive width when useMaxWidth is true', function () {
23+
const attrs = calculateSvgSizeAttrs(133, 392, true);
24+
25+
expect(attrs.get('width')).toEqual('100%');
26+
expect(attrs.get('style')).toEqual('max-width: 133px;');
27+
expect(attrs.has('height')).toBe(false);
28+
});
29+
30+
it('should return absolute dimensions when useMaxWidth is false', function () {
31+
const attrs = calculateSvgSizeAttrs(133, 392, false);
32+
33+
expect(attrs.get('width')).toEqual('133');
34+
expect(attrs.get('height')).toEqual('392');
35+
expect(attrs.has('style')).toBe(false);
36+
});
37+
});
38+
39+
describe('draw', () => {
40+
beforeEach(() => {
41+
vi.clearAllMocks();
42+
document.body.innerHTML = '';
43+
});
44+
45+
it('should render SVG content into the target element', async () => {
46+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
47+
svg.id = 'test-id';
48+
document.body.appendChild(svg);
49+
50+
const { draw } = await import('./zenumlRenderer.js');
51+
await draw('zenuml\n Alice->Bob: hello', 'test-id');
52+
53+
expect(svg.innerHTML).toContain('Alice-');
54+
expect(svg.getAttribute('viewBox')).toBe('0 0 400 300');
55+
expect(svg.getAttribute('width')).toBe('100%');
56+
expect(svg.getAttribute('style')).toBe('max-width: 400px;');
57+
});
58+
59+
it('should set absolute dimensions when useMaxWidth is false', async () => {
60+
const { getConfig } = await import('./mermaidUtils.js');
61+
vi.mocked(getConfig).mockReturnValue({
62+
securityLevel: 'loose',
63+
sequence: { useMaxWidth: false },
64+
});
65+
66+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
67+
svg.id = 'test-abs';
68+
document.body.appendChild(svg);
69+
70+
const { draw } = await import('./zenumlRenderer.js');
71+
await draw('zenuml\n A->B: msg', 'test-abs');
72+
73+
expect(svg.getAttribute('width')).toBe('400');
74+
expect(svg.getAttribute('height')).toBe('300');
75+
});
76+
77+
it('should handle missing SVG element gracefully', async () => {
78+
const { draw } = await import('./zenumlRenderer.js');
79+
const { log } = await import('./mermaidUtils.js');
80+
81+
await draw('zenuml\n A->B: msg', 'nonexistent');
82+
83+
expect(log.error).toHaveBeenCalledWith('Cannot find svg element');
84+
});
85+
86+
it('should strip the zenuml prefix before rendering', async () => {
87+
const { renderToSvg } = await import('@zenuml/core');
88+
89+
const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
90+
svg.id = 'test-strip';
91+
document.body.appendChild(svg);
92+
93+
const { draw } = await import('./zenumlRenderer.js');
94+
await draw('zenuml\n Alice->Bob: hello', 'test-strip');
95+
96+
expect(renderToSvg).toHaveBeenCalledWith('\n Alice->Bob: hello');
97+
});
98+
});

packages/mermaid-zenuml/src/zenumlRenderer.ts

Lines changed: 67 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,86 @@
1+
import { renderToSvg } from '@zenuml/core';
12
import { getConfig, log } from './mermaidUtils.js';
2-
import ZenUml from '@zenuml/core';
33

44
const regexp = /^\s*zenuml/;
55

6-
// Create a Zen UML container outside the svg first for rendering, otherwise the Zen UML diagram cannot be rendered properly
7-
function createTemporaryZenumlContainer(id: string) {
8-
const container = document.createElement('div');
9-
container.id = `container-${id}`;
10-
container.style.display = 'flex';
11-
container.innerHTML = `<div id="zenUMLApp-${id}"></div>`;
12-
const app = container.querySelector(`#zenUMLApp-${id}`)!;
13-
return { container, app };
14-
}
15-
16-
// Create a foreignObject to wrap the Zen UML container in the svg
17-
function createForeignObject(id: string) {
18-
const foreignObject = document.createElementNS('http://www.w3.org/2000/svg', 'foreignObject');
19-
foreignObject.setAttribute('x', '0');
20-
foreignObject.setAttribute('y', '0');
21-
foreignObject.setAttribute('width', '100%');
22-
foreignObject.setAttribute('height', '100%');
23-
const { container, app } = createTemporaryZenumlContainer(id);
24-
foreignObject.appendChild(container);
25-
return { foreignObject, container, app };
26-
}
6+
export const calculateSvgSizeAttrs = (
7+
width: number,
8+
height: number,
9+
useMaxWidth: boolean
10+
): Map<string, string> => {
11+
const attrs = new Map<string, string>();
12+
13+
if (useMaxWidth) {
14+
attrs.set('width', '100%');
15+
attrs.set('style', `max-width: ${width}px;`);
16+
} else {
17+
attrs.set('width', String(width));
18+
attrs.set('height', String(height));
19+
}
20+
21+
return attrs;
22+
};
2723

2824
/**
29-
* Draws a Zen UML in the tag with id: id based on the graph definition in text.
30-
*
31-
* @param text - The text of the diagram
32-
* @param id - The id of the diagram which will be used as a DOM element id¨
25+
* Resolves the root document and SVG element, handling sandbox mode.
26+
* Follows the same pattern as mermaid's selectSvgElement utility.
3327
*/
34-
export const draw = async function (text: string, id: string) {
35-
log.info('draw with Zen UML renderer', ZenUml);
36-
37-
text = text.replace(regexp, '');
28+
const selectSvgElement = (id: string): SVGSVGElement | null => {
3829
const { securityLevel } = getConfig();
39-
// Handle root and Document for when rendering in sandbox mode
40-
let sandboxElement: HTMLIFrameElement | null = null;
30+
let root: Document = document;
31+
4132
if (securityLevel === 'sandbox') {
42-
sandboxElement = document.getElementById('i' + id) as HTMLIFrameElement;
33+
const sandboxElement = document.querySelector<HTMLIFrameElement>(`#i${id}`);
34+
root = sandboxElement?.contentDocument ?? document;
4335
}
4436

45-
const root = securityLevel === 'sandbox' ? sandboxElement?.contentWindow?.document : document;
37+
return root.querySelector<SVGSVGElement>(`#${id}`);
38+
};
4639

47-
const svgContainer = root?.querySelector(`svg#${id}`);
40+
const configureSvgSize = (
41+
svgEl: SVGSVGElement,
42+
width: number,
43+
height: number,
44+
useMaxWidth: boolean
45+
) => {
46+
const attrs = calculateSvgSizeAttrs(width, height, useMaxWidth);
4847

49-
if (!root || !svgContainer) {
50-
log.error('Cannot find root or svgContainer');
51-
return;
48+
svgEl.removeAttribute('height');
49+
svgEl.style.removeProperty('max-width');
50+
51+
for (const [attr, value] of attrs) {
52+
svgEl.setAttribute(attr, value);
5253
}
54+
};
55+
56+
/**
57+
* Draws a ZenUML diagram in the SVG element with id: id based on the
58+
* graph definition in text, using native SVG rendering.
59+
*
60+
* @param text - The text of the diagram
61+
* @param id - The id of the diagram which will be used as a DOM element id
62+
*/
63+
export const draw = function (text: string, id: string): Promise<void> {
64+
log.info('draw with ZenUML native SVG renderer');
65+
66+
const code = text.replace(regexp, '');
67+
const config = getConfig();
68+
const useMaxWidth = config.sequence?.useMaxWidth ?? true;
69+
70+
const svgEl = selectSvgElement(id);
71+
72+
if (!svgEl) {
73+
log.error('Cannot find svg element');
74+
return Promise.resolve();
75+
}
76+
77+
const result = renderToSvg(code);
5378

54-
const { foreignObject, container, app } = createForeignObject(id);
55-
svgContainer.appendChild(foreignObject);
56-
const zenuml = new ZenUml(app);
57-
// default is a theme name. More themes to be added and will be configurable in the future
58-
await zenuml.render(text, { theme: 'default', mode: 'static' });
79+
configureSvgSize(svgEl, result.width, result.height, useMaxWidth);
80+
svgEl.setAttribute('viewBox', result.viewBox);
81+
svgEl.innerHTML = result.innerSvg;
5982

60-
const { width, height } = window.getComputedStyle(container);
61-
log.debug('zenuml diagram size', width, height);
62-
svgContainer.setAttribute('style', `width: ${width}; height: ${height};`);
83+
return Promise.resolve();
6384
};
6485

6586
export default {

0 commit comments

Comments
 (0)