Source: index.js

// 'use strict';

//
// Smartdown
// Copyright 2015, Daniel B Keith
//

/* global smartdown */
/* xglobal useFileSaver */
/* global useLocalForage */
/* global useGifffer */
/* global useMathJax */
/* global MathJax */

import createDOMPurify from 'dompurify';

import axios from 'axios';
import smoothscroll from 'smoothscroll-polyfill';

import lodashEach from 'lodash/forEach';
import lodashMap from 'lodash/map';
import lodashIsEqual from 'lodash/isEqual';

window.lodashMap = lodashMap;
window.lodashEach = lodashEach;
window.lodashIsEqual = lodashIsEqual;

import vdomToHtml from 'vdom-to-html';

import marked from 'marked';
import jsyaml from 'js-yaml';
import fileSaver from 'file-saver';
import localForage from 'localforage';
import Gifffer from 'gifffer';

window.jsyaml = jsyaml;

import {loadExternal, ensureExtension} from './extensions';

import hljs from './render/hljs';
import mathjaxConfigure from './extensions/MathJax';
import P5 from './extensions/P5';

import './styles.css';

import globalState from './util/globalState';
import expandHrefWithLinkRules from './util/expandHrefWithLinkRules';
import setLinkRules from './util/setLinkRules';
import setupYouTubePlayer from './util/setupYouTubePlayer';
import getFrontmatter from './parse/getFrontmatter';
import partitionMultipart from './parse/partitionMultipart';
import expandStringWithSubstitutions from './util/expandStringWithSubstitutions';
import runFunction from './util/runFunction';
import enhanceMarkedAndOpts from './util/enhanceMarkedAndOpts';
import registerExpression from './util/registerExpression';
import computeExpressions from './util/computeExpressions';
import playPlayable from './util/playPlayable';
import registerPlayable from './util/registerPlayable';
import transformPlayables from './util/transformPlayables';
import resetPlayable from './util/resetPlayable';

import runModule from './util/runModule';
import { consoleWrite, toggleConsole } from './util/console';
import { toggleDebug } from './util/debug';
import toggleKiosk from './util/toggleKiosk';
import startAutoplay from './runtime/startAutoplay';
import resetAllPlayables from './runtime/resetAllPlayables';
import cleanupOrphanedStuff from './runtime/cleanupOrphanedStuff';
import updateProcesses from './runtime/updateProcesses';
import resetPerPageState from './runtime/resetPerPageState';
import registerDefaultExtensions from './runtime/registerDefaultExtensions';
import configure from './runtime/configure';

import { openFullscreen, closeFullscreen, isFullscreen } from './util/fullscreen';
import {
  showDisclosure,
  hideDisclosure,
  toggleDisclosure,
  deactivateOnMouseLeave,
  activateOnMouseLeave,
  linkWrapperExit,
} from './util/disclosable';

import {
  importScriptUrl,
  importModuleUrl,
  importTextUrl,
  importCssCode,
  importCssUrl,
} from './importers';

import entityEscape from './render/entityEscape';
import decodeInlineScript from './parse/decodeInlineScript';
import areValuesSameEnough from './util/areValuesSameEnough';

const testing = process.env.BUILD === 'test';

// let fileSaver = {};
// if (useFileSaver) {
//   fileSaver = require('file-saver');
// }

// let localForage = {};
// if (useLocalForage) {
//   localForage = require('localforage');
// }

// let Gifffer = {};
// if (useGifffer) {
//   Gifffer = require('gifffer');
// }

const localForageSmartdownPrefix = 'smartdownVariable/';
const inlinePrefix = '^^InLiNe^^';


registerDefaultExtensions();

/**
 * Initialize the smartdown runtime.
 *
 * @constructor
 * @param {object} media - media
 * @param {string} baseURL - baseURL
 * @param {function} loadedHandler - loadedHandler
 * @param {function} cardLoader - cardLoader
 * @param {object} calcHandlers - calcHandlers
 * @param {object} linkRules - linkRules
 *
 */


