From 503a7a68ea34e5978c6fcc1d69eaca7da336d639 Mon Sep 17 00:00:00 2001 From: Mikael Sand Date: Sun, 25 Aug 2019 22:44:32 +0300 Subject: [PATCH] feature: Implement SvgUri and SvgXml #1074 --- README.md | 108 +++++++++----- index.js | 14 ++ xml.js | 428 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 511 insertions(+), 39 deletions(-) create mode 100644 xml.js diff --git a/README.md b/README.md index 3c715a0f..8933b402 100644 --- a/README.md +++ b/README.md @@ -21,6 +21,7 @@ - [Usage](#usage) - [Use with content loaded from uri](#use-with-content-loaded-from-uri) - [Use with svg files](#use-with-svg-files) + - [Use with xml strings](#use-with-xml-strings) - [Common props](#common-props) - [Supported elements](#supported-elements) - [Svg](#svg) @@ -67,7 +68,7 @@ ```bash cd ios && pod install ``` - + Pre 0.60 ```bash react-native link react-native-svg @@ -116,14 +117,14 @@ v7 and newer requires the patch for making android thread safe, to get native an 2. Append the following lines to `android/settings.gradle`: - ``` + ```gradle include ':react-native-svg' project(':react-native-svg').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-svg/android') ``` 3. Insert the following lines inside the dependencies block in `android/app/build.gradle`: - ``` + ```gradle implementation project(':react-native-svg') ``` @@ -146,7 +147,7 @@ To install react-native-svg on iOS visit the link referenced above or do the fol Alternatively, you can use [CocoaPods](https://cocoapods.org/) to manage your native (Objective-C and Swift) dependencies: 1. Add RNSVG to your Podfile (with RN 0.60+ autolinking, this is not needed) -``` +```ruby pod 'RNSVG', :path => '../node_modules/react-native-svg' ``` @@ -215,7 +216,7 @@ Here's a simple example. To render output like this: Use the following code: -```javascript +```jsx import Svg,{ Circle, Ellipse, @@ -285,18 +286,15 @@ export default class SvgExample extends React.Component { ### Use with content loaded from uri -Try [react-native-svg-uri](https://github.com/vault-development/react-native-svg-uri) ```jsx import * as React from 'react'; -import SvgUri from 'react-native-svg-uri'; +import { SvgUri } from 'react-native-svg'; export default () => ( ); ``` @@ -340,7 +338,7 @@ You can then use your image as a component: ``` -Alternatively, you can use [react-native-svg-uri](https://github.com/vault-development/react-native-svg-uri) with [babel-plugin-inline-import](https://github.com/credcollective/babel-plugin-inline-import/), but with transforms done at run-time. +Alternatively, you can use SvgXml with [babel-plugin-inline-import](https://github.com/credcollective/babel-plugin-inline-import/), but with transforms done at run-time. .babelrc ```json @@ -359,17 +357,49 @@ Alternatively, you can use [react-native-svg-uri](https://github.com/vault-devel App.js ```jsx import * as React from 'react'; -import SvgUri from 'react-native-svg-uri'; +import { SvgXml } from 'react-native-svg'; import testSvg from './test.svg'; export default () => ( - ); ``` +### Use with xml strings + +```jsx +import * as React from 'react'; +import { SvgXml } from 'react-native-svg'; + +const xml = ` + + + + + + + + + +`; + +export default () => ; +``` + ### Common props: Name | Default | Description @@ -397,7 +427,7 @@ originY | 0 | Transform originY coordinates for the current obj #### Svg -```html +```jsx element is used to create a rectangle and variations of a rectangle shape: -```html +```jsx element is used to create a rectangle and variations of a rectangle s The element is used to create a circle: -```html +```jsx element is used to create an ellipse. An ellipse is closely related to a circle. The difference is that an ellipse has an x and a y radius that differs from each other, while a circle has equal x and y radius. -```html +```jsx element is an SVG basic shape, used to create a line connecting two points. -```html +```jsx element is used to create a graphic that contains at least three sides. Polygons are made of straight lines, and the shape is "closed" (all the lines connect up). -```html +```jsx element is used to create any shape that consists of only straight lines: -```html +```jsx element is used to define text. -```html +```jsx element is used to define text. The element is used to draw multiple lines of text in SVG. Rather than having to position each line of text absolutely, the element makes it possible to position a line of text relatively to the previous line of text. -```html +```jsx element is used to draw multiple lines of text in SVG. Rather than h In addition to text drawn in a straight line, SVG also includes the ability to place text along the shape of a element. To specify that a block of text is to be rendered along the shape of a , include the given text within a element which includes an href attribute with a reference to a element. -```html +```jsx element is a container used to group other SVG elements. Transformations applied to the g element are performed on all of its child elements, and any of its props are inherited by its child elements. It can also group multiple elements to be referenced later with the [<Use />](#use) element. -```html +```jsx element is a container used to group other SVG elements. Transformations The element can reuse an SVG shape from elsewhere in the SVG document, including elements and elements. The reused shape can be defined inside the [<Defs>](#defs) element (which makes the shape invisible until used) or outside. -```html +```jsx element specifies where to show the reused shapes via its x and y prop The SVG element is used to define reusable symbols. The shapes nested inside a are not displayed unless referenced by a element. -```html +```jsx element is used to embed definitions that can be reused inside an SVG The element allows a raster image to be included in an Svg componenet. -```html +```jsx element allows a raster image to be included in an Svg componenet. The SVG element defines a clipping path. A clipping path is used/referenced using the clipPath property -```html +```jsx @@ -992,7 +1022,7 @@ This result is same as the example before. But it's recommend to use exact numbe The element is used to define a radial gradient. The element must be nested within a [<Defs>](#defs) tag. The [<Defs>](#defs) tag is short for definitions and contains definition of special elements (such as gradients). -```html +```jsx element must be nested within a [<Defs>](#defs) tag. The [<Defs>](#defs) tag is short for definitions and contains definition of special elements (such as gradients). https://www.w3.org/TR/SVG11/images/masking/mask01.svg -```html +```jsx element must be nested within a [<Defs>](#defs) tag. The [<Defs>](#defs) tag is short for definitions and contains definition of special elements (such as gradients). https://www.w3.org/TR/SVG11/images/pservers/pattern01.svg -```html +```jsx + {children} + + ); +} + +export function SvgXml({ xml, ...props }) { + const ast = useMemo(() => xml && parse(xml), [xml]); + return (ast && ) || null; +} + +async function fetchText(uri) { + const response = await fetch(uri); + return await response.text(); +} + +const err = console.error.bind(console); + +export function SvgUri({ uri, ...props }) { + const [xml, setXml] = useState(); + useEffect(() => { + fetchText(uri) + .then(setXml) + .catch(err); + }, [uri]); + return (xml && ) || null; +} + +// Extending Component is required for Animated support. + +export class SvgFromXml extends Component { + state = {}; + componentDidMount() { + const { xml } = this.props; + this.parse(xml); + } + componentDidUpdate(prevProps) { + const { xml } = this.props; + if (xml !== prevProps.xml) { + this.parse(xml); + } + } + parse(xml) { + try { + const ast = parse(xml); + this.setState({ ast }); + } catch (e) { + console.error(e); + } + } + render() { + const { ast } = this.state; + return ast ? : null; + } +} + +export class SvgFromUri extends Component { + state = {}; + componentDidMount() { + const { uri } = this.props; + this.fetch(uri); + } + componentDidUpdate(prevProps) { + const { uri } = this.props; + if (uri !== prevProps.uri) { + this.fetch(uri); + } + } + async fetch(uri) { + try { + const xml = await fetchText(uri); + this.setState({ xml }); + } catch (e) { + console.error(e); + } + } + render() { + const { xml } = this.state; + return xml ? : null; + } +} + +const upperCase = (match, letter) => letter.toUpperCase(); + +const camelCase = phrase => phrase.replace(/-([a-z])/g, upperCase); + +export function getStyle(string) { + const style = {}; + const declarations = string.split(";"); + for (let i = 0, l = declarations.length; i < l; i++) { + const declaration = declarations[i].split(":"); + const property = declaration[0]; + const value = declaration[1]; + style[camelCase(property.trim())] = value.trim(); + } + return style; +} + +export function astToReact(child, i) { + if (typeof child === "object") { + const { Tag, props, children } = child; + return ( + + {children.map(astToReact)} + + ); + } + return child; +} + +// slimmed down parser based on https://github.com/Rich-Harris/svg-parser + +function locate(source, search) { + const lines = source.split("\n"); + for (let line = 0, l = lines.length; line < l; line++) { + const { length } = lines[line]; + if (search < length) { + return { line, column: search }; + } else { + search -= length; + } + } +} + +const validNameCharacters = /[a-zA-Z0-9:_-]/; +const whitespace = /[\s\t\r\n]/; +const quotemark = /['"]/; + +function repeat(str, i) { + let result = ""; + while (i--) result += str; + return result; +} + +export function parse(source) { + const length = source.length; + let currentElement = null; + let state = metadata; + let children = null; + let root = null; + let stack = []; + + function error(message) { + const { line, column } = locate(source, i); + const before = source + .slice(0, i) + .replace(/^\t+/, match => repeat(" ", match.length)); + const beforeLine = /(^|\n).*$/.exec(before)[0]; + const after = source.slice(i); + const afterLine = /.*(\n|$)/.exec(after)[0]; + + const snippet = `${beforeLine}${afterLine}\n${repeat( + " ", + beforeLine.length + )}^`; + + throw new Error( + `${message} (${line}:${column}). If this is valid SVG, it's probably a bug. Please raise an issue\n\n${snippet}` + ); + } + + function metadata() { + while ( + (i < length && source[i] !== "<") || + !validNameCharacters.test(source[i + 1]) + ) { + i++; + } + + return neutral(); + } + + function neutral() { + let text = ""; + while (i < length && source[i] !== "<") text += source[i++]; + + if (/\S/.test(text)) { + children.push(text); + } + + if (source[i] === "<") { + return openingTag; + } + + return neutral; + } + + function openingTag() { + const char = source[i]; + + if (char === "?") return neutral; // ") { + error("Expected >"); + } + + if (!selfClosing) { + currentElement = element; + ({ children } = element); + stack.push(element); + } + + return neutral; + } + + function comment() { + const index = source.indexOf("-->", i); + if (!~index) error("expected -->"); + + i = index + 2; + return neutral; + } + + function cdata() { + const index = source.indexOf("]]>", i); + if (!~index) error("expected ]]>"); + + i = index + 2; + return neutral; + } + + function closingTag() { + const tag = getName(); + + if (!tag) error("Expected tag name"); + + if (tag !== currentElement.tag) { + error( + `Expected closing tag to match opening tag <${ + currentElement.tag + }>` + ); + } + + if (source[i] !== ">") { + error("Expected >"); + } + + stack.pop(); + currentElement = stack[stack.length - 1]; + if (currentElement) { + ({ children } = currentElement); + } + + return neutral; + } + + function getName() { + let name = ""; + while (i < length && validNameCharacters.test(source[i])) + name += source[i++]; + + return name; + } + + function getAttributes(props) { + while (i < length) { + if (!whitespace.test(source[i])) return; + allowSpaces(); + + const name = getName(); + if (!name) return; + + let value = true; + + allowSpaces(); + if (source[i] === "=") { + i += 1; + allowSpaces(); + + value = getAttributeValue(); + if (!isNaN(value) && value.trim() !== "") value = +value; + } + + props[camelCase(name)] = value; + } + } + + function getAttributeValue() { + return quotemark.test(source[i]) + ? getQuotedAttributeValue() + : getUnquotedAttributeValue(); + } + + function getUnquotedAttributeValue() { + let value = ""; + do { + const char = source[i]; + if (char === " " || char === ">" || char === "/") { + return value; + } + + value += char; + i += 1; + } while (i < length); + + return value; + } + + function getQuotedAttributeValue() { + const quotemark = source[i++]; + + let value = ""; + let escaped = false; + + while (i < length) { + const char = source[i++]; + if (char === quotemark && !escaped) { + return value; + } + + if (char === "\\" && !escaped) { + escaped = true; + } + + value += escaped ? `\\${char}` : char; + escaped = false; + } + } + + function allowSpaces() { + while (i < length && whitespace.test(source[i])) i += 1; + } + + let i = 0; + while (i < length) { + if (!state) error("Unexpected character"); + state = state(); + i += 1; + } + + if (state !== neutral) { + error("Unexpected end of input"); + } + + root.children = root.children.map(astToReact); + + return root; +}