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 | */
|
---|
14 |
|
---|
15 | // @ts-ignore sax will be replaced with something else later
|
---|
16 | const SAX = require('@trysound/sax');
|
---|
17 | const JSAPI = require('./svgo/jsAPI.js');
|
---|
18 | const { textElems } = require('../plugins/_collections.js');
|
---|
19 |
|
---|
20 | class 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 |
|
---|
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 | * Convert SVG (XML) string to SVG-as-JS object.
|
---|
89 | *
|
---|
90 | * @type {(data: string, from?: string) => XastRoot}
|
---|
91 | */
|
---|
92 | const parseSvg = (data, from) => {
|
---|
93 | const sax = SAX.parser(config.strict, config);
|
---|
94 | /**
|
---|
95 | * @type {XastRoot}
|
---|
96 | */
|
---|
97 | const root = new JSAPI({ type: 'root', children: [] });
|
---|
98 | /**
|
---|
99 | * @type {XastParent}
|
---|
100 | */
|
---|
101 | let current = root;
|
---|
102 | /**
|
---|
103 | * @type {Array<XastParent>}
|
---|
104 | */
|
---|
105 | const stack = [root];
|
---|
106 |
|
---|
107 | /**
|
---|
108 | * @type {<T extends XastNode>(node: T) => T}
|
---|
109 | */
|
---|
110 | const pushToContent = (node) => {
|
---|
111 | const wrapped = new JSAPI(node, current);
|
---|
112 | current.children.push(wrapped);
|
---|
113 | return wrapped;
|
---|
114 | };
|
---|
115 |
|
---|
116 | /**
|
---|
117 | * @type {(doctype: string) => void}
|
---|
118 | */
|
---|
119 | sax.ondoctype = (doctype) => {
|
---|
120 | /**
|
---|
121 | * @type {XastDoctype}
|
---|
122 | */
|
---|
123 | const node = {
|
---|
124 | type: 'doctype',
|
---|
125 | // TODO parse doctype for name, public and system to match xast
|
---|
126 | name: 'svg',
|
---|
127 | data: {
|
---|
128 | doctype,
|
---|
129 | },
|
---|
130 | };
|
---|
131 | pushToContent(node);
|
---|
132 | const subsetStart = doctype.indexOf('[');
|
---|
133 | if (subsetStart >= 0) {
|
---|
134 | entityDeclaration.lastIndex = subsetStart;
|
---|
135 | let entityMatch = entityDeclaration.exec(data);
|
---|
136 | while (entityMatch != null) {
|
---|
137 | sax.ENTITIES[entityMatch[1]] = entityMatch[2] || entityMatch[3];
|
---|
138 | entityMatch = entityDeclaration.exec(data);
|
---|
139 | }
|
---|
140 | }
|
---|
141 | };
|
---|
142 |
|
---|
143 | /**
|
---|
144 | * @type {(data: { name: string, body: string }) => void}
|
---|
145 | */
|
---|
146 | sax.onprocessinginstruction = (data) => {
|
---|
147 | /**
|
---|
148 | * @type {XastInstruction}
|
---|
149 | */
|
---|
150 | const node = {
|
---|
151 | type: 'instruction',
|
---|
152 | name: data.name,
|
---|
153 | value: data.body,
|
---|
154 | };
|
---|
155 | pushToContent(node);
|
---|
156 | };
|
---|
157 |
|
---|
158 | /**
|
---|
159 | * @type {(comment: string) => void}
|
---|
160 | */
|
---|
161 | sax.oncomment = (comment) => {
|
---|
162 | /**
|
---|
163 | * @type {XastComment}
|
---|
164 | */
|
---|
165 | const node = {
|
---|
166 | type: 'comment',
|
---|
167 | value: comment.trim(),
|
---|
168 | };
|
---|
169 | pushToContent(node);
|
---|
170 | };
|
---|
171 |
|
---|
172 | /**
|
---|
173 | * @type {(cdata: string) => void}
|
---|
174 | */
|
---|
175 | sax.oncdata = (cdata) => {
|
---|
176 | /**
|
---|
177 | * @type {XastCdata}
|
---|
178 | */
|
---|
179 | const node = {
|
---|
180 | type: 'cdata',
|
---|
181 | value: cdata,
|
---|
182 | };
|
---|
183 | pushToContent(node);
|
---|
184 | };
|
---|
185 |
|
---|
186 | /**
|
---|
187 | * @type {(data: { name: string, attributes: Record<string, { value: string }>}) => void}
|
---|
188 | */
|
---|
189 | sax.onopentag = (data) => {
|
---|
190 | /**
|
---|
191 | * @type {XastElement}
|
---|
192 | */
|
---|
193 | let element = {
|
---|
194 | type: 'element',
|
---|
195 | name: data.name,
|
---|
196 | attributes: {},
|
---|
197 | children: [],
|
---|
198 | };
|
---|
199 | for (const [name, attr] of Object.entries(data.attributes)) {
|
---|
200 | element.attributes[name] = attr.value;
|
---|
201 | }
|
---|
202 | element = pushToContent(element);
|
---|
203 | current = element;
|
---|
204 | stack.push(element);
|
---|
205 | };
|
---|
206 |
|
---|
207 | /**
|
---|
208 | * @type {(text: string) => void}
|
---|
209 | */
|
---|
210 | sax.ontext = (text) => {
|
---|
211 | if (current.type === 'element') {
|
---|
212 | // prevent trimming of meaningful whitespace inside textual tags
|
---|
213 | if (textElems.includes(current.name)) {
|
---|
214 | /**
|
---|
215 | * @type {XastText}
|
---|
216 | */
|
---|
217 | const node = {
|
---|
218 | type: 'text',
|
---|
219 | value: text,
|
---|
220 | };
|
---|
221 | pushToContent(node);
|
---|
222 | } else if (/\S/.test(text)) {
|
---|
223 | /**
|
---|
224 | * @type {XastText}
|
---|
225 | */
|
---|
226 | const node = {
|
---|
227 | type: 'text',
|
---|
228 | value: text.trim(),
|
---|
229 | };
|
---|
230 | pushToContent(node);
|
---|
231 | }
|
---|
232 | }
|
---|
233 | };
|
---|
234 |
|
---|
235 | sax.onclosetag = () => {
|
---|
236 | stack.pop();
|
---|
237 | current = stack[stack.length - 1];
|
---|
238 | };
|
---|
239 |
|
---|
240 | /**
|
---|
241 | * @type {(e: any) => void}
|
---|
242 | */
|
---|
243 | sax.onerror = (e) => {
|
---|
244 | const error = new SvgoParserError(
|
---|
245 | e.reason,
|
---|
246 | e.line + 1,
|
---|
247 | e.column,
|
---|
248 | data,
|
---|
249 | from
|
---|
250 | );
|
---|
251 | if (e.message.indexOf('Unexpected end') === -1) {
|
---|
252 | throw error;
|
---|
253 | }
|
---|
254 | };
|
---|
255 |
|
---|
256 | sax.write(data).close();
|
---|
257 | return root;
|
---|
258 | };
|
---|
259 | exports.parseSvg = parseSvg;
|
---|