function initialize(media, baseURL, loadedHandler, cardLoaderArg, calcHandlersArg, linkRulesArg) {
  const options = {
    media,
    baseURL,
    cardLoader: cardLoaderArg,
    calcHandlers: calcHandlersArg,
    linkRules: linkRulesArg,
  };

  configure(options, loadedHandler);
}

function propagateModel() {
  ensureCells();
  ensureVariables();
  lodashEach(smartdown.smartdownVariables, function (v, k) {
    propagateChangedVariable(k, v);
  });
}


function changeVariable(id, newValue) {
  smartdown.smartdownVariables[id] = newValue;
  // console.log('changeVariable', id, newValue, useLocalForage, smartdown.persistence);
  if (useLocalForage && smartdown.persistence) {
    const key = localForageSmartdownPrefix + id;
    const value = newValue;
    localForage.setItem(key, value).then(function () {
    }).catch(function (err) {
      console.log('localForage STORE ERROR', key, value, err);
    });
  }
}

function propagateChangedVariable(id, newValue, force) {
  const oldValue = smartdown.smartdownVariables[id];

  if (force || !areValuesSameEnough(id, oldValue, newValue)) {
    changeVariable(id, newValue);
    updateProcesses(id, newValue);
  }
}


function ensureCells() {
  lodashEach(smartdown.smartdownCells, function(newCell, cellID) {
    const element = document.getElementById(cellID);
    if (!element) {
      // console.log('...ensureCells element for cellID not found', cellID, smartdown.smartdownCells[cellID]);
      delete smartdown.smartdownCells[cellID];
    }
  });
}


function ensureVariables() {
  lodashEach(smartdown.smartdownCells, function(newCell) {
    const oldValue = smartdown.smartdownVariables[newCell.cellBinding];
    changeVariable(newCell.cellBinding, oldValue);
  });
}

function resetVariables() {
  smartdown.smartdownVariables = {};
  changeVariable(null, null);
  ensureVariables();
}

let scrollHoverDisableEnabled = false;
let lastY;

function setupScrollHoverDisable() {
  lastY = 0;
  if (!scrollHoverDisableEnabled) {
    let timer;
    scrollHoverDisableEnabled = true;
    // https://www.thecssninja.com/css/pointer-events-60fps
    const body = document.getElementsByTagName('body')[0];
    window.addEventListener('scroll', function() {
      const currentY = window.scrollY;
      const delta = Math.abs(lastY - currentY);

      if (delta > 25) {
        body.classList.add('disable-hover');

        clearTimeout(timer);
        timer = setTimeout(function() {
          body.classList.remove('disable-hover');
        }, 700);
      }
      lastY = currentY;
    }, false);  }
}


let patchesUnresolvedKludgeLimit = 0;

