mirror of
https://github.com/zoriya/react-native-svg.git
synced 2026-06-04 07:25:53 +00:00
fix: improve style element inlining, support more selectors and optimize
This commit is contained in:
+138
-189
@@ -61,24 +61,22 @@ const rnsvgCssSelectAdapter = baseCssAdapter(rnsvgCssSelectAdapterMin);
|
|||||||
*
|
*
|
||||||
* @param {Object} document to select elements from
|
* @param {Object} document to select elements from
|
||||||
* @param {String} selectors CSS selector(s) string
|
* @param {String} selectors CSS selector(s) string
|
||||||
* @return {Array} null if no elements matched
|
* @return {Array}
|
||||||
*/
|
*/
|
||||||
function querySelectorAll(document, selectors) {
|
function querySelectorAll(document, selectors) {
|
||||||
const matchedEls = cssSelect(selectors, document, cssSelectOpts);
|
return cssSelect(selectors, document, cssSelectOpts);
|
||||||
|
|
||||||
return matchedEls.length > 0 ? matchedEls : null;
|
|
||||||
}
|
}
|
||||||
const cssSelectOpts = {
|
const cssSelectOpts = {
|
||||||
xmlMode: true,
|
xmlMode: true,
|
||||||
adapter: rnsvgCssSelectAdapter,
|
adapter: rnsvgCssSelectAdapter,
|
||||||
};
|
};
|
||||||
|
|
||||||
function specificity(simpleSelector) {
|
function specificity(selector) {
|
||||||
let A = 0;
|
let A = 0;
|
||||||
let B = 0;
|
let B = 0;
|
||||||
let C = 0;
|
let C = 0;
|
||||||
|
|
||||||
simpleSelector.children.each(function walk(node) {
|
selector.children.each(function walk(node) {
|
||||||
switch (node.type) {
|
switch (node.type) {
|
||||||
case 'SelectorList':
|
case 'SelectorList':
|
||||||
case 'Selector':
|
case 'Selector':
|
||||||
@@ -134,49 +132,39 @@ function specificity(simpleSelector) {
|
|||||||
* Flatten a CSS AST to a selectors list.
|
* Flatten a CSS AST to a selectors list.
|
||||||
*
|
*
|
||||||
* @param {Object} cssAst css-tree AST to flatten
|
* @param {Object} cssAst css-tree AST to flatten
|
||||||
* @return {Array} selectors
|
* @param {Array} selectors
|
||||||
*/
|
*/
|
||||||
function flattenToSelectors(cssAst) {
|
function flattenToSelectors(cssAst, selectors) {
|
||||||
const selectors = [];
|
|
||||||
|
|
||||||
csstree.walk(cssAst, {
|
csstree.walk(cssAst, {
|
||||||
visit: 'Rule',
|
visit: 'Rule',
|
||||||
enter(node) {
|
enter(rule) {
|
||||||
if (node.type !== 'Rule') {
|
const { type, prelude } = rule;
|
||||||
|
if (type !== 'Rule') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const atrule = this.atrule;
|
const atrule = this.atrule;
|
||||||
const rule = node;
|
prelude.children.each(({ children }, item) => {
|
||||||
|
const pseudos = [];
|
||||||
node.prelude.children.each((selectorNode, selectorItem) => {
|
selectors.push({
|
||||||
const selector = {
|
item,
|
||||||
item: selectorItem,
|
atrule,
|
||||||
atrule: atrule,
|
rule,
|
||||||
rule: rule,
|
pseudos,
|
||||||
pseudos: [],
|
});
|
||||||
};
|
children.each(({ type: childType }, pseudoItem, list) => {
|
||||||
|
if (
|
||||||
selectorNode.children.each(
|
childType === 'PseudoClassSelector' ||
|
||||||
(selectorChildNode, selectorChildItem, selectorChildList) => {
|
childType === 'PseudoElementSelector'
|
||||||
if (
|
) {
|
||||||
selectorChildNode.type === 'PseudoClassSelector' ||
|
pseudos.push({
|
||||||
selectorChildNode.type === 'PseudoElementSelector'
|
item: pseudoItem,
|
||||||
) {
|
list,
|
||||||
selector.pseudos.push({
|
});
|
||||||
item: selectorChildItem,
|
}
|
||||||
list: selectorChildList,
|
});
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
selectors.push(selector);
|
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return selectors;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,20 +252,16 @@ function compareSpecificity(aSpecificity, bSpecificity) {
|
|||||||
/**
|
/**
|
||||||
* Compare two simple selectors.
|
* Compare two simple selectors.
|
||||||
*
|
*
|
||||||
* @param {Object} aSimpleSelectorNode Simple selector A
|
* @param {Object} selectorA Simple selector A
|
||||||
* @param {Object} bSimpleSelectorNode Simple selector B
|
* @param {Object} selectorB Simple selector B
|
||||||
* @return {Number} Score of selector A compared to selector B
|
* @return {Number} Score of selector A compared to selector B
|
||||||
*/
|
*/
|
||||||
function compareSimpleSelectorNode(aSimpleSelectorNode, bSimpleSelectorNode) {
|
function bySelectorSpecificity(selectorA, selectorB) {
|
||||||
const aSpecificity = specificity(aSimpleSelectorNode),
|
const aSpecificity = specificity(selectorA.item.data),
|
||||||
bSpecificity = specificity(bSimpleSelectorNode);
|
bSpecificity = specificity(selectorB.item.data);
|
||||||
return compareSpecificity(aSpecificity, bSpecificity);
|
return compareSpecificity(aSpecificity, bSpecificity);
|
||||||
}
|
}
|
||||||
|
|
||||||
function _bySelectorSpecificity(selectorA, selectorB) {
|
|
||||||
return compareSimpleSelectorNode(selectorA.item.data, selectorB.item.data);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sort selectors stably by their specificity.
|
* Sort selectors stably by their specificity.
|
||||||
*
|
*
|
||||||
@@ -285,98 +269,97 @@ function _bySelectorSpecificity(selectorA, selectorB) {
|
|||||||
* @return {Array} Stable sorted selectors
|
* @return {Array} Stable sorted selectors
|
||||||
*/
|
*/
|
||||||
function sortSelectors(selectors) {
|
function sortSelectors(selectors) {
|
||||||
return stable(selectors, _bySelectorSpecificity);
|
return stable(selectors, bySelectorSpecificity);
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets the CSS string of a style element
|
|
||||||
*
|
|
||||||
* @param {Object} element style element
|
|
||||||
* @return {String|Array} CSS string or empty array if no styles are set
|
|
||||||
*/
|
|
||||||
function getCssStr(element) {
|
|
||||||
return element.children || [];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function CSSStyleDeclaration(node) {
|
function CSSStyleDeclaration(node) {
|
||||||
this.style = node.props.style;
|
const style = {
|
||||||
this.properties = new Map();
|
style: node.props.style,
|
||||||
|
properties: new Map(),
|
||||||
|
};
|
||||||
const { styles } = node;
|
const { styles } = node;
|
||||||
if (!styles || styles.length === 0) {
|
if (!styles || styles.length === 0) {
|
||||||
return;
|
return style;
|
||||||
}
|
}
|
||||||
|
|
||||||
let declarations = {};
|
|
||||||
try {
|
try {
|
||||||
declarations = csstree.parse(styles, {
|
csstree
|
||||||
context: 'declarationList',
|
.parse(styles, {
|
||||||
parseValue: false,
|
context: 'declarationList',
|
||||||
});
|
parseValue: false,
|
||||||
|
})
|
||||||
|
.children.each(({ property, value, important }) => {
|
||||||
|
try {
|
||||||
|
setProperty(style, property, csstree.generate(value), important);
|
||||||
|
} 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) {
|
} catch (parseError) {
|
||||||
console.warn(
|
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: " +
|
"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: " +
|
||||||
parseError,
|
parseError,
|
||||||
);
|
);
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
declarations.children.each(declaration => {
|
return style;
|
||||||
try {
|
|
||||||
const { property, value, important } = declaration;
|
|
||||||
this.setProperty(property, csstree.generate(value), important);
|
|
||||||
} 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,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
CSSStyleDeclaration.prototype.getProperty = function(propertyName) {
|
|
||||||
if (typeof propertyName === 'undefined') {
|
|
||||||
throw Error('1 argument required, but only 0 present.');
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.properties.get(propertyName.trim());
|
|
||||||
};
|
|
||||||
|
|
||||||
// writes to properties
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Modify an existing CSS property or creates a new CSS property in the declaration block.
|
* Modify an existing CSS property or creates a new CSS property in the declaration block.
|
||||||
*
|
*
|
||||||
|
* @param {{properties: *, style: *}}
|
||||||
* @param {String} name representing the CSS property name to be modified.
|
* @param {String} name representing the CSS property name to be modified.
|
||||||
* @param {String} [value] containing the new property value. If not specified, treated as the empty string. value must not contain "!important" -- that should be set using the priority parameter.
|
* @param {String} [value] containing the new property value. If not specified, treated as the empty string. value must not contain "!important" -- that should be set using the priority parameter.
|
||||||
* @param {String} [important] allowing the "important" CSS priority to be set. If not specified, treated as the empty string.
|
* @param {String} [important] allowing the "important" CSS priority to be set. If not specified, treated as the empty string.
|
||||||
* @return {undefined}
|
* @return {undefined}
|
||||||
*/
|
*/
|
||||||
CSSStyleDeclaration.prototype.setProperty = function(name, value, important) {
|
function setProperty({ properties, style }, name, value, important) {
|
||||||
if (typeof name === 'undefined') {
|
if (typeof name === 'undefined') {
|
||||||
throw Error('propertyName argument required, but only not present.');
|
throw Error('propertyName argument required, but only not present.');
|
||||||
}
|
}
|
||||||
|
const v = value.trim();
|
||||||
const trimmedValue = value.trim();
|
|
||||||
const property = {
|
|
||||||
value: trimmedValue,
|
|
||||||
important,
|
|
||||||
};
|
|
||||||
const key = name.trim();
|
const key = name.trim();
|
||||||
this.properties.set(key, property);
|
properties.set(key, {
|
||||||
this.style[camelCase(key)] = trimmedValue;
|
value: v,
|
||||||
|
important,
|
||||||
|
});
|
||||||
|
style[camelCase(key)] = v;
|
||||||
|
}
|
||||||
|
|
||||||
return property;
|
/**
|
||||||
};
|
* Find the closest ancestor of the current element.
|
||||||
|
* @param node
|
||||||
|
* @param elemName
|
||||||
|
*
|
||||||
|
* @return {?Object}
|
||||||
|
*/
|
||||||
|
function closestElem(node, elemName) {
|
||||||
|
let elem = node;
|
||||||
|
|
||||||
|
while ((elem = elem.parent) && elem.tag !== elemName) {}
|
||||||
|
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
function initStyle(selectedEl) {
|
||||||
|
if (!selectedEl.style) {
|
||||||
|
if (!selectedEl.props.style) {
|
||||||
|
selectedEl.props.style = {};
|
||||||
|
}
|
||||||
|
selectedEl.style = CSSStyleDeclaration(selectedEl);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moves + merges styles from style elements to element styles
|
* Moves + merges styles from style elements to element styles
|
||||||
*
|
*
|
||||||
* Options
|
* Options
|
||||||
* onlyMatchedOnce (default: true)
|
|
||||||
* inline only selectors that match once
|
|
||||||
*
|
|
||||||
* useMqs (default: ['', 'screen'])
|
* useMqs (default: ['', 'screen'])
|
||||||
* what media queries to be used
|
* what media queries to be used
|
||||||
* empty string element for styles outside media queries
|
* empty string element for styles outside media queries
|
||||||
@@ -391,55 +374,42 @@ CSSStyleDeclaration.prototype.setProperty = function(name, value, important) {
|
|||||||
* @author strarsis <strarsis@gmail.com>
|
* @author strarsis <strarsis@gmail.com>
|
||||||
*/
|
*/
|
||||||
const opts = {
|
const opts = {
|
||||||
onlyMatchedOnce: true,
|
|
||||||
useMqs: ['', 'screen'],
|
useMqs: ['', 'screen'],
|
||||||
usePseudos: [''],
|
usePseudos: [''],
|
||||||
};
|
};
|
||||||
|
|
||||||
function initStyle(selectedEl) {
|
|
||||||
if (!selectedEl.style) {
|
|
||||||
if (!selectedEl.props.style) {
|
|
||||||
selectedEl.props.style = {};
|
|
||||||
}
|
|
||||||
selectedEl.style = new CSSStyleDeclaration(selectedEl);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function inlineStyles(document) {
|
export function inlineStyles(document) {
|
||||||
// collect <style/>s
|
// collect <style/>s
|
||||||
const styleEls = querySelectorAll(document, 'style');
|
const styleElements = querySelectorAll(document, 'style');
|
||||||
|
|
||||||
//no <styles/>s, nothing to do
|
//no <styles/>s, nothing to do
|
||||||
if (styleEls === null) {
|
if (styleElements.length === 0) {
|
||||||
return document;
|
return document;
|
||||||
}
|
}
|
||||||
|
|
||||||
let selectors = [];
|
const selectors = [];
|
||||||
|
|
||||||
for (let styleEl of styleEls) {
|
for (let element of styleElements) {
|
||||||
if (!styleEl.children.length /* || styleEl.closestElem('foreignObject')*/) {
|
const { children } = element;
|
||||||
|
if (!children.length || closestElem(element, 'foreignObject')) {
|
||||||
// skip empty <style/>s or <foreignObject> content.
|
// skip empty <style/>s or <foreignObject> content.
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
const cssStr = getCssStr(styleEl);
|
|
||||||
|
|
||||||
// collect <style/>s and their css ast
|
// collect <style/>s and their css ast
|
||||||
let cssAst = {};
|
|
||||||
try {
|
try {
|
||||||
cssAst = csstree.parse(cssStr, {
|
flattenToSelectors(
|
||||||
parseValue: false,
|
csstree.parse(children, {
|
||||||
parseCustomProperty: false,
|
parseValue: false,
|
||||||
});
|
parseCustomProperty: false,
|
||||||
|
}),
|
||||||
|
selectors,
|
||||||
|
);
|
||||||
} catch (parseError) {
|
} catch (parseError) {
|
||||||
console.warn(
|
console.warn(
|
||||||
'Warning: Parse error of styles of <style/> element, skipped. Error details: ' +
|
'Warning: Parse error of styles of <style/> element, skipped. Error details: ' +
|
||||||
parseError,
|
parseError,
|
||||||
);
|
);
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
selectors = selectors.concat(flattenToSelectors(cssAst));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// filter for mediaqueries to be used or without any mediaquery
|
// filter for mediaqueries to be used or without any mediaquery
|
||||||
@@ -454,72 +424,51 @@ export function inlineStyles(document) {
|
|||||||
// stable sort selectors
|
// stable sort selectors
|
||||||
const sortedSelectors = sortSelectors(selectorsPseudo).reverse();
|
const sortedSelectors = sortSelectors(selectorsPseudo).reverse();
|
||||||
|
|
||||||
let selector, selectedEl;
|
|
||||||
|
|
||||||
// match selectors
|
// match selectors
|
||||||
for (selector of sortedSelectors) {
|
for (let selector of sortedSelectors) {
|
||||||
const selectorStr = csstree.generate(selector.item.data);
|
const selectorStr = csstree.generate(selector.item.data);
|
||||||
let selectedEls = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
selectedEls = querySelectorAll(document, selectorStr);
|
const selected = querySelectorAll(document, selectorStr);
|
||||||
|
if (selected.length) {
|
||||||
|
// apply <style/> to matched elements
|
||||||
|
for (let element of selected) {
|
||||||
|
if (selector.rule === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
initStyle(element);
|
||||||
|
const { style } = element;
|
||||||
|
const { properties } = style;
|
||||||
|
|
||||||
|
// merge declarations
|
||||||
|
csstree.walk(selector.rule, {
|
||||||
|
visit: 'Declaration',
|
||||||
|
enter({ property, value, important }) {
|
||||||
|
// 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
|
||||||
|
// inline styles, external styles higher priority than inline styles, external styles used
|
||||||
|
const prop = properties.get(property.trim());
|
||||||
|
if (prop && prop.important >= important) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setProperty(style, property, csstree.generate(value), important);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
} catch (selectError) {
|
} catch (selectError) {
|
||||||
if (selectError.constructor === SyntaxError) {
|
if (selectError.constructor === SyntaxError) {
|
||||||
// console.warn('Warning: Syntax error when trying to select \n\n' + selectorStr + '\n\n, skipped. Error details: ' + selectError);
|
console.warn(
|
||||||
|
'Warning: Syntax error when trying to select \n\n' +
|
||||||
|
selectorStr +
|
||||||
|
'\n\n, skipped. Error details: ' +
|
||||||
|
selectError,
|
||||||
|
);
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
throw selectError;
|
throw selectError;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (selectedEls === null) {
|
|
||||||
// nothing selected
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
selector.selectedEls = selectedEls;
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply <style/> styles to matched elements
|
|
||||||
for (selector of sortedSelectors) {
|
|
||||||
if (!selector.selectedEls) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
opts.onlyMatchedOnce &&
|
|
||||||
selector.selectedEls !== null &&
|
|
||||||
selector.selectedEls.length > 1
|
|
||||||
) {
|
|
||||||
// skip selectors that match more than once if option onlyMatchedOnce is enabled
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
// apply <style/> to matched elements
|
|
||||||
for (selectedEl of selector.selectedEls) {
|
|
||||||
if (selector.rule === null) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
initStyle(selectedEl);
|
|
||||||
const { style } = selectedEl;
|
|
||||||
|
|
||||||
// merge declarations
|
|
||||||
csstree.walk(selector.rule, {
|
|
||||||
visit: 'Declaration',
|
|
||||||
enter(styleCsstreeDeclaration) {
|
|
||||||
// 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
|
|
||||||
// inline styles, external styles higher priority than inline styles, external styles used
|
|
||||||
const { property, value, important } = styleCsstreeDeclaration;
|
|
||||||
const styleProperty = style.getProperty(property);
|
|
||||||
if (styleProperty && styleProperty.important >= important) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const propertyValue = csstree.generate(value);
|
|
||||||
style.setProperty(property, propertyValue, important);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return document;
|
return document;
|
||||||
|
|||||||
Reference in New Issue
Block a user