1 | 'use strict';
|
2 |
|
3 |
|
4 |
|
5 |
|
6 |
|
7 |
|
8 |
|
9 |
|
10 |
|
11 |
|
12 |
|
13 |
|
14 |
|
15 |
|
16 |
|
17 | const SAX = require('@trysound/sax');
|
18 | const { textElems } = require('../plugins/_collections');
|
19 |
|
20 | class SvgoParserError extends Error {
|
21 | |
22 |
|
23 |
|
24 |
|
25 |
|
26 |
|
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 |
|
76 | const entityDeclaration = /<!ENTITY\s+(\S+)\s+(?:'([^']+)'|"([^"]+)")\s*>/g;
|
77 |
|
78 | const config = {
|
79 | strict: true,
|
80 | trim: false,
|
81 | normalize: false,
|
82 | lowercase: true,
|
83 | xmlns: true,
|
84 | position: true,
|
85 | };
|
86 |
|
87 |
|
88 |
|
89 |
|
90 |
|
91 |
|
92 | const parseSvg = (data, from) => {
|
93 | const sax = SAX.parser(config.strict, config);
|
94 | |
95 |
|
96 |
|
97 | const root = { type: 'root', children: [] };
|
98 | |
99 |
|
100 |
|
101 | let current = root;
|
102 | |
103 |
|
104 |
|
105 | const stack = [root];
|
106 |
|
107 | |
108 |
|
109 |
|
110 | const pushToContent = (node) => {
|
111 |
|
112 | Object.defineProperty(node, 'parentNode', {
|
113 | writable: true,
|
114 | value: current,
|
115 | });
|
116 | current.children.push(node);
|
117 | };
|
118 |
|
119 | |
120 |
|
121 |
|
122 | sax.ondoctype = (doctype) => {
|
123 | |
124 |
|
125 |
|
126 | const node = {
|
127 | type: 'doctype',
|
128 |
|
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 |
|
148 |
|
149 | sax.onprocessinginstruction = (data) => {
|
150 | |
151 |
|
152 |
|
153 | const node = {
|
154 | type: 'instruction',
|
155 | name: data.name,
|
156 | value: data.body,
|
157 | };
|
158 | pushToContent(node);
|
159 | };
|
160 |
|
161 | |
162 |
|
163 |
|
164 | sax.oncomment = (comment) => {
|
165 | |
166 |
|
167 |
|
168 | const node = {
|
169 | type: 'comment',
|
170 | value: comment.trim(),
|
171 | };
|
172 | pushToContent(node);
|
173 | };
|
174 |
|
175 | |
176 |
|
177 |
|
178 | sax.oncdata = (cdata) => {
|
179 | |
180 |
|
181 |
|
182 | const node = {
|
183 | type: 'cdata',
|
184 | value: cdata,
|
185 | };
|
186 | pushToContent(node);
|
187 | };
|
188 |
|
189 | |
190 |
|
191 |
|
192 | sax.onopentag = (data) => {
|
193 | |
194 |
|
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 |
|
212 |
|
213 | sax.ontext = (text) => {
|
214 | if (current.type === 'element') {
|
215 |
|
216 | if (textElems.has(current.name)) {
|
217 | |
218 |
|
219 |
|
220 | const node = {
|
221 | type: 'text',
|
222 | value: text,
|
223 | };
|
224 | pushToContent(node);
|
225 | } else if (/\S/.test(text)) {
|
226 | |
227 |
|
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 |
|
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 | };
|
262 | exports.parseSvg = parseSvg;
|