function setSmartdown(md, outputDiv, setSmartdownCompleted) {
  if (smartdown.currentRenderDiv) {
    console.log('setSmartdown REENTRANCY FAIL', smartdown.currentRenderDiv.id, md.slice(0, 40));
  }
  else {
    smartdown.currentRenderDiv = outputDiv;
  }
  smartdown.currentBackpatches[outputDiv.id] = [];

  setupScrollHoverDisable();
  cleanupOrphanedStuff();
  resetAllPlayables(outputDiv, true);

  const fm = getFrontmatter(md);
  md = fm.markdown;
  outputDiv.frontmatter = fm.frontmatter;
  // window.getSelection().removeAllRanges();

  function completeTypeset() {
    let resizeTimeout;

    function actualResizeHandler() {
      const playables = globalState.playablesRegistered;
      Object.keys(playables).forEach((k) => {
        const playable = playables[k];
        if (playable.playing) {
          const d = document.getElementById(playable.divId);
          if (d) {
            if (playable.embedThis && playable.embedThis.sizeChanged) {
              playable.embedThis.sizeChanged();
            }
          }
        }
      });
    }

    function resizeThrottler() {
      // ignore resize events as long as an actualResizeHandler execution is in the queue
      if (!resizeTimeout) {
        resizeTimeout = setTimeout(function() {
          resizeTimeout = null;
          actualResizeHandler();
         }, 500);
      }
    }

    function applyLocalStorage(done) {
      const doneHandler = done || function emptyDone() {};

      if (useLocalForage && smartdown.persistence) {
        localForage.iterate(function(value, key) {
          // Resulting key/value pair -- this callback
          // will be executed for every item in the
          // database.
          if (key.indexOf(localForageSmartdownPrefix) === 0) {
            const varName = key.slice(localForageSmartdownPrefix.length);
            if (value) {
              smartdown.smartdownVariables[varName] = value;
            }
          }
        }).then(function() {
          // updateProcesses();

          doneHandler();
        }).catch(function(err) {
          // This code runs if there were any errors
          console.log(err);
          doneHandler();
        });
      }
      else {
        // updateProcesses();
        doneHandler();
      }
    }

    function finishLoad(done) {
      ensureCells();
      ensureVariables();
      // resetAllPlayables(outputDiv, false);

      if (window.twttr && window.twttr.widgets) {
        window.twttr.widgets.load(outputDiv);
      }

      if (useGifffer) {
        Gifffer({
          playButtonStyles: {
            'width': '60px',
            'height': '60px',
            'border-radius': '30px',
            'background': 'rgba(200, 200, 200, 0.5)',
            'position': 'absolute',
            'top': '50%',
            'left': '50%',
            'margin': '-30px 0 0 -30px'
          },
          playButtonIconStyles: {
            'width': '0',
            'height': '0',
            'border-top': '14px solid transparent',
            'border-bottom': '14px solid transparent',
            'border-left': '14px solid rgba(0, 0, 0, 0.5)',
            'position': 'absolute',
            'left': '26px',
            'top': '16px'
          }
        });

        //
        // To deal with integrations like Impress.js, we need to ensure
        // event.stopPropagation() so that Impress.js doesn't pick up a
        // DOM element that doesn't exist, because of the way that Gifffer
        // works. Not necessarily a completely accurate explanation, but
        // we'll see if it works.
        //

        const gifs = document.querySelectorAll('.gifffer-container button');

        gifs.forEach((g) => {
          g.addEventListener('click', function (event) {
            event.stopPropagation();
          });
        });
      }


      transformPlayables(outputDiv, function() {
        if (globalState.cardLoading) {
          globalState.cardLoading = false;
          propagateModel();
          updateProcesses();
        }

        if (done) {
          done();
        }
      });
    }

    // window.onresize = resizeThrottler;
    window.addEventListener('resize', resizeThrottler);

    const firstTweetIndex = md.search(/[^`]!\[[^\]]*\]\(https:\/\/twitter\.com\/[^`]/);

    if (firstTweetIndex >= 0) {
      if (!globalState.twitterLoading) {
        globalState.twitterLoading = true;
        importScriptUrl(
          'https://platform.twitter.com/widgets.js',
          function () {
            console.log('Twitter loaded... window.twttr', window.twttr);
            finishLoad(function() {
              applyLocalStorage(setSmartdownCompleted);
            });
          //   window.setTimeout(function () {
          //     console.log('window.twttr.widgets.load');
          //     window.twttr.widgets.load();
          //   }, 5000);   // I hate myself
          });
      }
      else {
        finishLoad(function() {
          applyLocalStorage(setSmartdownCompleted);
        });
      }
    }
    else {
      finishLoad(function() {
        applyLocalStorage(setSmartdownCompleted);
      });
    }
  }

  // let result = marked(md);

  // Inline Playables need their tokens adjusted before
  // rendering, or else they will act as paragraphs and
  // not use the inline styling.
  // I wonder if WalkTokens would be easier...
  //  https://marked.js.org/using_pro#walk-tokens

  const lexer = new marked.Lexer();
  const tokens = lexer.lex(md);

  let precedingParagraph = null;
  let precedingInlinedCodeblock = null; // This is a code block with /inline
  tokens.forEach((t) => {
    if (t.type === 'paragraph') {
      if (precedingInlinedCodeblock) {
        const firstChild = t.tokens[0];
        if (firstChild && firstChild.type === 'text') {
          firstChild.text = inlinePrefix + firstChild.text;
          if (firstChild.text.indexOf(inlinePrefix) === 0) {
            // console.log(' WEIRD1 already inline prefixed', firstChild.text, firstChild.raw, firstChild);
          }
          else {
            firstChild.text = inlinePrefix + firstChild.text;
          }
        }
      }
      precedingParagraph = t;
      precedingInlinedCodeblock = null;
    }
    else if (t.type === 'code') {
      const inlineCode = t.lang && t.lang.indexOf('/inline') >= 0;
      if (precedingParagraph &&
          inlineCode &&
          precedingParagraph.text.indexOf(inlinePrefix) !== 0) {
        const firstChild = precedingParagraph.tokens[0];
        if (firstChild && firstChild.type === 'text') {
          if (firstChild.text.indexOf(inlinePrefix) === 0) {
            // console.log(' WEIRD2 already inline prefixed', firstChild.text, firstChild.raw, firstChild);
          }
          else {
            firstChild.text = inlinePrefix + firstChild.text;
          }
        }
      }
      if (inlineCode) {
        precedingInlinedCodeblock = t;
      }
      else {
        precedingInlinedCodeblock = null;
      }
      precedingParagraph = null;
    }
    else if (t.type === 'space' || t.type === 'script') {
      // These elements may appear between a paragraph and an inline code
      // block.
    }
    else {
      precedingParagraph = null;
      precedingInlinedCodeblock = null;
    }
  });

  let result = marked.parser(tokens);

  // https://github.com/cure53/DOMPurify/tree/master/demos#advanced-config-demo-link
  const config = {
    FORCE_BODY: true,
    ADD_TAGS: ['script', 'iframe'],
    ADD_ATTR: ['onblur', 'oninput', 'onchange', 'onclick', 'onmousedown', 'onmouseup', 'onmouseenter', 'onmouseleave', 'onkeydown', 'onkeyup', 'target', 'allow', 'allowfullscreen'],
  };

  const sanitized = createDOMPurify.sanitize(result, config);
  if (result !== sanitized) {
    // console.log('result !== sanitized', result.length, sanitized.length);
    // console.log('-------------------');
    // console.log(md);
    // console.log('-------------------');
    // console.log(result);
    // console.log('-------------------');
    // console.log('sanitized');
    // console.log('-------------------');
    // console.log(sanitized);
    result = sanitized;
  }

  smartdown.currentRenderDiv = null;

  function applyBackpatches(done) {
    const bp = smartdown.currentBackpatches[outputDiv.id];
    let patchesUnresolved = 0;
    bp.forEach((patch) => {
      if (patch.key) {
        if (patch.replace) {
          // console.log('###Resolved patch', patch.key);
          result = result.replace(patch.key, patch.replace);
          patch.key = null;
        }
        else {
          // console.log('###Unresolved patch', patch.key);
          ++patchesUnresolved;
        }
      }
      else {
        console.log('applyBackpatches anomaly no key', outputDiv.id, patch);
      }
    });

    if (patchesUnresolved > 0) {
      if (--patchesUnresolvedKludgeLimit <= 0) {
        console.log('Aborting applyBackpatches recursion...', bp);
      }
      else {
        // console.log('patchesUnresolved', patchesUnresolved, patchesUnresolvedKludgeLimit);
        window.setTimeout(function() {
          applyBackpatches(function() {
            done();
          });
        }, 1000);
      }
    }
    else {
      done();
    }
  }

  patchesUnresolvedKludgeLimit = 5;
  // console.log('applyBackpatches BEGIN', outputDiv.id, smartdown.currentRenderDiv);

  applyBackpatches(function() {
    if (useMathJax) {
      //
      // If you are changing this code, be sure that you
      // ensure that the mathjax menu still works. In the past,
      // I've tried to adjust the code to reduce flashing and it
      // has broken the mathjax menu.
      //

      // const renderDivId = outputDiv.id + '-render';
      // const renderDiv = document.getElementById(renderDivId);

      // if (!renderDiv) {
      //   renderDiv = document.createElement('div');
      //   renderDiv.id = renderDivId;
      //   outputDiv.appendChild(renderDiv);
      // }

      // renderDiv.style.display = 'none';
      // renderDiv.innerHTML = result;

      // function finishIt() {
      //   outputDiv.innerHTML = renderDiv.innerHTML;
      //   renderDiv.style.display = 'none';
      //   renderDiv.innerHTML = '';
      //   completeTypeset();
      // }

      // // MathJax.Hub.Typeset(renderDiv, finishIt);
      // MathJax.Hub.Queue(['Typeset', MathJax.Hub, renderDiv, finishIt]);

      outputDiv.innerHTML = result;
      if (testing) {
        MathJax.Hub.Typeset(outputDiv);
        completeTypeset();
      }
      else {
        MathJax.Hub.Typeset(outputDiv, completeTypeset);
      }
    }
    else {
      outputDiv.innerHTML = result;
      completeTypeset();
    }
  });
}


