svgo-v2 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (80) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +294 -0
  3. package/bin/svgo +10 -0
  4. package/dist/svgo.browser.js +1 -0
  5. package/lib/css-tools.js +239 -0
  6. package/lib/parser.js +259 -0
  7. package/lib/path.js +347 -0
  8. package/lib/stringifier.js +326 -0
  9. package/lib/style.js +283 -0
  10. package/lib/svgo/coa.js +517 -0
  11. package/lib/svgo/config.js +138 -0
  12. package/lib/svgo/css-class-list.js +72 -0
  13. package/lib/svgo/css-select-adapter.d.ts +2 -0
  14. package/lib/svgo/css-select-adapter.js +120 -0
  15. package/lib/svgo/css-style-declaration.js +232 -0
  16. package/lib/svgo/jsAPI.d.ts +2 -0
  17. package/lib/svgo/jsAPI.js +443 -0
  18. package/lib/svgo/plugins.js +109 -0
  19. package/lib/svgo/tools.js +137 -0
  20. package/lib/svgo-node.js +106 -0
  21. package/lib/svgo.js +83 -0
  22. package/lib/types.ts +172 -0
  23. package/lib/xast.js +102 -0
  24. package/package.json +130 -0
  25. package/plugins/_applyTransforms.js +335 -0
  26. package/plugins/_collections.js +2168 -0
  27. package/plugins/_path.js +816 -0
  28. package/plugins/_transforms.js +379 -0
  29. package/plugins/addAttributesToSVGElement.js +87 -0
  30. package/plugins/addClassesToSVGElement.js +87 -0
  31. package/plugins/cleanupAttrs.js +55 -0
  32. package/plugins/cleanupEnableBackground.js +75 -0
  33. package/plugins/cleanupIDs.js +297 -0
  34. package/plugins/cleanupListOfValues.js +154 -0
  35. package/plugins/cleanupNumericValues.js +113 -0
  36. package/plugins/collapseGroups.js +135 -0
  37. package/plugins/convertColors.js +152 -0
  38. package/plugins/convertEllipseToCircle.js +39 -0
  39. package/plugins/convertPathData.js +1023 -0
  40. package/plugins/convertShapeToPath.js +175 -0
  41. package/plugins/convertStyleToAttrs.js +132 -0
  42. package/plugins/convertTransform.js +432 -0
  43. package/plugins/inlineStyles.js +379 -0
  44. package/plugins/mergePaths.js +104 -0
  45. package/plugins/mergeStyles.js +93 -0
  46. package/plugins/minifyStyles.js +148 -0
  47. package/plugins/moveElemsAttrsToGroup.js +130 -0
  48. package/plugins/moveGroupAttrsToElems.js +62 -0
  49. package/plugins/plugins.js +56 -0
  50. package/plugins/prefixIds.js +241 -0
  51. package/plugins/preset-default.js +80 -0
  52. package/plugins/removeAttributesBySelector.js +99 -0
  53. package/plugins/removeAttrs.js +159 -0
  54. package/plugins/removeComments.js +31 -0
  55. package/plugins/removeDesc.js +41 -0
  56. package/plugins/removeDimensions.js +43 -0
  57. package/plugins/removeDoctype.js +42 -0
  58. package/plugins/removeEditorsNSData.js +68 -0
  59. package/plugins/removeElementsByAttr.js +78 -0
  60. package/plugins/removeEmptyAttrs.js +33 -0
  61. package/plugins/removeEmptyContainers.js +58 -0
  62. package/plugins/removeEmptyText.js +57 -0
  63. package/plugins/removeHiddenElems.js +318 -0
  64. package/plugins/removeMetadata.js +29 -0
  65. package/plugins/removeNonInheritableGroupAttrs.js +38 -0
  66. package/plugins/removeOffCanvasPaths.js +138 -0
  67. package/plugins/removeRasterImages.js +33 -0
  68. package/plugins/removeScriptElement.js +29 -0
  69. package/plugins/removeStyleElement.js +29 -0
  70. package/plugins/removeTitle.js +29 -0
  71. package/plugins/removeUnknownsAndDefaults.js +218 -0
  72. package/plugins/removeUnusedNS.js +61 -0
  73. package/plugins/removeUselessDefs.js +65 -0
  74. package/plugins/removeUselessStrokeAndFill.js +144 -0
  75. package/plugins/removeViewBox.js +51 -0
  76. package/plugins/removeXMLNS.js +30 -0
  77. package/plugins/removeXMLProcInst.js +30 -0
  78. package/plugins/reusePaths.js +113 -0
  79. package/plugins/sortAttrs.js +113 -0
  80. package/plugins/sortDefsChildren.js +60 -0
