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:
@ -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;
|
@ -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 {
|
||||||
|
@ -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 };
|
||||||
}
|
}
|
||||||
|
2
lib/svgo/svg2js.d.ts
vendored
2
lib/svgo/svg2js.d.ts
vendored
@ -1,2 +0,0 @@
|
|||||||
declare let obj: any;
|
|
||||||
export = obj;
|
|
10
lib/types.ts
10
lib/types.ts
@ -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;
|
||||||
};
|
};
|
||||||
|
@ -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();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -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",
|
||||||
|
Reference in New Issue
Block a user