import { describe, it, expect } from 'vitest'; import type { Gradient } from '../gradient'; import { loadSVGFromString, Path } from '../../fabric'; import { Rect } from '../shapes/Rect'; import { cos, sin } from '../util'; import { parseAttributes } from './parseAttributes'; import { parseStyleAttribute } from './parseStyleAttribute'; import { parseFontDeclaration } from './parseFontDeclaration'; import { parsePointsAttribute } from './parsePointsAttribute'; import { parseTransformAttribute } from './parseTransformAttribute'; import { getCSSRules } from './getCSSRules'; import { createSVGElement } from '../../test/utils'; function makeElement() { return createSVGElement('path', { cx: 101, x: 102, cy: 103, y: 104, r: 105, opacity: 0.45, 'fill-rule': 'foo', 'stroke-width': 4, }); } describe('fabric.Parser', () => { it('parseAttributes', () => { expect(parseAttributes).toBeDefined(); const element = makeElement(); const attributeNames = 'cx cy x y r opacity fill-rule stroke-width transform fill fill-rule'.split( ' ', ); const parsedAttributes = parseAttributes(element, attributeNames); expect(parsedAttributes).toEqual({ left: 102, top: 104, radius: 105, opacity: 0.45, fillRule: 'foo', strokeWidth: 4, }); }); it('parseAttributesNoneValues', () => { const element = createSVGElement('path', { fill: 'none', stroke: 'none', }); expect(parseAttributes(element, 'fill stroke'.split(' '))).toEqual({ fill: '', stroke: '', }); }); it('parseAttributesFillRule', () => { const element = createSVGElement('path', { 'fill-rule': 'evenodd', }); expect(parseAttributes(element, ['fill-rule'])).toEqual({ fillRule: 'evenodd', }); }); it('parseAttributesFillRuleWithoutTransformation', () => { const element = createSVGElement('path', { 'fill-rule': 'inherit', }); expect(parseAttributes(element, ['fill-rule'])).toEqual({ fillRule: 'inherit', }); }); it('parseAttributesTransform', () => { const element = createSVGElement('path', { transform: 'translate(5, 10)', }); expect(parseAttributes(element, ['transform'])).toEqual({ transformMatrix: [1, 0, 0, 1, 5, 10], }); }); it('parseAttributesWithParent', () => { const element = createSVGElement('path', { x: '100' }); const parent = createSVGElement('g', { y: '200' }); const grandParent = createSVGElement('g', { fill: 'red' }); parent.appendChild(element); grandParent.appendChild(parent); expect(parseAttributes(element, 'x y fill'.split(' '))).toEqual({ fill: 'red', left: 100, top: 200, }); }); it('parseAttributesWithGrandParentSvg', () => { const element = createSVGElement('path', { x: '100' }); const parent = createSVGElement('g', { y: '200' }); const grandParent = createSVGElement('svg', { width: '600', height: '600', }); parent.appendChild(element); grandParent.appendChild(parent); expect(parseAttributes(element, 'x y width height'.split(' '))).toEqual({ left: 100, top: 200, width: 600, height: 600, }); }); it('parseAttributeFontValueStartWithFontSize', () => { const element = createSVGElement('path', { style: 'font: 15px arial, sans-serif;', }); const styleObj = parseAttributes(element, ['font']); const expectedObject = { font: '15px arial, sans-serif', fontSize: 15, fontFamily: 'arial, sans-serif', }; expect(styleObj).toEqual(expectedObject); }); it('parseStyleAttribute', () => { const element = createSVGElement('path', { style: 'left:10px;top:22.3em;width:103.45pt;height:20%;', }); const styleObj = parseStyleAttribute(element); // TODO: looks like this still fails with % values const expectedObject = { left: '10px', top: '22.3em', width: '103.45pt', height: '20%', }; expect(styleObj).toEqual(expectedObject); }); it('parseStyleAttribute with one pair', () => { const element = createSVGElement('path', { style: 'left:10px' }); const expectedObject = { left: '10px', }; expect(parseStyleAttribute(element)).toEqual(expectedObject); }); it('parseStyleAttribute with trailing spaces', () => { const element = createSVGElement('path', { style: 'left:10px; top:5px; ', }); const expectedObject = { left: '10px', top: '5px', }; expect(parseStyleAttribute(element)).toEqual(expectedObject); }); it('parseStyleAttribute with value normalization', () => { const element = createSVGElement('path', { style: 'fill:none; stroke-dasharray: 2 0.4;', }); const expectedObject = { fill: 'none', 'stroke-dasharray': '2 0.4', }; expect(parseStyleAttribute(element)).toEqual(expectedObject); }); it('parseStyleAttribute with short font declaration', () => { const element = createSVGElement('path', { style: 'font: italic 12px Arial,Helvetica,sans-serif', }); const styleObj = parseStyleAttribute(element); if (styleObj.font) { parseFontDeclaration(styleObj.font, styleObj); } const expectedObject = { font: 'italic 12px Arial,Helvetica,sans-serif', fontSize: 12, fontStyle: 'italic', fontFamily: 'Arial,Helvetica,sans-serif', }; expect(styleObj).toEqual(expectedObject); //testing different unit element.setAttribute( 'style', 'font: italic 1.5em Arial,Helvetica,sans-serif', ); const styleObj2 = parseStyleAttribute(element); if (styleObj2.font) { parseFontDeclaration(styleObj2.font, styleObj2); } const expectedObject2 = { font: 'italic 1.5em Arial,Helvetica,sans-serif', fontSize: 24, fontStyle: 'italic', fontFamily: 'Arial,Helvetica,sans-serif', }; expect(styleObj2).toEqual(expectedObject2); }); it('parseAttributes (style to have higher priority than attribute)', () => { const element = createSVGElement('path', { style: 'fill:red', fill: 'green', }); const expectedObject = { fill: 'red', }; expect(parseAttributes(element, Path.ATTRIBUTE_NAMES)).toEqual( expectedObject, ); }); it('parseAttributes stroke-opacity and fill-opacity', () => { const element = createSVGElement('path', { style: 'fill:rgb(100,200,50);fill-opacity:0.2;', stroke: 'green', 'stroke-opacity': '0.5', fill: 'green', }); const expectedObject = { fill: 'rgba(100,200,50,0.2)', stroke: 'rgba(0,128,0,0.5)', fillOpacity: 0.2, strokeOpacity: 0.5, }; expect(parseAttributes(element, Path.ATTRIBUTE_NAMES)).toEqual( expectedObject, ); }); it('parse 0 attribute', () => { const element = createSVGElement('path', { opacity: 0 }); const expectedObject = { opacity: 0, }; expect(parseAttributes(element, Path.ATTRIBUTE_NAMES)).toEqual( expectedObject, ); }); it('parsePointsAttribute', () => { const element = createSVGElement('polygon', { points: '10, 12 20 ,22, -0.52,0.001 2.3e2,2.3E-2, 10,-1 ', }); const actualPoints = parsePointsAttribute(element.getAttribute('points')); expect(actualPoints[0].x).toBe(10); expect(actualPoints[0].y).toBe(12); expect(actualPoints[1].x).toBe(20); expect(actualPoints[1].y).toBe(22); expect(actualPoints[2].x).toBe(-0.52); expect(actualPoints[2].y).toBe(0.001); expect(actualPoints[3].x).toBe(2.3e2); expect(actualPoints[3].y).toBe(2.3e-2); expect(actualPoints[4].x).toBe(10); expect(actualPoints[4].y).toBe(-1); }); it('parseTransformAttribute', () => { let parsedValue; const element = createSVGElement('path', { transform: 'translate(5,10)', }); //'translate(-10,-20) scale(2) rotate(45) translate(5,10)' parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([1, 0, 0, 1, 5, 10]); element.setAttribute('transform', 'translate(-10,-20)'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([1, 0, 0, 1, -10, -20]); const ANGLE_DEG = 90; const ANGLE = (ANGLE_DEG * Math.PI) / 180; element.setAttribute('transform', 'rotate(' + ANGLE_DEG + ')'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([ cos(ANGLE), sin(ANGLE), -sin(ANGLE), cos(ANGLE), 0, 0, ]); element.setAttribute('transform', 'scale(3.5)'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([3.5, 0, 0, 3.5, 0, 0]); element.setAttribute('transform', 'scale(2 13)'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([2, 0, 0, 13, 0, 0]); element.setAttribute('transform', 'skewX(2)'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([1, 0, 0.03492076949174773, 1, 0, 0]); element.setAttribute('transform', 'skewY(234.111)'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([1, 1.3820043381762832, 0, 1, 0, 0]); element.setAttribute('transform', 'matrix(1,2,3.3,-4,5E1,6e-1)'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([1, 2, 3.3, -4, 50, 0.6]); element.setAttribute('transform', 'translate(21,31) translate(11,22)'); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([1, 0, 0, 1, 32, 53]); element.setAttribute( 'transform', 'scale(2 13) translate(5,15) skewX(11.22)', ); parsedValue = parseTransformAttribute(element.getAttribute('transform')!); expect(parsedValue).toEqual([2, 0, 0.3967362169237356, 13, 10, 195]); }); it('parseNestedTransformAttribute', () => { const element = createSVGElement('path', { transform: 'translate(10 10)', }); const parent = createSVGElement('g', { transform: 'translate(50)' }); parent.appendChild(element); const parsedAttributes = parseAttributes(element, ['transform']); expect(parsedAttributes.transformMatrix).toEqual([1, 0, 0, 1, 60, 10]); }); it('parseSVGFromString id polyfill', async () => { const string = '' + '' + '' + ''; expect(loadSVGFromString).toBeDefined(); const { objects } = await loadSVGFromString(string); const rect = objects[0]; expect(rect!.constructor).toHaveProperty('type', 'Rect'); }); it('parseSVGFromString with gradient and fill url with quotes', async () => { const string = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; const { objects } = await loadSVGFromString(string); expect(objects[0]!.fill).toHaveProperty('type', 'linear'); expect(objects[1]!.fill).toHaveProperty('type', 'linear'); expect(objects[2]!.fill).toHaveProperty('type', 'linear'); }); it('parseSVGFromString with xlink:href', async () => { const string = '' + '' + '' + ''; expect(loadSVGFromString).toBeDefined(); const { objects } = await loadSVGFromString(string); const rect = objects[0]; expect(rect!.constructor).toHaveProperty('type', 'Rect'); }); it('parseSVGFromString with href', async () => { const string = '' + '' + '' + ''; expect(loadSVGFromString).toBeDefined(); const { objects } = await loadSVGFromString(string); const rect = objects[0]!; expect(rect.constructor).toHaveProperty('type', 'Rect'); }); it('parseSVGFromString nested opacity', async () => { const string = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; const { objects } = await loadSVGFromString(string); expect(objects[0]).toHaveProperty('fill', 'rgba(255,0,0,0.3)'); expect(objects[0]).toHaveProperty('fillOpacity', 1); expect(objects[1]).toHaveProperty('fill', 'rgba(0,255,0,0.25)'); expect(objects[1]).toHaveProperty('fillOpacity', 0.5); expect(objects[2]).toHaveProperty('fill', 'rgba(255,0,0,0.5)'); expect(objects[2]).toHaveProperty('fillOpacity', 0.5); expect(objects[3]).toHaveProperty('fill', 'rgba(0,0,255,0.5)'); expect(objects[3]).toHaveProperty('fillOpacity', 0.5); expect(objects[4]).toHaveProperty('fill', 'rgba(0,0,255,0.25)'); expect(objects[4]).toHaveProperty('fillOpacity', 0.5); expect(objects[5]).toHaveProperty('fill', 'rgba(0,0,255,1)'); expect(objects[5]).toHaveProperty('fillOpacity', 1); expect(objects[6]).toHaveProperty('opacity', 0.25); expect(objects[7]).toHaveProperty('opacity', 0.5); }); it('parseSVGFromString path fill-opacity with gradient', async () => { const string = '' + '' + '' + '' + '' + '' + '' + ''; const { objects } = await loadSVGFromString(string); expect((objects[0]!.fill as Gradient).colorStops[0].color).toBe( 'rgba(255,0,0,0.5)', ); expect((objects[0]!.fill as Gradient).colorStops[1].color).toBe( 'rgba(0,255,0,0.25)', ); }); it('parseSVGFromString with svg:namespace', async () => { const string = '' + '' + '' + ''; expect(loadSVGFromString).toBeDefined(); const { objects } = await loadSVGFromString(string); const rect = objects[0]!; expect(rect.constructor).toHaveProperty('type', 'Rect'); }); it('opacity attribute', async () => { const tagNames = [ 'Rect', 'Path', 'Circle', 'Ellipse', 'Polygon', 'Polyline', 'Text', ] as const; const tests = tagNames.map(async (tagName) => { const opacityValue = Math.random().toFixed(2); const el = createSVGElement(tagName.toLowerCase(), { opacity: opacityValue, }); const module = await import('../../fabric'); const fabricClass = module[tagName]; // @ts-expect-error -- TODO: not all elements fromElement accept SVGElement as a type, but should it? currently it accepts only HTMLElement const obj = await fabricClass.fromElement(el, {}); expect(obj.opacity).toBe(parseFloat(opacityValue)); }); await Promise.all(tests); }); it('fill-opacity attribute with fill attribute', async () => { const opacityValue = Math.random().toFixed(2); const el = createSVGElement('rect', { 'fill-opacity': opacityValue, fill: '#FF0000', }); const obj = await Rect.fromElement(el); expect(obj.fill).toBe(`rgba(255,0,0,${parseFloat(opacityValue)})`); }); it('fill-opacity attribute without fill attribute', async () => { const opacityValue = Math.random().toFixed(2); const el = createSVGElement('rect', { 'fill-opacity': opacityValue, }); const obj = await Rect.fromElement(el); expect(obj.fill).toBe(`rgba(0,0,0,${parseFloat(opacityValue)})`); }); it('fill-opacity attribute with fill none', async () => { const opacityValue = Math.random().toFixed(2); const el = createSVGElement('rect', { 'fill-opacity': opacityValue, fill: 'none', }); const obj = await Rect.fromElement(el); expect(obj.fill).toBe(''); }); it('stroke-opacity attribute with stroke attribute', async () => { const opacityValue = Math.random().toFixed(2); const el = createSVGElement('rect', { 'stroke-opacity': opacityValue, stroke: '#FF0000', }); const obj = await Rect.fromElement(el); expect(obj.stroke).toBe(`rgba(255,0,0,${parseFloat(opacityValue)})`); }); it('stroke-opacity attribute without stroke attribute', async () => { const opacityValue = Math.random().toFixed(2); const el = createSVGElement('rect', { 'stroke-opacity': opacityValue, }); const obj = await Rect.fromElement(el); expect(obj.stroke).toBeNull(); }); it('stroke-opacity attribute with stroke none', async () => { const opacityValue = Math.random().toFixed(2); const el = createSVGElement('rect', { 'stroke-opacity': opacityValue, stroke: 'none', }); const obj = await Rect.fromElement(el); expect(obj.stroke).toBe(''); }); it('getCssRule', () => { const rules: Record = {}; // NOTE: We need to use a new fresh document here because vitest in browser mode already adds some stylesheets which pollutes the test const doc = globalThis.document.implementation.createHTMLDocument(''); const svgUid = 'uniqueId'; const styleElement = doc.createElement('style'); styleElement.textContent = 'g polygon.cls, rect {fill:#FF0000; stroke:#000000;stroke-width:0.25px;}\ polygon.cls {fill:none;stroke:#0000FF;}'; doc.body.appendChild(styleElement); const expectedObject = { 'g polygon.cls': { fill: '#FF0000', stroke: '#000000', 'stroke-width': '0.25px', }, rect: { fill: '#FF0000', stroke: '#000000', 'stroke-width': '0.25px', }, 'polygon.cls': { fill: 'none', stroke: '#0000FF', }, }; rules[svgUid] = getCSSRules(doc); expect(rules[svgUid]).toEqual(expectedObject); const elPolygon = createSVGElement('polygon', { points: '10,12 20,22', class: 'cls', svgUid: svgUid, }); const style = parseAttributes(elPolygon, ['fill', 'stroke']); expect(style).toEqual({}); styleElement.textContent = '\t\n'; const reparse = getCSSRules(doc); expect(reparse).toEqual({}); }); it('getCssRule with same selectors', () => { expect(getCSSRules).toBeDefined(); const rules: Record = {}; // NOTE: We need to use a new fresh document here because vitest in browser mode already adds some stylesheets which pollutes the test const doc = globalThis.document.implementation.createHTMLDocument(''); const svgUid = 'uniqueId'; const styleElement = doc.createElement('style'); styleElement.textContent = '.cls1,.cls2 { fill: #FF0000;} .cls1 { stroke: #00FF00;} .cls3,.cls1 { stroke-width: 3;}'; doc.body.appendChild(styleElement); const expectedObject = { '.cls1': { fill: '#FF0000', stroke: '#00FF00', 'stroke-width': '3', }, '.cls2': { fill: '#FF0000', }, '.cls3': { 'stroke-width': '3', }, }; rules[svgUid] = getCSSRules(doc); expect(rules[svgUid]).toEqual(expectedObject); }); it('parseSVGFromString with nested clippath', async () => { const string = '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + '' + ''; const { objects } = await loadSVGFromString(string); expect(objects[0]!.clipPath!.constructor).toHaveProperty('type', 'Polygon'); expect(objects[0]!.clipPath!.clipPath!.constructor).toHaveProperty( 'type', 'Rect', ); }); it('parseSVGFromString with missing clippath', async () => { const string = '' + '' + '' + '' + ''; const { objects } = await loadSVGFromString(string); expect(objects[0]!.clipPath).toBeUndefined(); }); it('parseSVGFromString with empty