Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Cover svg parser with tsdoc #1584

Merged
merged 1 commit into from
Sep 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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