1
0
mirror of https://github.com/svg/svgo.git synced 2025-07-31 07:44:22 +03:00

Cover svg parser with tsdoc (#1584)

Moved to lib/parser.js. The code will be slightly simpler
when JSAPI will be removed in v3.
This commit is contained in:
Bogdan Chadkin
2021-09-23 21:45:22 +03:00
committed by GitHub
parent 7111c52f96
commit 6e23b9cf56
7 changed files with 139 additions and 62 deletions

View File

@ -1,10 +1,30 @@
'use strict'; '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 SAX = require('@trysound/sax');
const JSAPI = require('./jsAPI.js'); const JSAPI = require('./svgo/jsAPI.js');
const { textElems } = require('../../plugins/_collections.js'); const { textElems } = require('../plugins/_collections.js');
class SvgoParserError extends Error { 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) { constructor(message, line, column, source, file) {
super(message); super(message);
this.name = 'SvgoParserError'; this.name = 'SvgoParserError';
@ -67,103 +87,160 @@ const config = {
/** /**
* Convert SVG (XML) string to SVG-as-JS object. * 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); const sax = SAX.parser(config.strict, config);
/**
* @type {XastRoot}
*/
const root = new JSAPI({ type: 'root', children: [] }); const root = new JSAPI({ type: 'root', children: [] });
/**
* @type {XastParent}
*/
let current = root; let current = root;
let stack = [root]; /**
* @type {Array<XastParent>}
*/
const stack = [root];
function pushToContent(node) { /**
* @type {<T extends XastNode>(node: T) => T}
*/
const pushToContent = (node) => {
const wrapped = new JSAPI(node, current); const wrapped = new JSAPI(node, current);
current.children.push(wrapped); current.children.push(wrapped);
return wrapped; return wrapped;
} };
sax.ondoctype = function (doctype) { /**
pushToContent({ * @type {(doctype: string) => void}
*/
sax.ondoctype = (doctype) => {
/**
* @type {XastDoctype}
*/
const node = {
type: 'doctype', type: 'doctype',
// TODO parse doctype for name, public and system to match xast // TODO parse doctype for name, public and system to match xast
name: 'svg', name: 'svg',
data: { data: {
doctype, doctype,
}, },
}); };
pushToContent(node);
const subsetStart = doctype.indexOf('['); const subsetStart = doctype.indexOf('[');
let entityMatch;
if (subsetStart >= 0) { if (subsetStart >= 0) {
entityDeclaration.lastIndex = subsetStart; entityDeclaration.lastIndex = subsetStart;
let entityMatch = entityDeclaration.exec(data);
while ((entityMatch = entityDeclaration.exec(data)) != null) { while (entityMatch != null) {
sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3]; 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', type: 'instruction',
name: data.name, name: data.name,
value: data.body, value: data.body,
}); };
pushToContent(node);
}; };
sax.oncomment = function (comment) { /**
pushToContent({ * @type {(comment: string) => void}
*/
sax.oncomment = (comment) => {
/**
* @type {XastComment}
*/
const node = {
type: 'comment', type: 'comment',
value: comment.trim(), value: comment.trim(),
}); };
pushToContent(node);
}; };
sax.oncdata = function (cdata) { /**
pushToContent({ * @type {(cdata: string) => void}
*/
sax.oncdata = (cdata) => {
/**
* @type {XastCdata}
*/
const node = {
type: 'cdata', type: 'cdata',
value: cdata, value: cdata,
}); };
pushToContent(node);
}; };
sax.onopentag = function (data) { /**
var element = { * @type {(data: { name: string, attributes: Record<string, { value: string }>}) => void}
*/
sax.onopentag = (data) => {
/**
* @type {XastElement}
*/
let element = {
type: 'element', type: 'element',
name: data.name, name: data.name,
attributes: {}, attributes: {},
children: [], children: [],
}; };
for (const [name, attr] of Object.entries(data.attributes)) { for (const [name, attr] of Object.entries(data.attributes)) {
element.attributes[name] = attr.value; element.attributes[name] = attr.value;
} }
element = pushToContent(element); element = pushToContent(element);
current = element; current = element;
stack.push(element); stack.push(element);
}; };
sax.ontext = function (text) { /**
// prevent trimming of meaningful whitespace inside textual tags * @type {(text: string) => void}
if (textElems.includes(current.name) && !data.prefix) { */
pushToContent({ sax.ontext = (text) => {
type: 'text', if (current.type === 'element') {
value: text, // prevent trimming of meaningful whitespace inside textual tags
}); if (textElems.includes(current.name)) {
} else if (/\S/.test(text)) { /**
pushToContent({ * @type {XastText}
type: 'text', */
value: text.trim(), 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(); stack.pop();
current = stack[stack.length - 1]; current = stack[stack.length - 1];
}; };
sax.onerror = function (e) { /**
* @type {(e: any) => void}
*/
sax.onerror = (e) => {
const error = new SvgoParserError( const error = new SvgoParserError(
e.reason, e.reason,
e.line + 1, e.line + 1,
@ -179,3 +256,4 @@ module.exports = function (data, from) {
sax.write(data).close(); sax.write(data).close();
return root; return root;
}; };
exports.parseSvg = parseSvg;

View File

@ -7,7 +7,7 @@
const { collectStylesheet, computeStyle } = require('./style.js'); const { collectStylesheet, computeStyle } = require('./style.js');
const { visit } = require('./xast.js'); const { visit } = require('./xast.js');
const svg2js = require('./svgo/svg2js.js'); const { parseSvg } = require('./parser.js');
/** /**
* @type {(node: XastParent, id: string) => XastElement} * @type {(node: XastParent, id: string) => XastElement}
@ -33,7 +33,7 @@ const getElementById = (node, id) => {
}; };
it('collects styles', () => { it('collects styles', () => {
const root = svg2js(` const root = parseSvg(`
<svg> <svg>
<rect id="class" class="a" /> <rect id="class" class="a" />
<rect id="two-classes" class="b a" /> <rect id="two-classes" class="b a" />
@ -88,7 +88,7 @@ it('collects styles', () => {
}); });
it('prioritizes different kinds of styles', () => { it('prioritizes different kinds of styles', () => {
const root = svg2js(` const root = parseSvg(`
<svg> <svg>
<style> <style>
g > .a { fill: red; } g > .a { fill: red; }
@ -130,7 +130,7 @@ it('prioritizes different kinds of styles', () => {
}); });
it('prioritizes important styles', () => { it('prioritizes important styles', () => {
const root = svg2js(` const root = parseSvg(`
<svg> <svg>
<style> <style>
g > .a { fill: red; } g > .a { fill: red; }
@ -166,7 +166,7 @@ it('prioritizes important styles', () => {
}); });
it('treats at-rules and pseudo-classes as dynamic styles', () => { it('treats at-rules and pseudo-classes as dynamic styles', () => {
const root = svg2js(` const root = parseSvg(`
<svg> <svg>
<style> <style>
@media screen { @media screen {
@ -208,7 +208,7 @@ it('treats at-rules and pseudo-classes as dynamic styles', () => {
}); });
it('considers <style> media attribute', () => { it('considers <style> media attribute', () => {
const root = svg2js(` const root = parseSvg(`
<svg> <svg>
<style media="print"> <style media="print">
@media screen { @media screen {
@ -241,7 +241,7 @@ it('considers <style> media attribute', () => {
}); });
it('ignores <style> with invalid type', () => { it('ignores <style> with invalid type', () => {
const root = svg2js(` const root = parseSvg(`
<svg> <svg>
<style type="text/css"> <style type="text/css">
.a { fill: red; } .a { fill: red; }
@ -270,7 +270,7 @@ it('ignores <style> with invalid type', () => {
}); });
it('ignores keyframes atrule', () => { it('ignores keyframes atrule', () => {
const root = svg2js(` const root = parseSvg(`
<svg> <svg>
<style> <style>
.a { .a {

View File

@ -5,7 +5,7 @@ const {
resolvePluginConfig, resolvePluginConfig,
extendDefaultPlugins, extendDefaultPlugins,
} = require('./svgo/config.js'); } = require('./svgo/config.js');
const svg2js = require('./svgo/svg2js.js'); const { parseSvg } = require('./parser.js');
const js2svg = require('./svgo/js2svg.js'); const js2svg = require('./svgo/js2svg.js');
const { invokePlugins } = require('./svgo/plugins.js'); const { invokePlugins } = require('./svgo/plugins.js');
const JSAPI = require('./svgo/jsAPI.js'); const JSAPI = require('./svgo/jsAPI.js');
@ -31,7 +31,7 @@ const optimize = (input, config) => {
info.multipassCount = i; info.multipassCount = i;
// TODO throw this error in v3 // TODO throw this error in v3
try { try {
svgjs = svg2js(input, config.path); svgjs = parseSvg(input, config.path);
} catch (error) { } catch (error) {
return { error: error.toString(), modernError: error }; return { error: error.toString(), modernError: error };
} }

View File

@ -1,2 +0,0 @@
declare let obj: any;
export = obj;

View File

@ -1,4 +1,4 @@
type XastDoctype = { export type XastDoctype = {
type: 'doctype'; type: 'doctype';
name: string; name: string;
data: { data: {
@ -6,23 +6,23 @@ type XastDoctype = {
}; };
}; };
type XastInstruction = { export type XastInstruction = {
type: 'instruction'; type: 'instruction';
name: string; name: string;
value: string; value: string;
}; };
type XastComment = { export type XastComment = {
type: 'comment'; type: 'comment';
value: string; value: string;
}; };
type XastCdata = { export type XastCdata = {
type: 'cdata'; type: 'cdata';
value: string; value: string;
}; };
type XastText = { export type XastText = {
type: 'text'; type: 'text';
value: string; value: string;
}; };

View File

@ -5,7 +5,7 @@ const PATH = require('path');
const JSAPI = require('../../lib/svgo/jsAPI'); const JSAPI = require('../../lib/svgo/jsAPI');
const CSSClassList = require('../../lib/svgo/css-class-list'); const CSSClassList = require('../../lib/svgo/css-class-list');
const CSSStyleDeclaration = require('../../lib/svgo/css-style-declaration'); const CSSStyleDeclaration = require('../../lib/svgo/css-style-declaration');
const SVG2JS = require('../../lib/svgo/svg2js'); const { parseSvg } = require('../../lib/parser.js');
describe('svg2js', function () { describe('svg2js', function () {
describe('working svg', function () { describe('working svg', function () {
@ -18,7 +18,7 @@ describe('svg2js', function () {
throw err; throw err;
} }
root = SVG2JS(data); root = parseSvg(data);
done(); done();
}); });
}); });

View File

@ -11,6 +11,7 @@
}, },
"include": [ "include": [
"plugins/**/*", "plugins/**/*",
"lib/svg-parser.js",
"lib/xast.test.js", "lib/xast.test.js",
"lib/path.test.js", "lib/path.test.js",
"lib/style.test.js", "lib/style.test.js",