demo.js 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396
  1. onunhandledrejection = (e) => {
  2. throw e.reason;
  3. };
  4. const $loadingElem = document.querySelector('#loading');
  5. const $mainElem = document.querySelector('#main');
  6. const $markdownElem = document.querySelector('#markdown');
  7. const $markedVerElem = document.querySelector('#markedVersion');
  8. const $optionsElem = document.querySelector('#options');
  9. const $outputTypeElem = document.querySelector('#outputType');
  10. const $inputTypeElem = document.querySelector('#inputType');
  11. const $responseTimeElem = document.querySelector('#responseTime');
  12. const $previewElem = document.querySelector('#preview');
  13. const $previewIframe = document.querySelector('#preview iframe');
  14. const $permalinkElem = document.querySelector('#permalink');
  15. const $clearElem = document.querySelector('#clear');
  16. const $htmlElem = document.querySelector('#html');
  17. const $lexerElem = document.querySelector('#lexer');
  18. const $panes = document.querySelectorAll('.pane');
  19. const $inputPanes = document.querySelectorAll('.inputPane');
  20. let lastInput = '';
  21. let inputDirty = true;
  22. let $activeOutputElem = null;
  23. let latestVersion = 'master';
  24. const search = searchToObject();
  25. const markedVersions = {
  26. master: '../',
  27. };
  28. let delayTime = 1;
  29. let checkChangeTimeout = null;
  30. let markedWorker;
  31. $previewIframe.addEventListener('load', handleIframeLoad);
  32. $outputTypeElem.addEventListener('change', handleOutputChange, false);
  33. $inputTypeElem.addEventListener('change', handleInputChange, false);
  34. $markedVerElem.addEventListener('change', handleVersionChange, false);
  35. $markdownElem.addEventListener('change', handleInput, false);
  36. $markdownElem.addEventListener('keyup', handleInput, false);
  37. $markdownElem.addEventListener('keypress', handleInput, false);
  38. $markdownElem.addEventListener('keydown', handleInput, false);
  39. $optionsElem.addEventListener('change', handleInput, false);
  40. $optionsElem.addEventListener('keyup', handleInput, false);
  41. $optionsElem.addEventListener('keypress', handleInput, false);
  42. $optionsElem.addEventListener('keydown', handleInput, false);
  43. $clearElem.addEventListener('click', handleClearClick, false);
  44. Promise.all([
  45. setInitialQuickref(),
  46. setInitialOutputType(),
  47. setInitialText(),
  48. setInitialVersion()
  49. .then(setInitialOptions),
  50. ]).then(() => {
  51. handleInputChange();
  52. handleOutputChange();
  53. checkForChanges();
  54. setScrollPercent(0);
  55. $loadingElem.style.display = 'none';
  56. $mainElem.style.display = 'block';
  57. }).catch(() => {
  58. $loadingElem.classList.add('loadingError');
  59. $loadingElem.textContent = 'Failed to load marked. Refresh the page to try again.';
  60. });
  61. function setInitialText() {
  62. if ('text' in search) {
  63. $markdownElem.value = search.text;
  64. } else {
  65. return fetch('./initial.md')
  66. .then(res => res.text())
  67. .then(text => {
  68. if ($markdownElem.value === '') {
  69. $markdownElem.value = text;
  70. }
  71. });
  72. }
  73. }
  74. function setInitialQuickref() {
  75. return fetch('./quickref.md')
  76. .then(res => res.text())
  77. .then(text => {
  78. document.querySelector('#quickref').value = text;
  79. });
  80. }
  81. function setInitialVersion() {
  82. return fetch('https://data.jsdelivr.com/v1/package/npm/marked')
  83. .then(res => res.json())
  84. .then(json => {
  85. for (const ver of json.versions) {
  86. markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver;
  87. const opt = document.createElement('option');
  88. opt.textContent = ver;
  89. opt.value = ver;
  90. $markedVerElem.appendChild(opt);
  91. }
  92. if (location.host === 'marked.js.org') {
  93. latestVersion = json.tags.latest;
  94. } else {
  95. $markedVerElem.querySelector('option[value="master"]').textContent = 'This Build';
  96. }
  97. if (search.version && markedVersions[search.version]) {
  98. $markedVerElem.value = search.version;
  99. return;
  100. }
  101. $markedVerElem.value = latestVersion;
  102. })
  103. .then(updateVersion);
  104. }
  105. function setInitialOptions() {
  106. if ('options' in search) {
  107. $optionsElem.value = search.options;
  108. } else {
  109. return setDefaultOptions();
  110. }
  111. }
  112. function setInitialOutputType() {
  113. if (search.outputType) {
  114. $outputTypeElem.value = search.outputType;
  115. }
  116. }
  117. function handleIframeLoad() {
  118. lastInput = '';
  119. inputDirty = true;
  120. }
  121. function handleInput() {
  122. inputDirty = true;
  123. }
  124. function handleVersionChange() {
  125. updateVersion();
  126. }
  127. function handleClearClick() {
  128. $markdownElem.value = '';
  129. $markedVerElem.value = latestVersion;
  130. updateVersion();
  131. setDefaultOptions();
  132. }
  133. function handleInputChange() {
  134. handleChange($inputPanes, $inputTypeElem.value);
  135. }
  136. function handleOutputChange() {
  137. $activeOutputElem = handleChange($panes, $outputTypeElem.value);
  138. updateLink();
  139. }
  140. function handleChange(panes, visiblePane) {
  141. let active = null;
  142. for (let i = 0; i < panes.length; i++) {
  143. if (panes[i].id === visiblePane) {
  144. panes[i].style.display = '';
  145. active = panes[i];
  146. } else {
  147. panes[i].style.display = 'none';
  148. }
  149. }
  150. return active;
  151. }
  152. function setDefaultOptions() {
  153. return messageWorker({
  154. task: 'defaults',
  155. version: markedVersions[$markedVerElem.value],
  156. });
  157. }
  158. function setOptions(opts) {
  159. $optionsElem.value = JSON.stringify(
  160. opts,
  161. (key, value) => {
  162. if (value !== null && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) {
  163. return undefined;
  164. }
  165. return value;
  166. }, ' ');
  167. }
  168. function searchToObject() {
  169. // modified from https://stackoverflow.com/a/7090123/806777
  170. const pairs = location.search.slice(1).split('&');
  171. const obj = {};
  172. for (let i = 0; i < pairs.length; i++) {
  173. if (pairs[i] === '') {
  174. continue;
  175. }
  176. const pair = pairs[i].split('=');
  177. obj[decodeURIComponent(pair.shift())] = decodeURIComponent(pair.join('='));
  178. }
  179. return obj;
  180. }
  181. function getScrollSize() {
  182. if (!$activeOutputElem) {
  183. return 0;
  184. }
  185. const e = $activeOutputElem;
  186. return e.scrollHeight - e.clientHeight;
  187. }
  188. function getScrollPercent() {
  189. if (!$activeOutputElem) {
  190. return 1;
  191. }
  192. const size = getScrollSize();
  193. if (size <= 0) {
  194. return 1;
  195. }
  196. return $activeOutputElem.scrollTop / size;
  197. }
  198. function setScrollPercent(percent) {
  199. if ($activeOutputElem) {
  200. $activeOutputElem.scrollTop = percent * getScrollSize();
  201. }
  202. }
  203. function updateLink() {
  204. let outputType = '';
  205. if ($outputTypeElem.value !== 'preview') {
  206. outputType = 'outputType=' + $outputTypeElem.value + '&';
  207. }
  208. $permalinkElem.href = '?' + outputType + 'text=' + encodeURIComponent($markdownElem.value)
  209. + '&options=' + encodeURIComponent($optionsElem.value)
  210. + '&version=' + encodeURIComponent($markedVerElem.value);
  211. history.replaceState('', document.title, $permalinkElem.href);
  212. }
  213. function updateVersion() {
  214. handleInput();
  215. }
  216. function checkForChanges() {
  217. if (inputDirty && $markedVerElem.value !== 'pr') {
  218. inputDirty = false;
  219. updateLink();
  220. let options = {};
  221. const optionsString = $optionsElem.value || '{}';
  222. try {
  223. const newOptions = JSON.parse(optionsString);
  224. options = newOptions;
  225. $optionsElem.classList.remove('error');
  226. } catch {
  227. $optionsElem.classList.add('error');
  228. }
  229. const version = markedVersions[$markedVerElem.value];
  230. const markdown = $markdownElem.value;
  231. const hash = version + markdown + optionsString;
  232. if (lastInput !== hash) {
  233. lastInput = hash;
  234. delayTime = 100;
  235. messageWorker({
  236. task: 'parse',
  237. version,
  238. markdown,
  239. options,
  240. });
  241. }
  242. }
  243. checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
  244. }
  245. function setResponseTime(ms) {
  246. let amount = ms;
  247. let suffix = 'ms';
  248. if (ms > 1000 * 60 * 60) {
  249. amount = 'Too Long';
  250. suffix = '';
  251. } else if (ms > 1000 * 60) {
  252. amount = '>' + Math.floor(ms / (1000 * 60));
  253. suffix = 'm';
  254. } else if (ms > 1000) {
  255. amount = '>' + Math.floor(ms / 1000);
  256. suffix = 's';
  257. }
  258. $responseTimeElem.textContent = amount + suffix;
  259. $responseTimeElem.animate([
  260. { transform: 'scale(1.2)' },
  261. { transform: 'scale(1)' },
  262. ], 200);
  263. }
  264. function setParsed(parsed, lexed) {
  265. try {
  266. $previewIframe.contentDocument.body.innerHTML = parsed;
  267. } catch {}
  268. $htmlElem.value = parsed;
  269. $lexerElem.value = lexed;
  270. }
  271. const workerPromises = {};
  272. function messageWorker(message) {
  273. if (!markedWorker || markedWorker.working) {
  274. if (markedWorker) {
  275. clearTimeout(markedWorker.timeout);
  276. markedWorker.terminate();
  277. }
  278. markedWorker = new Worker('worker.js');
  279. markedWorker.onmessage = (e) => {
  280. clearTimeout(markedWorker.timeout);
  281. markedWorker.working = false;
  282. switch (e.data.task) {
  283. case 'defaults': {
  284. setOptions(e.data.defaults);
  285. break;
  286. }
  287. case 'parse': {
  288. $previewElem.classList.remove('error');
  289. $htmlElem.classList.remove('error');
  290. $lexerElem.classList.remove('error');
  291. const scrollPercent = getScrollPercent();
  292. setParsed(e.data.parsed, e.data.lexed);
  293. setScrollPercent(scrollPercent);
  294. setResponseTime(e.data.time);
  295. break;
  296. }
  297. }
  298. clearTimeout(checkChangeTimeout);
  299. delayTime = 10;
  300. checkForChanges();
  301. workerPromises[e.data.id]();
  302. delete workerPromises[e.data.id];
  303. };
  304. markedWorker.onerror = markedWorker.onmessageerror = (err) => {
  305. clearTimeout(markedWorker.timeout);
  306. let error = 'There was an error in the Worker';
  307. if (err) {
  308. if (err.message) {
  309. error = err.message;
  310. } else {
  311. error = err;
  312. }
  313. }
  314. error = error.replace(/^Uncaught Error: /, '');
  315. $previewElem.classList.add('error');
  316. $htmlElem.classList.add('error');
  317. $lexerElem.classList.add('error');
  318. setParsed(error, error);
  319. setScrollPercent(0);
  320. };
  321. }
  322. if (message.task !== 'defaults') {
  323. markedWorker.working = true;
  324. workerTimeout(0);
  325. }
  326. return new Promise(resolve => {
  327. message.id = uniqueWorkerMessageId();
  328. workerPromises[message.id] = resolve;
  329. markedWorker.postMessage(message);
  330. });
  331. }
  332. function uniqueWorkerMessageId() {
  333. let id;
  334. do {
  335. id = Math.random().toString(36);
  336. } while (id in workerPromises);
  337. return id;
  338. }
  339. function workerTimeout(seconds) {
  340. markedWorker.timeout = setTimeout(() => {
  341. seconds++;
  342. markedWorker.onerror('Marked has taken longer than ' + seconds + ' second' + (seconds > 1 ? 's' : '') + ' to respond...');
  343. workerTimeout(seconds);
  344. }, 1000);
  345. }