| 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 laterconst 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;
 |