//
// md may contain frontmatter.
//

function setHome(md, outputDiv, done) {
  // console.log('setHome', md.slice(0, 20), outputDiv);
  globalState.currentMD = md;
  globalState.currentHomeDiv = outputDiv;
  window.getSelection().removeAllRanges();
  resetAllPlayables(outputDiv, true);
  resetPerPageState();
  setSmartdown(md, outputDiv, function() {
    updateProcesses();
    done();
  });
}

function setVariable(id, newValue, type) {
  // console.log('setVariable', id, JSON.stringify(newValue).slice(0, 20), type);
  if (type === 'number') {
    newValue = Number(newValue);
  }
  try {
    ensureCells();
  }
  catch (e) {
    console.log('exception during ensureCells', id, e);
  }

  try {
    propagateChangedVariable(id, newValue);
  }
  catch (e) {
    console.log('exception during propagateChangedVariable', id, e);
  }
}

function set(varnameOrAssignments, varValue, varType) {
  if (arguments.length > 1) {
    setVariable(varnameOrAssignments, varValue, varType);
  }
  else {
    setVariables(varnameOrAssignments);
  }
}


function setVariables(assignments) {
  if (Array.isArray(assignments)) {
    lodashEach(assignments, (assignment) => {
      let newValue = assignment.rhs;
      if (assignment.type === 'number') {
        newValue = Number(newValue);
      }
      changeVariable(assignment.lhs, newValue);
    });
  }
  else {
    Object.keys(assignments).forEach((varname) => {
      changeVariable(varname, assignments[varname]);
    });
  }

  ensureCells();
  updateProcesses();
}


