From 3e56f1d92dbe76022b41b04a54026beb62676fa8 Mon Sep 17 00:00:00 2001 From: gzm Date: Wed, 9 Sep 2026 23:26:56 +0800 Subject: [PATCH] fix: avoid duplicate stroke attrs on cylinder SVG Cylinder body rect inherited styleAttrs stroke and then appended stroke="none", producing invalid XML that browsers reject. Strip stroke-related attrs before forcing stroke="none". Co-authored-by: Cursor --- src/whiteboard/shape-paths.ts | 7 +++++-- src/whiteboard/utils.ts | 8 ++++++++ tests/whiteboard/shape-paths.test.ts | 12 ++++++++++++ tests/whiteboard/utils.test.ts | 17 ++++++++++++++++- 4 files changed, 41 insertions(+), 3 deletions(-) diff --git a/src/whiteboard/shape-paths.ts b/src/whiteboard/shape-paths.ts index 03c557f..cbb4817 100644 --- a/src/whiteboard/shape-paths.ts +++ b/src/whiteboard/shape-paths.ts @@ -1,5 +1,5 @@ import type { WNode } from './types.js'; -import { BORDER_WIDTH_MAP, r } from './utils.js'; +import { BORDER_WIDTH_MAP, r, withoutStrokeAttrs } from './utils.js'; // ─── Shape Path Registry ──────────────────────────────────────────────────── @@ -87,9 +87,12 @@ registerShapePath(['trapezoid', 'flow_chart_trapezoid'], (w, h, sa) => { registerShapePath(['cylinder', 'flow_chart_cylinder', 'data_base'], (w, h, sa, n) => { const ry = h * 0.1; + // 圆柱体侧面用 line 描边,中间矩形不能再继承 sa 里的 stroke, + // 否则会出现 stroke="..." stroke="none" 的非法重复属性。 + const bodySa = withoutStrokeAttrs(sa); return [ ``, - ``, + ``, ``, ``, ``, diff --git a/src/whiteboard/utils.ts b/src/whiteboard/utils.ts index 03c0fae..9e7e97d 100644 --- a/src/whiteboard/utils.ts +++ b/src/whiteboard/utils.ts @@ -179,6 +179,14 @@ export function styleAttrs (s?: WStyle): string { return parts.join(' '); } +/** 从 styleAttrs 结果中剥离 stroke 相关属性,便于再单独指定 stroke */ +export function withoutStrokeAttrs (sa: string): string { + return sa + .replace(/\bstroke(?:-width|-opacity|-dasharray)?="[^"]*"/g, '') + .replace(/\s+/g, ' ') + .trim(); +} + export function transformAttr (n: WNode): string { const parts: string[] = []; if (n.x != null || n.y != null) parts.push(`translate(${r(n.x ?? 0)},${r(n.y ?? 0)})`); diff --git a/tests/whiteboard/shape-paths.test.ts b/tests/whiteboard/shape-paths.test.ts index d63fa6a..96d1dee 100644 --- a/tests/whiteboard/shape-paths.test.ts +++ b/tests/whiteboard/shape-paths.test.ts @@ -83,6 +83,18 @@ describe('shape-paths: composite shapes with multiple elements', () => { expect(out).toContain('stroke="#444"'); }); + it('cylinder body rect does not redefine stroke attribute', () => { + const fn = getShapePath('cylinder')!; + const sa = 'fill="#dff5e5" stroke="#000000" stroke-width="2"'; + const out = fn(100, 80, sa, blankNode({ style: { border_color: '#000000' } as any })); + const body = out.split('\n').find((line) => line.includes(' { const fn = getShapePath('cylinder')!; const out = fn(100, 80, SA, blankNode()); diff --git a/tests/whiteboard/utils.test.ts b/tests/whiteboard/utils.test.ts index b1f5623..90eb9a2 100644 --- a/tests/whiteboard/utils.test.ts +++ b/tests/whiteboard/utils.test.ts @@ -1,6 +1,6 @@ import { describe, it, expect } from 'vitest'; import { - r, esc, styleAttrs, transformAttr, walkNodes, filterNodes, gWrap, nodeDataAttrs, + r, esc, styleAttrs, withoutStrokeAttrs, transformAttr, walkNodes, filterNodes, gWrap, nodeDataAttrs, BORDER_WIDTH_MAP, DASH_ARRAY_MAP, } from '../../src/whiteboard/utils.js'; @@ -78,6 +78,21 @@ describe('styleAttrs', () => { }); }); +describe('withoutStrokeAttrs', () => { + it('strips stroke and related attributes while keeping fill', () => { + expect(withoutStrokeAttrs('fill="#eee" stroke="#333"')).toBe('fill="#eee"'); + expect( + withoutStrokeAttrs('fill="#dff5e5" stroke="#000" stroke-width="2" stroke-dasharray="8,4"'), + ).toBe('fill="#dff5e5"'); + }); + + it('keeps fill-opacity', () => { + expect(withoutStrokeAttrs('fill="#f00" fill-opacity="0.5" stroke="#333"')).toBe( + 'fill="#f00" fill-opacity="0.5"', + ); + }); +}); + // ─── transformAttr ────────────────────────────────────────────────────────── describe('transformAttr', () => {