diff --git a/docs/demo/demo.css b/docs/demo/demo.css index 92d2f4a504..398c663a51 100644 --- a/docs/demo/demo.css +++ b/docs/demo/demo.css @@ -62,7 +62,11 @@ header h1 { flex-grow: 1; } -#options.badParse { +#main { + display: none; +} + +.error { border-color: red; background-color: #FEE } diff --git a/docs/demo/demo.js b/docs/demo/demo.js index 3762cb3819..de6882a5be 100644 --- a/docs/demo/demo.js +++ b/docs/demo/demo.js @@ -7,12 +7,21 @@ if (!window.fetch) { window.fetch = unfetch; } +onunhandledrejection = function (e) { + throw e.reason; +}; + +var $loadingElem = document.querySelector('#loading'); +var $mainElem = document.querySelector('#main'); var $markdownElem = document.querySelector('#markdown'); var $markedVerElem = document.querySelector('#markedVersion'); +var $commitVerElem = document.querySelector('#commitVersion'); var $markedVer = document.querySelector('#markedCdn'); var $optionsElem = document.querySelector('#options'); var $outputTypeElem = document.querySelector('#outputType'); var $inputTypeElem = document.querySelector('#inputType'); +var $responseTimeElem = document.querySelector('#responseTime'); +var $previewElem = document.querySelector('#preview'); var $previewIframe = document.querySelector('#preview iframe'); var $permalinkElem = document.querySelector('#permalink'); var $clearElem = document.querySelector('#clear'); @@ -20,81 +29,197 @@ var $htmlElem = document.querySelector('#html'); var $lexerElem = document.querySelector('#lexer'); var $panes = document.querySelectorAll('.pane'); var $inputPanes = document.querySelectorAll('.inputPane'); +var lastInput = ''; var inputDirty = true; var $activeOutputElem = null; var search = searchToObject(); - var markedVersions = { master: 'https://cdn.jsdelivr.net/gh/markedjs/marked/lib/marked.js' }; var markedVersionCache = {}; +var delayTime = 1; +var checkChangeTimeout = null; +var markedWorker; -var iframeLoaded = false; -$previewIframe.addEventListener('load', function () { - iframeLoaded = true; - inputDirty = true; +$previewIframe.addEventListener('load', handleIframeLoad); + +$outputTypeElem.addEventListener('change', handleOutputChange, false); + +$inputTypeElem.addEventListener('change', handleInputChange, false); + +$markedVerElem.addEventListener('change', handleVersionChange, false); + +$markdownElem.addEventListener('change', handleInput, false); +$markdownElem.addEventListener('keyup', handleInput, false); +$markdownElem.addEventListener('keypress', handleInput, false); +$markdownElem.addEventListener('keydown', handleInput, false); + +$optionsElem.addEventListener('change', handleInput, false); +$optionsElem.addEventListener('keyup', handleInput, false); +$optionsElem.addEventListener('keypress', handleInput, false); +$optionsElem.addEventListener('keydown', handleInput, false); + +$commitVerElem.style.display = 'none'; +$commitVerElem.addEventListener('keypress', handleAddVersion, false); + +$clearElem.addEventListener('click', handleClearClick, false); + +Promise.all([ + setInitialQuickref(), + setInitialOutputType(), + setInitialText(), + setInitialVersion() + .then(setInitialOptions) +]).then(function () { + handleInputChange(); + handleOutputChange(); + checkForChanges(); + setScrollPercent(0); + $loadingElem.style.display = 'none'; + $mainElem.style.display = 'block'; }); -if ('text' in search && search.text) { - $markdownElem.value = search.text; -} else { - fetch('./initial.md') +function setInitialText() { + if ('text' in search) { + $markdownElem.value = search.text; + } else { + return fetch('./initial.md') + .then(function (res) { return res.text(); }) + .then(function (text) { + if ($markdownElem.value === '') { + $markdownElem.value = text; + } + }); + } +} + +function setInitialQuickref() { + return fetch('./quickref.md') .then(function (res) { return res.text(); }) .then(function (text) { - if ($markdownElem.value === '') { - $markdownElem.value = text; - inputDirty = true; - setScrollPercent(0); - } + document.querySelector('#quickref').value = text; }); } -fetch('https://data.jsdelivr.com/v1/package/npm/marked') - .then(function (res) { - return res.json(); - }) - .then(function (json) { - for (var i = 0; i < json.versions.length; i++) { - var ver = json.versions[i]; - markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver + '/lib/marked.js'; - var opt = document.createElement('option'); - opt.textContent = ver; - opt.value = ver; - $markedVerElem.appendChild(opt); - } - }) - .then(function () { - if ('version' in search && search.version) { - $markedVerElem.value = search.version; - } else { - $markedVerElem.value = 'master'; - } - - updateVersion().then(function () { - if ('options' in search && search.options) { - $optionsElem.value = search.options; - } else { - $optionsElem.value = JSON.stringify( - marked.getDefaults(), - function (key, value) { - if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) { - return undefined; +function setInitialVersion() { + return fetch('https://data.jsdelivr.com/v1/package/npm/marked') + .then(function (res) { + return res.json(); + }) + .then(function (json) { + for (var i = 0; i < json.versions.length; i++) { + var ver = json.versions[i]; + markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver + '/lib/marked.js'; + var opt = document.createElement('option'); + opt.textContent = ver; + opt.value = ver; + $markedVerElem.appendChild(opt); + } + }) + .then(function () { + if (search.version) { + if (!markedVersions[search.version]) { + var match = search.version.match(/^(\w+):(.+)$/); + if (match) { + switch (match[1]) { + case 'commit': + addCommitVersion(search.version, match[2].substring(0, 7), match[2]); + return search.version; + case 'pr': + return getPrCommit(match[2]) + .then(function (commit) { + if (!commit) { + return 'master'; + } + addCommitVersion(search.version, 'PR #' + match[2], commit); + return search.version; + }); } - return value; - }, ' '); + } + } } - }); - }); -if (search.outputType) { - $outputTypeElem.value = search.outputType; + return 'master'; + }) + .then(function (version) { + $markedVerElem.value = version; + }) + .then(updateVersion); +} + +function setInitialOptions() { + if ('options' in search) { + $optionsElem.value = search.options; + } else { + setDefaultOptions(); + } +} + +function setInitialOutputType() { + if (search.outputType) { + $outputTypeElem.value = search.outputType; + } +} + +function handleIframeLoad() { + lastInput = ''; + inputDirty = true; +} + +function handleInput() { + inputDirty = true; +}; + +function handleVersionChange() { + if ($markedVerElem.value === 'commit' || $markedVerElem.value === 'pr') { + $commitVerElem.style.display = ''; + } else { + $commitVerElem.style.display = 'none'; + updateVersion(); + } +} + +function handleClearClick() { + $markdownElem.value = ''; + $markedVerElem.value = 'master'; + $commitVerElem.style.display = 'none'; + updateVersion().then(setDefaultOptions); } -fetch('./quickref.md') - .then(function (res) { return res.text(); }) - .then(function (text) { - document.querySelector('#quickref').value = text; - }); +function handleAddVersion(e) { + if (e.which === 13) { + switch ($markedVerElem.value) { + case 'commit': + var commit = $commitVerElem.value.toLowerCase(); + if (!commit.match(/^[0-9a-f]{40}$/)) { + alert('That is not a valid commit'); + return; + } + addCommitVersion('commit:' + commit, commit.substring(0, 7), commit); + $markedVerElem.value = 'commit:' + commit; + $commitVerElem.style.display = 'none'; + $commitVerElem.value = ''; + updateVersion(); + break; + case 'pr': + $commitVerElem.disabled = true; + var pr = $commitVerElem.value.replace(/\D/g, ''); + getPrCommit(pr) + .then(function (commit) { + $commitVerElem.disabled = false; + if (!commit) { + alert('That is not a valid PR'); + return; + } + addCommitVersion('pr:' + pr, 'PR #' + pr, commit); + $markedVerElem.value = 'pr:' + pr; + $commitVerElem.style.display = 'none'; + $commitVerElem.value = ''; + updateVersion(); + }); + } + } +} function handleInputChange() { handleChange($inputPanes, $inputTypeElem.value); @@ -118,40 +243,51 @@ function handleChange(panes, visiblePane) { return active; }; -$outputTypeElem.addEventListener('change', handleOutputChange, false); -handleOutputChange(); -$inputTypeElem.addEventListener('change', handleInputChange, false); -handleInputChange(); -$markedVerElem.addEventListener('change', updateVersion, false); - -function handleInput() { - inputDirty = true; -}; +function addCommitVersion(value, text, commit) { + if (markedVersions[value]) { + return; + } + markedVersions[value] = 'https://cdn.jsdelivr.net/gh/markedjs/marked@' + commit + '/lib/marked.js'; + var opt = document.createElement('option'); + opt.textContent = text; + opt.value = value; + $markedVerElem.insertBefore(opt, $markedVerElem.firstChild); +} -$markdownElem.addEventListener('change', handleInput, false); -$markdownElem.addEventListener('keyup', handleInput, false); -$markdownElem.addEventListener('keypress', handleInput, false); -$markdownElem.addEventListener('keydown', handleInput, false); +function getPrCommit(pr) { + return fetch('https://api.github.com/repos/markedjs/marked/pulls/' + pr + '/commits') + .then(function (res) { + return res.json(); + }) + .then(function (json) { + return json[json.length - 1].sha; + }).catch(function () { + // return undefined + }); +} -$optionsElem.addEventListener('change', handleInput, false); -$optionsElem.addEventListener('keyup', handleInput, false); -$optionsElem.addEventListener('keypress', handleInput, false); -$optionsElem.addEventListener('keydown', handleInput, false); +function setDefaultOptions() { + if (window.Worker) { + messageWorker({ + task: 'defaults', + version: markedVersions[$markedVerElem.value] + }); + } else { + var defaults = marked.getDefaults(); + setOptions(defaults); + } +} -$clearElem.addEventListener('click', function () { - $markdownElem.value = ''; - $markedVerElem.value = 'master'; - updateVersion().then(function () { - $optionsElem.value = JSON.stringify( - marked.getDefaults(), - function (key, value) { - if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) { - return undefined; - } - return value; - }, ' '); - }); -}, false); +function setOptions(opts) { + $optionsElem.value = JSON.stringify( + opts, + function (key, value) { + if (value && typeof value === 'object' && Object.getPrototypeOf(value) !== Object.prototype) { + return undefined; + } + return value; + }, ' '); +} function searchToObject() { // modified from https://stackoverflow.com/a/7090123/806777 @@ -187,6 +323,7 @@ function getScrollSize() { return e.scrollHeight - e.clientHeight; }; + function getScrollPercent() { var size = getScrollSize(); @@ -196,6 +333,7 @@ function getScrollPercent() { return $activeOutputElem.scrollTop / size; }; + function setScrollPercent(percent) { $activeOutputElem.scrollTop = percent * getScrollSize(); }; @@ -213,8 +351,12 @@ function updateLink() { } function updateVersion() { + if (window.Worker) { + handleInput(); + return Promise.resolve(); + } var promise; - if ($markedVerElem.value in markedVersionCache) { + if (markedVersionCache[$markedVerElem.value]) { promise = Promise.resolve(markedVersionCache[$markedVerElem.value]); } else { promise = fetch(markedVersions[$markedVerElem.value]) @@ -233,58 +375,145 @@ function updateVersion() { }).then(handleInput); } -var delayTime = 1; -var options = {}; function checkForChanges() { - if (inputDirty && typeof marked !== 'undefined') { + if (inputDirty && $markedVerElem.value !== 'commit' && $markedVerElem.value !== 'pr' && (typeof marked !== 'undefined' || window.Worker)) { inputDirty = false; updateLink(); - var startTime = new Date(); - - var scrollPercent = getScrollPercent(); - + var options = {}; + var optionsString = $optionsElem.value || '{}'; try { - var optionsString = $optionsElem.value || '{}'; var newOptions = JSON.parse(optionsString); options = newOptions; - $optionsElem.classList.remove('badParse'); + $optionsElem.classList.remove('error'); } catch (err) { - $optionsElem.classList.add('badParse'); + $optionsElem.classList.add('error'); } - var lexed = marked.lexer($markdownElem.value, options); - - var lexedList = []; - - for (var i = 0; i < lexed.length; i++) { - var lexedLine = []; - for (var j in lexed[i]) { - lexedLine.push(j + ':' + jsonString(lexed[i][j])); + var version = markedVersions[$markedVerElem.value]; + var markdown = $markdownElem.value; + var hash = version + markdown + optionsString; + if (lastInput !== hash) { + lastInput = hash; + if (window.Worker) { + delayTime = 100; + messageWorker({ + task: 'parse', + version: version, + markdown: markdown, + options: options + }); + } else { + var startTime = new Date(); + var lexed = marked.lexer(markdown, options); + var lexedList = []; + for (var i = 0; i < lexed.length; i++) { + var lexedLine = []; + for (var j in lexed[i]) { + lexedLine.push(j + ':' + jsonString(lexed[i][j])); + } + lexedList.push('{' + lexedLine.join(', ') + '}'); + } + var parsed = marked.parser(lexed, options); + var scrollPercent = getScrollPercent(); + setParsed(parsed, lexedList.join('\n')); + setScrollPercent(scrollPercent); + var endTime = new Date(); + delayTime = endTime - startTime; + setResponseTime(delayTime); + if (delayTime < 50) { + delayTime = 50; + } else if (delayTime > 500) { + delayTime = 1000; + } } - lexedList.push('{' + lexedLine.join(', ') + '}'); } + } + checkChangeTimeout = window.setTimeout(checkForChanges, delayTime); +}; - var parsed = marked.parser(lexed, options); - - if (iframeLoaded) { - $previewIframe.contentDocument.body.innerHTML = (parsed); - } - $htmlElem.value = (parsed); - $lexerElem.value = (lexedList.join('\n')); +function setResponseTime(ms) { + var amount = ms; + var suffix = 'ms'; + if (ms > 1000 * 60 * 60) { + amount = 'Too Long'; + suffix = ''; + } else if (ms > 1000 * 60) { + amount = '>' + Math.floor(ms / (1000 * 60)); + suffix = 'm'; + } else if (ms > 1000) { + amount = '>' + Math.floor(ms / 1000); + suffix = 's'; + } + $responseTimeElem.textContent = amount + suffix; +} - setScrollPercent(scrollPercent); +function setParsed(parsed, lexed) { + try { + $previewIframe.contentDocument.body.innerHTML = parsed; + } catch (ex) {} + $htmlElem.value = parsed; + $lexerElem.value = lexed; +} - var endTime = new Date(); - delayTime = endTime - startTime; - if (delayTime < 50) { - delayTime = 50; - } else if (delayTime > 500) { - delayTime = 1000; +function messageWorker(message) { + if (!markedWorker || markedWorker.working) { + if (markedWorker) { + clearTimeout(markedWorker.timeout); + markedWorker.terminate(); } + markedWorker = new Worker('worker.js'); + markedWorker.onmessage = function (e) { + clearTimeout(markedWorker.timeout); + markedWorker.working = false; + switch (e.data.task) { + case 'defaults': + setOptions(e.data.defaults); + break; + case 'parse': + $previewElem.classList.remove('error'); + $htmlElem.classList.remove('error'); + $lexerElem.classList.remove('error'); + var scrollPercent = getScrollPercent(); + setParsed(e.data.parsed, e.data.lexed); + setScrollPercent(scrollPercent); + setResponseTime(e.data.time); + break; + } + clearTimeout(checkChangeTimeout); + delayTime = 10; + checkForChanges(); + }; + markedWorker.onerror = markedWorker.onmessageerror = function (err) { + clearTimeout(markedWorker.timeout); + var error = 'There was an error in the Worker'; + if (err) { + if (err.message) { + error = err.message; + } else { + error = err; + } + } + error = error.replace(/^Uncaught Error: /, ''); + $previewElem.classList.add('error'); + $htmlElem.classList.add('error'); + $lexerElem.classList.add('error'); + setParsed(error, error); + setScrollPercent(0); + }; } - window.setTimeout(checkForChanges, delayTime); -}; -checkForChanges(); -setScrollPercent(0); + if (message.task !== 'defaults') { + markedWorker.working = true; + workerTimeout(0); + } + markedWorker.postMessage(message); +} + +function workerTimeout(seconds) { + markedWorker.timeout = setTimeout(function () { + seconds++; + markedWorker.onerror('Marked has taken longer than ' + seconds + ' second' + (seconds > 1 ? 's' : '') + ' to respond...'); + workerTimeout(seconds); + }, 1000); +} diff --git a/docs/demo/index.html b/docs/demo/index.html index 115b26696e..96a8ec14b1 100644 --- a/docs/demo/index.html +++ b/docs/demo/index.html @@ -18,53 +18,55 @@

