You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

533 lines
11 KiB

4 years ago
  1. 'use strict';
  2. var use = require('use');
  3. var util = require('util');
  4. var Cache = require('map-cache');
  5. var define = require('define-property');
  6. var debug = require('debug')('snapdragon:parser');
  7. var Position = require('./position');
  8. var utils = require('./utils');
  9. /**
  10. * Create a new `Parser` with the given `input` and `options`.
  11. * @param {String} `input`
  12. * @param {Object} `options`
  13. * @api public
  14. */
  15. function Parser(options) {
  16. debug('initializing', __filename);
  17. this.options = utils.extend({source: 'string'}, options);
  18. this.init(this.options);
  19. use(this);
  20. }
  21. /**
  22. * Prototype methods
  23. */
  24. Parser.prototype = {
  25. constructor: Parser,
  26. init: function(options) {
  27. this.orig = '';
  28. this.input = '';
  29. this.parsed = '';
  30. this.column = 1;
  31. this.line = 1;
  32. this.regex = new Cache();
  33. this.errors = this.errors || [];
  34. this.parsers = this.parsers || {};
  35. this.types = this.types || [];
  36. this.sets = this.sets || {};
  37. this.fns = this.fns || [];
  38. this.currentType = 'root';
  39. var pos = this.position();
  40. this.bos = pos({type: 'bos', val: ''});
  41. this.ast = {
  42. type: 'root',
  43. errors: this.errors,
  44. nodes: [this.bos]
  45. };
  46. define(this.bos, 'parent', this.ast);
  47. this.nodes = [this.ast];
  48. this.count = 0;
  49. this.setCount = 0;
  50. this.stack = [];
  51. },
  52. /**
  53. * Throw a formatted error with the cursor column and `msg`.
  54. * @param {String} `msg` Message to use in the Error.
  55. */
  56. error: function(msg, node) {
  57. var pos = node.position || {start: {column: 0, line: 0}};
  58. var line = pos.start.line;
  59. var column = pos.start.column;
  60. var source = this.options.source;
  61. var message = source + ' <line:' + line + ' column:' + column + '>: ' + msg;
  62. var err = new Error(message);
  63. err.source = source;
  64. err.reason = msg;
  65. err.pos = pos;
  66. if (this.options.silent) {
  67. this.errors.push(err);
  68. } else {
  69. throw err;
  70. }
  71. },
  72. /**
  73. * Define a non-enumberable property on the `Parser` instance.
  74. *
  75. * ```js
  76. * parser.define('foo', 'bar');
  77. * ```
  78. * @name .define
  79. * @param {String} `key` propery name
  80. * @param {any} `val` property value
  81. * @return {Object} Returns the Parser instance for chaining.
  82. * @api public
  83. */
  84. define: function(key, val) {
  85. define(this, key, val);
  86. return this;
  87. },
  88. /**
  89. * Mark position and patch `node.position`.
  90. */
  91. position: function() {
  92. var start = { line: this.line, column: this.column };
  93. var self = this;
  94. return function(node) {
  95. define(node, 'position', new Position(start, self));
  96. return node;
  97. };
  98. },
  99. /**
  100. * Set parser `name` with the given `fn`
  101. * @param {String} `name`
  102. * @param {Function} `fn`
  103. * @api public
  104. */
  105. set: function(type, fn) {
  106. if (this.types.indexOf(type) === -1) {
  107. this.types.push(type);
  108. }
  109. this.parsers[type] = fn.bind(this);
  110. return this;
  111. },
  112. /**
  113. * Get parser `name`
  114. * @param {String} `name`
  115. * @api public
  116. */
  117. get: function(name) {
  118. return this.parsers[name];
  119. },
  120. /**
  121. * Push a `token` onto the `type` stack.
  122. *
  123. * @param {String} `type`
  124. * @return {Object} `token`
  125. * @api public
  126. */
  127. push: function(type, token) {
  128. this.sets[type] = this.sets[type] || [];
  129. this.count++;
  130. this.stack.push(token);
  131. return this.sets[type].push(token);
  132. },
  133. /**
  134. * Pop a token off of the `type` stack
  135. * @param {String} `type`
  136. * @returns {Object} Returns a token
  137. * @api public
  138. */
  139. pop: function(type) {
  140. this.sets[type] = this.sets[type] || [];
  141. this.count--;
  142. this.stack.pop();
  143. return this.sets[type].pop();
  144. },
  145. /**
  146. * Return true if inside a `stack` node. Types are `braces`, `parens` or `brackets`.
  147. *
  148. * @param {String} `type`
  149. * @return {Boolean}
  150. * @api public
  151. */
  152. isInside: function(type) {
  153. this.sets[type] = this.sets[type] || [];
  154. return this.sets[type].length > 0;
  155. },
  156. /**
  157. * Return true if `node` is the given `type`.
  158. *
  159. * ```js
  160. * parser.isType(node, 'brace');
  161. * ```
  162. * @param {Object} `node`
  163. * @param {String} `type`
  164. * @return {Boolean}
  165. * @api public
  166. */
  167. isType: function(node, type) {
  168. return node && node.type === type;
  169. },
  170. /**
  171. * Get the previous AST node
  172. * @return {Object}
  173. */
  174. prev: function(n) {
  175. return this.stack.length > 0
  176. ? utils.last(this.stack, n)
  177. : utils.last(this.nodes, n);
  178. },
  179. /**
  180. * Update line and column based on `str`.
  181. */
  182. consume: function(len) {
  183. this.input = this.input.substr(len);
  184. },
  185. /**
  186. * Update column based on `str`.
  187. */
  188. updatePosition: function(str, len) {
  189. var lines = str.match(/\n/g);
  190. if (lines) this.line += lines.length;
  191. var i = str.lastIndexOf('\n');
  192. this.column = ~i ? len - i : this.column + len;
  193. this.parsed += str;
  194. this.consume(len);
  195. },
  196. /**
  197. * Match `regex`, return captures, and update the cursor position by `match[0]` length.
  198. * @param {RegExp} `regex`
  199. * @return {Object}
  200. */
  201. match: function(regex) {
  202. var m = regex.exec(this.input);
  203. if (m) {
  204. this.updatePosition(m[0], m[0].length);
  205. return m;
  206. }
  207. },
  208. /**
  209. * Capture `type` with the given regex.
  210. * @param {String} `type`
  211. * @param {RegExp} `regex`
  212. * @return {Function}
  213. */
  214. capture: function(type, regex) {
  215. if (typeof regex === 'function') {
  216. return this.set.apply(this, arguments);
  217. }
  218. this.regex.set(type, regex);
  219. this.set(type, function() {
  220. var parsed = this.parsed;
  221. var pos = this.position();
  222. var m = this.match(regex);
  223. if (!m || !m[0]) return;
  224. var prev = this.prev();
  225. var node = pos({
  226. type: type,
  227. val: m[0],
  228. parsed: parsed,
  229. rest: this.input
  230. });
  231. if (m[1]) {
  232. node.inner = m[1];
  233. }
  234. define(node, 'inside', this.stack.length > 0);
  235. define(node, 'parent', prev);
  236. prev.nodes.push(node);
  237. }.bind(this));
  238. return this;
  239. },
  240. /**
  241. * Create a parser with open and close for parens,
  242. * brackets or braces
  243. */
  244. capturePair: function(type, openRegex, closeRegex, fn) {
  245. this.sets[type] = this.sets[type] || [];
  246. /**
  247. * Open
  248. */
  249. this.set(type + '.open', function() {
  250. var parsed = this.parsed;
  251. var pos = this.position();
  252. var m = this.match(openRegex);
  253. if (!m || !m[0]) return;
  254. var val = m[0];
  255. this.setCount++;
  256. this.specialChars = true;
  257. var open = pos({
  258. type: type + '.open',
  259. val: val,
  260. rest: this.input
  261. });
  262. if (typeof m[1] !== 'undefined') {
  263. open.inner = m[1];
  264. }
  265. var prev = this.prev();
  266. var node = pos({
  267. type: type,
  268. nodes: [open]
  269. });
  270. define(node, 'rest', this.input);
  271. define(node, 'parsed', parsed);
  272. define(node, 'prefix', m[1]);
  273. define(node, 'parent', prev);
  274. define(open, 'parent', node);
  275. if (typeof fn === 'function') {
  276. fn.call(this, open, node);
  277. }
  278. this.push(type, node);
  279. prev.nodes.push(node);
  280. });
  281. /**
  282. * Close
  283. */
  284. this.set(type + '.close', function() {
  285. var pos = this.position();
  286. var m = this.match(closeRegex);
  287. if (!m || !m[0]) return;
  288. var parent = this.pop(type);
  289. var node = pos({
  290. type: type + '.close',
  291. rest: this.input,
  292. suffix: m[1],
  293. val: m[0]
  294. });
  295. if (!this.isType(parent, type)) {
  296. if (this.options.strict) {
  297. throw new Error('missing opening "' + type + '"');
  298. }
  299. this.setCount--;
  300. node.escaped = true;
  301. return node;
  302. }
  303. if (node.suffix === '\\') {
  304. parent.escaped = true;
  305. node.escaped = true;
  306. }
  307. parent.nodes.push(node);
  308. define(node, 'parent', parent);
  309. });
  310. return this;
  311. },
  312. /**
  313. * Capture end-of-string
  314. */
  315. eos: function() {
  316. var pos = this.position();
  317. if (this.input) return;
  318. var prev = this.prev();
  319. while (prev.type !== 'root' && !prev.visited) {
  320. if (this.options.strict === true) {
  321. throw new SyntaxError('invalid syntax:' + util.inspect(prev, null, 2));
  322. }
  323. if (!hasDelims(prev)) {
  324. prev.parent.escaped = true;
  325. prev.escaped = true;
  326. }
  327. visit(prev, function(node) {
  328. if (!hasDelims(node.parent)) {
  329. node.parent.escaped = true;
  330. node.escaped = true;
  331. }
  332. });
  333. prev = prev.parent;
  334. }
  335. var tok = pos({
  336. type: 'eos',
  337. val: this.append || ''
  338. });
  339. define(tok, 'parent', this.ast);
  340. return tok;
  341. },
  342. /**
  343. * Run parsers to advance the cursor position
  344. */
  345. next: function() {
  346. var parsed = this.parsed;
  347. var len = this.types.length;
  348. var idx = -1;
  349. var tok;
  350. while (++idx < len) {
  351. if ((tok = this.parsers[this.types[idx]].call(this))) {
  352. define(tok, 'rest', this.input);
  353. define(tok, 'parsed', parsed);
  354. this.last = tok;
  355. return tok;
  356. }
  357. }
  358. },
  359. /**
  360. * Parse the given string.
  361. * @return {Array}
  362. */
  363. parse: function(input) {
  364. if (typeof input !== 'string') {
  365. throw new TypeError('expected a string');
  366. }
  367. this.init(this.options);
  368. this.orig = input;
  369. this.input = input;
  370. var self = this;
  371. function parse() {
  372. // check input before calling `.next()`
  373. input = self.input;
  374. // get the next AST ndoe
  375. var node = self.next();
  376. if (node) {
  377. var prev = self.prev();
  378. if (prev) {
  379. define(node, 'parent', prev);
  380. if (prev.nodes) {
  381. prev.nodes.push(node);
  382. }
  383. }
  384. if (self.sets.hasOwnProperty(prev.type)) {
  385. self.currentType = prev.type;
  386. }
  387. }
  388. // if we got here but input is not changed, throw an error
  389. if (self.input && input === self.input) {
  390. throw new Error('no parsers registered for: "' + self.input.slice(0, 5) + '"');
  391. }
  392. }
  393. while (this.input) parse();
  394. if (this.stack.length && this.options.strict) {
  395. var node = this.stack.pop();
  396. throw this.error('missing opening ' + node.type + ': "' + this.orig + '"');
  397. }
  398. var eos = this.eos();
  399. var tok = this.prev();
  400. if (tok.type !== 'eos') {
  401. this.ast.nodes.push(eos);
  402. }
  403. return this.ast;
  404. }
  405. };
  406. /**
  407. * Visit `node` with the given `fn`
  408. */
  409. function visit(node, fn) {
  410. if (!node.visited) {
  411. define(node, 'visited', true);
  412. return node.nodes ? mapVisit(node.nodes, fn) : fn(node);
  413. }
  414. return node;
  415. }
  416. /**
  417. * Map visit over array of `nodes`.
  418. */
  419. function mapVisit(nodes, fn) {
  420. var len = nodes.length;
  421. var idx = -1;
  422. while (++idx < len) {
  423. visit(nodes[idx], fn);
  424. }
  425. }
  426. function hasOpen(node) {
  427. return node.nodes && node.nodes[0].type === (node.type + '.open');
  428. }
  429. function hasClose(node) {
  430. return node.nodes && utils.last(node.nodes).type === (node.type + '.close');
  431. }
  432. function hasDelims(node) {
  433. return hasOpen(node) && hasClose(node);
  434. }
  435. /**
  436. * Expose `Parser`
  437. */
  438. module.exports = Parser;