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.

compiler.js 18KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703
  1. 'use strict';
  2. var nodes = require('./nodes');
  3. var filters = require('./filters');
  4. var doctypes = require('./doctypes');
  5. var runtime = require('./runtime');
  6. var utils = require('./utils');
  7. var selfClosing = require('void-elements');
  8. var parseJSExpression = require('character-parser').parseMax;
  9. var constantinople = require('constantinople');
  10. function isConstant(src) {
  11. return constantinople(src, {jade: runtime, 'jade_interp': undefined});
  12. }
  13. function toConstant(src) {
  14. return constantinople.toConstant(src, {jade: runtime, 'jade_interp': undefined});
  15. }
  16. function errorAtNode(node, error) {
  17. error.line = node.line;
  18. error.filename = node.filename;
  19. return error;
  20. }
  21. /**
  22. * Initialize `Compiler` with the given `node`.
  23. *
  24. * @param {Node} node
  25. * @param {Object} options
  26. * @api public
  27. */
  28. var Compiler = module.exports = function Compiler(node, options) {
  29. this.options = options = options || {};
  30. this.node = node;
  31. this.hasCompiledDoctype = false;
  32. this.hasCompiledTag = false;
  33. this.pp = options.pretty || false;
  34. this.debug = false !== options.compileDebug;
  35. this.indents = 0;
  36. this.parentIndents = 0;
  37. this.terse = false;
  38. this.mixins = {};
  39. this.dynamicMixins = false;
  40. if (options.doctype) this.setDoctype(options.doctype);
  41. };
  42. /**
  43. * Compiler prototype.
  44. */
  45. Compiler.prototype = {
  46. /**
  47. * Compile parse tree to JavaScript.
  48. *
  49. * @api public
  50. */
  51. compile: function(){
  52. this.buf = [];
  53. if (this.pp) this.buf.push("var jade_indent = [];");
  54. this.lastBufferedIdx = -1;
  55. this.visit(this.node);
  56. if (!this.dynamicMixins) {
  57. // if there are no dynamic mixins we can remove any un-used mixins
  58. var mixinNames = Object.keys(this.mixins);
  59. for (var i = 0; i < mixinNames.length; i++) {
  60. var mixin = this.mixins[mixinNames[i]];
  61. if (!mixin.used) {
  62. for (var x = 0; x < mixin.instances.length; x++) {
  63. for (var y = mixin.instances[x].start; y < mixin.instances[x].end; y++) {
  64. this.buf[y] = '';
  65. }
  66. }
  67. }
  68. }
  69. }
  70. return this.buf.join('\n');
  71. },
  72. /**
  73. * Sets the default doctype `name`. Sets terse mode to `true` when
  74. * html 5 is used, causing self-closing tags to end with ">" vs "/>",
  75. * and boolean attributes are not mirrored.
  76. *
  77. * @param {string} name
  78. * @api public
  79. */
  80. setDoctype: function(name){
  81. this.doctype = doctypes[name.toLowerCase()] || '<!DOCTYPE ' + name + '>';
  82. this.terse = this.doctype.toLowerCase() == '<!doctype html>';
  83. this.xml = 0 == this.doctype.indexOf('<?xml');
  84. },
  85. /**
  86. * Buffer the given `str` exactly as is or with interpolation
  87. *
  88. * @param {String} str
  89. * @param {Boolean} interpolate
  90. * @api public
  91. */
  92. buffer: function (str, interpolate) {
  93. var self = this;
  94. if (interpolate) {
  95. var match = /(\\)?([#!]){((?:.|\n)*)$/.exec(str);
  96. if (match) {
  97. this.buffer(str.substr(0, match.index), false);
  98. if (match[1]) { // escape
  99. this.buffer(match[2] + '{', false);
  100. this.buffer(match[3], true);
  101. return;
  102. } else {
  103. var rest = match[3];
  104. var range = parseJSExpression(rest);
  105. var code = ('!' == match[2] ? '' : 'jade.escape') + "((jade_interp = " + range.src + ") == null ? '' : jade_interp)";
  106. this.bufferExpression(code);
  107. this.buffer(rest.substr(range.end + 1), true);
  108. return;
  109. }
  110. }
  111. }
  112. str = JSON.stringify(str);
  113. str = str.substr(1, str.length - 2);
  114. if (this.lastBufferedIdx == this.buf.length) {
  115. if (this.lastBufferedType === 'code') this.lastBuffered += ' + "';
  116. this.lastBufferedType = 'text';
  117. this.lastBuffered += str;
  118. this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + '");'
  119. } else {
  120. this.buf.push('buf.push("' + str + '");');
  121. this.lastBufferedType = 'text';
  122. this.bufferStartChar = '"';
  123. this.lastBuffered = str;
  124. this.lastBufferedIdx = this.buf.length;
  125. }
  126. },
  127. /**
  128. * Buffer the given `src` so it is evaluated at run time
  129. *
  130. * @param {String} src
  131. * @api public
  132. */
  133. bufferExpression: function (src) {
  134. if (isConstant(src)) {
  135. return this.buffer(toConstant(src) + '', false)
  136. }
  137. if (this.lastBufferedIdx == this.buf.length) {
  138. if (this.lastBufferedType === 'text') this.lastBuffered += '"';
  139. this.lastBufferedType = 'code';
  140. this.lastBuffered += ' + (' + src + ')';
  141. this.buf[this.lastBufferedIdx - 1] = 'buf.push(' + this.bufferStartChar + this.lastBuffered + ');'
  142. } else {
  143. this.buf.push('buf.push(' + src + ');');
  144. this.lastBufferedType = 'code';
  145. this.bufferStartChar = '';
  146. this.lastBuffered = '(' + src + ')';
  147. this.lastBufferedIdx = this.buf.length;
  148. }
  149. },
  150. /**
  151. * Buffer an indent based on the current `indent`
  152. * property and an additional `offset`.
  153. *
  154. * @param {Number} offset
  155. * @param {Boolean} newline
  156. * @api public
  157. */
  158. prettyIndent: function(offset, newline){
  159. offset = offset || 0;
  160. newline = newline ? '\n' : '';
  161. this.buffer(newline + Array(this.indents + offset).join(' '));
  162. if (this.parentIndents)
  163. this.buf.push("buf.push.apply(buf, jade_indent);");
  164. },
  165. /**
  166. * Visit `node`.
  167. *
  168. * @param {Node} node
  169. * @api public
  170. */
  171. visit: function(node){
  172. var debug = this.debug;
  173. if (debug) {
  174. this.buf.push('jade_debug.unshift({ lineno: ' + node.line
  175. + ', filename: ' + (node.filename
  176. ? JSON.stringify(node.filename)
  177. : 'jade_debug[0].filename')
  178. + ' });');
  179. }
  180. // Massive hack to fix our context
  181. // stack for - else[ if] etc
  182. if (false === node.debug && this.debug) {
  183. this.buf.pop();
  184. this.buf.pop();
  185. }
  186. this.visitNode(node);
  187. if (debug) this.buf.push('jade_debug.shift();');
  188. },
  189. /**
  190. * Visit `node`.
  191. *
  192. * @param {Node} node
  193. * @api public
  194. */
  195. visitNode: function(node){
  196. return this['visit' + node.type](node);
  197. },
  198. /**
  199. * Visit case `node`.
  200. *
  201. * @param {Literal} node
  202. * @api public
  203. */
  204. visitCase: function(node){
  205. var _ = this.withinCase;
  206. this.withinCase = true;
  207. this.buf.push('switch (' + node.expr + '){');
  208. this.visit(node.block);
  209. this.buf.push('}');
  210. this.withinCase = _;
  211. },
  212. /**
  213. * Visit when `node`.
  214. *
  215. * @param {Literal} node
  216. * @api public
  217. */
  218. visitWhen: function(node){
  219. if ('default' == node.expr) {
  220. this.buf.push('default:');
  221. } else {
  222. this.buf.push('case ' + node.expr + ':');
  223. }
  224. if (node.block) {
  225. this.visit(node.block);
  226. this.buf.push(' break;');
  227. }
  228. },
  229. /**
  230. * Visit literal `node`.
  231. *
  232. * @param {Literal} node
  233. * @api public
  234. */
  235. visitLiteral: function(node){
  236. this.buffer(node.str);
  237. },
  238. /**
  239. * Visit all nodes in `block`.
  240. *
  241. * @param {Block} block
  242. * @api public
  243. */
  244. visitBlock: function(block){
  245. var len = block.nodes.length
  246. , escape = this.escape
  247. , pp = this.pp
  248. // Pretty print multi-line text
  249. if (pp && len > 1 && !escape && block.nodes[0].isText && block.nodes[1].isText)
  250. this.prettyIndent(1, true);
  251. for (var i = 0; i < len; ++i) {
  252. // Pretty print text
  253. if (pp && i > 0 && !escape && block.nodes[i].isText && block.nodes[i-1].isText)
  254. this.prettyIndent(1, false);
  255. this.visit(block.nodes[i]);
  256. // Multiple text nodes are separated by newlines
  257. if (block.nodes[i+1] && block.nodes[i].isText && block.nodes[i+1].isText)
  258. this.buffer('\n');
  259. }
  260. },
  261. /**
  262. * Visit a mixin's `block` keyword.
  263. *
  264. * @param {MixinBlock} block
  265. * @api public
  266. */
  267. visitMixinBlock: function(block){
  268. if (this.pp) this.buf.push("jade_indent.push('" + Array(this.indents + 1).join(' ') + "');");
  269. this.buf.push('block && block();');
  270. if (this.pp) this.buf.push("jade_indent.pop();");
  271. },
  272. /**
  273. * Visit `doctype`. Sets terse mode to `true` when html 5
  274. * is used, causing self-closing tags to end with ">" vs "/>",
  275. * and boolean attributes are not mirrored.
  276. *
  277. * @param {Doctype} doctype
  278. * @api public
  279. */
  280. visitDoctype: function(doctype){
  281. if (doctype && (doctype.val || !this.doctype)) {
  282. this.setDoctype(doctype.val || 'default');
  283. }
  284. if (this.doctype) this.buffer(this.doctype);
  285. this.hasCompiledDoctype = true;
  286. },
  287. /**
  288. * Visit `mixin`, generating a function that
  289. * may be called within the template.
  290. *
  291. * @param {Mixin} mixin
  292. * @api public
  293. */
  294. visitMixin: function(mixin){
  295. var name = 'jade_mixins[';
  296. var args = mixin.args || '';
  297. var block = mixin.block;
  298. var attrs = mixin.attrs;
  299. var attrsBlocks = mixin.attributeBlocks;
  300. var pp = this.pp;
  301. var dynamic = mixin.name[0]==='#';
  302. var key = mixin.name;
  303. if (dynamic) this.dynamicMixins = true;
  304. name += (dynamic ? mixin.name.substr(2,mixin.name.length-3):'"'+mixin.name+'"')+']';
  305. this.mixins[key] = this.mixins[key] || {used: false, instances: []};
  306. if (mixin.call) {
  307. this.mixins[key].used = true;
  308. if (pp) this.buf.push("jade_indent.push('" + Array(this.indents + 1).join(' ') + "');")
  309. if (block || attrs.length || attrsBlocks.length) {
  310. this.buf.push(name + '.call({');
  311. if (block) {
  312. this.buf.push('block: function(){');
  313. // Render block with no indents, dynamically added when rendered
  314. this.parentIndents++;
  315. var _indents = this.indents;
  316. this.indents = 0;
  317. this.visit(mixin.block);
  318. this.indents = _indents;
  319. this.parentIndents--;
  320. if (attrs.length || attrsBlocks.length) {
  321. this.buf.push('},');
  322. } else {
  323. this.buf.push('}');
  324. }
  325. }
  326. if (attrsBlocks.length) {
  327. if (attrs.length) {
  328. var val = this.attrs(attrs);
  329. attrsBlocks.unshift(val);
  330. }
  331. this.buf.push('attributes: jade.merge([' + attrsBlocks.join(',') + '])');
  332. } else if (attrs.length) {
  333. var val = this.attrs(attrs);
  334. this.buf.push('attributes: ' + val);
  335. }
  336. if (args) {
  337. this.buf.push('}, ' + args + ');');
  338. } else {
  339. this.buf.push('});');
  340. }
  341. } else {
  342. this.buf.push(name + '(' + args + ');');
  343. }
  344. if (pp) this.buf.push("jade_indent.pop();")
  345. } else {
  346. var mixin_start = this.buf.length;
  347. this.buf.push(name + ' = function(' + args + '){');
  348. this.buf.push('var block = (this && this.block), attributes = (this && this.attributes) || {};');
  349. this.parentIndents++;
  350. this.visit(block);
  351. this.parentIndents--;
  352. this.buf.push('};');
  353. var mixin_end = this.buf.length;
  354. this.mixins[key].instances.push({start: mixin_start, end: mixin_end});
  355. }
  356. },
  357. /**
  358. * Visit `tag` buffering tag markup, generating
  359. * attributes, visiting the `tag`'s code and block.
  360. *
  361. * @param {Tag} tag
  362. * @api public
  363. */
  364. visitTag: function(tag){
  365. this.indents++;
  366. var name = tag.name
  367. , pp = this.pp
  368. , self = this;
  369. function bufferName() {
  370. if (tag.buffer) self.bufferExpression(name);
  371. else self.buffer(name);
  372. }
  373. if ('pre' == tag.name) this.escape = true;
  374. if (!this.hasCompiledTag) {
  375. if (!this.hasCompiledDoctype && 'html' == name) {
  376. this.visitDoctype();
  377. }
  378. this.hasCompiledTag = true;
  379. }
  380. // pretty print
  381. if (pp && !tag.isInline())
  382. this.prettyIndent(0, true);
  383. if (tag.selfClosing || (!this.xml && selfClosing.indexOf(tag.name) !== -1)) {
  384. this.buffer('<');
  385. bufferName();
  386. this.visitAttributes(tag.attrs, tag.attributeBlocks);
  387. this.terse
  388. ? this.buffer('>')
  389. : this.buffer('/>');
  390. // if it is non-empty throw an error
  391. if (tag.block &&
  392. !(tag.block.type === 'Block' && tag.block.nodes.length === 0) &&
  393. tag.block.nodes.some(function (tag) {
  394. return tag.type !== 'Text' || !/^\s*$/.test(tag.val)
  395. })) {
  396. throw errorAtNode(tag, new Error(name + ' is self closing and should not have content.'));
  397. }
  398. } else {
  399. // Optimize attributes buffering
  400. this.buffer('<');
  401. bufferName();
  402. this.visitAttributes(tag.attrs, tag.attributeBlocks);
  403. this.buffer('>');
  404. if (tag.code) this.visitCode(tag.code);
  405. this.visit(tag.block);
  406. // pretty print
  407. if (pp && !tag.isInline() && 'pre' != tag.name && !tag.canInline())
  408. this.prettyIndent(0, true);
  409. this.buffer('</');
  410. bufferName();
  411. this.buffer('>');
  412. }
  413. if ('pre' == tag.name) this.escape = false;
  414. this.indents--;
  415. },
  416. /**
  417. * Visit `filter`, throwing when the filter does not exist.
  418. *
  419. * @param {Filter} filter
  420. * @api public
  421. */
  422. visitFilter: function(filter){
  423. var text = filter.block.nodes.map(
  424. function(node){ return node.val; }
  425. ).join('\n');
  426. filter.attrs.filename = this.options.filename;
  427. try {
  428. this.buffer(filters(filter.name, text, filter.attrs), true);
  429. } catch (err) {
  430. throw errorAtNode(filter, err);
  431. }
  432. },
  433. /**
  434. * Visit `text` node.
  435. *
  436. * @param {Text} text
  437. * @api public
  438. */
  439. visitText: function(text){
  440. this.buffer(text.val, true);
  441. },
  442. /**
  443. * Visit a `comment`, only buffering when the buffer flag is set.
  444. *
  445. * @param {Comment} comment
  446. * @api public
  447. */
  448. visitComment: function(comment){
  449. if (!comment.buffer) return;
  450. if (this.pp) this.prettyIndent(1, true);
  451. this.buffer('<!--' + comment.val + '-->');
  452. },
  453. /**
  454. * Visit a `BlockComment`.
  455. *
  456. * @param {Comment} comment
  457. * @api public
  458. */
  459. visitBlockComment: function(comment){
  460. if (!comment.buffer) return;
  461. if (this.pp) this.prettyIndent(1, true);
  462. this.buffer('<!--' + comment.val);
  463. this.visit(comment.block);
  464. if (this.pp) this.prettyIndent(1, true);
  465. this.buffer('-->');
  466. },
  467. /**
  468. * Visit `code`, respecting buffer / escape flags.
  469. * If the code is followed by a block, wrap it in
  470. * a self-calling function.
  471. *
  472. * @param {Code} code
  473. * @api public
  474. */
  475. visitCode: function(code){
  476. // Wrap code blocks with {}.
  477. // we only wrap unbuffered code blocks ATM
  478. // since they are usually flow control
  479. // Buffer code
  480. if (code.buffer) {
  481. var val = code.val.trimLeft();
  482. val = 'null == (jade_interp = '+val+') ? "" : jade_interp';
  483. if (code.escape) val = 'jade.escape(' + val + ')';
  484. this.bufferExpression(val);
  485. } else {
  486. this.buf.push(code.val);
  487. }
  488. // Block support
  489. if (code.block) {
  490. if (!code.buffer) this.buf.push('{');
  491. this.visit(code.block);
  492. if (!code.buffer) this.buf.push('}');
  493. }
  494. },
  495. /**
  496. * Visit `each` block.
  497. *
  498. * @param {Each} each
  499. * @api public
  500. */
  501. visitEach: function(each){
  502. this.buf.push(''
  503. + '// iterate ' + each.obj + '\n'
  504. + ';(function(){\n'
  505. + ' var $$obj = ' + each.obj + ';\n'
  506. + ' if (\'number\' == typeof $$obj.length) {\n');
  507. if (each.alternative) {
  508. this.buf.push(' if ($$obj.length) {');
  509. }
  510. this.buf.push(''
  511. + ' for (var ' + each.key + ' = 0, $$l = $$obj.length; ' + each.key + ' < $$l; ' + each.key + '++) {\n'
  512. + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
  513. this.visit(each.block);
  514. this.buf.push(' }\n');
  515. if (each.alternative) {
  516. this.buf.push(' } else {');
  517. this.visit(each.alternative);
  518. this.buf.push(' }');
  519. }
  520. this.buf.push(''
  521. + ' } else {\n'
  522. + ' var $$l = 0;\n'
  523. + ' for (var ' + each.key + ' in $$obj) {\n'
  524. + ' $$l++;'
  525. + ' var ' + each.val + ' = $$obj[' + each.key + '];\n');
  526. this.visit(each.block);
  527. this.buf.push(' }\n');
  528. if (each.alternative) {
  529. this.buf.push(' if ($$l === 0) {');
  530. this.visit(each.alternative);
  531. this.buf.push(' }');
  532. }
  533. this.buf.push(' }\n}).call(this);\n');
  534. },
  535. /**
  536. * Visit `attrs`.
  537. *
  538. * @param {Array} attrs
  539. * @api public
  540. */
  541. visitAttributes: function(attrs, attributeBlocks){
  542. if (attributeBlocks.length) {
  543. if (attrs.length) {
  544. var val = this.attrs(attrs);
  545. attributeBlocks.unshift(val);
  546. }
  547. this.bufferExpression('jade.attrs(jade.merge([' + attributeBlocks.join(',') + ']), ' + JSON.stringify(this.terse) + ')');
  548. } else if (attrs.length) {
  549. this.attrs(attrs, true);
  550. }
  551. },
  552. /**
  553. * Compile attributes.
  554. */
  555. attrs: function(attrs, buffer){
  556. var buf = [];
  557. var classes = [];
  558. var classEscaping = [];
  559. attrs.forEach(function(attr){
  560. var key = attr.name;
  561. var escaped = attr.escaped;
  562. if (key === 'class') {
  563. classes.push(attr.val);
  564. classEscaping.push(attr.escaped);
  565. } else if (isConstant(attr.val)) {
  566. if (buffer) {
  567. this.buffer(runtime.attr(key, toConstant(attr.val), escaped, this.terse));
  568. } else {
  569. var val = toConstant(attr.val);
  570. if (escaped && !(key.indexOf('data') === 0 && typeof val !== 'string')) {
  571. val = runtime.escape(val);
  572. }
  573. buf.push(JSON.stringify(key) + ': ' + JSON.stringify(val));
  574. }
  575. } else {
  576. if (buffer) {
  577. this.bufferExpression('jade.attr("' + key + '", ' + attr.val + ', ' + JSON.stringify(escaped) + ', ' + JSON.stringify(this.terse) + ')');
  578. } else {
  579. var val = attr.val;
  580. if (escaped && !(key.indexOf('data') === 0)) {
  581. val = 'jade.escape(' + val + ')';
  582. } else if (escaped) {
  583. val = '(typeof (jade_interp = ' + val + ') == "string" ? jade.escape(jade_interp) : jade_interp)';
  584. }
  585. buf.push(JSON.stringify(key) + ': ' + val);
  586. }
  587. }
  588. }.bind(this));
  589. if (buffer) {
  590. if (classes.every(isConstant)) {
  591. this.buffer(runtime.cls(classes.map(toConstant), classEscaping));
  592. } else {
  593. this.bufferExpression('jade.cls([' + classes.join(',') + '], ' + JSON.stringify(classEscaping) + ')');
  594. }
  595. } else if (classes.length) {
  596. if (classes.every(isConstant)) {
  597. classes = JSON.stringify(runtime.joinClasses(classes.map(toConstant).map(runtime.joinClasses).map(function (cls, i) {
  598. return classEscaping[i] ? runtime.escape(cls) : cls;
  599. })));
  600. } else {
  601. classes = '(jade_interp = ' + JSON.stringify(classEscaping) + ',' +
  602. ' jade.joinClasses([' + classes.join(',') + '].map(jade.joinClasses).map(function (cls, i) {' +
  603. ' return jade_interp[i] ? jade.escape(cls) : cls' +
  604. ' }))' +
  605. ')';
  606. }
  607. if (classes.length)
  608. buf.push('"class": ' + classes);
  609. }
  610. return '{' + buf.join(',') + '}';
  611. }
  612. };