diff --git a/editors/editoria/src/config/config.js b/editors/editoria/src/config/config.js
index 7072aeab13c9c70d5f17b45059cd5bd9f521dec8..a10a5e399d5f0babefb052f069c8a1a7db5b723f 100644
--- a/editors/editoria/src/config/config.js
+++ b/editors/editoria/src/config/config.js
@@ -25,6 +25,7 @@ import {
   CodeBlockToolGroupService,
   TrackChangeToolGroupService,
   DisplayTextToolGroupService,
+  MathService,
 } from 'wax-prosemirror-services';
 
 import { WaxSelectionPlugin } from 'wax-prosemirror-plugins';
@@ -97,5 +98,6 @@ export default {
     new CodeBlockToolGroupService(),
     new TrackChangeToolGroupService(),
     new DisplayTextToolGroupService(),
+    new MathService(),
   ],
 };
diff --git a/wax-prosemirror-plugins/index.js b/wax-prosemirror-plugins/index.js
index 72d283b67322f401b18d557fa3cd3d42652009e0..819d258b5e5fcd2f7faa176493b63799d35f2056 100644
--- a/wax-prosemirror-plugins/index.js
+++ b/wax-prosemirror-plugins/index.js
@@ -3,3 +3,6 @@ export { default as TrackChangePlugin } from './src/trackChanges/TrackChangePlug
 export { default as CommentPlugin } from './src/comments/CommentPlugin';
 export { default as WaxSelectionPlugin } from './src/WaxSelectionPlugin';
 export { default as highlightPlugin } from './src/highlightPlugin';
+
+export { default as mathPlugin } from './src/math/math-plugin';
+export { default as mathSelectPlugin } from './src/math/math-select';
diff --git a/wax-prosemirror-plugins/package.json b/wax-prosemirror-plugins/package.json
index 3e3f5adebdaf1202b61631efcc525c78f3007c60..86635e611941ae990749878a9fb698a1895eec38 100644
--- a/wax-prosemirror-plugins/package.json
+++ b/wax-prosemirror-plugins/package.json
@@ -18,6 +18,9 @@
     "prosemirror-highlightjs": "^0.2.0",
     "prosemirror-state": "1.3.3",
     "prosemirror-view": "1.15.2",
+    "prosemirror-transform": "1.2.6",
+    "prosemirror-keymap": "1.1.4",
+    "prosemirror-commands": "1.1.4",
     "wax-prosemirror-components": "^0.0.20",
     "wax-prosemirror-core": "^0.0.20",
     "wax-prosemirror-utilities": "^0.0.20",