Marked Demo

-
-
-
- Input · - · - Version: - · - - - +
Loading...
+
+
+
+
+ Input · + · + Version: + + · + + +
+ +
- - -
-
-
- -
+
+
+ · + Response Time: + +
-
- - -
+
+ + +
- + - + - + +
diff --git a/docs/demo/initial.md b/docs/demo/initial.md index 2465aef041..d2b7d77c10 100644 --- a/docs/demo/initial.md +++ b/docs/demo/initial.md @@ -30,7 +30,7 @@ It's easy. It's not overly bloated, unlike HTML. Also, as the creator of [mark > or formatting instructions. Ready to start writing? Either start changing stuff on the left or -[clear everything](?text=) with a simple click. +[clear everything](/demo/?text=) with a simple click. [Marked]: https://github.com/markedjs/marked/ [Markdown]: http://daringfireball.net/projects/markdown/ diff --git a/docs/demo/worker.js b/docs/demo/worker.js new file mode 100644 index 0000000000..06b8befe78 --- /dev/null +++ b/docs/demo/worker.js @@ -0,0 +1,105 @@ +/* globals marked, unfetch, ES6Promise */ +if (!self.Promise) { + self.importScripts('https://cdn.jsdelivr.net/npm/es6-promise/dist/es6-promise.js'); + self.Promise = ES6Promise; +} +if (!self.fetch) { + self.importScripts('https://cdn.jsdelivr.net/npm/unfetch/dist/unfetch.umd.js'); + self.fetch = unfetch; +} + +var versionCache = {}; +var currentVersion; + +onunhandledrejection = function (e) { + throw e.reason; +}; + +onmessage = function (e) { + if (e.data.version === currentVersion) { + parse(e); + } else { + loadVersion(e.data.version).then(function () { + parse(e); + }); + } +}; + +function parse(e) { + switch (e.data.task) { + case 'defaults': + + var defaults = {}; + if (typeof marked.getDefaults === 'function') { + defaults = marked.getDefaults(); + delete defaults.renderer; + } else if ('defaults' in marked) { + for (var prop in marked.defaults) { + if (prop !== 'renderer') { + defaults[prop] = marked.defaults[prop]; + } + } + } + postMessage({ + task: e.data.task, + defaults: defaults + }); + break; + case 'parse': + var startTime = new Date(); + var lexed = marked.lexer(e.data.markdown, e.data.options); + var lexedList = []; + for (var i = 0; i < lexed.length; i++) { + var lexedLine = []; + for (var j in lexed[i]) { + lexedLine.push(j + ':' + jsonString(lexed[i][j])); + } + lexedList.push('{' + lexedLine.join(', ') + '}'); + } + var parsed = marked.parser(lexed, e.data.options); + var endTime = new Date(); + // setTimeout(function () { + postMessage({ + task: e.data.task, + lexed: lexedList.join('\n'), + parsed: parsed, + time: endTime - startTime + }); + // }, 10000); + break; + } +} + +function jsonString(input) { + var output = (input + '') + .replace(/\n/g, '\\n') + .replace(/\r/g, '\\r') + .replace(/\t/g, '\\t') + .replace(/\f/g, '\\f') + .replace(/[\\"']/g, '\\$&') + .replace(/\u0000/g, '\\0'); + return '"' + output + '"'; +}; + +function loadVersion(ver) { + var promise; + if (versionCache[ver]) { + promise = Promise.resolve(versionCache[ver]); + } else { + promise = fetch(ver) + .then(function (res) { return res.text(); }) + .then(function (text) { + versionCache[ver] = text; + return text; + }); + } + return promise.then(function (text) { + try { + // eslint-disable-next-line no-new-func + Function(text)(); + } catch (err) { + throw new Error('Cannot load that version of marked'); + } + currentVersion = ver; + }); +}