@@ -3,7 +3,13 @@ import fs from 'fs';
33import path from 'path' ;
44import { describe , it , expect , afterAll } from 'vitest' ;
55
6- import { parseIconSource , buildIconExportCode , getCreateFluentIconHeader , generatePerIconFiles } from './convert.utils' ;
6+ import {
7+ parseIconSource ,
8+ buildIconExportCode ,
9+ getCreateFluentIconHeader ,
10+ generatePerIconFiles ,
11+ parseSvgToNodes ,
12+ } from './convert.utils' ;
713
814describe ( `convert utils` , ( ) => {
915 describe ( `getCreateFluentIconHeader` , ( ) => {
@@ -13,7 +19,7 @@ describe(`convert utils`, () => {
1319 expect ( header ) . toHaveLength ( 3 ) ;
1420 expect ( header ) . toMatchInlineSnapshot ( `
1521 [
16- ""use client";",
22+ "\ "use client\ ";",
1723 "import type { FluentIcon } from '../utils/createFluentIcon';",
1824 "import { createFluentIcon } from '../utils/createFluentIcon';",
1925 ]
@@ -66,7 +72,7 @@ describe(`convert utils`, () => {
6672 expect ( res ?. iconData ) . toHaveProperty ( 'paths' ) ;
6773 } ) ;
6874
69- it ( 'parses color icon and extracts rawSvg' , ( ) => {
75+ it ( 'parses color icon and extracts rawSvg, nodes ' , ( ) => {
7076 writeFile ( 'ic_fluent_color_test_20_color.svg' , '<svg width="20"><g fill="#000"/><path d="M1 2"/></svg>' ) ;
7177 const res = parseIconSource ( {
7278 file : 'ic_fluent_color_test_20_color.svg' ,
@@ -79,6 +85,7 @@ describe(`convert utils`, () => {
7985 expect ( res ?. exportName ) . toBe ( 'ColorTestColor' ) ;
8086 expect ( res ?. isColor ) . toBe ( true ) ;
8187 expect ( res ?. iconData ) . toHaveProperty ( 'rawSvg' ) ;
88+ expect ( res ?. iconData ) . toHaveProperty ( 'nodes' ) ;
8289 } ) ;
8390
8491 it ( 'sets flipInRtl when metadata indicates mirror' , ( ) => {
@@ -150,22 +157,28 @@ describe(`convert utils`, () => {
150157 expect ( code ) . not . toContain ( 'color: true' ) ;
151158 } ) ;
152159
153- it ( 'generates export code for color icon with rawSvg' , ( ) => {
160+ it ( 'generates export code for color icon with rawSvg and nodes ' , ( ) => {
154161 const code = buildIconExportCode ( {
155162 exportName : 'PatientColor' ,
156163 fileName : 'patient-color.tsx' ,
157- iconData : { rawSvg : '<g fill="#000"/><path d="M1 2" fill="#ff0000"/>' } ,
164+ iconData : {
165+ rawSvg : '<g fill="#000"/><path d="M1 2" fill="#ff0000"/>' ,
166+ nodes : [
167+ [ 'g' , { fill : '#000' } ] ,
168+ [ 'path' , { d : 'M1 2' , fill : '#ff0000' } ] ,
169+ ] ,
170+ } ,
158171 width : '20' ,
159172 isColor : true ,
160173 flipInRtl : false ,
161174 } ) ;
162175 expect ( code ) . toMatchInlineSnapshot (
163176 `
164177 "/** @deprecated Color icons are deprecated. [See User Guidance](https://microsoft.github.io/fluentui-system-icons/?path=/docs/icons-user-guidance--docs#color-variants-deprecated) */
165- export const PatientColor: FluentIcon = (/*#__PURE__*/createFluentIcon('PatientColor', "20", \`<g fill=" #000"/>< path d=" M1 2" fill=" #ff0000"/>\` , { color: true }));"
178+ export const PatientColor: FluentIcon = (/*#__PURE__*/createFluentIcon('PatientColor', \ "20\ ", [[\"g\",{\" fill\":\" #000\"}],[\" path\",{\"d\":\" M1 2\",\" fill\":\" #ff0000\"}]] , { color: true }));"
166179 ` ,
167180 ) ;
168- expect ( code ) . toContain ( 'fill= ' ) ;
181+ expect ( code ) . toContain ( 'fill' ) ;
169182 expect ( code ) . toContain ( 'color: true' ) ;
170183 } ) ;
171184
@@ -324,4 +337,57 @@ describe(`convert utils`, () => {
324337 } ) ;
325338 } ) ;
326339 } ) ;
340+
341+ describe ( 'parseSvgToNodes' , ( ) => {
342+ it ( 'parses self-closing elements' , ( ) => {
343+ const nodes = parseSvgToNodes ( '<path d="M1 2" fill="#000"/>' ) ;
344+ expect ( nodes ) . toEqual ( [ [ 'path' , { d : 'M1 2' , fill : '#000' } ] ] ) ;
345+ } ) ;
346+
347+ it ( 'parses nested elements with children' , ( ) => {
348+ const nodes = parseSvgToNodes ( '<defs><linearGradient id="a"><stop stop-color="#fff"/></linearGradient></defs>' ) ;
349+ expect ( nodes ) . toEqual ( [ [ 'defs' , null , [ 'linearGradient' , { id : 'a' } , [ 'stop' , { stopColor : '#fff' } ] ] ] ] ) ;
350+ } ) ;
351+
352+ it ( 'converts hyphenated SVG attributes to camelCase' , ( ) => {
353+ const nodes = parseSvgToNodes ( '<path fill-opacity="0.5" fill-rule="evenodd" clip-rule="evenodd"/>' ) ;
354+ expect ( nodes ) . toEqual ( [ [ 'path' , { fillOpacity : '0.5' , fillRule : 'evenodd' , clipRule : 'evenodd' } ] ] ) ;
355+ } ) ;
356+
357+ it ( 'parses multiple top-level elements' , ( ) => {
358+ const nodes = parseSvgToNodes ( '<path d="M1"/><path d="M2"/>' ) ;
359+ expect ( nodes ) . toEqual ( [
360+ [ 'path' , { d : 'M1' } ] ,
361+ [ 'path' , { d : 'M2' } ] ,
362+ ] ) ;
363+ } ) ;
364+
365+ it ( 'parses inline style attributes as React-compatible style objects' , ( ) => {
366+ const nodes = parseSvgToNodes (
367+ '<g style="mask-type:alpha"><rect style="mix-blend-mode:multiply;opacity:0.5"/></g>' ,
368+ ) ;
369+ expect ( nodes ) . toEqual ( [
370+ [ 'g' , { style : { maskType : 'alpha' } } , [ 'rect' , { style : { mixBlendMode : 'multiply' , opacity : '0.5' } } ] ] ,
371+ ] ) ;
372+ } ) ;
373+
374+ it ( 'parses a real color icon SVG fragment' , ( ) => {
375+ const svg =
376+ '<path d="M2 8" fill="url(#a)"/><defs><linearGradient id="a" x1="1" gradientUnits="userSpaceOnUse"><stop stop-color="#52D17C"/><stop offset="1" stop-color="#22918B"/></linearGradient></defs>' ;
377+ const nodes = parseSvgToNodes ( svg ) ;
378+ expect ( nodes ) . toEqual ( [
379+ [ 'path' , { d : 'M2 8' , fill : 'url(#a)' } ] ,
380+ [
381+ 'defs' ,
382+ null ,
383+ [
384+ 'linearGradient' ,
385+ { id : 'a' , x1 : '1' , gradientUnits : 'userSpaceOnUse' } ,
386+ [ 'stop' , { stopColor : '#52D17C' } ] ,
387+ [ 'stop' , { offset : '1' , stopColor : '#22918B' } ] ,
388+ ] ,
389+ ] ,
390+ ] ) ;
391+ } ) ;
392+ } ) ;
327393} ) ;
0 commit comments