parser.js 6.4 KB

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