diff --git a/wax-prosemirror-plugins/src/math/math-nodeview.js b/wax-prosemirror-plugins/src/math/math-nodeview.js
new file mode 100644
index 0000000000000000000000000000000000000000..81bcd0d3373d512159aa0b84271644d7bfea65a0
--- /dev/null
+++ b/wax-prosemirror-plugins/src/math/math-nodeview.js
@@ -0,0 +1,281 @@
+/* eslint-disable */
+
+import { EditorState, TextSelection } from 'prosemirror-state';
+import { EditorView } from 'prosemirror-view';
+import { StepMap } from 'prosemirror-transform';
+import { keymap } from 'prosemirror-keymap';
+import {
+  newlineInCode,
+  chainCommands,
+  deleteSelection,
+} from 'prosemirror-commands';
+// katex
+import katex, { ParseError } from 'katex';
+export class MathView {
+  // == Lifecycle ===================================== //
+  /**
+   * @param onDestroy Callback for when this NodeView is destroyed.
+   *     This NodeView should unregister itself from the list of ICursorPosObservers.
+   *
+   * Math Views support the following options:
+   * @option displayMode If TRUE, will render math in display mode, otherwise in inline mode.
+   * @option tagName HTML tag name to use for this NodeView.  If none is provided,
+   *     will use the node name with underscores converted to hyphens.
+   */
+  constructor(node, view, getPos, options = {}, onDestroy) {
+    // store arguments
+    this._node = node;
+    this._outerView = view;
+    this._getPos = getPos;
+    this._onDestroy = onDestroy && onDestroy.bind(this);
+    // editing state
+    this.cursorSide = 'start';
+    this._isEditing = false;
+    // options
+    this._katexOptions = Object.assign(
+      { globalGroup: true, throwOnError: false },
+      options.katexOptions,
+    );
+    this._tagName = options.tagName || this._node.type.name.replace('_', '-');
+    // create dom representation of nodeview
+    this.dom = document.createElement(this._tagName);
+    this.dom.classList.add('math-node');
+    this._mathRenderElt = document.createElement('span');
+    this._mathRenderElt.textContent = '';
+    this._mathRenderElt.classList.add('math-render');
+    this.dom.appendChild(this._mathRenderElt);
+    this._mathSrcElt = document.createElement('span');
+    this._mathSrcElt.classList.add('math-src');
+    this.dom.appendChild(this._mathSrcElt);
+    // ensure
+    this.dom.addEventListener('click', () => this.ensureFocus());
+    // render initial content
+    this.renderMath();
+  }
+  destroy() {
+    // close the inner editor without rendering
+    this.closeEditor(false);
+    // clean up dom elements
+    if (this._mathRenderElt) {
+      this._mathRenderElt.remove();
+      delete this._mathRenderElt;
+    }
+    if (this._mathSrcElt) {
+      this._mathSrcElt.remove();
+      delete this._mathSrcElt;
+    }
+    delete this.dom;
+  }
+  /**
+   * Ensure focus on the inner editor whenever this node has focus.
+   * This helps to prevent accidental deletions of math blocks.
+   */
+  ensureFocus() {
+    if (this._innerView && this._outerView.hasFocus()) {
+      this._innerView.focus();
+    }
+  }
+  // == Updates ======================================= //
+  update(node, decorations) {
+    if (!node.sameMarkup(this._node)) return false;
+    this._node = node;
+    if (this._innerView) {
+      let state = this._innerView.state;
+      let start = node.content.findDiffStart(state.doc.content);
+      if (start != null) {
+        let diff = node.content.findDiffEnd(state.doc.content);
+        if (diff) {
+          let { a: endA, b: endB } = diff;
+          let overlap = start - Math.min(endA, endB);
+          if (overlap > 0) {
+            endA += overlap;
+            endB += overlap;
+          }
+          this._innerView.dispatch(
+            state.tr
+              .replace(start, endB, node.slice(start, endA))
+              .setMeta('fromOutside', true),
+          );
+        }
+      }
+    }
+    if (!this._isEditing) {
+      this.renderMath();
+    }
+    return true;
+  }
+  updateCursorPos(state) {
+    const pos = this._getPos();
+    const size = this._node.nodeSize;
+    const inPmSelection =
+      state.selection.from < pos + size && pos < state.selection.to;
+    if (!inPmSelection) {
+      this.cursorSide = pos < state.selection.from ? 'end' : 'start';
+    }
+  }
+  // == Events ===================================== //
+  selectNode() {
+    this.dom.classList.add('ProseMirror-selectednode');
+    if (!this._isEditing) {
+      this.openEditor();
+    }
+  }
+  deselectNode() {
+    this.dom.classList.remove('ProseMirror-selectednode');
+    if (this._isEditing) {
+      this.closeEditor();
+    }
+  }
+  stopEvent(event) {
+    return (
+      this._innerView !== undefined &&
+      event.target !== undefined &&
+      this._innerView.dom.contains(event.target)
+    );
+  }
+  ignoreMutation() {
+    return true;
+  }
+  // == Rendering ===================================== //
+  renderMath() {
+    if (!this._mathRenderElt) {
+      return;
+    }
+    // get tex string to render
+    let content = this._node.content.content;
+    let texString = '';
+    if (content.length > 0 && content[0].textContent !== null) {
+      texString = content[0].textContent.trim();
+    }
+    // empty math?
+    if (texString.length < 1) {
+      this.dom.classList.add('empty-math');
+      // clear rendered math, since this node is in an invalid state
+      while (this._mathRenderElt.firstChild) {
+        this._mathRenderElt.firstChild.remove();
+      }
+      // do not render empty math
+      return;
+    } else {
+      this.dom.classList.remove('empty-math');
+    }
+    // render katex, but fail gracefully
+    try {
+      katex.render(texString, this._mathRenderElt, this._katexOptions);
+      this._mathRenderElt.classList.remove('parse-error');
+      this.dom.setAttribute('title', '');
+    } catch (err) {
+      if (err instanceof ParseError) {
+        console.error(err);
+        this._mathRenderElt.classList.add('parse-error');
+        this.dom.setAttribute('title', err.toString());
+      } else {
+        throw err;
+      }
+    }
+  }
+  // == Inner Editor ================================== //
+  dispatchInner(tr) {
+    if (!this._innerView) {
+      return;
+    }
+    let { state, transactions } = this._innerView.state.applyTransaction(tr);
+    this._innerView.updateState(state);
+    if (!tr.getMeta('fromOutside')) {
+      let outerTr = this._outerView.state.tr,
+        offsetMap = StepMap.offset(this._getPos() + 1);
+      for (let i = 0; i < transactions.length; i++) {
+        let steps = transactions[i].steps;
+        for (let j = 0; j < steps.length; j++) {
+          let mapped = steps[j].map(offsetMap);
+          if (!mapped) {
+            throw Error('step discarded!');
+          }
+          outerTr.step(mapped);
+        }
+      }
+      if (outerTr.docChanged) this._outerView.dispatch(outerTr);
+    }
+  }
+  openEditor() {
+    if (this._innerView) {
+      throw Error('inner view should not exist!');
+    }
+    // create a nested ProseMirror view
+    this._innerView = new EditorView(this._mathSrcElt, {
+      state: EditorState.create({
+        doc: this._node,
+        plugins: [
+          keymap({
+            Tab: (state, dispatch) => {
+              if (dispatch) {
+                dispatch(state.tr.insertText('\t'));
+              }
+              return true;
+            },
+            Backspace: chainCommands(
+              deleteSelection,
+              (state, dispatch, tr_inner) => {
+                // default backspace behavior for non-empty selections
+                if (!state.selection.empty) {
+                  return false;
+                }
+                // default backspace behavior when math node is non-empty
+                if (this._node.textContent.length > 0) {
+                  return false;
+                }
+                // otherwise, we want to delete the empty math node and focus the outer view
+                this._outerView.dispatch(
+                  this._outerView.state.tr.insertText(''),
+                );
+                this._outerView.focus();
+                return true;
+              },
+            ),
+            Enter: newlineInCode,
+            'Ctrl-Enter': (state, dispatch) => {
+              let { to } = this._outerView.state.selection;
+              let outerState = this._outerView.state;
+              // place cursor outside of math node
+              this._outerView.dispatch(
+                outerState.tr.setSelection(
+                  TextSelection.create(outerState.doc, to),
+                ),
+              );
+              // must return focus to the outer view,
+              // otherwise no cursor will appear
+              this._outerView.focus();
+              return true;
+            },
+          }),
+        ],
+      }),
+      dispatchTransaction: this.dispatchInner.bind(this),
+    });
+    // focus element
+    let innerState = this._innerView.state;
+    this._innerView.focus();
+    // determine cursor position
+    let pos = this.cursorSide == 'start' ? 0 : this._node.nodeSize - 2;
+    this._innerView.dispatch(
+      innerState.tr.setSelection(TextSelection.create(innerState.doc, pos)),
+    );
+    this._isEditing = true;
+  }
+  /**
+   * Called when the inner ProseMirror editor should close.
+   *
+   * @param render Optionally update the rendered math after closing. (which
+   *    is generally what we want to do, since the user is done editing!)
+   */
+  closeEditor(render = true) {
+    if (this._innerView) {
+      this._innerView.destroy();
+      this._innerView = undefined;
+    }
+    if (render) {
+      this.renderMath();
+    }
+    this._isEditing = false;
+  }
+}
diff --git a/wax-prosemirror-plugins/src/math/math-plugin.js b/wax-prosemirror-plugins/src/math/math-plugin.js
new file mode 100644
index 0000000000000000000000000000000000000000..e9569035f1632a221b5f1e473df721acaf1eee06
--- /dev/null
+++ b/wax-prosemirror-plugins/src/math/math-plugin.js
@@ -0,0 +1,67 @@
+import { Plugin as ProsePlugin, PluginKey } from 'prosemirror-state';
+import { MathView } from './math-nodeview';
+/**
+ * Returns a function suitable for passing as a field in `EditorProps.nodeViews`.
+ * @param displayMode TRUE for block math, FALSE for inline math.
+ * @see https://prosemirror.net/docs/ref/#view.EditorProps.nodeViews
+ */
+function createMathView(displayMode) {
+  return (node, view, getPos) => {
+    /** @todo is this necessary?
+     * Docs says that for any function proprs, the current plugin instance
+     * will be bound to `this`.  However, the typings don't reflect this.
+     */
+    const pluginState = mathPluginKey.getState(view.state);
+    if (!pluginState) {
+      throw new Error('no math plugin!');
+    }
+    const nodeViews = pluginState.activeNodeViews;
+    // set up NodeView
+    const nodeView = new MathView(
+      node,
+      view,
+      getPos,
+      { katexOptions: { displayMode, macros: pluginState.macros } },
+      () => {
+        nodeViews.splice(nodeViews.indexOf(nodeView));
+      },
+    );
+    nodeViews.push(nodeView);
+    return nodeView;
+  };
+}
+const mathPluginKey = new PluginKey('prosemirror-math');
+const mathPluginSpec = {
+  key: mathPluginKey,
+  state: {
+    init(config, instance) {
+      return {
+        macros: {},
+        activeNodeViews: [],
+      };
+    },
+    apply(tr, value, oldState, newState) {
+      /** @todo (8/21/20)
+       * since new state has not been fully applied yet, we don't yet have
+       * information about any new MathViews that were created by this transaction.
+       * As a result, the cursor position may be wrong for any newly created math blocks.
+       */
+      const pluginState = mathPluginKey.getState(oldState);
+      if (pluginState) {
+        for (let mathView of pluginState.activeNodeViews) {
+          mathView.updateCursorPos(newState);
+        }
+      }
+      return value;
+    },
+  },
+  props: {
+    nodeViews: {
+      math_inline: createMathView(false),
+      math_display: createMathView(true),
+    },
+  },
+};
+const mathPlugin = new ProsePlugin(mathPluginSpec);
+
+export default mathPlugin;
diff --git a/wax-prosemirror-plugins/src/math/math-select.js b/wax-prosemirror-plugins/src/math/math-select.js
new file mode 100644
index 0000000000000000000000000000000000000000..9d9e874541f69e01299c2ed65e99af4f6a99a20c
--- /dev/null
+++ b/wax-prosemirror-plugins/src/math/math-select.js
@@ -0,0 +1,65 @@
+// prosemirror imports
+import { Plugin as ProsePlugin } from 'prosemirror-state';
+import { DecorationSet, Decoration } from 'prosemirror-view';
+
+/**
+ * Uses the selection to determine which math_select decorations
+ * should be applied to the given document.
+ * @param arg Should be either a Transaction or an EditorState,
+ *     although any object with `selection` and `doc` will work.
+ */
+
+const checkSelection = arg => {
+  const { from, to } = arg;
+  const { content } = arg.selection.content();
+  const result = [];
+  content.descendants((node, pos, parent) => {
+    if (node.type.name === 'text') {
+      return false;
+    }
+    if (node.type.name.startsWith('math_')) {
+      result.push({
+        start: Math.max(from + pos - 1, 0),
+        end: from + pos + node.nodeSize - 1,
+      });
+      return false;
+    }
+    return true;
+  });
+  return DecorationSet.create(
+    arg.doc,
+    result.map(({ start, end }) =>
+      Decoration.node(start, end, { class: 'math-select' }),
+    ),
+  );
+};
+/**
+ * Due to the internals of KaTeX, by default, selecting rendered
+ * math will put a box around each individual character of a
+ * math expression.  This plugin attempts to make math selections
+ * slightly prettier by instead setting a background color on the node.
+ *
+ * (remember to use the included math.css!)
+ *
+ * @todo (6/13/20) math selection rectangles are not quite even with text
+ */
+const mathSelectPlugin = new ProsePlugin({
+  state: {
+    init(config, partialState) {
+      return checkSelection(partialState);
+    },
+    apply(tr, oldState) {
+      if (!tr.selection || !tr.selectionSet) {
+        return oldState;
+      }
+      const sel = checkSelection(tr);
+      return sel;
+    },
+  },
+  props: {
+    decorations: state => {
+      return mathSelectPlugin.getState(state);
+    },
+  },
+});
+export default mathSelectPlugin;
diff --git a/wax-prosemirror-services/src/MathService/MathService.js b/wax-prosemirror-services/src/MathService/MathService.js
index e194427dc9f64bdd5ff35a21f6e8a65ce5cf8168..267aa147ee98f46d00a64d1b69adb91e0c2aed3a 100644
--- a/wax-prosemirror-services/src/MathService/MathService.js
+++ b/wax-prosemirror-services/src/MathService/MathService.js
@@ -1,9 +1,14 @@
+import { mathPlugin, mathSelectPlugin } from 'wax-prosemirror-plugins';
 import Service from '../Service';
 
 class MathService extends Service {
   name = 'MathService';
 
-  boot() {}
+  boot() {
+    this.app.PmPlugins.add('mathplugin', mathPlugin);
+    this.app.PmPlugins.add('mathselectplugin', mathSelectPlugin);
+    console.log(this.app);
+  }
 
   register() {}
 }