Ein Projekt das es ermöglicht Beerpong über das Internet von zwei unabhängigen positionen aus zu spielen. Entstehung im Rahmen einer Praktikumsaufgabe im Fach Interaktion.
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.

parser.js 19KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824
  1. 'use strict';
  2. var Lexer = require('./lexer');
  3. var nodes = require('./nodes');
  4. var utils = require('./utils');
  5. var filters = require('./filters');
  6. var path = require('path');
  7. var constantinople = require('constantinople');
  8. var parseJSExpression = require('character-parser').parseMax;
  9. var extname = path.extname;
  10. /**
  11. * Initialize `Parser` with the given input `str` and `filename`.
  12. *
  13. * @param {String} str
  14. * @param {String} filename
  15. * @param {Object} options
  16. * @api public
  17. */
  18. var Parser = exports = module.exports = function Parser(str, filename, options){
  19. //Strip any UTF-8 BOM off of the start of `str`, if it exists.
  20. this.input = str.replace(/^\uFEFF/, '');
  21. this.lexer = new Lexer(this.input, filename);
  22. this.filename = filename;
  23. this.blocks = {};
  24. this.mixins = {};
  25. this.options = options;
  26. this.contexts = [this];
  27. this.inMixin = false;
  28. this.dependencies = [];
  29. this.inBlock = 0;
  30. };
  31. /**
  32. * Parser prototype.
  33. */
  34. Parser.prototype = {
  35. /**
  36. * Save original constructor
  37. */
  38. constructor: Parser,
  39. /**
  40. * Push `parser` onto the context stack,
  41. * or pop and return a `Parser`.
  42. */
  43. context: function(parser){
  44. if (parser) {
  45. this.contexts.push(parser);
  46. } else {
  47. return this.contexts.pop();
  48. }
  49. },
  50. /**
  51. * Return the next token object.
  52. *
  53. * @return {Object}
  54. * @api private
  55. */
  56. advance: function(){
  57. return this.lexer.advance();
  58. },
  59. /**
  60. * Single token lookahead.
  61. *
  62. * @return {Object}
  63. * @api private
  64. */
  65. peek: function() {
  66. return this.lookahead(1);
  67. },
  68. /**
  69. * Return lexer lineno.
  70. *
  71. * @return {Number}
  72. * @api private
  73. */
  74. line: function() {
  75. return this.lexer.lineno;
  76. },
  77. /**
  78. * `n` token lookahead.
  79. *
  80. * @param {Number} n
  81. * @return {Object}
  82. * @api private
  83. */
  84. lookahead: function(n){
  85. return this.lexer.lookahead(n);
  86. },
  87. /**
  88. * Parse input returning a string of js for evaluation.
  89. *
  90. * @return {String}
  91. * @api public
  92. */
  93. parse: function(){
  94. var block = new nodes.Block, parser;
  95. block.line = 0;
  96. block.filename = this.filename;
  97. while ('eos' != this.peek().type) {
  98. if ('newline' == this.peek().type) {
  99. this.advance();
  100. } else {
  101. var next = this.peek();
  102. var expr = this.parseExpr();
  103. expr.filename = expr.filename || this.filename;
  104. expr.line = next.line;
  105. block.push(expr);
  106. }
  107. }
  108. if (parser = this.extending) {
  109. this.context(parser);
  110. var ast = parser.parse();
  111. this.context();
  112. // hoist mixins
  113. for (var name in this.mixins)
  114. ast.unshift(this.mixins[name]);
  115. return ast;
  116. }
  117. if (!this.extending && !this.included && Object.keys(this.blocks).length){
  118. var blocks = [];
  119. utils.walkAST(block, function (node) {
  120. if (node.type === 'Block' && node.name) {
  121. blocks.push(node.name);
  122. }
  123. });
  124. Object.keys(this.blocks).forEach(function (name) {
  125. if (blocks.indexOf(name) === -1 && !this.blocks[name].isSubBlock) {
  126. console.warn('Warning: Unexpected block "'
  127. + name
  128. + '" '
  129. + ' on line '
  130. + this.blocks[name].line
  131. + ' of '
  132. + (this.blocks[name].filename)
  133. + '. This block is never used. This warning will be an error in v2.0.0');
  134. }
  135. }.bind(this));
  136. }
  137. return block;
  138. },
  139. /**
  140. * Expect the given type, or throw an exception.
  141. *
  142. * @param {String} type
  143. * @api private
  144. */
  145. expect: function(type){
  146. if (this.peek().type === type) {
  147. return this.advance();
  148. } else {
  149. throw new Error('expected "' + type + '", but got "' + this.peek().type + '"');
  150. }
  151. },
  152. /**
  153. * Accept the given `type`.
  154. *
  155. * @param {String} type
  156. * @api private
  157. */
  158. accept: function(type){
  159. if (this.peek().type === type) {
  160. return this.advance();
  161. }
  162. },
  163. /**
  164. * tag
  165. * | doctype
  166. * | mixin
  167. * | include
  168. * | filter
  169. * | comment
  170. * | text
  171. * | each
  172. * | code
  173. * | yield
  174. * | id
  175. * | class
  176. * | interpolation
  177. */
  178. parseExpr: function(){
  179. switch (this.peek().type) {
  180. case 'tag':
  181. return this.parseTag();
  182. case 'mixin':
  183. return this.parseMixin();
  184. case 'block':
  185. return this.parseBlock();
  186. case 'mixin-block':
  187. return this.parseMixinBlock();
  188. case 'case':
  189. return this.parseCase();
  190. case 'extends':
  191. return this.parseExtends();
  192. case 'include':
  193. return this.parseInclude();
  194. case 'doctype':
  195. return this.parseDoctype();
  196. case 'filter':
  197. return this.parseFilter();
  198. case 'comment':
  199. return this.parseComment();
  200. case 'text':
  201. return this.parseText();
  202. case 'each':
  203. return this.parseEach();
  204. case 'code':
  205. return this.parseCode();
  206. case 'call':
  207. return this.parseCall();
  208. case 'interpolation':
  209. return this.parseInterpolation();
  210. case 'yield':
  211. this.advance();
  212. var block = new nodes.Block;
  213. block.yield = true;
  214. return block;
  215. case 'id':
  216. case 'class':
  217. var tok = this.advance();
  218. this.lexer.defer(this.lexer.tok('tag', 'div'));
  219. this.lexer.defer(tok);
  220. return this.parseExpr();
  221. default:
  222. throw new Error('unexpected token "' + this.peek().type + '"');
  223. }
  224. },
  225. /**
  226. * Text
  227. */
  228. parseText: function(){
  229. var tok = this.expect('text');
  230. var tokens = this.parseInlineTagsInText(tok.val);
  231. if (tokens.length === 1) return tokens[0];
  232. var node = new nodes.Block;
  233. for (var i = 0; i < tokens.length; i++) {
  234. node.push(tokens[i]);
  235. };
  236. return node;
  237. },
  238. /**
  239. * ':' expr
  240. * | block
  241. */
  242. parseBlockExpansion: function(){
  243. if (':' == this.peek().type) {
  244. this.advance();
  245. return new nodes.Block(this.parseExpr());
  246. } else {
  247. return this.block();
  248. }
  249. },
  250. /**
  251. * case
  252. */
  253. parseCase: function(){
  254. var val = this.expect('case').val;
  255. var node = new nodes.Case(val);
  256. node.line = this.line();
  257. var block = new nodes.Block;
  258. block.line = this.line();
  259. block.filename = this.filename;
  260. this.expect('indent');
  261. while ('outdent' != this.peek().type) {
  262. switch (this.peek().type) {
  263. case 'newline':
  264. this.advance();
  265. break;
  266. case 'when':
  267. block.push(this.parseWhen());
  268. break;
  269. case 'default':
  270. block.push(this.parseDefault());
  271. break;
  272. default:
  273. throw new Error('Unexpected token "' + this.peek().type
  274. + '", expected "when", "default" or "newline"');
  275. }
  276. }
  277. this.expect('outdent');
  278. node.block = block;
  279. return node;
  280. },
  281. /**
  282. * when
  283. */
  284. parseWhen: function(){
  285. var val = this.expect('when').val;
  286. if (this.peek().type !== 'newline')
  287. return new nodes.Case.When(val, this.parseBlockExpansion());
  288. else
  289. return new nodes.Case.When(val);
  290. },
  291. /**
  292. * default
  293. */
  294. parseDefault: function(){
  295. this.expect('default');
  296. return new nodes.Case.When('default', this.parseBlockExpansion());
  297. },
  298. /**
  299. * code
  300. */
  301. parseCode: function(afterIf){
  302. var tok = this.expect('code');
  303. var node = new nodes.Code(tok.val, tok.buffer, tok.escape);
  304. var block;
  305. node.line = this.line();
  306. // throw an error if an else does not have an if
  307. if (tok.isElse && !tok.hasIf) {
  308. throw new Error('Unexpected else without if');
  309. }
  310. // handle block
  311. block = 'indent' == this.peek().type;
  312. if (block) {
  313. node.block = this.block();
  314. }
  315. // handle missing block
  316. if (tok.requiresBlock && !block) {
  317. node.block = new nodes.Block();
  318. }
  319. // mark presense of if for future elses
  320. if (tok.isIf && this.peek().isElse) {
  321. this.peek().hasIf = true;
  322. } else if (tok.isIf && this.peek().type === 'newline' && this.lookahead(2).isElse) {
  323. this.lookahead(2).hasIf = true;
  324. }
  325. return node;
  326. },
  327. /**
  328. * comment
  329. */
  330. parseComment: function(){
  331. var tok = this.expect('comment');
  332. var node;
  333. var block;
  334. if (block = this.parseTextBlock()) {
  335. node = new nodes.BlockComment(tok.val, block, tok.buffer);
  336. } else {
  337. node = new nodes.Comment(tok.val, tok.buffer);
  338. }
  339. node.line = this.line();
  340. return node;
  341. },
  342. /**
  343. * doctype
  344. */
  345. parseDoctype: function(){
  346. var tok = this.expect('doctype');
  347. var node = new nodes.Doctype(tok.val);
  348. node.line = this.line();
  349. return node;
  350. },
  351. /**
  352. * filter attrs? text-block
  353. */
  354. parseFilter: function(){
  355. var tok = this.expect('filter');
  356. var attrs = this.accept('attrs');
  357. var block;
  358. block = this.parseTextBlock() || new nodes.Block();
  359. var options = {};
  360. if (attrs) {
  361. attrs.attrs.forEach(function (attribute) {
  362. options[attribute.name] = constantinople.toConstant(attribute.val);
  363. });
  364. }
  365. var node = new nodes.Filter(tok.val, block, options);
  366. node.line = this.line();
  367. return node;
  368. },
  369. /**
  370. * each block
  371. */
  372. parseEach: function(){
  373. var tok = this.expect('each');
  374. var node = new nodes.Each(tok.code, tok.val, tok.key);
  375. node.line = this.line();
  376. node.block = this.block();
  377. if (this.peek().type == 'code' && this.peek().val == 'else') {
  378. this.advance();
  379. node.alternative = this.block();
  380. }
  381. return node;
  382. },
  383. /**
  384. * Resolves a path relative to the template for use in
  385. * includes and extends
  386. *
  387. * @param {String} path
  388. * @param {String} purpose Used in error messages.
  389. * @return {String}
  390. * @api private
  391. */
  392. resolvePath: function (path, purpose) {
  393. var p = require('path');
  394. var dirname = p.dirname;
  395. var basename = p.basename;
  396. var join = p.join;
  397. if (path[0] !== '/' && !this.filename)
  398. throw new Error('the "filename" option is required to use "' + purpose + '" with "relative" paths');
  399. if (path[0] === '/' && !this.options.basedir)
  400. throw new Error('the "basedir" option is required to use "' + purpose + '" with "absolute" paths');
  401. path = join(path[0] === '/' ? this.options.basedir : dirname(this.filename), path);
  402. if (basename(path).indexOf('.') === -1) path += '.jade';
  403. return path;
  404. },
  405. /**
  406. * 'extends' name
  407. */
  408. parseExtends: function(){
  409. var fs = require('fs');
  410. var path = this.resolvePath(this.expect('extends').val.trim(), 'extends');
  411. if ('.jade' != path.substr(-5)) path += '.jade';
  412. this.dependencies.push(path);
  413. var str = fs.readFileSync(path, 'utf8');
  414. var parser = new this.constructor(str, path, this.options);
  415. parser.dependencies = this.dependencies;
  416. parser.blocks = this.blocks;
  417. parser.included = this.included;
  418. parser.contexts = this.contexts;
  419. this.extending = parser;
  420. // TODO: null node
  421. return new nodes.Literal('');
  422. },
  423. /**
  424. * 'block' name block
  425. */
  426. parseBlock: function(){
  427. var block = this.expect('block');
  428. var mode = block.mode;
  429. var name = block.val.trim();
  430. var line = block.line;
  431. this.inBlock++;
  432. block = 'indent' == this.peek().type
  433. ? this.block()
  434. : new nodes.Block(new nodes.Literal(''));
  435. this.inBlock--;
  436. block.name = name;
  437. block.line = line;
  438. var prev = this.blocks[name] || {prepended: [], appended: []}
  439. if (prev.mode === 'replace') return this.blocks[name] = prev;
  440. var allNodes = prev.prepended.concat(block.nodes).concat(prev.appended);
  441. switch (mode) {
  442. case 'append':
  443. prev.appended = prev.parser === this ?
  444. prev.appended.concat(block.nodes) :
  445. block.nodes.concat(prev.appended);
  446. break;
  447. case 'prepend':
  448. prev.prepended = prev.parser === this ?
  449. block.nodes.concat(prev.prepended) :
  450. prev.prepended.concat(block.nodes);
  451. break;
  452. }
  453. block.nodes = allNodes;
  454. block.appended = prev.appended;
  455. block.prepended = prev.prepended;
  456. block.mode = mode;
  457. block.parser = this;
  458. block.isSubBlock = this.inBlock > 0;
  459. return this.blocks[name] = block;
  460. },
  461. parseMixinBlock: function () {
  462. var block = this.expect('mixin-block');
  463. if (!this.inMixin) {
  464. throw new Error('Anonymous blocks are not allowed unless they are part of a mixin.');
  465. }
  466. return new nodes.MixinBlock();
  467. },
  468. /**
  469. * include block?
  470. */
  471. parseInclude: function(){
  472. var fs = require('fs');
  473. var tok = this.expect('include');
  474. var path = this.resolvePath(tok.val.trim(), 'include');
  475. this.dependencies.push(path);
  476. // has-filter
  477. if (tok.filter) {
  478. var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
  479. var options = {filename: path};
  480. if (tok.attrs) {
  481. tok.attrs.attrs.forEach(function (attribute) {
  482. options[attribute.name] = constantinople.toConstant(attribute.val);
  483. });
  484. }
  485. str = filters(tok.filter, str, options);
  486. return new nodes.Literal(str);
  487. }
  488. // non-jade
  489. if ('.jade' != path.substr(-5)) {
  490. var str = fs.readFileSync(path, 'utf8').replace(/\r/g, '');
  491. return new nodes.Literal(str);
  492. }
  493. var str = fs.readFileSync(path, 'utf8');
  494. var parser = new this.constructor(str, path, this.options);
  495. parser.dependencies = this.dependencies;
  496. parser.blocks = utils.merge({}, this.blocks);
  497. parser.included = true;
  498. parser.mixins = this.mixins;
  499. this.context(parser);
  500. var ast = parser.parse();
  501. this.context();
  502. ast.filename = path;
  503. if ('indent' == this.peek().type) {
  504. ast.includeBlock().push(this.block());
  505. }
  506. return ast;
  507. },
  508. /**
  509. * call ident block
  510. */
  511. parseCall: function(){
  512. var tok = this.expect('call');
  513. var name = tok.val;
  514. var args = tok.args;
  515. var mixin = new nodes.Mixin(name, args, new nodes.Block, true);
  516. this.tag(mixin);
  517. if (mixin.code) {
  518. mixin.block.push(mixin.code);
  519. mixin.code = null;
  520. }
  521. if (mixin.block.isEmpty()) mixin.block = null;
  522. return mixin;
  523. },
  524. /**
  525. * mixin block
  526. */
  527. parseMixin: function(){
  528. var tok = this.expect('mixin');
  529. var name = tok.val;
  530. var args = tok.args;
  531. var mixin;
  532. // definition
  533. if ('indent' == this.peek().type) {
  534. this.inMixin = true;
  535. mixin = new nodes.Mixin(name, args, this.block(), false);
  536. this.mixins[name] = mixin;
  537. this.inMixin = false;
  538. return mixin;
  539. // call
  540. } else {
  541. return new nodes.Mixin(name, args, null, true);
  542. }
  543. },
  544. parseInlineTagsInText: function (str) {
  545. var line = this.line();
  546. var match = /(\\)?#\[((?:.|\n)*)$/.exec(str);
  547. if (match) {
  548. if (match[1]) { // escape
  549. var text = new nodes.Text(str.substr(0, match.index) + '#[');
  550. text.line = line;
  551. var rest = this.parseInlineTagsInText(match[2]);
  552. if (rest[0].type === 'Text') {
  553. text.val += rest[0].val;
  554. rest.shift();
  555. }
  556. return [text].concat(rest);
  557. } else {
  558. var text = new nodes.Text(str.substr(0, match.index));
  559. text.line = line;
  560. var buffer = [text];
  561. var rest = match[2];
  562. var range = parseJSExpression(rest);
  563. var inner = new Parser(range.src, this.filename, this.options);
  564. buffer.push(inner.parse());
  565. return buffer.concat(this.parseInlineTagsInText(rest.substr(range.end + 1)));
  566. }
  567. } else {
  568. var text = new nodes.Text(str);
  569. text.line = line;
  570. return [text];
  571. }
  572. },
  573. /**
  574. * indent (text | newline)* outdent
  575. */
  576. parseTextBlock: function(){
  577. var block = new nodes.Block;
  578. block.line = this.line();
  579. var body = this.peek();
  580. if (body.type !== 'pipeless-text') return;
  581. this.advance();
  582. block.nodes = body.val.reduce(function (accumulator, text) {
  583. return accumulator.concat(this.parseInlineTagsInText(text));
  584. }.bind(this), []);
  585. return block;
  586. },
  587. /**
  588. * indent expr* outdent
  589. */
  590. block: function(){
  591. var block = new nodes.Block;
  592. block.line = this.line();
  593. block.filename = this.filename;
  594. this.expect('indent');
  595. while ('outdent' != this.peek().type) {
  596. if ('newline' == this.peek().type) {
  597. this.advance();
  598. } else {
  599. var expr = this.parseExpr();
  600. expr.filename = this.filename;
  601. block.push(expr);
  602. }
  603. }
  604. this.expect('outdent');
  605. return block;
  606. },
  607. /**
  608. * interpolation (attrs | class | id)* (text | code | ':')? newline* block?
  609. */
  610. parseInterpolation: function(){
  611. var tok = this.advance();
  612. var tag = new nodes.Tag(tok.val);
  613. tag.buffer = true;
  614. return this.tag(tag);
  615. },
  616. /**
  617. * tag (attrs | class | id)* (text | code | ':')? newline* block?
  618. */
  619. parseTag: function(){
  620. var tok = this.advance();
  621. var tag = new nodes.Tag(tok.val);
  622. tag.selfClosing = tok.selfClosing;
  623. return this.tag(tag);
  624. },
  625. /**
  626. * Parse tag.
  627. */
  628. tag: function(tag){
  629. tag.line = this.line();
  630. var seenAttrs = false;
  631. // (attrs | class | id)*
  632. out:
  633. while (true) {
  634. switch (this.peek().type) {
  635. case 'id':
  636. case 'class':
  637. var tok = this.advance();
  638. tag.setAttribute(tok.type, "'" + tok.val + "'");
  639. continue;
  640. case 'attrs':
  641. if (seenAttrs) {
  642. console.warn(this.filename + ', line ' + this.peek().line + ':\nYou should not have jade tags with multiple attributes.');
  643. }
  644. seenAttrs = true;
  645. var tok = this.advance();
  646. var attrs = tok.attrs;
  647. if (tok.selfClosing) tag.selfClosing = true;
  648. for (var i = 0; i < attrs.length; i++) {
  649. tag.setAttribute(attrs[i].name, attrs[i].val, attrs[i].escaped);
  650. }
  651. continue;
  652. case '&attributes':
  653. var tok = this.advance();
  654. tag.addAttributes(tok.val);
  655. break;
  656. default:
  657. break out;
  658. }
  659. }
  660. // check immediate '.'
  661. if ('dot' == this.peek().type) {
  662. tag.textOnly = true;
  663. this.advance();
  664. }
  665. // (text | code | ':')?
  666. switch (this.peek().type) {
  667. case 'text':
  668. tag.block.push(this.parseText());
  669. break;
  670. case 'code':
  671. tag.code = this.parseCode();
  672. break;
  673. case ':':
  674. this.advance();
  675. tag.block = new nodes.Block;
  676. tag.block.push(this.parseExpr());
  677. break;
  678. case 'newline':
  679. case 'indent':
  680. case 'outdent':
  681. case 'eos':
  682. case 'pipeless-text':
  683. break;
  684. default:
  685. throw new Error('Unexpected token `' + this.peek().type + '` expected `text`, `code`, `:`, `newline` or `eos`')
  686. }
  687. // newline*
  688. while ('newline' == this.peek().type) this.advance();
  689. // block?
  690. if (tag.textOnly) {
  691. tag.block = this.parseTextBlock();
  692. } else if ('indent' == this.peek().type) {
  693. var block = this.block();
  694. for (var i = 0, len = block.nodes.length; i < len; ++i) {
  695. tag.block.push(block.nodes[i]);
  696. }
  697. }
  698. return tag;
  699. }
  700. };