function computeStoredExpression(exprId) {
  const entry = globalState.expressionsRegistered[exprId];
  if (!entry) {
    console.log('computeStoredExpression no such expression', exprId, globalState.expressionsRegistered);
    // debugger;
  }
  else if (entry.manual) {
    computeExpression(entry, function() {
      updateProcesses();
    });


    // window.setTimeout(function() {
    //   computeExpression(entry, function() {
    //     window.setTimeout(function() {
    //       // console.log('timeout updateProcesses');
    //       updateProcesses();
    //     }, 1000);
    //   });
    // }, 1000);

    // ensureCells();
    // ensureVariables();
    // propagateModel();
  }
}


function computeExpression(entry, done) {
  const {lhss, rhss, types} = entry;
  let numPending = 0;
  function buildCompletionHandler(lhs) {
    return (result) => {
      propagateChangedVariable(lhs, result);
      // changeVariable(lhs, result);
      if (--numPending === 0) {
        if (done) {
          done();
        }
      }
    };
  }

  if (lhss.length !== rhss.length || types.length !== lhss.length) {
    console('lhss.length !== rhss.length || types.length !== lhss.length', lhss.length, rhss.length, types.length);
  }
  else {

    for (let i = 0; i < lhss.length; ++i) {
      const lhs = lhss[i];
      let rhs = rhss[i];
      const type = types[i];

      rhs = expandStringWithSubstitutions(rhs);
      if (lhs === 'TEMPLATECELLID') {
        // PASS
      }
      else if (!rhs) {
        //  smartdown.smartdownVariables[lhs] = smartdown.smartdownVariables[lhs] || '';
      }
      else if (rhs[0] === '/') {
        rhs = rhs.slice(1);
        if (globalState.calcHandlers) {
          const calcParts = rhs.split(/[./[]/);
          // const bracketIndex = rhs.indexOf('[');
          // const slashIndex = rhs.indexOf('/');

          const calcKey = calcParts[0];
          const calcBody = rhs.slice(calcKey.length);
          const calcHandler = globalState.calcHandlers[calcKey];
          if (calcHandler) {
            ++numPending;
            calcHandler(calcKey, calcBody, buildCompletionHandler(lhs));
          }
        }
      }
      else {
        let vars = '';
        lodashEach(smartdown.smartdownVariables, function (v, k) {
          vars += ',' + k;
        });
        vars = vars.slice(1);
        const vals = lodashMap(smartdown.smartdownVariables, function (v) {
          return v;
        });

        /* eslint-disable-next-line @typescript-eslint/no-implied-eval */
        const f = new Function(vars, 'return ' + rhs + ';');
        let newValue = f.apply({}, vals);
        // console.log('#rhs', f, vars, rhs, vals, type);
        if (type === 'number') {
          newValue = Number(newValue);
        }
        propagateChangedVariable(lhs, newValue);
        // const oldValue = smartdown.smartdownVariables[lhs];
        // smartdown.smartdownVariables[lhs] = newValue;
        // console.log('...', lhs, oldValue, newValue, entry);
      }
    }

    if (numPending > 0) {
      // console.log('computeExpression PENDING', entry, numPending);
    }
    else if (done) {
      done();
    }
  }
}

function goToCard(cardKey, event, outputDivId) {
  if (event) {
    event.preventDefault();
    event.stopPropagation();
  }
  if (!outputDivId) {
    outputDivId = 'smartdown-output';
  }

  globalState.cardLoading = true;
  if (globalState.cardLoader) {
    globalState.cardLoader(cardKey, outputDivId);
  }
  else {
    let modelAsMarkdown = null;

    if (!cardKey || cardKey === 'Home') {
      modelAsMarkdown = globalState.currentMD;
    }
    else {
      const scriptx = globalState.smartdownScriptsMap[cardKey];
      if (scriptx) {
        modelAsMarkdown = scriptx.text;
      }
    }

    if (modelAsMarkdown) {
      setSmartdown(modelAsMarkdown, globalState.currentHomeDiv, null);
    }
  }
}

function setPersistence(persistence) {
  smartdown.persistence = persistence;
}

function loadCardsFromDocumentScripts() {
  globalState.smartdownScripts.length = 0;
  Object.keys(globalState.smartdownScriptsMap).forEach((k) => {
    delete globalState.smartdownScriptsMap[k];
  });

  const scripts = document.scripts;
  Object.keys(scripts).forEach((s) => {
    const script = scripts[s];
    if (script && script.type && script.type === 'text/x-smartdown') {
      globalState.smartdownScripts.push(script);
      globalState.smartdownScriptsMap[script.id] = script;
    }
  });
}

function getMedia(mediaKey) {
  return smartdown.mediaRegistry[mediaKey];
}

module.exports = {
  initialize: initialize,
  configure: configure,
  expressionsRegistered: globalState.expressionsRegistered,
  playablesRegistered: globalState.playablesRegistered,
  playablesRegisteredOrder: globalState.playablesRegisteredOrder,
  enhanceMarkedAndOpts: enhanceMarkedAndOpts,
  partitionMultipart: partitionMultipart,
  registerPlayable: registerPlayable,
  playPlayable: playPlayable,
  resetPlayable: resetPlayable,
  toggleDebug: toggleDebug,
  toggleConsole: toggleConsole,
  consoleWrite: consoleWrite,
  showDisclosure: showDisclosure,
  hideDisclosure: hideDisclosure,
  isFullscreen: isFullscreen,
  openFullscreen: openFullscreen,
  closeFullscreen: closeFullscreen,
  toggleKiosk: toggleKiosk,
  toggleDisclosure: toggleDisclosure,
  activateOnMouseLeave: activateOnMouseLeave,
  deactivateOnMouseLeave: deactivateOnMouseLeave,
  linkWrapperExit: linkWrapperExit,
  startAutoplay: startAutoplay,
  setSmartdown: setSmartdown,
  setHome: setHome,
  resetVariables: resetVariables,
  loadCardsFromDocumentScripts: loadCardsFromDocumentScripts,
  registerExpression: registerExpression,
  computeExpressions: computeExpressions,
  computeStoredExpression: computeStoredExpression,
  setVariable: setVariable,
  set: set,
  setVariables: setVariables,
  setPersistence: setPersistence,
  computeExpression: computeExpression,
  goToCard: goToCard,
  smartdownScripts: globalState.smartdownScripts,
  smartdownScriptsMap: globalState.smartdownScriptsMap,
  currentHomeDiv: globalState.currentHomeDiv,
  cardLoader: globalState.cardLoader,
  calcHandlers: globalState.calcHandlers,
  importCssCode: importCssCode,
  importCssUrl: importCssUrl,
  importScriptUrl: importScriptUrl,
  importModuleUrl: importModuleUrl,
  importTextUrl: importTextUrl,
  linkRules: globalState.linkRules,
  expandHrefWithLinkRules: expandHrefWithLinkRules,
  setLinkRules: setLinkRules,
  getMedia: getMedia,
  resetPerPageState: resetPerPageState,
  decodeInlineScript: decodeInlineScript,
  hljs: hljs,
  marked: marked,
  Stdlib: null,
  P5Loader: P5.Loader,
  d3: null,
  d3fc: null,
  d3cloud: null,
  topojson: null,
  Three: null,
  lodashEach: window.lodashEach,
  lodashMap: window.lodashMap,
  lodashIsEqual: window.lodashIsEqual,
  jsyaml: window.jsyaml,
  axios: axios,
  getFrontmatter: getFrontmatter,
  updateProcesses: updateProcesses,
  cleanupOrphanedStuff: cleanupOrphanedStuff,
  version: '1.0.76',
  baseURL: null, // Filled in by initialize/configure
  setupYouTubePlayer: setupYouTubePlayer,
  entityEscape: entityEscape,
  mathjaxConfigure: mathjaxConfigure,
  persistence: false,
  openJSCAD: {},
  fileSaver: fileSaver,
  vdomToHtml: vdomToHtml,
  runFunction: runFunction,
  runModule: runModule,
  loadExternal: loadExternal,
  ensureExtension: ensureExtension,
  es6Playables: globalState.es6Playables,
  currentRenderDiv: null,
  currentBackpatches: {},
  smartdownVariables: null,
  uniqueCellIndex: null,
  mediaRegistry: null,
};

// kick off the polyfill!
smoothscroll.polyfill();

window.smartdown = module.exports;