index.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551
  1. #!/usr/bin/env node
  2. 'use strict';
  3. // 'use strict' is here so we can use let and const in node 4
  4. /**
  5. * marked tests
  6. * Copyright (c) 2011-2013, Christopher Jeffrey. (MIT Licensed)
  7. * https://github.com/markedjs/marked
  8. */
  9. /**
  10. * Modules
  11. */
  12. const fs = require('fs');
  13. const path = require('path');
  14. const fm = require('front-matter');
  15. const g2r = require('glob-to-regexp');
  16. let marked = require('../');
  17. const htmlDiffer = require('./helpers/html-differ.js');
  18. /**
  19. * Load Tests
  20. */
  21. function load(options) {
  22. options = options || {};
  23. const dir = path.join(__dirname, 'compiled_tests');
  24. const glob = g2r(options.glob || '*', { extended: true });
  25. const list = fs
  26. .readdirSync(dir)
  27. .filter(file => {
  28. return path.extname(file) === '.md';
  29. })
  30. .sort();
  31. const files = list.reduce((obj, item) => {
  32. const name = path.basename(item, '.md');
  33. if (glob.test(name)) {
  34. const file = path.join(dir, item);
  35. const content = fm(fs.readFileSync(file, 'utf8'));
  36. obj[name] = {
  37. options: content.attributes,
  38. text: content.body,
  39. html: fs.readFileSync(file.replace(/[^.]+$/, 'html'), 'utf8')
  40. };
  41. }
  42. return obj;
  43. }, {});
  44. if (options.bench || options.time) {
  45. if (!options.glob) {
  46. // Change certain tests to allow
  47. // comparison to older benchmark times.
  48. fs.readdirSync(path.join(__dirname, 'new')).forEach(name => {
  49. if (path.extname(name) === '.html') return;
  50. if (name === 'main.md') return;
  51. delete files[name];
  52. });
  53. }
  54. if (files['backslash_escapes.md']) {
  55. files['backslash_escapes.md'] = {
  56. text: 'hello world \\[how](are you) today'
  57. };
  58. }
  59. if (files['main.md']) {
  60. files['main.md'].text = files['main.md'].text.replace('* * *\n\n', '');
  61. }
  62. }
  63. return files;
  64. }
  65. /**
  66. * Test Runner
  67. */
  68. function runTests(engine, options) {
  69. if (typeof engine !== 'function') {
  70. options = engine;
  71. engine = null;
  72. }
  73. engine = engine || marked;
  74. options = options || {};
  75. let succeeded = 0;
  76. let failed = 0;
  77. const files = options.files || load(options);
  78. const filenames = Object.keys(files);
  79. if (options.marked) {
  80. marked.setOptions(options.marked);
  81. }
  82. for (let i = 0; i < filenames.length; i++) {
  83. const filename = filenames[i];
  84. const file = files[filename];
  85. const success = testFile(engine, file, filename, i + 1);
  86. if (success) {
  87. succeeded++;
  88. } else {
  89. failed++;
  90. if (options.stop) {
  91. break;
  92. }
  93. }
  94. }
  95. console.log('\n%d/%d tests completed successfully.', succeeded, filenames.length);
  96. if (failed) console.log('%d/%d tests failed.', failed, filenames.length);
  97. return !failed;
  98. }
  99. /**
  100. * Test a file
  101. */
  102. function testFile(engine, file, filename, index) {
  103. const opts = Object.keys(file.options);
  104. if (marked._original) {
  105. marked.defaults = marked._original;
  106. delete marked._original;
  107. }
  108. console.log('#%d. Test %s', index, filename);
  109. if (opts.length) {
  110. marked._original = marked.defaults;
  111. marked.defaults = {};
  112. Object.keys(marked._original).forEach(key => {
  113. marked.defaults[key] = marked._original[key];
  114. });
  115. opts.forEach(key => {
  116. if (marked.defaults.hasOwnProperty(key)) {
  117. marked.defaults[key] = file.options[key];
  118. }
  119. });
  120. }
  121. const before = process.hrtime();
  122. let text, html, elapsed;
  123. try {
  124. text = engine(file.text);
  125. html = file.html;
  126. } catch (e) {
  127. elapsed = process.hrtime(before);
  128. console.log('\n failed in %dms\n', prettyElapsedTime(elapsed));
  129. throw e;
  130. }
  131. elapsed = process.hrtime(before);
  132. if (htmlDiffer.isEqual(text, html)) {
  133. if (elapsed[0] > 0) {
  134. console.log('\n failed because it took too long.\n\n passed in %dms\n', prettyElapsedTime(elapsed));
  135. return false;
  136. }
  137. console.log(' passed in %dms', prettyElapsedTime(elapsed));
  138. return true;
  139. }
  140. const diff = htmlDiffer.firstDiff(text, html);
  141. console.log('\n failed in %dms', prettyElapsedTime(elapsed));
  142. console.log(' Expected: %s', diff.expected);
  143. console.log(' Actual: %s\n', diff.actual);
  144. return false;
  145. }
  146. /**
  147. * Benchmark a function
  148. */
  149. function bench(name, files, engine) {
  150. const start = Date.now();
  151. for (let i = 0; i < 1000; i++) {
  152. for (const filename in files) {
  153. engine(files[filename].text);
  154. }
  155. }
  156. const end = Date.now();
  157. console.log('%s completed in %dms.', name, end - start);
  158. }
  159. /**
  160. * Benchmark all engines
  161. */
  162. function runBench(options) {
  163. options = options || {};
  164. const files = load(options);
  165. // Non-GFM, Non-pedantic
  166. marked.setOptions({
  167. gfm: false,
  168. tables: false,
  169. breaks: false,
  170. pedantic: false,
  171. sanitize: false,
  172. smartLists: false
  173. });
  174. if (options.marked) {
  175. marked.setOptions(options.marked);
  176. }
  177. bench('marked', files, marked);
  178. // GFM
  179. marked.setOptions({
  180. gfm: true,
  181. tables: false,
  182. breaks: false,
  183. pedantic: false,
  184. sanitize: false,
  185. smartLists: false
  186. });
  187. if (options.marked) {
  188. marked.setOptions(options.marked);
  189. }
  190. bench('marked (gfm)', files, marked);
  191. // Pedantic
  192. marked.setOptions({
  193. gfm: false,
  194. tables: false,
  195. breaks: false,
  196. pedantic: true,
  197. sanitize: false,
  198. smartLists: false
  199. });
  200. if (options.marked) {
  201. marked.setOptions(options.marked);
  202. }
  203. bench('marked (pedantic)', files, marked);
  204. try {
  205. bench('commonmark', files, (() => {
  206. const commonmark = require('commonmark');
  207. const parser = new commonmark.Parser();
  208. const writer = new commonmark.HtmlRenderer();
  209. return function (text) {
  210. return writer.render(parser.parse(text));
  211. };
  212. })());
  213. } catch (e) {
  214. console.log('Could not bench commonmark. (Error: %s)', e.message);
  215. }
  216. try {
  217. bench('markdown-it', files, (() => {
  218. const MarkdownIt = require('markdown-it');
  219. const md = new MarkdownIt();
  220. return md.render.bind(md);
  221. })());
  222. } catch (e) {
  223. console.log('Could not bench markdown-it. (Error: %s)', e.message);
  224. }
  225. try {
  226. bench('markdown.js', files, (() => {
  227. const markdown = require('markdown').markdown;
  228. return markdown.toHTML.bind(markdown);
  229. })());
  230. } catch (e) {
  231. console.log('Could not bench markdown.js. (Error: %s)', e.message);
  232. }
  233. return true;
  234. }
  235. /**
  236. * A simple one-time benchmark
  237. */
  238. function time(options) {
  239. options = options || {};
  240. const files = load(options);
  241. if (options.marked) {
  242. marked.setOptions(options.marked);
  243. }
  244. bench('marked', files, marked);
  245. return true;
  246. }
  247. /**
  248. * Markdown Test Suite Fixer
  249. * This function is responsible for "fixing"
  250. * the markdown test suite. There are
  251. * certain aspects of the suite that
  252. * are strange or might make tests
  253. * fail for reasons unrelated to
  254. * conformance.
  255. */
  256. function fix() {
  257. ['compiled_tests', 'original', 'new', 'redos'].forEach(dir => {
  258. try {
  259. fs.mkdirSync(path.resolve(__dirname, dir));
  260. } catch (e) {
  261. // directory already exists
  262. }
  263. });
  264. // rm -rf tests
  265. fs.readdirSync(path.resolve(__dirname, 'compiled_tests')).forEach(file => {
  266. fs.unlinkSync(path.resolve(__dirname, 'compiled_tests', file));
  267. });
  268. // cp -r original tests
  269. fs.readdirSync(path.resolve(__dirname, 'original')).forEach(file => {
  270. let text = fs.readFileSync(path.resolve(__dirname, 'original', file), 'utf8');
  271. if (path.extname(file) === '.md') {
  272. if (fm.test(text)) {
  273. text = fm(text);
  274. text = `---\n${text.frontmatter}\ngfm: false\n---\n${text.body}`;
  275. } else {
  276. text = `---\ngfm: false\n---\n${text}`;
  277. }
  278. }
  279. fs.writeFileSync(path.resolve(__dirname, 'compiled_tests', file), text);
  280. });
  281. // node fix.js
  282. const dir = path.join(__dirname, 'compiled_tests');
  283. fs.readdirSync(dir).filter(file => {
  284. return path.extname(file) === '.html';
  285. }).forEach(file => {
  286. file = path.join(dir, file);
  287. let html = fs.readFileSync(file, 'utf8');
  288. // fix unencoded quotes
  289. html = html
  290. .replace(/='([^\n']*)'(?=[^<>\n]*>)/g, '=&__APOS__;$1&__APOS__;')
  291. .replace(/="([^\n"]*)"(?=[^<>\n]*>)/g, '=&__QUOT__;$1&__QUOT__;')
  292. .replace(/"/g, '&quot;')
  293. .replace(/'/g, '&#39;')
  294. .replace(/&__QUOT__;/g, '"')
  295. .replace(/&__APOS__;/g, '\'');
  296. fs.writeFileSync(file, html);
  297. });
  298. // turn <hr /> into <hr>
  299. fs.readdirSync(dir).forEach(file => {
  300. file = path.join(dir, file);
  301. let text = fs.readFileSync(file, 'utf8');
  302. text = text.replace(/(<|&lt;)hr\s*\/(>|&gt;)/g, '$1hr$2');
  303. fs.writeFileSync(file, text);
  304. });
  305. // markdown does some strange things.
  306. // it does not encode naked `>`, marked does.
  307. {
  308. const file = `${dir}/amps_and_angles_encoding.html`;
  309. const html = fs.readFileSync(file, 'utf8')
  310. .replace('6 > 5.', '6 &gt; 5.');
  311. fs.writeFileSync(file, html);
  312. }
  313. // cp new/* tests/
  314. fs.readdirSync(path.resolve(__dirname, 'new')).forEach(file => {
  315. fs.writeFileSync(path.resolve(__dirname, 'compiled_tests', file),
  316. fs.readFileSync(path.resolve(__dirname, 'new', file)));
  317. });
  318. // cp redos/* tests/
  319. fs.readdirSync(path.resolve(__dirname, 'redos')).forEach(file => {
  320. fs.writeFileSync(path.resolve(__dirname, 'compiled_tests', file),
  321. fs.readFileSync(path.resolve(__dirname, 'redos', file)));
  322. });
  323. }
  324. /**
  325. * Argument Parsing
  326. */
  327. function parseArg(argv) {
  328. argv = argv.slice(2);
  329. const options = {};
  330. const orphans = [];
  331. function getarg() {
  332. let arg = argv.shift();
  333. if (arg.indexOf('--') === 0) {
  334. // e.g. --opt
  335. arg = arg.split('=');
  336. if (arg.length > 1) {
  337. // e.g. --opt=val
  338. argv.unshift(arg.slice(1).join('='));
  339. }
  340. arg = arg[0];
  341. } else if (arg[0] === '-') {
  342. if (arg.length > 2) {
  343. // e.g. -abc
  344. argv = arg.substring(1).split('').map(ch => {
  345. return `-${ch}`;
  346. }).concat(argv);
  347. arg = argv.shift();
  348. } else {
  349. // e.g. -a
  350. }
  351. } else {
  352. // e.g. foo
  353. }
  354. return arg;
  355. }
  356. while (argv.length) {
  357. let arg = getarg();
  358. switch (arg) {
  359. case '-f':
  360. case '--fix':
  361. case 'fix':
  362. if (options.fix !== false) {
  363. options.fix = true;
  364. }
  365. break;
  366. case '--no-fix':
  367. case 'no-fix':
  368. options.fix = false;
  369. break;
  370. case '-b':
  371. case '--bench':
  372. options.bench = true;
  373. break;
  374. case '-s':
  375. case '--stop':
  376. options.stop = true;
  377. break;
  378. case '-t':
  379. case '--time':
  380. options.time = true;
  381. break;
  382. case '-m':
  383. case '--minified':
  384. options.minified = true;
  385. break;
  386. case '--glob':
  387. arg = argv.shift();
  388. options.glob = arg.replace(/^=/, '');
  389. break;
  390. default:
  391. if (arg.indexOf('--') === 0) {
  392. const opt = camelize(arg.replace(/^--(no-)?/, ''));
  393. if (!marked.defaults.hasOwnProperty(opt)) {
  394. continue;
  395. }
  396. options.marked = options.marked || {};
  397. if (arg.indexOf('--no-') === 0) {
  398. options.marked[opt] = typeof marked.defaults[opt] !== 'boolean'
  399. ? null
  400. : false;
  401. } else {
  402. options.marked[opt] = typeof marked.defaults[opt] !== 'boolean'
  403. ? argv.shift()
  404. : true;
  405. }
  406. } else {
  407. orphans.push(arg);
  408. }
  409. break;
  410. }
  411. }
  412. return options;
  413. }
  414. /**
  415. * Helpers
  416. */
  417. function camelize(text) {
  418. return text.replace(/(\w)-(\w)/g, (_, a, b) => a + b.toUpperCase());
  419. }
  420. /**
  421. * Main
  422. */
  423. function main(argv) {
  424. const opt = parseArg(argv);
  425. if (opt.fix !== false) {
  426. fix();
  427. }
  428. if (opt.fix) {
  429. // only run fix
  430. return;
  431. }
  432. if (opt.bench) {
  433. return runBench(opt);
  434. }
  435. if (opt.time) {
  436. return time(opt);
  437. }
  438. if (opt.minified) {
  439. marked = require('../marked.min.js');
  440. }
  441. return runTests(opt);
  442. }
  443. /**
  444. * Execute
  445. */
  446. if (!module.parent) {
  447. process.title = 'marked';
  448. process.exit(main(process.argv.slice()) ? 0 : 1);
  449. } else {
  450. exports = main;
  451. exports.main = main;
  452. exports.runTests = runTests;
  453. exports.testFile = testFile;
  454. exports.runBench = runBench;
  455. exports.load = load;
  456. exports.bench = bench;
  457. module.exports = exports;
  458. }
  459. // returns time to millisecond granularity
  460. function prettyElapsedTime(hrtimeElapsed) {
  461. const seconds = hrtimeElapsed[0];
  462. const frac = Math.round(hrtimeElapsed[1] / 1e3) / 1e3;
  463. return seconds * 1e3 + frac;
  464. }