| import Group from '../graphic/Group'; | 
| import ZRImage from '../graphic/Image'; | 
| import Circle from '../graphic/shape/Circle'; | 
| import Rect from '../graphic/shape/Rect'; | 
| import Ellipse from '../graphic/shape/Ellipse'; | 
| import Line from '../graphic/shape/Line'; | 
| import Polygon from '../graphic/shape/Polygon'; | 
| import Polyline from '../graphic/shape/Polyline'; | 
| import * as matrix from '../core/matrix'; | 
| import { createFromString } from './path'; | 
| import { defaults, trim, each, map, keys, hasOwn } from '../core/util'; | 
| import Displayable from '../graphic/Displayable'; | 
| import Element from '../Element'; | 
| import { RectLike } from '../core/BoundingRect'; | 
| import { Dictionary } from '../core/types'; | 
| import { PatternObject } from '../graphic/Pattern'; | 
| import LinearGradient, { LinearGradientObject } from '../graphic/LinearGradient'; | 
| import RadialGradient, { RadialGradientObject } from '../graphic/RadialGradient'; | 
| import Gradient, { GradientObject } from '../graphic/Gradient'; | 
| import TSpan, { TSpanStyleProps } from '../graphic/TSpan'; | 
| import { parseXML } from './parseXML'; | 
|   | 
|   | 
| interface SVGParserOption { | 
|     // Default width if svg width not specified or is a percent value. | 
|     width?: number; | 
|     // Default height if svg height not specified or is a percent value. | 
|     height?: number; | 
|     ignoreViewBox?: boolean; | 
|     ignoreRootClip?: boolean; | 
| } | 
|   | 
| export interface SVGParserResult { | 
|     // Group, The root of the the result tree of zrender shapes | 
|     root: Group; | 
|     // number, the viewport width of the SVG | 
|     width: number; | 
|     // number, the viewport height of the SVG | 
|     height: number; | 
|     //  {x, y, width, height}, the declared viewBox rect of the SVG, if exists | 
|     viewBoxRect: RectLike; | 
|     // the {scale, position} calculated by viewBox and viewport, is exists | 
|     viewBoxTransform: { | 
|         x: number; | 
|         y: number; | 
|         scale: number; | 
|     }; | 
|     named: SVGParserResultNamedItem[]; | 
| } | 
| export interface SVGParserResultNamedItem { | 
|     name: string; | 
|     // If a tag has no name attribute but its ancester <g> is named, | 
|     // `namedFrom` is set to the named item of the ancester <g>. | 
|     // Otherwise null/undefined | 
|     namedFrom: SVGParserResultNamedItem; | 
|     svgNodeTagLower: SVGNodeTagLower; | 
|     el: Element; | 
| }; | 
|   | 
| export type SVGNodeTagLower = | 
|     'g' | 'rect' | 'circle' | 'line' | 'ellipse' | 'polygon' | 
|     | 'polyline' | 'image' | 'text' | 'tspan' | 'path' | 'defs' | 'switch'; | 
|   | 
|   | 
| type DefsId = string; | 
| type DefsMap = { [id in DefsId]: LinearGradientObject | RadialGradientObject | PatternObject }; | 
| type DefsUsePending = [Displayable, 'fill' | 'stroke', DefsId][]; | 
|   | 
| type ElementExtended = Element & { | 
|     __inheritedStyle?: InheritedStyleByZRKey; | 
|     __selfStyle?: SelfStyleByZRKey; | 
| } | 
| type DisplayableExtended = Displayable & { | 
|     __inheritedStyle?: InheritedStyleByZRKey; | 
|     __selfStyle?: SelfStyleByZRKey; | 
| } | 
|   | 
| type TextStyleOptionExtended = TSpanStyleProps & { | 
|     fontSize: number; | 
|     fontFamily: string; | 
|     fontWeight: string; | 
|     fontStyle: string; | 
| } | 
| let nodeParsers: {[name in SVGNodeTagLower]?: ( | 
|     this: SVGParser, xmlNode: SVGElement, parentGroup: Group | 
| ) => Element}; | 
|   | 
| type InheritedStyleByZRKey = {[name in InheritableStyleZRKey]?: string}; | 
| type InheritableStyleZRKey = | 
|     typeof INHERITABLE_STYLE_ATTRIBUTES_MAP[keyof typeof INHERITABLE_STYLE_ATTRIBUTES_MAP]; | 
| const INHERITABLE_STYLE_ATTRIBUTES_MAP = { | 
|     'fill': 'fill', | 
|     'stroke': 'stroke', | 
|     'stroke-width': 'lineWidth', | 
|     'opacity': 'opacity', | 
|     'fill-opacity': 'fillOpacity', | 
|     'stroke-opacity': 'strokeOpacity', | 
|     'stroke-dasharray': 'lineDash', | 
|     'stroke-dashoffset': 'lineDashOffset', | 
|     'stroke-linecap': 'lineCap', | 
|     'stroke-linejoin': 'lineJoin', | 
|     'stroke-miterlimit': 'miterLimit', | 
|     'font-family': 'fontFamily', | 
|     'font-size': 'fontSize', | 
|     'font-style': 'fontStyle', | 
|     'font-weight': 'fontWeight', | 
|     'text-anchor': 'textAlign', | 
|     'visibility': 'visibility', | 
|     'display': 'display' | 
| } as const; | 
| const INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS = keys(INHERITABLE_STYLE_ATTRIBUTES_MAP); | 
|   | 
| type SelfStyleByZRKey = {[name in SelfStyleZRKey]?: string}; | 
| type SelfStyleZRKey = | 
|     typeof SELF_STYLE_ATTRIBUTES_MAP[keyof typeof SELF_STYLE_ATTRIBUTES_MAP]; | 
| const SELF_STYLE_ATTRIBUTES_MAP = { | 
|     'alignment-baseline': 'textBaseline', | 
|     'stop-color': 'stopColor' | 
| }; | 
| const SELF_STYLE_ATTRIBUTES_MAP_KEYS = keys(SELF_STYLE_ATTRIBUTES_MAP); | 
|   | 
|   | 
| class SVGParser { | 
|   | 
|     private _defs: DefsMap = {}; | 
|     // The use of <defs> can be in front of <defs> declared. | 
|     // So save them temporarily in `_defsUsePending`. | 
|     private _defsUsePending: DefsUsePending; | 
|     private _root: Group = null; | 
|   | 
|     private _textX: number; | 
|     private _textY: number; | 
|   | 
|     parse(xml: string | Document | SVGElement, opt: SVGParserOption): SVGParserResult { | 
|         opt = opt || {}; | 
|   | 
|         const svg = parseXML(xml); | 
|   | 
|         if (process.env.NODE_ENV !== 'production') { | 
|             if (!svg) { | 
|                 throw new Error('Illegal svg'); | 
|             } | 
|         } | 
|   | 
|         this._defsUsePending = []; | 
|         let root = new Group(); | 
|         this._root = root; | 
|         const named: SVGParserResult['named'] = []; | 
|         // parse view port | 
|         const viewBox = svg.getAttribute('viewBox') || ''; | 
|   | 
|         // If width/height not specified, means "100%" of `opt.width/height`. | 
|         // TODO: Other percent value not supported yet. | 
|         let width = parseFloat((svg.getAttribute('width') || opt.width) as string); | 
|         let height = parseFloat((svg.getAttribute('height') || opt.height) as string); | 
|         // If width/height not specified, set as null for output. | 
|         isNaN(width) && (width = null); | 
|         isNaN(height) && (height = null); | 
|   | 
|         // Apply inline style on svg element. | 
|         parseAttributes(svg, root, null, true, false); | 
|   | 
|         let child = svg.firstChild as SVGElement; | 
|         while (child) { | 
|             this._parseNode(child, root, named, null, false, false); | 
|             child = child.nextSibling as SVGElement; | 
|         } | 
|   | 
|         applyDefs(this._defs, this._defsUsePending); | 
|         this._defsUsePending = []; | 
|   | 
|         let viewBoxRect; | 
|         let viewBoxTransform; | 
|   | 
|         if (viewBox) { | 
|             const viewBoxArr = splitNumberSequence(viewBox); | 
|             // Some invalid case like viewBox: 'none'. | 
|             if (viewBoxArr.length >= 4) { | 
|                 viewBoxRect = { | 
|                     x: parseFloat((viewBoxArr[0] || 0) as string), | 
|                     y: parseFloat((viewBoxArr[1] || 0) as string), | 
|                     width: parseFloat(viewBoxArr[2]), | 
|                     height: parseFloat(viewBoxArr[3]) | 
|                 }; | 
|             } | 
|         } | 
|   | 
|         if (viewBoxRect && width != null && height != null) { | 
|             viewBoxTransform = makeViewBoxTransform(viewBoxRect, { x: 0, y: 0, width: width, height: height }); | 
|   | 
|             if (!opt.ignoreViewBox) { | 
|                 // If set transform on the output group, it probably bring trouble when | 
|                 // some users only intend to show the clipped content inside the viewBox, | 
|                 // but not intend to transform the output group. So we keep the output | 
|                 // group no transform. If the user intend to use the viewBox as a | 
|                 // camera, just set `opt.ignoreViewBox` as `true` and set transfrom | 
|                 // manually according to the viewBox info in the output of this method. | 
|                 const elRoot = root; | 
|                 root = new Group(); | 
|                 root.add(elRoot); | 
|                 elRoot.scaleX = elRoot.scaleY = viewBoxTransform.scale; | 
|                 elRoot.x = viewBoxTransform.x; | 
|                 elRoot.y = viewBoxTransform.y; | 
|             } | 
|         } | 
|   | 
|         // Some shapes might be overflow the viewport, which should be | 
|         // clipped despite whether the viewBox is used, as the SVG does. | 
|         if (!opt.ignoreRootClip && width != null && height != null) { | 
|             root.setClipPath(new Rect({ | 
|                 shape: {x: 0, y: 0, width: width, height: height} | 
|             })); | 
|         } | 
|   | 
|         // Set width/height on group just for output the viewport size. | 
|         return { | 
|             root: root, | 
|             width: width, | 
|             height: height, | 
|             viewBoxRect: viewBoxRect, | 
|             viewBoxTransform: viewBoxTransform, | 
|             named: named | 
|         }; | 
|     } | 
|   | 
|     private _parseNode( | 
|         xmlNode: SVGElement, | 
|         parentGroup: Group, | 
|         named: SVGParserResultNamedItem[], | 
|         namedFrom: SVGParserResultNamedItem['namedFrom'], | 
|         isInDefs: boolean, | 
|         isInText: boolean | 
|     ): void { | 
|   | 
|         const nodeName = xmlNode.nodeName.toLowerCase() as SVGNodeTagLower; | 
|   | 
|         // TODO: | 
|         // support <style>...</style> in svg, where nodeName is 'style', | 
|         // CSS classes is defined globally wherever the style tags are declared. | 
|   | 
|         let el; | 
|         let namedFromForSub = namedFrom; | 
|   | 
|         if (nodeName === 'defs') { | 
|             isInDefs = true; | 
|         } | 
|         if (nodeName === 'text') { | 
|             isInText = true; | 
|         } | 
|   | 
|         if (nodeName === 'defs' || nodeName === 'switch') { | 
|             // Just make <switch> displayable. Do not support | 
|             // the full feature of it. | 
|             el = parentGroup; | 
|         } | 
|         else { | 
|             // In <defs>, elments will not be rendered. | 
|             // TODO: | 
|             // do not support elements in <defs> yet, until requirement come. | 
|             // other graphic elements can also be in <defs> and referenced by | 
|             // <use x="5" y="5" xlink:href="#myCircle" /> | 
|             // multiple times | 
|             if (!isInDefs) { | 
|                 const parser = nodeParsers[nodeName]; | 
|                 if (parser && hasOwn(nodeParsers, nodeName)) { | 
|   | 
|                     el = parser.call(this, xmlNode, parentGroup); | 
|   | 
|                     // Do not support empty string; | 
|                     const nameAttr = xmlNode.getAttribute('name'); | 
|                     if (nameAttr) { | 
|                         const newNamed: SVGParserResultNamedItem = { | 
|                             name: nameAttr, | 
|                             namedFrom: null, | 
|                             svgNodeTagLower: nodeName, | 
|                             el: el | 
|                         }; | 
|                         named.push(newNamed); | 
|                         if (nodeName === 'g') { | 
|                             namedFromForSub = newNamed; | 
|                         } | 
|                     } | 
|                     else if (namedFrom) { | 
|                         named.push({ | 
|                             name: namedFrom.name, | 
|                             namedFrom: namedFrom, | 
|                             svgNodeTagLower: nodeName, | 
|                             el: el | 
|                         }); | 
|                     } | 
|   | 
|                     parentGroup.add(el); | 
|                 } | 
|             } | 
|   | 
|             // Whether gradients/patterns are declared in <defs> or not, | 
|             // they all work. | 
|             const parser = paintServerParsers[nodeName]; | 
|             if (parser && hasOwn(paintServerParsers, nodeName)) { | 
|                 const def = parser.call(this, xmlNode); | 
|                 const id = xmlNode.getAttribute('id'); | 
|                 if (id) { | 
|                     this._defs[id] = def; | 
|                 } | 
|             } | 
|         } | 
|   | 
|         // If xmlNode is <g>, <text>, <tspan>, <defs>, <switch>, | 
|         // el will be a group, and traverse the children. | 
|         if (el && el.isGroup) { | 
|             let child = xmlNode.firstChild as SVGElement; | 
|             while (child) { | 
|                 if (child.nodeType === 1) { | 
|                     this._parseNode(child, el as Group, named, namedFromForSub, isInDefs, isInText); | 
|                 } | 
|                 // Is plain text rather than a tagged node. | 
|                 else if (child.nodeType === 3 && isInText) { | 
|                     this._parseText(child, el as Group); | 
|                 } | 
|                 child = child.nextSibling as SVGElement; | 
|             } | 
|         } | 
|   | 
|     } | 
|   | 
|     private _parseText(xmlNode: SVGElement, parentGroup: Group): TSpan { | 
|         const text = new TSpan({ | 
|             style: { | 
|                 text: xmlNode.textContent | 
|             }, | 
|             silent: true, | 
|             x: this._textX || 0, | 
|             y: this._textY || 0 | 
|         }); | 
|   | 
|         inheritStyle(parentGroup, text); | 
|   | 
|         parseAttributes(xmlNode, text, this._defsUsePending, false, false); | 
|   | 
|         applyTextAlignment(text, parentGroup); | 
|   | 
|         const textStyle = text.style as TextStyleOptionExtended; | 
|         const fontSize = textStyle.fontSize; | 
|         if (fontSize && fontSize < 9) { | 
|             // PENDING | 
|             textStyle.fontSize = 9; | 
|             text.scaleX *= fontSize / 9; | 
|             text.scaleY *= fontSize / 9; | 
|         } | 
|   | 
|         const font = (textStyle.fontSize || textStyle.fontFamily) && [ | 
|             textStyle.fontStyle, | 
|             textStyle.fontWeight, | 
|             (textStyle.fontSize || 12) + 'px', | 
|             // If font properties are defined, `fontFamily` should not be ignored. | 
|             textStyle.fontFamily || 'sans-serif' | 
|         ].join(' '); | 
|         // Make font | 
|         textStyle.font = font; | 
|   | 
|         const rect = text.getBoundingRect(); | 
|         this._textX += rect.width; | 
|   | 
|         parentGroup.add(text); | 
|   | 
|         return text; | 
|     } | 
|   | 
|     static internalField = (function () { | 
|   | 
|         nodeParsers = { | 
|             'g': function (xmlNode, parentGroup) { | 
|                 const g = new Group(); | 
|                 inheritStyle(parentGroup, g); | 
|                 parseAttributes(xmlNode, g, this._defsUsePending, false, false); | 
|   | 
|                 return g; | 
|             }, | 
|             'rect': function (xmlNode, parentGroup) { | 
|                 const rect = new Rect(); | 
|                 inheritStyle(parentGroup, rect); | 
|                 parseAttributes(xmlNode, rect, this._defsUsePending, false, false); | 
|   | 
|                 rect.setShape({ | 
|                     x: parseFloat(xmlNode.getAttribute('x') || '0'), | 
|                     y: parseFloat(xmlNode.getAttribute('y') || '0'), | 
|                     width: parseFloat(xmlNode.getAttribute('width') || '0'), | 
|                     height: parseFloat(xmlNode.getAttribute('height') || '0') | 
|                 }); | 
|   | 
|                 rect.silent = true; | 
|   | 
|                 return rect; | 
|             }, | 
|             'circle': function (xmlNode, parentGroup) { | 
|                 const circle = new Circle(); | 
|                 inheritStyle(parentGroup, circle); | 
|                 parseAttributes(xmlNode, circle, this._defsUsePending, false, false); | 
|   | 
|                 circle.setShape({ | 
|                     cx: parseFloat(xmlNode.getAttribute('cx') || '0'), | 
|                     cy: parseFloat(xmlNode.getAttribute('cy') || '0'), | 
|                     r: parseFloat(xmlNode.getAttribute('r') || '0') | 
|                 }); | 
|   | 
|                 circle.silent = true; | 
|   | 
|                 return circle; | 
|             }, | 
|             'line': function (xmlNode, parentGroup) { | 
|                 const line = new Line(); | 
|                 inheritStyle(parentGroup, line); | 
|                 parseAttributes(xmlNode, line, this._defsUsePending, false, false); | 
|   | 
|                 line.setShape({ | 
|                     x1: parseFloat(xmlNode.getAttribute('x1') || '0'), | 
|                     y1: parseFloat(xmlNode.getAttribute('y1') || '0'), | 
|                     x2: parseFloat(xmlNode.getAttribute('x2') || '0'), | 
|                     y2: parseFloat(xmlNode.getAttribute('y2') || '0') | 
|                 }); | 
|   | 
|                 line.silent = true; | 
|   | 
|                 return line; | 
|             }, | 
|             'ellipse': function (xmlNode, parentGroup) { | 
|                 const ellipse = new Ellipse(); | 
|                 inheritStyle(parentGroup, ellipse); | 
|                 parseAttributes(xmlNode, ellipse, this._defsUsePending, false, false); | 
|   | 
|                 ellipse.setShape({ | 
|                     cx: parseFloat(xmlNode.getAttribute('cx') || '0'), | 
|                     cy: parseFloat(xmlNode.getAttribute('cy') || '0'), | 
|                     rx: parseFloat(xmlNode.getAttribute('rx') || '0'), | 
|                     ry: parseFloat(xmlNode.getAttribute('ry') || '0') | 
|                 }); | 
|   | 
|                 ellipse.silent = true; | 
|   | 
|                 return ellipse; | 
|             }, | 
|             'polygon': function (xmlNode, parentGroup) { | 
|                 const pointsStr = xmlNode.getAttribute('points'); | 
|                 let pointsArr; | 
|                 if (pointsStr) { | 
|                     pointsArr = parsePoints(pointsStr); | 
|                 } | 
|                 const polygon = new Polygon({ | 
|                     shape: { | 
|                         points: pointsArr || [] | 
|                     }, | 
|                     silent: true | 
|                 }); | 
|   | 
|                 inheritStyle(parentGroup, polygon); | 
|                 parseAttributes(xmlNode, polygon, this._defsUsePending, false, false); | 
|   | 
|                 return polygon; | 
|             }, | 
|             'polyline': function (xmlNode, parentGroup) { | 
|                 const pointsStr = xmlNode.getAttribute('points'); | 
|                 let pointsArr; | 
|                 if (pointsStr) { | 
|                     pointsArr = parsePoints(pointsStr); | 
|                 } | 
|                 const polyline = new Polyline({ | 
|                     shape: { | 
|                         points: pointsArr || [] | 
|                     }, | 
|                     silent: true | 
|                 }); | 
|   | 
|                 inheritStyle(parentGroup, polyline); | 
|                 parseAttributes(xmlNode, polyline, this._defsUsePending, false, false); | 
|   | 
|                 return polyline; | 
|             }, | 
|             'image': function (xmlNode, parentGroup) { | 
|                 const img = new ZRImage(); | 
|                 inheritStyle(parentGroup, img); | 
|                 parseAttributes(xmlNode, img, this._defsUsePending, false, false); | 
|   | 
|                 img.setStyle({ | 
|                     image: xmlNode.getAttribute('xlink:href') || xmlNode.getAttribute('href'), | 
|                     x: +xmlNode.getAttribute('x'), | 
|                     y: +xmlNode.getAttribute('y'), | 
|                     width: +xmlNode.getAttribute('width'), | 
|                     height: +xmlNode.getAttribute('height') | 
|                 }); | 
|                 img.silent = true; | 
|   | 
|                 return img; | 
|             }, | 
|             'text': function (xmlNode, parentGroup) { | 
|                 const x = xmlNode.getAttribute('x') || '0'; | 
|                 const y = xmlNode.getAttribute('y') || '0'; | 
|                 const dx = xmlNode.getAttribute('dx') || '0'; | 
|                 const dy = xmlNode.getAttribute('dy') || '0'; | 
|   | 
|                 this._textX = parseFloat(x) + parseFloat(dx); | 
|                 this._textY = parseFloat(y) + parseFloat(dy); | 
|   | 
|                 const g = new Group(); | 
|                 inheritStyle(parentGroup, g); | 
|                 parseAttributes(xmlNode, g, this._defsUsePending, false, true); | 
|   | 
|                 return g; | 
|             }, | 
|             'tspan': function (xmlNode, parentGroup) { | 
|                 const x = xmlNode.getAttribute('x'); | 
|                 const y = xmlNode.getAttribute('y'); | 
|                 if (x != null) { | 
|                     // new offset x | 
|                     this._textX = parseFloat(x); | 
|                 } | 
|                 if (y != null) { | 
|                     // new offset y | 
|                     this._textY = parseFloat(y); | 
|                 } | 
|                 const dx = xmlNode.getAttribute('dx') || '0'; | 
|                 const dy = xmlNode.getAttribute('dy') || '0'; | 
|   | 
|                 const g = new Group(); | 
|   | 
|                 inheritStyle(parentGroup, g); | 
|                 parseAttributes(xmlNode, g, this._defsUsePending, false, true); | 
|   | 
|                 this._textX += parseFloat(dx); | 
|                 this._textY += parseFloat(dy); | 
|   | 
|                 return g; | 
|             }, | 
|             'path': function (xmlNode, parentGroup) { | 
|                 // TODO svg fill rule | 
|                 // https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/fill-rule | 
|                 // path.style.globalCompositeOperation = 'xor'; | 
|                 const d = xmlNode.getAttribute('d') || ''; | 
|   | 
|                 // Performance sensitive. | 
|   | 
|                 const path = createFromString(d); | 
|   | 
|                 inheritStyle(parentGroup, path); | 
|                 parseAttributes(xmlNode, path, this._defsUsePending, false, false); | 
|   | 
|                 path.silent = true; | 
|   | 
|                 return path; | 
|             } | 
|         }; | 
|   | 
|   | 
|     })(); | 
| } | 
|   | 
| const paintServerParsers: Dictionary<(xmlNode: SVGElement) => any> = { | 
|   | 
|     'lineargradient': function (xmlNode: SVGElement) { | 
|         // TODO: | 
|         // Support that x1,y1,x2,y2 are not declared lineargradient but in node. | 
|         const x1 = parseInt(xmlNode.getAttribute('x1') || '0', 10); | 
|         const y1 = parseInt(xmlNode.getAttribute('y1') || '0', 10); | 
|         const x2 = parseInt(xmlNode.getAttribute('x2') || '10', 10); | 
|         const y2 = parseInt(xmlNode.getAttribute('y2') || '0', 10); | 
|   | 
|         const gradient = new LinearGradient(x1, y1, x2, y2); | 
|   | 
|         parsePaintServerUnit(xmlNode, gradient); | 
|   | 
|         parseGradientColorStops(xmlNode, gradient); | 
|   | 
|         return gradient; | 
|     }, | 
|   | 
|     'radialgradient': function (xmlNode) { | 
|         // TODO: | 
|         // Support that x1,y1,x2,y2 are not declared radialgradient but in node. | 
|         // TODO: | 
|         // Support fx, fy, fr. | 
|         const cx = parseInt(xmlNode.getAttribute('cx') || '0', 10); | 
|         const cy = parseInt(xmlNode.getAttribute('cy') || '0', 10); | 
|         const r = parseInt(xmlNode.getAttribute('r') || '0', 10); | 
|   | 
|         const gradient = new RadialGradient(cx, cy, r); | 
|   | 
|         parsePaintServerUnit(xmlNode, gradient); | 
|   | 
|         parseGradientColorStops(xmlNode, gradient); | 
|   | 
|         return gradient; | 
|     } | 
|   | 
|     // TODO | 
|     // 'pattern': function (xmlNode: SVGElement) { | 
|     // } | 
| }; | 
|   | 
| function parsePaintServerUnit(xmlNode: SVGElement, gradient: Gradient) { | 
|     const gradientUnits = xmlNode.getAttribute('gradientUnits'); | 
|     if (gradientUnits === 'userSpaceOnUse') { | 
|         gradient.global = true; | 
|     } | 
| } | 
|   | 
| function parseGradientColorStops(xmlNode: SVGElement, gradient: GradientObject): void { | 
|   | 
|     let stop = xmlNode.firstChild as SVGStopElement; | 
|   | 
|     while (stop) { | 
|         if (stop.nodeType === 1 | 
|             // there might be some other irrelevant tags used by editor. | 
|             && stop.nodeName.toLocaleLowerCase() === 'stop' | 
|         ) { | 
|             const offsetStr = stop.getAttribute('offset'); | 
|             let offset: number; | 
|             if (offsetStr && offsetStr.indexOf('%') > 0) {  // percentage | 
|                 offset = parseInt(offsetStr, 10) / 100; | 
|             } | 
|             else if (offsetStr) { // number from 0 to 1 | 
|                 offset = parseFloat(offsetStr); | 
|             } | 
|             else { | 
|                 offset = 0; | 
|             } | 
|   | 
|             // <stop style="stop-color:red"/> has higher priority than | 
|             // <stop stop-color="red"/> | 
|             const styleVals = {} as Dictionary<string>; | 
|             parseInlineStyle(stop, styleVals, styleVals); | 
|             const stopColor = styleVals.stopColor | 
|                 || stop.getAttribute('stop-color') | 
|                 || '#000000'; | 
|   | 
|             gradient.colorStops.push({ | 
|                 offset: offset, | 
|                 color: stopColor | 
|             }); | 
|         } | 
|         stop = stop.nextSibling as SVGStopElement; | 
|     } | 
| } | 
|   | 
| function inheritStyle(parent: Element, child: Element): void { | 
|     if (parent && (parent as ElementExtended).__inheritedStyle) { | 
|         if (!(child as ElementExtended).__inheritedStyle) { | 
|             (child as ElementExtended).__inheritedStyle = {}; | 
|         } | 
|         defaults((child as ElementExtended).__inheritedStyle, (parent as ElementExtended).__inheritedStyle); | 
|     } | 
| } | 
|   | 
| function parsePoints(pointsString: string): number[][] { | 
|     const list = splitNumberSequence(pointsString); | 
|     const points = []; | 
|   | 
|     for (let i = 0; i < list.length; i += 2) { | 
|         const x = parseFloat(list[i]); | 
|         const y = parseFloat(list[i + 1]); | 
|         points.push([x, y]); | 
|     } | 
|     return points; | 
| } | 
|   | 
| function parseAttributes( | 
|     xmlNode: SVGElement, | 
|     el: Element, | 
|     defsUsePending: DefsUsePending, | 
|     onlyInlineStyle: boolean, | 
|     isTextGroup: boolean | 
| ): void { | 
|     const disp = el as DisplayableExtended; | 
|     const inheritedStyle = disp.__inheritedStyle = disp.__inheritedStyle || {}; | 
|     const selfStyle: SelfStyleByZRKey = {}; | 
|   | 
|     // TODO Shadow | 
|     if (xmlNode.nodeType === 1) { | 
|         parseTransformAttribute(xmlNode, el); | 
|   | 
|         parseInlineStyle(xmlNode, inheritedStyle, selfStyle); | 
|   | 
|         if (!onlyInlineStyle) { | 
|             parseAttributeStyle(xmlNode, inheritedStyle, selfStyle); | 
|         } | 
|     } | 
|   | 
|     disp.style = disp.style || {}; | 
|   | 
|     if (inheritedStyle.fill != null) { | 
|         disp.style.fill = getFillStrokeStyle(disp, 'fill', inheritedStyle.fill, defsUsePending); | 
|     } | 
|     if (inheritedStyle.stroke != null) { | 
|         disp.style.stroke = getFillStrokeStyle(disp, 'stroke', inheritedStyle.stroke, defsUsePending); | 
|     } | 
|   | 
|     each([ | 
|         'lineWidth', 'opacity', 'fillOpacity', 'strokeOpacity', 'miterLimit', 'fontSize' | 
|     ] as const, function (propName) { | 
|         if (inheritedStyle[propName] != null) { | 
|             disp.style[propName] = parseFloat(inheritedStyle[propName]); | 
|         } | 
|     }); | 
|   | 
|     each([ | 
|         'lineDashOffset', 'lineCap', 'lineJoin', 'fontWeight', 'fontFamily', 'fontStyle', 'textAlign' | 
|     ] as const, function (propName) { | 
|         if (inheritedStyle[propName] != null) { | 
|             disp.style[propName] = inheritedStyle[propName]; | 
|         } | 
|     }); | 
|   | 
|     // Because selfStyle only support textBaseline, so only text group need it. | 
|     // in other cases selfStyle can be released. | 
|     if (isTextGroup) { | 
|         disp.__selfStyle = selfStyle; | 
|     } | 
|   | 
|     if (inheritedStyle.lineDash) { | 
|         disp.style.lineDash = map(splitNumberSequence(inheritedStyle.lineDash), function (str) { | 
|             return parseFloat(str); | 
|         }); | 
|     } | 
|   | 
|     if (inheritedStyle.visibility === 'hidden' || inheritedStyle.visibility === 'collapse') { | 
|         disp.invisible = true; | 
|     } | 
|   | 
|     if (inheritedStyle.display === 'none') { | 
|         disp.ignore = true; | 
|     } | 
| } | 
|   | 
| function applyTextAlignment( | 
|     text: TSpan, | 
|     parentGroup: Group | 
| ): void { | 
|     const parentSelfStyle = (parentGroup as ElementExtended).__selfStyle; | 
|     if (parentSelfStyle) { | 
|         const textBaseline = parentSelfStyle.textBaseline; | 
|         let zrTextBaseline = textBaseline as CanvasTextBaseline; | 
|         if (!textBaseline || textBaseline === 'auto') { | 
|             // FIXME: 'auto' means the value is the dominant-baseline of the script to | 
|             // which the character belongs - i.e., use the dominant-baseline of the parent. | 
|             zrTextBaseline = 'alphabetic'; | 
|         } | 
|         else if (textBaseline === 'baseline') { | 
|             zrTextBaseline = 'alphabetic'; | 
|         } | 
|         else if (textBaseline === 'before-edge' || textBaseline === 'text-before-edge') { | 
|             zrTextBaseline = 'top'; | 
|         } | 
|         else if (textBaseline === 'after-edge' || textBaseline === 'text-after-edge') { | 
|             zrTextBaseline = 'bottom'; | 
|         } | 
|         else if (textBaseline === 'central' || textBaseline === 'mathematical') { | 
|             zrTextBaseline = 'middle'; | 
|         } | 
|         text.style.textBaseline = zrTextBaseline; | 
|     } | 
|   | 
|     const parentInheritedStyle = (parentGroup as ElementExtended).__inheritedStyle; | 
|     if (parentInheritedStyle) { | 
|         // PENDING: | 
|         // canvas `direction` is an experimental attribute. | 
|         // so we do not support SVG direction "rtl" for text-anchor yet. | 
|         const textAlign = parentInheritedStyle.textAlign; | 
|         let zrTextAlign = textAlign as CanvasTextAlign; | 
|         if (textAlign) { | 
|             if (textAlign === 'middle') { | 
|                 zrTextAlign = 'center'; | 
|             } | 
|             text.style.textAlign = zrTextAlign; | 
|         } | 
|     } | 
| } | 
|   | 
| // Support `fill:url(#someId)`. | 
| const urlRegex = /^url\(\s*#(.*?)\)/; | 
| function getFillStrokeStyle( | 
|     el: Displayable, | 
|     method: 'fill' | 'stroke', | 
|     str: string, | 
|     defsUsePending: DefsUsePending | 
| ): string { | 
|     const urlMatch = str && str.match(urlRegex); | 
|     if (urlMatch) { | 
|         const url = trim(urlMatch[1]); | 
|         defsUsePending.push([el, method, url]); | 
|         return; | 
|     } | 
|     // SVG fill and stroke can be 'none'. | 
|     if (str === 'none') { | 
|         str = null; | 
|     } | 
|     return str; | 
| } | 
|   | 
| function applyDefs( | 
|     defs: DefsMap, | 
|     defsUsePending: DefsUsePending | 
| ): void { | 
|     for (let i = 0; i < defsUsePending.length; i++) { | 
|         const item = defsUsePending[i]; | 
|         item[0].style[item[1]] = defs[item[2]]; | 
|     } | 
| } | 
|   | 
| // value can be like: | 
| // '2e-4', 'l.5.9' (ignore 0), 'M-10-10', 'l-2.43e-1,34.9983', | 
| // 'l-.5E1,54', '121-23-44-11' (no delimiter) | 
| // PENDING: here continuous commas are treat as one comma, but the | 
| // browser SVG parser treats this by printing error. | 
| const numberReg = /-?([0-9]*\.)?[0-9]+([eE]-?[0-9]+)?/g; | 
| function splitNumberSequence(rawStr: string): string[] { | 
|     return rawStr.match(numberReg) || []; | 
| } | 
| // Most of the values can be separated by comma and/or white space. | 
| // const DILIMITER_REG = /[\s,]+/; | 
|   | 
|   | 
| const transformRegex = /(translate|scale|rotate|skewX|skewY|matrix)\(([\-\s0-9\.eE,]*)\)/g; | 
| const DEGREE_TO_ANGLE = Math.PI / 180; | 
|   | 
| function parseTransformAttribute(xmlNode: SVGElement, node: Element): void { | 
|     let transform = xmlNode.getAttribute('transform'); | 
|     if (transform) { | 
|         transform = transform.replace(/,/g, ' '); | 
|         const transformOps: string[] = []; | 
|         let mt = null; | 
|         transform.replace(transformRegex, function (str: string, type: string, value: string) { | 
|             transformOps.push(type, value); | 
|             return ''; | 
|         }); | 
|   | 
|         for (let i = transformOps.length - 1; i > 0; i -= 2) { | 
|             const value = transformOps[i]; | 
|             const type = transformOps[i - 1]; | 
|             const valueArr: string[] = splitNumberSequence(value); | 
|             mt = mt || matrix.create(); | 
|             switch (type) { | 
|                 case 'translate': | 
|                     matrix.translate(mt, mt, [parseFloat(valueArr[0]), parseFloat(valueArr[1] || '0')]); | 
|                     break; | 
|                 case 'scale': | 
|                     matrix.scale(mt, mt, [parseFloat(valueArr[0]), parseFloat(valueArr[1] || valueArr[0])]); | 
|                     break; | 
|                 case 'rotate': | 
|                     // TODO: zrender use different hand in coordinate system. | 
|                     matrix.rotate(mt, mt, -parseFloat(valueArr[0]) * DEGREE_TO_ANGLE, [ | 
|                         parseFloat(valueArr[1] || '0'), | 
|                         parseFloat(valueArr[2] || '0') | 
|                     ]); | 
|                     break; | 
|                 case 'skewX': | 
|                     const sx = Math.tan(parseFloat(valueArr[0]) * DEGREE_TO_ANGLE); | 
|                     matrix.mul(mt, [1, 0, sx, 1, 0, 0], mt); | 
|                     break; | 
|                 case 'skewY': | 
|                     const sy = Math.tan(parseFloat(valueArr[0]) * DEGREE_TO_ANGLE); | 
|                     matrix.mul(mt, [1, sy, 0, 1, 0, 0], mt); | 
|                     break; | 
|                 case 'matrix': | 
|                     mt[0] = parseFloat(valueArr[0]); | 
|                     mt[1] = parseFloat(valueArr[1]); | 
|                     mt[2] = parseFloat(valueArr[2]); | 
|                     mt[3] = parseFloat(valueArr[3]); | 
|                     mt[4] = parseFloat(valueArr[4]); | 
|                     mt[5] = parseFloat(valueArr[5]); | 
|                     break; | 
|             } | 
|         } | 
|         node.setLocalTransform(mt); | 
|     } | 
| } | 
|   | 
| // Value may contain space. | 
| const styleRegex = /([^\s:;]+)\s*:\s*([^:;]+)/g; | 
| function parseInlineStyle( | 
|     xmlNode: SVGElement, | 
|     inheritableStyleResult: Dictionary<string>, | 
|     selfStyleResult: Dictionary<string> | 
| ): void { | 
|     const style = xmlNode.getAttribute('style'); | 
|   | 
|     if (!style) { | 
|         return; | 
|     } | 
|   | 
|     styleRegex.lastIndex = 0; | 
|     let styleRegResult; | 
|     while ((styleRegResult = styleRegex.exec(style)) != null) { | 
|         const svgStlAttr = styleRegResult[1]; | 
|   | 
|         const zrInheritableStlAttr = hasOwn(INHERITABLE_STYLE_ATTRIBUTES_MAP, svgStlAttr) | 
|             ? INHERITABLE_STYLE_ATTRIBUTES_MAP[svgStlAttr as keyof typeof INHERITABLE_STYLE_ATTRIBUTES_MAP] | 
|             : null; | 
|         if (zrInheritableStlAttr) { | 
|             inheritableStyleResult[zrInheritableStlAttr] = styleRegResult[2]; | 
|         } | 
|   | 
|         const zrSelfStlAttr = hasOwn(SELF_STYLE_ATTRIBUTES_MAP, svgStlAttr) | 
|             ? SELF_STYLE_ATTRIBUTES_MAP[svgStlAttr as keyof typeof SELF_STYLE_ATTRIBUTES_MAP] | 
|             : null; | 
|         if (zrSelfStlAttr) { | 
|             selfStyleResult[zrSelfStlAttr] = styleRegResult[2]; | 
|         } | 
|     } | 
| } | 
|   | 
| function parseAttributeStyle( | 
|     xmlNode: SVGElement, | 
|     inheritableStyleResult: Dictionary<string>, | 
|     selfStyleResult: Dictionary<string> | 
| ): void { | 
|     for (let i = 0; i < INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS.length; i++) { | 
|         const svgAttrName = INHERITABLE_STYLE_ATTRIBUTES_MAP_KEYS[i]; | 
|         const attrValue = xmlNode.getAttribute(svgAttrName); | 
|         if (attrValue != null) { | 
|             inheritableStyleResult[INHERITABLE_STYLE_ATTRIBUTES_MAP[svgAttrName]] = attrValue; | 
|         } | 
|     } | 
|     for (let i = 0; i < SELF_STYLE_ATTRIBUTES_MAP_KEYS.length; i++) { | 
|         const svgAttrName = SELF_STYLE_ATTRIBUTES_MAP_KEYS[i]; | 
|         const attrValue = xmlNode.getAttribute(svgAttrName); | 
|         if (attrValue != null) { | 
|             selfStyleResult[SELF_STYLE_ATTRIBUTES_MAP[svgAttrName]] = attrValue; | 
|         } | 
|     } | 
| } | 
|   | 
| export function makeViewBoxTransform(viewBoxRect: RectLike, boundingRect: RectLike): { | 
|     scale: number; | 
|     x: number; | 
|     y: number; | 
| } { | 
|     const scaleX = boundingRect.width / viewBoxRect.width; | 
|     const scaleY = boundingRect.height / viewBoxRect.height; | 
|     const scale = Math.min(scaleX, scaleY); | 
|     // preserveAspectRatio 'xMidYMid' | 
|   | 
|     return { | 
|         scale, | 
|         x: -(viewBoxRect.x + viewBoxRect.width / 2) * scale + (boundingRect.x + boundingRect.width / 2), | 
|         y: -(viewBoxRect.y + viewBoxRect.height / 2) * scale + (boundingRect.y + boundingRect.height / 2) | 
|     }; | 
| } | 
|   | 
| export function parseSVG(xml: string | Document | SVGElement, opt: SVGParserOption): SVGParserResult { | 
|     const parser = new SVGParser(); | 
|     return parser.parse(xml, opt); | 
| } | 
|   | 
|   | 
| // Also export parseXML to avoid breaking change. | 
| export {parseXML}; |