parser.js 6.1 KB

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