diff options
Diffstat (limited to 'node_modules/svgo/lib/parser.js')
| -rw-r--r-- | node_modules/svgo/lib/parser.js | 262 |
1 files changed, 262 insertions, 0 deletions
diff --git a/node_modules/svgo/lib/parser.js b/node_modules/svgo/lib/parser.js new file mode 100644 index 00000000..8c40eda0 --- /dev/null +++ b/node_modules/svgo/lib/parser.js @@ -0,0 +1,262 @@ +'use strict'; + +/** + * @typedef {import('./types').XastNode} XastNode + * @typedef {import('./types').XastInstruction} XastInstruction + * @typedef {import('./types').XastDoctype} XastDoctype + * @typedef {import('./types').XastComment} XastComment + * @typedef {import('./types').XastRoot} XastRoot + * @typedef {import('./types').XastElement} XastElement + * @typedef {import('./types').XastCdata} XastCdata + * @typedef {import('./types').XastText} XastText + * @typedef {import('./types').XastParent} XastParent + * @typedef {import('./types').XastChild} XastChild + */ + +// @ts-ignore sax will be replaced with something else later +const SAX = require('@trysound/sax'); +const { textElems } = require('../plugins/_collections.js'); + +class SvgoParserError extends Error { + /** + * @param message {string} + * @param line {number} + * @param column {number} + * @param source {string} + * @param file {void | string} + */ + constructor(message, line, column, source, file) { + super(message); + this.name = 'SvgoParserError'; + this.message = `${file || '<input>'}:${line}:${column}: ${message}`; + this.reason = message; + this.line = line; + this.column = column; + this.source = source; + if (Error.captureStackTrace) { + Error.captureStackTrace(this, SvgoParserError); + } + } + toString() { + const lines = this.source.split(/\r?\n/); + const startLine = Math.max(this.line - 3, 0); + const endLine = Math.min(this.line + 2, lines.length); + const lineNumberWidth = String(endLine).length; + const startColumn = Math.max(this.column - 54, 0); + const endColumn = Math.max(this.column + 20, 80); + const code = lines + .slice(startLine, endLine) + .map((line, index) => { + const lineSlice = line.slice(startColumn, endColumn); + let ellipsisPrefix = ''; + let ellipsisSuffix = ''; + if (startColumn !== 0) { + ellipsisPrefix = startColumn > line.length - 1 ? ' ' : '…'; + } + if (endColumn < line.length - 1) { + ellipsisSuffix = '…'; + } + const number = startLine + 1 + index; + const gutter = ` ${number.toString().padStart(lineNumberWidth)} | `; + if (number === this.line) { + const gutterSpacing = gutter.replace(/[^|]/g, ' '); + const lineSpacing = ( + ellipsisPrefix + line.slice(startColumn, this.column - 1) + ).replace(/[^\t]/g, ' '); + const spacing = gutterSpacing + lineSpacing; + return `>${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}\n ${spacing}^`; + } + return ` ${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}`; + }) + .join('\n'); + return `${this.name}: ${this.message}\n\n${code}\n`; + } +} + +const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^']+)'|"([^"]+)")\s*>/g; + +const config = { + strict: true, + trim: false, + normalize: false, + lowercase: true, + xmlns: true, + position: true, +}; + +/** + * Convert SVG (XML) string to SVG-as-JS object. + * + * @type {(data: string, from?: string) => XastRoot} + */ +const parseSvg = (data, from) => { + const sax = SAX.parser(config.strict, config); + /** + * @type {XastRoot} + */ + const root = { type: 'root', children: [] }; + /** + * @type {XastParent} + */ + let current = root; + /** + * @type {Array<XastParent>} + */ + const stack = [root]; + + /** + * @type {(node: XastChild) => void} + */ + const pushToContent = (node) => { + // TODO remove legacy parentNode in v4 + Object.defineProperty(node, 'parentNode', { + writable: true, + value: current, + }); + current.children.push(node); + }; + + /** + * @type {(doctype: string) => void} + */ + sax.ondoctype = (doctype) => { + /** + * @type {XastDoctype} + */ + const node = { + type: 'doctype', + // TODO parse doctype for name, public and system to match xast + name: 'svg', + data: { + doctype, + }, + }; + pushToContent(node); + const subsetStart = doctype.indexOf('['); + if (subsetStart >= 0) { + entityDeclaration.lastIndex = subsetStart; + let entityMatch = entityDeclaration.exec(data); + while (entityMatch != null) { + sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3]; + entityMatch = entityDeclaration.exec(data); + } + } + }; + + /** + * @type {(data: { name: string, body: string }) => void} + */ + sax.onprocessinginstruction = (data) => { + /** + * @type {XastInstruction} + */ + const node = { + type: 'instruction', + name: data.name, + value: data.body, + }; + pushToContent(node); + }; + + /** + * @type {(comment: string) => void} + */ + sax.oncomment = (comment) => { + /** + * @type {XastComment} + */ + const node = { + type: 'comment', + value: comment.trim(), + }; + pushToContent(node); + }; + + /** + * @type {(cdata: string) => void} + */ + sax.oncdata = (cdata) => { + /** + * @type {XastCdata} + */ + const node = { + type: 'cdata', + value: cdata, + }; + pushToContent(node); + }; + + /** + * @type {(data: { name: string, attributes: Record<string, { value: string }>}) => void} + */ + sax.onopentag = (data) => { + /** + * @type {XastElement} + */ + let element = { + type: 'element', + name: data.name, + attributes: {}, + children: [], + }; + for (const [name, attr] of Object.entries(data.attributes)) { + element.attributes[name] = attr.value; + } + pushToContent(element); + current = element; + stack.push(element); + }; + + /** + * @type {(text: string) => void} + */ + sax.ontext = (text) => { + if (current.type === 'element') { + // prevent trimming of meaningful whitespace inside textual tags + if (textElems.includes(current.name)) { + /** + * @type {XastText} + */ + const node = { + type: 'text', + value: text, + }; + pushToContent(node); + } else if (/\S/.test(text)) { + /** + * @type {XastText} + */ + const node = { + type: 'text', + value: text.trim(), + }; + pushToContent(node); + } + } + }; + + sax.onclosetag = () => { + stack.pop(); + current = stack[stack.length - 1]; + }; + + /** + * @type {(e: any) => void} + */ + sax.onerror = (e) => { + const error = new SvgoParserError( + e.reason, + e.line + 1, + e.column, + data, + from + ); + if (e.message.indexOf('Unexpected end') === -1) { + throw error; + } + }; + + sax.write(data).close(); + return root; +}; +exports.parseSvg = parseSvg; |
