260 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			260 lines
		
	
	
		
			6.4 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| '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;
 |