123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259 |
- '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
- */
- // @ts-ignore sax will be replaced with something else later
- const SAX = require('@trysound/sax');
- const JSAPI = require('./svgo/jsAPI.js');
- 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 = new JSAPI({ type: 'root', children: [] });
- /**
- * @type {XastParent}
- */
- let current = root;
- /**
- * @type {Array<XastParent>}
- */
- const stack = [root];
- /**
- * @type {<T extends XastNode>(node: T) => T}
- */
- const pushToContent = (node) => {
- const wrapped = new JSAPI(node, current);
- current.children.push(wrapped);
- return wrapped;
- };
- /**
- * @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;
- }
- element = 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;
|