Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

refactor(): Remove storage + cssRules, gradientDefs, clipPathDefs globals #9030

Merged
merged 11 commits into from
Jul 1, 2023
11 changes: 0 additions & 11 deletions src/parser/constants.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,6 @@
//@ts-nocheck
import { getSvgRegex } from './getSvgRegex';
import { LEFT, TOP } from '../constants';

export const cssRules = {};
export const gradientDefs = {};
export const clipPaths = {};

export const storage = {
cssRules,
gradientDefs,
clipPaths,
};

export const reNum = String.raw`(?:[-+]?(?:\d*\.\d+|\d+\.?)(?:[eE][-+]?\d+)?)`;

export const svgNS = 'http://www.w3.org/2000/svg';
Expand Down
15 changes: 9 additions & 6 deletions src/parser/elements_parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,11 @@ import {
multiplyTransformMatrices,
qrDecompose,
} from '../util/misc/matrix';
import { storage } from './constants';
import { removeTransformMatrixForSvgParsing } from '../util/transform_matrix_removal';
import type { FabricObject } from '../shapes/Object/FabricObject';
import { Point } from '../Point';
import { CENTER } from '../constants';
import { getGradientDefs } from './getGradientDefs';

const findTag = (el: HTMLElement) =>
classRegistry.getSVGClass(el.tagName.toLowerCase().replace('svg:', ''));
Expand All @@ -22,7 +22,8 @@ const ElementsParser = function (
options,
reviver,
parsingOptions,
doc
doc,
clipPaths
) {
this.elements = elements;
this.options = options;
Expand All @@ -31,6 +32,8 @@ const ElementsParser = function (
this.parsingOptions = parsingOptions;
this.regexUrl = /^url\(['"]?#([^'"]+)['"]?\)/g;
this.doc = doc;
this.clipPaths = clipPaths;
this.gradientDefs = getGradientDefs(doc);
};

(function (proto) {
Expand Down Expand Up @@ -61,7 +64,7 @@ const ElementsParser = function (
return null;
};

proto.extractPropertyDefinition = function (obj, property, storageType) {
proto.extractPropertyDefinition = function (obj, property, storage) {
const value = obj[property],
regex = this.regexUrl;
if (!regex.test(value)) {
Expand All @@ -71,14 +74,14 @@ const ElementsParser = function (
const id = regex.exec(value)[1];
regex.lastIndex = 0;
// @todo fix this
return storage[storageType][this.svgUid][id];
return storage[id];
};

proto.resolveGradient = function (obj, el, property) {
const gradientDef = this.extractPropertyDefinition(
obj,
property,
'gradientDefs'
this.gradientDefs
);
if (gradientDef) {
const opacityAttr = el.getAttribute(property + '-opacity');
Expand All @@ -94,7 +97,7 @@ const ElementsParser = function (
const clipPathElements = this.extractPropertyDefinition(
obj,
'clipPath',
'clipPaths'
this.clipPaths
);
if (clipPathElements) {
const objTransformInv = invertTransform(obj.calcTransformMatrix());
Expand Down
70 changes: 36 additions & 34 deletions src/parser/getCSSRules.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,15 @@
import type { CSSRules } from './typedefs';

/**
* Returns CSS rules for a given SVG document
* @param {SVGDocument} doc SVG document to parse
* @param {HTMLElement} doc SVG document to parse
* @return {Object} CSS rules of this document
*/
export function getCSSRules(doc: Document) {
export function getCSSRules(doc: HTMLElement) {
const styles = doc.getElementsByTagName('style');
let i;
let len;
const allRules: Record<string, Record<string, string>> = {};
let rules;
const allRules: CSSRules = {};

// very crude parsing of style contents
for (i = 0, len = styles.length; i < len; i++) {
Expand All @@ -23,39 +24,40 @@ export function getCSSRules(doc: Document) {
}
// recovers all the rule in this form `body { style code... }`
// rules = styleContents.match(/[^{]*\{[\s\S]*?\}/g);
rules = styleContents.split('}');
// remove empty rules.
rules = rules.filter(function (rule) {
return rule.trim();
});
// at this point we have hopefully an array of rules `body { style code... `
// eslint-disable-next-line no-loop-func
rules.forEach(function (rule) {
const match = rule.split('{'),
ruleObj: Record<string, string> = {},
declaration = match[1].trim(),
propertyValuePairs = declaration.split(';').filter(function (pair) {
return pair.trim();
});
styleContents
.split('}')
// remove empty rules.
.filter(function (rule) {
return rule.trim();
})
// at this point we have hopefully an array of rules `body { style code... `
// eslint-disable-next-line no-loop-func
.forEach(function (rule) {
const match = rule.split('{'),
ruleObj: Record<string, string> = {},
declaration = match[1].trim(),
propertyValuePairs = declaration.split(';').filter(function (pair) {
return pair.trim();
});

for (i = 0, len = propertyValuePairs.length; i < len; i++) {
const pair = propertyValuePairs[i].split(':'),
property = pair[0].trim(),
value = pair[1].trim();
ruleObj[property] = value;
}
rule = match[0].trim();
rule.split(',').forEach((_rule) => {
_rule = _rule.replace(/^svg/i, '').trim();
if (_rule === '') {
return;
for (i = 0, len = propertyValuePairs.length; i < len; i++) {
const pair = propertyValuePairs[i].split(':'),
property = pair[0].trim(),
value = pair[1].trim();
ruleObj[property] = value;
}
allRules[_rule] = {
...(allRules[_rule] || {}),
...ruleObj,
};
rule = match[0].trim();
rule.split(',').forEach((_rule) => {
_rule = _rule.replace(/^svg/i, '').trim();
if (_rule === '') {
return;
}
allRules[_rule] = {
...(allRules[_rule] || {}),
...ruleObj,
};
});
});
});
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

looks bad but i really just removed the var rules and chained 3 array methods of which we didn't need the return value

}
return allRules;
}
19 changes: 11 additions & 8 deletions src/parser/getGlobalStylesForElement.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
//@ts-nocheck
import { cssRules } from './constants';
import { elementMatchesRule } from './elementMatchesRule';
import type { CSSRules } from './typedefs';

/**
* @private
*/

export function getGlobalStylesForElement(element, svgUid) {
const styles = {};
for (const rule in cssRules[svgUid]) {
export function getGlobalStylesForElement(
element: HTMLElement,
cssRules: CSSRules = {}
) {
let styles: Record<string, string> = {};
for (const rule in cssRules) {
if (elementMatchesRule(element, rule.split(' '))) {
for (const property in cssRules[svgUid][rule]) {
styles[property] = cssRules[svgUid][rule][property];
}
styles = {
...styles,
...cssRules[rule],
};
}
}
return styles;
Expand Down
65 changes: 32 additions & 33 deletions src/parser/parseAttributes.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
//@ts-nocheck
import { DEFAULT_SVG_FONT_SIZE } from '../constants';
import { parseUnit } from '../util/misc/svgParsing';
import { cPath, fSize, svgValidParentsRegEx } from './constants';
Expand All @@ -8,6 +7,7 @@ import { normalizeValue } from './normalizeValue';
import { parseFontDeclaration } from './parseFontDeclaration';
import { parseStyleAttribute } from './parseStyleAttribute';
import { setStrokeFillOpacity } from './setStrokeFillOpacity';
import type { CSSRules } from './typedefs';

/**
* Returns an object of attributes' name/value, given element and an array of attribute names;
Expand All @@ -17,59 +17,58 @@ import { setStrokeFillOpacity } from './setStrokeFillOpacity';
* @return {Object} object containing parsed attributes' names/values
*/
export function parseAttributes(
element: SVGElement | HTMLElement,
element: HTMLElement | null,
attributes: string[],
svgUid?: string
cssRules?: CSSRules
): Record<string, any> {
if (!element) {
return {};
}

let value,
parentAttributes = {},
parentAttributes: Record<string, string> = {},
fontSize,
parentFontSize;
parentFontSize = DEFAULT_SVG_FONT_SIZE;

if (typeof svgUid === 'undefined') {
svgUid = element.getAttribute('svgUid');
}
// if there's a parent container (`g` or `a` or `symbol` node), parse its attributes recursively upwards
if (
element.parentNode &&
svgValidParentsRegEx.test(element.parentNode.nodeName)
) {
parentAttributes = parseAttributes(element.parentNode, attributes, svgUid);
parentAttributes = parseAttributes(
element.parentElement,
attributes,
cssRules
);
if (parentAttributes.fontSize) {
fontSize = parentFontSize = parseUnit(parentAttributes.fontSize);
}
}

let ownAttributes = attributes.reduce(function (memo, attr) {
value = element.getAttribute(attr);
if (value) {
// eslint-disable-line
memo[attr] = value;
}
return memo;
}, {});
// add values parsed from style, which take precedence over attributes
// (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes)
const cssAttrs = Object.assign(
getGlobalStylesForElement(element, svgUid),
parseStyleAttribute(element)
);
ownAttributes = Object.assign(ownAttributes, cssAttrs);
if (cssAttrs[cPath]) {
element.setAttribute(cPath, cssAttrs[cPath]);
const ownAttributes: Record<string, string> = {
...attributes.reduce<Record<string, string>>((memo, attr) => {
value = element.getAttribute(attr);
if (value) {
memo[attr] = value;
}
return memo;
}, {}),
// add values parsed from style, which take precedence over attributes
// (see: http://www.w3.org/TR/SVG/styling.html#UsingPresentationAttributes)
...getGlobalStylesForElement(element, cssRules),
...parseStyleAttribute(element),
};

if (ownAttributes[cPath]) {
element.setAttribute(cPath, ownAttributes[cPath]);
}
fontSize = parentFontSize =
parentAttributes.fontSize || DEFAULT_SVG_FONT_SIZE;
if (ownAttributes[fSize]) {
// looks like the minimum should be 9px when dealing with ems. this is what looks like in browsers.
ownAttributes[fSize] = fontSize = parseUnit(
ownAttributes[fSize],
parentFontSize
);
fontSize = parseUnit(ownAttributes[fSize], parentFontSize);
ownAttributes[fSize] = `${fontSize}`;
}

const normalizedStyle = {};
const normalizedStyle: Record<string, string> = {};
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i m not sure what happened here, i will rely on tests.

for (const attr in ownAttributes) {
const normalizedAttr = normalizeAttr(attr);
const normalizedValue = normalizeValue(
Expand Down
20 changes: 7 additions & 13 deletions src/parser/parseSVGDocument.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,16 @@
// @ts-nocheck
import { uid } from '../util/internals/uid';
import { applyViewboxTransform } from './applyViewboxTransform';
import {
clipPaths,
cssRules,
gradientDefs,
svgInvalidAncestorsRegEx,
svgValidTagNamesRegEx,
} from './constants';
import { getCSSRules } from './getCSSRules';
import { getGradientDefs } from './getGradientDefs';
import { hasAncestorWithNodeName } from './hasAncestorWithNodeName';
import { parseUseDirectives } from './parseUseDirectives';
import type { SVGParsingOutput, TSvgReviverCallback } from './typedefs';
import type { LoadImageOptions } from '../util/misc/objectEnlive';
import { ElementsParser } from './elements_parser';
import { getCSSRules } from './getCSSRules';

/**
* Parses an SVG document, converts it to an array of corresponding fabric.* instances and passes them to a callback
Expand Down Expand Up @@ -77,20 +73,18 @@ export async function parseSVGDocument(
allElements: descendants,
};
}
const localClipPaths = {};
const localClipPaths: Record<string, Element[]> = {};
descendants
.filter((el) => el.nodeName.replace('svg:', '') === 'clipPath')
.forEach((el) => {
const id = el.getAttribute('id');
const id = el.getAttribute('id')!;
localClipPaths[id] = Array.from(el.getElementsByTagName('*')).filter(
(el) => svgValidTagNamesRegEx.test(el.nodeName.replace('svg:', ''))
);
});

// thos are like globals we need to fix
gradientDefs[svgUid] = getGradientDefs(doc);
// those are like globals we need to fix
cssRules[svgUid] = getCSSRules(doc);
clipPaths[svgUid] = localClipPaths;

// Precedence of rules: style > class > attribute
const elementParser = new ElementsParser(
Expand All @@ -101,13 +95,13 @@ export async function parseSVGDocument(
crossOrigin,
signal,
},
doc
doc,
localClipPaths
);

const instances = await elementParser.parse();

delete gradientDefs[svgUid];
delete cssRules[svgUid];
delete clipPaths[svgUid];

return {
objects: instances,
Expand Down
2 changes: 2 additions & 0 deletions src/parser/typedefs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export type TSvgReviverCallback = (
element: Element,
fabricObject: FabricObject
) => void;

export type CSSRules = Record<string, Record<string, string>>;
Loading