demo.js 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573
  1. /* globals marked, unfetch, ES6Promise, Promise */ // eslint-disable-line no-redeclare
  2. if (!window.Promise) {
  3. window.Promise = ES6Promise;
  4. }
  5. if (!window.fetch) {
  6. window.fetch = unfetch;
  7. }
  8. onunhandledrejection = function(e) {
  9. throw e.reason;
  10. };
  11. const $loadingElem = document.querySelector('#loading');
  12. const $mainElem = document.querySelector('#main');
  13. const $markdownElem = document.querySelector('#markdown');
  14. const $markedVerElem = document.querySelector('#markedVersion');
  15. const $commitVerElem = document.querySelector('#commitVersion');
  16. let $markedVer = document.querySelector('#markedCdn');
  17. const $optionsElem = document.querySelector('#options');
  18. const $outputTypeElem = document.querySelector('#outputType');
  19. const $inputTypeElem = document.querySelector('#inputType');
  20. const $responseTimeElem = document.querySelector('#responseTime');
  21. const $previewElem = document.querySelector('#preview');
  22. const $previewIframe = document.querySelector('#preview iframe');
  23. const $permalinkElem = document.querySelector('#permalink');
  24. const $clearElem = document.querySelector('#clear');
  25. const $htmlElem = document.querySelector('#html');
  26. const $lexerElem = document.querySelector('#lexer');
  27. const $panes = document.querySelectorAll('.pane');
  28. const $inputPanes = document.querySelectorAll('.inputPane');
  29. let lastInput = '';
  30. let inputDirty = true;
  31. let $activeOutputElem = null;
  32. const search = searchToObject();
  33. const markedVersions = {
  34. master: 'https://cdn.jsdelivr.net/gh/markedjs/marked/marked.min.js'
  35. };
  36. const markedVersionCache = {};
  37. let delayTime = 1;
  38. let checkChangeTimeout = null;
  39. let markedWorker;
  40. $previewIframe.addEventListener('load', handleIframeLoad);
  41. $outputTypeElem.addEventListener('change', handleOutputChange, false);
  42. $inputTypeElem.addEventListener('change', handleInputChange, false);
  43. $markedVerElem.addEventListener('change', handleVersionChange, false);
  44. $markdownElem.addEventListener('change', handleInput, false);
  45. $markdownElem.addEventListener('keyup', handleInput, false);
  46. $markdownElem.addEventListener('keypress', handleInput, false);
  47. $markdownElem.addEventListener('keydown', handleInput, false);
  48. $optionsElem.addEventListener('change', handleInput, false);
  49. $optionsElem.addEventListener('keyup', handleInput, false);
  50. $optionsElem.addEventListener('keypress', handleInput, false);
  51. $optionsElem.addEventListener('keydown', handleInput, false);
  52. $commitVerElem.style.display = 'none';
  53. $commitVerElem.addEventListener('keypress', handleAddVersion, false);
  54. $clearElem.addEventListener('click', handleClearClick, false);
  55. Promise.all([
  56. setInitialQuickref(),
  57. setInitialOutputType(),
  58. setInitialText(),
  59. setInitialVersion()
  60. .then(setInitialOptions)
  61. ]).then(function() {
  62. handleInputChange();
  63. handleOutputChange();
  64. checkForChanges();
  65. setScrollPercent(0);
  66. $loadingElem.style.display = 'none';
  67. $mainElem.style.display = 'block';
  68. });
  69. function setInitialText() {
  70. if ('text' in search) {
  71. $markdownElem.value = search.text;
  72. } else {
  73. return fetch('./initial.md')
  74. .then(function(res) { return res.text(); })
  75. .then(function(text) {
  76. if ($markdownElem.value === '') {
  77. $markdownElem.value = text;
  78. }
  79. });
  80. }
  81. }
  82. function setInitialQuickref() {
  83. return fetch('./quickref.md')
  84. .then(function(res) { return res.text(); })
  85. .then(function(text) {
  86. document.querySelector('#quickref').value = text;
  87. });
  88. }
  89. function setInitialVersion() {
  90. return fetch('https://data.jsdelivr.com/v1/package/npm/marked')
  91. .then(function(res) {
  92. return res.json();
  93. })
  94. .then(function(json) {
  95. for (let i = 0; i < json.versions.length; i++) {
  96. const ver = json.versions[i];
  97. markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver + '/marked.min.js';
  98. const opt = document.createElement('option');
  99. opt.textContent = ver;
  100. opt.value = ver;
  101. $markedVerElem.appendChild(opt);
  102. }
  103. })
  104. .then(function() {
  105. return fetch('https://api.github.com/repos/markedjs/marked/commits')
  106. .then(function(res) {
  107. return res.json();
  108. })
  109. .then(function(json) {
  110. markedVersions.master = 'https://cdn.jsdelivr.net/gh/markedjs/marked@' + json[0].sha + '/marked.min.js';
  111. })
  112. .catch(function() {
  113. // do nothing
  114. // uses url without commit
  115. });
  116. })
  117. .then(function() {
  118. if (search.version) {
  119. if (markedVersions[search.version]) {
  120. return search.version;
  121. } else {
  122. const match = search.version.match(/^(\w+):(.+)$/);
  123. if (match) {
  124. switch (match[1]) {
  125. case 'commit':
  126. addCommitVersion(search.version, match[2].substring(0, 7), match[2]);
  127. return search.version;
  128. case 'pr':
  129. return getPrCommit(match[2])
  130. .then(function(commit) {
  131. if (!commit) {
  132. return 'master';
  133. }
  134. addCommitVersion(search.version, 'PR #' + match[2], commit);
  135. return search.version;
  136. });
  137. }
  138. }
  139. }
  140. }
  141. return 'master';
  142. })
  143. .then(function(version) {
  144. $markedVerElem.value = version;
  145. })
  146. .then(updateVersion);
  147. }
  148. function setInitialOptions() {
  149. if ('options' in search) {
  150. $optionsElem.value = search.options;
  151. } else {
  152. return setDefaultOptions();
  153. }
  154. }
  155. function setInitialOutputType() {
  156. if (search.outputType) {
  157. $outputTypeElem.value = search.outputType;
  158. }
  159. }
  160. function handleIframeLoad() {
  161. lastInput = '';
  162. inputDirty = true;
  163. }
  164. function handleInput() {
  165. inputDirty = true;
  166. }
  167. function handleVersionChange() {
  168. if ($markedVerElem.value === 'commit' || $markedVerElem.value === 'pr') {
  169. $commitVerElem.style.display = '';
  170. } else {
  171. $commitVerElem.style.display = 'none';
  172. updateVersion();
  173. }
  174. }
  175. function handleClearClick() {
  176. $markdownElem.value = '';
  177. $markedVerElem.value = 'master';
  178. $commitVerElem.style.display = 'none';
  179. updateVersion().then(setDefaultOptions);
  180. }
  181. function handleAddVersion(e) {
  182. if (e.which === 13) {
  183. switch ($markedVerElem.value) {
  184. case 'commit': {
  185. const commit = $commitVerElem.value.toLowerCase();
  186. if (!commit.match(/^[0-9a-f]{40}$/)) {
  187. alert('That is not a valid commit');
  188. return;
  189. }
  190. addCommitVersion('commit:' + commit, commit.substring(0, 7), commit);
  191. $markedVerElem.value = 'commit:' + commit;
  192. $commitVerElem.style.display = 'none';
  193. $commitVerElem.value = '';
  194. updateVersion();
  195. break;
  196. }
  197. case 'pr': {
  198. $commitVerElem.disabled = true;
  199. const pr = $commitVerElem.value.replace(/\D/g, '');
  200. getPrCommit(pr)
  201. .then(function(commit) {
  202. $commitVerElem.disabled = false;
  203. if (!commit) {
  204. alert('That is not a valid PR');
  205. return;
  206. }
  207. addCommitVersion('pr:' + pr, 'PR #' + pr, commit);
  208. $markedVerElem.value = 'pr:' + pr;
  209. $commitVerElem.style.display = 'none';
  210. $commitVerElem.value = '';
  211. updateVersion();
  212. });
  213. break;
  214. }
  215. }
  216. }
  217. }
  218. function handleInputChange() {
  219. handleChange($inputPanes, $inputTypeElem.value);
  220. }
  221. function handleOutputChange() {
  222. $activeOutputElem = handleChange($panes, $outputTypeElem.value);
  223. updateLink();
  224. }
  225. function handleChange(panes, visiblePane) {
  226. let active = null;
  227. for (let i = 0; i < panes.length; i++) {
  228. if (panes[i].id === visiblePane) {
  229. panes[i].style.display = '';
  230. active = panes[i];
  231. } else {
  232. panes[i].style.display = 'none';
  233. }
  234. }
  235. return active;
  236. }
  237. function addCommitVersion(value, text, commit) {
  238. if (markedVersions[value]) {
  239. return;
  240. }
  241. markedVersions[value] = 'https://cdn.jsdelivr.net/gh/markedjs/marked@' + commit + '/marked.min.js';
  242. const opt = document.createElement('option');
  243. opt.textContent = text;
  244. opt.value = value;
  245. $markedVerElem.insertBefore(opt, $markedVerElem.firstChild);
  246. }
  247. function getPrCommit(pr) {
  248. return fetch('https://api.github.com/repos/markedjs/marked/pulls/' + pr + '/commits')
  249. .then(function(res) {
  250. return res.json();
  251. })
  252. .then(function(json) {
  253. return json[json.length - 1].sha;
  254. }).catch(function() {
  255. // return undefined
  256. });
  257. }
  258. function setDefaultOptions() {
  259. if (window.Worker) {
  260. return messageWorker({
  261. task: 'defaults',
  262. version: markedVersions[$markedVerElem.value]
  263. });
  264. } else {
  265. const defaults = marked.getDefaults();
  266. setOptions(defaults);
  267. }
  268. }
  269. function setOptions(opts) {
  270. $optionsElem.value = JSON.stringify(
  271. opts,
  272. function(key, value) {
  273. if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
  274. return undefined;
  275. }
  276. return value;
  277. }, ' ');
  278. }
  279. function searchToObject() {
  280. // modified from https://stackoverflow.com/a/7090123/806777
  281. const pairs = location.search.slice(1).split('&');
  282. const obj = {};
  283. for (let i = 0; i < pairs.length; i++) {
  284. if (pairs[i] === '') {
  285. continue;
  286. }
  287. const pair = pairs[i].split('=');
  288. obj[decodeURIComponent(pair.shift())] = decodeURIComponent(pair.join('='));
  289. }
  290. return obj;
  291. }
  292. function isArray(arr) {
  293. return Object.prototype.toString.call(arr) === '[object Array]';
  294. }
  295. function jsonString(input, level) {
  296. level = level || 0;
  297. if (isArray(input)) {
  298. if (input.length === 0) {
  299. return '[]';
  300. }
  301. const items = [];
  302. let i;
  303. if (!isArray(input[0]) && typeof input[0] === 'object' && input[0] !== null) {
  304. for (i = 0; i < input.length; i++) {
  305. items.push(' '.repeat(2 * level) + jsonString(input[i], level + 1));
  306. }
  307. return '[\n' + items.join('\n') + '\n]';
  308. }
  309. for (i = 0; i < input.length; i++) {
  310. items.push(jsonString(input[i], level));
  311. }
  312. return '[' + items.join(', ') + ']';
  313. } else if (typeof input === 'object' && input !== null) {
  314. const props = [];
  315. for (const prop in input) {
  316. props.push(prop + ':' + jsonString(input[prop], level));
  317. }
  318. return '{' + props.join(', ') + '}';
  319. } else {
  320. return JSON.stringify(input);
  321. }
  322. }
  323. function getScrollSize() {
  324. const e = $activeOutputElem;
  325. return e.scrollHeight - e.clientHeight;
  326. }
  327. function getScrollPercent() {
  328. const size = getScrollSize();
  329. if (size <= 0) {
  330. return 1;
  331. }
  332. return $activeOutputElem.scrollTop / size;
  333. }
  334. function setScrollPercent(percent) {
  335. $activeOutputElem.scrollTop = percent * getScrollSize();
  336. }
  337. function updateLink() {
  338. let outputType = '';
  339. if ($outputTypeElem.value !== 'preview') {
  340. outputType = 'outputType=' + $outputTypeElem.value + '&';
  341. }
  342. $permalinkElem.href = '?' + outputType + 'text=' + encodeURIComponent($markdownElem.value)
  343. + '&options=' + encodeURIComponent($optionsElem.value)
  344. + '&version=' + encodeURIComponent($markedVerElem.value);
  345. history.replaceState('', document.title, $permalinkElem.href);
  346. }
  347. function updateVersion() {
  348. if (window.Worker) {
  349. handleInput();
  350. return Promise.resolve();
  351. }
  352. let promise;
  353. if (markedVersionCache[$markedVerElem.value]) {
  354. promise = Promise.resolve(markedVersionCache[$markedVerElem.value]);
  355. } else {
  356. promise = fetch(markedVersions[$markedVerElem.value])
  357. .then(function(res) { return res.text(); })
  358. .then(function(text) {
  359. markedVersionCache[$markedVerElem.value] = text;
  360. return text;
  361. });
  362. }
  363. return promise.then(function(text) {
  364. const script = document.createElement('script');
  365. script.textContent = text;
  366. $markedVer.parentNode.replaceChild(script, $markedVer);
  367. $markedVer = script;
  368. }).then(handleInput);
  369. }
  370. function checkForChanges() {
  371. if (inputDirty && $markedVerElem.value !== 'commit' && $markedVerElem.value !== 'pr' && (typeof marked !== 'undefined' || window.Worker)) {
  372. inputDirty = false;
  373. updateLink();
  374. let options = {};
  375. const optionsString = $optionsElem.value || '{}';
  376. try {
  377. const newOptions = JSON.parse(optionsString);
  378. options = newOptions;
  379. $optionsElem.classList.remove('error');
  380. } catch (err) {
  381. $optionsElem.classList.add('error');
  382. }
  383. const version = markedVersions[$markedVerElem.value];
  384. const markdown = $markdownElem.value;
  385. const hash = version + markdown + optionsString;
  386. if (lastInput !== hash) {
  387. lastInput = hash;
  388. if (window.Worker) {
  389. delayTime = 100;
  390. messageWorker({
  391. task: 'parse',
  392. version,
  393. markdown,
  394. options
  395. });
  396. } else {
  397. const startTime = new Date();
  398. const lexed = marked.lexer(markdown, options);
  399. const lexedList = jsonString(lexed);
  400. const parsed = marked.parser(lexed, options);
  401. const endTime = new Date();
  402. $previewElem.classList.remove('error');
  403. $htmlElem.classList.remove('error');
  404. $lexerElem.classList.remove('error');
  405. const scrollPercent = getScrollPercent();
  406. setParsed(parsed, lexedList);
  407. setScrollPercent(scrollPercent);
  408. delayTime = endTime - startTime;
  409. setResponseTime(delayTime);
  410. if (delayTime < 50) {
  411. delayTime = 50;
  412. } else if (delayTime > 500) {
  413. delayTime = 1000;
  414. }
  415. }
  416. }
  417. }
  418. checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
  419. }
  420. function setResponseTime(ms) {
  421. let amount = ms;
  422. let suffix = 'ms';
  423. if (ms > 1000 * 60 * 60) {
  424. amount = 'Too Long';
  425. suffix = '';
  426. } else if (ms > 1000 * 60) {
  427. amount = '>' + Math.floor(ms / (1000 * 60));
  428. suffix = 'm';
  429. } else if (ms > 1000) {
  430. amount = '>' + Math.floor(ms / 1000);
  431. suffix = 's';
  432. }
  433. $responseTimeElem.textContent = amount + suffix;
  434. }
  435. function setParsed(parsed, lexed) {
  436. try {
  437. $previewIframe.contentDocument.body.innerHTML = parsed;
  438. } catch (ex) {}
  439. $htmlElem.value = parsed;
  440. $lexerElem.value = lexed;
  441. }
  442. const workerPromises = {};
  443. function messageWorker(message) {
  444. if (!markedWorker || markedWorker.working) {
  445. if (markedWorker) {
  446. clearTimeout(markedWorker.timeout);
  447. markedWorker.terminate();
  448. }
  449. markedWorker = new Worker('worker.js');
  450. markedWorker.onmessage = function(e) {
  451. clearTimeout(markedWorker.timeout);
  452. markedWorker.working = false;
  453. switch (e.data.task) {
  454. case 'defaults': {
  455. setOptions(e.data.defaults);
  456. break;
  457. }
  458. case 'parse': {
  459. $previewElem.classList.remove('error');
  460. $htmlElem.classList.remove('error');
  461. $lexerElem.classList.remove('error');
  462. const scrollPercent = getScrollPercent();
  463. setParsed(e.data.parsed, e.data.lexed);
  464. setScrollPercent(scrollPercent);
  465. setResponseTime(e.data.time);
  466. break;
  467. }
  468. }
  469. clearTimeout(checkChangeTimeout);
  470. delayTime = 10;
  471. checkForChanges();
  472. workerPromises[e.data.id]();
  473. delete workerPromises[e.data.id];
  474. };
  475. markedWorker.onerror = markedWorker.onmessageerror = function(err) {
  476. clearTimeout(markedWorker.timeout);
  477. let error = 'There was an error in the Worker';
  478. if (err) {
  479. if (err.message) {
  480. error = err.message;
  481. } else {
  482. error = err;
  483. }
  484. }
  485. error = error.replace(/^Uncaught Error: /, '');
  486. $previewElem.classList.add('error');
  487. $htmlElem.classList.add('error');
  488. $lexerElem.classList.add('error');
  489. setParsed(error, error);
  490. setScrollPercent(0);
  491. };
  492. }
  493. if (message.task !== 'defaults') {
  494. markedWorker.working = true;
  495. workerTimeout(0);
  496. }
  497. return new Promise(function(resolve) {
  498. message.id = uniqueWorkerMessageId();
  499. workerPromises[message.id] = resolve;
  500. markedWorker.postMessage(message);
  501. });
  502. }
  503. function uniqueWorkerMessageId() {
  504. let id;
  505. do {
  506. id = Math.random().toString(36);
  507. } while (id in workerPromises);
  508. return id;
  509. }
  510. function workerTimeout(seconds) {
  511. markedWorker.timeout = setTimeout(function() {
  512. seconds++;
  513. markedWorker.onerror('Marked has taken longer than ' + seconds + ' second' + (seconds > 1 ? 's' : '') + ' to respond...');
  514. workerTimeout(seconds);
  515. }, 1000);
  516. }