package/lib/path.js ADDED
@@ -0,0 +1,347 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('./types').PathDataItem} PathDataItem
5
+ * @typedef {import('./types').PathDataCommand} PathDataCommand
6
+ */
7
+
8
+ // Based on https://www.w3.org/TR/SVG11/paths.html#PathDataBNF
9
+
10
+ const argsCountPerCommand = {
11
+ M: 2,
12
+ m: 2,
13
+ Z: 0,
14
+ z: 0,
15
+ L: 2,
16
+ l: 2,
17
+ H: 1,
18
+ h: 1,
19
+ V: 1,
20
+ v: 1,
21
+ C: 6,
22
+ c: 6,
23
+ S: 4,
24
+ s: 4,
25
+ Q: 4,
26
+ q: 4,
27
+ T: 2,
28
+ t: 2,
29
+ A: 7,
30
+ a: 7,
31
+ };
32
+
33
+ /**
34
+ * @type {(c: string) => c is PathDataCommand}
35
+ */
36
+ const isCommand = (c) => {
37
+ return c in argsCountPerCommand;
38
+ };
39
+
40
+ /**
41
+ * @type {(c: string) => boolean}
42
+ */
43
+ const isWsp = (c) => {
44
+ const codePoint = c.codePointAt(0);
45
+ return (
46
+ codePoint === 0x20 ||
47
+ codePoint === 0x9 ||
48
+ codePoint === 0xd ||
49
+ codePoint === 0xa
50
+ );
51
+ };
52
+
53
+ /**
54
+ * @type {(c: string) => boolean}
55
+ */
56
+ const isDigit = (c) => {
57
+ const codePoint = c.codePointAt(0);
58
+ if (codePoint == null) {
59
+ return false;
60
+ }
61
+ return 48 <= codePoint && codePoint <= 57;
62
+ };
63
+
64
+ /**
65
+ * @typedef {'none' | 'sign' | 'whole' | 'decimal_point' | 'decimal' | 'e' | 'exponent_sign' | 'exponent'} ReadNumberState
66
+ */
67
+
68
+ /**
69
+ * @type {(string: string, cursor: number) => [number, number | null]}
70
+ */
71
+ const readNumber = (string, cursor) => {
72
+ let i = cursor;
73
+ let value = '';
74
+ let state = /** @type {ReadNumberState} */ ('none');
75
+ for (; i < string.length; i += 1) {
76
+ const c = string[i];
77
+ if (c === '+' || c === '-') {
78
+ if (state === 'none') {
79
+ state = 'sign';
80
+ value += c;
81
+ continue;
82
+ }
83
+ if (state === 'e') {
84
+ state = 'exponent_sign';
85
+ value += c;
86
+ continue;
87
+ }
88
+ }
89
+ if (isDigit(c)) {
90
+ if (state === 'none' || state === 'sign' || state === 'whole') {
91
+ state = 'whole';
92
+ value += c;
93
+ continue;
94
+ }
95
+ if (state === 'decimal_point' || state === 'decimal') {
96
+ state = 'decimal';
97
+ value += c;
98
+ continue;
99
+ }
100
+ if (state === 'e' || state === 'exponent_sign' || state === 'exponent') {
101
+ state = 'exponent';
102
+ value += c;
103
+ continue;
104
+ }
105
+ }
106
+ if (c === '.') {
107
+ if (state === 'none' || state === 'sign' || state === 'whole') {
108
+ state = 'decimal_point';
109
+ value += c;
110
+ continue;
111
+ }
112
+ }
113
+ if (c === 'E' || c == 'e') {
114
+ if (
115
+ state === 'whole' ||
116
+ state === 'decimal_point' ||
117
+ state === 'decimal'
118
+ ) {
119
+ state = 'e';
120
+ value += c;
121
+ continue;
122
+ }
123
+ }
124
+ break;
125
+ }
126
+ const number = Number.parseFloat(value);
127
+ if (Number.isNaN(number)) {
128
+ return [cursor, null];
129
+ } else {
130
+ // step back to delegate iteration to parent loop
131
+ return [i - 1, number];
132
+ }
133
+ };
134
+
135
+ /**
136
+ * @type {(string: string) => Array<PathDataItem>}
137
+ */
138
+ const parsePathData = (string) => {
139
+ /**
140
+ * @type {Array<PathDataItem>}
141
+ */
142
+ const pathData = [];
143
+ /**
144
+ * @type {null | PathDataCommand}
145
+ */
146
+ let command = null;
147
+ let args = /** @type {number[]} */ ([]);
148
+ let argsCount = 0;
149
+ let canHaveComma = false;
150
+ let hadComma = false;
151
+ for (let i = 0; i < string.length; i += 1) {
152
+ const c = string.charAt(i);
153
+ if (isWsp(c)) {
154
+ continue;
155
+ }
156
+ // allow comma only between arguments
157
+ if (canHaveComma && c === ',') {
158
+ if (hadComma) {
159
+ break;
160
+ }
161
+ hadComma = true;
162
+ continue;
163
+ }
164
+ if (isCommand(c)) {
165
+ if (hadComma) {
166
+ return pathData;
167
+ }
168
+ if (command == null) {
169
+ // moveto should be leading command
170
+ if (c !== 'M' && c !== 'm') {
171
+ return pathData;
172
+ }
173
+ } else {
174
+ // stop if previous command arguments are not flushed
175
+ if (args.length !== 0) {
176
+ return pathData;
177
+ }
178
+ }
179
+ command = c;
180
+ args = [];
181
+ argsCount = argsCountPerCommand[command];
182
+ canHaveComma = false;
183
+ // flush command without arguments
184
+ if (argsCount === 0) {
185
+ pathData.push({ command, args });
186
+ }
187
+ continue;
188
+ }
189
+ // avoid parsing arguments if no command detected
190
+ if (command == null) {
191
+ return pathData;
192
+ }
193
+ // read next argument
194
+ let newCursor = i;
195
+ let number = null;
196
+ if (command === 'A' || command === 'a') {
197
+ const position = args.length;
198
+ if (position === 0 || position === 1) {
199
+ // allow only positive number without sign as first two arguments
200
+ if (c !== '+' && c !== '-') {
201
+ [newCursor, number] = readNumber(string, i);
202
+ }
203
+ }
204
+ if (position === 2 || position === 5 || position === 6) {
205
+ [newCursor, number] = readNumber(string, i);
206
+ }
207
+ if (position === 3 || position === 4) {
208
+ // read flags
209
+ if (c === '0') {
210
+ number = 0;
211
+ }
212
+ if (c === '1') {
213
+ number = 1;
214
+ }
215
+ }
216
+ } else {
217
+ [newCursor, number] = readNumber(string, i);
218
+ }
219
+ if (number == null) {
220
+ return pathData;
221
+ }
222
+ args.push(number);
223
+ canHaveComma = true;
224
+ hadComma = false;
225
+ i = newCursor;
226
+ // flush arguments when necessary count is reached
227
+ if (args.length === argsCount) {
228
+ pathData.push({ command, args });
229
+ // subsequent moveto coordinates are threated as implicit lineto commands
230
+ if (command === 'M') {
231
+ command = 'L';
232
+ }
233
+ if (command === 'm') {
234
+ command = 'l';
235
+ }
236
+ args = [];
237
+ }
238
+ }
239
+ return pathData;
240
+ };
241
+ exports.parsePathData = parsePathData;
242
+
243
+ /**
244
+ * @type {(number: number, precision?: number) => string}
245
+ */
246
+ const stringifyNumber = (number, precision) => {
247
+ if (precision != null) {
248
+ const ratio = 10 ** precision;
249
+ number = Math.round(number * ratio) / ratio;
250
+ }
251
+ // remove zero whole from decimal number
252
+ return number.toString().replace(/^0\./, '.').replace(/^-0\./, '-.');
253
+ };
254
+
255
+ /**
256
+ * Elliptical arc large-arc and sweep flags are rendered with spaces
257
+ * because many non-browser environments are not able to parse such paths
258
+ *
259
+ * @type {(
260
+ * command: string,
261
+ * args: number[],
262
+ * precision?: number,
263
+ * disableSpaceAfterFlags?: boolean
264
+ * ) => string}
265
+ */
266
+ const stringifyArgs = (command, args, precision, disableSpaceAfterFlags) => {
267
+ let result = '';
268
+ let prev = '';
269
+ for (let i = 0; i < args.length; i += 1) {
270
+ const number = args[i];
271
+ const numberString = stringifyNumber(number, precision);
272
+ if (
273
+ disableSpaceAfterFlags &&
274
+ (command === 'A' || command === 'a') &&
275
+ // consider combined arcs
276
+ (i % 7 === 4 || i % 7 === 5)
277
+ ) {
278
+ result += numberString;
279
+ } else if (i === 0 || numberString.startsWith('-')) {
280
+ // avoid space before first and negative numbers
281
+ result += numberString;
282
+ } else if (prev.includes('.') && numberString.startsWith('.')) {
283
+ // remove space before decimal with zero whole
284
+ // only when previous number is also decimal
285
+ result += numberString;
286
+ } else {
287
+ result += ` ${numberString}`;
288
+ }
289
+ prev = numberString;
290
+ }
291
+ return result;
292
+ };
293
+
294
+ /**
295
+ * @typedef {{
296
+ * pathData: Array<PathDataItem>;
297
+ * precision?: number;
298
+ * disableSpaceAfterFlags?: boolean;
299
+ * }} StringifyPathDataOptions
300
+ */
301
+
302
+ /**
303
+ * @type {(options: StringifyPathDataOptions) => string}
304
+ */
305
+ const stringifyPathData = ({ pathData, precision, disableSpaceAfterFlags }) => {
306
+ // combine sequence of the same commands
307
+ let combined = [];
308
+ for (let i = 0; i < pathData.length; i += 1) {
309
+ const { command, args } = pathData[i];
310
+ if (i === 0) {
311
+ combined.push({ command, args });
312
+ } else {
313
+ /**
314
+ * @type {PathDataItem}
315
+ */
316
+ const last = combined[combined.length - 1];
317
+ // match leading moveto with following lineto
318
+ if (i === 1) {
319
+ if (command === 'L') {
320
+ last.command = 'M';
321
+ }
322
+ if (command === 'l') {
323
+ last.command = 'm';
324
+ }
325
+ }
326
+ if (
327
+ (last.command === command &&
328
+ last.command !== 'M' &&
329
+ last.command !== 'm') ||
330
+ // combine matching moveto and lineto sequences
331
+ (last.command === 'M' && command === 'L') ||
332
+ (last.command === 'm' && command === 'l')
333
+ ) {
334
+ last.args = [...last.args, ...args];
335
+ } else {
336
+ combined.push({ command, args });
337
+ }
338
+ }
339
+ }
340
+ let result = '';
341
+ for (const { command, args } of combined) {
342
+ result +=
343
+ command + stringifyArgs(command, args, precision, disableSpaceAfterFlags);
344
+ }
345
+ return result;
346
+ };
347
+ exports.stringifyPathData = stringifyPathData;
@@ -0,0 +1,326 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @typedef {import('./types').XastParent} XastParent
5
+ * @typedef {import('./types').XastRoot} XastRoot
6
+ * @typedef {import('./types').XastElement} XastElement
7
+ * @typedef {import('./types').XastInstruction} XastInstruction
8
+ * @typedef {import('./types').XastDoctype} XastDoctype
9
+ * @typedef {import('./types').XastText} XastText
10
+ * @typedef {import('./types').XastCdata} XastCdata
11
+ * @typedef {import('./types').XastComment} XastComment
12
+ * @typedef {import('./types').StringifyOptions} StringifyOptions
13
+ */
14
+
15
+ const { textElems } = require('../plugins/_collections.js');
16
+
17
+ /**
18
+ * @typedef {{
19
+ * width: void | string,
20
+ * height: void | string,
21
+ * indent: string,
22
+ * textContext: null | XastElement,
23
+ * indentLevel: number,
24
+ * }} State
25
+ */
26
+
27
+ /**
28
+ * @typedef {Required<StringifyOptions>} Options
29
+ */
30
+
31
+ /**
32
+ * @type {(char: string) => string}
33
+ */
34
+ const encodeEntity = (char) => {
35
+ return entities[char];
36
+ };
37
+
38
+ /**
39
+ * @type {Options}
40
+ */
41
+ const defaults = {
42
+ doctypeStart: '<!DOCTYPE',
43
+ doctypeEnd: '>',
44
+ procInstStart: '<?',
45
+ procInstEnd: '?>',
46
+ tagOpenStart: '<',
47
+ tagOpenEnd: '>',
48
+ tagCloseStart: '</',
49
+ tagCloseEnd: '>',
50
+ tagShortStart: '<',
51
+ tagShortEnd: '/>',
52
+ attrStart: '="',
53
+ attrEnd: '"',
54
+ commentStart: '<!--',
55
+ commentEnd: '-->',
56
+ cdataStart: '<![CDATA[',
57
+ cdataEnd: ']]>',
58
+ textStart: '',
59
+ textEnd: '',
60
+ indent: 4,
61
+ regEntities: /[&'"<>]/g,
62
+ regValEntities: /[&"<>]/g,
63
+ encodeEntity: encodeEntity,
64
+ pretty: false,
65
+ useShortTags: true,
66
+ eol: 'lf',
67
+ finalNewline: false,
68
+ };
69
+
70
+ /**
71
+ * @type {Record<string, string>}
72
+ */
73
+ const entities = {
74
+ '&': '&amp;',
75
+ "'": '&apos;',
76
+ '"': '&quot;',
77
+ '>': '&gt;',
78
+ '<': '&lt;',
79
+ };
80
+
81
+ /**
82
+ * convert XAST to SVG string
83
+ *
84
+ * @type {(data: XastRoot, config: StringifyOptions) => {
85
+ * data: string,
86
+ * info: {
87
+ * width: void | string,
88
+ * height: void | string
89
+ * }
90
+ * }}
91
+ */
92
+ const stringifySvg = (data, userOptions = {}) => {
93
+ /**
94
+ * @type {Options}
95
+ */
96
+ const config = { ...defaults, ...userOptions };
97
+ const indent = config.indent;
98
+ let newIndent = ' ';
99
+ if (typeof indent === 'number' && Number.isNaN(indent) === false) {
100
+ newIndent = indent < 0 ? '\t' : ' '.repeat(indent);
101
+ } else if (typeof indent === 'string') {
102
+ newIndent = indent;
103
+ }
104
+ /**
105
+ * @type {State}
106
+ */
107
+ const state = {
108
+ // TODO remove width and height in v3
109
+ width: undefined,
110
+ height: undefined,
111
+ indent: newIndent,
112
+ textContext: null,
113
+ indentLevel: 0,
114
+ };
115
+ const eol = config.eol === 'crlf' ? '\r\n' : '\n';
116
+ if (config.pretty) {
117
+ config.doctypeEnd += eol;
118
+ config.procInstEnd += eol;
119
+ config.commentEnd += eol;
120
+ config.cdataEnd += eol;
121
+ config.tagShortEnd += eol;
122
+ config.tagOpenEnd += eol;
123
+ config.tagCloseEnd += eol;
124
+ config.textEnd += eol;
125
+ }
126
+ let svg = stringifyNode(data, config, state);
127
+ if (config.finalNewline && svg.length > 0 && svg[svg.length - 1] !== '\n') {
128
+ svg += eol;
129
+ }
130
+ return {
131
+ data: svg,
132
+ info: {
133
+ width: state.width,
134
+ height: state.height,
135
+ },
136
+ };
137
+ };
138
+ exports.stringifySvg = stringifySvg;
139
+
140
+ /**
141
+ * @type {(node: XastParent, config: Options, state: State) => string}
142
+ */
143
+ const stringifyNode = (data, config, state) => {
144
+ let svg = '';
145
+ state.indentLevel += 1;
146
+ for (const item of data.children) {
147
+ if (item.type === 'element') {
148
+ svg += stringifyElement(item, config, state);
149
+ }
150
+ if (item.type === 'text') {
151
+ svg += stringifyText(item, config, state);
152
+ }
153
+ if (item.type === 'doctype') {
154
+ svg += stringifyDoctype(item, config);
155
+ }
156
+ if (item.type === 'instruction') {
157
+ svg += stringifyInstruction(item, config);
158
+ }
159
+ if (item.type === 'comment') {
160
+ svg += stringifyComment(item, config);
161
+ }
162
+ if (item.type === 'cdata') {
163
+ svg += stringifyCdata(item, config, state);
164
+ }
165
+ }
166
+ state.indentLevel -= 1;
167
+ return svg;
168
+ };
169
+
170
+ /**
171
+ * create indent string in accordance with the current node level.
172
+ *
173
+ * @type {(config: Options, state: State) => string}
174
+ */
175
+ const createIndent = (config, state) => {
176
+ let indent = '';
177
+ if (config.pretty && state.textContext == null) {
178
+ indent = state.indent.repeat(state.indentLevel - 1);
179
+ }
180
+ return indent;
181
+ };
182
+
183
+ /**
184
+ * @type {(node: XastDoctype, config: Options) => string}
185
+ */
186
+ const stringifyDoctype = (node, config) => {
187
+ return config.doctypeStart + node.data.doctype + config.doctypeEnd;
188
+ };
189
+
190
+ /**
191
+ * @type {(node: XastInstruction, config: Options) => string}
192
+ */
193
+ const stringifyInstruction = (node, config) => {
194
+ return (
195
+ config.procInstStart + node.name + ' ' + node.value + config.procInstEnd
196
+ );
197
+ };
198
+
199
+ /**
200
+ * @type {(node: XastComment, config: Options) => string}
201
+ */
202
+ const stringifyComment = (node, config) => {
203
+ return config.commentStart + node.value + config.commentEnd;
204
+ };
205
+
206
+ /**
207
+ * @type {(node: XastCdata, config: Options, state: State) => string}
208
+ */
209
+ const stringifyCdata = (node, config, state) => {
210
+ return (
211
+ createIndent(config, state) +
212
+ config.cdataStart +
213
+ node.value +
214
+ config.cdataEnd
215
+ );
216
+ };
217
+
218
+ /**
219
+ * @type {(node: XastElement, config: Options, state: State) => string}
220
+ */
221
+ const stringifyElement = (node, config, state) => {
222
+ // beautiful injection for obtaining SVG information :)
223
+ if (
224
+ node.name === 'svg' &&
225
+ node.attributes.width != null &&
226
+ node.attributes.height != null
227
+ ) {
228
+ state.width = node.attributes.width;
229
+ state.height = node.attributes.height;
230
+ }
231
+
232
+ // empty element and short tag
233
+ if (node.children.length === 0) {
234
+ if (config.useShortTags) {
235
+ return (
236
+ createIndent(config, state) +
237
+ config.tagShortStart +
238
+ node.name +
239
+ stringifyAttributes(node, config) +
240
+ config.tagShortEnd
241
+ );
242
+ } else {
243
+ return (
244
+ createIndent(config, state) +
245
+ config.tagShortStart +
246
+ node.name +
247
+ stringifyAttributes(node, config) +
248
+ config.tagOpenEnd +
249
+ config.tagCloseStart +
250
+ node.name +
251
+ config.tagCloseEnd
252
+ );
253
+ }
254
+ // non-empty element
255
+ } else {
256
+ let tagOpenStart = config.tagOpenStart;
257
+ let tagOpenEnd = config.tagOpenEnd;
258
+ let tagCloseStart = config.tagCloseStart;
259
+ let tagCloseEnd = config.tagCloseEnd;
260
+ let openIndent = createIndent(config, state);
261
+ let closeIndent = createIndent(config, state);
262
+
263
+ if (state.textContext) {
264
+ tagOpenStart = defaults.tagOpenStart;
265
+ tagOpenEnd = defaults.tagOpenEnd;
266
+ tagCloseStart = defaults.tagCloseStart;
267
+ tagCloseEnd = defaults.tagCloseEnd;
268
+ openIndent = '';
269
+ } else if (textElems.includes(node.name)) {
270
+ tagOpenEnd = defaults.tagOpenEnd;
271
+ tagCloseStart = defaults.tagCloseStart;
272
+ closeIndent = '';
273
+ state.textContext = node;
274
+ }
275
+
276
+ const children = stringifyNode(node, config, state);
277
+
278
+ if (state.textContext === node) {
279
+ state.textContext = null;
280
+ }
281
+
282
+ return (
283
+ openIndent +
284
+ tagOpenStart +
285
+ node.name +
286
+ stringifyAttributes(node, config) +
287
+ tagOpenEnd +
288
+ children +
289
+ closeIndent +
290
+ tagCloseStart +
291
+ node.name +
292
+ tagCloseEnd
293
+ );
294
+ }
295
+ };
296
+
297
+ /**
298
+ * @type {(node: XastElement, config: Options) => string}
299
+ */
300
+ const stringifyAttributes = (node, config) => {
301
+ let attrs = '';
302
+ for (const [name, value] of Object.entries(node.attributes)) {
303
+ // TODO remove attributes without values support in v3
304
+ if (value !== undefined) {
305
+ const encodedValue = value
306
+ .toString()
307
+ .replace(config.regValEntities, config.encodeEntity);
308
+ attrs += ' ' + name + config.attrStart + encodedValue + config.attrEnd;
309
+ } else {
310
+ attrs += ' ' + name;
311
+ }
312
+ }
313
+ return attrs;
314
+ };
315
+
316
+ /**
317
+ * @type {(node: XastText, config: Options, state: State) => string}
318
+ */
319
+ const stringifyText = (node, config, state) => {
320
+ return (
321
+ createIndent(config, state) +
322
+ config.textStart +
323
+ node.value.replace(config.regEntities, config.encodeEntity) +
324
+ (state.textContext ? '' : config.textEnd)
325
+ );
326
+ };