Skip to content

Commit

Permalink
Cover svg parser with tsdoc (svg#1584)
Browse files Browse the repository at this point in the history
Moved to lib/parser.js. The code will be slightly simpler
when JSAPI will be removed in v3.
  • Loading branch information
TrySound authored Sep 23, 2021
1 parent 7111c52 commit 6e23b9c
Show file tree
Hide file tree
Showing 7 changed files with 139 additions and 62 deletions.
164 changes: 121 additions & 43 deletions lib/svgo/svg2js.js → lib/parser.js
Original file line number Diff line number Diff line change
@@ -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';
Expand Down Expand Up @@ -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<XastParent>}
*/
const stack = [root];

function pushToContent(node) {
/**
* @type {<T extends XastNode>(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<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);
};

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,
Expand All @@ -179,3 +256,4 @@ module.exports = function (data, from) {
sax.write(data).close();
return root;
};
exports.parseSvg = parseSvg;
16 changes: 8 additions & 8 deletions lib/style.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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}
Expand All @@ -33,7 +33,7 @@ const getElementById = (node, id) => {
};

it('collects styles', () => {
const root = svg2js(`
const root = parseSvg(`
<svg>
<rect id="class" class="a" />
<rect id="two-classes" class="b a" />
Expand Down Expand Up @@ -88,7 +88,7 @@ it('collects styles', () => {
});

it('prioritizes different kinds of styles', () => {
const root = svg2js(`
const root = parseSvg(`
<svg>
<style>
g > .a { fill: red; }
Expand Down Expand Up @@ -130,7 +130,7 @@ it('prioritizes different kinds of styles', () => {
});

it('prioritizes important styles', () => {
const root = svg2js(`
const root = parseSvg(`
<svg>
<style>
g > .a { fill: red; }
Expand Down Expand Up @@ -166,7 +166,7 @@ it('prioritizes important styles', () => {
});

it('treats at-rules and pseudo-classes as dynamic styles', () => {
const root = svg2js(`
const root = parseSvg(`
<svg>
<style>
@media screen {
Expand Down Expand Up @@ -208,7 +208,7 @@ it('treats at-rules and pseudo-classes as dynamic styles', () => {
});

it('considers <style> media attribute', () => {
const root = svg2js(`
const root = parseSvg(`
<svg>
<style media="print">
@media screen {
Expand Down Expand Up @@ -241,7 +241,7 @@ it('considers <style> media attribute', () => {
});

it('ignores <style> with invalid type', () => {
const root = svg2js(`
const root = parseSvg(`
<svg>
<style type="text/css">
.a { fill: red; }
Expand Down Expand Up @@ -270,7 +270,7 @@ it('ignores <style> with invalid type', () => {
});

it('ignores keyframes atrule', () => {
const root = svg2js(`
const root = parseSvg(`
<svg>
<style>
.a {
Expand Down
4 changes: 2 additions & 2 deletions lib/svgo.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const {
resolvePluginConfig,
extendDefaultPlugins,
} = require('./svgo/config.js');
const svg2js = require('./svgo/svg2js.js');
const { parseSvg } = require('./parser.js');
const js2svg = require('./svgo/js2svg.js');
const { invokePlugins } = require('./svgo/plugins.js');
const JSAPI = require('./svgo/jsAPI.js');
Expand All @@ -31,7 +31,7 @@ const optimize = (input, config) => {
info.multipassCount = i;
// TODO throw this error in v3
try {
svgjs = svg2js(input, config.path);
svgjs = parseSvg(input, config.path);
} catch (error) {
return { error: error.toString(), modernError: error };
}
Expand Down
2 changes: 0 additions & 2 deletions lib/svgo/svg2js.d.ts

This file was deleted.

10 changes: 5 additions & 5 deletions lib/types.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,28 @@
type XastDoctype = {
export type XastDoctype = {
type: 'doctype';
name: string;
data: {
doctype: string;
};
};

type XastInstruction = {
export type XastInstruction = {
type: 'instruction';
name: string;
value: string;
};

type XastComment = {
export type XastComment = {
type: 'comment';
value: string;
};

type XastCdata = {
export type XastCdata = {
type: 'cdata';
value: string;
};

type XastText = {
export type XastText = {
type: 'text';
value: string;
};
Expand Down
4 changes: 2 additions & 2 deletions test/svg2js/_index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ const PATH = require('path');
const JSAPI = require('../../lib/svgo/jsAPI');
const CSSClassList = require('../../lib/svgo/css-class-list');
const CSSStyleDeclaration = require('../../lib/svgo/css-style-declaration');
const SVG2JS = require('../../lib/svgo/svg2js');
const { parseSvg } = require('../../lib/parser.js');

describe('svg2js', function () {
describe('working svg', function () {
Expand All @@ -18,7 +18,7 @@ describe('svg2js', function () {
throw err;
}

root = SVG2JS(data);
root = parseSvg(data);
done();
});
});
Expand Down
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
},
"include": [
"plugins/**/*",
"lib/svg-parser.js",
"lib/xast.test.js",
"lib/path.test.js",
"lib/style.test.js",
Expand Down

0 comments on commit 6e23b9c

Please sign in to comment.