From 92e821ab80cb6d44be6a49e886f68bf6343f6e5a Mon Sep 17 00:00:00 2001 From: bohdanprog Date: Tue, 30 Jul 2024 13:28:03 +0200 Subject: [PATCH 01/10] fix: Require cycles are allowed, but can result in uninitialized values. Consider refactoring to remove the need for a cycle. --- src/ReactNativeSVG.ts | 62 +++++++++++++++++++++++-------------------- src/tags.tsx | 59 +++++++++++++++++++++------------------- src/xml.tsx | 2 +- 3 files changed, 65 insertions(+), 58 deletions(-) diff --git a/src/ReactNativeSVG.ts b/src/ReactNativeSVG.ts index 89aea0bf7..676010ca5 100644 --- a/src/ReactNativeSVG.ts +++ b/src/ReactNativeSVG.ts @@ -1,32 +1,4 @@ -import Shape from './elements/Shape'; -import Rect from './elements/Rect'; -import Circle from './elements/Circle'; -import Ellipse from './elements/Ellipse'; -import Polygon from './elements/Polygon'; -import Polyline from './elements/Polyline'; -import Line from './elements/Line'; -import Svg from './elements/Svg'; -import Path from './elements/Path'; -import G from './elements/G'; -import Text from './elements/Text'; -import TSpan from './elements/TSpan'; -import TextPath from './elements/TextPath'; -import Use from './elements/Use'; -import Image from './elements/Image'; -import Symbol from './elements/Symbol'; -import Defs from './elements/Defs'; -import LinearGradient from './elements/LinearGradient'; -import RadialGradient from './elements/RadialGradient'; -import Stop from './elements/Stop'; -import ClipPath from './elements/ClipPath'; -import Pattern from './elements/Pattern'; -import Mask from './elements/Mask'; -import Marker from './elements/Marker'; -import ForeignObject from './elements/ForeignObject'; -import Filter from './elements/filters/Filter'; -import FeColorMatrix from './elements/filters/FeColorMatrix'; -import FeGaussianBlur from './elements/filters/FeGaussianBlur'; -import FeOffset from './elements/filters/FeOffset'; +import { tags } from './tags'; import { parse, @@ -77,6 +49,38 @@ import { RNSVGFeOffset, } from './fabric'; +const { + circle: Circle, + clipPath: ClipPath, + defs: Defs, + ellipse: Ellipse, + filter: Filter, + feColorMatrix: FeColorMatrix, + feGaussianBlur: FeGaussianBlur, + feOffset: FeOffset, + g: G, + image: Image, + line: Line, + linearGradient: LinearGradient, + marker: Marker, + mask: Mask, + path: Path, + pattern: Pattern, + polygon: Polygon, + polyline: Polyline, + radialGradient: RadialGradient, + rect: Rect, + stop: Stop, + svg: Svg, + symbol: Symbol, + text: Text, + textPath: TextPath, + tspan: TSpan, + use: Use, + foreignObject: ForeignObject, + Shape, +} = tags; + export { SvgCss, SvgCssUri, diff --git a/src/tags.tsx b/src/tags.tsx index d029e2ef4..184a60354 100644 --- a/src/tags.tsx +++ b/src/tags.tsx @@ -1,31 +1,32 @@ -import Svg, { - Circle, - ClipPath, - Defs, - Ellipse, - G, - Image, - Line, - LinearGradient, - Marker, - Mask, - Path, - Pattern, - Polygon, - Polyline, - RadialGradient, - Rect, - Stop, - Text, - TextPath, - TSpan, - Use, - Symbol, - Filter, - FeColorMatrix, - FeGaussianBlur, - FeOffset, -} from './ReactNativeSVG'; +import Shape from './elements/Shape'; +import Rect from './elements/Rect'; +import Circle from './elements/Circle'; +import Ellipse from './elements/Ellipse'; +import Polygon from './elements/Polygon'; +import Polyline from './elements/Polyline'; +import Line from './elements/Line'; +import Svg from './elements/Svg'; +import Path from './elements/Path'; +import G from './elements/G'; +import Text from './elements/Text'; +import TSpan from './elements/TSpan'; +import TextPath from './elements/TextPath'; +import Use from './elements/Use'; +import Image from './elements/Image'; +import Symbol from './elements/Symbol'; +import Defs from './elements/Defs'; +import LinearGradient from './elements/LinearGradient'; +import RadialGradient from './elements/RadialGradient'; +import Stop from './elements/Stop'; +import ClipPath from './elements/ClipPath'; +import Pattern from './elements/Pattern'; +import Mask from './elements/Mask'; +import Marker from './elements/Marker'; +import ForeignObject from './elements/ForeignObject'; +import Filter from './elements/filters/Filter'; +import FeColorMatrix from './elements/filters/FeColorMatrix'; +import FeGaussianBlur from './elements/filters/FeGaussianBlur'; +import FeOffset from './elements/filters/FeOffset'; export const tags = { circle: Circle, @@ -55,4 +56,6 @@ export const tags = { textPath: TextPath, tspan: TSpan, use: Use, + foreignObject: ForeignObject, + Shape, } as const; diff --git a/src/xml.tsx b/src/xml.tsx index d0bb5e087..ca0de7a0d 100644 --- a/src/xml.tsx +++ b/src/xml.tsx @@ -19,7 +19,7 @@ export interface AST { props: { [prop: string]: Styles | string | undefined; }; - Tag: Tag; + Tag: ComponentType; } export interface XmlAST extends AST { From aab003c282696d5e5562737868a6d303f1ea285d Mon Sep 17 00:00:00 2001 From: bohdanprog Date: Tue, 30 Jul 2024 15:01:48 +0200 Subject: [PATCH 02/10] feat: extract svg components, utils function, and types --- src/ReactNativeSVG.ts | 2 +- src/ReactNativeSVG.web.ts | 688 +++++--------------------------------- src/tags.tsx | 2 - src/tags.web.ts | 60 ++++ src/web/WebShape.ts | 102 ++++++ src/web/elements.ts | 207 ++++++++++++ src/web/types.ts | 66 ++++ src/web/utils/index.ts | 113 +++++++ src/web/utils/prepare.ts | 127 +++++++ src/xml.tsx | 2 +- 10 files changed, 767 insertions(+), 602 deletions(-) create mode 100644 src/tags.web.ts create mode 100644 src/web/WebShape.ts create mode 100644 src/web/elements.ts create mode 100644 src/web/types.ts create mode 100644 src/web/utils/index.ts create mode 100644 src/web/utils/prepare.ts diff --git a/src/ReactNativeSVG.ts b/src/ReactNativeSVG.ts index 676010ca5..fff08edc3 100644 --- a/src/ReactNativeSVG.ts +++ b/src/ReactNativeSVG.ts @@ -1,4 +1,5 @@ import { tags } from './tags'; +import Shape from './elements/Shape'; import { parse, @@ -78,7 +79,6 @@ const { tspan: TSpan, use: Use, foreignObject: ForeignObject, - Shape, } = tags; export { diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index 335bebf04..f6f34cfa5 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -1,601 +1,93 @@ -import * as React from 'react'; -import type { CircleProps } from './elements/Circle'; -import type { ClipPathProps } from './elements/ClipPath'; -import type { EllipseProps } from './elements/Ellipse'; -import type { ForeignObjectProps } from './elements/ForeignObject'; -import type { GProps } from './elements/G'; -import type { ImageProps } from './elements/Image'; -import type { LineProps } from './elements/Line'; -import type { LinearGradientProps } from './elements/LinearGradient'; -import type { MarkerProps } from './elements/Marker'; -import type { MaskProps } from './elements/Mask'; -import type { PathProps } from './elements/Path'; -import type { PatternProps } from './elements/Pattern'; -import type { PolygonProps } from './elements/Polygon'; -import type { PolylineProps } from './elements/Polyline'; -import type { RadialGradientProps } from './elements/RadialGradient'; -import type { RectProps } from './elements/Rect'; -import type { StopProps } from './elements/Stop'; -import type { SvgProps } from './elements/Svg'; -import type { SymbolProps } from './elements/Symbol'; -import type { TextProps } from './elements/Text'; -import type { TextPathProps } from './elements/TextPath'; -import type { TSpanProps } from './elements/TSpan'; -import type { UseProps } from './elements/Use'; -import type { FilterProps } from './elements/filters/Filter'; -import type { FeColorMatrixProps } from './elements/filters/FeColorMatrix'; -import type { FeGaussianBlurProps } from './elements/filters/FeGaussianBlur'; -import type { FeOffsetProps } from './elements/filters/FeOffset'; -import type { - GestureResponderEvent, - ImageProps as RNImageProps, -} from 'react-native'; -import { - // @ts-ignore it is not seen in exports - unstable_createElement as createElement, -} from 'react-native'; -import type { - NumberArray, - NumberProp, - TransformProps, -} from './lib/extract/types'; -import SvgTouchableMixin from './lib/SvgTouchableMixin'; -import { resolve } from './lib/resolve'; -import { - transformsArrayToProps, - type TransformsStyleArray, -} from './lib/extract/extractTransform'; -import { resolveAssetUri } from './lib/resolveAssetUri'; - -type BlurEvent = object; -type FocusEvent = object; -type PressEvent = object; -type LayoutEvent = object; -type EdgeInsetsProp = object; - -interface BaseProps { - accessible?: boolean; - accessibilityLabel?: string; - accessibilityHint?: string; - accessibilityIgnoresInvertColors?: boolean; - accessibilityRole?: string; - accessibilityState?: object; - delayLongPress?: number; - delayPressIn?: number; - delayPressOut?: number; - disabled?: boolean; - hitSlop?: EdgeInsetsProp; - href?: RNImageProps['source'] | string | number; - nativeID?: string; - touchSoundDisabled?: boolean; - onBlur?: (e: BlurEvent) => void; - onFocus?: (e: FocusEvent) => void; - onLayout?: (event: LayoutEvent) => object; - onLongPress?: (event: PressEvent) => object; - onClick?: (event: PressEvent) => object; - onPress?: (event: PressEvent) => object; - onPressIn?: (event: PressEvent) => object; - onPressOut?: (event: PressEvent) => object; - pressRetentionOffset?: EdgeInsetsProp; - rejectResponderTermination?: boolean; - - transform?: TransformProps['transform']; - translate?: NumberArray; - translateX?: NumberProp; - translateY?: NumberProp; - scale?: NumberArray; - scaleX?: NumberProp; - scaleY?: NumberProp; - rotation?: NumberProp; - skewX?: NumberProp; - skewY?: NumberProp; - origin?: NumberArray; - originX?: NumberProp; - originY?: NumberProp; - - fontStyle?: string; - fontWeight?: NumberProp; - fontSize?: NumberProp; - fontFamily?: string; - forwardedRef?: - | React.RefCallback - | React.MutableRefObject; - style?: Iterable; - - // different tranform props - gradientTransform?: TransformProps['transform']; - patternTransform?: TransformProps['transform']; -} - -const hasTouchableProperty = (props: BaseProps) => - props.onPress || props.onPressIn || props.onPressOut || props.onLongPress; - -const camelCaseToDashed = (camelCase: string) => { - return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); -}; - -function stringifyTransformProps(transformProps: TransformProps) { - const transformArray = []; - if (transformProps.translate != null) { - transformArray.push(`translate(${transformProps.translate})`); - } - if (transformProps.translateX != null || transformProps.translateY != null) { - transformArray.push( - `translate(${transformProps.translateX || 0}, ${ - transformProps.translateY || 0 - })` - ); - } - if (transformProps.scale != null) { - transformArray.push(`scale(${transformProps.scale})`); - } - if (transformProps.scaleX != null || transformProps.scaleY != null) { - transformArray.push( - `scale(${transformProps.scaleX || 1}, ${transformProps.scaleY || 1})` - ); - } - // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block) - if (transformProps.rotation != null) { - transformArray.push(`rotate(${transformProps.rotation})`); - } - if (transformProps.skewX != null) { - transformArray.push(`skewX(${transformProps.skewX})`); - } - if (transformProps.skewY != null) { - transformArray.push(`skewY(${transformProps.skewY})`); - } - return transformArray; -} - -function parseTransformProp( - transform: TransformProps['transform'], - props?: BaseProps -) { - const transformArray: string[] = []; - - props && transformArray.push(...stringifyTransformProps(props)); - - if (Array.isArray(transform)) { - if (typeof transform[0] === 'number') { - transformArray.push(`matrix(${transform.join(' ')})`); - } else { - const stringifiedProps = transformsArrayToProps( - transform as TransformsStyleArray - ); - transformArray.push(...stringifyTransformProps(stringifiedProps)); - } - } else if (typeof transform === 'string') { - transformArray.push(transform); - } - - return transformArray.length ? transformArray.join(' ') : undefined; -} - -/** - * `react-native-svg` supports additional props that aren't defined in the spec. - * This function replaces them in a spec conforming manner. - * - * @param {WebShape} self Instance given to us. - * @param {Object?} props Optional overridden props given to us. - * @returns {Object} Cleaned props object. - * @private - */ -const prepare = ( - self: WebShape, - props = self.props -) => { - const { - transform, - origin, - originX, - originY, - fontFamily, - fontSize, - fontWeight, - fontStyle, - style, - forwardedRef, - gradientTransform, - patternTransform, - ...rest - } = props; - - const clean: { - onStartShouldSetResponder?: (e: GestureResponderEvent) => boolean; - onResponderMove?: (e: GestureResponderEvent) => void; - onResponderGrant?: (e: GestureResponderEvent) => void; - onResponderRelease?: (e: GestureResponderEvent) => void; - onResponderTerminate?: (e: GestureResponderEvent) => void; - onResponderTerminationRequest?: (e: GestureResponderEvent) => boolean; - onClick?: (e: GestureResponderEvent) => void; - transform?: string; - gradientTransform?: string; - patternTransform?: string; - 'transform-origin'?: string; - href?: RNImageProps['source'] | string | null; - style?: object; - ref?: unknown; - } = { - ...(hasTouchableProperty(props) - ? { - onStartShouldSetResponder: - self.touchableHandleStartShouldSetResponder, - onResponderTerminationRequest: - self.touchableHandleResponderTerminationRequest, - onResponderGrant: self.touchableHandleResponderGrant, - onResponderMove: self.touchableHandleResponderMove, - onResponderRelease: self.touchableHandleResponderRelease, - onResponderTerminate: self.touchableHandleResponderTerminate, - } - : null), - ...rest, - }; - - if (origin != null) { - clean['transform-origin'] = origin.toString().replace(',', ' '); - } else if (originX != null || originY != null) { - clean['transform-origin'] = `${originX || 0} ${originY || 0}`; - } - - // we do it like this because setting transform as undefined causes error in web - const parsedTransform = parseTransformProp(transform, props); - if (parsedTransform) { - clean.transform = parsedTransform; - } - const parsedGradientTransform = parseTransformProp(gradientTransform); - if (parsedGradientTransform) { - clean.gradientTransform = parsedGradientTransform; - } - const parsedPatternTransform = parseTransformProp(patternTransform); - if (parsedPatternTransform) { - clean.patternTransform = parsedPatternTransform; - } +import { tags } from './tags.web'; - clean.ref = (el: SVGElement | null) => { - self.elementRef.current = el; - if (typeof forwardedRef === 'function') { - forwardedRef(el); - } else if (forwardedRef) { - forwardedRef.current = el; - } - }; - - const styles: { - fontStyle?: string; - fontFamily?: string; - fontSize?: NumberProp; - fontWeight?: NumberProp; - } = {}; - - if (fontFamily != null) { - styles.fontFamily = fontFamily; - } - if (fontSize != null) { - styles.fontSize = fontSize; - } - if (fontWeight != null) { - styles.fontWeight = fontWeight; - } - if (fontStyle != null) { - styles.fontStyle = fontStyle; - } - clean.style = resolve(style, styles); - if (props.onPress != null) { - clean.onClick = props.onPress; - } - if (props.href !== null) { - clean.href = resolveAssetUri(props.href)?.uri; - } - return clean; -}; - -const getBoundingClientRect = (node: SVGElement) => { - if (node) { - const isElement = node.nodeType === 1; /* Node.ELEMENT_NODE */ - if (isElement && typeof node.getBoundingClientRect === 'function') { - return node.getBoundingClientRect(); - } - } - throw new Error('Can not get boundingClientRect of ' + node || 'undefined'); -}; - -const measureLayout = ( - node: SVGElement, - callback: ( - x: number, - y: number, - width: number, - height: number, - left: number, - top: number - ) => void -) => { - const relativeNode = node?.parentNode; - if (relativeNode) { - setTimeout(() => { - // @ts-expect-error TODO: handle it better - const relativeRect = getBoundingClientRect(relativeNode); - const { height, left, top, width } = getBoundingClientRect(node); - const x = left - relativeRect.left; - const y = top - relativeRect.top; - callback(x, y, width, height, left, top); - }, 0); - } +import { + parse, + SvgAst, + SvgFromUri, + SvgFromXml, + SvgUri, + SvgXml, + camelCase, + fetchText, +} from './xml'; + +const { + circle: Circle, + clipPath: ClipPath, + defs: Defs, + ellipse: Ellipse, + filter: Filter, + feColorMatrix: FeColorMatrix, + feGaussianBlur: FeGaussianBlur, + feOffset: FeOffset, + g: G, + image: Image, + line: Line, + linearGradient: LinearGradient, + marker: Marker, + mask: Mask, + path: Path, + pattern: Pattern, + polygon: Polygon, + polyline: Polyline, + radialGradient: RadialGradient, + rect: Rect, + stop: Stop, + svg: Svg, + symbol: Symbol, + text: Text, + textPath: TextPath, + tspan: TSpan, + use: Use, + foreignObject: ForeignObject, +} = tags; + +export { + Svg, + Circle, + Ellipse, + G, + Text, + TSpan, + TextPath, + Path, + Polygon, + Polyline, + Line, + Rect, + Use, + Image, + Symbol, + Defs, + LinearGradient, + RadialGradient, + Stop, + ClipPath, + Pattern, + Mask, + Marker, + ForeignObject, + parse, + SvgAst, + SvgFromUri, + SvgFromXml, + SvgUri, + SvgXml, + camelCase, + fetchText, + Filter, + FeColorMatrix, + FeGaussianBlur, + FeOffset, }; -// eslint-disable-next-line @typescript-eslint/no-explicit-any -function remeasure(this: any) { - const tag = this.state.touchable.responderID; - if (tag === null) { - return; - } - measureLayout(tag, this._handleQueryLayout); -} - -export class WebShape< - P extends BaseProps = BaseProps, -> extends React.Component

{ - [x: string]: unknown; - protected tag?: React.ElementType; - protected prepareProps(props: P) { - return props; - } - - elementRef = - React.createRef() as React.MutableRefObject; - - lastMergedProps: Partial

= {}; - - /** - * disclaimer: I am not sure why the props are wrapped in a `style` attribute here, but that's how reanimated calls it - */ - setNativeProps(props: { style: P }) { - const merged = Object.assign( - {}, - this.props, - this.lastMergedProps, - props.style - ); - this.lastMergedProps = merged; - const clean = prepare(this, this.prepareProps(merged)); - const current = this.elementRef.current; - if (current) { - for (const cleanAttribute of Object.keys(clean)) { - const cleanValue = clean[cleanAttribute as keyof typeof clean]; - switch (cleanAttribute) { - case 'ref': - case 'children': - break; - case 'style': - // style can be an object here or an array, so we convert it to an array and assign each element - for (const partialStyle of ([] as unknown[]).concat( - clean.style ?? [] - )) { - Object.assign(current.style, partialStyle); - } - break; - default: - // apply all other incoming prop updates as attributes on the node - // same logic as in https://github.com/software-mansion/react-native-reanimated/blob/d04720c82f5941532991b235787285d36d717247/src/reanimated2/js-reanimated/index.ts#L38-L39 - // @ts-expect-error TODO: fix this - current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue); - break; - } - } - } - } - - _remeasureMetricsOnActivation: () => void; - touchableHandleStartShouldSetResponder?: ( - e: GestureResponderEvent - ) => boolean; - - touchableHandleResponderMove?: (e: GestureResponderEvent) => void; - touchableHandleResponderGrant?: (e: GestureResponderEvent) => void; - touchableHandleResponderRelease?: (e: GestureResponderEvent) => void; - touchableHandleResponderTerminate?: (e: GestureResponderEvent) => void; - touchableHandleResponderTerminationRequest?: ( - e: GestureResponderEvent - ) => boolean; - - constructor(props: P) { - super(props); - - // Do not attach touchable mixin handlers if SVG element doesn't have a touchable prop - if (hasTouchableProperty(props)) { - SvgTouchableMixin(this); - } - - this._remeasureMetricsOnActivation = remeasure.bind(this); - } - - render(): JSX.Element { - if (!this.tag) { - throw new Error( - 'When extending `WebShape` you need to overwrite either `tag` or `render`!' - ); - } - this.lastMergedProps = {}; - return createElement( - this.tag, - prepare(this, this.prepareProps(this.props)) - ); - } -} - -export class Circle extends WebShape { - tag = 'circle' as const; -} - -export class ClipPath extends WebShape { - tag = 'clipPath' as const; -} - -export class Defs extends WebShape { - tag = 'defs' as const; -} - -export class Ellipse extends WebShape { - tag = 'ellipse' as const; -} - -export class G extends WebShape { - tag = 'g' as const; - prepareProps(props: BaseProps & GProps) { - const { x, y, ...rest } = props; - - if ((x || y) && !rest.translate) { - rest.translate = `${x || 0}, ${y || 0}`; - } - - return rest; - } -} - -export class Image extends WebShape { - tag = 'image' as const; -} - -export class Line extends WebShape { - tag = 'line' as const; -} - -export class LinearGradient extends WebShape { - tag = 'linearGradient' as const; -} - -export class Path extends WebShape { - tag = 'path' as const; -} - -export class Polygon extends WebShape { - tag = 'polygon' as const; -} - -export class Polyline extends WebShape { - tag = 'polyline' as const; -} - -export class RadialGradient extends WebShape { - tag = 'radialGradient' as const; -} - -export class Rect extends WebShape { - tag = 'rect' as const; -} - -export class Stop extends WebShape { - tag = 'stop' as const; -} - -/* Taken from here: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 */ -function encodeSvg(svgString: string) { - return svgString - .replace( - '/g, '%3E') - .replace(/\s+/g, ' '); -} - -export class Svg extends WebShape { - tag = 'svg' as const; - toDataURL( - callback: (data: string) => void, - options: { width?: number; height?: number } = {} - ) { - const ref = this.elementRef.current; - - if (ref === null) { - return; - } - - const rect = getBoundingClientRect(ref); - - const width = Number(options.width) || rect.width; - const height = Number(options.height) || rect.height; - - const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); - svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`); - svg.setAttribute('width', String(width)); - svg.setAttribute('height', String(height)); - svg.appendChild(ref.cloneNode(true)); - - const img = new window.Image(); - img.onload = () => { - const canvas = document.createElement('canvas'); - canvas.width = width; - canvas.height = height; - const context = canvas.getContext('2d'); - context?.drawImage(img, 0, 0); - callback(canvas.toDataURL().replace('data:image/png;base64,', '')); - }; - - img.src = `data:image/svg+xml;utf8,${encodeSvg( - new window.XMLSerializer().serializeToString(svg) - )}`; - } -} - -export class Symbol extends WebShape { - tag = 'symbol' as const; -} - -export class Text extends WebShape { - tag = 'text' as const; -} - -export class TSpan extends WebShape { - tag = 'tspan' as const; -} - -export class TextPath extends WebShape { - tag = 'textPath' as const; -} - -export class Use extends WebShape { - tag = 'use' as const; -} - -export class Mask extends WebShape { - tag = 'mask' as const; -} - -export class ForeignObject extends WebShape { - tag = 'foreignObject' as const; -} - -export class Marker extends WebShape { - tag = 'marker' as const; -} - -export class Pattern extends WebShape { - tag = 'pattern' as const; -} - -export class Filter extends WebShape { - tag = 'filter' as const; -} - -export class FeColorMatrix extends WebShape { - tag = 'feColorMatrix' as const; -} - -export class FeGaussianBlur extends WebShape { - tag = 'feGaussianBlur' as const; -} - -export class FeOffset extends WebShape { - tag = 'feOffset' as const; -} - -export default Svg; +export { + SvgCss, + SvgCssUri, + SvgWithCss, + SvgWithCssUri, + inlineStyles, + LocalSvg, + WithLocalSvg, + loadLocalRawResource, +} from './deprecated'; diff --git a/src/tags.tsx b/src/tags.tsx index 184a60354..07b5aed60 100644 --- a/src/tags.tsx +++ b/src/tags.tsx @@ -1,4 +1,3 @@ -import Shape from './elements/Shape'; import Rect from './elements/Rect'; import Circle from './elements/Circle'; import Ellipse from './elements/Ellipse'; @@ -57,5 +56,4 @@ export const tags = { tspan: TSpan, use: Use, foreignObject: ForeignObject, - Shape, } as const; diff --git a/src/tags.web.ts b/src/tags.web.ts new file mode 100644 index 000000000..91fe5722f --- /dev/null +++ b/src/tags.web.ts @@ -0,0 +1,60 @@ +import { + Circle, + ClipPath, + Defs, + Ellipse, + Filter, + FeColorMatrix, + FeGaussianBlur, + FeOffset, + ForeignObject, + G, + Line, + LinearGradient, + Marker, + Mask, + Path, + Pattern, + Polygon, + Polyline, + RadialGradient, + Rect, + Stop, + Svg, + Symbol, + Text, + TextPath, + TSpan, + Use, +} from './web/elements'; + +export const tags = { + circle: Circle, + clipPath: ClipPath, + defs: Defs, + ellipse: Ellipse, + filter: Filter, + feColorMatrix: FeColorMatrix, + feGaussianBlur: FeGaussianBlur, + feOffset: FeOffset, + foreignObject: ForeignObject, + g: G, + image: Image, + line: Line, + linearGradient: LinearGradient, + marker: Marker, + mask: Mask, + path: Path, + pattern: Pattern, + polygon: Polygon, + polyline: Polyline, + radialGradient: RadialGradient, + rect: Rect, + stop: Stop, + svg: Svg, + symbol: Symbol, + text: Text, + textPath: TextPath, + tspan: TSpan, + use: Use, +} as const; diff --git a/src/web/WebShape.ts b/src/web/WebShape.ts new file mode 100644 index 000000000..04a986adc --- /dev/null +++ b/src/web/WebShape.ts @@ -0,0 +1,102 @@ +import React from 'react'; +import { + GestureResponderEvent, + // @ts-ignore it is not seen in exports + unstable_createElement as createElement, +} from 'react-native'; + +import { BaseProps } from './types'; +import { prepare } from './utils/prepare'; +import { hasTouchableProperty, remeasure } from './utils'; +import SvgTouchableMixin from '../lib/SvgTouchableMixin'; + +export class WebShape< + P extends BaseProps = BaseProps, +> extends React.Component

{ + [x: string]: unknown; + protected tag?: React.ElementType; + protected prepareProps(props: P) { + return props; + } + + elementRef = + React.createRef() as React.MutableRefObject; + + lastMergedProps: Partial

= {}; + + /** + * disclaimer: I am not sure why the props are wrapped in a `style` attribute here, but that's how reanimated calls it + */ + setNativeProps(props: { style: P }) { + const merged = Object.assign( + {}, + this.props, + this.lastMergedProps, + props.style + ); + this.lastMergedProps = merged; + const clean = prepare(this, this.prepareProps(merged)); + const current = this.elementRef.current; + if (current) { + for (const cleanAttribute of Object.keys(clean)) { + const cleanValue = clean[cleanAttribute as keyof typeof clean]; + switch (cleanAttribute) { + case 'ref': + case 'children': + break; + case 'style': + // style can be an object here or an array, so we convert it to an array and assign each element + for (const partialStyle of ([] as unknown[]).concat( + clean.style ?? [] + )) { + Object.assign(current.style, partialStyle); + } + break; + default: + // apply all other incoming prop updates as attributes on the node + // same logic as in https://github.com/software-mansion/react-native-reanimated/blob/d04720c82f5941532991b235787285d36d717247/src/reanimated2/js-reanimated/index.ts#L38-L39 + // @ts-expect-error TODO: fix this + current.setAttribute(camelCaseToDashed(cleanAttribute), cleanValue); + break; + } + } + } + } + + _remeasureMetricsOnActivation: () => void; + touchableHandleStartShouldSetResponder?: ( + e: GestureResponderEvent + ) => boolean; + + touchableHandleResponderMove?: (e: GestureResponderEvent) => void; + touchableHandleResponderGrant?: (e: GestureResponderEvent) => void; + touchableHandleResponderRelease?: (e: GestureResponderEvent) => void; + touchableHandleResponderTerminate?: (e: GestureResponderEvent) => void; + touchableHandleResponderTerminationRequest?: ( + e: GestureResponderEvent + ) => boolean; + + constructor(props: P) { + super(props); + + // Do not attach touchable mixin handlers if SVG element doesn't have a touchable prop + if (hasTouchableProperty(props)) { + SvgTouchableMixin(this); + } + + this._remeasureMetricsOnActivation = remeasure.bind(this); + } + + render(): JSX.Element { + if (!this.tag) { + throw new Error( + 'When extending `WebShape` you need to overwrite either `tag` or `render`!' + ); + } + this.lastMergedProps = {}; + return createElement( + this.tag, + prepare(this, this.prepareProps(this.props)) + ); + } +} diff --git a/src/web/elements.ts b/src/web/elements.ts new file mode 100644 index 000000000..61133909b --- /dev/null +++ b/src/web/elements.ts @@ -0,0 +1,207 @@ +import { BaseProps } from './types'; +import { WebShape } from './WebShape'; +import type { CircleProps } from '../elements/Circle'; +import type { ClipPathProps } from '../elements/ClipPath'; +import type { EllipseProps } from '../elements/Ellipse'; +import type { ForeignObjectProps } from '../elements/ForeignObject'; +import type { GProps } from '../elements/G'; +import type { ImageProps } from '../elements/Image'; +import type { LineProps } from '../elements/Line'; +import type { LinearGradientProps } from '../elements/LinearGradient'; +import type { MarkerProps } from '../elements/Marker'; +import type { MaskProps } from '../elements/Mask'; +import type { PathProps } from '../elements/Path'; +import type { PatternProps } from '../elements/Pattern'; +import type { PolygonProps } from '../elements/Polygon'; +import type { PolylineProps } from '../elements/Polyline'; +import type { RadialGradientProps } from '../elements/RadialGradient'; +import type { RectProps } from '../elements/Rect'; +import type { StopProps } from '../elements/Stop'; +import type { SvgProps } from '../elements/Svg'; +import type { SymbolProps } from '../elements/Symbol'; +import type { TextProps } from '../elements/Text'; +import type { TextPathProps } from '../elements/TextPath'; +import type { TSpanProps } from '../elements/TSpan'; +import type { UseProps } from '../elements/Use'; +import type { FilterProps } from '../elements/filters/Filter'; +import type { FeColorMatrixProps } from '../elements/filters/FeColorMatrix'; +import type { FeGaussianBlurProps } from '../elements/filters/FeGaussianBlur'; +import type { FeOffsetProps } from '../elements/filters/FeOffset'; +import { getBoundingClientRect } from './utils'; + +export class Circle extends WebShape { + tag = 'circle' as const; +} + +export class ClipPath extends WebShape { + tag = 'clipPath' as const; +} + +export class Defs extends WebShape { + tag = 'defs' as const; +} + +export class Ellipse extends WebShape { + tag = 'ellipse' as const; +} + +export class G extends WebShape { + tag = 'g' as const; + prepareProps(props: BaseProps & GProps) { + const { x, y, ...rest } = props; + + if ((x || y) && !rest.translate) { + rest.translate = `${x || 0}, ${y || 0}`; + } + + return rest; + } +} + +export class Image extends WebShape { + tag = 'image' as const; +} + +export class Line extends WebShape { + tag = 'line' as const; +} + +export class LinearGradient extends WebShape { + tag = 'linearGradient' as const; +} + +export class Path extends WebShape { + tag = 'path' as const; +} + +export class Polygon extends WebShape { + tag = 'polygon' as const; +} + +export class Polyline extends WebShape { + tag = 'polyline' as const; +} + +export class RadialGradient extends WebShape { + tag = 'radialGradient' as const; +} + +export class Rect extends WebShape { + tag = 'rect' as const; +} + +export class Stop extends WebShape { + tag = 'stop' as const; +} + +/* Taken from here: https://gist.github.com/jennyknuth/222825e315d45a738ed9d6e04c7a88d0 */ +function encodeSvg(svgString: string) { + return svgString + .replace( + '/g, '%3E') + .replace(/\s+/g, ' '); +} + +export class Svg extends WebShape { + tag = 'svg' as const; + toDataURL( + callback: (data: string) => void, + options: { width?: number; height?: number } = {} + ) { + const ref = this.elementRef.current; + + if (ref === null) { + return; + } + + const rect = getBoundingClientRect(ref); + + const width = Number(options.width) || rect.width; + const height = Number(options.height) || rect.height; + + const svg = document.createElementNS('http://www.w3.org/2000/svg', 'svg'); + svg.setAttribute('viewBox', `0 0 ${rect.width} ${rect.height}`); + svg.setAttribute('width', String(width)); + svg.setAttribute('height', String(height)); + svg.appendChild(ref.cloneNode(true)); + + const img = new window.Image(); + img.onload = () => { + const canvas = document.createElement('canvas'); + canvas.width = width; + canvas.height = height; + const context = canvas.getContext('2d'); + context?.drawImage(img, 0, 0); + callback(canvas.toDataURL().replace('data:image/png;base64,', '')); + }; + + img.src = `data:image/svg+xml;utf8,${encodeSvg( + new window.XMLSerializer().serializeToString(svg) + )}`; + } +} + +export class Symbol extends WebShape { + tag = 'symbol' as const; +} + +export class Text extends WebShape { + tag = 'text' as const; +} + +export class TSpan extends WebShape { + tag = 'tspan' as const; +} + +export class TextPath extends WebShape { + tag = 'textPath' as const; +} + +export class Use extends WebShape { + tag = 'use' as const; +} + +export class Mask extends WebShape { + tag = 'mask' as const; +} + +export class ForeignObject extends WebShape { + tag = 'foreignObject' as const; +} + +export class Marker extends WebShape { + tag = 'marker' as const; +} + +export class Pattern extends WebShape { + tag = 'pattern' as const; +} + +export class Filter extends WebShape { + tag = 'filter' as const; +} + +export class FeColorMatrix extends WebShape { + tag = 'feColorMatrix' as const; +} + +export class FeGaussianBlur extends WebShape { + tag = 'feGaussianBlur' as const; +} + +export class FeOffset extends WebShape { + tag = 'feOffset' as const; +} + +export default Svg; diff --git a/src/web/types.ts b/src/web/types.ts new file mode 100644 index 000000000..81add91b1 --- /dev/null +++ b/src/web/types.ts @@ -0,0 +1,66 @@ +import type { ImageProps as RNImageProps } from 'react-native'; +import type { + NumberArray, + NumberProp, + TransformProps, +} from '../lib/extract/types'; + +type BlurEvent = object; +type FocusEvent = object; +type PressEvent = object; +type LayoutEvent = object; +type EdgeInsetsProp = object; + +export interface BaseProps { + accessible?: boolean; + accessibilityLabel?: string; + accessibilityHint?: string; + accessibilityIgnoresInvertColors?: boolean; + accessibilityRole?: string; + accessibilityState?: object; + delayLongPress?: number; + delayPressIn?: number; + delayPressOut?: number; + disabled?: boolean; + hitSlop?: EdgeInsetsProp; + href?: RNImageProps['source'] | string | number; + nativeID?: string; + touchSoundDisabled?: boolean; + onBlur?: (e: BlurEvent) => void; + onFocus?: (e: FocusEvent) => void; + onLayout?: (event: LayoutEvent) => object; + onLongPress?: (event: PressEvent) => object; + onClick?: (event: PressEvent) => object; + onPress?: (event: PressEvent) => object; + onPressIn?: (event: PressEvent) => object; + onPressOut?: (event: PressEvent) => object; + pressRetentionOffset?: EdgeInsetsProp; + rejectResponderTermination?: boolean; + + transform?: TransformProps['transform']; + translate?: NumberArray; + translateX?: NumberProp; + translateY?: NumberProp; + scale?: NumberArray; + scaleX?: NumberProp; + scaleY?: NumberProp; + rotation?: NumberProp; + skewX?: NumberProp; + skewY?: NumberProp; + origin?: NumberArray; + originX?: NumberProp; + originY?: NumberProp; + + fontStyle?: string; + fontWeight?: NumberProp; + fontSize?: NumberProp; + fontFamily?: string; + forwardedRef?: + | React.RefCallback + | React.MutableRefObject; + style?: Iterable; + + // different tranform props + gradientTransform?: TransformProps['transform']; + patternTransform?: TransformProps['transform']; +} diff --git a/src/web/utils/index.ts b/src/web/utils/index.ts new file mode 100644 index 000000000..b9f33fb16 --- /dev/null +++ b/src/web/utils/index.ts @@ -0,0 +1,113 @@ +import { BaseProps } from '../types'; +import type { TransformProps } from '../../lib/extract/types'; +import { + transformsArrayToProps, + TransformsStyleArray, +} from '../../lib/extract/extractTransform'; + +export const hasTouchableProperty = (props: BaseProps) => + props.onPress || props.onPressIn || props.onPressOut || props.onLongPress; + +export const camelCaseToDashed = (camelCase: string) => { + return camelCase.replace(/[A-Z]/g, (m) => '-' + m.toLowerCase()); +}; + +function stringifyTransformProps(transformProps: TransformProps) { + const transformArray = []; + if (transformProps.translate != null) { + transformArray.push(`translate(${transformProps.translate})`); + } + if (transformProps.translateX != null || transformProps.translateY != null) { + transformArray.push( + `translate(${transformProps.translateX || 0}, ${ + transformProps.translateY || 0 + })` + ); + } + if (transformProps.scale != null) { + transformArray.push(`scale(${transformProps.scale})`); + } + if (transformProps.scaleX != null || transformProps.scaleY != null) { + transformArray.push( + `scale(${transformProps.scaleX || 1}, ${transformProps.scaleY || 1})` + ); + } + // rotation maps to rotate, not to collide with the text rotate attribute (which acts per glyph rather than block) + if (transformProps.rotation != null) { + transformArray.push(`rotate(${transformProps.rotation})`); + } + if (transformProps.skewX != null) { + transformArray.push(`skewX(${transformProps.skewX})`); + } + if (transformProps.skewY != null) { + transformArray.push(`skewY(${transformProps.skewY})`); + } + return transformArray; +} + +export function parseTransformProp( + transform: TransformProps['transform'], + props?: BaseProps +) { + const transformArray: string[] = []; + + props && transformArray.push(...stringifyTransformProps(props)); + + if (Array.isArray(transform)) { + if (typeof transform[0] === 'number') { + transformArray.push(`matrix(${transform.join(' ')})`); + } else { + const stringifiedProps = transformsArrayToProps( + transform as TransformsStyleArray + ); + transformArray.push(...stringifyTransformProps(stringifiedProps)); + } + } else if (typeof transform === 'string') { + transformArray.push(transform); + } + + return transformArray.length ? transformArray.join(' ') : undefined; +} + +export const getBoundingClientRect = (node: SVGElement) => { + if (node) { + const isElement = node.nodeType === 1; /* Node.ELEMENT_NODE */ + if (isElement && typeof node.getBoundingClientRect === 'function') { + return node.getBoundingClientRect(); + } + } + throw new Error('Can not get boundingClientRect of ' + node || 'undefined'); +}; + +const measureLayout = ( + node: SVGElement, + callback: ( + x: number, + y: number, + width: number, + height: number, + left: number, + top: number + ) => void +) => { + const relativeNode = node?.parentNode; + if (relativeNode) { + setTimeout(() => { + // @ts-expect-error TODO: handle it better + const relativeRect = getBoundingClientRect(relativeNode); + const { height, left, top, width } = getBoundingClientRect(node); + const x = left - relativeRect.left; + const y = top - relativeRect.top; + callback(x, y, width, height, left, top); + }, 0); + } +}; + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +export function remeasure(this: any) { + const tag = this.state.touchable.responderID; + if (tag === null) { + return; + } + measureLayout(tag, this._handleQueryLayout); +} diff --git a/src/web/utils/prepare.ts b/src/web/utils/prepare.ts new file mode 100644 index 000000000..13bbc7af9 --- /dev/null +++ b/src/web/utils/prepare.ts @@ -0,0 +1,127 @@ +import { + GestureResponderEvent, + type ImageProps as RNImageProps, +} from 'react-native'; +import { BaseProps } from '../types'; +import { WebShape } from '../WebShape'; +import { hasTouchableProperty, parseTransformProp } from '.'; +import { resolve } from '../../lib/resolve'; +import { NumberProp } from '../../lib/extract/types'; +import { resolveAssetUri } from '../../lib/resolveAssetUri'; +/** + * `react-native-svg` supports additional props that aren't defined in the spec. + * This function replaces them in a spec conforming manner. + * + * @param {WebShape} self Instance given to us. + * @param {Object?} props Optional overridden props given to us. + * @returns {Object} Cleaned props object. + * @private + */ +export const prepare = ( + self: WebShape, + props = self.props +) => { + const { + transform, + origin, + originX, + originY, + fontFamily, + fontSize, + fontWeight, + fontStyle, + style, + forwardedRef, + gradientTransform, + patternTransform, + ...rest + } = props; + + const clean: { + onStartShouldSetResponder?: (e: GestureResponderEvent) => boolean; + onResponderMove?: (e: GestureResponderEvent) => void; + onResponderGrant?: (e: GestureResponderEvent) => void; + onResponderRelease?: (e: GestureResponderEvent) => void; + onResponderTerminate?: (e: GestureResponderEvent) => void; + onResponderTerminationRequest?: (e: GestureResponderEvent) => boolean; + onClick?: (e: GestureResponderEvent) => void; + transform?: string; + gradientTransform?: string; + patternTransform?: string; + 'transform-origin'?: string; + href?: RNImageProps['source'] | string | null; + style?: object; + ref?: unknown; + } = { + ...(hasTouchableProperty(props) + ? { + onStartShouldSetResponder: + self.touchableHandleStartShouldSetResponder, + onResponderTerminationRequest: + self.touchableHandleResponderTerminationRequest, + onResponderGrant: self.touchableHandleResponderGrant, + onResponderMove: self.touchableHandleResponderMove, + onResponderRelease: self.touchableHandleResponderRelease, + onResponderTerminate: self.touchableHandleResponderTerminate, + } + : null), + ...rest, + }; + + if (origin != null) { + clean['transform-origin'] = origin.toString().replace(',', ' '); + } else if (originX != null || originY != null) { + clean['transform-origin'] = `${originX || 0} ${originY || 0}`; + } + + // we do it like this because setting transform as undefined causes error in web + const parsedTransform = parseTransformProp(transform, props); + if (parsedTransform) { + clean.transform = parsedTransform; + } + const parsedGradientTransform = parseTransformProp(gradientTransform); + if (parsedGradientTransform) { + clean.gradientTransform = parsedGradientTransform; + } + const parsedPatternTransform = parseTransformProp(patternTransform); + if (parsedPatternTransform) { + clean.patternTransform = parsedPatternTransform; + } + + clean.ref = (el: SVGElement | null) => { + self.elementRef.current = el; + if (typeof forwardedRef === 'function') { + forwardedRef(el); + } else if (forwardedRef) { + forwardedRef.current = el; + } + }; + + const styles: { + fontStyle?: string; + fontFamily?: string; + fontSize?: NumberProp; + fontWeight?: NumberProp; + } = {}; + + if (fontFamily != null) { + styles.fontFamily = fontFamily; + } + if (fontSize != null) { + styles.fontSize = fontSize; + } + if (fontWeight != null) { + styles.fontWeight = fontWeight; + } + if (fontStyle != null) { + styles.fontStyle = fontStyle; + } + clean.style = resolve(style, styles); + if (props.onPress != null) { + clean.onClick = props.onPress; + } + if (props.href !== null) { + clean.href = resolveAssetUri(props.href)?.uri; + } + return clean; +}; diff --git a/src/xml.tsx b/src/xml.tsx index ca0de7a0d..d0bb5e087 100644 --- a/src/xml.tsx +++ b/src/xml.tsx @@ -19,7 +19,7 @@ export interface AST { props: { [prop: string]: Styles | string | undefined; }; - Tag: ComponentType; + Tag: Tag; } export interface XmlAST extends AST { From 313044e62b910c4855cc458d077cbf2d9c544c10 Mon Sep 17 00:00:00 2001 From: bohdanprog Date: Tue, 30 Jul 2024 15:07:12 +0200 Subject: [PATCH 03/10] feat: change file type, reordering object variable --- src/{tags.tsx => tags.ts} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/{tags.tsx => tags.ts} (100%) diff --git a/src/tags.tsx b/src/tags.ts similarity index 100% rename from src/tags.tsx rename to src/tags.ts index 07b5aed60..b2fbe2264 100644 --- a/src/tags.tsx +++ b/src/tags.ts @@ -36,6 +36,7 @@ export const tags = { feColorMatrix: FeColorMatrix, feGaussianBlur: FeGaussianBlur, feOffset: FeOffset, + foreignObject: ForeignObject, g: G, image: Image, line: Line, @@ -55,5 +56,4 @@ export const tags = { textPath: TextPath, tspan: TSpan, use: Use, - foreignObject: ForeignObject, } as const; From 51b6157676810b2ea0eb5ae606ed2d6924f99a9b Mon Sep 17 00:00:00 2001 From: bohdanprog Date: Tue, 30 Jul 2024 16:16:52 +0200 Subject: [PATCH 04/10] feat: create elements file with imports all svg elements, update import in ReactNativeSvg --- src/ReactNativeSVG.ts | 62 +-------------------- src/ReactNativeSVG.web.ts | 65 ++-------------------- src/{tags.ts => elements.ts} | 60 ++++++++++---------- src/{web/elements.ts => elements.web.ts} | 60 ++++++++++---------- src/filter-image/extract/extractFilters.ts | 2 +- src/xml.tsx | 3 +- src/{tags.web.ts => xmlTags.ts} | 3 +- 7 files changed, 71 insertions(+), 184 deletions(-) rename src/{tags.ts => elements.ts} (67%) rename src/{web/elements.ts => elements.web.ts} (72%) rename src/{tags.web.ts => xmlTags.ts} (96%) diff --git a/src/ReactNativeSVG.ts b/src/ReactNativeSVG.ts index fff08edc3..6589db650 100644 --- a/src/ReactNativeSVG.ts +++ b/src/ReactNativeSVG.ts @@ -1,5 +1,5 @@ -import { tags } from './tags'; import Shape from './elements/Shape'; +import { Svg } from './elements'; import { parse, @@ -50,37 +50,6 @@ import { RNSVGFeOffset, } from './fabric'; -const { - circle: Circle, - clipPath: ClipPath, - defs: Defs, - ellipse: Ellipse, - filter: Filter, - feColorMatrix: FeColorMatrix, - feGaussianBlur: FeGaussianBlur, - feOffset: FeOffset, - g: G, - image: Image, - line: Line, - linearGradient: LinearGradient, - marker: Marker, - mask: Mask, - path: Path, - pattern: Pattern, - polygon: Polygon, - polyline: Polyline, - radialGradient: RadialGradient, - rect: Rect, - stop: Stop, - svg: Svg, - symbol: Symbol, - text: Text, - textPath: TextPath, - tspan: TSpan, - use: Use, - foreignObject: ForeignObject, -} = tags; - export { SvgCss, SvgCssUri, @@ -124,30 +93,6 @@ export type { FilterPrimitiveCommonProps } from './elements/filters/FilterPrimit export * from './lib/extract/types'; export { - Svg, - Circle, - Ellipse, - G, - Text, - TSpan, - TextPath, - Path, - Polygon, - Polyline, - Line, - Rect, - Use, - Image, - Symbol, - Defs, - LinearGradient, - RadialGradient, - Stop, - ClipPath, - Pattern, - Mask, - Marker, - ForeignObject, parse, SvgAst, SvgFromUri, @@ -157,10 +102,6 @@ export { camelCase, fetchText, Shape, - Filter, - FeColorMatrix, - FeGaussianBlur, - FeOffset, RNSVGMarker, RNSVGMask, RNSVGPattern, @@ -201,4 +142,5 @@ export type { AstProps, }; +export * from './elements'; export default Svg; diff --git a/src/ReactNativeSVG.web.ts b/src/ReactNativeSVG.web.ts index f6f34cfa5..293bd303f 100644 --- a/src/ReactNativeSVG.web.ts +++ b/src/ReactNativeSVG.web.ts @@ -1,5 +1,4 @@ -import { tags } from './tags.web'; - +import { Svg } from './elements'; import { parse, SvgAst, @@ -11,62 +10,7 @@ import { fetchText, } from './xml'; -const { - circle: Circle, - clipPath: ClipPath, - defs: Defs, - ellipse: Ellipse, - filter: Filter, - feColorMatrix: FeColorMatrix, - feGaussianBlur: FeGaussianBlur, - feOffset: FeOffset, - g: G, - image: Image, - line: Line, - linearGradient: LinearGradient, - marker: Marker, - mask: Mask, - path: Path, - pattern: Pattern, - polygon: Polygon, - polyline: Polyline, - radialGradient: RadialGradient, - rect: Rect, - stop: Stop, - svg: Svg, - symbol: Symbol, - text: Text, - textPath: TextPath, - tspan: TSpan, - use: Use, - foreignObject: ForeignObject, -} = tags; - export { - Svg, - Circle, - Ellipse, - G, - Text, - TSpan, - TextPath, - Path, - Polygon, - Polyline, - Line, - Rect, - Use, - Image, - Symbol, - Defs, - LinearGradient, - RadialGradient, - Stop, - ClipPath, - Pattern, - Mask, - Marker, - ForeignObject, parse, SvgAst, SvgFromUri, @@ -75,10 +19,6 @@ export { SvgXml, camelCase, fetchText, - Filter, - FeColorMatrix, - FeGaussianBlur, - FeOffset, }; export { @@ -91,3 +31,6 @@ export { WithLocalSvg, loadLocalRawResource, } from './deprecated'; + +export * from './elements'; +export default Svg; diff --git a/src/tags.ts b/src/elements.ts similarity index 67% rename from src/tags.ts rename to src/elements.ts index b2fbe2264..8ff330e61 100644 --- a/src/tags.ts +++ b/src/elements.ts @@ -27,33 +27,33 @@ import FeColorMatrix from './elements/filters/FeColorMatrix'; import FeGaussianBlur from './elements/filters/FeGaussianBlur'; import FeOffset from './elements/filters/FeOffset'; -export const tags = { - circle: Circle, - clipPath: ClipPath, - defs: Defs, - ellipse: Ellipse, - filter: Filter, - feColorMatrix: FeColorMatrix, - feGaussianBlur: FeGaussianBlur, - feOffset: FeOffset, - foreignObject: ForeignObject, - g: G, - image: Image, - line: Line, - linearGradient: LinearGradient, - marker: Marker, - mask: Mask, - path: Path, - pattern: Pattern, - polygon: Polygon, - polyline: Polyline, - radialGradient: RadialGradient, - rect: Rect, - stop: Stop, - svg: Svg, - symbol: Symbol, - text: Text, - textPath: TextPath, - tspan: TSpan, - use: Use, -} as const; +export { + Rect, + Circle, + Ellipse, + Polygon, + Polyline, + Line, + Svg, + Path, + G, + Text, + TSpan, + TextPath, + Use, + Image, + Symbol, + Defs, + LinearGradient, + RadialGradient, + Stop, + ClipPath, + Pattern, + Mask, + Marker, + ForeignObject, + Filter, + FeColorMatrix, + FeGaussianBlur, + FeOffset, +}; diff --git a/src/web/elements.ts b/src/elements.web.ts similarity index 72% rename from src/web/elements.ts rename to src/elements.web.ts index 61133909b..57fbb3ee5 100644 --- a/src/web/elements.ts +++ b/src/elements.web.ts @@ -1,33 +1,33 @@ -import { BaseProps } from './types'; -import { WebShape } from './WebShape'; -import type { CircleProps } from '../elements/Circle'; -import type { ClipPathProps } from '../elements/ClipPath'; -import type { EllipseProps } from '../elements/Ellipse'; -import type { ForeignObjectProps } from '../elements/ForeignObject'; -import type { GProps } from '../elements/G'; -import type { ImageProps } from '../elements/Image'; -import type { LineProps } from '../elements/Line'; -import type { LinearGradientProps } from '../elements/LinearGradient'; -import type { MarkerProps } from '../elements/Marker'; -import type { MaskProps } from '../elements/Mask'; -import type { PathProps } from '../elements/Path'; -import type { PatternProps } from '../elements/Pattern'; -import type { PolygonProps } from '../elements/Polygon'; -import type { PolylineProps } from '../elements/Polyline'; -import type { RadialGradientProps } from '../elements/RadialGradient'; -import type { RectProps } from '../elements/Rect'; -import type { StopProps } from '../elements/Stop'; -import type { SvgProps } from '../elements/Svg'; -import type { SymbolProps } from '../elements/Symbol'; -import type { TextProps } from '../elements/Text'; -import type { TextPathProps } from '../elements/TextPath'; -import type { TSpanProps } from '../elements/TSpan'; -import type { UseProps } from '../elements/Use'; -import type { FilterProps } from '../elements/filters/Filter'; -import type { FeColorMatrixProps } from '../elements/filters/FeColorMatrix'; -import type { FeGaussianBlurProps } from '../elements/filters/FeGaussianBlur'; -import type { FeOffsetProps } from '../elements/filters/FeOffset'; -import { getBoundingClientRect } from './utils'; +import { BaseProps } from './web/types'; +import { WebShape } from './web/WebShape'; +import type { CircleProps } from './elements/Circle'; +import type { ClipPathProps } from './elements/ClipPath'; +import type { EllipseProps } from './elements/Ellipse'; +import type { ForeignObjectProps } from './elements/ForeignObject'; +import type { GProps } from './elements/G'; +import type { ImageProps } from './elements/Image'; +import type { LineProps } from './elements/Line'; +import type { LinearGradientProps } from './elements/LinearGradient'; +import type { MarkerProps } from './elements/Marker'; +import type { MaskProps } from './elements/Mask'; +import type { PathProps } from './elements/Path'; +import type { PatternProps } from './elements/Pattern'; +import type { PolygonProps } from './elements/Polygon'; +import type { PolylineProps } from './elements/Polyline'; +import type { RadialGradientProps } from './elements/RadialGradient'; +import type { RectProps } from './elements/Rect'; +import type { StopProps } from './elements/Stop'; +import type { SvgProps } from './elements/Svg'; +import type { SymbolProps } from './elements/Symbol'; +import type { TextProps } from './elements/Text'; +import type { TextPathProps } from './elements/TextPath'; +import type { TSpanProps } from './elements/TSpan'; +import type { UseProps } from './elements/Use'; +import type { FilterProps } from './elements/filters/Filter'; +import type { FeColorMatrixProps } from './elements/filters/FeColorMatrix'; +import type { FeGaussianBlurProps } from './elements/filters/FeGaussianBlur'; +import type { FeOffsetProps } from './elements/filters/FeOffset'; +import { getBoundingClientRect } from './web/utils'; export class Circle extends WebShape { tag = 'circle' as const; diff --git a/src/filter-image/extract/extractFilters.ts b/src/filter-image/extract/extractFilters.ts index 44a18c507..aef3153ae 100644 --- a/src/filter-image/extract/extractFilters.ts +++ b/src/filter-image/extract/extractFilters.ts @@ -1,5 +1,5 @@ import React from 'react'; -import { tags } from '../../tags'; +import { tags } from '../../xmlTags'; import { FilterElement, Filters } from '../types'; import { parse } from './extractFiltersString'; diff --git a/src/xml.tsx b/src/xml.tsx index d0bb5e087..30349f931 100644 --- a/src/xml.tsx +++ b/src/xml.tsx @@ -2,7 +2,7 @@ import type { ComponentType, ComponentProps } from 'react'; import * as React from 'react'; import { Component, useEffect, useMemo, useState } from 'react'; import type { SvgProps } from './elements/Svg'; -import { tags } from './tags'; +import { tags } from './xmlTags'; function missingTag() { return null; @@ -534,6 +534,7 @@ export function parse(source: string, middleware?: Middleware): JsxAST | null { if (root) { const xml: XmlAST = (middleware ? middleware(root) : root) || root; + console.log(xml.children.map((el) => console.log(el))); const ast: (JSX.Element | string)[] = xml.children.map(astToReact); const jsx: JsxAST = xml as JsxAST; jsx.children = ast; diff --git a/src/tags.web.ts b/src/xmlTags.ts similarity index 96% rename from src/tags.web.ts rename to src/xmlTags.ts index 91fe5722f..5425b2f92 100644 --- a/src/tags.web.ts +++ b/src/xmlTags.ts @@ -9,6 +9,7 @@ import { FeOffset, ForeignObject, G, + Image, Line, LinearGradient, Marker, @@ -26,7 +27,7 @@ import { TextPath, TSpan, Use, -} from './web/elements'; +} from './elements'; export const tags = { circle: Circle, From 6f6dabd5328f26170da9b0fcf35fd7ec77471a29 Mon Sep 17 00:00:00 2001 From: bohdanprog Date: Tue, 30 Jul 2024 16:36:53 +0200 Subject: [PATCH 05/10] feat: fix web implementation for SvgXml --- src/css/LocalSvg.tsx | 17 +++++++++-------- src/xml.tsx | 11 +++++++++-- 2 files changed, 18 insertions(+), 10 deletions(-) diff --git a/src/css/LocalSvg.tsx b/src/css/LocalSvg.tsx index ab96d119d..b2f1fe3d4 100644 --- a/src/css/LocalSvg.tsx +++ b/src/css/LocalSvg.tsx @@ -1,15 +1,16 @@ import * as React from 'react'; import { useState, useEffect, Component } from 'react'; -import type { ImageSourcePropType } from 'react-native'; -import { Platform, Image } from 'react-native'; - -import { fetchText } from 'react-native-svg'; +import { Image, Platform, type ImageSourcePropType } from 'react-native'; +import { fetchText, type SvgProps } from 'react-native-svg'; +import { resolveAssetUri } from '../lib/resolveAssetUri'; import { SvgCss, SvgWithCss } from './css'; -import type { SvgProps } from 'react-native-svg'; export function getUriFromSource(source: ImageSourcePropType) { - const resolvedAssetSource = Image.resolveAssetSource(source); - return resolvedAssetSource.uri; + const resolvedAssetSource = + Platform.OS === 'web' + ? resolveAssetUri(source) + : Image.resolveAssetSource(source); + return resolvedAssetSource?.uri; } export function loadLocalRawResourceDefault(source: ImageSourcePropType) { @@ -39,7 +40,7 @@ export async function loadAndroidRawResource(uri: string) { export function loadLocalRawResourceAndroid(source: ImageSourcePropType) { const uri = getUriFromSource(source); - if (isUriAnAndroidResourceIdentifier(uri)) { + if (uri && isUriAnAndroidResourceIdentifier(uri)) { return loadAndroidRawResource(uri); } else { return fetchText(uri); diff --git a/src/xml.tsx b/src/xml.tsx index 30349f931..cb72dd058 100644 --- a/src/xml.tsx +++ b/src/xml.tsx @@ -78,7 +78,10 @@ export function SvgXml(props: XmlProps) { } } -export async function fetchText(uri: string) { +export async function fetchText(uri?: string) { + if (!uri) { + return null; + } const response = await fetch(uri); if (response.ok || (response.status === 0 && uri.startsWith('file://'))) { return await response.text(); @@ -207,6 +210,11 @@ export function astToReact( ): JSX.Element | string { if (typeof value === 'object') { const { Tag, props, children } = value; + if (props?.class) { + props.className = props.class; + delete props.class; + } + return ( {(children as (AST | string)[]).map(astToReact)} @@ -534,7 +542,6 @@ export function parse(source: string, middleware?: Middleware): JsxAST | null { if (root) { const xml: XmlAST = (middleware ? middleware(root) : root) || root; - console.log(xml.children.map((el) => console.log(el))); const ast: (JSX.Element | string)[] = xml.children.map(astToReact); const jsx: JsxAST = xml as JsxAST; jsx.children = ast; From 6cb5268a31ed1f4e27e3322660858660ecdc4bda Mon Sep 17 00:00:00 2001 From: bohdanprog Date: Tue, 30 Jul 2024 16:42:39 +0200 Subject: [PATCH 06/10] fix: clean code --- src/xml.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/xml.tsx b/src/xml.tsx index 30349f931..d307bfb5c 100644 --- a/src/xml.tsx +++ b/src/xml.tsx @@ -534,7 +534,6 @@ export function parse(source: string, middleware?: Middleware): JsxAST | null { if (root) { const xml: XmlAST = (middleware ? middleware(root) : root) || root; - console.log(xml.children.map((el) => console.log(el))); const ast: (JSX.Element | string)[] = xml.children.map(astToReact); const jsx: JsxAST = xml as JsxAST; jsx.children = ast; From 6a7178936e9d2170a6efa501a28896dd82b64c9d Mon Sep 17 00:00:00 2001 From: Jakub Grzywacz Date: Wed, 31 Jul 2024 08:55:05 +0200 Subject: [PATCH 07/10] fix: test examples on web --- apps/test-examples/src/ColorTest.tsx | 42 ++++----- apps/test-examples/src/Test1845.tsx | 122 ++++++++++++++------------- 2 files changed, 81 insertions(+), 83 deletions(-) diff --git a/apps/test-examples/src/ColorTest.tsx b/apps/test-examples/src/ColorTest.tsx index d28792a0f..c9eb63fd2 100644 --- a/apps/test-examples/src/ColorTest.tsx +++ b/apps/test-examples/src/ColorTest.tsx @@ -1,18 +1,17 @@ import React from 'react'; -import { PlatformColor, Platform, Button, DynamicColorIOS } from 'react-native'; -import { - Svg, - Circle, - Rect, - Text, - TSpan, -} from 'react-native-svg'; +import {PlatformColor, Platform, Button, DynamicColorIOS} from 'react-native'; +import {Svg, Circle, Rect, Text, TSpan} from 'react-native-svg'; -const color = PlatformColor(Platform.select({ - ios: 'systemTealColor', - android: '@android:color/holo_blue_bright', - default: 'black', -})) +const color = + Platform.OS !== 'web' + ? PlatformColor( + Platform.select({ + ios: 'systemTealColor', + android: '@android:color/holo_blue_bright', + default: 'black', + }), + ) + : 'black'; // const customContrastDynamicTextColor = DynamicColorIOS({ // dark: 'hsla(360, 40%, 30%, 1.0)', @@ -27,13 +26,7 @@ export default () => { return ( <> - + { - - Testing word-wrap... Testing word-wrap... Testing word-wrap... Testing word-wrap... + + Testing word-wrap... Testing word-wrap... Testing word-wrap... + Testing word-wrap... -