jade.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. // CodeMirror, copyright (c) by Marijn Haverbeke and others
  2. // Distributed under an MIT license: http://codemirror.net/LICENSE
  3. (function(mod) {
  4. if (typeof exports == "object" && typeof module == "object") // CommonJS
  5. mod(require("../../lib/codemirror"), require("../javascript/javascript"), require("../css/css"), require("../htmlmixed/htmlmixed"));
  6. else if (typeof define == "function" && define.amd) // AMD
  7. define(["../../lib/codemirror", "../javascript/javascript", "../css/css", "../htmlmixed/htmlmixed"], mod);
  8. else // Plain browser env
  9. mod(CodeMirror);
  10. })(function(CodeMirror) {
  11. "use strict";
  12. CodeMirror.defineMode('jade', function (config) {
  13. // token types
  14. var KEYWORD = 'keyword';
  15. var DOCTYPE = 'meta';
  16. var ID = 'builtin';
  17. var CLASS = 'qualifier';
  18. var ATTRS_NEST = {
  19. '{': '}',
  20. '(': ')',
  21. '[': ']'
  22. };
  23. var jsMode = CodeMirror.getMode(config, 'javascript');
  24. function State() {
  25. this.javaScriptLine = false;
  26. this.javaScriptLineExcludesColon = false;
  27. this.javaScriptArguments = false;
  28. this.javaScriptArgumentsDepth = 0;
  29. this.isInterpolating = false;
  30. this.interpolationNesting = 0;
  31. this.jsState = jsMode.startState();
  32. this.restOfLine = '';
  33. this.isIncludeFiltered = false;
  34. this.isEach = false;
  35. this.lastTag = '';
  36. this.scriptType = '';
  37. // Attributes Mode
  38. this.isAttrs = false;
  39. this.attrsNest = [];
  40. this.inAttributeName = true;
  41. this.attributeIsType = false;
  42. this.attrValue = '';
  43. // Indented Mode
  44. this.indentOf = Infinity;
  45. this.indentToken = '';
  46. this.innerMode = null;
  47. this.innerState = null;
  48. this.innerModeForLine = false;
  49. }
  50. /**
  51. * Safely copy a state
  52. *
  53. * @return {State}
  54. */
  55. State.prototype.copy = function () {
  56. var res = new State();
  57. res.javaScriptLine = this.javaScriptLine;
  58. res.javaScriptLineExcludesColon = this.javaScriptLineExcludesColon;
  59. res.javaScriptArguments = this.javaScriptArguments;
  60. res.javaScriptArgumentsDepth = this.javaScriptArgumentsDepth;
  61. res.isInterpolating = this.isInterpolating;
  62. res.interpolationNesting = this.intpolationNesting;
  63. res.jsState = CodeMirror.copyState(jsMode, this.jsState);
  64. res.innerMode = this.innerMode;
  65. if (this.innerMode && this.innerState) {
  66. res.innerState = CodeMirror.copyState(this.innerMode, this.innerState);
  67. }
  68. res.restOfLine = this.restOfLine;
  69. res.isIncludeFiltered = this.isIncludeFiltered;
  70. res.isEach = this.isEach;
  71. res.lastTag = this.lastTag;
  72. res.scriptType = this.scriptType;
  73. res.isAttrs = this.isAttrs;
  74. res.attrsNest = this.attrsNest.slice();
  75. res.inAttributeName = this.inAttributeName;
  76. res.attributeIsType = this.attributeIsType;
  77. res.attrValue = this.attrValue;
  78. res.indentOf = this.indentOf;
  79. res.indentToken = this.indentToken;
  80. res.innerModeForLine = this.innerModeForLine;
  81. return res;
  82. };
  83. function javaScript(stream, state) {
  84. if (stream.sol()) {
  85. // if javaScriptLine was set at end of line, ignore it
  86. state.javaScriptLine = false;
  87. state.javaScriptLineExcludesColon = false;
  88. }
  89. if (state.javaScriptLine) {
  90. if (state.javaScriptLineExcludesColon && stream.peek() === ':') {
  91. state.javaScriptLine = false;
  92. state.javaScriptLineExcludesColon = false;
  93. return;
  94. }
  95. var tok = jsMode.token(stream, state.jsState);
  96. if (stream.eol()) state.javaScriptLine = false;
  97. return tok || true;
  98. }
  99. }
  100. function javaScriptArguments(stream, state) {
  101. if (state.javaScriptArguments) {
  102. if (state.javaScriptArgumentsDepth === 0 && stream.peek() !== '(') {
  103. state.javaScriptArguments = false;
  104. return;
  105. }
  106. if (stream.peek() === '(') {
  107. state.javaScriptArgumentsDepth++;
  108. } else if (stream.peek() === ')') {
  109. state.javaScriptArgumentsDepth--;
  110. }
  111. if (state.javaScriptArgumentsDepth === 0) {
  112. state.javaScriptArguments = false;
  113. return;
  114. }
  115. var tok = jsMode.token(stream, state.jsState);
  116. return tok || true;
  117. }
  118. }
  119. function yieldStatement(stream) {
  120. if (stream.match(/^yield\b/)) {
  121. return 'keyword';
  122. }
  123. }
  124. function doctype(stream) {
  125. if (stream.match(/^(?:doctype) *([^\n]+)?/)) {
  126. return DOCTYPE;
  127. }
  128. }
  129. function interpolation(stream, state) {
  130. if (stream.match('#{')) {
  131. state.isInterpolating = true;
  132. state.interpolationNesting = 0;
  133. return 'punctuation';
  134. }
  135. }
  136. function interpolationContinued(stream, state) {
  137. if (state.isInterpolating) {
  138. if (stream.peek() === '}') {
  139. state.interpolationNesting--;
  140. if (state.interpolationNesting < 0) {
  141. stream.next();
  142. state.isInterpolating = false;
  143. return 'puncutation';
  144. }
  145. } else if (stream.peek() === '{') {
  146. state.interpolationNesting++;
  147. }
  148. return jsMode.token(stream, state.jsState) || true;
  149. }
  150. }
  151. function caseStatement(stream, state) {
  152. if (stream.match(/^case\b/)) {
  153. state.javaScriptLine = true;
  154. return KEYWORD;
  155. }
  156. }
  157. function when(stream, state) {
  158. if (stream.match(/^when\b/)) {
  159. state.javaScriptLine = true;
  160. state.javaScriptLineExcludesColon = true;
  161. return KEYWORD;
  162. }
  163. }
  164. function defaultStatement(stream) {
  165. if (stream.match(/^default\b/)) {
  166. return KEYWORD;
  167. }
  168. }
  169. function extendsStatement(stream, state) {
  170. if (stream.match(/^extends?\b/)) {
  171. state.restOfLine = 'string';
  172. return KEYWORD;
  173. }
  174. }
  175. function append(stream, state) {
  176. if (stream.match(/^append\b/)) {
  177. state.restOfLine = 'variable';
  178. return KEYWORD;
  179. }
  180. }
  181. function prepend(stream, state) {
  182. if (stream.match(/^prepend\b/)) {
  183. state.restOfLine = 'variable';
  184. return KEYWORD;
  185. }
  186. }
  187. function block(stream, state) {
  188. if (stream.match(/^block\b *(?:(prepend|append)\b)?/)) {
  189. state.restOfLine = 'variable';
  190. return KEYWORD;
  191. }
  192. }
  193. function include(stream, state) {
  194. if (stream.match(/^include\b/)) {
  195. state.restOfLine = 'string';
  196. return KEYWORD;
  197. }
  198. }
  199. function includeFiltered(stream, state) {
  200. if (stream.match(/^include:([a-zA-Z0-9\-]+)/, false) && stream.match('include')) {
  201. state.isIncludeFiltered = true;
  202. return KEYWORD;
  203. }
  204. }
  205. function includeFilteredContinued(stream, state) {
  206. if (state.isIncludeFiltered) {
  207. var tok = filter(stream, state);
  208. state.isIncludeFiltered = false;
  209. state.restOfLine = 'string';
  210. return tok;
  211. }
  212. }
  213. function mixin(stream, state) {
  214. if (stream.match(/^mixin\b/)) {
  215. state.javaScriptLine = true;
  216. return KEYWORD;
  217. }
  218. }
  219. function call(stream, state) {
  220. if (stream.match(/^\+([-\w]+)/)) {
  221. if (!stream.match(/^\( *[-\w]+ *=/, false)) {
  222. state.javaScriptArguments = true;
  223. state.javaScriptArgumentsDepth = 0;
  224. }
  225. return 'variable';
  226. }
  227. if (stream.match(/^\+#{/, false)) {
  228. stream.next();
  229. state.mixinCallAfter = true;
  230. return interpolation(stream, state);
  231. }
  232. }
  233. function callArguments(stream, state) {
  234. if (state.mixinCallAfter) {
  235. state.mixinCallAfter = false;
  236. if (!stream.match(/^\( *[-\w]+ *=/, false)) {
  237. state.javaScriptArguments = true;
  238. state.javaScriptArgumentsDepth = 0;
  239. }
  240. return true;
  241. }
  242. }
  243. function conditional(stream, state) {
  244. if (stream.match(/^(if|unless|else if|else)\b/)) {
  245. state.javaScriptLine = true;
  246. return KEYWORD;
  247. }
  248. }
  249. function each(stream, state) {
  250. if (stream.match(/^(- *)?(each|for)\b/)) {
  251. state.isEach = true;
  252. return KEYWORD;
  253. }
  254. }
  255. function eachContinued(stream, state) {
  256. if (state.isEach) {
  257. if (stream.match(/^ in\b/)) {
  258. state.javaScriptLine = true;
  259. state.isEach = false;
  260. return KEYWORD;
  261. } else if (stream.sol() || stream.eol()) {
  262. state.isEach = false;
  263. } else if (stream.next()) {
  264. while (!stream.match(/^ in\b/, false) && stream.next());
  265. return 'variable';
  266. }
  267. }
  268. }
  269. function whileStatement(stream, state) {
  270. if (stream.match(/^while\b/)) {
  271. state.javaScriptLine = true;
  272. return KEYWORD;
  273. }
  274. }
  275. function tag(stream, state) {
  276. var captures;
  277. if (captures = stream.match(/^(\w(?:[-:\w]*\w)?)\/?/)) {
  278. state.lastTag = captures[1].toLowerCase();
  279. if (state.lastTag === 'script') {
  280. state.scriptType = 'application/javascript';
  281. }
  282. return 'tag';
  283. }
  284. }
  285. function filter(stream, state) {
  286. if (stream.match(/^:([\w\-]+)/)) {
  287. var innerMode;
  288. if (config && config.innerModes) {
  289. innerMode = config.innerModes(stream.current().substring(1));
  290. }
  291. if (!innerMode) {
  292. innerMode = stream.current().substring(1);
  293. }
  294. if (typeof innerMode === 'string') {
  295. innerMode = CodeMirror.getMode(config, innerMode);
  296. }
  297. setInnerMode(stream, state, innerMode);
  298. return 'atom';
  299. }
  300. }
  301. function code(stream, state) {
  302. if (stream.match(/^(!?=|-)/)) {
  303. state.javaScriptLine = true;
  304. return 'punctuation';
  305. }
  306. }
  307. function id(stream) {
  308. if (stream.match(/^#([\w-]+)/)) {
  309. return ID;
  310. }
  311. }
  312. function className(stream) {
  313. if (stream.match(/^\.([\w-]+)/)) {
  314. return CLASS;
  315. }
  316. }
  317. function attrs(stream, state) {
  318. if (stream.peek() == '(') {
  319. stream.next();
  320. state.isAttrs = true;
  321. state.attrsNest = [];
  322. state.inAttributeName = true;
  323. state.attrValue = '';
  324. state.attributeIsType = false;
  325. return 'punctuation';
  326. }
  327. }
  328. function attrsContinued(stream, state) {
  329. if (state.isAttrs) {
  330. if (ATTRS_NEST[stream.peek()]) {
  331. state.attrsNest.push(ATTRS_NEST[stream.peek()]);
  332. }
  333. if (state.attrsNest[state.attrsNest.length - 1] === stream.peek()) {
  334. state.attrsNest.pop();
  335. } else if (stream.eat(')')) {
  336. state.isAttrs = false;
  337. return 'punctuation';
  338. }
  339. if (state.inAttributeName && stream.match(/^[^=,\)!]+/)) {
  340. if (stream.peek() === '=' || stream.peek() === '!') {
  341. state.inAttributeName = false;
  342. state.jsState = jsMode.startState();
  343. if (state.lastTag === 'script' && stream.current().trim().toLowerCase() === 'type') {
  344. state.attributeIsType = true;
  345. } else {
  346. state.attributeIsType = false;
  347. }
  348. }
  349. return 'attribute';
  350. }
  351. var tok = jsMode.token(stream, state.jsState);
  352. if (state.attributeIsType && tok === 'string') {
  353. state.scriptType = stream.current().toString();
  354. }
  355. if (state.attrsNest.length === 0 && (tok === 'string' || tok === 'variable' || tok === 'keyword')) {
  356. try {
  357. Function('', 'var x ' + state.attrValue.replace(/,\s*$/, '').replace(/^!/, ''));
  358. state.inAttributeName = true;
  359. state.attrValue = '';
  360. stream.backUp(stream.current().length);
  361. return attrsContinued(stream, state);
  362. } catch (ex) {
  363. //not the end of an attribute
  364. }
  365. }
  366. state.attrValue += stream.current();
  367. return tok || true;
  368. }
  369. }
  370. function attributesBlock(stream, state) {
  371. if (stream.match(/^&attributes\b/)) {
  372. state.javaScriptArguments = true;
  373. state.javaScriptArgumentsDepth = 0;
  374. return 'keyword';
  375. }
  376. }
  377. function indent(stream) {
  378. if (stream.sol() && stream.eatSpace()) {
  379. return 'indent';
  380. }
  381. }
  382. function comment(stream, state) {
  383. if (stream.match(/^ *\/\/(-)?([^\n]*)/)) {
  384. state.indentOf = stream.indentation();
  385. state.indentToken = 'comment';
  386. return 'comment';
  387. }
  388. }
  389. function colon(stream) {
  390. if (stream.match(/^: */)) {
  391. return 'colon';
  392. }
  393. }
  394. function text(stream, state) {
  395. if (stream.match(/^(?:\| ?| )([^\n]+)/)) {
  396. return 'string';
  397. }
  398. if (stream.match(/^(<[^\n]*)/, false)) {
  399. // html string
  400. setInnerMode(stream, state, 'htmlmixed');
  401. state.innerModeForLine = true;
  402. return innerMode(stream, state, true);
  403. }
  404. }
  405. function dot(stream, state) {
  406. if (stream.eat('.')) {
  407. var innerMode = null;
  408. if (state.lastTag === 'script' && state.scriptType.toLowerCase().indexOf('javascript') != -1) {
  409. innerMode = state.scriptType.toLowerCase().replace(/"|'/g, '');
  410. } else if (state.lastTag === 'style') {
  411. innerMode = 'css';
  412. }
  413. setInnerMode(stream, state, innerMode);
  414. return 'dot';
  415. }
  416. }
  417. function fail(stream) {
  418. stream.next();
  419. return null;
  420. }
  421. function setInnerMode(stream, state, mode) {
  422. mode = CodeMirror.mimeModes[mode] || mode;
  423. mode = config.innerModes ? config.innerModes(mode) || mode : mode;
  424. mode = CodeMirror.mimeModes[mode] || mode;
  425. mode = CodeMirror.getMode(config, mode);
  426. state.indentOf = stream.indentation();
  427. if (mode && mode.name !== 'null') {
  428. state.innerMode = mode;
  429. } else {
  430. state.indentToken = 'string';
  431. }
  432. }
  433. function innerMode(stream, state, force) {
  434. if (stream.indentation() > state.indentOf || (state.innerModeForLine && !stream.sol()) || force) {
  435. if (state.innerMode) {
  436. if (!state.innerState) {
  437. state.innerState = state.innerMode.startState ? state.innerMode.startState(stream.indentation()) : {};
  438. }
  439. return stream.hideFirstChars(state.indentOf + 2, function () {
  440. return state.innerMode.token(stream, state.innerState) || true;
  441. });
  442. } else {
  443. stream.skipToEnd();
  444. return state.indentToken;
  445. }
  446. } else if (stream.sol()) {
  447. state.indentOf = Infinity;
  448. state.indentToken = null;
  449. state.innerMode = null;
  450. state.innerState = null;
  451. }
  452. }
  453. function restOfLine(stream, state) {
  454. if (stream.sol()) {
  455. // if restOfLine was set at end of line, ignore it
  456. state.restOfLine = '';
  457. }
  458. if (state.restOfLine) {
  459. stream.skipToEnd();
  460. var tok = state.restOfLine;
  461. state.restOfLine = '';
  462. return tok;
  463. }
  464. }
  465. function startState() {
  466. return new State();
  467. }
  468. function copyState(state) {
  469. return state.copy();
  470. }
  471. /**
  472. * Get the next token in the stream
  473. *
  474. * @param {Stream} stream
  475. * @param {State} state
  476. */
  477. function nextToken(stream, state) {
  478. var tok = innerMode(stream, state)
  479. || restOfLine(stream, state)
  480. || interpolationContinued(stream, state)
  481. || includeFilteredContinued(stream, state)
  482. || eachContinued(stream, state)
  483. || attrsContinued(stream, state)
  484. || javaScript(stream, state)
  485. || javaScriptArguments(stream, state)
  486. || callArguments(stream, state)
  487. || yieldStatement(stream, state)
  488. || doctype(stream, state)
  489. || interpolation(stream, state)
  490. || caseStatement(stream, state)
  491. || when(stream, state)
  492. || defaultStatement(stream, state)
  493. || extendsStatement(stream, state)
  494. || append(stream, state)
  495. || prepend(stream, state)
  496. || block(stream, state)
  497. || include(stream, state)
  498. || includeFiltered(stream, state)
  499. || mixin(stream, state)
  500. || call(stream, state)
  501. || conditional(stream, state)
  502. || each(stream, state)
  503. || whileStatement(stream, state)
  504. || tag(stream, state)
  505. || filter(stream, state)
  506. || code(stream, state)
  507. || id(stream, state)
  508. || className(stream, state)
  509. || attrs(stream, state)
  510. || attributesBlock(stream, state)
  511. || indent(stream, state)
  512. || text(stream, state)
  513. || comment(stream, state)
  514. || colon(stream, state)
  515. || dot(stream, state)
  516. || fail(stream, state);
  517. return tok === true ? null : tok;
  518. }
  519. return {
  520. startState: startState,
  521. copyState: copyState,
  522. token: nextToken
  523. };
  524. });
  525. CodeMirror.defineMIME('text/x-jade', 'jade');
  526. });