mirror of
https://github.com/zoriya/react-native-svg.git
synced 2026-06-04 15:44:24 +00:00
Merge branch 'css' into css_test
This commit is contained in:
+114
-65
@@ -1,7 +1,29 @@
|
||||
import React, { useEffect, useMemo, useState } from 'react';
|
||||
import { camelCase, parse, SvgAst } from './xml';
|
||||
import {
|
||||
AST,
|
||||
camelCase,
|
||||
err,
|
||||
fetchText,
|
||||
parse,
|
||||
Styles,
|
||||
SvgAst,
|
||||
UriProps,
|
||||
XmlProps,
|
||||
} from './xml';
|
||||
// @ts-ignore
|
||||
import baseCssAdapter from 'css-select-base-adapter';
|
||||
import csstree, { List } from 'css-tree';
|
||||
import csstree, {
|
||||
Atrule,
|
||||
CssNode,
|
||||
Declaration,
|
||||
DeclarationList,
|
||||
List,
|
||||
ListItem,
|
||||
PseudoClassSelector,
|
||||
Rule,
|
||||
Selector,
|
||||
SelectorList,
|
||||
} from 'css-tree';
|
||||
import cssSelect from 'css-select';
|
||||
import stable from 'stable';
|
||||
|
||||
@@ -15,26 +37,26 @@ import stable from 'stable';
|
||||
const rnsvgCssSelectAdapterMin = {
|
||||
// is the node a tag?
|
||||
// isTag: ( node:Node ) => isTag:Boolean
|
||||
isTag(node) {
|
||||
isTag(node: AST) {
|
||||
return node.tag;
|
||||
},
|
||||
|
||||
// get the parent of the node
|
||||
// getParent: ( node:Node ) => parentNode:Node
|
||||
// returns null when no parent exists
|
||||
getParent(node) {
|
||||
getParent(node: AST) {
|
||||
return node.parent || null;
|
||||
},
|
||||
|
||||
// get the node's children
|
||||
// getChildren: ( node:Node ) => children:[Node]
|
||||
getChildren(node) {
|
||||
getChildren(node: AST) {
|
||||
return node.children || [];
|
||||
},
|
||||
|
||||
// get the name of the tag
|
||||
// getName: ( elem:ElementNode ) => tagName:String
|
||||
getName(elemAst) {
|
||||
getName(elemAst: AST) {
|
||||
return elemAst.tag;
|
||||
},
|
||||
|
||||
@@ -48,8 +70,8 @@ const rnsvgCssSelectAdapterMin = {
|
||||
// get the attribute value
|
||||
// getAttributeValue: ( elem:ElementNode, name:String ) => value:String
|
||||
// returns null when attribute doesn't exist
|
||||
getAttributeValue(elem, name) {
|
||||
return elem && elem.props.hasOwnProperty(name) ? elem.props[name] : null;
|
||||
getAttributeValue(elem: AST, name: string) {
|
||||
return elem.props.hasOwnProperty(name) ? elem.props[name] : null;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -63,7 +85,7 @@ const rnsvgCssSelectAdapter = baseCssAdapter(rnsvgCssSelectAdapterMin);
|
||||
* @param {String} selectors CSS selector(s) string
|
||||
* @return {Array}
|
||||
*/
|
||||
function querySelectorAll(document, selectors) {
|
||||
function querySelectorAll(document: AST, selectors: string) {
|
||||
return cssSelect(selectors, document, cssSelectOpts);
|
||||
}
|
||||
const cssSelectOpts = {
|
||||
@@ -71,23 +93,37 @@ const cssSelectOpts = {
|
||||
adapter: rnsvgCssSelectAdapter,
|
||||
};
|
||||
|
||||
type FlatPseudoSelector = {
|
||||
item: ListItem<CssNode>;
|
||||
list: List<CssNode>;
|
||||
};
|
||||
type FlatPseudoSelectorList = FlatPseudoSelector[];
|
||||
type FlatSelector = {
|
||||
item: ListItem<CssNode>;
|
||||
atrule: Atrule | null;
|
||||
rule: CssNode;
|
||||
pseudos: FlatPseudoSelectorList;
|
||||
};
|
||||
type FlatSelectorList = FlatSelector[];
|
||||
|
||||
/**
|
||||
* Flatten a CSS AST to a selectors list.
|
||||
*
|
||||
* @param {Object} cssAst css-tree AST to flatten
|
||||
* @param {Array} selectors
|
||||
*/
|
||||
function flattenToSelectors(cssAst, selectors) {
|
||||
function flattenToSelectors(cssAst: CssNode, selectors: FlatSelectorList) {
|
||||
csstree.walk(cssAst, {
|
||||
visit: 'Rule',
|
||||
enter(rule) {
|
||||
const { type, prelude } = rule;
|
||||
enter(rule: CssNode) {
|
||||
const { type, prelude } = rule as Rule;
|
||||
if (type !== 'Rule') {
|
||||
return;
|
||||
}
|
||||
const atrule = this.atrule;
|
||||
prelude.children.each(({ children }, item) => {
|
||||
const pseudos = [];
|
||||
(prelude as SelectorList).children.each((node, item) => {
|
||||
const { children } = node as Selector;
|
||||
const pseudos: FlatPseudoSelectorList = [];
|
||||
selectors.push({
|
||||
item,
|
||||
atrule,
|
||||
@@ -116,11 +152,12 @@ function flattenToSelectors(cssAst, selectors) {
|
||||
* @param {Array} selectors to filter
|
||||
* @return {Array} Filtered selectors that match the passed media queries
|
||||
*/
|
||||
function filterByMqs(selectors) {
|
||||
function filterByMqs(selectors: FlatSelectorList) {
|
||||
return selectors.filter(({ atrule }) => {
|
||||
if (atrule === null) {
|
||||
return ~useMqs.indexOf('');
|
||||
}
|
||||
// @ts-ignore
|
||||
const { name, expression } = atrule;
|
||||
return ~useMqs.indexOf(
|
||||
expression && expression.children.first().type === 'MediaQueryList'
|
||||
@@ -138,13 +175,13 @@ const useMqs = ['', 'screen'];
|
||||
* @param {Array} selectors to filter
|
||||
* @return {Array} Filtered selectors that match the passed pseudo-elements and/or -classes
|
||||
*/
|
||||
function filterByPseudos(selectors) {
|
||||
function filterByPseudos(selectors: FlatSelectorList) {
|
||||
return selectors.filter(
|
||||
({ pseudos }) =>
|
||||
~usePseudos.indexOf(
|
||||
csstree.generate({
|
||||
type: 'Selector',
|
||||
children: new List().fromArray(
|
||||
children: new List<CssNode>().fromArray(
|
||||
pseudos.map(pseudo => pseudo.item.data),
|
||||
),
|
||||
}),
|
||||
@@ -160,18 +197,19 @@ const usePseudos = [''];
|
||||
* @param {Array} selectors to clean
|
||||
* @return {Array} Selectors without pseudo-elements and/or -classes
|
||||
*/
|
||||
function cleanPseudos(selectors) {
|
||||
function cleanPseudos(selectors: FlatSelectorList) {
|
||||
selectors.forEach(({ pseudos }) =>
|
||||
pseudos.forEach(pseudo => pseudo.list.remove(pseudo.item)),
|
||||
);
|
||||
}
|
||||
|
||||
function specificity(selector) {
|
||||
type Specificity = [number, number, number];
|
||||
function specificity(selector: Selector): Specificity {
|
||||
let A = 0;
|
||||
let B = 0;
|
||||
let C = 0;
|
||||
|
||||
selector.children.each(function walk(node) {
|
||||
selector.children.each(function walk(node: CssNode) {
|
||||
switch (node.type) {
|
||||
case 'SelectorList':
|
||||
case 'Selector':
|
||||
@@ -190,7 +228,8 @@ function specificity(selector) {
|
||||
case 'PseudoClassSelector':
|
||||
switch (node.name.toLowerCase()) {
|
||||
case 'not':
|
||||
node.children.each(walk);
|
||||
const children = (node as PseudoClassSelector).children;
|
||||
children && children.each(walk);
|
||||
break;
|
||||
|
||||
case 'before':
|
||||
@@ -232,7 +271,10 @@ function specificity(selector) {
|
||||
* @param {Array} bSpecificity Specificity of selector B
|
||||
* @return {Number} Score of selector specificity A compared to selector specificity B
|
||||
*/
|
||||
function compareSpecificity(aSpecificity, bSpecificity) {
|
||||
function compareSpecificity(
|
||||
aSpecificity: Specificity,
|
||||
bSpecificity: Specificity,
|
||||
) {
|
||||
for (let i = 0; i < 4; i += 1) {
|
||||
if (aSpecificity[i] < bSpecificity[i]) {
|
||||
return -1;
|
||||
@@ -250,9 +292,12 @@ function compareSpecificity(aSpecificity, bSpecificity) {
|
||||
* @param {Object} selectorB Simple selector B
|
||||
* @return {Number} Score of selector A compared to selector B
|
||||
*/
|
||||
function bySelectorSpecificity(selectorA, selectorB) {
|
||||
const aSpecificity = specificity(selectorA.item.data),
|
||||
bSpecificity = specificity(selectorB.item.data);
|
||||
function bySelectorSpecificity(
|
||||
selectorA: FlatSelector,
|
||||
selectorB: FlatSelector,
|
||||
) {
|
||||
const aSpecificity = specificity(selectorA.item.data as Selector),
|
||||
bSpecificity = specificity(selectorB.item.data as Selector);
|
||||
return compareSpecificity(aSpecificity, bSpecificity);
|
||||
}
|
||||
|
||||
@@ -262,7 +307,7 @@ function bySelectorSpecificity(selectorA, selectorB) {
|
||||
* @param {Array} selectors to be sorted
|
||||
* @return {Array} Stable sorted selectors
|
||||
*/
|
||||
function sortSelectors(selectors) {
|
||||
function sortSelectors(selectors: FlatSelectorList) {
|
||||
return stable(selectors, bySelectorSpecificity);
|
||||
}
|
||||
|
||||
@@ -270,8 +315,13 @@ const declarationParseProps = {
|
||||
context: 'declarationList',
|
||||
parseValue: false,
|
||||
};
|
||||
function CSSStyleDeclaration({ props: { style }, styles }) {
|
||||
type CSSStyleDeclaration = {
|
||||
style: Styles;
|
||||
properties: Map<string, boolean | undefined>;
|
||||
};
|
||||
function CSSStyleDeclaration({ props, styles }: AST): CSSStyleDeclaration {
|
||||
const properties = new Map();
|
||||
const style = props.style as Styles;
|
||||
const styleDeclaration = {
|
||||
style,
|
||||
properties,
|
||||
@@ -280,22 +330,25 @@ function CSSStyleDeclaration({ props: { style }, styles }) {
|
||||
return styleDeclaration;
|
||||
}
|
||||
try {
|
||||
csstree
|
||||
.parse(styles, declarationParseProps)
|
||||
.children.each(({ property, value, important }) => {
|
||||
try {
|
||||
const name = property.trim();
|
||||
properties.set(name, important);
|
||||
style[camelCase(name)] = csstree.generate(value).trim();
|
||||
} catch (styleError) {
|
||||
if (styleError.message !== 'Unknown node type: undefined') {
|
||||
console.warn(
|
||||
"Warning: Parse error when parsing inline styles, style properties of this element cannot be used. The raw styles can still be get/set using .attr('style').value. Error details: " +
|
||||
styleError,
|
||||
);
|
||||
}
|
||||
const declarations = csstree.parse(
|
||||
styles,
|
||||
declarationParseProps,
|
||||
) as DeclarationList;
|
||||
declarations.children.each(node => {
|
||||
try {
|
||||
const { property, value, important } = node as Declaration;
|
||||
const name = property.trim();
|
||||
properties.set(name, important);
|
||||
style[camelCase(name)] = csstree.generate(value).trim();
|
||||
} catch (styleError) {
|
||||
if (styleError.message !== 'Unknown node type: undefined') {
|
||||
console.warn(
|
||||
"Warning: Parse error when parsing inline styles, style properties of this element cannot be used. The raw styles can still be get/set using .attr('style').value. Error details: " +
|
||||
styleError,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
} catch (parseError) {
|
||||
console.warn(
|
||||
"Warning: Parse error when parsing inline styles, style properties of this element cannot be used. The raw styles can still be get/set using .attr('style').value. Error details: " +
|
||||
@@ -305,7 +358,7 @@ function CSSStyleDeclaration({ props: { style }, styles }) {
|
||||
return styleDeclaration;
|
||||
}
|
||||
|
||||
function initStyle(selectedEl) {
|
||||
function initStyle(selectedEl: AST) {
|
||||
if (!selectedEl.style) {
|
||||
if (!selectedEl.props.style) {
|
||||
selectedEl.props.style = {};
|
||||
@@ -320,8 +373,8 @@ function initStyle(selectedEl) {
|
||||
* @param elemName
|
||||
* @return {?Object}
|
||||
*/
|
||||
function closestElem(node, elemName) {
|
||||
let elem = node;
|
||||
function closestElem(node: AST, elemName: string) {
|
||||
let elem: AST | null = node;
|
||||
while ((elem = elem.parent) && elem.tag !== elemName) {}
|
||||
return elem;
|
||||
}
|
||||
@@ -346,9 +399,9 @@ const parseProps = {
|
||||
* @param {Object} document document element
|
||||
*
|
||||
* @author strarsis <strarsis@gmail.com>
|
||||
* @author modified by: msand <msand@abo.fim>
|
||||
* @author modified by: msand <msand@abo.fi>
|
||||
*/
|
||||
export function inlineStyles(document) {
|
||||
export function inlineStyles(document: AST) {
|
||||
// collect <style/>s
|
||||
const styleElements = querySelectorAll(document, 'style');
|
||||
|
||||
@@ -357,7 +410,7 @@ export function inlineStyles(document) {
|
||||
return document;
|
||||
}
|
||||
|
||||
const selectors = [];
|
||||
const selectors: FlatSelectorList = [];
|
||||
|
||||
for (let element of styleElements) {
|
||||
const { children } = element;
|
||||
@@ -368,7 +421,8 @@ export function inlineStyles(document) {
|
||||
|
||||
// collect <style/>s and their css ast
|
||||
try {
|
||||
flattenToSelectors(csstree.parse(children, parseProps), selectors);
|
||||
const styleString = children.join('');
|
||||
flattenToSelectors(csstree.parse(styleString, parseProps), selectors);
|
||||
} catch (parseError) {
|
||||
console.warn(
|
||||
'Warning: Parse error of styles of <style/> element, skipped. Error details: ' +
|
||||
@@ -406,7 +460,8 @@ export function inlineStyles(document) {
|
||||
}
|
||||
csstree.walk(rule, {
|
||||
visit: 'Declaration',
|
||||
enter({ property, value, important }) {
|
||||
enter(node: CssNode) {
|
||||
const { property, value, important } = node as Declaration;
|
||||
// existing inline styles have higher priority
|
||||
// no inline styles, external styles, external styles used
|
||||
// inline styles, external styles same priority as inline styles, inline styles used
|
||||
@@ -415,10 +470,10 @@ export function inlineStyles(document) {
|
||||
const camel = camelCase(name);
|
||||
const val = csstree.generate(value).trim();
|
||||
for (let element of matched) {
|
||||
const { style, properties } = element.style;
|
||||
const { style, properties } = element.style as CSSStyleDeclaration;
|
||||
const current = properties.get(name);
|
||||
if (current === undefined || current < important) {
|
||||
properties.set(name, important);
|
||||
properties.set(name, important as boolean);
|
||||
style[camel] = val;
|
||||
}
|
||||
}
|
||||
@@ -441,24 +496,18 @@ export function inlineStyles(document) {
|
||||
return document;
|
||||
}
|
||||
|
||||
export function SvgCss(props) {
|
||||
export function SvgCss(props: XmlProps) {
|
||||
const { xml, override } = props;
|
||||
const ast = useMemo(() => (xml !== null ? parse(xml, inlineStyles) : null), [
|
||||
xml,
|
||||
]);
|
||||
const ast = useMemo<AST | null>(
|
||||
() => (xml !== null ? parse(xml, inlineStyles) : null),
|
||||
[xml],
|
||||
);
|
||||
return <SvgAst ast={ast} override={override || props} />;
|
||||
}
|
||||
|
||||
async function fetchText(uri) {
|
||||
const response = await fetch(uri);
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
const err = console.error.bind(console);
|
||||
|
||||
export function SvgCssUri(props) {
|
||||
export function SvgCssUri(props: UriProps) {
|
||||
const { uri } = props;
|
||||
const [xml, setXml] = useState(null);
|
||||
const [xml, setXml] = useState<string | null>(null);
|
||||
useEffect(() => {
|
||||
uri
|
||||
? fetchText(uri)
|
||||
+8
-5
@@ -61,10 +61,13 @@ function missingTag() {
|
||||
|
||||
export interface AST {
|
||||
tag: string;
|
||||
style?: unknown;
|
||||
styles?: string;
|
||||
parent: AST | null;
|
||||
children: (AST | string)[] | (JSX.Element | string)[];
|
||||
props: {};
|
||||
props: {
|
||||
[prop: string]: Styles | string | undefined;
|
||||
};
|
||||
Tag: ComponentType;
|
||||
}
|
||||
|
||||
@@ -96,12 +99,12 @@ export function SvgXml(props: XmlProps) {
|
||||
return <SvgAst ast={ast} override={override || props} />;
|
||||
}
|
||||
|
||||
async function fetchText(uri: string) {
|
||||
export async function fetchText(uri: string) {
|
||||
const response = await fetch(uri);
|
||||
return await response.text();
|
||||
}
|
||||
|
||||
const err = console.error.bind(console);
|
||||
export const err = console.error.bind(console);
|
||||
|
||||
export function SvgUri(props: UriProps) {
|
||||
const { uri } = props;
|
||||
@@ -177,7 +180,7 @@ const upperCase = (_match: string, letter: string) => letter.toUpperCase();
|
||||
export const camelCase = (phrase: string) =>
|
||||
phrase.replace(/[:\-]([a-z])/g, upperCase);
|
||||
|
||||
type Styles = { [property: string]: string };
|
||||
export type Styles = { [property: string]: string };
|
||||
|
||||
export function getStyle(string: string): Styles {
|
||||
const style: Styles = {};
|
||||
@@ -324,7 +327,7 @@ export function parse(
|
||||
}
|
||||
|
||||
const tag = getName();
|
||||
const props: { style?: Styles | string } = {};
|
||||
const props: { [prop: string]: Styles | string | undefined } = {};
|
||||
const element: AST = {
|
||||
tag,
|
||||
props,
|
||||
|
||||
Reference in New Issue
Block a user