summaryrefslogtreecommitdiff
path: root/node_modules/svgo/lib/parser.js
diff options
context:
space:
mode:
Diffstat (limited to 'node_modules/svgo/lib/parser.js')
-rw-r--r--node_modules/svgo/lib/parser.js262
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;