UNPKG

6.57 kBJavaScriptView Raw
1'use strict';
2
3/**
4 * @typedef {import('./types').XastNode} XastNode
5 * @typedef {import('./types').XastInstruction} XastInstruction
6 * @typedef {import('./types').XastDoctype} XastDoctype
7 * @typedef {import('./types').XastComment} XastComment
8 * @typedef {import('./types').XastRoot} XastRoot
9 * @typedef {import('./types').XastElement} XastElement
10 * @typedef {import('./types').XastCdata} XastCdata
11 * @typedef {import('./types').XastText} XastText
12 * @typedef {import('./types').XastParent} XastParent
13 * @typedef {import('./types').XastChild} XastChild
14 */
15
16// @ts-ignore sax will be replaced with something else later
17const SAX = require('@trysound/sax');
18const { textElems } = require('../plugins/_collections');
19
20class SvgoParserError extends Error {
21 /**
22 * @param message {string}
23 * @param line {number}
24 * @param column {number}
25 * @param source {string}
26 * @param file {void | string}
27 */
28 constructor(message, line, column, source, file) {
29 super(message);
30 this.name = 'SvgoParserError';
31 this.message = `${file || '<input>'}:${line}:${column}: ${message}`;
32 this.reason = message;
33 this.line = line;
34 this.column = column;
35 this.source = source;
36 if (Error.captureStackTrace) {
37 Error.captureStackTrace(this, SvgoParserError);
38 }
39 }
40 toString() {
41 const lines = this.source.split(/\r?\n/);
42 const startLine = Math.max(this.line - 3, 0);
43 const endLine = Math.min(this.line + 2, lines.length);
44 const lineNumberWidth = String(endLine).length;
45 const startColumn = Math.max(this.column - 54, 0);
46 const endColumn = Math.max(this.column + 20, 80);
47 const code = lines
48 .slice(startLine, endLine)
49 .map((line, index) => {
50 const lineSlice = line.slice(startColumn, endColumn);
51 let ellipsisPrefix = '';
52 let ellipsisSuffix = '';
53 if (startColumn !== 0) {
54 ellipsisPrefix = startColumn > line.length - 1 ? ' ' : '…';
55 }
56 if (endColumn < line.length - 1) {
57 ellipsisSuffix = '…';
58 }
59 const number = startLine + 1 + index;
60 const gutter = ` ${number.toString().padStart(lineNumberWidth)} | `;
61 if (number === this.line) {
62 const gutterSpacing = gutter.replace(/[^|]/g, ' ');
63 const lineSpacing = (
64 ellipsisPrefix + line.slice(startColumn, this.column - 1)
65 ).replace(/[^\t]/g, ' ');
66 const spacing = gutterSpacing + lineSpacing;
67 return `>${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}\n ${spacing}^`;
68 }
69 return ` ${gutter}${ellipsisPrefix}${lineSlice}${ellipsisSuffix}`;
70 })
71 .join('\n');
72 return `${this.name}: ${this.message}\n\n${code}\n`;
73 }
74}
75
76const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^']+)'|"([^"]+)")\s*>/g;
77
78const config = {
79 strict: true,
80 trim: false,
81 normalize: false,
82 lowercase: true,
83 xmlns: true,
84 position: true,
85};
86
87/**
88 * Convert SVG (XML) string to SVG-as-JS object.
89 *
90 * @type {(data: string, from?: string) => XastRoot}
91 */
92const parseSvg = (data, from) => {
93 const sax = SAX.parser(config.strict, config);
94 /**
95 * @type {XastRoot}
96 */
97 const root = { type: 'root', children: [] };
98 /**
99 * @type {XastParent}
100 */
101 let current = root;
102 /**
103 * @type {XastParent[]}
104 */
105 const stack = [root];
106
107 /**
108 * @type {(node: XastChild) => void}
109 */
110 const pushToContent = (node) => {
111 // TODO remove legacy parentNode in v4
112 Object.defineProperty(node, 'parentNode', {
113 writable: true,
114 value: current,
115 });
116 current.children.push(node);
117 };
118
119 /**
120 * @type {(doctype: string) => void}
121 */
122 sax.ondoctype = (doctype) => {
123 /**
124 * @type {XastDoctype}
125 */
126 const node = {
127 type: 'doctype',
128 // TODO parse doctype for name, public and system to match xast
129 name: 'svg',
130 data: {
131 doctype,
132 },
133 };
134 pushToContent(node);
135 const subsetStart = doctype.indexOf('[');
136 if (subsetStart >= 0) {
137 entityDeclaration.lastIndex = subsetStart;
138 let entityMatch = entityDeclaration.exec(data);
139 while (entityMatch != null) {
140 sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3];
141 entityMatch = entityDeclaration.exec(data);
142 }
143 }
144 };
145
146 /**
147 * @type {(data: { name: string, body: string }) => void}
148 */
149 sax.onprocessinginstruction = (data) => {
150 /**
151 * @type {XastInstruction}
152 */
153 const node = {
154 type: 'instruction',
155 name: data.name,
156 value: data.body,
157 };
158 pushToContent(node);
159 };
160
161 /**
162 * @type {(comment: string) => void}
163 */
164 sax.oncomment = (comment) => {
165 /**
166 * @type {XastComment}
167 */
168 const node = {
169 type: 'comment',
170 value: comment.trim(),
171 };
172 pushToContent(node);
173 };
174
175 /**
176 * @type {(cdata: string) => void}
177 */
178 sax.oncdata = (cdata) => {
179 /**
180 * @type {XastCdata}
181 */
182 const node = {
183 type: 'cdata',
184 value: cdata,
185 };
186 pushToContent(node);
187 };
188
189 /**
190 * @type {(data: { name: string, attributes: Record<string, { value: string }>}) => void}
191 */
192 sax.onopentag = (data) => {
193 /**
194 * @type {XastElement}
195 */
196 let element = {
197 type: 'element',
198 name: data.name,
199 attributes: {},
200 children: [],
201 };
202 for (const [name, attr] of Object.entries(data.attributes)) {
203 element.attributes[name] = attr.value;
204 }
205 pushToContent(element);
206 current = element;
207 stack.push(element);
208 };
209
210 /**
211 * @type {(text: string) => void}
212 */
213 sax.ontext = (text) => {
214 if (current.type === 'element') {
215 // prevent trimming of meaningful whitespace inside textual tags
216 if (textElems.has(current.name)) {
217 /**
218 * @type {XastText}
219 */
220 const node = {
221 type: 'text',
222 value: text,
223 };
224 pushToContent(node);
225 } else if (/\S/.test(text)) {
226 /**
227 * @type {XastText}
228 */
229 const node = {
230 type: 'text',
231 value: text.trim(),
232 };
233 pushToContent(node);
234 }
235 }
236 };
237
238 sax.onclosetag = () => {
239 stack.pop();
240 current = stack[stack.length - 1];
241 };
242
243 /**
244 * @type {(e: any) => void}
245 */
246 sax.onerror = (e) => {
247 const error = new SvgoParserError(
248 e.reason,
249 e.line + 1,
250 e.column,
251 data,
252 from,
253 );
254 if (e.message.indexOf('Unexpected end') === -1) {
255 throw error;
256 }
257 };
258
259 sax.write(data).close();
260 return root;
261};
262exports.parseSvg = parseSvg;