diff --git a/src/css.js b/src/css.tsx similarity index 72% rename from src/css.js rename to src/css.tsx index 21ff2512..8d278fd1 100644 --- a/src/css.js +++ b/src/css.tsx @@ -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; + list: List; +}; +type FlatPseudoSelectorList = FlatPseudoSelector[]; +type FlatSelector = { + item: ListItem; + 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().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; +}; +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 - * @author modified by: msand + * @author modified by: msand */ -export function inlineStyles(document) { +export function inlineStyles(document: AST) { // collect