svgo-browser
Version:
svgo tool for browser and node usage
309 lines (276 loc) • 6.77 kB
JavaScript
const textElem = require('../../plugins/_collections.js').elemsGroups.textContent.concat('title');
const EOL = '\n';
const defaults = {
doctypeStart: '<!DOCTYPE',
doctypeEnd: '>',
procInstStart: '<?',
procInstEnd: '?>',
tagOpenStart: '<',
tagOpenEnd: '>',
tagCloseStart: '</',
tagCloseEnd: '>',
tagShortStart: '<',
tagShortEnd: '/>',
attrStart: '="',
attrEnd: '"',
commentStart: '<!--',
commentEnd: '-->',
cdataStart: '<![CDATA[',
cdataEnd: ']]>',
textStart: '',
textEnd: '',
indent: 4,
regEntities: /[&'"<>]/g,
regValEntities: /[&"<>]/g,
encodeEntity,
pretty: false,
useShortTags: true,
};
const entities = {
'&': '&',
"'": ''',
'"': '"',
'>': '>',
'<': '<',
};
/**
* Convert SVG-as-JS object to SVG (XML) string.
*
* @param {Object} data input data
* @param {Object} config config
*
* @return {Object} output data
*/
module.exports = function (data, config) {
return new JS2SVG(config).convert(data);
};
function JS2SVG(config) {
if (config) {
this.config = { ...defaults, ...config };
} else {
this.config = { ...defaults };
}
const { indent } = this.config;
if (typeof indent === 'number' && !isNaN(indent)) {
this.config.indent = indent < 0 ? '\t' : ' '.repeat(indent);
} else if (typeof indent !== 'string') {
this.config.indent = ' ';
}
if (this.config.pretty) {
this.config.doctypeEnd += EOL;
this.config.procInstEnd += EOL;
this.config.commentEnd += EOL;
this.config.cdataEnd += EOL;
this.config.tagShortEnd += EOL;
this.config.tagOpenEnd += EOL;
this.config.tagCloseEnd += EOL;
this.config.textEnd += EOL;
}
this.indentLevel = 0;
this.textContext = null;
}
function encodeEntity(char) {
return entities[char];
}
/**
* Start conversion.
*
* @param {Object} data input data
*
* @return {String}
*/
JS2SVG.prototype.convert = function (data) {
let svg = '';
if (data.content) {
this.indentLevel++;
data.content.forEach(function (item) {
if (item.elem) {
svg += this.createElem(item);
} else if (item.text) {
svg += this.createText(item.text);
} else if (item.doctype) {
svg += this.createDoctype(item.doctype);
} else if (item.processinginstruction) {
svg += this.createProcInst(item.processinginstruction);
} else if (item.comment) {
svg += this.createComment(item.comment);
} else if (item.cdata) {
svg += this.createCDATA(item.cdata);
}
}, this);
}
this.indentLevel--;
return {
data: svg,
info: {
width: this.width,
height: this.height,
},
};
};
/**
* Create indent string in accordance with the current node level.
*
* @return {String}
*/
JS2SVG.prototype.createIndent = function () {
let indent = '';
if (this.config.pretty && !this.textContext) {
indent = this.config.indent.repeat(this.indentLevel - 1);
}
return indent;
};
/**
* Create doctype tag.
*
* @param {String} doctype doctype body string
*
* @return {String}
*/
JS2SVG.prototype.createDoctype = function (doctype) {
return this.config.doctypeStart + doctype + this.config.doctypeEnd;
};
/**
* Create XML Processing Instruction tag.
*
* @param {Object} instruction instruction object
*
* @return {String}
*/
JS2SVG.prototype.createProcInst = function (instruction) {
return `${this.config.procInstStart + instruction.name} ${instruction.body}${
this.config.procInstEnd
}`;
};
/**
* Create comment tag.
*
* @param {String} comment comment body
*
* @return {String}
*/
JS2SVG.prototype.createComment = function (comment) {
return this.config.commentStart + comment + this.config.commentEnd;
};
/**
* Create CDATA section.
*
* @param {String} cdata CDATA body
*
* @return {String}
*/
JS2SVG.prototype.createCDATA = function (cdata) {
return this.createIndent() + this.config.cdataStart + cdata + this.config.cdataEnd;
};
/**
* Create element tag.
*
* @param {Object} data element object
*
* @return {String}
*/
JS2SVG.prototype.createElem = function (data) {
// beautiful injection for obtaining SVG information :)
if (data.isElem('svg') && data.hasAttr('width') && data.hasAttr('height')) {
this.width = data.attr('width').value;
this.height = data.attr('height').value;
}
// empty element and short tag
if (data.isEmpty()) {
if (this.config.useShortTags) {
return (
this.createIndent()
+ this.config.tagShortStart
+ data.elem
+ this.createAttrs(data)
+ this.config.tagShortEnd
);
}
return (
this.createIndent()
+ this.config.tagShortStart
+ data.elem
+ this.createAttrs(data)
+ this.config.tagOpenEnd
+ this.config.tagCloseStart
+ data.elem
+ this.config.tagCloseEnd
);
// non-empty element
}
let { tagOpenStart } = this.config;
let { tagOpenEnd } = this.config;
let { tagCloseStart } = this.config;
let { tagCloseEnd } = this.config;
let openIndent = this.createIndent();
let textIndent = '';
let processedData = '';
let dataEnd = '';
if (this.textContext) {
tagOpenStart = defaults.tagOpenStart;
tagOpenEnd = defaults.tagOpenEnd;
tagCloseStart = defaults.tagCloseStart;
tagCloseEnd = defaults.tagCloseEnd;
openIndent = '';
} else if (data.isElem(textElem)) {
if (this.config.pretty) {
textIndent += openIndent + this.config.indent;
}
this.textContext = data;
}
processedData += this.convert(data).data;
if (this.textContext == data) {
this.textContext = null;
if (this.config.pretty) dataEnd = EOL;
}
return (
openIndent
+ tagOpenStart
+ data.elem
+ this.createAttrs(data)
+ tagOpenEnd
+ textIndent
+ processedData
+ dataEnd
+ this.createIndent()
+ tagCloseStart
+ data.elem
+ tagCloseEnd
);
};
/**
* Create element attributes.
*
* @param {Object} elem attributes object
*
* @return {String}
*/
JS2SVG.prototype.createAttrs = function (elem) {
let attrs = '';
elem.eachAttr(function (attr) {
if (attr.value !== undefined) {
attrs += ` ${attr.name}${this.config.attrStart}${String(attr.value).replace(
this.config.regValEntities,
this.config.encodeEntity,
)}${this.config.attrEnd}`;
} else {
attrs += ` ${attr.name}`;
}
}, this);
return attrs;
};
/**
* Create text node.
*
* @param {String} text text
*
* @return {String}
*/
JS2SVG.prototype.createText = function (text) {
return (
this.createIndent()
+ this.config.textStart
+ text.replace(this.config.regEntities, this.config.encodeEntity)
+ (this.textContext ? '' : this.config.textEnd)
);
};