OurBigBook logoOurBigBook Docs OurBigBook logoOurBigBook.comSite Source code
marked/docs/demo/demo.js
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);

// --- Theme Toggle Setup ---
const $themeToggle = document.getElementById('theme-toggle');
const $themeToggleIcon = $themeToggle ? $themeToggle.querySelector('[data-theme-icon]') : null;
const $themeToggleText = $themeToggle ? $themeToggle.querySelector('[data-theme-text]') : null;

const THEME_STORAGE_KEY = 'theme-preference';
const LEGACY_STORAGE_KEY = 'theme';
const THEME_ORDER = ['system', 'light', 'dark'];
const TOGGLE_UI = {
  system: { icon: 'brightness_auto', text: 'System' },
  light: { icon: 'light_mode', text: 'Light' },
  dark: { icon: 'dark_mode', text: 'Dark' },
};

function applyTheme(theme) {
  if (theme === 'dark') {
    document.documentElement.classList.add('dark');
  } else {
    document.documentElement.classList.remove('dark');
  }
  document.documentElement.setAttribute('data-theme', theme);

  try {
    if ($previewIframe && $previewIframe.contentDocument) {
      if (theme === 'dark') {
        $previewIframe.contentDocument.documentElement.classList.add('dark');
      } else {
        $previewIframe.contentDocument.documentElement.classList.remove('dark');
      }
    }
  } catch {
    // Ignore cross-origin errors
  }
}

function getSystemTheme() {
  if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
    return 'dark';
  }
  return 'light';
}

function sanitisePreference(value) {
  return THEME_ORDER.includes(value) ? value : null;
}

function readStoredPreference() {
  try {
    const stored = sanitisePreference(localStorage.getItem(THEME_STORAGE_KEY));
    if (stored) {
      return stored;
    }
    return sanitisePreference(localStorage.getItem(LEGACY_STORAGE_KEY));
  } catch {
    return null;
  }
}

function writeStoredPreference(preference) {
  try {
    localStorage.setItem(THEME_STORAGE_KEY, preference);
    if (preference === 'light' || preference === 'dark') {
      localStorage.setItem(LEGACY_STORAGE_KEY, preference);
    } else {
      localStorage.removeItem(LEGACY_STORAGE_KEY);
    }
  } catch {
    // Storage might be unavailable; ignore
  }
}

function getEffectiveTheme(preference) {
  return preference === 'system' ? getSystemTheme() : preference;
}

function updateToggle(preference) {
  if (!$themeToggle) {
    return;
  }
  const details = TOGGLE_UI[preference] || TOGGLE_UI.system;
  if ($themeToggleIcon) {
    $themeToggleIcon.textContent = details.icon;
  }
  if ($themeToggleText) {
    $themeToggleText.textContent = details.text;
  }
  $themeToggle.setAttribute('data-theme-mode', preference);
  const label = `Switch theme (current: ${details.text})`;
  $themeToggle.setAttribute('aria-label', label);
  $themeToggle.title = label;
}

let currentPreference = readStoredPreference() || 'system';

function applyPreference(preference, persist) {
  currentPreference = preference;
  const effectiveTheme = getEffectiveTheme(preference);
  applyTheme(effectiveTheme);
  document.documentElement.setAttribute('data-theme-preference', preference);
  updateToggle(preference);
  if (persist) {
    writeStoredPreference(preference);
  }
}

applyPreference(currentPreference, true);

if ($themeToggle) {
  $themeToggle.addEventListener('click', function() {
    const index = THEME_ORDER.indexOf(currentPreference);
    const nextIndex = (index + 1) % THEME_ORDER.length;
    const nextPreference = THEME_ORDER[nextIndex];
    applyPreference(nextPreference, true);
  });
}

const systemMatcher = window.matchMedia ? window.matchMedia('(prefers-color-scheme: dark)') : null;
if (systemMatcher) {
  const handleSystemChange = function() {
    if (currentPreference === 'system') {
      applyPreference('system', false);
    }
  };

  if (typeof systemMatcher.addEventListener === 'function') {
    systemMatcher.addEventListener('change', handleSystemChange);
  } else if (typeof systemMatcher.addListener === 'function') {
    systemMatcher.addListener(handleSystemChange);
  }
}

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;

  // Apply current theme to the iframe
  try {
    const currentTheme = document.documentElement.classList.contains('dark')
      ? 'dark'
      : 'light';
    if ($previewIframe && $previewIframe.contentDocument) {
      if (currentTheme === 'dark') {
        $previewIframe.contentDocument.documentElement.classList.add('dark');
      } else {
        $previewIframe.contentDocument.documentElement.classList.remove('dark');
      }
    }
  } catch {
    // Ignore cross-origin errors
  }
}

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 !== null
        && 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 {
      $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 {}
  $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);
}