svgo-browser
Version:
svgo tool for browser and node usage
274 lines (224 loc) • 7.9 kB
JavaScript
const csstree = require('css-tree');
const csstools = require('../css-tools');
const CSSStyleDeclaration = function (node) {
this.parentNode = node;
this.properties = new Map();
this.hasSynced = false;
this.styleAttr = null;
this.styleValue = null;
this.parseError = false;
};
/**
* Performs a deep clone of this object.
*
* @param parentNode the parentNode to assign to the cloned result
*/
CSSStyleDeclaration.prototype.clone = function (parentNode) {
const node = this;
let nodeData = {};
Object.keys(node).forEach((key) => {
if (key !== 'parentNode') {
nodeData[key] = node[key];
}
});
// Deep-clone node data.
nodeData = JSON.parse(JSON.stringify(nodeData));
const clone = new CSSStyleDeclaration(parentNode);
Object.assign(clone, nodeData);
return clone;
};
CSSStyleDeclaration.prototype.hasStyle = function () {
this.addStyleHandler();
};
// attr.style
CSSStyleDeclaration.prototype.addStyleHandler = function () {
this.styleAttr = {
// empty style attr
name: 'style',
value: null,
};
Object.defineProperty(this.parentNode.attrs, 'style', {
get: this.getStyleAttr.bind(this),
set: this.setStyleAttr.bind(this),
enumerable: true,
configurable: true,
});
this.addStyleValueHandler();
};
// attr.style.value
CSSStyleDeclaration.prototype.addStyleValueHandler = function () {
Object.defineProperty(this.styleAttr, 'value', {
get: this.getStyleValue.bind(this),
set: this.setStyleValue.bind(this),
enumerable: true,
configurable: true,
});
};
CSSStyleDeclaration.prototype.getStyleAttr = function () {
return this.styleAttr;
};
CSSStyleDeclaration.prototype.setStyleAttr = function (newStyleAttr) {
this.setStyleValue(newStyleAttr.value); // must before applying value handler!
this.styleAttr = newStyleAttr;
this.addStyleValueHandler();
this.hasSynced = false; // raw css changed
};
CSSStyleDeclaration.prototype.getStyleValue = function () {
return this.getCssText();
};
CSSStyleDeclaration.prototype.setStyleValue = function (newValue) {
this.properties.clear(); // reset all existing properties
this.styleValue = newValue;
this.hasSynced = false; // raw css changed
};
CSSStyleDeclaration.prototype._loadCssText = function () {
if (this.hasSynced) {
return;
}
this.hasSynced = true; // must be set here to prevent loop in setProperty(...)
if (!this.styleValue || this.styleValue.length === 0) {
return;
}
const inlineCssStr = this.styleValue;
let declarations = {};
try {
declarations = csstree.parse(inlineCssStr, {
context: 'declarationList',
parseValue: false,
});
} catch (parseError) {
this.parseError = parseError;
return;
}
this.parseError = false;
const self = this;
declarations.children.each((declaration) => {
try {
const styleDeclaration = csstools.csstreeToStyleDeclaration(declaration);
self.setProperty(styleDeclaration.name, styleDeclaration.value, styleDeclaration.priority);
} catch (styleError) {
if (styleError.message !== 'Unknown node type: undefined') {
self.parseError = styleError;
}
}
});
};
// only reads from properties
/**
* Get the textual representation of the declaration block (equivalent to .cssText attribute).
*
* @return {String} Textual representation of the declaration block (empty string for no properties)
*/
CSSStyleDeclaration.prototype.getCssText = function () {
const properties = this.getProperties();
if (this.parseError) {
// in case of a parse error, pass through original styles
return this.styleValue;
}
const cssText = [];
properties.forEach((property, propertyName) => {
const strImportant = property.priority === 'important' ? '!important' : '';
cssText.push(`${propertyName.trim()}:${property.value.trim()}${strImportant}`);
});
return cssText.join(';');
};
CSSStyleDeclaration.prototype._handleParseError = function () {
if (this.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: ${this.parseError}`,
);
}
};
CSSStyleDeclaration.prototype._getProperty = function (propertyName) {
if (typeof propertyName === 'undefined') {
throw Error('1 argument required, but only 0 present.');
}
const properties = this.getProperties();
this._handleParseError();
const property = properties.get(propertyName.trim());
return property;
};
/**
* Return the optional priority, "important".
*
* @param {String} propertyName representing the property name to be checked.
* @return {String} priority that represents the priority (e.g. "important") if one exists. If none exists, returns the empty string.
*/
CSSStyleDeclaration.prototype.getPropertyPriority = function (propertyName) {
const property = this._getProperty(propertyName);
return property ? property.priority : '';
};
/**
* Return the property value given a property name.
*
* @param {String} propertyName representing the property name to be checked.
* @return {String} value containing the value of the property. If not set, returns the empty string.
*/
CSSStyleDeclaration.prototype.getPropertyValue = function (propertyName) {
const property = this._getProperty(propertyName);
return property ? property.value : null;
};
/**
* Return a property name.
*
* @param {Number} index of the node to be fetched. The index is zero-based.
* @return {String} propertyName that is the name of the CSS property at the specified index.
*/
CSSStyleDeclaration.prototype.item = function (index) {
if (typeof index === 'undefined') {
throw Error('1 argument required, but only 0 present.');
}
const properties = this.getProperties();
this._handleParseError();
return Array.from(properties.keys())[index];
};
/**
* Return all properties of the node.
*
* @return {Map} properties that is a Map with propertyName as key and property (propertyValue + propertyPriority) as value.
*/
CSSStyleDeclaration.prototype.getProperties = function () {
this._loadCssText();
return this.properties;
};
// writes to properties
/**
* Remove a property from the CSS declaration block.
*
* @param {String} propertyName representing the property name to be removed.
* @return {String} oldValue equal to the value of the CSS property before it was removed.
*/
CSSStyleDeclaration.prototype.removeProperty = function (propertyName) {
if (typeof propertyName === 'undefined') {
throw Error('1 argument required, but only 0 present.');
}
this.hasStyle();
const properties = this.getProperties();
this._handleParseError();
const oldValue = this.getPropertyValue(propertyName);
properties.delete(propertyName.trim());
return oldValue;
};
/**
* Modify an existing CSS property or creates a new CSS property in the declaration block.
*
* @param {String} propertyName 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} [priority] allowing the "important" CSS priority to be set. If not specified, treated as the empty string.
* @return {undefined}
*/
CSSStyleDeclaration.prototype.setProperty = function (propertyName, value, priority) {
if (typeof propertyName === 'undefined') {
throw Error('propertyName argument required, but only not present.');
}
this.hasStyle();
const properties = this.getProperties();
this._handleParseError();
const property = {
value: value.trim(),
priority: priority.trim(),
};
properties.set(propertyName.trim(), property);
return property;
};
module.exports = CSSStyleDeclaration;