mirror of
https://github.com/markedjs/marked
synced 2024-11-22 17:37:24 +00:00
7d95a91093
BREAKING CHANGE: - remove built files from git repo. - If you need to use the latest version of marked on the web you can use a cdn to get marked.min.js from npm: - `https://cdn.jsdelivr.net/npm/marked/marked.min.js`
397 lines
10 KiB
JavaScript
397 lines
10 KiB
JavaScript
onunhandledrejection = (e) => {
|
|
throw e.reason;
|
|
};
|
|
|
|
const $loadingElem = document.querySelector('#loading');
|
|
const $mainElem = document.querySelector('#main');
|
|
const $markdownElem = document.querySelector('#markdown');
|
|
const $markedVerElem = document.querySelector('#markedVersion');
|
|
const $optionsElem = document.querySelector('#options');
|
|
const $outputTypeElem = document.querySelector('#outputType');
|
|
const $inputTypeElem = document.querySelector('#inputType');
|
|
const $responseTimeElem = document.querySelector('#responseTime');
|
|
const $previewElem = document.querySelector('#preview');
|
|
const $previewIframe = document.querySelector('#preview iframe');
|
|
const $permalinkElem = document.querySelector('#permalink');
|
|
const $clearElem = document.querySelector('#clear');
|
|
const $htmlElem = document.querySelector('#html');
|
|
const $lexerElem = document.querySelector('#lexer');
|
|
const $panes = document.querySelectorAll('.pane');
|
|
const $inputPanes = document.querySelectorAll('.inputPane');
|
|
let lastInput = '';
|
|
let inputDirty = true;
|
|
let $activeOutputElem = null;
|
|
let latestVersion = 'master';
|
|
const search = searchToObject();
|
|
const markedVersions = {
|
|
master: '../'
|
|
};
|
|
let delayTime = 1;
|
|
let checkChangeTimeout = null;
|
|
let markedWorker;
|
|
|
|
$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);
|
|
|
|
$clearElem.addEventListener('click', handleClearClick, false);
|
|
|
|
Promise.all([
|
|
setInitialQuickref(),
|
|
setInitialOutputType(),
|
|
setInitialText(),
|
|
setInitialVersion()
|
|
.then(setInitialOptions)
|
|
]).then(() => {
|
|
handleInputChange();
|
|
handleOutputChange();
|
|
checkForChanges();
|
|
setScrollPercent(0);
|
|
$loadingElem.style.display = 'none';
|
|
$mainElem.style.display = 'block';
|
|
}).catch(() => {
|
|
$loadingElem.classList.add('loadingError');
|
|
$loadingElem.textContent = 'Failed to load marked. Refresh the page to try again.';
|
|
});
|
|
|
|
function setInitialText() {
|
|
if ('text' in search) {
|
|
$markdownElem.value = search.text;
|
|
} else {
|
|
return fetch('./initial.md')
|
|
.then(res => res.text())
|
|
.then(text => {
|
|
if ($markdownElem.value === '') {
|
|
$markdownElem.value = text;
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function setInitialQuickref() {
|
|
return fetch('./quickref.md')
|
|
.then(res => res.text())
|
|
.then(text => {
|
|
document.querySelector('#quickref').value = text;
|
|
});
|
|
}
|
|
|
|
function setInitialVersion() {
|
|
return fetch('https://data.jsdelivr.com/v1/package/npm/marked')
|
|
.then(res => res.json())
|
|
.then(json => {
|
|
for (const ver of json.versions) {
|
|
markedVersions[ver] = 'https://cdn.jsdelivr.net/npm/marked@' + ver;
|
|
const opt = document.createElement('option');
|
|
opt.textContent = ver;
|
|
opt.value = ver;
|
|
$markedVerElem.appendChild(opt);
|
|
}
|
|
|
|
if (location.host === 'marked.js.org') {
|
|
latestVersion = json.tags.latest;
|
|
} else {
|
|
$markedVerElem.querySelector('option[value="master"]').textContent = 'This Build';
|
|
}
|
|
|
|
if (search.version && markedVersions[search.version]) {
|
|
$markedVerElem.value = search.version;
|
|
return;
|
|
}
|
|
|
|
$markedVerElem.value = latestVersion;
|
|
})
|
|
.then(updateVersion);
|
|
}
|
|
|
|
function setInitialOptions() {
|
|
if ('options' in search) {
|
|
$optionsElem.value = search.options;
|
|
} else {
|
|
return setDefaultOptions();
|
|
}
|
|
}
|
|
|
|
function setInitialOutputType() {
|
|
if (search.outputType) {
|
|
$outputTypeElem.value = search.outputType;
|
|
}
|
|
}
|
|
|
|
function handleIframeLoad() {
|
|
lastInput = '';
|
|
inputDirty = true;
|
|
}
|
|
|
|
function handleInput() {
|
|
inputDirty = true;
|
|
}
|
|
|
|
function handleVersionChange() {
|
|
updateVersion();
|
|
}
|
|
|
|
function handleClearClick() {
|
|
$markdownElem.value = '';
|
|
$markedVerElem.value = latestVersion;
|
|
updateVersion();
|
|
setDefaultOptions();
|
|
}
|
|
|
|
function handleInputChange() {
|
|
handleChange($inputPanes, $inputTypeElem.value);
|
|
}
|
|
|
|
function handleOutputChange() {
|
|
$activeOutputElem = handleChange($panes, $outputTypeElem.value);
|
|
updateLink();
|
|
}
|
|
|
|
function handleChange(panes, visiblePane) {
|
|
let active = null;
|
|
for (let i = 0; i < panes.length; i++) {
|
|
if (panes[i].id === visiblePane) {
|
|
panes[i].style.display = '';
|
|
active = panes[i];
|
|
} else {
|
|
panes[i].style.display = 'none';
|
|
}
|
|
}
|
|
return active;
|
|
}
|
|
|
|
function setDefaultOptions() {
|
|
return messageWorker({
|
|
task: 'defaults',
|
|
version: markedVersions[$markedVerElem.value]
|
|
});
|
|
}
|
|
|
|
function setOptions(opts) {
|
|
$optionsElem.value = JSON.stringify(
|
|
opts,
|
|
(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
|
|
const pairs = location.search.slice(1).split('&');
|
|
const obj = {};
|
|
|
|
for (let i = 0; i < pairs.length; i++) {
|
|
if (pairs[i] === '') {
|
|
continue;
|
|
}
|
|
|
|
const pair = pairs[i].split('=');
|
|
|
|
obj[decodeURIComponent(pair.shift())] = decodeURIComponent(pair.join('='));
|
|
}
|
|
|
|
return obj;
|
|
}
|
|
|
|
function getScrollSize() {
|
|
if (!$activeOutputElem) {
|
|
return 0;
|
|
}
|
|
|
|
const e = $activeOutputElem;
|
|
|
|
return e.scrollHeight - e.clientHeight;
|
|
}
|
|
|
|
function getScrollPercent() {
|
|
if (!$activeOutputElem) {
|
|
return 1;
|
|
}
|
|
|
|
const size = getScrollSize();
|
|
|
|
if (size <= 0) {
|
|
return 1;
|
|
}
|
|
|
|
return $activeOutputElem.scrollTop / size;
|
|
}
|
|
|
|
function setScrollPercent(percent) {
|
|
if ($activeOutputElem) {
|
|
$activeOutputElem.scrollTop = percent * getScrollSize();
|
|
}
|
|
}
|
|
|
|
function updateLink() {
|
|
let outputType = '';
|
|
if ($outputTypeElem.value !== 'preview') {
|
|
outputType = 'outputType=' + $outputTypeElem.value + '&';
|
|
}
|
|
|
|
$permalinkElem.href = '?' + outputType + 'text=' + encodeURIComponent($markdownElem.value)
|
|
+ '&options=' + encodeURIComponent($optionsElem.value)
|
|
+ '&version=' + encodeURIComponent($markedVerElem.value);
|
|
history.replaceState('', document.title, $permalinkElem.href);
|
|
}
|
|
|
|
function updateVersion() {
|
|
handleInput();
|
|
}
|
|
|
|
function checkForChanges() {
|
|
if (inputDirty && $markedVerElem.value !== 'pr') {
|
|
inputDirty = false;
|
|
|
|
updateLink();
|
|
|
|
let options = {};
|
|
const optionsString = $optionsElem.value || '{}';
|
|
try {
|
|
const newOptions = JSON.parse(optionsString);
|
|
options = newOptions;
|
|
$optionsElem.classList.remove('error');
|
|
} catch (err) {
|
|
$optionsElem.classList.add('error');
|
|
}
|
|
|
|
const version = markedVersions[$markedVerElem.value];
|
|
const markdown = $markdownElem.value;
|
|
const hash = version + markdown + optionsString;
|
|
if (lastInput !== hash) {
|
|
lastInput = hash;
|
|
delayTime = 100;
|
|
messageWorker({
|
|
task: 'parse',
|
|
version,
|
|
markdown,
|
|
options
|
|
});
|
|
}
|
|
}
|
|
checkChangeTimeout = window.setTimeout(checkForChanges, delayTime);
|
|
}
|
|
|
|
function setResponseTime(ms) {
|
|
let amount = ms;
|
|
let 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;
|
|
$responseTimeElem.animate([
|
|
{ transform: 'scale(1.2)' },
|
|
{ transform: 'scale(1)' }
|
|
], 200);
|
|
}
|
|
|
|
function setParsed(parsed, lexed) {
|
|
try {
|
|
$previewIframe.contentDocument.body.innerHTML = parsed;
|
|
} catch (ex) {}
|
|
$htmlElem.value = parsed;
|
|
$lexerElem.value = lexed;
|
|
}
|
|
|
|
const workerPromises = {};
|
|
function messageWorker(message) {
|
|
if (!markedWorker || markedWorker.working) {
|
|
if (markedWorker) {
|
|
clearTimeout(markedWorker.timeout);
|
|
markedWorker.terminate();
|
|
}
|
|
markedWorker = new Worker('worker.js');
|
|
markedWorker.onmessage = (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');
|
|
const scrollPercent = getScrollPercent();
|
|
setParsed(e.data.parsed, e.data.lexed);
|
|
setScrollPercent(scrollPercent);
|
|
setResponseTime(e.data.time);
|
|
break;
|
|
}
|
|
}
|
|
clearTimeout(checkChangeTimeout);
|
|
delayTime = 10;
|
|
checkForChanges();
|
|
workerPromises[e.data.id]();
|
|
delete workerPromises[e.data.id];
|
|
};
|
|
markedWorker.onerror = markedWorker.onmessageerror = (err) => {
|
|
clearTimeout(markedWorker.timeout);
|
|
let 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);
|
|
};
|
|
}
|
|
if (message.task !== 'defaults') {
|
|
markedWorker.working = true;
|
|
workerTimeout(0);
|
|
}
|
|
return new Promise(resolve => {
|
|
message.id = uniqueWorkerMessageId();
|
|
workerPromises[message.id] = resolve;
|
|
markedWorker.postMessage(message);
|
|
});
|
|
}
|
|
|
|
function uniqueWorkerMessageId() {
|
|
let id;
|
|
do {
|
|
id = Math.random().toString(36);
|
|
} while (id in workerPromises);
|
|
return id;
|
|
}
|
|
|
|
function workerTimeout(seconds) {
|
|
markedWorker.timeout = setTimeout(() => {
|
|
seconds++;
|
|
markedWorker.onerror('Marked has taken longer than ' + seconds + ' second' + (seconds > 1 ? 's' : '') + ' to respond...');
|
|
workerTimeout(seconds);
|
|
}, 1000);
|
|
}
|