diff --git a/lib/svgo/svg2js.js b/lib/parser.js similarity index 54% rename from lib/svgo/svg2js.js rename to lib/parser.js index 3d6e8862..79357c31 100644 --- a/lib/svgo/svg2js.js +++ b/lib/parser.js @@ -1,10 +1,30 @@ '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('./jsAPI.js'); -const { textElems } = require('../../plugins/_collections.js'); +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'; @@ -67,103 +87,160 @@ const config = { /** * Convert SVG (XML) string to SVG-as-JS object. * - * @param {String} data input data + * @type {(data: string, from?: string) => XastRoot} */ -module.exports = function (data, from) { +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; - let stack = [root]; + /** + * @type {Array} + */ + const stack = [root]; - function pushToContent(node) { + /** + * @type {(node: T) => T} + */ + const pushToContent = (node) => { const wrapped = new JSAPI(node, current); current.children.push(wrapped); return wrapped; - } + }; - sax.ondoctype = function (doctype) { - pushToContent({ + /** + * @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('['); - let entityMatch; - if (subsetStart >= 0) { entityDeclaration.lastIndex = subsetStart; - - while ((entityMatch = entityDeclaration.exec(data)) != null) { + let entityMatch = entityDeclaration.exec(data); + while (entityMatch != null) { sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3]; + entityMatch = entityDeclaration.exec(data); } } }; - sax.onprocessinginstruction = function (data) { - pushToContent({ + /** + * @type {(data: { name: string, body: string }) => void} + */ + sax.onprocessinginstruction = (data) => { + /** + * @type {XastInstruction} + */ + const node = { type: 'instruction', name: data.name, value: data.body, - }); + }; + pushToContent(node); }; - sax.oncomment = function (comment) { - pushToContent({ + /** + * @type {(comment: string) => void} + */ + sax.oncomment = (comment) => { + /** + * @type {XastComment} + */ + const node = { type: 'comment', value: comment.trim(), - }); + }; + pushToContent(node); }; - sax.oncdata = function (cdata) { - pushToContent({ + /** + * @type {(cdata: string) => void} + */ + sax.oncdata = (cdata) => { + /** + * @type {XastCdata} + */ + const node = { type: 'cdata', value: cdata, - }); + }; + pushToContent(node); }; - sax.onopentag = function (data) { - var element = { + /** + * @type {(data: { name: string, attributes: Record}) => 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); }; - sax.ontext = function (text) { - // prevent trimming of meaningful whitespace inside textual tags - if (textElems.includes(current.name) && !data.prefix) { - pushToContent({ - type: 'text', - value: text, - }); - } else if (/\S/.test(text)) { - pushToContent({ - type: 'text', - value: text.trim(), - }); + /** + * @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 = function () { + sax.onclosetag = () => { stack.pop(); current = stack[stack.length - 1]; }; - sax.onerror = function (e) { + /** + * @type {(e: any) => void} + */ + sax.onerror = (e) => { const error = new SvgoParserError( e.reason, e.line + 1, @@ -179,3 +256,4 @@ module.exports = function (data, from) { sax.write(data).close(); return root; }; +exports.parseSvg = parseSvg; diff --git a/lib/style.test.js b/lib/style.test.js index fdb191b4..11669d0b 100644 --- a/lib/style.test.js +++ b/lib/style.test.js @@ -7,7 +7,7 @@ const { collectStylesheet, computeStyle } = require('./style.js'); const { visit } = require('./xast.js'); -const svg2js = require('./svgo/svg2js.js'); +const { parseSvg } = require('./parser.js'); /** * @type {(node: XastParent, id: string) => XastElement} @@ -33,7 +33,7 @@ const getElementById = (node, id) => { }; it('collects styles', () => { - const root = svg2js(` + const root = parseSvg(` @@ -88,7 +88,7 @@ it('collects styles', () => { }); it('prioritizes different kinds of styles', () => { - const root = svg2js(` + const root = parseSvg(`