Repository: LIVVkit/LIVVkit (language: python)

Issue:
Refactor LIVVkit elements
Jupyter notebooks can render custom html if a class implements the `_repr_html` method. If the element definitions were refactored as objects they could include `_repr_html` methods which would just call the relevant javascript functions inside a script tag.  

If this was done I think the TeX helper module could be merged into the main element subclasses as well.

@jhkennedy I just randomly had this idea after learning about `_repr_html` and figured I'd throw it out there.  Feel free to close if you don't think it's a good/feasible addition.
Provide a LatexHelper class
_From @arbennett on July 29, 2016 18:29_

It would be nice if we could provide Latex output alongside the web output.  A straightforward way to do this would be to have a new class `LatexHelper` that would have functions to translate the results of the `ElementHelper` class's functions from Python dictionaries into the relevant  Latex code.


_Copied from original issue: ACME-Climate/LIVV#77_
Add element for V-H Table
Currently, there is a `js` method to render a table with both vertical and horizontal headers (`V-H Table`), but there is no associated LIVVkit element. 


Discussion / hints:
@arbennett this is a great idea; some of the elements have been getting more complex, so the shift to class elements was likely going to be happening anyways. 
Actually, along this line, I was also considering shifting the website generation to a static site generator -- possibly just using sphinx (since we use it for docs) or something like pelican/lektor. Either way, having element classes and implementing some `_repr_*` methods will likely happen. 



Fix patch:
diff --git a/.coveragerc b/.coveragerc
new file mode 100644
index 0000000..a098ddd
--- /dev/null
+++ b/.coveragerc
@@ -0,0 +1,4 @@
+[run]
+branch = True
+[report]
+ignore_errors = True
\ No newline at end of file
diff --git a/.deepsource.toml b/.deepsource.toml
new file mode 100644
index 0000000..506f458
--- /dev/null
+++ b/.deepsource.toml
@@ -0,0 +1,21 @@
+version = 1
+
+test_patterns = [
+  'tests/test_*.py'
+]
+
+exclude_patterns = [
+  'livvkit/resources/js/*.min.js',
+  'livvkit/resources/css/*.min.css',
+  'livvkit/resources/imgs/*',
+  'livvkit/resources/imgs/*',
+  'livvkit/data/*'
+]
+
+[[analyzers]]
+name = 'python'
+enabled = true
+
+[[analyzers]]
+name = 'test-coverage'
+enabled = true
\ No newline at end of file
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
new file mode 100644
index 0000000..d4dccc6
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -0,0 +1,26 @@
+---
+name: Bug report
+about: Create a report to help us improve
+
+---
+
+Describe your issue here.
+
+## Your environment
+
+* version of LIVVkit
+* Did you install LIVVkit via pypi, conda, or was it provided to you? 
+* What type of machine are you working on?
+
+## Steps to reproduce
+
+* How can we reproduce this issue?  
+* Do we need any external data to reproduce this issue? 
+* If so, where can we find it?
+
+## LIVVkit output
+
+```
+Please copy and past the command you used and **all** following output here
+
+```
diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md
new file mode 100644
index 0000000..066b2d9
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature_request.md
@@ -0,0 +1,17 @@
+---
+name: Feature request
+about: Suggest an idea for this project
+
+---
+
+**Is your feature request related to a problem? Please describe.**
+A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
+
+**Describe the solution you'd like**
+A clear and concise description of what you want to happen.
+
+**Describe alternatives you've considered**
+A clear and concise description of any alternative solutions or features you've considered.
+
+**Additional context**
+Add any other context or screenshots about the feature request here.
diff --git a/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
new file mode 100644
index 0000000..96f8e2d
--- /dev/null
+++ b/.github/PULL_REQUEST_TEMPLATE/pull_request_template.md
@@ -0,0 +1,42 @@
+## Proposed changes
+
+Describe the big picture of your changes here to communicate to the maintainers 
+why we should accept this pull request. If it fixes a bug or resolves a feature 
+request, be sure to link to that issue.
+
+## Types of changes
+
+What types of changes does your code introduce to LIVVkit?
+_Put an `x` in the boxes that apply_
+
+- [ ] Bugfix (non-breaking change which fixes an issue)
+- [ ] New feature (non-breaking change which adds functionality)
+- [ ] Breaking change (fix or feature that would cause existing functionality to 
+      not work as expected)
+
+## Checklist
+🚨Please review the [guidelines for contributing](../docs/CONTRIBUTING.rst) to 
+this repository.
+
+_Put an `x` in the boxes that apply. You can also fill these out after creating 
+the PR. If you're unsure about any of them, don't hesitate to ask. We're here to 
+help! This is simply a reminder of what we are going to look for before merging 
+your code._
+
+- [ ] Make sure you are requesting to **pull a topic/feature/bugfix branch** (right side). Don't request your master!
+- [ ] Make sure you are making a pull request against the `develop` branch (left side). Also you should start *your branch* off *our* `develop`.
+- [ ] Created tests which fail without the change (if possible)
+- [ ] I have added necessary documentation (if appropriate)
+- [ ] All tests passing
+
+
+
+## Further comments
+
+If this is a relatively large or complex change, kick off the discussion by 
+explaining why you chose the solution you did and what alternatives you 
+considered, etc...
+
+
+
+## Thank you!
\ No newline at end of file
diff --git a/.travis.yml b/.travis.yml
index 873d4a5..d01c065 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -1,8 +1,22 @@
-sudo: false
 language: python
+dist: xenial
 python:
-  - "2.7"
-  - "3.5"
+  # Only legacy python versions here; latest included below
   - "3.6"
-install: pip install tox-travis
-script: tox
+  - "3.7"
+install:
+  - pip install tox-travis
+script:
+  - tox
+
+jobs:
+  include:
+    - stage: test
+      python: "3.8"
+      before_script:
+        - npm install -g jsdoc
+        - pip install -e .[develop]
+      script:
+        - pytest --cov=livvkit --cov-config=.coveragerc --cov-report xml
+        - curl https://deepsource.io/cli | sh
+        - ./bin/deepsource report --analyzer test-coverage --key python --value-file ./coverage.xml
diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md
new file mode 100644
index 0000000..aab074c
--- /dev/null
+++ b/CODE_OF_CONDUCT.md
@@ -0,0 +1,46 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment include:
+
+* Using welcoming and inclusive language
+* Being respectful of differing viewpoints and experiences
+* Gracefully accepting constructive criticism
+* Focusing on what is best for the community
+* Showing empathy towards other community members
+
+Examples of unacceptable behavior by participants include:
+
+* The use of sexualized language or imagery and unwelcome sexual attention or advances
+* Trolling, insulting/derogatory comments, and personal or political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or electronic address, without explicit permission
+* Other conduct which could reasonably be considered inappropriate in a professional setting
+
+## Our Responsibilities
+
+Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
+
+Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
+
+## Scope
+
+This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at kennedyjh@ornl.gov. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
+
+Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
+
+[homepage]: http://contributor-covenant.org
+[version]: http://contributor-covenant.org/version/1/4/
diff --git a/LIVVkit.yml b/LIVVkit.yml
index 2318df8..dab1c72 100644
--- a/LIVVkit.yml
+++ b/LIVVkit.yml
@@ -10,8 +10,10 @@ dependencies:
 - netcdf4
 - matplotlib
 - json_tricks==3.11.0
+- pybtex==0.21
 - nodejs
 - pytest
+- pytest-cov
 - tox
 - requests
 - pip:
diff --git a/MANIFEST.in b/MANIFEST.in
index f9aba00..ad3c1df 100644
--- a/MANIFEST.in
+++ b/MANIFEST.in
@@ -3,7 +3,10 @@ include LICENSE
 include LIVVkit.yml
 
 recursive-include livvkit *.json
-recursive-include livvkit/data *
-recursive-include livvkit/resources *
 
-recursive-include hpcs *
+graft livvkit/data
+graft livvkit/resources
+graft livvkit/elements/templates
+
+global-exclude *.py[co]
+
diff --git a/docs/contrib.rst b/docs/CONTRIBUTING.rst
similarity index 55%
rename from docs/contrib.rst
rename to docs/CONTRIBUTING.rst
index a5d2e5a..776cd42 100644
--- a/docs/contrib.rst
+++ b/docs/CONTRIBUTING.rst
@@ -20,8 +20,9 @@ There are two encouraged ways to contribute to the development of LIVVkit:
 ^^^^^^^^^^^^^^^^^^^^^^^^^^
 
 If you have many contributions and are planning on being an active developer, you may request to
-become a core developer by contacting `Joseph H. Kennedy <kennedyjh@ornl.gov>`_ and briefely
-describing your intended contribution goals. Once approved, you will be given push access to the
+become a core developer by `opening an issue on github
+<https://github.com/livvkit/livvkit/issues>`_ and briefly
+describ your intended contribution goals. Once approved, you will be given push access to the
 ``LIVVkit/LIVVkit`` repository (and any other needed repositories). 
 
 When developing the code, please follow the `GitFlow workflow
@@ -43,7 +44,6 @@ First, go to the `LIVVkit github page <https://github.com/LIVVkit/LIVVkit>`_ and
 button on the top right of the page. This will create a fork of LIVVkit in your profile page. Clone
 the fork, make your changes, merge them to development branch, and then submit a pull request to our
 public repository. 
-   
 
 If you have any questions, concerns, requests, etc., open an issue in our `public issues queue
 <https://github.com/LIVVkit/LIVVkit/issues>`_, and we will help you out. 
@@ -52,7 +52,58 @@ If you have any questions, concerns, requests, etc., open an issue in our `publi
 Code Guidelines
 ---------------
 
-.. include:: code-guidelines.rst
+LIVVkit 3.0+ will be developed and tested on the three latest Python versions (3.6--3.8 for LIVVkit 3.0).
+Additionally, We generally follow `PEP 8 guidelines <https://www.python.org/dev/peps/pep-0008/>`__
+as presented at `pep8.org <http://pep8.org/>`__, with the exception of line lengths:
+lines have a soft limit of 100 characters and a hard limit of 120 characters when
+extra length improves readability. We use the
+`Google Style <https://github.com/google/styleguide/blob/gh-pages/pyguide.md#38-comments-and-docstrings>`__
+for our Docstrings. A sample code snippet below highlights most of our
+coding conventions.
+
+.. code-block:: python
+
+    # coding=utf-8
+    # LICENSE...
+
+    """
+    Description of module
+    """
+
+    import pymodule
+    import pymodule2
+
+    import livvmodule
+
+
+    class ClassName(object):
+        """
+        Class descriptions
+        """
+
+
+        def __init__(self):
+            """ Constructor """
+            self.var = "value"
+            self.auto = "nalue"
+            self.bagger = "salue"
+            self.autocrummify = "dalue"
+
+
+        @functionAnnotation
+        def foo(self, bar, baz):
+            """
+            A description of foo.
+
+            Args:
+                bar: What is this.
+                baz: What is this.
+            Returns:
+                a combo of bar and baz
+            """
+            # Some extra logic
+            return bar + baz
+
 
 
 .. include:: README.rst
diff --git a/docs/README.rst b/docs/README.rst
index f74c803..56bc292 100644
--- a/docs/README.rst
+++ b/docs/README.rst
@@ -10,8 +10,19 @@ To build this documentation you will need to install some additional tools:
 
 
 We recommend you use ``npm``, a `Javascript package manager <https://www.npmjs.com/get-npm>`_, to install
-``JSDoc`` and ``pip`` to install the Sphinx packages (``sphinx`` itself can be installed
-via ``conda`` as well, but the others will still need ``pip``).
+``JSDoc``:
+
+.. code-block:: bash
+
+    npm install -g jsdoc
+
+and ``pip`` to install the development version of LIVVkit:
+
+.. code-block:: bash
+
+    git clone git@github.com:LIVVkit/LIVVkit.git
+    cd LIVVkit
+    pip install -e .[develop]
 
 To build the current version of the documentation website, simply run:
 
@@ -34,7 +45,7 @@ running:
     If you run this command you will need to regenerate all the auto-generated files! ``make clean`` will
     only remove the old website and leave the autogenerated files in place. 
 
-Then, auto-generate the documentation of LIVVkit's python source code:
+Then, auto-generate the documentation of LIVVkit's Python source code:
 
 .. code-block:: bash
     
diff --git a/docs/_static/layout.png b/docs/_static/layout.png
index 7f78c1b..d87f871 100644
Binary files a/docs/_static/layout.png and b/docs/_static/layout.png differ
diff --git a/docs/_static/layout.svg b/docs/_static/layout.svg
index f717064..2ba800a 100644
--- a/docs/_static/layout.svg
+++ b/docs/_static/layout.svg
@@ -13,11 +13,11 @@
    height="540"
    id="svg2"
    version="1.1"
-   inkscape:version="0.48.4 r9939"
+   inkscape:version="0.92.4 (5da689c313, 2019-01-14)"
    sodipodi:docname="layout.svg"
-   inkscape:export-filename="/home/fjk/Documents/Code/LIVVkit/livv-dev/docs/imgs/layout.png"
-   inkscape:export-xdpi="90"
-   inkscape:export-ydpi="90">
+   inkscape:export-filename="/home/jhkennedy/Documents/Code/LIVVkit/livv-dev/docs/_static/layout.png"
+   inkscape:export-xdpi="96"
+   inkscape:export-ydpi="96">
   <defs
      id="defs4">
     <marker
@@ -99,16 +99,16 @@
      inkscape:pageopacity="0.0"
      inkscape:pageshadow="2"
      inkscape:zoom="1.4"
-     inkscape:cx="364.08596"
+     inkscape:cx="230.87167"
      inkscape:cy="261.27574"
      inkscape:document-units="in"
-     inkscape:current-layer="layer1"
+     inkscape:current-layer="g3511"
      showgrid="false"
      units="in"
-     inkscape:window-width="1280"
-     inkscape:window-height="1000"
-     inkscape:window-x="1680"
-     inkscape:window-y="24"
+     inkscape:window-width="3440"
+     inkscape:window-height="1376"
+     inkscape:window-x="0"
+     inkscape:window-y="27"
      inkscape:window-maximized="1" />
   <metadata
      id="metadata7">
@@ -118,7 +118,7 @@
         <dc:format>image/svg+xml</dc:format>
         <dc:type
            rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
-        <dc:title></dc:title>
+        <dc:title />
       </cc:Work>
     </rdf:RDF>
   </metadata>
@@ -297,16 +297,16 @@
            inkscape:connector-curvature="0" />
       </g>
       <text
-         sodipodi:linespacing="125%"
          id="text3931"
          y="604.422"
          x="246.02679"
-         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:53.722332px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
          xml:space="preserve"><tspan
            y="604.422"
            x="246.02679"
            id="tspan3933"
-           sodipodi:role="line">LIVV</tspan></text>
+           sodipodi:role="line"
+           style="font-size:53.722332px;line-height:1.25">LIVV</tspan></text>
     </g>
     <path
        style="fill:none;stroke:#000000;stroke-width:4.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send)"
@@ -323,7 +323,7 @@
     <g
        id="g3511">
       <g
-         id="g3478">
+         id="g1059">
         <rect
            style="fill:#aaeeff;fill-opacity:1;stroke:#2ad4ff;stroke-width:1"
            id="rect10127-9"
@@ -344,163 +344,162 @@
            ry="2" />
         <path
            style="fill:#aaeeff;fill-opacity:1;stroke:#2ad4ff;stroke-width:1"
-           d="m 534.04902,997.06878 144.56704,0 c 2.50421,0 4.02407,2.36314 5.29606,4.52022 l 12.78482,21.6811 c 1.272,2.1571 -2.01601,4.5202 -4.52021,4.5202 l -171.68836,0 c -2.5042,0 -5.84815,-2.3971 -4.52023,-4.5202 l 13.56066,-21.6811 c 1.32794,-2.12312 2.01602,-4.52022 4.52022,-4.52022 z"
+           d="m 534.04902,997.06878 h 144.56704 c 2.50421,0 4.02407,2.36314 5.29606,4.52022 l 12.78482,21.6811 c 1.272,2.1571 -2.01601,4.5202 -4.52021,4.5202 H 520.48837 c -2.5042,0 -5.84815,-2.3971 -4.52023,-4.5202 l 13.56066,-21.6811 c 1.32794,-2.12312 2.01602,-4.52022 4.52022,-4.52022 z"
            id="rect10131-7"
            inkscape:connector-curvature="0"
            sodipodi:nodetypes="sssssssss" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 530.00388,1019.3511 3.40569,-6.2889 11.71131,0 -2.87697,6.2889 z"
+           d="m 530.00388,1019.3511 3.40569,-6.2889 h 11.71131 l -2.87697,6.2889 z"
            id="path10174-4"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 533.40957,1013.0622 3.55635,-6.5672 11.15918,0 -3.00422,6.5672 z"
+           d="m 533.40957,1013.0622 3.55635,-6.5672 h 11.15918 l -3.00422,6.5672 z"
            id="path10176-5"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 536.96592,1006.495 2.7195,-5.0218 10.73697,0 -2.29729,5.0218 z"
+           d="m 536.96592,1006.495 2.7195,-5.0218 h 10.73697 l -2.29729,5.0218 z"
            id="path10178-0"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 550.42239,1001.4732 11.20997,0 -1.85652,5.0218 -11.65074,0 z"
+           d="m 550.42239,1001.4732 h 11.20997 l -1.85652,5.0218 H 548.1251 Z"
            id="path10180-3"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 548.1251,1006.495 -3.00422,6.5672 12.22717,0 2.42779,-6.5672 z"
+           d="m 548.1251,1006.495 -3.00422,6.5672 h 12.22717 l 2.42779,-6.5672 z"
            id="path10182-6"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 545.12088,1013.0622 -2.87697,6.2889 12.77921,0 2.32493,-6.2889 z"
+           d="m 545.12088,1013.0622 -2.87697,6.2889 h 12.77921 l 2.32493,-6.2889 z"
            id="path10184-1"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 555.02312,1019.3511 12.91614,0 1.767,-6.2889 -12.35821,0 z"
+           d="m 555.02312,1019.3511 h 12.91614 l 1.767,-6.2889 h -12.35821 z"
            id="path10186-0"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 557.34805,1013.0622 2.42779,-6.5672 11.7756,0 -1.84518,6.5672 z"
+           d="m 557.34805,1013.0622 2.42779,-6.5672 h 11.7756 l -1.84518,6.5672 z"
            id="path10188-6"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 559.77584,1006.495 1.85652,-5.0218 11.33009,0 -1.41101,5.0218 -11.7756,0 z"
+           d="m 559.77584,1006.495 1.85652,-5.0218 h 11.33009 l -1.41101,5.0218 z"
            id="path10190-3"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 572.96245,1001.4732 11.77142,0 -0.94811,5.0218 -12.23432,0 z"
+           d="m 572.96245,1001.4732 h 11.77142 l -0.94811,5.0218 h -12.23432 z"
            id="path10192-2"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 571.55144,1006.495 -1.84518,6.5672 12.83964,0 1.23986,-6.5672 z"
+           d="m 571.55144,1006.495 -1.84518,6.5672 h 12.83964 l 1.23986,-6.5672 z"
            id="path10194-0"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 567.93926,1019.3511 1.767,-6.2889 69.72947,0 1.38106,6.2889 z"
+           d="m 567.93926,1019.3511 1.767,-6.2889 h 69.72947 l 1.38106,6.2889 z"
            id="path10210-6"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 582.5459,1013.0622 1.23986,-6.5672 12.51477,0 -0.62067,6.5672 z"
+           d="m 582.5459,1013.0622 1.23986,-6.5672 h 12.51477 l -0.62067,6.5672 z"
            id="path10212-1"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 583.78576,1006.495 0.94811,-5.0218 12.04128,0 -0.47462,5.0218 z"
+           d="m 583.78576,1006.495 0.94811,-5.0218 h 12.04128 l -0.47462,5.0218 z"
            id="path10214-5"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 595.67986,1013.0622 13.16575,0 0,-6.5672 -12.54508,0 -0.62067,6.5672 z"
+           d="m 595.67986,1013.0622 h 13.16575 v -6.5672 h -12.54508 z"
            id="path10216-5"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 596.30053,1006.495 0.47462,-5.0218 12.07046,0 0,5.0218 -12.54508,0 z"
+           d="m 596.30053,1006.495 0.47462,-5.0218 h 12.07046 v 5.0218 z"
            id="path10218-4"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 608.84561,1001.4732 14.19051,0 0.558,5.0218 -14.74851,0 z"
+           d="m 608.84561,1001.4732 h 14.19051 l 0.558,5.0218 h -14.74851 z"
            id="path10220-7"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 608.84561,1006.495 0,6.5672 15.47821,0 -0.7297,-6.5672 z"
+           d="m 608.84561,1006.495 v 6.5672 h 15.47821 l -0.7297,-6.5672 z"
            id="path10222-6"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 623.03612,1001.4732 13.8547,0 1.10277,5.0218 -14.39947,0 z"
+           d="m 623.03612,1001.4732 h 13.8547 l 1.10277,5.0218 h -14.39947 z"
            id="path10224-5"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 623.59412,1006.495 0.7297,6.5672 15.11191,0 -1.44214,-6.5672 z"
+           d="m 623.59412,1006.495 0.7297,6.5672 h 15.11191 l -1.44214,-6.5672 z"
            id="path10226-6"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 639.43573,1013.0622 1.38106,6.2889 14.8296,0 -2.02162,-6.2889 z"
+           d="m 639.43573,1013.0622 1.38106,6.2889 h 14.8296 l -2.02162,-6.2889 z"
            id="path10228-9"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 639.43573,1013.0622 -1.44214,-6.5672 13.52012,0 2.11106,6.5672 z"
+           d="m 639.43573,1013.0622 -1.44214,-6.5672 h 13.52012 l 2.11106,6.5672 z"
            id="path10230-3"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 637.99359,1006.495 -1.10277,-5.0218 13.00858,0 1.61431,5.0218 z"
+           d="m 637.99359,1006.495 -1.10277,-5.0218 h 13.00858 l 1.61431,5.0218 z"
            id="path10232-7"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 649.8994,1001.4732 12.76027,0 2.11605,5.0218 -13.26201,0 z"
+           d="m 649.8994,1001.4732 h 12.76027 l 2.11605,5.0218 h -13.26201 z"
            id="path10234-4"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 651.51371,1006.495 2.11106,6.5672 13.91816,0 -2.76721,-6.5672 z"
+           d="m 651.51371,1006.495 2.11106,6.5672 h 13.91816 l -2.76721,-6.5672 z"
            id="path10236-5"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 653.62477,1013.0622 2.02162,6.2889 14.54654,0 -2.65,-6.2889 z"
+           d="m 653.62477,1013.0622 2.02162,6.2889 h 14.54654 l -2.65,-6.2889 z"
            id="path10238-2"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 670.19293,1019.3511 13.62187,0 -6.62007,-12.8561 -12.41901,0 z"
+           d="m 670.19293,1019.3511 h 13.62187 l -6.62007,-12.8561 h -12.41901 z"
            id="path10240-5"
            inkscape:connector-curvature="0" />
         <path
            style="fill:#ffffff;fill-rule:evenodd;stroke:#2ad4ff;stroke-width:1px;stroke-linecap:butt;stroke-linejoin:miter;stroke-opacity:1"
-           d="m 664.77572,1006.495 -2.11605,-5.0218 11.94913,0 2.58593,5.0218 z"
+           d="m 664.77572,1006.495 -2.11605,-5.0218 h 11.94913 l 2.58593,5.0218 z"
            id="path10242-4"
            inkscape:connector-curvature="0" />
+        <text
+           xml:space="preserve"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+           x="555.1507"
+           y="954.66046"
+           id="text3931-7"><tspan
+             style="font-size:29.54728317px;line-height:1.25"
+             sodipodi:role="line"
+             id="tspan3933-4"
+             x="555.1507"
+             y="954.66046">Report</tspan></text>
       </g>
-      <text
-         sodipodi:linespacing="125%"
-         id="text3931-7"
-         y="954.85529"
-         x="553.40497"
-         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:53.722332px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
-         xml:space="preserve"><tspan
-           y="954.85529"
-           x="553.40497"
-           id="tspan3933-4"
-           sodipodi:role="line"
-           style="font-size:29.54728317px">Output</tspan></text>
     </g>
     <path
        style="fill:none;stroke:#000000;stroke-width:4.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:4.49999993, 4.49999993;stroke-dashoffset:0;stroke-opacity:1;marker-end:url(#Arrow1Send)"
@@ -519,16 +518,15 @@
        ry="1.9999999" />
     <text
        xml:space="preserve"
-       style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:21.31831169px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+       style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
        x="255.04721"
        y="796.28564"
-       id="text4821"
-       sodipodi:linespacing="125%"><tspan
+       id="text4821"><tspan
          sodipodi:role="line"
          id="tspan4823"
          x="255.04721"
          y="796.28564"
-         style="font-size:21.31831169px">Scheduler</tspan></text>
+         style="font-size:21.31831169px;line-height:1.25">Scheduler</tspan></text>
     <g
        id="g3599">
       <rect
@@ -541,13 +539,12 @@
          id="rect4819-7"
          style="fill:none;stroke:#9955ff;stroke-width:4.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
       <text
-         sodipodi:linespacing="125%"
          id="text4821-0"
          y="640.93842"
          x="45.434429"
-         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:31.97746849px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
          xml:space="preserve"><tspan
-           style="font-size:19.54178619px"
+           style="font-size:19.54178619px;line-height:1.25"
            y="640.93842"
            x="45.434429"
            id="tspan4823-9"
@@ -573,16 +570,15 @@
        ry="1.9999999" />
     <text
        xml:space="preserve"
-       style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:19.54178619px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+       style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
        x="30.805101"
        y="754.41754"
-       id="text4821-3"
-       sodipodi:linespacing="125%"><tspan
+       id="text4821-3"><tspan
          sodipodi:role="line"
          id="tspan4823-5"
          x="30.805101"
          y="754.41754"
-         style="font-size:19.54178619px">Components</tspan></text>
+         style="font-size:19.54178619px;line-height:1.25">Components</tspan></text>
     <g
        id="g3609">
       <rect
@@ -595,13 +591,12 @@
          id="rect4819-8"
          style="fill:#ffffff;stroke:#0066ff;stroke-width:4.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1" />
       <text
-         sodipodi:linespacing="125%"
          id="text4821-7"
          y="834.76324"
          x="53.123577"
-         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:19.54178619px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
          xml:space="preserve"><tspan
-           style="font-size:19.54178619px"
+           style="font-size:19.54178619px;line-height:1.25"
            y="834.76324"
            x="53.123577"
            id="tspan4823-92"
@@ -611,16 +606,16 @@
        id="g3577">
       <text
          transform="scale(0.54093575,1.8486484)"
-         sodipodi:linespacing="125%"
          id="text5018"
          y="455.60828"
          x="730.91357"
-         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:97.7677002px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
          xml:space="preserve"><tspan
            y="455.60828"
            x="730.91357"
            id="tspan5020"
-           sodipodi:role="line">{</tspan></text>
+           sodipodi:role="line"
+           style="font-size:97.7677002px;line-height:1.25">{</tspan></text>
       <g
          id="g3557">
         <rect
@@ -634,16 +629,15 @@
            ry="1.9999998" />
         <text
            xml:space="preserve"
-           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13.02785778px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
            x="449.68396"
            y="721.81433"
-           id="text4821-9"
-           sodipodi:linespacing="125%"><tspan
+           id="text4821-9"><tspan
              sodipodi:role="line"
              id="tspan4823-1"
              x="449.68396"
              y="721.81433"
-             style="font-size:13.02785778px">A1</tspan></text>
+             style="font-size:13.02785778px;line-height:1.25">A1</tspan></text>
       </g>
       <g
          id="g3562">
@@ -658,16 +652,15 @@
            ry="1.9999998" />
         <text
            xml:space="preserve"
-           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13.02785778px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
            x="449.73486"
            y="769.41516"
-           id="text4821-06"
-           sodipodi:linespacing="125%"><tspan
+           id="text4821-06"><tspan
              sodipodi:role="line"
              id="tspan4823-2"
              x="449.73486"
              y="769.41516"
-             style="font-size:13.02785778px">A2</tspan></text>
+             style="font-size:13.02785778px;line-height:1.25">A2</tspan></text>
       </g>
       <g
          id="g3567">
@@ -682,16 +675,15 @@
            ry="1.9999998" />
         <text
            xml:space="preserve"
-           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:13.02785778px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
            x="449.60446"
            y="816.83783"
-           id="text4821-75"
-           sodipodi:linespacing="125%"><tspan
+           id="text4821-75"><tspan
              sodipodi:role="line"
              id="tspan4823-922"
              x="449.60446"
              y="816.83783"
-             style="font-size:13.02785778px">A3</tspan></text>
+             style="font-size:13.02785778px;line-height:1.25">A3</tspan></text>
       </g>
       <g
          id="g3572">
@@ -706,29 +698,28 @@
            ry="1.9999999" />
         <text
            xml:space="preserve"
-           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:21.51856422px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+           style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
            x="447.47076"
            y="860.94489"
-           id="text4821-75-1"
-           sodipodi:linespacing="125%"><tspan
+           id="text4821-75-1"><tspan
              sodipodi:role="line"
              id="tspan4823-922-2"
              x="447.47076"
              y="860.94489"
-             style="font-size:21.51856422px">...</tspan></text>
+             style="font-size:21.51856422px;line-height:1.25">...</tspan></text>
       </g>
       <text
          transform="scale(-0.54093575,1.8486484)"
-         sodipodi:linespacing="125%"
          id="text5018-9"
          y="455.60828"
          x="-961.4549"
-         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:97.7677002px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+         style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
          xml:space="preserve"><tspan
            y="455.60828"
            x="-961.4549"
            id="tspan5020-3"
-           sodipodi:role="line">{</tspan></text>
+           sodipodi:role="line"
+           style="font-size:97.7677002px;line-height:1.25">{</tspan></text>
     </g>
     <rect
        style="fill:none;stroke:#008066;stroke-width:4.5;stroke-miterlimit:4;stroke-dasharray:none;stroke-dashoffset:0;stroke-opacity:1"
@@ -741,16 +732,15 @@
        ry="1.9999999" />
     <text
        xml:space="preserve"
-       style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;font-size:31.97746849px;line-height:125%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
+       style="font-style:normal;font-variant:normal;font-weight:500;font-stretch:normal;line-height:0%;font-family:KacstTitle;-inkscape-font-specification:'KacstTitle Medium';text-align:start;letter-spacing:0px;word-spacing:0px;text-anchor:start;fill:#000000;fill-opacity:1;stroke:none"
        x="572.2301"
        y="795.62335"
-       id="text4821-0-8"
-       sodipodi:linespacing="125%"><tspan
+       id="text4821-0-8"><tspan
          sodipodi:role="line"
          id="tspan4823-9-8"
          x="572.2301"
          y="795.62335"
-         style="font-size:19.54178619px">Collate</tspan></text>
+         style="font-size:19.54178619px;line-height:1.25">Collate</tspan></text>
     <path
        style="fill:none;stroke:#000000;stroke-width:4.5;stroke-linecap:butt;stroke-linejoin:miter;stroke-miterlimit:4;stroke-dasharray:none;stroke-opacity:1;marker-end:url(#Arrow1Send)"
        d="m 606.32328,832.3726 0,48.1777"
diff --git a/docs/api.rst b/docs/api.rst
index 1895fe7..1905c77 100644
--- a/docs/api.rst
+++ b/docs/api.rst
@@ -16,18 +16,5 @@ Web Frontend
 
 .. js:autofunction:: drawNav
 .. js:autofunction:: drawContent
-.. js:autofunction:: drawError
-.. js:autofunction:: drawSummary
-.. js:autofunction:: drawValSummary
-.. js:autofunction:: drawDiff
-.. js:autofunction:: drawBitForBit
-.. js:autofunction:: drawGallery
-.. js:autofunction:: drawImage
-.. js:autofunction:: drawLightbox
-.. js:autofunction:: drawThumbnail
-.. js:autofunction:: drawTable
-.. js:autofunction:: drawVTable
-.. js:autofunction:: drawVHTable
-.. js:autofunction:: drawHTML
 .. js:autofunction:: loadJSON
 
diff --git a/docs/bundles.rst b/docs/bundles.rst
index 9682aaa..6020ac9 100644
--- a/docs/bundles.rst
+++ b/docs/bundles.rst
@@ -8,12 +8,12 @@ Model Bundles
 
 .. warning::
 
-    The model bundle abstraction is undergoing an overhaul (slated for LIVVkit 2.2) to make adding
+    The model bundle abstraction is undergoing an overhaul (slated for LIVVkit 3.1) to make adding
     new models significantly easier. We are actively working on supporting
     MALI (`MPAS-Albany Land Ice <http://mpas-dev.github.io/land_ice/download.html>`_),
-    PISM (`Parallel Ice Sheet Model <http://pism-docs.org/wiki/doku.php>`_),
-    and others. If you'd like to help or request support for your model, please contact
-    Joseph H. Kennedy at kennedyjh@ornl.gov.
+    BISICLES (`Parallel Ice Sheet Model <https://commons.lbl.gov/display/bisicles/BISICLES>`_),
+    and others. If you'd like to help or request support for your model, please `open an issue on github
+    <https://github.com/livvkit/livvkit/issues>`_.
 
 
 Adding new models requires a new implementation in the ``bundles`` directory.  Currently, ``CISM_glissade``
@@ -23,7 +23,7 @@ entire directory to a new directory where the name should indicate the model bei
 
 .. warning::
 
-    The bundle's name needs to follow the python naming convention for a python package (see
+    The bundle's name needs to follow the Python naming convention for a Python package (see
     :pep:`8#package-and-module-names`).
 
 The new bundle will need to be edited for use each component (verification, performance, numerics,
diff --git a/docs/code-guidelines.rst b/docs/code-guidelines.rst
deleted file mode 100644
index 0f57b34..0000000
--- a/docs/code-guidelines.rst
+++ /dev/null
@@ -1,24 +0,0 @@
-Until `LIVVkit 3.0 <https://github.com/LIVVkit/LIVVkit/milestone/3>`__
-is released, all code should be written as python 2 and 3 compatible
-code -- see http://python-future.org/compatible_idioms.html.  Additionally, We
-generally follow `PEP 8 guidelines <https://www.python.org/dev/peps/pep-0008/>`__
-as presented at `pep8.org <http://pep8.org/>`__, with the exception of line lengths:
-lines have a soft limit of 100 characters and a hard limit of 120 characters when
-extra length improves readability.  See of a very nice presentation of the PEP 8
-style guide.  A sample code snippet below highlights most of our
-coding conventions.
-
-.. literalinclude:: style.py
-    :language: python
-
-Tips
-----
-
-- Each source file containing functions that are called by the main ``livv.py`` script should have
-  the license header. (There should be a script to fix when this is forgotten in util).
-- Global variables should be stored in the module ``util.variables``.  When adding new global
-  variables list them in that module with some basic default value so that it is easy for others to
-  see which variables may be used by outside modules.
-- When refering to system paths use the ``os.sep`` variable or ``os.path.join()`` instead of
-  relying on whichever version of path separator your system uses.
-
diff --git a/docs/design.rst b/docs/design.rst
index f244513..d0f0fd1 100644
--- a/docs/design.rst
+++ b/docs/design.rst
@@ -32,15 +32,15 @@ Therefore, LIVVkit strives to meet these core **usage requirements**:
   LIVVkit should be able to perform the desired sets of analyses with minimal overhead and time to
   results.
 
-Similarly, LIVVkit strives to meet these core **output requirements**:
+Similarly, LIVVkit strives to meet these core **reporting requirements**:
 
 **Informative**
-  The analysis results should be described in a quantified manner, using appropriate metrics that
+  Analysis results should be described in a quantified manner, using appropriate metrics that
   indicate the degree of certainty, *and* provide the necessary context to make the figures, metrics
   and other results understandable.
 
 **Shareable**
-  The results should be easy to share, publish, and examine using common tools. 
+  Results should be easy to share, publish, and examine using common tools.
 
 If you find LIVVkit isn't working for you, meeting these requirements, or have other suggestions for
 improvement, please let us know by `opening an issue on github
@@ -53,18 +53,16 @@ Architecture
     :align: center
     :alt: Schematic of LIVVkit's architecture and program flow.
 
-    Schematic of LIVVkit’s architecture and program flow [Kennedy2017]_. The light blue computers
-    indicate where the user will interact with LIVVkit, first using the livv interface and then
-    finally viewing the web output. The green boxes indicate core LIVVkit functionality, the black
-    boxes are the different analyses which are executed in parallel, and the blue and purple boxes
-    represent the ice-sheet model description bundles and any extensions analyses that may have been
-    requested, respectively. Programmatically, the livv interface is used to schedule a series of
-    analyses; the scheduler then pulls the requested analysis codes from the required bundles,
-    components, and extensions; and executes the analyses. As each analysis finishes, some
-    high-level summary information will be displayed on the command line through the interface and
-    fully detailed information will be collated into the output website. Once all analyses are
-    finished, the interface will inform the user and the results of all the analyses can be viewed
-    on the output website.
+    Schematic of LIVVkit’s architecture and program flow [Kennedy2017]_. The light blue computers indicate where the
+    user will interact with LIVVkit, first using the ``livv`` interface and then finally viewing the analyses report,
+    typically as a portable website. The green boxes indicate core LIVVkit functionality, the black boxes are the
+    different analyses which are executed in parallel, and the blue and purple boxes represent the ice-sheet model
+    description bundles and any extensions analyses that may have been requested, respectively. Programmatically, the
+    livv interface is used to schedule a series of analyses; the scheduler then pulls the requested analysis codes from
+    the required bundles, components, and extensions; and executes the analyses. As each analysis finishes, some
+    high-level summary information will be displayed on the command line through the interface and fully detailed
+    information will be collated into the report. Once all analyses are finished, the interface will inform the
+    user and the results of all the analyses can be viewed.
 
 
 Components
@@ -99,8 +97,11 @@ The major components of LIVVkit are:
 **Utilities**
   All the functionality that falls outside the V&V paradigm is contained in:
 
+  - :py:mod:`livvkit.elements`: LIVVkit element classes used to place figure/table/etc. elements into
+    the report.
+
   - :py:mod:`livvkit.util`: utility classes and functions for dealing with general LIVVkit data
-    manipulations, JSON element skeletons for output data, general I/O, etc. 
+    manipulations, general I/O, etc.
 
   - :py:mod:`livvkit.resources`: HTML, CSS, Javascript, and image resources for generating the
     output website.
diff --git a/docs/faq.rst b/docs/faq.rst
index 774b390..8a0b862 100644
--- a/docs/faq.rst
+++ b/docs/faq.rst
@@ -6,8 +6,8 @@
 Frequently Asked Questions
 ==========================
 
-Why is the output website blank? 
---------------------------------
+Why does the output website look weird and/or blank?
+----------------------------------------------------
 
 Web browsers are disabling the use of local resources, like javascript, when viewing local files
 (``file://``) due to possible security risks. As of writing this (2017/05/11), Firefox and Safari
diff --git a/docs/index.rst b/docs/index.rst
index ca1638e..d02eee8 100644
--- a/docs/index.rst
+++ b/docs/index.rst
@@ -5,7 +5,7 @@
 
 Introduction
 ============
-LIVVkit, the land ice verification & validation toolkit, is a python based V&V toolkit for
+LIVVkit, the land ice verification & validation toolkit, is a Python based V&V toolkit for
 computational ice sheet models, in both a stand-alone or coupled (to an Earth system model)
 configuration. It is intended to be a comprehensive testing suite that covers:
 
@@ -37,11 +37,15 @@ License and Attribution
 
 LIVVkit |version| is an open source project licensed under a BSD 3-clause License. We ask that you
 please acknowledge LIVVkit in any work it is used or supports. In any corresponding published
-work, please cite [Kennedy2017]_:
+work, please cite [Kennedy2017]_ and [Evans2019]_:
 
-    Kennedy, J.H., et al. (2017), LIVVkit: An extensible, python-based, land ice
+    Evans, K. J., et al. (2019), LIVVkit 2.1: Automated and extensible ice sheet model validation,
+    Geosci. Model Dev., 12, 1067–1086,
+    `DOI:10.5194/gmd-12-1067-2019 <https://doi.org/10.5194/gmd-12-1067-2019>`_.
+
+    Kennedy, J.H., et al. (2017), LIVVkit: An extensible, Python-based, land ice
     verification and validation toolkit for ice sheet models, J. Adv. Model. Earth Syst., 9,
-    `doi:10.1002/2017MS000916 <http://dx.doi.org/10.1002/2017MS000916>`_.
+    `DOI:10.1002/2017MS000916 <http://dx.doi.org/10.1002/2017MS000916>`_.
 
 
 User's Guide
@@ -57,7 +61,7 @@ User's Guide
     usage
     lex
     bundles
-    contrib
+    CONTRIBUTING
     api
     ref
     faq
diff --git a/docs/install.rst b/docs/install.rst
index 6b6b1ec..c81a3ea 100644
--- a/docs/install.rst
+++ b/docs/install.rst
@@ -10,22 +10,26 @@ Installation
 Basic
 -----
 
-The latest LIVVkit release can be installed via `Python pip <https://pip.pypa.io/en/stable/>`__:
+The latest LIVVkit release can be installed via `Anaconda/Miniconda <https://conda.io/docs/download.html>`__
+(preferred):
 
 .. code-block:: bash
 
-    pip isntall --user livvkit
+    conda install -c conda-forge livvkit
 
-or `Anaconda/Miniconda <https://conda.io/docs/download.html>`__: 
+or `Python pip <https://pip.pypa.io/en/stable/>`__:
 
 .. code-block:: bash
 
-    conda install -c conda-forge livvkit
+    pip isntall --user livvkit
+
 
+LIVVkit 3.0 is being developed and tested against Python 3.6+, although earlier versions may work.
+If you require Python 2, you may use LIVVkit 2, but it is no longer supported in conjunction with the
+`Python 2 End of Life <https://www.python.org/doc/sunset-python-2/>`_.
 
-LIVVkit was designed to be Python 2 and 3 compatible, so either version will work, but Python 3 is
-preferred (and highly recommended). If you're having problems installing LIVVkit, try the advanced
-installation below, and/or `open an issue on github <https://github.com/livvkit/livvkit/issues>`__.
+If you're having problems installing LIVVkit, try the advanced installation below, and/or
+`open an issue on github <https://github.com/livvkit/livvkit/issues>`__.
 
 
 Advanced
@@ -39,16 +43,15 @@ First, clone the source from GitHub:
     git clone https://github.com/LIVVkit/LIVVkit.git
 
 
-Then, you can install an "editable" version of LIVVkit into your python environment by: 
+Then, you can install an "editable" version of LIVVkit into a Python 3.6+ development environment by:
 
 .. code-block:: bash
 
     cd LIVVkit
-    pip install -e .
+    pip install -e .[develop]
 
 This will allow you to checkout different versions of LIVVkit, make changes, etc. and have those
-changes immediately reflected in your python environment. 
+changes immediately reflected in your Python environment.
 
-LIVVkit was designed to be Python 2 and 3 compatible, so it should install into an environment using
-either but Python 3 is preferred (and highly recommended). If you're having problems installing
-LIVVkit, `open an issue on github <https://github.com/livvkit/livvkit/issues>`__.
+If you're having problems installing/developing/using LIVVkit,
+`open an issue on github <https://github.com/livvkit/livvkit/issues>`__.
diff --git a/docs/lex.rst b/docs/lex.rst
index 18b7276..24d12c4 100644
--- a/docs/lex.rst
+++ b/docs/lex.rst
@@ -10,16 +10,16 @@ LIVVkit is extensible to more in-depth or larger validation analyses. However, b
 analyses are particularly data intensive, many of the observational and example model output files are
 much too large to distribute in the LIVVkit package. Therefore, we've developed a LIVVkit Extensions
 repository (LEX) which uses `git-lfs <https://git-lfs.github.com>`_ (Git Large File Support) in order to
-distribute the required data  [#]_. LEX holds a collection of  validation and custom analyses of ice sheet models and their associated Earth
-system models.
+distribute the required data  [#]_. LEX holds a collection of  validation and custom analyses of ice
+sheet models and their associated Earth system models.
 
-LEX was first described in [Evans2018]_; to reproduce the analyses there, see the
-`Reproducing Evans et al. (2018)`_ section.
+LEX was first described in [Evans2019]_; to reproduce the analyses there, see the
+`Reproducing Evans et al. (2019)`_ section.
+
+    Evans, K. J., et al. (2019), LIVVkit 2.1: Automated and extensible ice sheet model validation,
+    Geosci. Model Dev., 12, 1067–1086,
+    `DOI:10.5194/gmd-12-1067-2019 <https://doi.org/10.5194/gmd-12-1067-2019>`_.
 
-    Evans, K.J., J.H. Kennedy, D. Lu, M.M. Forrester, S. Price, J. Fyke,
-    A.R. Bennett, M.J. Hoffman, I. Tezaur, C.S. Zender, and M. Vizcaino (In Review).
-    LIVVkit 2.1: Automated and extensible ice sheet model validation.
-    *Geoscientific Model Development.*
 
 Dependencies
 ------------
@@ -81,12 +81,12 @@ that ``livv`` can find the required files.
 Likewise, you can also apply these analyses to any new model run [#]_ by adjusting
 the paths to point to your model run.
 
-Reproducing Evans et al. (2018)
+Reproducing Evans et al. (2019)
 -------------------------------
 
 If all the required `Dependencies`_ are installed, and you've cloned the repository
 into the directory ``lex``, you can reproduce all the figures and tables in
-[Evans2018]_ by running this command from within the ``lex`` directory:
+[Evans2019]_ by running this command from within the ``lex`` directory:
 
 .. code:: bash
 
@@ -113,7 +113,7 @@ Developing a custom extension
     we'll help you through the process.
 
 A template extension is provided as an absolute minimum working example
-in the ``examples/`` directory. To start developing a new extension, copy the
+in LEX's ``examples/`` directory. To start developing a new extension, copy the
 ``examples/template.*`` files to a (possibly new) relevant directory, and change
 these files' name to a descriptive name. These files will provide the basis for your
 new extension.
@@ -127,13 +127,13 @@ new extension.
 template.py:
 ^^^^^^^^^^^^
 
-This is the primary extension python module. In order to work with LIVVkit, the
+This is the primary extension Python module. In order to work with LIVVkit, the
 extension needs to provide a ``run(name, config)`` function which accepts two arguments:
 ``name``, the name of the extension which will be displayed on the extensions output
 webpage; and ``config`` which will contain the information in ``template.json``. This
-function will then need to return a LIVVkit page element (:func:`livvkit.util.elements.page`)
+function will then need to return a LIVVkit page element (:class:`livvkit.elements.elements.Page`)
 which will contain a summary description of the extension (typically the extensions docstring),
-and all the page elements to display (see :mod:`livvkit.util.elements`).
+and all the page elements to display (see :mod:`livvkit.elements`).
 
 
 template.json:
@@ -171,14 +171,14 @@ template.bib:
 
 The references that are relevant to this extension, and should be cited by anyone
 using the extension to support any work that will be published. These references
-will be displayed a the bottom of the extension's output webpage, and the reference
-list should minimally include [Kennedy2017]_ and [Evans2018]_.
+will be displayed a the bottom of the extension's output webpage, and will include
+[Kennedy2017]_ and [Evans2019]_ by default.
 
 
 template.yml:
 ^^^^^^^^^^^^^
 
-If you use any python packages/modules beyond the required LIVVkit python dependencies
+If you use any Python packages/modules beyond the required LIVVkit Python dependencies
 (see LIVVkit's ``setup.py``), you should list them in this `Anaconda style environment
 YAML file <https://conda.io/docs/user-guide/tasks/manage-environments.html?highlight=yml#create-env-file-manually>`__.
 By doing so, when LIVVkit runs an extension in an environment without the required
diff --git a/docs/quickstart.rst b/docs/quickstart.rst
index 7145da8..a8326a7 100644
--- a/docs/quickstart.rst
+++ b/docs/quickstart.rst
@@ -174,7 +174,7 @@ Both the verification and validation commands can be executed at the same time a
 be placed into the same website. Additionally, you can pass the ``-V/validate`` option multiple
 JSON configuration files, and it will run all of them. 
 
-For more information, see :doc:`install`, :doc:`usage`, and :doc:`contrib`.
+For more information, see :doc:`install`, :doc:`usage`, and :doc:`CONTRIBUTING`.
 
 
 ----------------------------------------------------------------------------------------------------
diff --git a/docs/ref.rst b/docs/ref.rst
index 24c7f4e..8b9b167 100644
--- a/docs/ref.rst
+++ b/docs/ref.rst
@@ -9,20 +9,23 @@ References and indices
 References
 ----------
 
-.. [Evans2018] Evans, K.J., J.H. Kennedy, D. Lu, M.M. Forrester, S. Price, J. Fyke,
-   A.R. Bennett, M.J. Hoffman, I. Tezaur, C.S. Zender, and M. Vizcaino (In Review).
-   LIVVkit 2.1: Automated and extensible ice sheet model validation.
-   *Geoscientific Model Development.*
+.. [Evans2019] Evans, K. J., et al. (2019), LIVVkit 2.1: Automated and extensible ice sheet model validation,
+    Geosci. Model Dev., 12, 1067–1086,
+    `DOI:10.5194/gmd-12-1067-2019 <https://doi.org/10.5194/gmd-12-1067-2019>`__.
 
-.. [Kennedy2017] Kennedy, J.H., et al. (2017), LIVVkit: An extensible, python-based, land ice
-   verification and validation toolkit for ice sheet models, J. Adv. Model. Earth Syst., 9,
-   `doi:10.1002/2017MS000916 <http://dx.doi.org/10.1002/2017MS000916>`__.
+
+.. [Kennedy2017] Kennedy, J.H., et al. (2017), LIVVkit: An extensible, Python-based, land ice
+    verification and validation toolkit for ice sheet models, J. Adv. Model. Earth Syst., 9,
+    `DOI:10.1002/2017MS000916 <http://dx.doi.org/10.1002/2017MS000916>`__.
 
 .. [Oberkampf2010] Oberkampf, W. L., & Roy, C. J. (2010). Verification and Validation
-   in Scientific Computing. Cambridge University Press.
+    in Scientific Computing. Cambridge University Press,
+    `DOI:10.1017/CBO9780511760396 <https://doi.org/10.1017/CBO9780511760396>`__,
+    `ISBN:9780511760396 <http://www.worldcat.org/oclc/933219483>`_
 
 .. [Roache1998] Roache., P.J. (1998). Verification and Validation in Computational \
-   Science and Engineering. Hermosa Publishers, Albuquerque, New Mexico.
+    Science and Engineering. Hermosa Publishers, Albuquerque, New Mexico,
+    `ISBN:9780913478080 <http://www.worldcat.org/oclc/440878443>`__.
 
 
 Indices
diff --git a/docs/source/livvkit.bundles.CISM_glissade.rst b/docs/source/livvkit.bundles.CISM_glissade.rst
index 1855622..daf1762 100644
--- a/docs/source/livvkit.bundles.CISM_glissade.rst
+++ b/docs/source/livvkit.bundles.CISM_glissade.rst
@@ -8,23 +8,23 @@ livvkit.bundles.CISM\_glissade.numerics module
 ----------------------------------------------
 
 .. automodule:: livvkit.bundles.CISM_glissade.numerics
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 livvkit.bundles.CISM\_glissade.verification module
 --------------------------------------------------
 
 .. automodule:: livvkit.bundles.CISM_glissade.verification
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 
 Module contents
 ---------------
 
 .. automodule:: livvkit.bundles.CISM_glissade
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.bundles.rst b/docs/source/livvkit.bundles.rst
index f05c5d6..e2f9c76 100644
--- a/docs/source/livvkit.bundles.rst
+++ b/docs/source/livvkit.bundles.rst
@@ -6,12 +6,12 @@ Subpackages
 
 .. toctree::
 
-    livvkit.bundles.CISM_glissade
+   livvkit.bundles.CISM_glissade
 
 Module contents
 ---------------
 
 .. automodule:: livvkit.bundles
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.components.rst b/docs/source/livvkit.components.rst
index e6776ce..5e11c12 100644
--- a/docs/source/livvkit.components.rst
+++ b/docs/source/livvkit.components.rst
@@ -6,7 +6,7 @@ Subpackages
 
 .. toctree::
 
-    livvkit.components.numerics_tests
+   livvkit.components.numerics_tests
 
 Submodules
 ----------
@@ -15,39 +15,39 @@ livvkit.components.numerics module
 ----------------------------------
 
 .. automodule:: livvkit.components.numerics
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 livvkit.components.performance module
 -------------------------------------
 
 .. automodule:: livvkit.components.performance
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 livvkit.components.validation module
 ------------------------------------
 
 .. automodule:: livvkit.components.validation
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 livvkit.components.verification module
 --------------------------------------
 
 .. automodule:: livvkit.components.verification
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 
 Module contents
 ---------------
 
 .. automodule:: livvkit.components
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.data.numerics.rst b/docs/source/livvkit.data.numerics.rst
index a0e10b0..a80c3b9 100644
--- a/docs/source/livvkit.data.numerics.rst
+++ b/docs/source/livvkit.data.numerics.rst
@@ -5,6 +5,6 @@ Module contents
 ---------------
 
 .. automodule:: livvkit.data.numerics
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.data.rst b/docs/source/livvkit.data.rst
index d50df6f..eaeee7d 100644
--- a/docs/source/livvkit.data.rst
+++ b/docs/source/livvkit.data.rst
@@ -6,12 +6,12 @@ Subpackages
 
 .. toctree::
 
-    livvkit.data.numerics
+   livvkit.data.numerics
 
 Module contents
 ---------------
 
 .. automodule:: livvkit.data
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.elements.rst b/docs/source/livvkit.elements.rst
new file mode 100644
index 0000000..64a7e8f
--- /dev/null
+++ b/docs/source/livvkit.elements.rst
@@ -0,0 +1,22 @@
+livvkit.elements package
+========================
+
+Submodules
+----------
+
+livvkit.elements.elements module
+--------------------------------
+
+.. automodule:: livvkit.elements.elements
+   :members:
+   :undoc-members:
+   :show-inheritance:
+
+
+Module contents
+---------------
+
+.. automodule:: livvkit.elements
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.resources.rst b/docs/source/livvkit.resources.rst
index 0828a30..f7b4cd5 100644
--- a/docs/source/livvkit.resources.rst
+++ b/docs/source/livvkit.resources.rst
@@ -5,6 +5,6 @@ Module contents
 ---------------
 
 .. automodule:: livvkit.resources
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.rst b/docs/source/livvkit.rst
index c668498..986251e 100644
--- a/docs/source/livvkit.rst
+++ b/docs/source/livvkit.rst
@@ -6,11 +6,12 @@ Subpackages
 
 .. toctree::
 
-    livvkit.bundles
-    livvkit.components
-    livvkit.data
-    livvkit.resources
-    livvkit.util
+   livvkit.bundles
+   livvkit.components
+   livvkit.data
+   livvkit.elements
+   livvkit.resources
+   livvkit.util
 
 Submodules
 ----------
@@ -19,15 +20,15 @@ livvkit.scheduler module
 ------------------------
 
 .. automodule:: livvkit.scheduler
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 
 Module contents
 ---------------
 
 .. automodule:: livvkit
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/source/livvkit.util.rst b/docs/source/livvkit.util.rst
index 46833d7..d12c3dc 100644
--- a/docs/source/livvkit.util.rst
+++ b/docs/source/livvkit.util.rst
@@ -8,71 +8,47 @@ livvkit.util.LIVVDict module
 ----------------------------
 
 .. automodule:: livvkit.util.LIVVDict
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
-livvkit.util.TexHelper module
------------------------------
+livvkit.util.bib module
+-----------------------
 
-.. automodule:: livvkit.util.TexHelper
-    :members:
-    :undoc-members:
-    :show-inheritance:
+.. automodule:: livvkit.util.bib
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 livvkit.util.colormaps module
 -----------------------------
 
 .. automodule:: livvkit.util.colormaps
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-livvkit.util.elements module
-----------------------------
-
-.. automodule:: livvkit.util.elements
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 livvkit.util.functions module
 -----------------------------
 
 .. automodule:: livvkit.util.functions
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-livvkit.util.make\_tex module
------------------------------
-
-.. automodule:: livvkit.util.make_tex
-    :members:
-    :undoc-members:
-    :show-inheritance:
-
-livvkit.util.netcdf module
---------------------------
-
-.. automodule:: livvkit.util.netcdf
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 livvkit.util.options module
 ---------------------------
 
 .. automodule:: livvkit.util.options
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 
 Module contents
 ---------------
 
 .. automodule:: livvkit.util
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/docs/style.py b/docs/style.py
deleted file mode 100644
index 56160fe..0000000
--- a/docs/style.py
+++ /dev/null
@@ -1,42 +0,0 @@
-# coding=utf-8
-# LICENSE...
-
-"""
-Description of module
-"""
-
-from __future__ import absolute_import, division, print_function, unicode_literals
-
-import pymodule
-import pymodule2
-
-import livvmodule
-
-
-class ClassName(object):
-    """
-    Class descriptions
-    """
-
-
-    def __init__(self):
-        """ Constructor """
-        self.var = "value"
-        self.auto = "nalue"
-        self.bagger = "salue"
-        self.autocrummify = "dalue"
-
-
-    @functionAnnotation
-    def foo(self, bar, baz):
-        """
-        A description of foo.
-
-        Args:
-            bar: What is this.
-            baz: What is this.
-        Returns:
-            a combo of bar and baz
-        """
-        # Some extra logic
-        return bar + baz
diff --git a/docs/usage.rst b/docs/usage.rst
index dc6dd5a..ebb4f75 100644
--- a/docs/usage.rst
+++ b/docs/usage.rst
@@ -16,7 +16,7 @@ into this development process like:
     :align: center
     :alt: Development cycle
 
-    Schematic of the public-private development cycle used by many scientfic modeling groups
+    Schematic of the public-private development cycle used by many scientific modeling groups
     [Kennedy2017]_. A feature branch is created (``d124``, green path) from the current development
     branch (light purple path) and incremental changes are made (``f001``--``f003``). These changes
     frequently undergo the standard set of LIVVkit verification tests (dashed cyan circles). Once
diff --git a/docs/vv-intro.rst b/docs/vv-intro.rst
index 15396fb..81db94c 100644
--- a/docs/vv-intro.rst
+++ b/docs/vv-intro.rst
@@ -15,7 +15,7 @@ Introduction to V&V
 
 Verification and validation (V&V) is a set of techniques that are used to build and quantify
 confidence in *something*. That something can be a *numerical model* or a piece of *software*, and
-building confidence in each of these will require the use of specialized teniques.
+building confidence in each of these will require the use of specialized techniques.
 
 Model V&V
 ---------
diff --git a/hpcs/NERSC/setup_cori b/hpcs/NERSC/setup_cori
deleted file mode 100755
index dcb84df..0000000
--- a/hpcs/NERSC/setup_cori
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/usr/bin/env bash
-
-# This combination of modules worked as of:
-#     July 7 2015
-# You may need to adjust these as systems change.
-
-module load python
-module load cray-netcdf
-module load netcdf4-python
-module load nco
-module load ncl
-
-module list
diff --git a/hpcs/OLCF/activate_rhea b/hpcs/OLCF/activate_rhea
deleted file mode 100755
index 6bb141a..0000000
--- a/hpcs/OLCF/activate_rhea
+++ /dev/null
@@ -1,24 +0,0 @@
-#!/usr/bin/env bash
-
-# This combination of modules worked as of:
-#     May 19 2017
-# You may need to adjust these as systems change.
-
-module unload PE-pgi PE-gnu PE-intel
-module load PE-gnu
-
-module load netcdf 
-module load hdf5-parallel
-
-module unload gcc
-module load gcc
-
-module load anaconda-cli106/rhea
-
-export PYTHONPATH=''
-
-# activate LIVVkit environment
-source activate LIVVkit
-
-module list
-
diff --git a/hpcs/OLCF/activate_titan b/hpcs/OLCF/activate_titan
deleted file mode 100755
index af1c2cf..0000000
--- a/hpcs/OLCF/activate_titan
+++ /dev/null
@@ -1,20 +0,0 @@
-#!/usr/bin/env bash
-
-# This combination of modules worked as of:
-#     June 5 2016
-# You may need to adjust these as systems change.
-
-module unload PrgEnv-pgi PrgEnv-gnu
-module load PrgEnv-gnu
-
-module load cray-netcdf-hdf5parallel
-module load gcc
-module load anaconda-cli106/titan
-
-export PYTHONPATH=''
-
-# activate LIVVkit environment
-source activate LIVVkit
-
-module list
-
diff --git a/hpcs/OLCF/rhea b/hpcs/OLCF/rhea
deleted file mode 100644
index 09bef5d..0000000
--- a/hpcs/OLCF/rhea
+++ /dev/null
@@ -1,6 +0,0 @@
-#%Module
-module-whatis "Anaconda for cli106 on Rhea"
-prereq PE-gnu
-conflict python python_anaconda anaconda-cli106
-set PREFIX /ccs/proj/cli106/conda/rhea
-prepend-path PATH $PREFIX/bin
diff --git a/hpcs/OLCF/setup_rhea b/hpcs/OLCF/setup_rhea
deleted file mode 100755
index 6c2d09f..0000000
--- a/hpcs/OLCF/setup_rhea
+++ /dev/null
@@ -1,22 +0,0 @@
-#!/usr/bin/env bash
-
-# This combination of modules worked as of:
-#     May 19 2017
-# You may need to adjust these as systems change.
-
-module unload PE-pgi PE-gnu PE-intel
-module load PE-gnu
-
-module load netcdf 
-module load hdf5-parallel
-
-module unload gcc
-module load gcc
-
-module load anaconda-cli106/rhea
-
-# make a anaconda environment for LIVVkit
-conda create --name LIVVkit python=3 ipython numpy scipy matplotlib netCDF4 pyproj basemap hdf4
-
-module list
-
diff --git a/hpcs/OLCF/setup_titan b/hpcs/OLCF/setup_titan
deleted file mode 100755
index 466a992..0000000
--- a/hpcs/OLCF/setup_titan
+++ /dev/null
@@ -1,17 +0,0 @@
-#!/usr/bin/env bash
-
-# This combination of modules worked as of:
-#     June 5 2016
-# You may need to adjust these as systems change.
-
-module unload PrgEnv-pgi PrgEnv-gnu
-module load PrgEnv-gnu
-
-module load cray-netcdf-hdf5parallel
-module load gcc
-module load anaconda-cli106/titan
-
-# make a anaconda environment for LIVVkit
-conda create --name LIVVkit python=3 ipython numpy scipy matplotlib netCDF4 pyproj basemap hdf4
-
-module list
diff --git a/hpcs/OLCF/titan b/hpcs/OLCF/titan
deleted file mode 100644
index 7bf5ee4..0000000
--- a/hpcs/OLCF/titan
+++ /dev/null
@@ -1,6 +0,0 @@
-#%Module
-module-whatis "Anaconda for cli106 on Titan"
-prereq PrgEnv-gnu
-conflict python python_anaconda anaconda-cli106
-set PREFIX /ccs/proj/cli106/conda/titan
-prepend-path PATH $PREFIX/bin
diff --git a/livvkit/__init__.py b/livvkit/__init__.py
index 2be4ce1..ad5a98b 100644
--- a/livvkit/__init__.py
+++ b/livvkit/__init__.py
@@ -39,7 +39,7 @@
 from livvkit import bundles
 from livvkit import resources
 
-__version_info__ = (2, 1, 6)
+__version_info__ = (3, 0, 0)
 __version__ = '.'.join(str(vi) for vi in __version_info__)
 
 cwd = os.getcwd()
@@ -76,4 +76,4 @@
 # optional flags
 verify = False
 validate = False
-publish = False
+pool_size = None
diff --git a/livvkit/__main__.py b/livvkit/__main__.py
index 50e2be8..d11c8dc 100755
--- a/livvkit/__main__.py
+++ b/livvkit/__main__.py
@@ -31,15 +31,21 @@
 Executable script to start a verification and validation test suite.
 Management of the tests to be run is handled by the scheduler in livvkit.util
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
 
 import os
 import sys
+import http.server as server
+import socketserver as socket
 
 import livvkit
 from livvkit.util import options
 
 
+if not sys.warnoptions:
+    import warnings
+    warnings.simplefilter("ignore")
+
+
 def main(cl_args=None):
     """ Direct execution. """
 
@@ -47,28 +53,27 @@ def main(cl_args=None):
         cl_args = sys.argv[1:]
     args = options.parse_args(cl_args)
 
-    print("-------------------------------------------------------------------")
-    print("                      __   _____   ___   ____    _ __     ")
-    print("                     / /  /  _/ | / / | / / /__ (_) /_    ")
-    print("                    / /___/ / | |/ /| |/ /  '_// / __/    ")
-    print("                   /____/___/ |___/ |___/_/\_\/_/\__/     ")
-    print("")
-    print("                   Land Ice Verification & Validation     ")
-    print("-------------------------------------------------------------------")
+    print(r"-------------------------------------------------------------------")
+    print(r"                      __   _____   ___   ____    _ __     ")
+    print(r"                     / /  /  _/ | / / | / / /__ (_) /_    ")
+    print(r"                    / /___/ / | |/ /| |/ /  '_// / __/    ")
+    print(r"                   /____/___/ |___/ |___/_/\_\/_/\__/     ")
+    print(r"")
+    print(r"                   Land Ice Verification & Validation     ")
+    print(r"-------------------------------------------------------------------")
     print("")
     print("  Current run: " + livvkit.timestamp)
     print("  User: " + livvkit.user)
     print("  OS Type: " + livvkit.os_type)
     print("  Machine: " + livvkit.machine)
-    print("  " + livvkit.comment)
 
     from livvkit.components import numerics
     from livvkit.components import verification
     from livvkit.components import performance
     from livvkit.components import validation
+    from livvkit import elements
     from livvkit import scheduler
     from livvkit.util import functions
-    from livvkit.util import elements
 
     summary_elements = []
 
@@ -91,7 +96,7 @@ def main(cl_args=None):
         for conf in livvkit.validation_model_configs:
             validation_config = functions.merge_dicts(validation_config,
                                                       functions.read_json(conf))
-        summary_elements.extend(scheduler.run_quiet(validation, validation_config,
+        summary_elements.extend(scheduler.run_quiet("validation", validation, validation_config,
                                                     group=False))
         print(" -----------------------------------------------------------------")
         print("   Validation test suite complete ")
@@ -99,25 +104,15 @@ def main(cl_args=None):
         print("")
 
     if livvkit.verify or livvkit.validate:
-        result = elements.page("Summary", "", element_list=summary_elements)
-        functions.write_json(result, livvkit.output_dir, "index.json")
+        result = elements.Page("Summary", "", summary_elements)
+        with open(os.path.join(livvkit.output_dir, 'index.json'), 'w') as index_data:
+            index_data.write(result._repr_json())
         print("-------------------------------------------------------------------")
         print(" Done!  Results can be seen in a web browser at:")
         print("  " + os.path.join(livvkit.output_dir, 'index.html'))
         print("-------------------------------------------------------------------")
 
     if args.serve:
-        try:
-            # Python 3
-            import http.server as server
-            import socketserver as socket
-        except ImportError:
-            # Python 2
-            # noinspection PyPep8Naming
-            import SimpleHTTPServer as server
-            # noinspection PyPep8Naming
-            import SocketServer as socket
-
         httpd = socket.TCPServer(('', args.serve), server.SimpleHTTPRequestHandler)
 
         sa = httpd.socket.getsockname()
diff --git a/livvkit/bundles/CISM_glissade/numerics.py b/livvkit/bundles/CISM_glissade/numerics.py
index 1fbd088..8419489 100644
--- a/livvkit/bundles/CISM_glissade/numerics.py
+++ b/livvkit/bundles/CISM_glissade/numerics.py
@@ -29,7 +29,6 @@
 """
 CISM_glissade module for numerics analysis
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
 
 import math
 
diff --git a/livvkit/bundles/CISM_glissade/verification.py b/livvkit/bundles/CISM_glissade/verification.py
index a432eec..f25d208 100644
--- a/livvkit/bundles/CISM_glissade/verification.py
+++ b/livvkit/bundles/CISM_glissade/verification.py
@@ -29,15 +29,13 @@
 """
 Provides CISM_glissade specific verification tools
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
-import six
 
 import os
 import numpy as np
 
-from six.moves.configparser import ConfigParser
+from configparser import ConfigParser
 
-from livvkit.util import elements
+from livvkit import elements
 
 
 def parse_log(file_path):
@@ -52,14 +50,9 @@ def parse_log(file_path):
         the results of the bit for bit testing
     """
     if not os.path.isfile(file_path):
-        return elements.error("Output Log", "Could not open file: " + file_path.split(os.sep)[-1])
+        return elements.Error("Output Log", "Could not open file: " + file_path.split(os.sep)[-1])
 
-    headers = ["Converged Iterations",
-               "Avg. Iterations to Converge",
-               "Processor Count",
-               "Dycore Type"]
-
-    with open(file_path, 'r') as f:
+    with open(file_path) as f:
         dycore_types = {"0": "Glide",
                         "1": "Glam",
                         "2": "Glissade",
@@ -72,34 +65,34 @@ def parse_log(file_path):
         iters_to_converge = []
         for line in f:
             split = line.split()
-            if ('CISM dycore type' in line):
+            if 'CISM dycore type' in line:
                 if line.split()[-1] == '=':
                     dycore_type = dycore_types[next(f).strip()]
                 else:
                     dycore_type = dycore_types[line.split()[-1]]
-            elif ('total procs' in line):
+            elif 'total procs' in line:
                 proc_count += int(line.split()[-1])
-            elif ('Nonlinear Solver Step' in line):
+            elif 'Nonlinear Solver Step' in line:
                 curr_step = int(line.split()[4])
-            elif ('Compute ice velocities, time = ' in line):
+            elif 'Compute ice velocities, time = ' in line:
                 converged_iters.append(curr_step)
                 curr_step = float(line.split()[-1])
-            elif ('"SOLVE_STATUS_CONVERGED"' in line):
+            elif '"SOLVE_STATUS_CONVERGED"' in line:
                 split = line.split()
                 iters_to_converge.append(int(split[split.index('"SOLVE_STATUS_CONVERGED"') + 2]))
-            elif ("Compute dH/dt" in line):
+            elif "Compute dH/dt" in line:
                 iters_to_converge.append(int(iter_number))
             elif len(split) > 0 and split[0].isdigit():
                 iter_number = split[0]
-        if iters_to_converge == []:
+        if not iters_to_converge:
             iters_to_converge.append(int(iter_number))
     data = {
-        "Dycore Type": dycore_type,
-        "Processor Count": proc_count,
-        "Converged Iterations": len(converged_iters),
-        "Avg. Iterations to Converge": np.mean(iters_to_converge)
+        "Dycore Type": [dycore_type],
+        "Processor Count": [proc_count],
+        "Converged Iterations": [len(converged_iters)],
+        "Avg. Iterations to Converge": [np.mean(iters_to_converge)]
     }
-    return elements.table("Output Log", headers, data)
+    return elements.Table("Output Log", data)
 
 
 def parse_config(file_path):
@@ -118,6 +111,6 @@ def parse_config(file_path):
     parser.read(file_path)
     # Strip out inline comments
     for s in parser._sections:
-        for v in six.iterkeys(parser._sections[s]):
+        for v in parser._sections[s]:
             parser._sections[s][v] = parser._sections[s][v].split("#")[0].strip()
     return parser._sections
diff --git a/livvkit/components/numerics.py b/livvkit/components/numerics.py
index 44b7ac4..0385f36 100644
--- a/livvkit/components/numerics.py
+++ b/livvkit/components/numerics.py
@@ -29,17 +29,16 @@
 """
 Numerics Test Base Module.
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
 
 import os
 import importlib
 
 import livvkit
 from livvkit.util import functions
-from livvkit.util import elements
+from livvkit import elements
 
 
-def run_suite(case, config, summary):
+def run_suite(case, config):
     """ Run the full suite of numerics tests """
     m = importlib.import_module(config['module'])
     m.set_up()
@@ -70,13 +69,20 @@ def run_suite(case, config, summary):
     try:
         el = m.run(config, analysis_data)
     except KeyError:
-        el = elements.error("Numerics Plots", "Missing data")
-    result = elements.page(case, config['description'], element_list=el)
-    summary[case] = _summarize_result(m, analysis_data, config)
-    _print_summary(m, case, summary[case])
-    functions.create_page_from_template("numerics.html",
-                                        os.path.join(livvkit.index_dir, "numerics", case + ".html"))
-    functions.write_json(result, os.path.join(livvkit.output_dir, "numerics"), case + ".json")
+        el = elements.Error("Numerics Plots", "Missing data")
+
+    result = elements.Page(case, config['description'], elements=[el])
+    summary = _summarize_result(m, analysis_data, config)
+
+    _print_summary(m, case, summary)
+
+    functions.create_page_from_template(
+        "numerics.html", os.path.join(livvkit.index_dir, "numerics", case + ".html")
+    )
+    with open(os.path.join(livvkit.output_dir, "numerics", case + ".json"), 'w') as f:
+        f.write(result._repr_json())
+
+    return summary
 
 
 def _print_summary(module, case, summary):
@@ -98,7 +104,6 @@ def _summarize_result(module, data, config):
     return summary
 
 
-# noinspection PyUnusedLocal
 def populate_metadata(case, config):
     metadata = {"Type": "Summary",
                 "Title": "Numerics",
diff --git a/livvkit/components/performance.py b/livvkit/components/performance.py
index ecadf16..4299665 100644
--- a/livvkit/components/performance.py
+++ b/livvkit/components/performance.py
@@ -29,8 +29,6 @@
 """
 Performance Test Base Module.
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
-import six
 
 import os
 import glob
@@ -38,15 +36,15 @@
 import matplotlib.pyplot as plt
 
 import livvkit
+from livvkit import elements
 from livvkit.util import functions
 from livvkit.util import colormaps
 from livvkit.util.LIVVDict import LIVVDict
-from livvkit.util import elements
 
 SEC_PER_DAY = 86400.0
 
 
-def run_suite(case, config, summary):
+def run_suite(case, config):
     """ Run the full suite of performance tests """
     config["name"] = case
     timing_data = dict()
@@ -98,27 +96,31 @@ def run_suite(case, config, summary):
                                        ),
         ]
 
-    timing_plots = timing_plots + \
+    timing_plots += \
         [generate_timing_breakdown_plot(timing_data[s],
                                         config['scaling_var'],
                                         "Timing breakdown for " + case.capitalize()+" "+s,
                                         "",
                                         os.path.join(plot_dir, case+"_"+s+"_timing_breakdown.png")
                                         )
-         for s in sorted(six.iterkeys(timing_data), key=functions.sort_scale)]
+         for s in sorted(timing_data, key=functions.sort_scale)]
 
     # Build an image gallery and write the results
     el = [
-            elements.gallery("Performance Plots", timing_plots)
+            elements.Gallery("Performance Plots", timing_plots)
          ]
-    result = elements.page(case, config["description"], element_list=el)
-    summary[case] = _summarize_result(timing_data, config)
+    result = elements.Page(case, config["description"], elements=el)
+    summary = _summarize_result(timing_data, config)
+
     _print_result(case, summary)
+
     functions.create_page_from_template("performance.html",
                                         os.path.join(livvkit.index_dir, "performance",
                                                      case + ".html"))
-    functions.write_json(result, os.path.join(livvkit.output_dir, "performance"),
-                         case + ".json")
+    with open(os.path.join(livvkit.output_dir, "performance", case + ".json"), 'w') as f:
+        f.write(result._repr_json())
+
+    return summary
 
 
 def _analyze_case(model_dir, bench_dir, config):
@@ -135,16 +137,14 @@ def _analyze_case(model_dir, bench_dir, config):
     return dict(model=model_stats, bench=bench_stats)
 
 
-# noinspection PyUnusedLocal
 def _print_result(case, summary):
     """ Show some statistics from the run """
-    for case, case_data in summary.items():
-        for dof, data in case_data.items():
-            print("    " + case + " " + dof)
-            print("    -------------------")
-            for header, val in data.items():
-                print("    " + header + " : " + str(val))
-            print("")
+    for dof, data in summary.items():
+        print("    " + case + " " + dof)
+        print("    -------------------")
+        for header, val in data.items():
+            print("    " + header + " : " + str(val))
+        print("")
 
 
 def _summarize_result(result, config):
@@ -170,11 +170,10 @@ def _summarize_result(result, config):
         else:
             time_diff = 'NA'
         summary[size]['Proc. Counts'] = ", ".join([str(x) for x in sorted(proc_counts)])
-        summary[size]['Mean Time Diff (% of benchmark)'] = time_diff
+        summary[size]['Mean Time Diff (% of benchmark)'] = '{:.4f}'.format(time_diff*100)
     return summary
 
 
-# noinspection PyUnusedLocal
 def populate_metadata(case, config):
     """ Provide some top level information for the summary """
     return {"Type": "Summary",
@@ -347,11 +346,12 @@ def generate_scaling_plot(timing_data, title, ylabel, description, plot_file):
         plt.text(0.0, 0.44, "To generate this data rerun BATS with the")
         plt.text(0.0, 0.36, "performance option enabled.")
 
-    if livvkit.publish:
-        plt.savefig(os.path.splitext(plot_file)[0]+'.eps', dpi=600)
     plt.savefig(plot_file)
     plt.close()
-    return elements.image(title, description, os.path.basename(plot_file))
+
+    image = elements.Image(title, description, plot_file)
+
+    return image
 
 
 def scaling_sypd_plot(timing_data, title, ylabel, description, plot_file):
@@ -416,15 +416,14 @@ def generate_timing_breakdown_plot(timing_stats, scaling_var, title, description
     Returns:
         an image element containing the plot file and metadata
     """
-    # noinspection PyProtectedMember
     cmap_data = colormaps._viridis_data
-    n_subplots = len(six.viewkeys(timing_stats))
+    n_subplots = len(timing_stats)
     fig, ax = plt.subplots(1, n_subplots+1, figsize=(3*(n_subplots+2), 5))
     for plot_num, p_count in enumerate(
-            sorted(six.iterkeys(timing_stats), key=functions.sort_processor_counts)):
+            sorted(timing_stats, key=functions.sort_processor_counts)):
 
         case_data = timing_stats[p_count]
-        all_timers = set(six.iterkeys(case_data['model'])) | set(six.iterkeys(case_data['bench']))
+        all_timers = set(case_data['model']) | set(case_data['bench'])
         all_timers = sorted(list(all_timers), reverse=True)
         cmap_stride = int(len(cmap_data)/(len(all_timers)+1))
         colors = {all_timers[i]: cmap_data[i*cmap_stride] for i in range(len(all_timers))}
@@ -440,7 +439,7 @@ def generate_timing_breakdown_plot(timing_stats, scaling_var, title, description
 
             offset = 0
             if var_data != {}:
-                for var in sorted(six.iterkeys(var_data), reverse=True):
+                for var in sorted(var_data, reverse=True):
                     if var != scaling_var:
                         plt.bar(bar_num, var_data[var]['mean'], 0.8, bottom=offset,
                                 color=colors[var], label=(var if bar_num == 1 else '_none'))
@@ -461,8 +460,9 @@ def generate_timing_breakdown_plot(timing_stats, scaling_var, title, description
             group.set_visible(False)
     sub_ax.set_visible(False)
 
-    if livvkit.publish:
-        plt.savefig(os.path.splitext(plot_file)[0]+'.eps', dpi=600)
     plt.savefig(plot_file)
     plt.close()
-    return elements.image(title, description, os.path.basename(plot_file))
+
+    image = elements.Image(title, description, plot_file)
+
+    return image
diff --git a/livvkit/components/validation.py b/livvkit/components/validation.py
index addc170..c2cf887 100644
--- a/livvkit/components/validation.py
+++ b/livvkit/components/validation.py
@@ -29,8 +29,6 @@
 """
 Validation Test Base Module
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
-import six
 
 import os
 import importlib
@@ -94,56 +92,44 @@ def _load_case_module(case, config):
     except ImportError:
         mod_path = os.path.abspath(config['module'])
         try:
-            if six.PY2:
-                import imp
-                m = imp.load_source(case, mod_path)
-            elif six.PY3:
-                # noinspection PyUnresolvedReferences
-                spec = importlib.util.spec_from_file_location(case, mod_path,
-                                                              submodule_search_locations=os.path.dirname(mod_path))
-                # noinspection PyUnresolvedReferences
-                m = importlib.util.module_from_spec(spec)
-                spec.loader.exec_module(m)
-            else:
-                raise
+            spec = importlib.util.spec_from_file_location(
+                case, mod_path, submodule_search_locations=os.path.dirname(mod_path)
+            )
+            m = importlib.util.module_from_spec(spec)
+            spec.loader.exec_module(m)
         except IOError:
             # imp.load_source (py2) and spec.loader.exec_module (py3) raises an IOError if module isn't found
             print(ERR_MISSING_MOD_MSG.format(case, os.path.relpath(mod_path, os.getcwd())))
             raise
         except ImportError as iie:
             # If module's internal import statements fail
-            dep = str(iie).split()[-1] if six.PY2 else iie.name 
-            print(_case_dep_err(mod_path).format(case, dep))
+            print(_case_dep_err(mod_path).format(case, iie.name))
             raise
         
     return m
 
 
-def run_suite(case, config, summary):
+def run_suite(case, config):
     """ Run the full suite of validation tests """
     m = _load_case_module(case, config)
 
     result = m.run(case, config)
-    summary[case] = _summarize_result(m, result)
+    summary = _summarize_result(m, result)
     _print_summary(m, case, summary)
-   
-    if result['Type'] == 'Book':
-        for name, page in six.iteritems(result['Data']):
-            functions.create_page_from_template("validation.html",
-                                                os.path.join(livvkit.index_dir, "validation", name + ".html"))
-            functions.write_json(page, os.path.join(livvkit.output_dir, "validation"), name + ".json")
-    else:
-        functions.create_page_from_template("validation.html",
-                                            os.path.join(livvkit.index_dir, "validation", case + ".html"))
-        functions.write_json(result, os.path.join(livvkit.output_dir, "validation"), case + ".json")
+
+    functions.create_page_from_template("validation.html",
+                                        os.path.join(livvkit.index_dir, "validation", case + ".html"))
+    functions.write_json(result, os.path.join(livvkit.output_dir, "validation"), case + ".json")
+
+    return summary
 
 
 def _print_summary(module, case, summary):
     try:
         try:
-            module.print_summary(summary[case])
+            module.print_summary(summary)
         except TypeError:
-            module.print_summary(case, summary[case])
+            module.print_summary(case, summary)
     except (NotImplementedError, AttributeError):
         print("    Ran " + case + "!")
         print("")
diff --git a/livvkit/components/verification.py b/livvkit/components/verification.py
index 4e170d2..008badf 100644
--- a/livvkit/components/verification.py
+++ b/livvkit/components/verification.py
@@ -29,8 +29,6 @@
 """
 Verification Test Base Module
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
-import six
 
 import os
 
@@ -40,24 +38,23 @@
 from netCDF4 import Dataset
 
 import livvkit
-from livvkit.util import netcdf
+from livvkit import elements
 from livvkit.util import functions
 from livvkit.util import colormaps
 from livvkit.util.LIVVDict import LIVVDict
-from livvkit.util import elements
 
 
-def run_suite(case, config, summary):
+def run_suite(case, config):
     """ Run the full suite of verification tests """
     config["name"] = case
     model_dir = os.path.join(livvkit.model_dir, config['data_dir'], case)
     bench_dir = os.path.join(livvkit.bench_dir, config['data_dir'], case)
-    tabs = []
-    case_summary = LIVVDict()
+    tabs = {}
+    summary = LIVVDict()
     model_cases = functions.collect_cases(model_dir)
     bench_cases = functions.collect_cases(bench_dir)
 
-    for subcase in sorted(six.iterkeys(model_cases)):
+    for subcase in sorted(model_cases):
         bench_subcases = bench_cases[subcase] if subcase in bench_cases else []
         case_sections = []
         for mcase in sorted(model_cases[subcase], key=functions.sort_processor_counts):
@@ -65,33 +62,35 @@ def run_suite(case, config, summary):
                      if mcase in bench_subcases else "")
             mpath = os.path.join(model_dir, subcase, mcase.replace("-", os.path.sep))
             case_result = _analyze_case(mpath, bpath, config)
-            case_sections.append(elements.section(mcase, case_result))
-            case_summary[subcase] = _summarize_result(case_result,
-                                                      case_summary[subcase])
-        tabs.append(elements.tab(subcase, section_list=case_sections))
-
-    result = elements.page(case, config["description"], tab_list=tabs)
-    summary[case] = case_summary
-    _print_summary(case, summary[case])
-    functions.create_page_from_template("verification.html",
-                                        os.path.join(livvkit.index_dir,
-                                                     "verification",
-                                                     case + ".html")
-                                        )
-    functions.write_json(result, os.path.join(livvkit.output_dir, "verification"), case+".json")
-
-
-def _analyze_case(model_dir, bench_dir, config):
+            case_sections.append(elements.Section(mcase, case_result))
+            summary[subcase] = _summarize_result(case_result, summary[subcase])
+        tabs[subcase] = case_sections
+
+    result = elements.Page(case, config["description"], elements=[elements.Tabs(tabs)])
+
+    _print_summary(case, summary)
+
+    functions.create_page_from_template(
+        "verification.html", os.path.join(livvkit.index_dir, "verification", case + ".html")
+    )
+    with open(os.path.join(livvkit.output_dir, "verification", case+".json"), 'w') as f:
+        f.write(result._repr_json())
+
+    return summary
+
+
+def _analyze_case(test_dir, ref_dir, config):
     """ Runs all of the verification checks on a particular case """
     bundle = livvkit.verification_model_module
-    model_out = functions.find_file(model_dir, "*"+config["output_ext"])
-    bench_out = functions.find_file(bench_dir, "*"+config["output_ext"])
-    model_config = functions.find_file(model_dir, "*"+config["config_ext"])
-    bench_config = functions.find_file(bench_dir, "*"+config["config_ext"])
-    model_log = functions.find_file(model_dir, "*"+config["logfile_ext"])
+    test_out = functions.find_file(test_dir, "*"+config["output_ext"])
+    ref_out = functions.find_file(ref_dir, "*"+config["output_ext"])
+    test_config = functions.find_file(test_dir, "*"+config["config_ext"])
+    ref_config = functions.find_file(ref_dir, "*"+config["config_ext"])
+    model_log = functions.find_file(test_dir, "*"+config["logfile_ext"])
     el = [
-            bit_for_bit(model_out, bench_out, config),
-            diff_configurations(model_config, bench_config, bundle, bundle),
+            bit_for_bit(test_out, ref_out, config),
+            elements.FileDiff("Configuration Comparison",
+                              ref_config, test_config),
             bundle.parse_log(model_log)
          ]
     return el
@@ -125,12 +124,12 @@ def _summarize_result(result, summary):
     failure_count = 0
     summary_data = None
     for elem in result:
-        if elem["Type"] == "Bit for Bit" and "Data" in elem:
-            elem_data = elem["Data"]
+        if isinstance(elem, elements.BitForBit):
+            elem_data = elem.data
             summary_data = summary["Bit for Bit"]
             total_count += 1
-            for var in six.iterkeys(elem_data):
-                if elem_data[var]["Max Error"] != 0:
+            for ii, var in enumerate(elem_data['Variable']):
+                if elem_data["Max Error"][ii] != 0:
                     failure_count += 1
                     break
     if summary_data is not None:
@@ -142,17 +141,13 @@ def _summarize_result(result, summary):
     total_count = 0
     failure_count = 0
     for elem in result:
-        if elem["Title"] == "Configuration Comparison" and elem["Type"] == "Diff":
-            elem_data = elem["Data"]
+        if isinstance(elem, elements.FileDiff) and elem.title == 'Configuration Comparison':
             summary_data = summary["Configurations"]
             total_count += 1
-            failed = False
-            for section_name, varlist in elem_data.items():
-                for var, val in varlist.items():
-                    if not val[0]:
-                        failed = True
-            if failed:
+            if elem.diff_status:
                 failure_count += 1
+            else:
+                continue
     if summary_data is not None:
         success_count = total_count - failure_count
         summary_data = np.add(summary_data, [success_count, total_count]).tolist()
@@ -160,13 +155,13 @@ def _summarize_result(result, summary):
 
     # Get the number of files parsed
     for elem in result:
-        if elem["Title"] == "Output Log" and elem["Type"] == "Table":
+        if isinstance(elem, elements.Table) and elem.title == 'Output Log':
             summary["Std. Out Files"] += 1
             break
+
     return summary
 
 
-# noinspection PyUnusedLocal
 def populate_metadata(case, config):
     """ Provide some top level information for the summary """
     return {"Type": "Summary",
@@ -191,79 +186,51 @@ def bit_for_bit(model_path, bench_path, config):
     fname = model_path.split(os.path.sep)[-1]
     # Error handling
     if not (os.path.isfile(bench_path) and os.path.isfile(model_path)):
-        return elements.error("Bit for Bit",
+        return elements.Error("Bit for Bit",
                               "File named " + fname + " has no suitable match!")
     try:
         model_data = Dataset(model_path)
         bench_data = Dataset(bench_path)
     except (FileNotFoundError, PermissionError):
-        return elements.error("Bit for Bit",
+        return elements.Error("Bit for Bit",
                               "File named " + fname + " could not be read!")
-    if not (netcdf.has_time(model_data) and netcdf.has_time(bench_data)):
-        return elements.error("Bit for Bit",
+    if not (len(model_data.dimensions['time']) > 0 and len(bench_data.dimensions['time']) > 0):
+        return elements.Error("Bit for Bit",
                               "File named " + fname + " could not be read!")
 
     # Begin bit for bit analysis
-    headers = ["Max Error", "Index of Max Error", "RMS Error", "Plot"]
-    stats = LIVVDict()
-    for i, var in enumerate(config["bit_for_bit_vars"]):
+    plot_elements = []
+    table_data = {'Variable': [], 'Max Error': [], 'Index of Max Error': [], 'RMS Error': []}
+    for var in config["bit_for_bit_vars"]:
         if var in model_data.variables and var in bench_data.variables:
+            table_data['Variable'].append(var)
+
             m_vardata = model_data.variables[var][:]
             b_vardata = bench_data.variables[var][:]
             diff_data = m_vardata - b_vardata
+
             if diff_data.any():
-                stats[var]["Max Error"] = np.amax(np.absolute(diff_data))
-                stats[var]["Index of Max Error"] = str(
-                        np.unravel_index(np.absolute(diff_data).argmax(), diff_data.shape))
-                stats[var]["RMS Error"] = np.sqrt(np.sum(np.square(diff_data).flatten()) /
-                                                  diff_data.size)
-                pf = plot_bit_for_bit(fname, var, m_vardata, b_vardata, diff_data)
+                table_data["Max Error"].append(np.amax(np.absolute(diff_data)))
+                table_data["Index of Max Error"].append(str(
+                        np.unravel_index(np.absolute(diff_data).argmax(), diff_data.shape)))
+                table_data["RMS Error"].append(np.sqrt(np.sum(np.square(diff_data).flatten()) /
+                                               diff_data.size))
+                plot_elements.append(plot_bit_for_bit(fname, var, m_vardata, b_vardata, diff_data))
             else:
-                stats[var]["Max Error"] = stats[var]["RMS Error"] = 0
-                pf = stats[var]["Index of Max Error"] = "N/A"
-            stats[var]["Plot"] = pf
+                table_data["Max Error"].append(0)
+                table_data["Index of Max Error"].append("N/A")
+                table_data["RMS Error"].append(0)
+                plot_elements.append(elements.B4BImage('', '{} is bit-for-bit'.format(var),
+                                                       page_path=os.path.join(livvkit.output_dir, "verification")))
         else:
-            stats[var] = {"Max Error": "No Match", "RMS Error": "N/A", "Plot": "N/A"}
+            table_data["Max Error"].append("No Match")
+            table_data["Index of Max Error"].append("N/A")
+            table_data["RMS Error"].append("N/A")
+            plot_elements.append(elements.NAImage('', '{} is not in both test and reference data'.format(var),
+                                                  page_path=os.path.join(livvkit.output_dir, "verification")))
     model_data.close()
     bench_data.close()
-    return elements.bit_for_bit("Bit for Bit", headers, stats)
-
-
-def diff_configurations(model_config, bench_config, model_bundle, bench_bundle):
-    """
-    Description
-
-    Args:
-        model_config: a dictionary with the model configuration data
-        bench_config: a dictionary with the benchmark configuration data
-        model_bundle: a LIVVkit model bundle object
-        bench_bundle: a LIVVkit model bundle object
-
-    Returns:
-        A dictionary created by the elements object corresponding to
-        the results of the bit for bit testing
-    """
-    diff_dict = LIVVDict()
-    model_data = model_bundle.parse_config(model_config)
-    bench_data = bench_bundle.parse_config(bench_config)
-    if model_data == {} and bench_data == {}:
-        return elements.error("Configuration Comparison",
-                              "Could not open file: " + model_config.split(os.path.sep)[-1])
-
-    model_sections = set(six.iterkeys(model_data))
-    bench_sections = set(six.iterkeys(bench_data))
-    all_sections = set(model_sections.union(bench_sections))
-
-    for s in all_sections:
-        model_vars = set(six.iterkeys(model_data[s])) if s in model_sections else set()
-        bench_vars = set(six.iterkeys(bench_data[s])) if s in bench_sections else set()
-        all_vars = set(model_vars.union(bench_vars))
-        for v in all_vars:
-            model_val = model_data[s][v] if s in model_sections and v in model_vars else 'NA'
-            bench_val = bench_data[s][v] if s in bench_sections and v in bench_vars else 'NA'
-            same = True if model_val == bench_val and model_val != 'NA' else False
-            diff_dict[s][v] = (same, model_val, bench_val)
-    return elements.file_diff("Configuration Comparison", diff_dict)
+    return elements.BitForBit("Bit for Bit", table_data, imgs=plot_elements)
 
 
 def plot_bit_for_bit(case, var_name, model_data, bench_data, diff_data):
@@ -322,10 +289,14 @@ def plot_bit_for_bit(case, var_name, model_data, bench_data, diff_data):
     plt.suptitle(plot_title)
 
     plot_file = os.path.sep.join([plot_path, plot_name])
-    if livvkit.publish:
-        plt.savefig(os.path.splitext(plot_file)[0]+'.eps', dpi=600)
     plt.savefig(plot_file)
     plt.close()
-    return os.path.join(os.path.relpath(plot_path,
-                                        os.path.join(livvkit.output_dir, "verification")),
-                        plot_name)
+
+    # NOTE: If you don't include a title, you must include a group for the image
+    #       to appear in a lightbox when clicked instead of as it's own page.
+    plot_element = elements.Image(
+            '', 'Bit for bit differences between test and reference for '
+                '{} in {}'.format(var_name, case),
+            plot_file, height=50, group='not-b4b'
+    )
+    return plot_element
diff --git a/livvkit/data/Evans2019.bib b/livvkit/data/Evans2019.bib
new file mode 100644
index 0000000..b19951e
--- /dev/null
+++ b/livvkit/data/Evans2019.bib
@@ -0,0 +1,11 @@
+@Article{gmd-12-1067-2019,
+AUTHOR = {Evans, K. J. and Kennedy, J. H. and Lu, D. and Forrester, M. M. and Price, S. and Fyke, J. and Bennett, A. R. and Hoffman, M. J. and Tezaur, I. and Zender, C. S. and Vizca\'{\i}no, M.},
+TITLE = {LIVVkit 2.1: automated and extensible ice sheet model validation},
+JOURNAL = {Geoscientific Model Development},
+VOLUME = {12},
+YEAR = {2019},
+NUMBER = {3},
+PAGES = {1067--1086},
+URL = {https://www.geosci-model-dev.net/12/1067/2019/},
+DOI = {10.5194/gmd-12-1067-2019}
+}
\ No newline at end of file
diff --git a/livvkit/data/Kennedy2017.bib b/livvkit/data/Kennedy2017.bib
new file mode 100644
index 0000000..3103f84
--- /dev/null
+++ b/livvkit/data/Kennedy2017.bib
@@ -0,0 +1,11 @@
+@article {Kennedy2017,
+    AUTHOR = {Kennedy, Joseph H. and Bennett, Andrew R. and Evans, Katherine J. and Price, Stephen and Hoffman, Matthew and Lipscomb, William H. and Fyke, Jeremy and Vargo, Lauren and Boghozian, Adrianna and Norman, Matthew and Worley, Patrick H.},
+    TITLE = {LIVVkit: An extensible, python-based, land ice verification and validation toolkit for ice sheet models},
+    JOURNAL = {Journal of Advances in Modeling Earth Systems},
+    VOLUME = {9},
+    NUMBER = {2},
+    ISSN = {1942-2466},
+    DOI = {10.1002/2017MS000916},
+    PAGES = {854--869},
+    YEAR = {2017},
+}
\ No newline at end of file
diff --git a/livvkit/util/netcdf.py b/livvkit/elements/__init__.py
similarity index 74%
rename from livvkit/util/netcdf.py
rename to livvkit/elements/__init__.py
index 7783d96..cde4958 100644
--- a/livvkit/util/netcdf.py
+++ b/livvkit/elements/__init__.py
@@ -26,25 +26,5 @@
 # CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-"""
-Helper functions for dealing with netcdf datasets
-"""
 
-from __future__ import absolute_import, division, print_function, unicode_literals
-
-
-def has_time(dataset):
-    """
-    Check netCDF files time dimension for emptiness. This likely
-    indicates the run did not complete.
-
-    Args:
-        dataset: An open, readable netcdf file
-
-    Returns:
-        a boolean indicating if the time dimension is set correctly
-    """
-    try:
-        return len(dataset.dimensions['time']) > 0
-    except KeyError:
-        return False
+from .elements import *
diff --git a/livvkit/elements/elements.py b/livvkit/elements/elements.py
new file mode 100644
index 0000000..69eea08
--- /dev/null
+++ b/livvkit/elements/elements.py
@@ -0,0 +1,807 @@
+# coding=utf-8
+# Copyright (c) 2015-2018, UT-BATTELLE, LLC
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors
+# may be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+# FIXME: This docstring
+"""Module containing report generation and display elements
+
+The elements in this module are used by LIVVkit to generate analyses reports.
+Reports by default will be a portable HTML website, but each of these elements
+provide some experimental (and therefore undocumented) report formats: JSON-Only
+and LaTeX.
+
+New elements should derive from, or implement the same interface as, the
+BaseElement abstract class.
+"""
+
+import os
+import abc
+import glob
+import difflib
+import collections
+
+from pathlib import Path
+
+import jinja2
+import json_tricks
+import pandas as pd
+
+import livvkit
+import livvkit.data
+from livvkit.util import bib
+
+_HERE = os.path.dirname(__file__)
+
+# skipcq: BAN-B701
+_html_env = jinja2.Environment(
+        loader=jinja2.FileSystemLoader(os.path.join(_HERE, 'templates')))
+
+# skipcq: BAN-B701
+_latex_env = jinja2.Environment(
+        block_start_string=r'\BLOCK{',    # default: {%
+        block_end_string=r'}',            # default: %}
+        variable_start_string=r'\VAR{',   # default: {{
+        variable_end_string=r'}',         # default: }}
+        comment_start_string=r'\#{',      # default: {#
+        comment_end_string=r'}',          # default: #}
+        trim_blocks=True,
+        loader=jinja2.FileSystemLoader(os.path.join(_HERE, 'templates')))
+
+
+class BaseElement(abc.ABC):
+    """An abstract base LIVVkit element
+
+    An abstract base LIVVkit element providing the basic element interface
+    expected by LIVVkit. All LIVVkit elements should either derive from this
+    class or implement the same interface.
+    """
+
+    # FIXME: There's got to be a better way.
+    #  We want _html_template (_latex_template) to be required and act like:
+    #     >>> self._html_template
+    #     'template.html'
+    #  This could be satisfied be a simple class attribute or a more complex property,
+    #  but NOT a method, which would have to be called like:
+    #     >>> self._html_template()
+    #     'template.html'
+    #  Unfortunately, the chained @property and @abc.abstractmethod doesn't enforce
+    #  an attribute/property like action and can be satisfied by defining a method,
+    #  so we make sure that if it's not a property, it's also not callable (a method)
+    def __init__(self):
+        """Initialize a LIVVkit element
+        """
+        if not isinstance(type(self)._html_template, property) and callable(self._html_template):
+            raise TypeError('You must define _html_template as a property or attribute for this class')
+        if not isinstance(type(self)._latex_template, property) and callable(self._latex_template):
+            raise TypeError('You must define _latex_template as a property or attribute for this class')
+
+    @property
+    @abc.abstractmethod
+    def _html_template(self):
+        """The jinja2 HTML template
+
+        An attribute or property which holds the jinja2 template used to
+        represent the element as HTML.
+
+        Returns:
+            str: The jinja2 HTML template
+        """
+        raise NotImplementedError
+
+    @property
+    @abc.abstractmethod
+    def _latex_template(self):
+        """The jinja2 LaTeX template
+
+        An attribute or property which holds the jinja2 template used to
+        represent the element as LaTeX.
+
+        Returns:
+            str: The jinja2 LaTeX template
+        """
+        raise NotImplementedError
+
+    def _repr_json(self):
+        """Represent this element as JSON
+
+        Using the internal dictionary representation of this element, return a
+        JSON representation of this element
+
+        Returns:
+            str: The JSON representation of this element
+        """
+        jsn = {type(self).__name__: self.__dict__.copy()}
+        jsn[type(self).__name__].update({'__module__': type(self).__module__,
+                                         '_html_template': self._html_template,
+                                         '_latex_template': self._latex_template})
+        return json_tricks.dumps(jsn, indent=4, primitives=True, allow_nan=True)
+
+    def _repr_html(self):
+        """Represent this element as HTML
+
+        Using the jinja2 template defined by ``self._html_template``, return an
+        HTML representation of this element
+
+        Returns:
+            str: The HTML representation of this element
+        """
+        template = _html_env.get_template(self._html_template)
+        return template.render(data=self.__dict__)
+
+    def _repr_latex(self):
+        """Represent this element as LaTeX
+
+        Using the jinja2 template defined by ``self._latex_template``, return an
+        LaTeX representation of this element
+
+        Returns:
+            str: The LaTeX representation of this element
+        """
+        template = _latex_env.get_template(self._latex_template)
+        return template.render(data=self.__dict__)
+
+
+class CompositeElement(BaseElement, abc.ABC):
+    """An abstract base LIVVkit element that contains other elements
+
+    An abstract base LIVVkit element that contains other elements in self.elements
+    and provides the basic element interface expected by LIVVkit. All LIVVkit
+    elements should either be derived from the LIVVkit BaseElement or implement
+    the same interface.
+    """
+    def __init__(self, elements):
+        """Initialize a composite LIVVkit element
+
+        Args:
+            elements: A list of LIVVkit elements
+        """
+        super(CompositeElement, self).__init__()
+        self.elements = elements
+
+    def _repr_json(self):
+        """Represent this element as JSON
+
+        Using the internal dictionary representation of this element, return a
+        JSON representation of this element
+
+        Returns:
+            str: The JSON representation of this element
+        """
+        jsn = {type(self).__name__: self.__dict__.copy()}
+        jsn[type(self).__name__].update({'__module__': type(self).__module__,
+                                         '_html_template': self._html_template,
+                                         '_latex_template': self._latex_template})
+
+        elem_repr = [json_tricks.loads(elem._repr_json()) for elem in self.elements]
+        jsn[type(self).__name__]['elements'] = elem_repr
+
+        return json_tricks.dumps(jsn, indent=4, primitives=True, allow_nan=True)
+
+    def _repr_html(self):
+        """Represent this element as HTML
+
+        Using the jinja2 template defined by ``self._html_template``, return an
+        HTML representation of this element
+
+        Returns:
+            str: The HTML representation of this element
+        """
+        elem_repr = [elem._repr_html() for elem in self.elements]
+        template = _html_env.get_template(self._html_template)
+        return template.render(data=self.__dict__, elements=elem_repr)
+
+    def _repr_latex(self):
+        """Represent this element as LaTeX
+
+        Using the jinja2 template defined by ``self._latex_template``, return an
+        LaTeX representation of this element
+
+        Returns:
+            str: The LaTeX representation of this element
+        """
+        template = _latex_env.get_template(self._latex_template)
+        elem_repr = [elem._repr_latex() for elem in self.elements]
+        return template.render(data=self.__dict__, elements=elem_repr)
+
+
+class NamedCompositeElement(BaseElement, abc.ABC):
+    """An abstract base LIVVkit element that contains multiple other composite elements
+
+    An abstract base LIVVkit element that allows to logically group multiple
+    other composite elements in self.element_dict and provides the basic element
+    interface expected by LIVVkit. All LIVVkit elements should either be derived
+    from the LIVVkit BaseElement or implement the same interface.
+    """
+    def __init__(self, elements_dict):
+        """Initialize  a multi-composite LIVVkit element
+
+        Args:
+            elements_dict: A dictionary where the (key, value) item represents a
+             collection of LIVVkit elements. The key should be a name for the
+             collection and the value should be a list of elements.
+        """
+        super(NamedCompositeElement, self).__init__()
+        self.elements_dict = elements_dict
+
+    def _repr_json(self):
+        """Represent this element as JSON
+
+        Using the internal dictionary representation of this element, return a
+        JSON representation of this element
+
+        Returns:
+            str: The JSON representation of this element
+        """
+        jsn = {type(self).__name__: self.__dict__.copy()}
+        jsn[type(self).__name__].update({'__module__': type(self).__module__,
+                                         '_html_template': self._html_template,
+                                         '_latex_template': self._latex_template})
+
+        elem_repr = {}
+        for title, elements in self.elements_dict.items():
+            elem_repr[title] = [json_tricks.loads(elem._repr_json()) for elem in elements]
+
+        jsn[type(self).__name__]['elements_dict'] = elem_repr
+
+        return json_tricks.dumps(jsn, indent=4, primitives=True, allow_nan=True)
+
+    def _repr_html(self):
+        """Represent this element as HTML
+
+        Using the jinja2 template defined by ``self._html_template``, return an
+        HTML representation of this element
+
+        Returns:
+            str: The HTML representation of this element
+        """
+        elem_repr = {}
+        for title, elements in self.elements_dict.items():
+            elem_repr[title] = [elem._repr_html() for elem in elements]
+
+        template = _html_env.get_template(self._html_template)
+        return template.render(data=self.__dict__, elements_dict=elem_repr)
+
+    def _repr_latex(self):
+        """Represent this element as LaTeX
+
+        Using the jinja2 template defined by ``self._latex_template``, return an
+        LaTeX representation of this element
+
+        Returns:
+            str: The LaTeX representation of this element
+        """
+        elem_repr = {}
+        for title, elements in self.elements_dict.items():
+            elem_repr[title] = [elem._repr_latex() for elem in elements]
+
+        template = _latex_env.get_template(self._latex_template)
+        return template.render(data=self.__dict__, elements_dict=elem_repr)
+
+
+class Page(CompositeElement):
+    """A LIVVkit Page element
+
+    The Page element contains the description of an analysis, the elements
+    that should be displayed for this analysis on the report, as well as any
+    references that should be included in the report. In general usage, this
+    will be used to create an HTML page inside LIVVkit output website. It also
+    will allow for the generation of other (experimental!) Report types
+    (e.g., LaTeX), where the "page" meaning might be better interpreted as
+    a "section".
+
+    For LIVVkit Extensions (LEX), an instance of this class should be returned
+    from the extensions `run()` function.
+    """
+    _html_template = 'page.html'
+    _latex_template = 'page.tex'
+
+    def __init__(self, title, description, elements, references=''):
+        """Initialize a Page elements
+
+        Args:
+            title: the title to display on the analysis
+            description: A long (paragraph or more) description of the analysis
+                being performed. Typically, it's best to write this description
+                as the LEX extension's docstring and pass this class `__doc__`
+            elements: A list of LIVVkit elements to include in the report
+            references: The references to include as part of this analysis. This
+                can be a path to a bibtex file containing the references, or a
+                list/set/tuple of bibtex files containing the references (Note:
+                ALL references inside the bibtex file(s) will be included!). Default
+                value is `references=''` which will cause only the default LIVVkit
+                references to be displayed. References can be entirely removed by
+                setting `references=None`, however, this is *not* recommended.
+        """
+        super(Page, self).__init__(elements)
+        self.title = title
+        self.description = description
+        self._ref_list = None
+        if references is not None:
+            self.add_references(references)
+        # FIXME: remove once common.js is obsolete
+        self.Data = self._repr_html()
+
+    def add_references(self, references):
+        """Add a reference to the internal reference list
+
+        Args:
+            references: The references to add to this page's internal reference
+                list. This can be a path to a bibtex file containing the
+                references, or a list/set/tuple of bibtex files containing the
+                references (Note: This will include the default LIVVkit
+                references and ALL references inside the bibtex file(s)!).
+        """
+        if self._ref_list is None:
+            self._ref_list = glob.glob(
+                os.path.join(os.path.dirname(livvkit.data.__file__), '*.bib')
+            )
+
+        if references:
+            if isinstance(references, (str, Path)):
+                self._ref_list.append(references)
+            elif isinstance(references, (list, set, tuple)):
+                self._ref_list += list(references)
+            else:
+                raise NotImplementedError(
+                    'Cannot add {} type to the reference list. References must be either a (str or '
+                    'Path) path to a bibtex file, or a list/set/tuple of bibtex files.'.format(type(references))
+                )
+
+    def _repr_html(self):
+        """Represent this element as HTML
+
+        Using the jinja2 template defined by ``self._html_template``, return an
+        HTML representation of this element
+
+        Returns:
+            str: The HTML representation of this element
+        """
+        template = _html_env.get_template(self._html_template)
+        elem_repr = [elem._repr_html() for elem in self.elements]
+        rendered_html = template.render(data=self.__dict__, elements=elem_repr)
+        if self._ref_list is not None:
+            rendered_html += bib.bib2html(self._ref_list)
+
+        return rendered_html
+
+    def _repr_latex(self):
+        """Represent this element as LaTeX
+
+        Using the jinja2 template defined by ``self._latex_template``, return an
+        LaTeX representation of this element
+
+        Returns:
+            str: The LaTeX representation of this element
+        """
+        template = _latex_env.get_template(self._latex_template)
+        elem_repr = [elem._repr_latex() for elem in self.elements]
+        rendered_tex = template.render(data=self.__dict__, elements=elem_repr)
+
+        # FIXME: This is hacky! We're cheating the livvkit.util.bib.bib2html
+        #  functionality to actually return latex... See the LatexBackend class
+        if self._ref_list is not None:
+            rendered_tex += bib.bib2html(self._ref_list, backend=bib.LatexBackend())
+
+        return rendered_tex
+
+
+class Tabs(NamedCompositeElement):
+    """A LIVVkit Tabs element
+
+    The Tabs element is a super element intended to logically separate elements
+    into clickable tabs on the output website. It also will allow for the
+    generation of other (experimental!) Report types (e.g., LaTeX), where the
+    "tabs" meaning might be better interpreted as a "subsection".
+    """
+    _html_template = 'tabs.html'
+    _latex_template = 'tabs.tex'
+
+    def __init__(self, tabs):
+        """Initialize a Tabs element
+
+        Args:
+            tabs: A dictionary where each (key, value) item represents a tab.
+                Keys will become the tab text and values should be lists of
+                LIVVkit elements to display within the tab
+        """
+        super(Tabs, self).__init__(tabs)
+
+
+class Section(CompositeElement):
+    """A LIVVkit Section element
+
+    The Section element is a super element intended to logically separate elements
+    into titled sections. It also will allow for the generation of other
+    (experimental!) Report types (e.g., LaTeX), where the "section" meaning might
+    be better interpreted as a "subsection".
+    """
+    _html_template = 'section.html'
+    _latex_template = 'section.tex'
+
+    def __init__(self, title, elements):
+        """Initialize a Section element
+
+        Args:
+            title: The title of the section
+            elements: A list of LIVVkit elements to display within the section
+        """
+        super(Section, self).__init__(elements)
+        self.title = title
+
+
+class Table(BaseElement):
+    """A LIVVkit Table element
+
+    The Table element will produce a table in the analysis report.
+    """
+    _html_template = 'table.html'
+    _latex_template = 'table.tex'
+
+    def __init__(self, title, data, index=False, transpose=False):
+        """Initialize a Section element
+
+                Args:
+                    title: The title of the table
+                    data: The data to display in the table in the form of either
+                        a pandas DataFrame or a dictionary of the form
+                        {column1:[row1, row2...],... } where each (key, value)
+                        item is a table column, with the key being the column
+                        header and the value being a list of that's columns' row
+                        values.
+                    index: The index to include in the table. If False, no index
+                        will be included. If True, a numbered index beginning at
+                        zero will be included in the table. If a collection of
+                        values the same length as the data rows, the collection
+                        will be used to label the rows.
+                    transpose: A boolean (default: False) which will flip the
+                        table (headers become the index, index becomes the header).
+                """
+        super(Table, self).__init__()
+        self.title = title
+
+        if isinstance(data, pd.DataFrame):
+            self.data = data.to_dict(orient='list')
+            self.index = data.index.to_list()
+        else:
+            self.data = data
+            self.index = None
+
+        self.rows = len(next(iter(self.data.values())))
+
+        if index is True and self.index is None:
+            self.index = range(self.rows)
+        elif isinstance(index, collections.abc.Collection):
+            if len(index) != self.rows:
+                raise IndexError('Table index must be the same length as the table. '
+                                 'Table rows: {}, index length: {}.'.format(self.rows, len(index)))
+            self.index = index
+
+        if transpose:
+            self._html_template = 'table_transposed.html'
+            self._latex_template = 'table_transposed.tex'
+
+    def _repr_html(self):
+        """Represent this element as HTML
+
+        Using the jinja2 template defined by ``self._html_template``, return an
+        HTML representation of this element
+
+        Returns:
+            str: The HTML representation of this element
+        """
+        template = _html_env.get_template(self._html_template)
+        return template.render(data=self.__dict__, rows=self.rows, index=self.index)
+
+    def _repr_latex(self):
+        """Represent this element as LaTeX
+
+        Using the jinja2 template defined by ``self._latex_template``, return an
+        LaTeX representation of this element
+
+        Returns:
+            str: The LaTeX representation of this element
+        """
+        template = _latex_env.get_template(self._latex_template)
+        return template.render(data=self.__dict__, rows=self.rows, index=self.index)
+
+
+class BitForBit(CompositeElement):
+    """A LIVVkit BitForBit element
+
+    The BitForBit element will produce a table in the analysis report indicating
+    bit-for-bit statuses with a difference image shown in the final column of
+    the table.
+    """
+    _html_template = 'bit4bit.html'
+    _latex_template = 'bit4bit.tex'
+
+    def __init__(self, title, data, imgs):
+        """Initialize a BitForBit element
+
+        Args:
+            title: The title of the bit-for-bit comparisons
+            data: The data to display in the table in the form of either
+                a pandas DataFrame or a dictionary of the form
+                {column1:[row1, row2...],... } where each (key, value)
+                item is a table column, with the key being the column
+                header and the value being a list of that's columns' row
+                values
+            imgs: A list of LIVVkit Image elements to display in an additional
+                final column of the bit-for-bit table. Note: The list must be
+                same length as the number of rows in the table data
+        """
+        super(BitForBit, self).__init__(imgs)
+        self.title = title
+
+        if isinstance(data, pd.DataFrame):
+            self.data = data.to_dict(orient='list')
+        else:
+            self.data = data
+
+        self.rows = len(next(iter(self.data.values())))
+        if len(imgs) != self.rows:
+            raise IndexError('Imgs must be the same length as the table. '
+                             'Table rows: {}, imgs length: {}.'.format(self.rows, len(imgs)))
+
+    def _repr_html(self):
+        """Represent this element as HTML
+
+        Using the jinja2 template defined by ``self._html_template``, return an
+        HTML representation of this element
+
+        Returns:
+            str: The HTML representation of this element
+        """
+        imgs_repr = [img._repr_html() for img in self.elements]
+        template = _html_env.get_template(self._html_template)
+        return template.render(data=self.__dict__, rows=self.rows, b4b_imgs=imgs_repr)
+
+    def _repr_latex(self):
+        """Represent this element as LaTeX
+
+        Using the jinja2 template defined by ``self._latex_template``, return an
+        LaTeX representation of this element
+
+        Returns:
+            str: The LaTeX representation of this element
+        """
+        imgs_repr = [img._repr_latex() for img in self.elements]
+        template = _latex_env.get_template(self._latex_template)
+        return template.render(data=self.__dict__, rows=self.rows, b4b_imgs=imgs_repr)
+
+
+class Gallery(CompositeElement):
+    """A LIVVkit Gallery element
+
+    The Gallery element is a super element intended to group LIVVkit Image
+    elements into a gallery. It also will allow for the generation of other
+    (experimental!) Report types (e.g., LaTeX), where the "Gallery" meaning
+    might be better interpreted as a figure "subsection".
+    """
+    _html_template = 'gallery.html'
+    _latex_template = 'gallery.tex'
+
+    def __init__(self, title, elements):
+        """Initialize a Gallery element
+
+        Args:
+            title: The title of the image gallery
+            elements: A list of LIVVkit Image elements to display within the
+                gallery
+        """
+        super(Gallery, self).__init__(elements)
+        self.title = title
+
+
+class Image(BaseElement):
+    """A LIVVkit Image element
+
+    The Image element produces an image/figure in the report.
+    """
+    _html_template = 'image.html'
+    _latex_template = 'image.tex'
+
+    def __init__(self, title, desc, image_file, group=None, height=None, relative_to=None):
+        """Initialize a Section element
+
+        Args:
+            title: The title of the image
+            desc: A description of the image which in most report forms will be
+                figure caption
+            image_file: The path to the image to display. Note: this should resolve
+                to a path inside the report output directory
+            group: Group the images into a JavaScript Lightbox with this name
+                (default: None). Note: this is only relevant for an HTML report
+            height: The height of the image in pixels
+            relative_to: Transform the image path to be relative to this
+                directory. By default, the image will assumed to be in the
+                directory, or a subdirectory, of the page it's displayed on.
+        """
+        super(Image, self).__init__()
+        self.title = title
+        self.desc = desc
+        self.path, self.name = os.path.split(image_file)
+        # FIXME: This assumes that images are images are always located in a
+        #        subdirectory of the current page if a relative path start
+        #        location isn't specified
+        if relative_to is None:
+            relative_to = os.path.dirname(self.path)
+        self.path = os.path.relpath(self.path, relative_to)
+        self.group = group
+        self.height = height
+
+    def _repr_latex(self):
+        template = _latex_env.get_template(self._latex_template)
+        data = self.__dict__
+        data['path'] = self.path.lstrip('/')
+        return template.render(data=data)
+
+
+class B4BImage(Image):
+    """A B4BImage element
+
+    A dummy Image that can be used by the BitForBit element indicating a
+    bit-for-bit verification result.
+    """
+    def __init__(self, title, description, page_path):
+        """Initialize a dummy B4BImage element
+
+        Args:
+            title: The title of the image
+            description: A description of the image which in most report forms
+                will be figure caption
+            page_path: The path to the page on which the dummy image will be
+                displayed
+        """
+        image_file = os.path.join(livvkit.output_dir, 'imgs', 'b4b.png')
+
+        super(B4BImage, self).__init__(title, description,
+                                       image_file=image_file,
+                                       relative_to=page_path,
+                                       height=50, group='b4b')
+
+
+class NAImage(Image):
+    """A NAImage element
+
+    A dummy Image that can be used to indicate a missing image
+    """
+    def __init__(self, title, description, page_path):
+        """Initialize a dummy NAImage element
+
+        Args:
+            title: The title of the image
+            description: A description of the image which in most report forms
+                will be figure caption
+            page_path: The path to the page on which the dummy image will be
+                displayed
+        """
+        image_file = os.path.join(livvkit.output_dir, 'imgs', 'na.png')
+
+        super(NAImage, self).__init__(title, description,
+                                      image_file=image_file,
+                                      relative_to=page_path,
+                                      height=50, group='na')
+
+
+class FileDiff(BaseElement):
+    """A LIVVkit FileDiff element
+
+    The FilleDiff element will compare two text files and produce a git-diff
+    style diff of the files.
+    """
+    _html_template = 'diff.html'
+    _latex_template = 'diff.tex'
+
+    def __init__(self, title, from_file, to_file, context=3):
+        """Initialize a FileDiff element
+
+        Args:
+            title: The title of the diff
+            from_file: A path to the file which will be compared against
+            to_file: A path to the file which which to compare
+            context: An positive int indicating the number of lines of context
+                to display on either side of each difference found
+        """
+        super(FileDiff, self).__init__()
+        self.title = title
+        self.from_file = from_file
+        self.to_file = to_file
+        self.diff, self.diff_status = self.diff_files(context=context)
+
+    def diff_files(self, context=3):
+        """Perform the file diff
+
+        Args:
+            context: An positive int indicating the number of lines of context
+                to display on either side of each difference found
+
+        Returns:
+            (tuple): Tuple containing:
+                difference: A str containing either a git-style diff of the
+                    files if a difference was found or the original file in
+                    full
+                diff_status: A boolean indicating whether any differences were
+                    found
+        """
+        with open(self.from_file) as from_, open(self.to_file) as to_:
+            fromlines = from_.read().splitlines()
+            tolines = to_.read().splitlines()
+
+            if context is None:
+                context = max(len(fromlines), len(tolines))
+
+            diff = list(difflib.unified_diff(fromlines, tolines,
+                                             n=context,  lineterm=''))
+            diff_status = True
+            if not diff:
+                diff_status = False
+                diff = fromlines
+            return diff, diff_status
+
+
+class Error(BaseElement):
+    """A LIVVkit Error element
+
+    The Error element will produce an error message in the analysis report.
+    """
+    _html_template = 'err.html'
+    _latex_template = 'err.tex'
+
+    def __init__(self, title, message):
+        """Initialize a LIVVkit Error element
+
+        Args:
+            title: The title of the error
+            message: The error message to display
+        """
+        super(Error, self).__init__()
+        self.title = title
+        self.message = message
+
+
+class RawHTML(BaseElement):
+    """A LIVVkit RawHTML element
+
+    The RawHTML element will directly display the contained HTML in the analysis
+    report. For an HTML report (default) this will be directly written onto the
+    page so is a potential security hole and should be used with caution. For the
+    experimental report types (e.g., LaTeX) the contained HTML will be written to
+    report in a code display block or as a raw string.
+    """
+    _html_template = 'raw.html'
+    _latex_template = 'raw.tex'
+
+    def __init__(self, html):
+        """Initialize a LIVVkit RawHTML element
+
+        Args:
+            html: An HTML str
+        """
+        super(RawHTML, self).__init__()
+        self.html = html
diff --git a/livvkit/elements/templates/bit4bit.html b/livvkit/elements/templates/bit4bit.html
new file mode 100644
index 0000000..dd6ea4e
--- /dev/null
+++ b/livvkit/elements/templates/bit4bit.html
@@ -0,0 +1,21 @@
+<div class="bitForBit">
+    <h3>{{ data['title'] }}</h3>
+    <table>
+        <tr>
+            {%- for column in data['data'].keys() %}
+            <th>{{ column }}</th>
+            {%- endfor %}
+            <th> Plot </th>
+        </tr>
+        {%- for row in range(rows) %}
+        <tr>
+            {%- for column in data['data'].keys() %}
+            <td>{% if data['data'][column][row] is number %}{{ '{:.5e}'.format(data['data'][column][row]) }}{% else %}{{ data['data'][column][row] }}{% endif %}</td>
+            {%- endfor %}
+            <td>
+                {{ b4b_imgs[row] }}
+            </td>
+        </tr>
+        {%- endfor %}
+    </table>
+</div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/bit4bit.tex b/livvkit/elements/templates/bit4bit.tex
new file mode 100644
index 0000000..0b30a06
--- /dev/null
+++ b/livvkit/elements/templates/bit4bit.tex
@@ -0,0 +1,9 @@
+\begin{table}[h!]
+    \centering
+    \begin{tabular}{\BLOCK{ for column in data['data'].keys() } c \BLOCK{ endfor } c }
+        \BLOCK{ for column in data['data'].keys() }\VAR{ column } & \BLOCK{ endfor }Plots \\
+        \hline
+        \BLOCK{ for row in range(rows) -}
+        \BLOCK{ for column in data['data'].keys() }\BLOCK{ if data['data'][column][row] is number }\VAR{ '{:.5e}'.format(data['data'][column][row]) }\BLOCK{ else }\VAR{ data['data'][column][row] }\BLOCK{ endif } & \BLOCK{ endfor }\begin{minipage}{0.3\textwidth}\VAR{ b4b_imgs[row] }\end{minipage} \\
+        \BLOCK{ endfor }\end{tabular}
+\end{table}
\ No newline at end of file
diff --git a/livvkit/elements/templates/diff.html b/livvkit/elements/templates/diff.html
new file mode 100644
index 0000000..680d6d7
--- /dev/null
+++ b/livvkit/elements/templates/diff.html
@@ -0,0 +1,8 @@
+<h3>{{ data['title'] }}</h3>
+<div class="diff">{% for line in data['diff'] -%}{% if line[0] == '+' %}
+    <p class="new">{{ line }}</p>{% elif line[0] == '-' %}
+    <p class="old">{{ line }}</p>{% elif line[0:2] == '@@' %}
+    <p class="range">{{ line }}</p>{% else %}
+    <p>{{ line }}</p>{% endif %}
+{%- endfor %}
+</div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/diff.tex b/livvkit/elements/templates/diff.tex
new file mode 100644
index 0000000..4b29455
--- /dev/null
+++ b/livvkit/elements/templates/diff.tex
@@ -0,0 +1,5 @@
+\begin{minted}{diff}
+\BLOCK{ for line in data['diff'] }
+\VAR{ line }
+\BLOCK{ endfor }
+\end{minted}
\ No newline at end of file
diff --git a/livvkit/elements/templates/document.tex b/livvkit/elements/templates/document.tex
new file mode 100644
index 0000000..fcc5b6c
--- /dev/null
+++ b/livvkit/elements/templates/document.tex
@@ -0,0 +1,13 @@
+\documentclass[11pt]{article}
+
+\usepackage{amsmath}
+\usepackage{graphicx}
+\usepackage{xcolor}
+\usepackage{minted}
+\usepackage{coseoul}
+
+\begin{document}
+
+    \VAR{ data['doc_body'] }
+
+\end{document}
\ No newline at end of file
diff --git a/livvkit/elements/templates/err.html b/livvkit/elements/templates/err.html
new file mode 100644
index 0000000..29f7e1d
--- /dev/null
+++ b/livvkit/elements/templates/err.html
@@ -0,0 +1,4 @@
+<div class="error">
+    <h3>{{ data["title"] }}</h3>
+    <p>{{ data["message"] }}</p>
+</div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/err.tex b/livvkit/elements/templates/err.tex
new file mode 100644
index 0000000..96a34bb
--- /dev/null
+++ b/livvkit/elements/templates/err.tex
@@ -0,0 +1,3 @@
+\colorbox{red}{\parbox{\textwidth}{
+    \textbf{\VAR{data['title']}}: \VAR{data['message']}
+}}
\ No newline at end of file
diff --git a/livvkit/elements/templates/gallery.html b/livvkit/elements/templates/gallery.html
new file mode 100644
index 0000000..b7df50e
--- /dev/null
+++ b/livvkit/elements/templates/gallery.html
@@ -0,0 +1,7 @@
+<div class="gallery">
+    <h3>{{ data['title'] }}</h3>
+    {% for elem in elements -%}
+    {{ elem }}
+    {% endfor %}
+</div>
+<div style="clear:both"></div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/gallery.tex b/livvkit/elements/templates/gallery.tex
new file mode 100644
index 0000000..62dab16
--- /dev/null
+++ b/livvkit/elements/templates/gallery.tex
@@ -0,0 +1,4 @@
+\levelstay{\VAR{ data['title']}}
+\BLOCK{ for elem in elements }
+    \VAR{ elem }
+\BLOCK{ endfor }
\ No newline at end of file
diff --git a/livvkit/elements/templates/image.html b/livvkit/elements/templates/image.html
new file mode 100644
index 0000000..1fca46d
--- /dev/null
+++ b/livvkit/elements/templates/image.html
@@ -0,0 +1,13 @@
+<div>
+    <a href="{{ data['path'] ~ '/' ~ data['name'] }}"
+       data-lightbox="{{ data['group'] if data['group'] else data['title'] }}"
+       data-title="{{ data['desc'] }}"
+    >
+        <img class="thumbnail caption"
+             data-caption="{{ data['title'] }}"
+             alt="{{ data['title'] }}"
+             src="{{ data['path'] ~ '/' ~ data['name'] }}"
+             style="height: {{ data['height'] if data['height'] else 200 }}px; overflow: hidden; position: relative;"
+        >
+    </a>
+</div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/image.tex b/livvkit/elements/templates/image.tex
new file mode 100644
index 0000000..8d50aef
--- /dev/null
+++ b/livvkit/elements/templates/image.tex
@@ -0,0 +1,5 @@
+\begin{figure}[h]
+    \centering
+    \includegraphics[height=\VAR{ data['height']|string()+'px' if data['height'] else '200px' }]{\VAR{ data['path'] ~ '/' ~ data['name'] }}
+    \caption[\VAR{ data['title'] }]{\VAR{ data['desc'] }}
+\end{figure}
\ No newline at end of file
diff --git a/livvkit/elements/templates/page.html b/livvkit/elements/templates/page.html
new file mode 100644
index 0000000..ad0d29e
--- /dev/null
+++ b/livvkit/elements/templates/page.html
@@ -0,0 +1,7 @@
+<div id="{{ data["title"] }}">
+    {% if data["title"] != "Summary" %}<h2>{{ data["title"] }}</h2>{% endif %}
+    {% if data["description"] %}<p>{{ data["description"] }}</p>{% endif %}
+    {% for elem in elements -%}
+    {{ elem }}
+    {%- endfor %}
+</div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/page.tex b/livvkit/elements/templates/page.tex
new file mode 100644
index 0000000..0ce3b37
--- /dev/null
+++ b/livvkit/elements/templates/page.tex
@@ -0,0 +1,5 @@
+\levelstay{\VAR{ data['title']}}
+\VAR{data['description']}
+\BLOCK{ for elem in elements }
+    \VAR{ elem }
+\BLOCK{ endfor }
\ No newline at end of file
diff --git a/livvkit/elements/templates/raw.html b/livvkit/elements/templates/raw.html
new file mode 100644
index 0000000..87f8a28
--- /dev/null
+++ b/livvkit/elements/templates/raw.html
@@ -0,0 +1,3 @@
+<div>
+    {{ data['html'] }}
+</div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/raw.tex b/livvkit/elements/templates/raw.tex
new file mode 100644
index 0000000..ff704d7
--- /dev/null
+++ b/livvkit/elements/templates/raw.tex
@@ -0,0 +1,3 @@
+\begin{minted}{html}
+    \VAR{ data['html'] }
+\end{minted}
\ No newline at end of file
diff --git a/livvkit/elements/templates/section.html b/livvkit/elements/templates/section.html
new file mode 100644
index 0000000..d15254e
--- /dev/null
+++ b/livvkit/elements/templates/section.html
@@ -0,0 +1,6 @@
+<div class="section">
+    <h2>{{ data['title'] }}</h2>
+    {% for elem in elements -%}
+    {{ elem }}
+    {%- endfor %}
+</div>
\ No newline at end of file
diff --git a/livvkit/elements/templates/section.tex b/livvkit/elements/templates/section.tex
new file mode 100644
index 0000000..62dab16
--- /dev/null
+++ b/livvkit/elements/templates/section.tex
@@ -0,0 +1,4 @@
+\levelstay{\VAR{ data['title']}}
+\BLOCK{ for elem in elements }
+    \VAR{ elem }
+\BLOCK{ endfor }
\ No newline at end of file
diff --git a/livvkit/elements/templates/table.html b/livvkit/elements/templates/table.html
new file mode 100644
index 0000000..d3f247f
--- /dev/null
+++ b/livvkit/elements/templates/table.html
@@ -0,0 +1,23 @@
+<div class="table">
+    <h3>{{ data['title'] }}</h3>
+    <table>
+        <tr>
+            {%- if index is not none %}
+            <th>&nbsp;</th>
+            {%- endif %}
+            {%- for column in data['data'].keys() %}
+            <th>{{ column }}</th>
+            {%- endfor %}
+        </tr>
+        {%- for row in range(rows) %}
+        <tr>
+            {%- if index is not none %}
+            <th>{{index[row]}}</th>
+            {%- endif %}
+            {%- for column in data['data'].keys() %}
+            <td>{{ data['data'][column][row] }}</td>
+            {%- endfor %}
+        </tr>
+        {%- endfor %}
+    </table>
+</div>
diff --git a/livvkit/elements/templates/table.tex b/livvkit/elements/templates/table.tex
new file mode 100644
index 0000000..5968907
--- /dev/null
+++ b/livvkit/elements/templates/table.tex
@@ -0,0 +1,9 @@
+\begin{table}[h!]
+    \centering
+    \begin{tabular}{\BLOCK{ if index is not none } r | \BLOCK{ endif }\BLOCK{ for column in data['data'].keys() }c\BLOCK{ endfor }}
+        \BLOCK{ if index is not none } & \BLOCK{ endif }\BLOCK{ for column in data['data'].keys() }\VAR{ column }\BLOCK{ if not loop.last } & \BLOCK{ endif }\BLOCK{ endfor } \\
+        \hline
+        \BLOCK{ for row in range(rows) -}
+        \BLOCK{ if index is not none }\VAR{index[row]} & \BLOCK{ endif }\BLOCK{ for column in data['data'].keys() }\VAR{ data['data'][column][row] }\BLOCK{ if not loop.last } & \BLOCK{ endif }\BLOCK{ endfor }  \\
+        \BLOCK{ endfor }\end{tabular}
+\end{table}
\ No newline at end of file
diff --git a/livvkit/elements/templates/table_transposed.html b/livvkit/elements/templates/table_transposed.html
new file mode 100644
index 0000000..b818465
--- /dev/null
+++ b/livvkit/elements/templates/table_transposed.html
@@ -0,0 +1,21 @@
+<div class="table">
+    <h3>{{ data['title'] }}</h3>
+    <table>
+        {%- if index is not none %}
+        <tr>
+            <th>&nbsp;</th>
+            {%- for idx in index %}
+            <th>{{ idx }}</th>
+            {%- endfor %}
+        </tr>
+        {%- endif %}
+        {%- for column in data['data'].keys() %}
+        <tr>
+            <th>{{column}}</th>
+            {%- for val in data['data'][column] %}
+            <td>{{ val }}</td>
+            {%- endfor %}
+        </tr>
+        {%- endfor %}
+    </table>
+</div>
diff --git a/livvkit/elements/templates/table_transposed.tex b/livvkit/elements/templates/table_transposed.tex
new file mode 100644
index 0000000..e8c8ffd
--- /dev/null
+++ b/livvkit/elements/templates/table_transposed.tex
@@ -0,0 +1,10 @@
+\begin{table}[h!]
+    \centering
+    \begin{tabular}{r|\BLOCK{ for row in range(data['rows'])}c\BLOCK{ endfor }}
+        \BLOCK{ if index is not none } & \BLOCK{ for idx in index }\VAR{ idx }\BLOCK{ if not loop.last } & \BLOCK{ endif }\BLOCK{ endfor }\\
+        \hline
+        \BLOCK{ endif -}
+        \BLOCK{ for column in data['data'].keys() -}
+        \VAR{column} & \BLOCK{ for val in data['data'][column] }\VAR{val }\BLOCK{ if not loop.last } & \BLOCK{ endif }\BLOCK{ endfor }  \\
+        \BLOCK{ endfor }\end{tabular}
+\end{table}
\ No newline at end of file
diff --git a/livvkit/elements/templates/tabs.html b/livvkit/elements/templates/tabs.html
new file mode 100644
index 0000000..556ff55
--- /dev/null
+++ b/livvkit/elements/templates/tabs.html
@@ -0,0 +1,12 @@
+<div id="tabs">
+    <ul>
+        {%- for title in elements_dict.keys() %}
+        <li><a href="#{{title}}">{{ title }}</a></li>
+        {%- endfor %}
+    </ul>
+    {%- for title, elements in elements_dict.items() %}
+    <div id="{{title}}">
+        {% for elem in elements %}{{ elem }}{% endfor %}
+    </div>
+    {%- endfor %}
+</div>
diff --git a/livvkit/elements/templates/tabs.tex b/livvkit/elements/templates/tabs.tex
new file mode 100644
index 0000000..40c9106
--- /dev/null
+++ b/livvkit/elements/templates/tabs.tex
@@ -0,0 +1,4 @@
+\BLOCK{ for title, elements in elements_dict.items() }
+\levelstay{\VAR{ title }}
+    \BLOCK{ for elem in elements }\VAR{ elem }\BLOCK{ endfor }
+\BLOCK{ endfor }
\ No newline at end of file
diff --git a/livvkit/resources/css/livv.css b/livvkit/resources/css/livv.css
index 7fbd3c7..2f127fe 100644
--- a/livvkit/resources/css/livv.css
+++ b/livvkit/resources/css/livv.css
@@ -8,10 +8,7 @@ html {
 
 body {
 	height: 100%;
-	margin-top: 0px;
-	margin-right: 0px;
-	margin-bottom: 0px;
-	margin-left: 0px;
+    margin: 0;
     min-width: 1125px;
     background: url("../imgs/background.jpg") no-repeat center center fixed;
 }
@@ -54,9 +51,8 @@ a {
 }
 
 #header-icon {
-    float: left; 
-    padding: 10px; 
-    padding-left: 15px;
+    float: left;
+    padding: 10px 10px 10px 15px;
     width: 40px; 
 }
 
@@ -94,7 +90,7 @@ a {
 #wrapper {
     margin: auto;
     width: 80%;
-    background-color: white;
+    background-color: #ffffff;
     overflow: auto;
 }
 
@@ -111,7 +107,7 @@ a {
 }
 
 #content div {
-    background-color: white;
+    background-color: #ffffff;
 }
 
 /******************************************************************************
@@ -149,7 +145,7 @@ td {
 .error {}
 
 .error p {
-    color: red;
+    color: #ff0000;
 }
 
 .tabs {
@@ -163,17 +159,23 @@ td {
 
 .diff p {
     line-height: 1.05em;
-    margin: 0px;
+    margin: 0;
     padding-left:10%;
     font-family: monospace;
 }
 
 .diff p.old {
-    background-color: #ffcccc;
+    background-color: #ffeef0;
+    color: #b31d28;
 }
 
 .diff p.new {
-    background-color: #ccffcc;
+    background-color: #f0fff4;
+    color: #22863a;
+}
+
+.diff p.range {
+    color: #6f42c1;
 }
 
 .bitForBit {}
@@ -184,8 +186,8 @@ td {
 
 .gallery p {
     text-align: center;
-    padding: none;
-    margin: none;
+    padding: 0;
+    margin: 0;
 }
 
 .gallery div {
@@ -199,3 +201,12 @@ td {
 }
 
 .summary {}
+
+.bitForBit figure {
+    margin-block-start: 0;
+    margin-block-end: 0;
+    margin-inline-start: 0;
+    margin-inline-end: 0;
+    margin-left: auto;
+    margin-right: auto;
+}
diff --git a/livvkit/resources/css/normalize.css b/livvkit/resources/css/normalize.css
new file mode 100644
index 0000000..b0c1902
--- /dev/null
+++ b/livvkit/resources/css/normalize.css
@@ -0,0 +1,349 @@
+/*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */
+
+/* Document
+   ========================================================================== */
+
+/**
+ * 1. Correct the line height in all browsers.
+ * 2. Prevent adjustments of font size after orientation changes in iOS.
+ */
+
+html {
+  line-height: 1.15; /* 1 */
+  -webkit-text-size-adjust: 100%; /* 2 */
+}
+
+/* Sections
+   ========================================================================== */
+
+/**
+ * Remove the margin in all browsers.
+ */
+
+body {
+  margin: 0;
+}
+
+/**
+ * Render the `main` element consistently in IE.
+ */
+
+main {
+  display: block;
+}
+
+/**
+ * Correct the font size and margin on `h1` elements within `section` and
+ * `article` contexts in Chrome, Firefox, and Safari.
+ */
+
+h1 {
+  font-size: 2em;
+  margin: 0.67em 0;
+}
+
+/* Grouping content
+   ========================================================================== */
+
+/**
+ * 1. Add the correct box sizing in Firefox.
+ * 2. Show the overflow in Edge and IE.
+ */
+
+hr {
+  box-sizing: content-box; /* 1 */
+  height: 0; /* 1 */
+  overflow: visible; /* 2 */
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+pre {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/* Text-level semantics
+   ========================================================================== */
+
+/**
+ * Remove the gray background on active links in IE 10.
+ */
+
+a {
+  background-color: transparent;
+}
+
+/**
+ * 1. Remove the bottom border in Chrome 57-
+ * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari.
+ */
+
+abbr[title] {
+  border-bottom: none; /* 1 */
+  text-decoration: underline; /* 2 */
+  text-decoration: underline dotted; /* 2 */
+}
+
+/**
+ * Add the correct font weight in Chrome, Edge, and Safari.
+ */
+
+b,
+strong {
+  font-weight: bolder;
+}
+
+/**
+ * 1. Correct the inheritance and scaling of font size in all browsers.
+ * 2. Correct the odd `em` font sizing in all browsers.
+ */
+
+code,
+kbd,
+samp {
+  font-family: monospace, monospace; /* 1 */
+  font-size: 1em; /* 2 */
+}
+
+/**
+ * Add the correct font size in all browsers.
+ */
+
+small {
+  font-size: 80%;
+}
+
+/**
+ * Prevent `sub` and `sup` elements from affecting the line height in
+ * all browsers.
+ */
+
+sub,
+sup {
+  font-size: 75%;
+  line-height: 0;
+  position: relative;
+  vertical-align: baseline;
+}
+
+sub {
+  bottom: -0.25em;
+}
+
+sup {
+  top: -0.5em;
+}
+
+/* Embedded content
+   ========================================================================== */
+
+/**
+ * Remove the border on images inside links in IE 10.
+ */
+
+img {
+  border-style: none;
+}
+
+/* Forms
+   ========================================================================== */
+
+/**
+ * 1. Change the font styles in all browsers.
+ * 2. Remove the margin in Firefox and Safari.
+ */
+
+button,
+input,
+optgroup,
+select,
+textarea {
+  font-family: inherit; /* 1 */
+  font-size: 100%; /* 1 */
+  line-height: 1.15; /* 1 */
+  margin: 0; /* 2 */
+}
+
+/**
+ * Show the overflow in IE.
+ * 1. Show the overflow in Edge.
+ */
+
+button,
+input { /* 1 */
+  overflow: visible;
+}
+
+/**
+ * Remove the inheritance of text transform in Edge, Firefox, and IE.
+ * 1. Remove the inheritance of text transform in Firefox.
+ */
+
+button,
+select { /* 1 */
+  text-transform: none;
+}
+
+/**
+ * Correct the inability to style clickable types in iOS and Safari.
+ */
+
+button,
+[type="button"],
+[type="reset"],
+[type="submit"] {
+  -webkit-appearance: button;
+}
+
+/**
+ * Remove the inner border and padding in Firefox.
+ */
+
+button::-moz-focus-inner,
+[type="button"]::-moz-focus-inner,
+[type="reset"]::-moz-focus-inner,
+[type="submit"]::-moz-focus-inner {
+  border-style: none;
+  padding: 0;
+}
+
+/**
+ * Restore the focus styles unset by the previous rule.
+ */
+
+button:-moz-focusring,
+[type="button"]:-moz-focusring,
+[type="reset"]:-moz-focusring,
+[type="submit"]:-moz-focusring {
+  outline: 1px dotted ButtonText;
+}
+
+/**
+ * Correct the padding in Firefox.
+ */
+
+fieldset {
+  padding: 0.35em 0.75em 0.625em;
+}
+
+/**
+ * 1. Correct the text wrapping in Edge and IE.
+ * 2. Correct the color inheritance from `fieldset` elements in IE.
+ * 3. Remove the padding so developers are not caught out when they zero out
+ *    `fieldset` elements in all browsers.
+ */
+
+legend {
+  box-sizing: border-box; /* 1 */
+  color: inherit; /* 2 */
+  display: table; /* 1 */
+  max-width: 100%; /* 1 */
+  padding: 0; /* 3 */
+  white-space: normal; /* 1 */
+}
+
+/**
+ * Add the correct vertical alignment in Chrome, Firefox, and Opera.
+ */
+
+progress {
+  vertical-align: baseline;
+}
+
+/**
+ * Remove the default vertical scrollbar in IE 10+.
+ */
+
+textarea {
+  overflow: auto;
+}
+
+/**
+ * 1. Add the correct box sizing in IE 10.
+ * 2. Remove the padding in IE 10.
+ */
+
+[type="checkbox"],
+[type="radio"] {
+  box-sizing: border-box; /* 1 */
+  padding: 0; /* 2 */
+}
+
+/**
+ * Correct the cursor style of increment and decrement buttons in Chrome.
+ */
+
+[type="number"]::-webkit-inner-spin-button,
+[type="number"]::-webkit-outer-spin-button {
+  height: auto;
+}
+
+/**
+ * 1. Correct the odd appearance in Chrome and Safari.
+ * 2. Correct the outline style in Safari.
+ */
+
+[type="search"] {
+  -webkit-appearance: textfield; /* 1 */
+  outline-offset: -2px; /* 2 */
+}
+
+/**
+ * Remove the inner padding in Chrome and Safari on macOS.
+ */
+
+[type="search"]::-webkit-search-decoration {
+  -webkit-appearance: none;
+}
+
+/**
+ * 1. Correct the inability to style clickable types in iOS and Safari.
+ * 2. Change font properties to `inherit` in Safari.
+ */
+
+::-webkit-file-upload-button {
+  -webkit-appearance: button; /* 1 */
+  font: inherit; /* 2 */
+}
+
+/* Interactive
+   ========================================================================== */
+
+/*
+ * Add the correct display in Edge, IE 10+, and Firefox.
+ */
+
+details {
+  display: block;
+}
+
+/*
+ * Add the correct display in all browsers.
+ */
+
+summary {
+  display: list-item;
+}
+
+/* Misc
+   ========================================================================== */
+
+/**
+ * Add the correct display in IE 10+.
+ */
+
+template {
+  display: none;
+}
+
+/**
+ * Add the correct display in IE 10.
+ */
+
+[hidden] {
+  display: none;
+}
\ No newline at end of file
diff --git a/livvkit/resources/favicon.ico b/livvkit/resources/favicon.ico
new file mode 100644
index 0000000..b84cc64
Binary files /dev/null and b/livvkit/resources/favicon.ico differ
diff --git a/livvkit/resources/imgs/b4b.png b/livvkit/resources/imgs/b4b.png
new file mode 100644
index 0000000..dff486d
Binary files /dev/null and b/livvkit/resources/imgs/b4b.png differ
diff --git a/livvkit/resources/imgs/na.png b/livvkit/resources/imgs/na.png
new file mode 100644
index 0000000..5948675
Binary files /dev/null and b/livvkit/resources/imgs/na.png differ
diff --git a/livvkit/resources/index.html b/livvkit/resources/index.html
index b7e75a6..1891841 100644
--- a/livvkit/resources/index.html
+++ b/livvkit/resources/index.html
@@ -3,7 +3,9 @@
     <head>
         <meta charset="utf-8">
         <title>LIVVkit</title>
+        <link rel="shortcut icon" type="image/x-icon" href="favicon.ico"/>
         <link href='http://fonts.googleapis.com/css?family=Lato' rel='stylesheet' type='text/css'>
+        <link href="css/normalize.css" type="text/css" rel="stylesheet">
         <link href="css/jquery-ui.min.css" type="text/css" rel="stylesheet">
         <link href="css/captionjs.min.css" type="text/css" rel="stylesheet">
         <link href="css/lightbox.min.css" type="text/css" rel="stylesheet">
diff --git a/livvkit/resources/js/common.js b/livvkit/resources/js/common.js
index 91ee9f1..05bce8a 100644
--- a/livvkit/resources/js/common.js
+++ b/livvkit/resources/js/common.js
@@ -33,22 +33,6 @@
  * When the page is loaded, do some stuff
  */
 $(document).ready(function() {
-    // Used to draw the correct element type
-    elementMap = {
-        "Error"          : drawError,
-        "Summary"        : drawSummary,
-        "ValSummary"     : drawValSummary,
-        "bookSummary"    : drawValSummary,
-        "Diff"           : drawDiff,
-        "Bit for Bit"    : drawBitForBit,
-        "Gallery"        : drawGallery,
-        "Table"          : drawTable,
-        "Vertical Table" : drawVTable,
-        "V-H Table"      : drawVHTable,
-        "HTML"           : drawHTML
-    };
-
-    // Append the generated content
     drawNav();
     drawContent();
 });
@@ -65,34 +49,17 @@ $(window).load(function() {
 function drawNav() {
     // Get the dataset, to do so the html page must have the `indexPath` variable defined
     var html = "";
-    getUrl = window.location.href.substr(0,window.location.href.lastIndexOf('/')+1);
-    data = loadJSON(getUrl + indexPath + '/index.json');
-    
+    var getUrl = window.location.href.substr(0,window.location.href.lastIndexOf('/')+1);
+    var data = loadJSON(getUrl + indexPath + '/index.json');
+    data = data["Page"];
     // Go through each category: numerics, verification, performance, and validation
-    for (var cat in data["Data"]["Elements"]) {
-        if (data["Data"]["Elements"][cat] != null && Object.keys(data["Data"]["Elements"][cat]["Data"]).length > 0) {
-            if (data["Data"]["Elements"][cat]["Title"] === "Validation" && 
-                data["Data"]["Elements"][cat]["Type"] === "ValSummary") {
-                html += "<h3>" + data["Data"]["Elements"][cat]["TableTitle"] + "</h3>\n";
-            } else if (data["Data"]["Elements"][cat]["Type"] !== "bookSummary") { 
-                html += "<h3>" + data["Data"]["Elements"][cat]["Title"] + "</h3>\n";
-            }
-            // Add the tests for each category
-            if (data["Data"]["Elements"][cat]["Type"] === "bookSummary") {
-                pageList = Object.keys(data["Data"]["Elements"][cat]["Data"]).sort();
-                for (idx in pageList) {
-                html += "<h3>" + pageList[idx] + "</h3>\n";
-                    for (jdx in data["Data"]["Elements"][cat]["Data"][pageList[idx]]) {
-                        html += "<a href=" + indexPath + "/" + data["Data"]["Elements"][cat]["Title"].toLowerCase() + 
-                                "/" + jdx + ".html>" + jdx + "</a></br>";
-                    }
-                }
-            } else {
-                testList = Object.keys(data["Data"]["Elements"][cat]["Data"]).sort();
-                for (idx in testList) {
-                    html += "<a href=" + indexPath + "/" + data["Data"]["Elements"][cat]["Title"].toLowerCase() + 
-                            "/" + testList[idx] + ".html>" + testList[idx] + "</a></br>";
-                }
+    for (var el_idx in data["elements"]) {
+        if (data["elements"][el_idx] != null && Object.keys(data["elements"][el_idx]["Table"]["data"]).length > 0) {
+            html += "<h3>" + data["elements"][el_idx]["Table"]["title"] + "</h3>\n";
+            var testList = Array.from(new Set( data["elements"][el_idx]["Table"]["index"])).sort();
+            for (var idx in testList) {
+                html += "<a href=" + indexPath + "/" + data["elements"][el_idx]["Table"]["title"].toLowerCase() +
+                        "/" + testList[idx] + ".html>" + testList[idx] + "</a></br>";
             }
         }
     }
@@ -106,467 +73,16 @@ function drawNav() {
  */
 function drawContent() {
     // Load the data and add header information
-    var verType = window.location.href.substr(
+    var html_file = window.location.href.substr(
             window.location.href.lastIndexOf("/")+1).split("#")[0].replace(".html", "");
-    var data = loadJSON('./' + verType + ".json");
-    var html = "<div id=" + data["Title"] + ">";
-    if (data["Title"] != "Summary") {
-        html += "<h2>" + data["Title"] + "</h2>";
+    // Assume index.html if empty page name
+    if (html_file === "") {
+        html_file = "index";
     }
-    html += "<p>" + data["Description"];
-    html += "</div>";
+    var data = loadJSON('./' + html_file + ".json");
+    var html = data["Page"]["Data"];
     $("#content").append(html);
-  
-    // If there are plain data elements in the dataset draw and add them to the div
-    var content = data["Data"];
-    if ("Elements" in content) {
-        // Add the content
-        for (var idx in content["Elements"]) {
-            elem = content["Elements"][idx];
-            elementMap[elem["Type"]](elem, "#"+data["Title"]);
-        }
-    }
-   
-    // If there are tabbed data elements in the dataset draw and add them to the div
-    if ("Tabs" in content) {
-        // Add the tabs
-        var tabs = content["Tabs"];
-        tabs = tabs.sort();
-        console.log(tabs);
-
-        var html = "<div id=\"tabs\">\n";
-        html += "<ul>\n";
-        for (var idx in tabs) {
-            console.log(idx);
-            var tab = tabs[idx];
-            html += "<li><a href=\"#" + tab["Title"] + "\">" + 
-                    tab["Title"] + "</a></li>\n";
-        }
-        html += "</ul>\n";
-
-        for (var idx in tabs) {
-            console.log(idx);
-            var tab = tabs[idx];
-            html += "<div id=\"" + tabs[idx]["Title"] + "\"></div>";
-        }
-        html += "</div>";
-        $("#content").append(html);
-
-        // Add the content
-        for (var idx in tabs) {
-            var tab = tabs[idx];
-            if ("Elements" in tab) {
-                var elements = tab["Elements"];
-                for (var elem_idx in elements) {
-                   elem = elements[elem_idx];
-                   elementMap[elem["Type"]](elem, "#"+tab["Title"]);
-                }
-            }
-            if ("Sections" in tab) {
-                var sections = tab["Sections"];
-                for (var tab_idx in sections) {
-                    var section  = sections[tab_idx];
-                    // Draw header
-                    var html = "<h2>" + section["Title"] + "</h3>";
-                    $("#"+tab["Title"]).append(html);
-                    if ("Elements" in section) {
-                        var elements = section["Elements"];
-                        for (var elem_idx in elements) {
-                            elem = elements[elem_idx];
-                            elementMap[elem["Type"]](elem, "#"+tab["Title"]);
-                        }
-                    }          
-                }
-            }
-        }
-        $("#tabs").tabs();
-    }
-}
-
-
-/**
- * Build a summary and adds it to the div.
- *
- * @param {Object} data - The data representing the summary.  Determined by data["Type"] = "Summary"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawSummary(data, div) {
-    var html = "<h3>" + data["Title"] + "</h3>";
-    html += "<table class=\"summary\">\n";
-    // Add the headers
-    html += "<th></th>\n";
-    for (var header in data["Headers"]) {
-        html += "<th>" + data["Headers"][header] + "</th>\n";
-    }
-    // Add the data
-    var testNames = Object.keys(data["Data"]).sort();
-    for (var idx in testNames) {
-        testName = testNames[idx];
-        html += "<tr class=\"testName\"><td>" + testName + "</td></tr>\n";
-        for (var testCase in data["Data"][testName]) {
-            html_tmp1 = "<tr ";
-            html_tmp2 = ">\n<td class=\"testCase\">" + testCase + "</td>\n";
-            style = "";  
-            for (var headerIdx in data["Headers"]) {
-                header = data["Headers"][headerIdx];
-                html_tmp2 += "<td>"; 
-                value = data["Data"][testName][testCase][header];
-                dtype = typeof value;
-                if (dtype == 'number' || dtype == 'string') {
-                    html_tmp2 += value;
-                } else if (dtype == 'object') {
-                    if (value.length == 2) {
-                        html_tmp2 += value[0] + " of " + value[1];
-                        // Handle failures
-                        if (value[0] != value[1]) {
-                            style = "style=\"color:red\"; ";
-                        }
-                    }
-                }
-                html_tmp2 += "</td>\n";
-            }
-            html += html_tmp1 + style + html_tmp2 + "</tr>\n";
-        }
-    }
-    html += "</table>\n";
-    $(div).append(html);
-}
-
-/**
- * Build a validation extension summary and adds it to the div.
- *
- * @param {Object} data - The data representing the summary.  Determined by data["Type"] = "Summary"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawValSummary(data, div) {
-    var html = "<h3>" + data["TableTitle"] + "</h3>";
-    html += "<table class=\"summary\">\n";
-    // Add the headers
-    html += "<th></th>\n";
-    for (var header in data["Headers"]) {
-        html += "<th>" + data["Headers"][header] + "</th>\n";
-    }
-    // Add the data
-    var testNames = Object.keys(data["Data"]).sort();
-    for (var idx in testNames) {
-        testName = testNames[idx];
-        html += "<tr class=\"testName\"><td>" + testName + "</td></tr>\n";
-        for (var testCase in data["Data"][testName]) {
-            html_tmp1 = "<tr ";
-            html_tmp2 = ">\n<td class=\"testCase\">" + testCase + "</td>\n";
-            style = "";  
-            for (var headerIdx in data["Headers"]) {
-                header = data["Headers"][headerIdx];
-                html_tmp2 += "<td>"; 
-                value = data["Data"][testName][testCase][header];
-                dtype = typeof value;
-                if (dtype == 'number' || dtype == 'string') {
-                    html_tmp2 += value;
-                } else if (dtype == 'object') {
-                    if (value.length == 2) {
-                        html_tmp2 += value[0] + " of " + value[1];
-                        // Handle failures
-                        if (value[0] != value[1]) {
-                            style = "style=\"color:red\"; ";
-                        }
-                    }
-                }
-                html_tmp2 += "</td>\n";
-            }
-            html += html_tmp1 + style + html_tmp2 + "</tr>\n";
-        }
-    }
-    html += "</table>\n";
-    $(div).append(html);
-}
-
-/**
- * Build an error message and appends it to the div.
- *
- * @param {Object} data - The error element data.  Determined by having data["Type"] = "Error"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawError(data, div) {
-    var html = "<div class=\"error\">";
-    html += "<h3>" + data["Title"] + "</h3>\n";
-    html += "<p>ERROR: " + data["Message"] + "</p>\n";
-    html += "</div>";
-    $(div).append(html);
-}
-
-
-/**
- * Build a file diff
- *
- * @param {Object} data - The data representing the table.  Determined by data["Type"] = "Diff"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawDiff(data, div) {
-    controller = div.replace("#","") + "_controller";
-    // TODO: What's up with this button!?
-    var html = "<h3>" + data["Title"] +"</h3>";//+"  <button id=\"" + controller + "\">Hide/Show</button></h3>";
-    html += "<div class=\"diff\" id=\""+div+"\">";
-    for (var section in data["Data"]) {
-        html += "<b>[" + section + "]</b>";
-        for (var varName in data["Data"][section]) {
-            arr = data["Data"][section][varName];
-            // First element determines if the elements matched, otherwise draw what the change was
-            if (arr[0]) {
-                html += "<p>   " + varName + " = " + arr[1] + "</p>";
-            } else {
-                html += "<p class=\"new\"> + " + varName + " = " + arr[1] + "</p>";
-                html += "<p class=\"old\"> - " + varName + " = " + arr[2] + "</p>";
-            }
-        }
-    }
-    html += "</div>";
-    $(div).append(html);
-    $("."+controller).click(function() {
-        $(div).toggle();
-    });
-}
-
-
-/**
- * Build a bit for bit table
- * 
- * @param {Object} data - The data representing the table.  Determined by 
- *                        data["Type"] = "Bit for Bit"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawBitForBit(data, div) {
-    var html = "<h3>" + data["Title"] + "</h3>\n";
-    html += "<div class=\"bitForBit\">";
-    html += "<table>\n";
-    html += "<th>Variable</th>\n";
-    // Draw the headers
-    for (var idx in data["Headers"]) {
-        html += "<th>" + data["Headers"][idx] + "</th>\n";
-    }
-    // Draw the cells
-    for (var varName in data["Data"]) {
-        html += "<tr>\n";
-        html += "<td>" + varName + "</td>\n";
-        for (var j in data["Headers"]) {
-            var header = data["Headers"][j];
-            var hData = data["Data"][varName][header];
-            // Handle the different data types to draw (image vs string/numeric data)
-            if (header == "Plot" && (hData !== "N/A" || hData.indexOf("ERROR:")!==-1)) {
-                var img_dict = {};
-                img_dict["Plot File"] = data["Data"][varName][header];
-                img_dict["Title"] = varName + "Bit-for-bit compairson";
-                html += "<td>" + drawThumbnail(img_dict, 50) + "</td>\n";
-            } else {
-                if (typeof hData == 'number') {
-                    hData = hData.toExponential(5);
-                }
-                html += "<td>" + hData + "</td>\n";
-            }
-        }
-        html += "</tr>\n";
-    }
-    html += "</table>\n";
-    html += "</div>";
-    $(div).append(html);
-}
-
-/**
- * Build a table
- *
- * @param {Object} data - The data representing the table.  Determined by data["Type"] = "Table"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawTable(data, div) {
-    var html = "<h3>" + data["Title"] + "</h3>\n";
-    html += "<div class=\"table\">";
-    html += "<table>\n";
-    // Draw the headers
-    for (var idx in data["Headers"]) {
-        html += "<th>" + data["Headers"][idx] + "</th>\n";
-    }
-
-    // Draw the cells
-    html += "<tr>\n";
-    for (var idx in data["Headers"]) {
-        html += "<td>" + data["Data"][data["Headers"][idx]] + "</td>\n";
-    }
-    html += "</tr>\n </table>\n </div>";
-    $(div).append(html);
-}
-
-/**
- * Build a vertical table
- *
- * @param {Object} data - The data representing the table.  Determined by data["Type"] = "Vertical Table"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawVTable(data, div) {
-    var html = "<h3>" + data["Title"] + "</h3>\n";
-    html += "<div class=\"table\">";
-    html += "<table>\n";
-    for (var idx in data["Headers"]) {
-        html += "<tr>\n";
-        // Draw the headers
-        html += "<th>" + data["Headers"][idx] + "</th>\n";
-        // Draw the cells
-        html += "<td>" + data["Data"][data["Headers"][idx]] + "</td>\n";
-        html += "</tr>\n";
-    }
-    html += "</table>\n </div>";
-    $(div).append(html);
-}
-
-/**
- * Build a table with horizontal and vertical headers and add it to the div.
- *
- * @param {Object} data - The data representing the summary.  Determined by data["Type"] = "V-H Table"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawVHTable(data, div) {
-    var html = "<h3>" + data["TableTitle"] + "</h3>";
-    html += "<table class=\"table\">\n";
-    // Add the headers
-    html += "<th></th>\n";
-    for (var header in data["Headers"]) {
-        html += "<th>" + data["Headers"][header] + "</th>\n";
-    }
-    // Add the data
-    var testNames = Object.keys(data["Data"]).sort();
-    for (var idx in testNames) {
-        testName = testNames[idx];
-        html += "<tr class=\"testName\"><td>" + testName + "</td></tr>\n";
-        for (var testCase in data["Data"][testName]) {
-            html_tmp1 = "<tr ";
-            html_tmp2 = ">\n<td>" + testCase + "</td>\n";
-            style = "";  
-            for (var headerIdx in data["Headers"]) {
-                header = data["Headers"][headerIdx];
-                html_tmp2 += "<td>"; 
-                value = data["Data"][testName][testCase][header];
-                dtype = typeof value;
-                if (dtype == 'string') {
-                    html_tmp2 += value;
-                } else if (dtype == 'number') {
-                    html_tmp2 += value.toFixed(3);
-                } else if (dtype == 'object') {
-                    html_tmp2 += " (";
-                    for (var v in data["Data"][testName][testCase][header]) {
-                        v_val = data["Data"][testName][testCase][header][v];
-                        vtype = typeof v_val;
-                        if (vtype == 'number') {
-                            html_tmp2 += v_val.toExponential(3);
-                        } else {
-                            html_tmp2 += v_val;
-                            html_tmp2 += vtype;
-                        }
-                        html_tmp2 += ", ";
-                    }
-                    html_tmp2 = html_tmp2.replace(/, $/, '');
-                    html_tmp2 += ")";
-                }
-                html_tmp2 += "</td>\n";
-            }
-            html += html_tmp1 + style + html_tmp2 + "</tr>\n";
-        }
-    }
-    html += "</table>\n";
-    $(div).append(html);
-}
-
-/**
- * Build a gallery
- *
- * @param {Object} data - The data representing the table.  Determined by data["Type"] = "Gallery"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawGallery(data, div) {
-    var html = "<div class=\"gallery\">";
-    html += "<h3>" + data["Title"] + "</h3>";
-    html += "</div>";
-
-    // Create the gallery div to put all the images into
-    $(div).append(html);
-    
-    for (var idx in data["Data"]) {
-        img_elem = data["Data"][idx];
-        // Draw an image 
-        drawImage(img_elem, div+" > div.gallery");
-    }
-    
-    $(div).append("<div style=\"clear:both\"></div>");
-}
-
-/**
- * Draw an image
- *
- * @param {Object} data - The data representing the image.  Determined by data["Type"] = "Image"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawImage(img_elem, div) {
-    var html = "<div>";
-    html += drawLightbox(img_elem);
-    html += "</div>"
-    $(div).append(html);
-}
-
-/**
- * Draw an image thumbnail with a link to open the image in a lightbox 
- *
- * @param {dictionary} img_elem  - Dictionary describing the image location, title, album, and caption
- * @param {number} size - The desired height to draw
- *
- * @return the html to embed into another element
- */
-function drawLightbox(img_elem) {
-    var img_dir = window.location.href.substr(0,window.location.href.lastIndexOf('/')+1) + "imgs/";
-    var path = img_dir + img_elem["Plot File"];
-    var lbox = img_elem["Group"] ? img_elem["Group"]  : img_elem["Title"];
-    var size = img_elem["Height"] ? img_elem["Height"]  : 200;
-    var html = "<a href=\"" + path + "\" data-lightbox=\"" + lbox + "\" data-title=\"" + img_elem["Description"] + "\">";
-    html += "<img class=\"thumbnail caption\" data-caption=\"" + img_elem["Title"]+ "\" alt=\"" + img_elem["Title"] + "\" src=\"" + path + "\" style=\"height: " + size + "px; overflow: hidden; position: relative\">";
-    html += "</a>";
-    return html;
-}
-
-/**
- * Draw an image thumbnail with a link to open in a new tab 
- *
- * @param {string} path - The location of the image to thumbnail-size 
- * @param {number} size - The desired height to draw
- *
- * @return the html to embed into another element
- */
-function drawThumbnail(img_elem, size) {
-    var img_dir = window.location.href.substr(0,window.location.href.lastIndexOf('/')+1) + "imgs/";
-    var path = img_elem["Plot File"];
-    var html = "<a target=\"_blank\" href=\"" + path + "\">";
-    html += "<img class=\"thumbnail\" alt=\"" + img_elem["Title"] + "\" src=\"" + path + "\" style=\"height: " + size + "px; overflow: hidden; position: relative\">";
-    html += "</a>";
-    return html;
-}
-
-/**
- * Draw HTML element
- *
- * @param {Object} data - The data representing the html element. Determined by data["Type"] = "HTML"
- * @param {string} div - The name of the div to draw to.  Should be referenced as a string that 
- *                       determines whether it is a class or id (ie include # or .)
- */
-function drawHTML (data, div) {
-    var html = "<div>";
-    html += data["Data"];
-    html += "</div>";
-    $(div).append(html);
+    $("#tabs").tabs();
 }
 
 /**
diff --git a/livvkit/resources/js/lightbox.min.map b/livvkit/resources/js/lightbox.min.map
new file mode 100644
index 0000000..1b12021
--- /dev/null
+++ b/livvkit/resources/js/lightbox.min.map
@@ -0,0 +1,1 @@
+{"version":3,"sources":["../../src/js/lightbox.js"],"names":["root","factory","define","amd","exports","module","require","lightbox","jQuery","this","$","Lightbox","options","album","currentImageIndex","init","extend","constructor","defaults","option","albumLabel","alwaysShowNavOnTouchDevices","fadeDuration","fitImagesInViewport","imageFadeDuration","positionFromTop","resizeDuration","showImageNumberLabel","wrapAround","disableScrolling","sanitizeTitle","prototype","imageCountLabel","currentImageNum","totalImages","replace","self","document","ready","enable","build","on","event","start","currentTarget","appendTo","$lightbox","$overlay","$outerContainer","find","$container","$image","$nav","containerPadding","top","parseInt","css","right","bottom","left","imageBorderWidth","hide","end","target","attr","changeImage","length","which","one","setTimeout","bind","$link","addToAlbum","push","link","title","$window","window","proxy","sizeOverlay","visibility","$links","imageNumber","dataLightboxValue","prop","i","j","scrollTop","scrollLeft","fadeIn","addClass","disableKeyboardNav","preloader","Image","onload","$preloader","imageHeight","imageWidth","maxImageHeight","maxImageWidth","windowHeight","windowWidth","width","height","maxWidth","maxHeight","sizeContainer","src","postResize","newWidth","newHeight","showImage","oldWidth","outerWidth","oldHeight","outerHeight","animate","stop","updateNav","updateDetails","preloadNeighboringImages","enableKeyboardNav","alwaysShowNav","createEvent","e","show","$caption","text","html","undefined","open","location","href","labelText","removeClass","preloadNext","preloadPrev","keyboardAction","off","KEYCODE_ESC","KEYCODE_LEFTARROW","KEYCODE_RIGHTARROW","keycode","keyCode","key","String","fromCharCode","toLowerCase","match","fadeOut"],"mappings":";;;;;;;;;;;CAaC,SAAUA,EAAMC,GACS,kBAAXC,SAAyBA,OAAOC,IAEvCD,QAAQ,UAAWD,GACO,gBAAZG,SAIdC,OAAOD,QAAUH,EAAQK,QAAQ,WAGjCN,EAAKO,SAAWN,EAAQD,EAAKQ,SAEnCC,KAAM,SAAUC,GAEhB,QAASC,GAASC,GAChBH,KAAKI,SACLJ,KAAKK,kBAAoB,OACzBL,KAAKM,OAGLN,KAAKG,QAAUF,EAAEM,UAAWP,KAAKQ,YAAYC,UAC7CT,KAAKU,OAAOP,GAudd,MAldAD,GAASO,UACPE,WAAY,iBACZC,6BAA6B,EAC7BC,aAAc,IACdC,qBAAqB,EACrBC,kBAAmB,IAGnBC,gBAAiB,GACjBC,eAAgB,IAChBC,sBAAsB,EACtBC,YAAY,EACZC,kBAAkB,EASlBC,eAAe,GAGjBnB,EAASoB,UAAUZ,OAAS,SAASP,GACnCF,EAAEM,OAAOP,KAAKG,QAASA,IAGzBD,EAASoB,UAAUC,gBAAkB,SAASC,EAAiBC,GAC7D,MAAOzB,MAAKG,QAAQQ,WAAWe,QAAQ,MAAOF,GAAiBE,QAAQ,MAAOD,IAGhFvB,EAASoB,UAAUhB,KAAO,WACxB,GAAIqB,GAAO3B,IAEXC,GAAE2B,UAAUC,MAAM,WAChBF,EAAKG,SACLH,EAAKI,WAMT7B,EAASoB,UAAUQ,OAAS,WAC1B,GAAIH,GAAO3B,IACXC,GAAE,QAAQ+B,GAAG,QAAS,+EAAgF,SAASC,GAE7G,MADAN,GAAKO,MAAMjC,EAAEgC,EAAME,iBACZ,KAMXjC,EAASoB,UAAUS,MAAQ,WACzB,GAAIJ,GAAO3B,IACXC,GAAE,qoBAAqoBmC,SAASnC,EAAE,SAGlpBD,KAAKqC,UAAkBpC,EAAE,aACzBD,KAAKsC,SAAkBrC,EAAE,oBACzBD,KAAKuC,gBAAkBvC,KAAKqC,UAAUG,KAAK,sBAC3CxC,KAAKyC,WAAkBzC,KAAKqC,UAAUG,KAAK,iBAC3CxC,KAAK0C,OAAkB1C,KAAKqC,UAAUG,KAAK,aAC3CxC,KAAK2C,KAAkB3C,KAAKqC,UAAUG,KAAK,WAG3CxC,KAAK4C,kBACHC,IAAKC,SAAS9C,KAAKyC,WAAWM,IAAI,eAAgB,IAClDC,MAAOF,SAAS9C,KAAKyC,WAAWM,IAAI,iBAAkB,IACtDE,OAAQH,SAAS9C,KAAKyC,WAAWM,IAAI,kBAAmB,IACxDG,KAAMJ,SAAS9C,KAAKyC,WAAWM,IAAI,gBAAiB,KAGtD/C,KAAKmD,kBACHN,IAAKC,SAAS9C,KAAK0C,OAAOK,IAAI,oBAAqB,IACnDC,MAAOF,SAAS9C,KAAK0C,OAAOK,IAAI,sBAAuB,IACvDE,OAAQH,SAAS9C,KAAK0C,OAAOK,IAAI,uBAAwB,IACzDG,KAAMJ,SAAS9C,KAAK0C,OAAOK,IAAI,qBAAsB,KAIvD/C,KAAKsC,SAASc,OAAOpB,GAAG,QAAS,WAE/B,MADAL,GAAK0B,OACE,IAGTrD,KAAKqC,UAAUe,OAAOpB,GAAG,QAAS,SAASC,GAIzC,MAHmC,aAA/BhC,EAAEgC,EAAMqB,QAAQC,KAAK,OACvB5B,EAAK0B,OAEA,IAGTrD,KAAKuC,gBAAgBP,GAAG,QAAS,SAASC,GAIxC,MAHmC,aAA/BhC,EAAEgC,EAAMqB,QAAQC,KAAK,OACvB5B,EAAK0B,OAEA,IAGTrD,KAAKqC,UAAUG,KAAK,YAAYR,GAAG,QAAS,WAM1C,MAL+B,KAA3BL,EAAKtB,kBACPsB,EAAK6B,YAAY7B,EAAKvB,MAAMqD,OAAS,GAErC9B,EAAK6B,YAAY7B,EAAKtB,kBAAoB,IAErC,IAGTL,KAAKqC,UAAUG,KAAK,YAAYR,GAAG,QAAS,WAM1C,MALIL,GAAKtB,oBAAsBsB,EAAKvB,MAAMqD,OAAS,EACjD9B,EAAK6B,YAAY,GAEjB7B,EAAK6B,YAAY7B,EAAKtB,kBAAoB,IAErC,IAgBTL,KAAK2C,KAAKX,GAAG,YAAa,SAASC,GACb,IAAhBA,EAAMyB,QACR/B,EAAKgB,KAAKI,IAAI,iBAAkB,QAEhCpB,EAAKU,UAAUsB,IAAI,cAAe,WAChCC,WAAW,WACP5D,KAAK2C,KAAKI,IAAI,iBAAkB,SAClCc,KAAKlC,GAAO,QAMpB3B,KAAKqC,UAAUG,KAAK,yBAAyBR,GAAG,QAAS,WAEvD,MADAL,GAAK0B,OACE,KAKXnD,EAASoB,UAAUY,MAAQ,SAAS4B,GAelC,QAASC,GAAWD,GAClBnC,EAAKvB,MAAM4D,MACTC,KAAMH,EAAMP,KAAK,QACjBW,MAAOJ,EAAMP,KAAK,eAAiBO,EAAMP,KAAK,WAjBlD,GAAI5B,GAAU3B,KACVmE,EAAUlE,EAAEmE,OAEhBD,GAAQnC,GAAG,SAAU/B,EAAEoE,MAAMrE,KAAKsE,YAAatE,OAE/CC,EAAE,yBAAyB8C,KACzBwB,WAAY,WAGdvE,KAAKsE,cAELtE,KAAKI,QACL,IAWIoE,GAXAC,EAAc,EAUdC,EAAoBZ,EAAMP,KAAK,gBAGnC,IAAImB,EAAmB,CACrBF,EAASvE,EAAE6D,EAAMa,KAAK,WAAa,mBAAqBD,EAAoB,KAC5E,KAAK,GAAIE,GAAI,EAAGA,EAAIJ,EAAOf,OAAQmB,IAAMA,EACvCb,EAAW9D,EAAEuE,EAAOI,KAChBJ,EAAOI,KAAOd,EAAM,KACtBW,EAAcG,OAIlB,IAA0B,aAAtBd,EAAMP,KAAK,OAEbQ,EAAWD,OACN,CAELU,EAASvE,EAAE6D,EAAMa,KAAK,WAAa,SAAWb,EAAMP,KAAK,OAAS,KAClE,KAAK,GAAIsB,GAAI,EAAGA,EAAIL,EAAOf,OAAQoB,IAAMA,EACvCd,EAAW9D,EAAEuE,EAAOK,KAChBL,EAAOK,KAAOf,EAAM,KACtBW,EAAcI,GAOtB,GAAIhC,GAAOsB,EAAQW,YAAc9E,KAAKG,QAAQa,gBAC1CkC,EAAOiB,EAAQY,YACnB/E,MAAKqC,UAAUU,KACbF,IAAKA,EAAM,KACXK,KAAMA,EAAO,OACZ8B,OAAOhF,KAAKG,QAAQU,cAGnBb,KAAKG,QAAQiB,kBACfnB,EAAE,QAAQgF,SAAS,wBAGrBjF,KAAKwD,YAAYiB,IAInBvE,EAASoB,UAAUkC,YAAc,SAASiB,GACxC,GAAI9C,GAAO3B,IAEXA,MAAKkF,oBACL,IAAIxC,GAAS1C,KAAKqC,UAAUG,KAAK,YAEjCxC,MAAKsC,SAAS0C,OAAOhF,KAAKG,QAAQU,cAElCZ,EAAE,cAAc+E,OAAO,QACvBhF,KAAKqC,UAAUG,KAAK,uFAAuFY,OAE3GpD,KAAKuC,gBAAgB0C,SAAS,YAG9B,IAAIE,GAAY,GAAIC,MACpBD,GAAUE,OAAS,WACjB,GAAIC,GACAC,EACAC,EACAC,EACAC,EACAC,EACAC,CAEJlD,GAAOa,KAAK,MAAO5B,EAAKvB,MAAMqE,GAAaR,MAE3CqB,EAAarF,EAAEkF,GAEfzC,EAAOmD,MAAMV,EAAUU,OACvBnD,EAAOoD,OAAOX,EAAUW,QAEpBnE,EAAKxB,QAAQW,sBAIf8E,EAAiB3F,EAAEmE,QAAQyB,QAC3BF,EAAiB1F,EAAEmE,QAAQ0B,SAC3BJ,EAAiBE,EAAcjE,EAAKiB,iBAAiBM,KAAOvB,EAAKiB,iBAAiBI,MAAQrB,EAAKwB,iBAAiBD,KAAOvB,EAAKwB,iBAAiBH,MAAQ,GACrJyC,EAAiBE,EAAehE,EAAKiB,iBAAiBC,IAAMlB,EAAKiB,iBAAiBK,OAAStB,EAAKwB,iBAAiBN,IAAMlB,EAAKwB,iBAAiBF,OAAS,IAGlJtB,EAAKxB,QAAQ4F,UAAYpE,EAAKxB,QAAQ4F,SAAWL,IACnDA,EAAgB/D,EAAKxB,QAAQ4F,UAE3BpE,EAAKxB,QAAQ6F,WAAarE,EAAKxB,QAAQ6F,UAAYN,IACrDD,EAAiB9D,EAAKxB,QAAQ6F,YAI3Bb,EAAUU,MAAQH,GAAmBP,EAAUW,OAASL,KACtDN,EAAUU,MAAQH,EAAkBP,EAAUW,OAASL,GAC1DD,EAAcE,EACdH,EAAczC,SAASqC,EAAUW,QAAUX,EAAUU,MAAQL,GAAa,IAC1E9C,EAAOmD,MAAML,GACb9C,EAAOoD,OAAOP,KAEdA,EAAcE,EACdD,EAAa1C,SAASqC,EAAUU,OAASV,EAAUW,OAASP,GAAc,IAC1E7C,EAAOmD,MAAML,GACb9C,EAAOoD,OAAOP,MAIpB5D,EAAKsE,cAAcvD,EAAOmD,QAASnD,EAAOoD,WAG5CX,EAAUe,IAAelG,KAAKI,MAAMqE,GAAaR,KACjDjE,KAAKK,kBAAoBoE,GAI3BvE,EAASoB,UAAUgD,YAAc,WAC/BtE,KAAKsC,SACFuD,MAAM5F,EAAE2B,UAAUiE,SAClBC,OAAO7F,EAAE2B,UAAUkE,WAIxB5F,EAASoB,UAAU2E,cAAgB,SAAST,EAAYD,GAQtD,QAASY,KACPxE,EAAKU,UAAUG,KAAK,qBAAqBqD,MAAMO,GAC/CzE,EAAKU,UAAUG,KAAK,gBAAgBsD,OAAOO,GAC3C1E,EAAKU,UAAUG,KAAK,gBAAgBsD,OAAOO,GAC3C1E,EAAK2E,YAXP,GAAI3E,GAAO3B,KAEPuG,EAAYvG,KAAKuC,gBAAgBiE,aACjCC,EAAYzG,KAAKuC,gBAAgBmE,cACjCN,EAAYZ,EAAaxF,KAAK4C,iBAAiBM,KAAOlD,KAAK4C,iBAAiBI,MAAQhD,KAAKmD,iBAAiBD,KAAOlD,KAAKmD,iBAAiBH,MACvIqD,EAAYd,EAAcvF,KAAK4C,iBAAiBC,IAAM7C,KAAK4C,iBAAiBK,OAASjD,KAAKmD,iBAAiBN,IAAM7C,KAAKmD,iBAAiBF,MASvIsD,KAAaH,GAAYK,IAAcJ,EACzCrG,KAAKuC,gBAAgBoE,SACnBd,MAAOO,EACPN,OAAQO,GACPrG,KAAKG,QAAQc,eAAgB,QAAS,WACvCkF,MAGFA,KAKJjG,EAASoB,UAAUgF,UAAY,WAC7BtG,KAAKqC,UAAUG,KAAK,cAAcoE,MAAK,GAAMxD,OAC7CpD,KAAKqC,UAAUG,KAAK,aAAawC,OAAOhF,KAAKG,QAAQY,mBAErDf,KAAK6G,YACL7G,KAAK8G,gBACL9G,KAAK+G,2BACL/G,KAAKgH,qBAIP9G,EAASoB,UAAUuF,UAAY,WAI7B,GAAII,IAAgB,CACpB,KACErF,SAASsF,YAAY,cACrBD,EAAiBjH,KAAKG,QAAmC,6BAAI,GAAO,EACpE,MAAOgH,IAETnH,KAAKqC,UAAUG,KAAK,WAAW4E,OAE3BpH,KAAKI,MAAMqD,OAAS,IAClBzD,KAAKG,QAAQgB,YACX8F,GACFjH,KAAKqC,UAAUG,KAAK,sBAAsBO,IAAI,UAAW,KAE3D/C,KAAKqC,UAAUG,KAAK,sBAAsB4E,SAEtCpH,KAAKK,kBAAoB,IAC3BL,KAAKqC,UAAUG,KAAK,YAAY4E,OAC5BH,GACFjH,KAAKqC,UAAUG,KAAK,YAAYO,IAAI,UAAW,MAG/C/C,KAAKK,kBAAoBL,KAAKI,MAAMqD,OAAS,IAC/CzD,KAAKqC,UAAUG,KAAK,YAAY4E,OAC5BH,GACFjH,KAAKqC,UAAUG,KAAK,YAAYO,IAAI,UAAW,SAQzD7C,EAASoB,UAAUwF,cAAgB,WACjC,GAAInF,GAAO3B,IAIX,IAAwD,mBAA7CA,MAAKI,MAAMJ,KAAKK,mBAAmB6D,OACC,KAA7ClE,KAAKI,MAAMJ,KAAKK,mBAAmB6D,MAAc,CACjD,GAAImD,GAAWrH,KAAKqC,UAAUG,KAAK,cAC/BxC,MAAKG,QAAQkB,cACfgG,EAASC,KAAKtH,KAAKI,MAAMJ,KAAKK,mBAAmB6D,OAEjDmD,EAASE,KAAKvH,KAAKI,MAAMJ,KAAKK,mBAAmB6D,OAEnDmD,EAASrC,OAAO,QACbxC,KAAK,KAAKR,GAAG,QAAS,SAASC,GACCuF,SAA3BvH,EAAED,MAAMuD,KAAK,UACfa,OAAOqD,KAAKxH,EAAED,MAAMuD,KAAK,QAAStD,EAAED,MAAMuD,KAAK,WAE/CmE,SAASC,KAAO1H,EAAED,MAAMuD,KAAK,UAKrC,GAAIvD,KAAKI,MAAMqD,OAAS,GAAKzD,KAAKG,QAAQe,qBAAsB,CAC9D,GAAI0G,GAAY5H,KAAKuB,gBAAgBvB,KAAKK,kBAAoB,EAAGL,KAAKI,MAAMqD,OAC5EzD,MAAKqC,UAAUG,KAAK,cAAc8E,KAAKM,GAAW5C,OAAO,YAEzDhF,MAAKqC,UAAUG,KAAK,cAAcY,MAGpCpD,MAAKuC,gBAAgBsF,YAAY,aAEjC7H,KAAKqC,UAAUG,KAAK,qBAAqBwC,OAAOhF,KAAKG,QAAQc,eAAgB,WAC3E,MAAOU,GAAK2C,iBAKhBpE,EAASoB,UAAUyF,yBAA2B,WAC5C,GAAI/G,KAAKI,MAAMqD,OAASzD,KAAKK,kBAAoB,EAAG,CAClD,GAAIyH,GAAc,GAAI1C,MACtB0C,GAAY5B,IAAMlG,KAAKI,MAAMJ,KAAKK,kBAAoB,GAAG4D,KAE3D,GAAIjE,KAAKK,kBAAoB,EAAG,CAC9B,GAAI0H,GAAc,GAAI3C,MACtB2C,GAAY7B,IAAMlG,KAAKI,MAAMJ,KAAKK,kBAAoB,GAAG4D,OAI7D/D,EAASoB,UAAU0F,kBAAoB,WACrC/G,EAAE2B,UAAUI,GAAG,iBAAkB/B,EAAEoE,MAAMrE,KAAKgI,eAAgBhI,QAGhEE,EAASoB,UAAU4D,mBAAqB,WACtCjF,EAAE2B,UAAUqG,IAAI,cAGlB/H,EAASoB,UAAU0G,eAAiB,SAAS/F,GAC3C,GAAIiG,GAAqB,GACrBC,EAAqB,GACrBC,EAAqB,GAErBC,EAAUpG,EAAMqG,QAChBC,EAAUC,OAAOC,aAAaJ,GAASK,aACvCL,KAAYH,GAAeK,EAAII,MAAM,SACvC3I,KAAKqD,MACY,MAARkF,GAAeF,IAAYF,EACL,IAA3BnI,KAAKK,kBACPL,KAAKwD,YAAYxD,KAAKK,kBAAoB,GACjCL,KAAKG,QAAQgB,YAAcnB,KAAKI,MAAMqD,OAAS,GACxDzD,KAAKwD,YAAYxD,KAAKI,MAAMqD,OAAS,IAEtB,MAAR8E,GAAeF,IAAYD,KAChCpI,KAAKK,oBAAsBL,KAAKI,MAAMqD,OAAS,EACjDzD,KAAKwD,YAAYxD,KAAKK,kBAAoB,GACjCL,KAAKG,QAAQgB,YAAcnB,KAAKI,MAAMqD,OAAS,GACxDzD,KAAKwD,YAAY,KAMvBtD,EAASoB,UAAU+B,IAAM,WACvBrD,KAAKkF,qBACLjF,EAAEmE,QAAQ6D,IAAI,SAAUjI,KAAKsE,aAC7BtE,KAAKqC,UAAUuG,QAAQ5I,KAAKG,QAAQU,cACpCb,KAAKsC,SAASsG,QAAQ5I,KAAKG,QAAQU,cACnCZ,EAAE,yBAAyB8C,KACzBwB,WAAY,YAEVvE,KAAKG,QAAQiB,kBACfnB,EAAE,QAAQ4H,YAAY,yBAInB,GAAI3H","file":"lightbox.min.js"}
\ No newline at end of file
diff --git a/livvkit/resources/numerics.html b/livvkit/resources/numerics.html
index e6a764b..dba5e6f 100644
--- a/livvkit/resources/numerics.html
+++ b/livvkit/resources/numerics.html
@@ -3,7 +3,9 @@
     <head>
         <meta charset="utf-8">
         <title>LIVVkit</title>
+        <link rel="shortcut icon" type="image/x-icon" href="../favicon.ico"/>
         <link href='http://fonts.googleapis.com/css?family=Lato' rel='stylesheet' type='text/css'>
+        <link href="../css/normalize.css" type="text/css" rel="stylesheet">
         <link href="../css/jquery-ui.min.css" type="text/css" rel="stylesheet">
         <link href="../css/captionjs.min.css" type="text/css" rel="stylesheet">
         <link href="../css/lightbox.min.css" type="text/css" rel="stylesheet">
diff --git a/livvkit/resources/performance.html b/livvkit/resources/performance.html
index 013de2f..954a072 100644
--- a/livvkit/resources/performance.html
+++ b/livvkit/resources/performance.html
@@ -3,7 +3,9 @@
     <head>
         <meta charset="utf-8">
         <title>LIVVkit</title>
+        <link rel="shortcut icon" type="image/x-icon" href="../favicon.ico"/>
         <link href='http://fonts.googleapis.com/css?family=Lato' rel='stylesheet' type='text/css'>
+        <link href="../css/normalize.css" type="text/css" rel="stylesheet">
         <link href="../css/jquery-ui.min.css" type="text/css" rel="stylesheet">
         <link href="../css/captionjs.min.css" type="text/css" rel="stylesheet">
         <link href="../css/lightbox.min.css" type="text/css" rel="stylesheet">
diff --git a/livvkit/resources/validation.html b/livvkit/resources/validation.html
index e091823..7664bc1 100644
--- a/livvkit/resources/validation.html
+++ b/livvkit/resources/validation.html
@@ -3,7 +3,9 @@
     <head>
         <meta charset="utf-8">
         <title>LIVVkit</title>
+        <link rel="shortcut icon" type="image/x-icon" href="../favicon.ico"/>
         <link href='http://fonts.googleapis.com/css?family=Lato' rel='stylesheet' type='text/css'>
+        <link href="../css/normalize.css" type="text/css" rel="stylesheet">
         <link href="../css/jquery-ui.min.css" type="text/css" rel="stylesheet">
         <link href="../css/captionjs.min.css" type="text/css" rel="stylesheet">
         <link href="../css/lightbox.min.css" type="text/css" rel="stylesheet">
diff --git a/livvkit/resources/verification.html b/livvkit/resources/verification.html
index ed41a56..db74377 100644
--- a/livvkit/resources/verification.html
+++ b/livvkit/resources/verification.html
@@ -3,7 +3,9 @@
     <head>
         <meta charset="utf-8">
         <title>LIVVkit</title>
+        <link rel="shortcut icon" type="image/x-icon" href="../favicon.ico"/>
         <link href='http://fonts.googleapis.com/css?family=Lato' rel='stylesheet' type='text/css'>
+        <link href="../css/normalize.css" type="text/css" rel="stylesheet">
         <link href="../css/jquery-ui.min.css" type="text/css" rel="stylesheet">
         <link href="../css/captionjs.min.css" type="text/css" rel="stylesheet">
         <link href="../css/lightbox.min.css" type="text/css" rel="stylesheet">
diff --git a/livvkit/scheduler.py b/livvkit/scheduler.py
index 224bf6e..22313ce 100644
--- a/livvkit/scheduler.py
+++ b/livvkit/scheduler.py
@@ -31,10 +31,32 @@
 Provides functions for scheduling the runs of tests.
 """
 
-from __future__ import absolute_import, division, print_function, unicode_literals
-import six
+import os
+import sys
+import multiprocessing as mp
 
-import multiprocessing
+import pandas as pd
+
+import livvkit
+from livvkit import elements
+
+
+def pool_worker(run_type, run_suite, test, config):
+    sys.stdout = open(
+        os.path.join(livvkit.index_dir, 'logs', '{}-{}.stdout'.format(run_type, test)),
+        'a',
+    )
+    sys.stderr = open(
+        os.path.join(livvkit.index_dir, 'logs', '{}-{}.stderr'.format(run_type, test)),
+        'a',
+    )
+
+    summary = run_suite(test, config)
+
+    sys.stdout.flush()
+    sys.stderr.flush()
+
+    return summary
 
 
 def run(run_type, module, config):
@@ -51,7 +73,7 @@ def run(run_type, module, config):
     print("   Beginning " + run_type.lower() + " test suite ")
     print(" -----------------------------------------------------------------")
     print("")
-    summary = run_quiet(module, config)
+    summary = run_quiet(run_type, module, config)
     print(" -----------------------------------------------------------------")
     print("   " + run_type.capitalize() + " test suite complete ")
     print(" -----------------------------------------------------------------")
@@ -59,32 +81,46 @@ def run(run_type, module, config):
     return summary
 
 
-def run_quiet(module, config, group=True):
-    tests = [t for t in six.iterkeys(config) if isinstance(config[t], dict)]
-    summary = launch_processes(tests, module, group=group, **config)
-    return summary
-
+def run_quiet(run_type, module, config, group=True):
+    tests = [t for t in config if isinstance(config[t], dict)]
+    if livvkit.pool_size == 0:
+        test_summaries = {}
+        for test in tests:
+            test_summaries[test] = module.run_suite(test, config[test])
+    else:
+        test_summaries = launch_processes(run_type, tests, module, config)
 
-def launch_processes(tests, run_module, group=True, **config):
-    """ Helper method to launch processes and sync output """
-    manager = multiprocessing.Manager()
-    test_summaries = manager.dict()
-    process_handles = [multiprocessing.Process(target=run_module.run_suite,
-                       args=(test, config[test], test_summaries)) for test in tests]
-    for p in process_handles:
-        p.start()
-    for p in process_handles:
-        p.join()
+        for t in tests:
+            with open(os.path.join(livvkit.index_dir, 'logs', '{}-{}.stdout'.format(run_type, t))) as log:
+                stdout = log.read()
+            print(stdout)
 
     if group:
-        summary = run_module.populate_metadata(tests[0], config[tests[0]])
-        summary["Data"] = dict(test_summaries)
-        return summary
+        meta = module.populate_metadata(tests[0], config[tests[0]])
+        df = pd.concat(
+            {k: pd.DataFrame.from_dict(v, orient='index') for k, v, in test_summaries.items()},
+            names=['case', 'scale']
+        ).reset_index()
+        summary = elements.Table(meta['Title'], df.set_index('case'))
     else:
-        test_summaries = dict(test_summaries)
         summary = []
-        for ii, test in enumerate(tests):
-            summary.append(run_module.populate_metadata(test, config[test]))
-            if summary[ii]:
-                summary[ii]['Data'] = {test: test_summaries[test]}
-        return summary
+        for ii, t in enumerate(tests):
+            meta = module.populate_metadata(t, config[t])
+            df = pd.DataFrame.from_dict(test_summaries[t], orient='index')
+            summary.append(elements.Table(meta['title'], df))
+
+    return summary
+
+
+def launch_processes(run_type, tests, run_module, config):
+    """ Helper method to launch processes and sync output """
+    test_summaries = {}
+    with mp.Pool(livvkit.pool_size) as pool:
+        results = [
+            pool.apply_async(pool_worker, (run_type, run_module.run_suite, t, config[t])) for t in tests
+        ]
+
+        for t, r in zip(tests, results):
+            test_summaries[t] = r.get()
+
+    return test_summaries
diff --git a/livvkit/util/LIVVDict.py b/livvkit/util/LIVVDict.py
index 1e149dd..4ce18e2 100644
--- a/livvkit/util/LIVVDict.py
+++ b/livvkit/util/LIVVDict.py
@@ -27,8 +27,6 @@
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-from __future__ import absolute_import, division, print_function, unicode_literals
-
 
 class LIVVDict(dict):
     """
diff --git a/livvkit/util/TexHelper.py b/livvkit/util/TexHelper.py
deleted file mode 100644
index c4a9880..0000000
--- a/livvkit/util/TexHelper.py
+++ /dev/null
@@ -1,252 +0,0 @@
-# coding=utf-8
-# Copyright (c) 2015-2018, UT-BATTELLE, LLC
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# 1. Redistributions of source code must retain the above copyright notice, this
-# list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above copyright notice,
-# this list of conditions and the following disclaimer in the documentation
-# and/or other materials provided with the distribution.
-#
-# 3. Neither the name of the copyright holder nor the names of its contributors
-# may be used to endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-The TexHelper module provides capabilities to convert LIVVkits output
-from JSON to tex files.  It does not convert the tex files to any finished
-output.   For example, if you want a PDF you will still have to pass the
-tex files through pdflatex.
-"""
-from __future__ import absolute_import, division, print_function, unicode_literals
-import six
-
-import os
-import glob
-
-import livvkit
-from livvkit.util import functions
-
-
-def write_tex():
-    """
-    Finds all of the output data files, and writes them out to .tex
-    """
-    datadir = livvkit.index_dir
-    outdir = os.path.join(datadir, "tex")
-    print(outdir)
-    # functions.mkdir_p(outdir)
-
-    data_files = glob.glob(datadir + "/**/*.json", recursive=True)
-
-    for each in data_files:
-        data = functions.read_json(each)
-        tex = translate_page(data)
-        outfile = os.path.join(outdir, os.path.basename(each).replace('json', 'tex'))
-        with open(outfile, 'w') as f:
-            f.write(tex)
-
-
-def translate_page(data):
-    """
-    Translates data elements with data['Type'] = 'page'.  This is the
-    top level of translation that occurs, and delegates the translation
-    of other element types contained on a page to their proper functions.
-    """
-    if "Page" != data["Type"]:
-        return ""
-
-    tex_str = ('\\documentclass{article}\\n' +
-               '\\usepackage{placeins}\\n' +
-               '\\title{LIVVkit}\\n' +
-               '\\author{$USER}\\n' +
-               '\\usepackage[parfill]{parskip}\\n' +
-               '\\begin{document}\\n' +
-               '\\maketitle\\n'
-               ).replace('$USER', livvkit.user)
-
-    content = data["Data"]
-    for tag_name in ["Elements", "Tabs"]:
-        for tag in content.get(tag_name, []):
-                print("Translating " + tag["Type"])
-                tex_str += translate_map[tag["Type"]](tag)
-
-    tex_str += '\n\\end{document}'
-    return tex_str
-
-
-def translate_section(data):
-    """ Translates data where data["Type"]=="Section" """
-    sect_str = ""
-    elements = data.get("Elements", [])
-    for elem in elements:
-            print("    Translating " + elem["Type"])
-            sect_str += translate_map[elem["Type"]](elem)
-    return sect_str
-
-
-def translate_tab(data):
-    """ Translates data where data["Type"]=="Tab" """
-    tab_str = ""
-    sections = data.get("Sections", [])
-    for section in sections:
-            print("  Translating " + section["Type"])
-            tab_str += translate_map[section["Type"]](section)
-    return tab_str
-
-
-def translate_summary(data):
-    """ Translates data where data["Type"]=="Summary" """
-    headers = sorted(data.get("Headers", []))
-    summary = '\\FloatBarrier \n \\section{$NAME} \n'.replace('$NAME', data.get("Title", "table"))
-    summary += ' \\begin{table}[!ht] \n \\begin{center}'
-
-    # Set the number of columns
-    n_cols = len(headers)
-    col_str = "l" + "c" * n_cols
-    summary += '\n \\begin{tabular}{$NCOLS} \n'.replace("$NCOLS", col_str)
-    spacer = ' &' * n_cols + r'\\[.5em]'
-
-    for header in headers:
-        summary += '& $HEADER '.replace('$HEADER', header).replace('%', '\%')
-    summary += ' \\\\ \hline \n'
-
-    names = sorted(six.iterkeys(data.get("Data", [])))
-    for name in names:
-        summary += '\n\n \\textbf{{{}}} {} \n'.format(name, spacer)
-        cases = data.get("Data", []).get(name, {})
-        for case, c_data in cases.items():
-            summary += ' $CASE & '.replace('$CASE', str(case))
-            for header in headers:
-                h_data = c_data.get(header, "")
-                if list is type(h_data) and len(h_data) == 2:
-                    summary += (' $H_DATA_0 of $H_DATA_1 &'
-                                .replace('$H_DATA_0', str(h_data[0]))
-                                .replace('$H_DATA_1', str(h_data[1]))
-                                .replace('%', '\%'))
-                else:
-                    summary += ' $H_DATA &'.replace('$H_DATA', str(h_data)).replace('%', '\%')
-
-            # This takes care of the trailing & that comes from processing the headers.
-            summary = summary[:-1] + r' \\'
-
-    summary += '\n \end{tabular} \n \end{center} \n \end{table}\n'
-    return summary
-
-
-def translate_table(data):
-    """ Translates data where data["Type"]=="Table" """
-    headers = sorted(data.get("Headers", []))
-    table = '\\FloatBarrier \n \\section{$NAME} \n'.replace('$NAME', data.get("Title", "table"))
-    table += '\\begin{table}[!ht] \n \\begin{center}'
-
-    # Set the number of columns
-    n_cols = "c"*(len(headers)+1)
-    table += '\n \\begin{tabular}{$NCOLS} \n'.replace("$NCOLS", n_cols)
-
-    # Put in the headers
-    for header in headers:
-        table += ' $HEADER &'.replace('$HEADER', header).replace('%', '\%')
-    table = table[:-1] + ' \\\\ \n \hline \n'
-
-    # Put in the data
-    for header in headers:
-        table += ' $VAL &'.replace("$VAL", str(data["Data"][header]))
-    table = table[:-1] + ' \\\\ \n \hline'
-    table += '\n \end{tabular} \n \end{center} \n \end{table}\n'
-    return table
-
-
-def translate_bit_for_bit(data):
-    """ Translates data where data["Type"]=="Bit for Bit" """
-    headers = sorted(data.get("Headers", []))
-    table = '\\FloatBarrier \n \\section{$NAME} \n'.replace('$NAME', data.get("Title", "table"))
-    table += '\\begin{table}[!ht] \n \\begin{center}'
-    # Set the number of columns
-    n_cols = "c"*(len(headers)+1)
-    table += '\n \\begin{tabular}{$NCOLS} \n'.replace("$NCOLS", n_cols)
-
-    # Put in the headers
-    table += " Variable &"
-    for header in headers:
-        table += ' $HEADER &'.replace('$HEADER', header).replace('%', '\%')
-    table = table[:-1] + ' \\\\ \n \hline \n'
-
-    # Put in the data
-    for k, v in data.get("Data", []).items():
-        table += "\n \\textbf{$VAR} & ".replace("$VAR", k)
-        for header in headers:
-            table += ' $VAL &'.replace("$VAL", str(v[header]))
-        table = table[:-1] + ' \\\\'
-    table += '\n \hline \n \end{tabular} \n \end{center} \n \end{table}\n'
-    return table
-
-
-def translate_gallery(data):
-    """ Translates data where data["Type"]=="Gallery" """
-    return ""
-
-
-def translate_image(data):
-    """ Translates data where data["Type"]=="Image" """
-    return ""
-
-
-def translate_file_diff(data):
-    """ Translates data where data["Type"]=="Diff" """
-    diff = '\\FloatBarrier \section{Configuration}'
-    sections = data.get('Data')
-    for title, config in sections.items():
-        title = title.replace('_', '\_')
-        diff += ' \n \\subsection{$NAME}'.replace('$NAME', title)
-        for opt, vals in config.items():
-            opt = opt.replace('_', '\_')
-            diff += '\n\n \\texttt{$NAME} : '.replace('$NAME', opt)
-            if vals[0]:
-                diff += '$NAME'.replace('$NAME', vals[-1])
-            else:
-                diff += ('{} \\textit{{{}}}'.format(vals[1], vals[-1]))
-    diff += '\n\n'
-    return diff
-
-
-def translate_error(data):
-    """ Translates data where data["Type"]=="Error" """
-    return ""
-
-
-# Map element types to translations
-translate_map = {
-           "Summary": translate_summary,
-           "Section": translate_section,
-           "Tab": translate_tab,
-           "Table": translate_table,
-           "Bit for Bit": translate_bit_for_bit,
-           "Gallery": translate_gallery,
-           "Image": translate_image,
-           "Diff": translate_file_diff,
-           "Error": translate_error
-       }
-
-
-def main():
-    write_tex()
-
-
-if __name__ == "__main__":
-    main()
diff --git a/livvkit/util/bib.py b/livvkit/util/bib.py
new file mode 100644
index 0000000..d686c3d
--- /dev/null
+++ b/livvkit/util/bib.py
@@ -0,0 +1,155 @@
+# coding=utf-8
+# Copyright (c) 2015-2018, UT-BATTELLE, LLC
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are met:
+#
+# 1. Redistributions of source code must retain the above copyright notice, this
+# list of conditions and the following disclaimer.
+#
+# 2. Redistributions in binary form must reproduce the above copyright notice,
+# this list of conditions and the following disclaimer in the documentation
+# and/or other materials provided with the distribution.
+#
+# 3. Neither the name of the copyright holder nor the names of its contributors
+# may be used to endorse or promote products derived from this software without
+# specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+import io
+from pathlib import Path
+
+import pybtex.database
+import pybtex.io
+
+from pybtex.backends.latex import Backend as LatexBaseBackend
+from pybtex.backends.html import Backend as HTMLBaseBackend
+from pybtex.style.formatting.plain import Style as PlainStyle
+
+
+# FIXME: This is hacky! Here we're returning actual latex as if it's html so we
+#        can coopt the bib2html function. Either need to make a generic renderbib
+#        function or a bib2latex function...
+class LatexBackend(LatexBaseBackend):
+    def __init__(self, *args, **kwargs):
+        super(LatexBackend, self).__init__(*args, **kwargs)
+
+    def _repr_html(self, formatted_bibliography):
+        stream = io.StringIO()
+        self.write_to_stream(formatted_bibliography, stream)
+        return stream.getvalue()
+
+
+class HTMLBackend(HTMLBaseBackend):
+    def __init__(self, *args, **kwargs):
+        super(HTMLBackend, self).__init__(*args, **kwargs)
+        self._html = ''
+
+    def output(self, html):
+        self._html += html
+
+    def format_protected(self, text):
+        if text[:4] == 'http':
+            return self.format_href(text, text)
+        else:
+            return r'<span class="bibtex-protected">{}</span>'.format(text)
+
+    def write_prologue(self):
+        self.output('<div class="bibliography"><h2>References</h2>'
+                    '<p>LIVVkit is an open source project licensed under a BSD 3-clause License. '
+                    'We ask that you please acknowledge LIVVkit in any work it is used or supports. '
+                    'In any corresponding published work, please cite: </p><dl>')
+
+    def write_epilogue(self):
+        self.output('</dl></div>')
+
+    def _repr_html(self, formatted_bibliography):
+        self.write_prologue()
+        for entry in formatted_bibliography:
+            self.write_entry(entry.key, entry.label, entry.text.render(self))
+        self.write_epilogue()
+
+        return self._html.replace('\n', ' ').replace('\\url <a', '<a')
+
+
+# FIXME: For python 3.7+ only...
+# from functools import singledispatch
+# from collections.abc import Iterable
+# from typing import Union
+#
+# @singledispatch
+# def bib2html(bib, style=None, backend=None):
+#     raise NotImplementedError('I do not now how to convert a {} type to a bibliography'.format(type(bib)))
+def bib2html(bib, style=None, backend=None):
+    if isinstance(bib, (str, Path)):
+        return _bib2html_string(bib, style=style, backend=backend)
+    if isinstance(bib, (list, set, tuple)):
+        return _bib2html_list(bib, style=style, backend=backend)
+    if isinstance(bib, pybtex.database.BibliographyData):
+        return _bib2html_bibdata(bib, style=style, backend=backend)
+    else:
+        raise NotImplementedError('I do not now how to convert a {} type to a bibliography'.format(type(bib)))
+
+
+# FIXME: For python 3.7+ only...
+# @bib2html.register
+# def _bib2html_string(bib: Union[str, Path], style=None, backend=None):
+def _bib2html_string(bib, style=None, backend=None):
+    if style is None:
+        style = PlainStyle()
+    if backend is None:
+        backend = HTMLBackend()
+
+    formatted_bib = style.format_bibliography(pybtex.database.parse_file(bib))
+
+    return backend._repr_html(formatted_bib)
+
+
+# FIXME: For python 3.7+ only...
+# @bib2html.register
+# def _bib2html_list(bib: Iterable, style=None, backend=None):
+def _bib2html_list(bib, style=None, backend=None):
+    if style is None:
+        style = PlainStyle()
+    if backend is None:
+        backend = HTMLBackend()
+
+    bibliography = pybtex.database.BibliographyData()
+    for bib_file in bib:
+        temp_bib = pybtex.database.parse_file(bib_file)
+        for key, entry in temp_bib.entries.items():
+            try:
+                bibliography.add_entry(key, entry)
+            except pybtex.database.BibliographyDataError:
+                # FIXME: should log this...
+                # Skip duplicate entries
+                continue
+
+    formatted_bib = style.format_bibliography(bibliography)
+
+    return backend._repr_html(formatted_bib)
+
+
+# FIXME: For python 3.7+ only...
+# @bib2html.register
+# def _bib2html_bibdata(bib: pybtex.database.BibliographyData, style=None, backend=None):
+def _bib2html_bibdata(bib, style=None, backend=None):
+    if style is None:
+        style = PlainStyle()
+    if backend is None:
+        backend = HTMLBackend()
+
+    formatted_bib = style.format_bibliography(bib)
+
+    return backend._repr_html(formatted_bib)
diff --git a/livvkit/util/elements.py b/livvkit/util/elements.py
deleted file mode 100644
index 95b817b..0000000
--- a/livvkit/util/elements.py
+++ /dev/null
@@ -1,356 +0,0 @@
-# coding=utf-8
-# Copyright (c) 2015-2018, UT-BATTELLE, LLC
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# 1. Redistributions of source code must retain the above copyright notice, this
-# list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above copyright notice,
-# this list of conditions and the following disclaimer in the documentation
-# and/or other materials provided with the distribution.
-#
-# 3. Neither the name of the copyright holder nor the names of its contributors
-# may be used to endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-"""
-Module to help building new display elements in the output
-files easier and less error prone.
-
-Implementing new elements is possible simply by adding new functions
-They will be written out to the JSON files as sub-objects, which must
-be interpreted by the Javascript found in the resources directory.
-"""
-
-from __future__ import absolute_import, division, print_function, unicode_literals
-
-
-def book(title, description, page_dict=None):
-
-    _book = {'Type': 'Book',
-             'Title': title,
-             'Description': description,
-             }
-    if page_dict is not None:
-        _book['Data'] = page_dict
-
-    return _book
-
-
-def page(title, description, element_list=None, tab_list=None):
-    """
-    Returns a dictionary representing a new page to display elements.
-    This can be thought of as a simple container for displaying multiple
-    types of information. The ``section`` method can be used to create
-    separate tabs.
-
-    Args:
-        title: The title to display
-        description: A description of the section
-        element_list: The list of elements to display. If a single element is
-                      given it will be wrapped in a list.
-        tab_list: A list of tabs to display.
-
-    Returns:
-        A dictionary with metadata specifying that it is to be rendered
-        as a page containing multiple elements and/or tabs.
-    """
-    _page = {
-             'Type': 'Page',
-             'Title': title,
-             'Description': description,
-             'Data': {},
-             }
-
-    if element_list is not None:
-        if isinstance(element_list, list):
-            _page['Data']['Elements'] = element_list
-        else:
-            _page['Data']['Elements'] = [element_list]
-    if tab_list is not None:
-        if isinstance(tab_list, list):
-            _page['Data']['Tabs'] = tab_list
-        else:
-            _page['Data']['Tabs'] = [tab_list]
-    return _page
-
-
-def tab(tab_name, element_list=None, section_list=None):
-    """
-    Returns a dictionary representing a new tab to display elements.
-    This can be thought of as a simple container for displaying multiple
-    types of information.
-
-    Args:
-        tab_name: The title to display
-        element_list: The list of elements to display. If a single element is
-                      given it will be wrapped in a list.
-        section_list: A list of sections to display.
-
-    Returns:
-        A dictionary with metadata specifying that it is to be rendered
-        as a page containing multiple elements and/or tab.
-    """
-    _tab = {
-            'Type': 'Tab',
-            'Title': tab_name,
-            }
-
-    if element_list is not None:
-        if isinstance(element_list, list):
-            _tab['Elements'] = element_list
-        else:
-            _tab['Elements'] = [element_list]
-    if section_list is not None:
-        if isinstance(section_list, list):
-            _tab['Sections'] = section_list
-        else:
-            if 'Elements' not in section_list:
-                _tab['Elements'] = element_list
-            else:
-                _tab['Elements'].append(element_list)
-    return _tab
-
-
-def section(title, element_list):
-    """
-    Returns a dictionary representing a new section.  Sections
-    contain a list of elements that are displayed separately from
-    the global elements on the page.
-
-    Args:
-        title: The title of the section to be displayed
-        element_list: The list of elements to display within the section
-
-    Returns:
-        A dictionary with metadata specifying that it is to be rendered as
-        a section containing multiple elements
-    """
-    sect = {
-            'Type': 'Section',
-            'Title': title,
-            }
-
-    if isinstance(element_list, list):
-        sect['Elements'] = element_list
-    else:
-        sect['Elements'] = [element_list]
-    return sect
-
-
-def table(title, headers, data_node):
-    """
-    Returns a dictionary representing a new table element.  Tables
-    are specified with two main pieces of information, the headers
-    and the data to put into the table.  Rendering of the table is
-    the responsibility of the Javascript in the resources directory.
-    When the data does not line up with the headers given this should
-    be handled within the Javascript itself, not here.
-
-    Args:
-        title: The title to display
-        headers: The columns to put into the table
-        data_node: A dictionary with the form::
-            {'case' : {'subcase' : { 'header' : data } } }
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered as a table.
-    """
-    tb = {
-          'Type': 'Table',
-          'Title': title,
-          'Headers': headers,
-          'Data': data_node,
-          }
-    return tb
-
-
-def vtable(title, headers, data_node):
-    """
-    Returns a dictionary representing a new table element.  Tables
-    are specified with two main pieces of information, the headers
-    and the data to put into the table.  Rendering of the table is
-    the responsibility of the Javascript in the resources directory.
-    When the data does not line up with the headers given this should
-    be handled within the Javascript itself, not here.
-
-    Args:
-        title: The title to display
-        headers: The columns to put into the table
-        data_node: A dictionary with the form::
-            {'case' : {'subcase' : { 'header' : data } } }
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered as a table.
-    """
-    tb = {
-          'Type': 'Vertical Table',
-          'Title': title,
-          'Headers': headers,
-          'Data': data_node,
-          }
-    return tb
-
-
-def bit_for_bit(title, headers, data_node):
-    """
-    Returns a dictionary representing a new bit for bit table element.
-    Bit for bit elements can be thought of as tables, but require the
-    special addition that a diff plot may need to be embedded within
-    the table.
-
-    Args:
-        title: The title to display
-        headers: Columns of the table
-        data_node: A dictionary with the form:
-            {'var_name' : {'header : { data } } }
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered as a bit for bit table
-    """
-    b4b = {
-           'Type': 'Bit for Bit',
-           'Title': title,
-           'Headers': headers,
-           'Data': data_node,
-           }
-    return b4b
-
-
-def gallery(title, image_elem_list):
-    """
-    Builds an image gallery out of a list of image elements. The
-    gallery element is provided as a way of grouping images under
-    a single heading and conserving space on the output page.
-
-    Args:
-        title: The title to display
-        image_elem_list: The image elements to display.  If a single
-            image element is given it will automatically be wrapped into
-            a list.
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered as an image gallery
-    """
-    gal = {
-           'Type': 'Gallery',
-           'Title': title,
-           'Data': image_elem_list,
-           }
-    return gal
-
-
-def image(title, desc, image_name, group=None, height=None):
-    """
-    Builds an image element.  Image elements are primarily created
-    and then wrapped into an image gallery element.  This is not required
-    behavior, however and it's independent usage should be allowed depending
-    on the behavior required.
-
-    The Javascript will search for the `image_name` in the component's
-    `imgs` directory when rendering.  For example, all verification images
-    are output to `vv_xxxx-xx-xx/verification/imgs` and then the verification
-    case's output page will search for `image_name` within that directory.
-
-    Args:
-        title: The title to display
-        desc: A description of the image or plot
-        image_name: The filename of the image
-        group: (optional) Title of lightbox group to join
-        height: (optional) Height of image thumbnail to draw
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered as an image element
-    """
-    ie = {
-          'Type': 'Image',
-          'Title': title,
-          'Description': desc,
-          'Plot File': image_name,
-          }
-    if group:
-        ie['Group'] = group
-    if height:
-        ie['Height'] = height
-    return ie
-
-
-def file_diff(title, diff_data):
-    """
-    Builds a file diff element.  This element can be used to check whether
-    configuration files have changed or other similar checks.  Differences
-    will be highlighted when rendered via Javascript.
-
-    Args:
-        title: The title to display
-        diff_data: A dictionary of the form:
-            { 'section_name' : { 'variable_name' : [diff, val_1, val_2] } }
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered as a file diff element
-    """
-    fd = {
-          'Type': 'Diff',
-          'Title': title,
-          'Data': diff_data,
-          }
-    return fd
-
-
-def error(title, error_msg):
-    """
-    Builds an error element.  Provides a way to show errors or other
-    anomalous behavior in the web output.
-
-    Args:
-        title: The title to display
-        error_msg: A description of the error or other helpful message
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered as an error element
-    """
-    err = {
-           'Type': 'Error',
-           'Title': title,
-           'Message': error_msg,
-           }
-    return err
-
-
-def html(html_data):
-    """
-    Builds a raw HTML element.  Provides a way to directly display some HTML.
-
-    Args:
-        html_data: The HTML to display 
-
-    Returns:
-        A dictionary with the metadata specifying that it is to be
-        rendered directly as HTML
-    """
-    html_el = {
-               'Type': 'HTML',
-               'Data': html_data,
-               }
-    return html_el
diff --git a/livvkit/util/functions.py b/livvkit/util/functions.py
index 6abac80..d30fd62 100644
--- a/livvkit/util/functions.py
+++ b/livvkit/util/functions.py
@@ -29,7 +29,6 @@
 """
 Module to hold LIVVkit specific functions
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
 
 import os
 import sys
@@ -43,7 +42,8 @@
 import livvkit
 
 
-class temp_sys_path(object):
+class TempSysPath(object):
+    """Add a path to the PYTHONPATH temporarily"""
     def __init__(self, path):
         self.path = path
 
@@ -110,10 +110,10 @@ def find_file(search_dir, file_pattern):
     Returns:
         The path to the file if it was found, otherwise an empty string
     """
-    for root, dirnames, fnames in os.walk(search_dir):
-            for fname in fnames:
-                if fnmatch.fnmatch(fname, file_pattern):
-                    return os.path.join(root, fname)
+    for root, _, fnames in os.walk(search_dir):
+        for fname in fnames:
+            if fnmatch.fnmatch(fname, file_pattern):
+                return os.path.join(root, fname)
     return ""
 
 
@@ -220,7 +220,22 @@ def setup_output(cssd=None, jsd=None, imgd=None):
     # Get the index template from the resource directory
     shutil.copy(os.path.join(livvkit.resource_dir, "index.html"),
                 os.path.join(livvkit.index_dir, "index.html"))
+
+    shutil.copy(os.path.join(livvkit.resource_dir, "favicon.ico"),
+                os.path.join(livvkit.index_dir, "favicon.ico"))
+
     # Record when this data was recorded so we can make nice backups
     with open(os.path.join(livvkit.index_dir, "data.txt"), "w") as f:
-        f.write(livvkit.timestamp + "\n")
-        f.write(livvkit.comment)
+        f.write(livvkit.timestamp + '\n')
+        f.write('Call: livv ')
+        for arg in sys.argv[1:]:
+            f.write(arg)
+            f.write(' ')
+        f.write('\n')
+        f.write('Version: ' + livvkit.__version__ + '\n')
+        f.write("User: " + livvkit.user + '\n')
+        f.write("OS Type: " + livvkit.os_type + '\n')
+        f.write("Machine: " + livvkit.machine + '\n')
+
+    # Make a directory to keep log files
+    mkdir_p(os.path.join(livvkit.index_dir, 'logs'))
diff --git a/livvkit/util/make_tex.py b/livvkit/util/make_tex.py
deleted file mode 100644
index 76c2d7f..0000000
--- a/livvkit/util/make_tex.py
+++ /dev/null
@@ -1,60 +0,0 @@
-#!/usr/bin/env python3
-# coding=utf-8
-# Copyright (c) 2015-2018, UT-BATTELLE, LLC
-# All rights reserved.
-#
-# Redistribution and use in source and binary forms, with or without
-# modification, are permitted provided that the following conditions are met:
-#
-# 1. Redistributions of source code must retain the above copyright notice, this
-# list of conditions and the following disclaimer.
-#
-# 2. Redistributions in binary form must reproduce the above copyright notice,
-# this list of conditions and the following disclaimer in the documentation
-# and/or other materials provided with the distribution.
-#
-# 3. Neither the name of the copyright holder nor the names of its contributors
-# may be used to endorse or promote products derived from this software without
-# specific prior written permission.
-#
-# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
-# ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
-# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
-# DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
-# FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
-# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
-# SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
-# CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
-# OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
-# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
-
-from __future__ import absolute_import, division, print_function, unicode_literals
-
-
-import os
-import sys
-import glob
-
-from livvkit.util import TexHelper as th
-from livvkit.util import functions
-
-
-def main():
-    datadir = sys.argv[1]
-    outdir = sys.argv[2]
-    functions.mkdir_p(outdir)
-
-    data_files = glob.glob(datadir + "/**/*.json", recursive=True)
-    data_files = [datadir + '/verification/dome.json']
-    # data_files = [datadir + '/index.json']
-
-    for each in data_files:
-        data = functions.read_json(each)
-        tex = th.translate_page(data)
-        outfile = os.path.join(outdir, os.path.basename(each).replace('json', 'tex'))
-        with open(outfile, 'w') as f:
-            f.write(tex)
-
-
-if __name__ == '__main__':
-    main()
diff --git a/livvkit/util/options.py b/livvkit/util/options.py
index 4150901..551d900 100644
--- a/livvkit/util/options.py
+++ b/livvkit/util/options.py
@@ -27,19 +27,31 @@
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-from __future__ import absolute_import, division, print_function, unicode_literals
-
 import os
 import sys
 import time
 import pkgutil
 import argparse
 import importlib
+import multiprocessing as mp
 
 import livvkit
 from livvkit import bundles
 
 
+def positive_int(integer):
+    """
+    Argparse helper function to specify a zero or positive integer as an
+    argument type
+    :param integer: A zero or positive integer
+    :return: integer
+    """
+    integer = int(integer)
+    if integer < 0:
+        raise argparse.ArgumentTypeError('Must be zero or a positive integer')
+    return integer
+
+
 def parse_args(args=None):
     """
     Handles the parsing of options for LIVVkit's command line interface
@@ -47,30 +59,28 @@ def parse_args(args=None):
     Args:
         args: The list of arguments, typically sys.argv[1:]
     """
-    parser = argparse.ArgumentParser(description="Main script to run LIVVkit.",
+    parser = argparse.ArgumentParser(description='Main script to run LIVVkit.',
                                      formatter_class=argparse.ArgumentDefaultsHelpFormatter,
                                      fromfile_prefix_chars='@')
 
     parser.add_argument('-o', '--out-dir',
-                        default=os.path.join(os.getcwd(), "vv_" + time.strftime("%Y-%m-%d")),
+                        default=os.path.join(os.getcwd(), 'vv_' + time.strftime('%Y-%m-%d')),
                         help='Location to output the LIVVkit webpages.'
                         )
 
     parser.add_argument('-v', '--verify',
                         nargs=2,
                         default=None,
-                        help=' '.join(['Specify the locations of the test and bench bundle to',
-                                       'compare (respectively).'
-                                       ])
+                        help='Specify the locations of the test and bench bundle '
+                             'to compare (respectively).'
                         )
 
     parser.add_argument('-V', '--validate',
                         action='store',
                         nargs='+',
                         default=None,
-                        help=' '.join(['Specify the location of the configuration files for',
-                                       'validation tests.'
-                                       ])
+                        help='Specify the location of the configuration files '
+                             'for validation tests.'
                         )
 
     # FIXME: this just short-circuits to the validation option, and should become its own module
@@ -80,25 +90,26 @@ def parse_args(args=None):
                         default=None,
                         dest='validate',
                         metavar='EXTENSION',
-                        help=' '.join(['Specify the location of the configuration files for',
-                                       'LIVVkit extensions.'
-                                       ])
-                        )
-
-    parser.add_argument('-p', '--publish',
-                        action='store_true',
-                        help=' '.join(['Also produce a publication quality copy of the figure in',
-                                       'the output directory (eps, 600d pi).'
-                                       ])
+                        help='Specify the location of the configuration files '
+                             'for LIVVkit extensions.'
                         )
 
     parser.add_argument('-s', '--serve',
-                        nargs='?', type=int, const=8000,
-                        help=' '.join(['Start a simple HTTP server for the output website specified',
-                                       'by OUT_DIR on port SERVE.'
-                                       ])
+                        nargs='?',
+                        type=int,
+                        const=8000,
+                        help='Start a simple HTTP server for the output website '
+                             'specified by OUT_DIR on port SERVE.'
                         )
 
+    parser.add_argument('-p', '--pool-size',
+                        nargs='?',
+                        type=int,
+                        default=(mp.cpu_count() - 1 or 1),
+                        help='The number of multiprocessing processes to run '
+                             'analyses in. If zero, processes will run serially '
+                             'outside of the multiprocessing module.')
+
     parser.add_argument('--version',
                         action='version',
                         version='LIVVkit {}'.format(livvkit.__version__),
@@ -119,7 +130,7 @@ def init(options):
     livvkit.index_dir = livvkit.output_dir
     livvkit.verify = True if options.verify is not None else False
     livvkit.validate = True if options.validate is not None else False
-    livvkit.publish = options.publish
+    livvkit.pool_size = options.pool_size
 
     # Get a list of bundles that provide model specific implementations
     available_bundles = [mod for imp, mod, ispkg in pkgutil.iter_modules(bundles.__path__)]
diff --git a/setup.py b/setup.py
index 2d0a8a9..5c6e953 100644
--- a/setup.py
+++ b/setup.py
@@ -27,8 +27,6 @@
 # OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-from __future__ import absolute_import, print_function, unicode_literals
-
 import os
 import re
 
@@ -67,20 +65,31 @@
 
                    'License :: OSI Approved :: BSD License',
 
-                   'Programming Language :: Python :: 2',
-                   'Programming Language :: Python :: 2.7',
                    'Programming Language :: Python :: 3',
-                   'Programming Language :: Python :: 3.5',
                    'Programming Language :: Python :: 3.6',
+                   'Programming Language :: Python :: 3.7',
+                   'Programming Language :: Python :: 3.8',
                    ],
 
-      install_requires=['six',
-                        'numpy',
+      install_requires=['numpy',
                         'scipy',
                         'netCDF4',
                         'matplotlib',
-                        'json_tricks==3.11.0'
+                        'jinja2',
+                        'json_tricks==3.11.0',
+                        'pybtex',
+                        'pandas',
                         ],
+      extras_require={'develop': ['requests',
+                                  'pytest',
+                                  'pytest-cov',
+                                  'tox',
+                                  'sphinx',
+                                  'sphinx-js',
+                                  'sphinx-py3doc-enhanced-theme',
+                                  ]},
+
+      python_requires='>=3.6',
 
       packages=['livvkit',
                 'livvkit.bundles',
@@ -88,7 +97,9 @@
                 'livvkit.components',
                 'livvkit.components.numerics_tests',
                 'livvkit.data',
+                'livvkit.data.numerics',
                 'livvkit.util',
+                'livvkit.elements',
                 ],
 
       entry_points={'console_scripts': ['livv = livvkit.__main__:main']},
diff --git a/tox.ini b/tox.ini
index 4ea0f20..06e3764 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,10 +4,11 @@
 # and then run "tox" from this directory.
 
 [tox]
-envlist = py27, py35, py36
+envlist = py36, py37, py38
 
 [testenv]
 commands = pytest
 deps =
     pytest
     requests
+    cython


Test patch:
diff --git a/docs/source/livvkit.components.numerics_tests.rst b/docs/source/livvkit.components.numerics_tests.rst
index 91f7c0b..d12b6d6 100644
--- a/docs/source/livvkit.components.numerics_tests.rst
+++ b/docs/source/livvkit.components.numerics_tests.rst
@@ -8,15 +8,15 @@ livvkit.components.numerics\_tests.ismip module
 -----------------------------------------------
 
 .. automodule:: livvkit.components.numerics_tests.ismip
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
 
 
 Module contents
 ---------------
 
 .. automodule:: livvkit.components.numerics_tests
-    :members:
-    :undoc-members:
-    :show-inheritance:
+   :members:
+   :undoc-members:
+   :show-inheritance:
diff --git a/livvkit/components/numerics_tests/ismip.py b/livvkit/components/numerics_tests/ismip.py
index fdd52d1..901438f 100644
--- a/livvkit/components/numerics_tests/ismip.py
+++ b/livvkit/components/numerics_tests/ismip.py
@@ -30,9 +30,6 @@
 """
 Utilities to provide numerical verification for the ISMIP test cases
 """
-from __future__ import absolute_import, division, print_function, unicode_literals
-import six
-
 
 import os
 
@@ -41,7 +38,7 @@
 
 import livvkit
 from livvkit.util.LIVVDict import LIVVDict
-from livvkit.util import elements
+from livvkit import elements
 from livvkit.util import functions
 
 
@@ -80,7 +77,7 @@ def run(config, analysis_data):
         coord = 'y_hat'
 
     lengths = list(set(
-        [get_case_length(d) for d in six.iterkeys(analysis_data)]
+        [get_case_length(d) for d in analysis_data]
         ))
 
     plot_list = []
@@ -116,12 +113,12 @@ def run(config, analysis_data):
             plt.plot(axis, ho_mean, 'g-', linewidth=2, label='Higher order')
 
             analysis = {}
-            for a in six.iterkeys(analysis_data):
+            for a in analysis_data:
                 if int(l) == int(a.split('-')[-1][1:]):
                     analysis[a] = analysis_data[a]
 
-            for a in six.iterkeys(analysis):
-                for model in sorted(six.iterkeys(analysis[a])):
+            for a in analysis:
+                for model in sorted(analysis[a]):
                     plt.plot(analysis[a][model][coord],
                              analysis[a][model][config['plot_vars'][p]],
                              line_style[model],
@@ -130,19 +127,19 @@ def run(config, analysis_data):
                              label=a+'-'+model)
 
             plt.legend(loc='best')
-            if livvkit.publish:
-                plt.savefig(os.path.splitext(plot_file)[0]+'.eps', dpi=600)
             plt.savefig(plot_file)
             plt.close()
-            plot_list.append(elements.image(title, description, os.path.basename(plot_file)))
 
-    return elements.gallery("Numerics Plots", plot_list)
+            image = elements.Image(title, description, plot_file)
+            plot_list.append(image)
+
+    return elements.Gallery("Numerics Plots", plot_list)
 
 
 def summarize_result(data, config):
     case = config['name']
     summary = LIVVDict()
-    lengths = list(set([get_case_length(d) for d in six.iterkeys(data)]))
+    lengths = list(set([get_case_length(d) for d in data]))
 
     for p, pattern in enumerate(sorted(setup[case]['pattern'])):
         for l in sorted(lengths):
@@ -155,12 +152,12 @@ def summarize_result(data, config):
                 np.genfromtxt(recreate_file, delimiter=',', missing_values='nan', unpack=True)
 
             analysis = {}
-            for a in six.iterkeys(data):
+            for a in data:
                 if int(l) == int(a.split('-')[-1][1:]):
                     analysis[a] = data[a]
 
-            for a in six.iterkeys(analysis):
-                for model in sorted(six.iterkeys(analysis[a])):
+            for a in analysis:
+                for model in sorted(analysis[a]):
                     if setup[case]['ylabel'][p].split(" ")[0].lower() == 'surface':
                         percent_errors = np.divide(analysis[a][model][config['plot_vars'][p]]
                                                    - ho_mean, ho_mean+1000)
@@ -186,7 +183,7 @@ def summarize_result(data, config):
 
 def print_summary(case, summary):
     """ Show some statistics from the run """
-    for subcase in six.iterkeys(summary):
+    for subcase in summary:
         message = case + " " + subcase
         print("    " + message)
         print("    " + "-"*len(message))
diff --git a/tests/conftest.py b/tests/conftest.py
index db6e3db..6a64beb 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -1,7 +1,5 @@
 # coding=utf-8
 
-from __future__ import absolute_import, print_function, unicode_literals
-
 import os
 import pytest
 import requests
@@ -104,3 +102,31 @@ def test_data(tmpdir_factory):
         tar.extractall(path=ref_dir)
 
     return ref_dir.join('cism-2.0.6-tests')
+
+
+@pytest.fixture(scope='session')
+def diff_data(tmpdir_factory):
+    content_template = '[DOME-TEST]\n\n' \
+                       '[grid]\n' \
+                       'upn = 10\n' \
+                       'ewn = 31\n' \
+                       'nsn = 31\n' \
+                       'dew = {}\n' \
+                       'dns = 2000.0\n\n' \
+                       '[time]\n' \
+                       'tstart = 0.\n' \
+                       'tend = 10.\n' \
+                       'dt = 1.\n' \
+                       'dt_diag = 1.\n' \
+                       'idiag = 10\n' \
+                       'jdiag = 10\n'
+
+    diff_dir = tmpdir_factory.mktemp('diff')
+    from_file = diff_dir.join('from.cfg')
+    to_file = diff_dir.join('to.cfg')
+
+    with open(from_file, 'w') as from_, open(to_file, 'w') as to_:
+        from_.write(content_template.format(2000.0))
+        to_.write(content_template.format(200.0))
+
+    return from_file, to_file
diff --git a/tests/test_elements.py b/tests/test_elements.py
new file mode 100644
index 0000000..fe28247
--- /dev/null
+++ b/tests/test_elements.py
@@ -0,0 +1,1120 @@
+# coding=utf-8
+
+"""Test the LIVVkit elements"""
+
+import json
+from contextlib import ContextDecorator
+
+import pytest
+
+import livvkit
+from livvkit import elements
+
+
+class LIVVkitOutput(ContextDecorator):
+    """Decorator to set the required livvkit variable `livvkit.output_dir`"""
+    def __enter__(self):
+        livvkit.output_dir = 'vv_test'
+
+    def __exit__(self, exc_type, exc_val, exc_tb):
+        livvkit.output_dir = None
+
+
+def test_el_base_templates_as_property():
+    truth = 'template_string'
+
+    class Property(elements.BaseElement):
+        """Class to test property definition of _*_template"""
+        @property
+        def _html_template(self):
+            return truth
+
+        @property
+        def _latex_template(self):
+            return truth
+
+    p = Property()
+
+    errors = []
+    if p._html_template != truth:
+        errors.append("Error:_html_template doesn't act like a property")
+    if p._latex_template != truth:
+        errors.append("Error:_latex_template doesn't act like a property")
+
+    assert not errors, 'These errors occurred:\n    {}'.format(
+            '\n    '.join(errors))
+
+
+def test_el_base_templates_as_class_attribute():
+    errors = []
+    truth = 'template_string'
+
+    class ClassAttribute(elements.BaseElement):
+        """Class to test attribute definition of _*_template"""
+        _html_template = truth
+        _latex_template = truth
+
+    ca = ClassAttribute()
+
+    if ca._html_template != truth:
+        errors.append("Error:_html_template doesn't act like a class attribute")
+    if ca._latex_template != truth:
+        errors.append("Error:_latex_template doesn't act like a class attribute")
+
+    assert not errors, 'These errors occurred:\n    {}'.format(
+            '\n    '.join(errors))
+
+
+def test_el_base_templates_as_instance_attribute():
+    truth = 'template_string'
+
+    class HTMLInstanceAttribute(elements.BaseElement):
+        """Class to test instance attribute definition of _*_template"""
+        def __init__(self, html):
+            self._html_template = html
+            super(HTMLInstanceAttribute, self).__init__()
+
+        @property
+        def _latex_template(self):
+            return truth
+
+    class LatexInstanceAttribute(elements.BaseElement):
+        def __init__(self, latex):
+            self._latex_template = latex
+            super(LatexInstanceAttribute, self).__init__()
+
+        @property
+        def _html_template(self):
+            return truth
+
+    with pytest.raises(TypeError):
+        _ = HTMLInstanceAttribute(truth)
+
+    with pytest.raises(TypeError):
+        _ = LatexInstanceAttribute(truth)
+
+
+def test_el_base_templates_as_method():
+    truth = 'template_string'
+
+    class HTMLMethod(elements.BaseElement):
+        """Class to test method definition of _*_template"""
+        def _html_template(self):
+            return truth
+
+        @property
+        def _latex_template(self):
+            return truth
+
+    class LatexMethod(elements.BaseElement):
+        """Class to test method definition of _*_template"""
+        @property
+        def _html_template(self):
+            return truth
+
+        def _latex_template(self):
+            return truth
+
+    with pytest.raises(TypeError):
+        _ = HTMLMethod()
+
+    with pytest.raises(TypeError):
+        _ = LatexMethod()
+
+
+def test_el_page_json():
+    truth = '{\n' \
+            '    "Page": {\n' \
+            '        "elements": [\n' \
+            '            {\n' \
+            '                "Section": {\n' \
+            '                    "elements": [\n' \
+            '                        {\n' \
+            '                            "Table": {\n' \
+            '                                "title": "title",\n' \
+            '                                "data": {\n' \
+            '                                    "h1": [\n' \
+            '                                        "v1",\n' \
+            '                                        "v2"\n' \
+            '                                    ],\n' \
+            '                                    "h2": [\n' \
+            '                                        "v3",\n' \
+            '                                        "v4"\n' \
+            '                                    ]\n' \
+            '                                },\n' \
+            '                                "index": null,\n' \
+            '                                "rows": 2,\n' \
+            '                                "__module__": "livvkit.elements.elements",\n' \
+            '                                "_html_template": "table.html",\n' \
+            '                                "_latex_template": "table.tex"\n' \
+            '                            }\n' \
+            '                        }\n' \
+            '                    ],\n' \
+            '                    "title": "A cool table",\n' \
+            '                    "__module__": "livvkit.elements.elements",\n' \
+            '                    "_html_template": "section.html",\n' \
+            '                    "_latex_template": "section.tex"\n' \
+            '                }\n' \
+            '            }\n' \
+            '        ],\n' \
+            '        "title": "A Page",\n' \
+            '        "description": "A good description",\n' \
+            '        "_ref_list": null,\n' \
+            '        "Data": "<div id=\\"A Page\\">\\n    <h2>A Page</h2>\\n    <p>A good description</p>\\n    <div class=\\"section\\">\\n    <h2>A cool table</h2>\\n    <div class=\\"table\\">\\n    <h3>title</h3>\\n    <table>\\n        <tr>\\n            <th>h1</th>\\n            <th>h2</th>\\n        </tr>\\n        <tr>\\n            <td>v1</td>\\n            <td>v3</td>\\n        </tr>\\n        <tr>\\n            <td>v2</td>\\n            <td>v4</td>\\n        </tr>\\n    </table>\\n</div>\\n</div>\\n</div>",\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "page.html",\n' \
+            '        "_latex_template": "page.tex"\n' \
+            '    }\n' \
+            '}'
+
+    page = elements.Page(
+        'A Page', 'A good description',
+        [elements.Section('A cool table', [elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})])],
+        references=None,
+    )
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(page._repr_json())
+
+    assert page._repr_json() == truth
+
+
+def test_el_page_html():
+    truth = '<div id="A Page">\n' \
+            '    <h2>A Page</h2>\n' \
+            '    <p>A good description</p>\n' \
+            '    <div class="section">\n' \
+            '    <h2>A cool table</h2>\n' \
+            '    <div class="table">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>h1</th>\n' \
+            '            <th>h2</th>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>v1</td>\n' \
+            '            <td>v3</td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>v2</td>\n' \
+            '            <td>v4</td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>\n' \
+            '</div>\n' \
+            '</div><div class="bibliography"><h2>References</h2><p>LIVVkit is an open source project licensed under a BSD 3-clause License. We ask that you please acknowledge LIVVkit in any work it is used or supports. In any corresponding published work, please cite: </p><dl><dt>1</dt> <dd>K.&nbsp;J. Evans, J.&nbsp;H. Kennedy, D.&nbsp;Lu, M.&nbsp;M. Forrester, S.&nbsp;Price, J.&nbsp;Fyke, A.&nbsp;R. Bennett, M.&nbsp;J. Hoffman, I.&nbsp;Tezaur, C.&nbsp;S. Zender, and M.&nbsp;Vizca\\\'<span class="bibtex-protected">ı</span>no. Livvkit 2.1: automated and extensible ice sheet model validation. <em>Geoscientific Model Development</em>, 12(3):1067–1086, 2019. URL: <a href="https://www.geosci-model-dev.net/12/1067/2019/">https://www.geosci-model-dev.net/12/1067/2019/</a>, <a href="https://doi.org/10.5194/gmd-12-1067-2019">doi:10.5194/gmd-12-1067-2019</a>.</dd> <dt>2</dt> <dd>Joseph&nbsp;H. Kennedy, Andrew&nbsp;R. Bennett, Katherine&nbsp;J. Evans, Stephen Price, Matthew Hoffman, William&nbsp;H. Lipscomb, Jeremy Fyke, Lauren Vargo, Adrianna Boghozian, Matthew Norman, and Patrick&nbsp;H. Worley. Livvkit: an extensible, python-based, land ice verification and validation toolkit for ice sheet models. <em>Journal of Advances in Modeling Earth Systems</em>, 9(2):854–869, 2017. <a href="https://doi.org/10.1002/2017MS000916">doi:10.1002/2017MS000916</a>.</dd> </dl></div>'
+
+
+    page = elements.Page(
+        'A Page', 'A good description',
+        [elements.Section('A cool table', [elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})])]
+    )
+
+    assert page._repr_html() == truth
+
+
+def test_el_page_latex():
+    truth = "\\levelstay{A Page}\n" \
+            "A good description\n" \
+            "    \\levelstay{A cool table}\n" \
+            "    \\begin{table}[h!]\n" \
+            "    \\centering\n" \
+            "    \\begin{tabular}{cc}\n" \
+            "        h1 & h2 \\\\\n" \
+            "        \\hline\n" \
+            "        v1 & v3  \\\\\n" \
+            "        v2 & v4  \\\\\n" \
+            "        \\end{tabular}\n" \
+            "\\end{table}\n" \
+            "\n" \
+            "\\begin{thebibliography}{1}\n" \
+            "\n" \
+            "\\bibitem[1]{gmd-12-1067-2019}\n" \
+            "K.~J. Evans, J.~H. Kennedy, D.~Lu, M.~M. Forrester, S.~Price, J.~Fyke, A.~R. Bennett, M.~J. Hoffman, I.~Tezaur, C.~S. Zender, and M.~Vizca\\'{ı}no.\n" \
+            "\\newblock Livvkit 2.1: automated and extensible ice sheet model validation.\n" \
+            "\\newblock \\emph{Geoscientific Model Development}, 12(3):1067–1086, 2019.\n" \
+            "\\newblock URL: \\url{https://www.geosci-model-dev.net/12/1067/2019/}, \\href{https://doi.org/10.5194/gmd-12-1067-2019}{doi:10.5194/gmd-12-1067-2019}.\n" \
+            "\n" \
+            "\\bibitem[2]{Kennedy2017}\n" \
+            "Joseph~H. Kennedy, Andrew~R. Bennett, Katherine~J. Evans, Stephen Price, Matthew Hoffman, William~H. Lipscomb, Jeremy Fyke, Lauren Vargo, Adrianna Boghozian, Matthew Norman, and Patrick~H. Worley.\n" \
+            "\\newblock Livvkit: an extensible, python-based, land ice verification and validation toolkit for ice sheet models.\n" \
+            "\\newblock \\emph{Journal of Advances in Modeling Earth Systems}, 9(2):854–869, 2017.\n" \
+            "\\newblock \\href{https://doi.org/10.1002/2017MS000916}{doi:10.1002/2017MS000916}.\n" \
+            "\n" \
+            "\\end{thebibliography}\n"
+
+    page = elements.Page(
+        'A Page', 'A good description',
+        [elements.Section('A cool table', [elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})])]
+    )
+
+    assert page._repr_latex() == truth
+
+
+def test_el_tabs_json():
+    truth = '{\n' \
+            '    "Tabs": {\n' \
+            '        "elements_dict": {\n' \
+            '            "a": [\n' \
+            '                {\n' \
+            '                    "Error": {\n' \
+            '                        "title": "AError",\n' \
+            '                        "message": "woops",\n' \
+            '                        "__module__": "livvkit.elements.elements",\n' \
+            '                        "_html_template": "err.html",\n' \
+            '                        "_latex_template": "err.tex"\n' \
+            '                    }\n' \
+            '                }\n' \
+            '            ],\n' \
+            '            "b": [\n' \
+            '                {\n' \
+            '                    "Error": {\n' \
+            '                        "title": "BError",\n' \
+            '                        "message": "boogers",\n' \
+            '                        "__module__": "livvkit.elements.elements",\n' \
+            '                        "_html_template": "err.html",\n' \
+            '                        "_latex_template": "err.tex"\n' \
+            '                    }\n' \
+            '                }\n' \
+            '            ]\n' \
+            '        },\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "tabs.html",\n' \
+            '        "_latex_template": "tabs.tex"\n' \
+            '    }\n' \
+            '}'
+
+    tabs = elements.Tabs(
+        {'a': [elements.Error('AError', 'woops')], 'b': [elements.Error('BError', 'boogers')]}
+    )
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(tabs._repr_json())
+
+    assert tabs._repr_json() == truth
+
+
+def test_el_tabs_html():
+    truth = '<div id="tabs">\n' \
+            '    <ul>\n' \
+            '        <li><a href="#a">a</a></li>\n' \
+            '        <li><a href="#b">b</a></li>\n' \
+            '    </ul>\n' \
+            '    <div id="a">\n' \
+            '        <div class="error">\n' \
+            '    <h3>AError</h3>\n' \
+            '    <p>woops</p>\n' \
+            '</div>\n' \
+            '    </div>\n' \
+            '    <div id="b">\n' \
+            '        <div class="error">\n' \
+            '    <h3>BError</h3>\n' \
+            '    <p>boogers</p>\n' \
+            '</div>\n' \
+            '    </div>\n' \
+            '</div>'
+
+    tabs = elements.Tabs({'a': [elements.Error('AError', 'woops')], 'b': [elements.Error('BError', 'boogers')]})
+
+    assert tabs._repr_html() == truth
+
+
+def test_el_tabs_latex():
+    truth = '\\levelstay{a}\n' \
+            '    \\colorbox{red}{\\parbox{\\textwidth}{\n' \
+            '    \\textbf{AError}: woops\n' \
+            '}}\\levelstay{b}\n' \
+            '    \\colorbox{red}{\\parbox{\\textwidth}{\n' \
+            '    \\textbf{BError}: boogers\n' \
+            '}}'
+
+    tabs = elements.Tabs({'a': [elements.Error('AError', 'woops')], 'b': [elements.Error('BError', 'boogers')]})
+
+    assert tabs._repr_latex() == truth
+
+
+def test_el_section_json():
+    truth = '{\n' \
+            '    "Section": {\n' \
+            '        "elements": [\n' \
+            '            {\n' \
+            '                "Table": {\n' \
+            '                    "title": "title",\n' \
+            '                    "data": {\n' \
+            '                        "h1": [\n' \
+            '                            "v1",\n' \
+            '                            "v2"\n' \
+            '                        ],\n' \
+            '                        "h2": [\n' \
+            '                            "v3",\n' \
+            '                            "v4"\n' \
+            '                        ]\n' \
+            '                    },\n' \
+            '                    "index": null,\n' \
+            '                    "rows": 2,\n' \
+            '                    "__module__": "livvkit.elements.elements",\n' \
+            '                    "_html_template": "table.html",\n' \
+            '                    "_latex_template": "table.tex"\n' \
+            '                }\n' \
+            '            }\n' \
+            '        ],\n' \
+            '        "title": "A cool table",\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "section.html",\n' \
+            '        "_latex_template": "section.tex"\n' \
+            '    }\n' \
+            '}'
+
+    section = elements.Section('A cool table', [elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})])
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(section._repr_json())
+
+    assert section._repr_json() == truth
+
+
+def test_el_section_html():
+    truth = '<div class="section">\n' \
+            '    <h2>A cool table</h2>\n' \
+            '    <div class="table">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>h1</th>\n' \
+            '            <th>h2</th>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>v1</td>\n' \
+            '            <td>v3</td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>v2</td>\n' \
+            '            <td>v4</td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>\n' \
+            '</div>'
+
+    section = elements.Section('A cool table', [elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})])
+
+    assert section._repr_html() == truth
+
+
+def test_el_section_latex():
+    truth = '\\levelstay{A cool table}\n' \
+            '    \\begin{table}[h!]\n' \
+            '    \\centering\n' \
+            '    \\begin{tabular}{cc}\n' \
+            '        h1 & h2 \\\\\n' \
+            '        \\hline\n' \
+            '        v1 & v3  \\\\\n' \
+            '        v2 & v4  \\\\\n' \
+            '        \\end{tabular}\n' \
+            '\\end{table}\n'
+
+    section = elements.Section('A cool table', [elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})])
+
+    assert section._repr_latex() == truth
+
+
+def test_el_table_json():
+    truth = '{\n' \
+            '    "Table": {\n' \
+            '        "title": "title",\n' \
+            '        "data": {\n' \
+            '            "h1": [\n' \
+            '                "v1",\n' \
+            '                "v2"\n' \
+            '            ],\n' \
+            '            "h2": [\n' \
+            '                "v3",\n' \
+            '                "v4"\n' \
+            '            ]\n' \
+            '        },\n' \
+            '        "index": null,\n' \
+            '        "rows": 2,\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "table.html",\n' \
+            '        "_latex_template": "table.tex"\n' \
+            '    }\n' \
+            '}'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(table._repr_json())
+
+    assert table._repr_json() == truth
+
+
+def test_el_table_html():
+    truth = '<div class="table">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>h1</th>\n' \
+            '            <th>h2</th>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>v1</td>\n' \
+            '            <td>v3</td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>v2</td>\n' \
+            '            <td>v4</td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})
+
+    assert table._repr_html() == truth
+
+
+def test_el_table_latex():
+    truth = '\\begin{table}[h!]\n' \
+            '    \\centering\n' \
+            '    \\begin{tabular}{cc}\n' \
+            '        h1 & h2 \\\\\n' \
+            '        \\hline\n' \
+            '        v1 & v3  \\\\\n' \
+            '        v2 & v4  \\\\\n' \
+            '        \\end{tabular}\n' \
+            '\\end{table}'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']})
+
+    assert table._repr_latex() == truth
+
+
+def test_el_table_w_index_html():
+    truth = '<div class="table">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>&nbsp;</th>\n' \
+            '            <th>h1</th>\n' \
+            '            <th>h2</th>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <th>0</th>\n' \
+            '            <td>v1</td>\n' \
+            '            <td>v3</td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <th>1</th>\n' \
+            '            <td>v2</td>\n' \
+            '            <td>v4</td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']}, index=True)
+
+    assert table._repr_html() == truth
+
+
+def test_el_table_w_index_latex():
+    truth = '\\begin{table}[h!]\n' \
+            '    \\centering\n' \
+            '    \\begin{tabular}{ r | cc}\n' \
+            '         & h1 & h2 \\\\\n' \
+            '        \\hline\n' \
+            '        0 & v1 & v3  \\\\\n' \
+            '        1 & v2 & v4  \\\\\n' \
+            '        \\end{tabular}\n' \
+            '\\end{table}'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']}, index=True)
+
+    assert table._repr_latex() == truth
+
+
+def test_el_table_w_custom_index_html():
+    truth = '<div class="table">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>&nbsp;</th>\n' \
+            '            <th>h1</th>\n' \
+            '            <th>h2</th>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <th>i0</th>\n' \
+            '            <td>v1</td>\n' \
+            '            <td>v3</td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <th>i1</th>\n' \
+            '            <td>v2</td>\n' \
+            '            <td>v4</td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']}, index=['i0', 'i1'])
+
+    assert table._repr_html() == truth
+
+
+def test_el_table_w_custom_index_latex():
+    truth = '\\begin{table}[h!]\n' \
+            '    \\centering\n' \
+            '    \\begin{tabular}{ r | cc}\n' \
+            '         & h1 & h2 \\\\\n' \
+            '        \\hline\n' \
+            '        i0 & v1 & v3  \\\\\n' \
+            '        i1 & v2 & v4  \\\\\n' \
+            '        \\end{tabular}\n' \
+            '\\end{table}'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']}, index=['i0', 'i1'])
+
+    assert table._repr_latex() == truth
+
+
+def test_el_table_transposed_html():
+    truth = '<div class="table">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>h1</th>\n' \
+            '            <td>v1</td>\n' \
+            '            <td>v2</td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <th>h2</th>\n' \
+            '            <td>v3</td>\n' \
+            '            <td>v4</td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']}, transpose=True)
+
+    assert table._repr_html() == truth
+
+
+def test_el_table_transposed_latex():
+    truth = '\\begin{table}[h!]\n' \
+            '    \\centering\n' \
+            '    \\begin{tabular}{r|cc}\n' \
+            '        h1 & v1 & v2  \\\\\n' \
+            '        h2 & v3 & v4  \\\\\n' \
+            '        \\end{tabular}\n' \
+            '\\end{table}'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']}, transpose=True)
+
+    assert table._repr_latex() == truth
+
+
+def test_el_table_w_index_transposed_html():
+    truth = '<div class="table">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>&nbsp;</th>\n' \
+            '            <th>0</th>\n' \
+            '            <th>1</th>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <th>h1</th>\n' \
+            '            <td>v1</td>\n' \
+            '            <td>v2</td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <th>h2</th>\n' \
+            '            <td>v3</td>\n' \
+            '            <td>v4</td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']},
+                           index=True, transpose=True)
+
+    assert table._repr_html() == truth
+
+
+def test_el_table_w_index_transposed_latex():
+    truth = '\\begin{table}[h!]\n' \
+            '    \\centering\n' \
+            '    \\begin{tabular}{r|cc}\n' \
+            '         & 0 & 1\\\\\n' \
+            '        \\hline\n' \
+            '        h1 & v1 & v2  \\\\\n' \
+            '        h2 & v3 & v4  \\\\\n' \
+            '        \\end{tabular}\n' \
+            '\\end{table}'
+
+    table = elements.Table('title', {'h1': ['v1', 'v2'], 'h2': ['v3', 'v4']},
+                           index=True, transpose=True)
+
+    assert table._repr_latex() == truth
+
+
+@LIVVkitOutput()
+def test_el_b4b_json():
+    truth = '{\n' \
+            '    "BitForBit": {\n' \
+            '        "elements": [\n' \
+            '            {\n' \
+            '                "B4BImage": {\n' \
+            '                    "title": "",\n' \
+            '                    "desc": "desc.",\n' \
+            '                    "path": "../imgs",\n' \
+            '                    "name": "b4b.png",\n' \
+            '                    "group": "b4b",\n' \
+            '                    "height": 50,\n' \
+            '                    "__module__": "livvkit.elements.elements",\n' \
+            '                    "_html_template": "image.html",\n' \
+            '                    "_latex_template": "image.tex"\n' \
+            '                }\n' \
+            '            },\n' \
+            '            {\n' \
+            '                "B4BImage": {\n' \
+            '                    "title": "",\n' \
+            '                    "desc": "desc.",\n' \
+            '                    "path": "../imgs",\n' \
+            '                    "name": "b4b.png",\n' \
+            '                    "group": "b4b",\n' \
+            '                    "height": 50,\n' \
+            '                    "__module__": "livvkit.elements.elements",\n' \
+            '                    "_html_template": "image.html",\n' \
+            '                    "_latex_template": "image.tex"\n' \
+            '                }\n' \
+            '            }\n' \
+            '        ],\n' \
+            '        "title": "title",\n' \
+            '        "data": {\n' \
+            '            "Variable": [\n' \
+            '                "velnorm",\n' \
+            '                "thk"\n' \
+            '            ],\n' \
+            '            "Max Error": [\n' \
+            '                5.0707,\n' \
+            '                0.376806\n' \
+            '            ],\n' \
+            '            "Index of Max Error": [\n' \
+            '                [\n' \
+            '                    2,\n' \
+            '                    1,\n' \
+            '                    24,\n' \
+            '                    19\n' \
+            '                ],\n' \
+            '                [\n' \
+            '                    3,\n' \
+            '                    23,\n' \
+            '                    23\n' \
+            '                ]\n' \
+            '            ],\n' \
+            '            "RMS Error": [\n' \
+            '                0.260977,\n' \
+            '                0.0354492\n' \
+            '            ]\n' \
+            '        },\n' \
+            '        "rows": 2,\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "bit4bit.html",\n' \
+            '        "_latex_template": "bit4bit.tex"\n' \
+            '    }\n' \
+            '}'
+
+    b4b = elements.BitForBit(
+        'title', {'Variable': ['velnorm', 'thk'],
+                  'Max Error': [5.07070, 0.376806],
+                  'Index of Max Error': [(2, 1, 24, 19), (3, 23, 23)],
+                  'RMS Error': [.260977, .0354492],
+                  },
+        imgs=[elements.B4BImage('', 'desc.', page_path='vv_test/verification'),
+              elements.B4BImage('', 'desc.', page_path='vv_test/verification'),
+              ]
+    )
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(b4b._repr_json())
+
+    assert b4b._repr_json() == truth
+
+
+@LIVVkitOutput()
+def test_el_b4b_html():
+    truth = '<div class="bitForBit">\n' \
+            '    <h3>title</h3>\n' \
+            '    <table>\n' \
+            '        <tr>\n' \
+            '            <th>Variable</th>\n' \
+            '            <th>Max Error</th>\n' \
+            '            <th>Index of Max Error</th>\n' \
+            '            <th>RMS Error</th>\n' \
+            '            <th> Plot </th>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>velnorm</td>\n' \
+            '            <td>5.07070e+00</td>\n' \
+            '            <td>(2, 1, 24, 19)</td>\n' \
+            '            <td>2.60977e-01</td>\n' \
+            '            <td>\n' \
+            '                <div>\n' \
+            '    <a href="../imgs/b4b.png"\n' \
+            '       data-lightbox="b4b"\n' \
+            '       data-title="desc."\n' \
+            '    >\n' \
+            '        <img class="thumbnail caption"\n' \
+            '             data-caption=""\n' \
+            '             alt=""\n' \
+            '             src="../imgs/b4b.png"\n' \
+            '             style="height: 50px; overflow: hidden; position: relative;"\n' \
+            '        >\n' \
+            '    </a>\n' \
+            '</div>\n' \
+            '            </td>\n' \
+            '        </tr>\n' \
+            '        <tr>\n' \
+            '            <td>thk</td>\n' \
+            '            <td>3.76806e-01</td>\n' \
+            '            <td>(3, 23, 23)</td>\n' \
+            '            <td>3.54492e-02</td>\n' \
+            '            <td>\n' \
+            '                <div>\n' \
+            '    <a href="../imgs/b4b.png"\n' \
+            '       data-lightbox="b4b"\n' \
+            '       data-title="desc."\n' \
+            '    >\n' \
+            '        <img class="thumbnail caption"\n' \
+            '             data-caption=""\n' \
+            '             alt=""\n' \
+            '             src="../imgs/b4b.png"\n' \
+            '             style="height: 50px; overflow: hidden; position: relative;"\n' \
+            '        >\n' \
+            '    </a>\n' \
+            '</div>\n' \
+            '            </td>\n' \
+            '        </tr>\n' \
+            '    </table>\n' \
+            '</div>'
+
+    b4b = elements.BitForBit(
+        'title', {'Variable': ['velnorm', 'thk'],
+                  'Max Error': [5.07070, 0.376806],
+                  'Index of Max Error': [(2, 1, 24, 19), (3, 23, 23)],
+                  'RMS Error': [.260977, .0354492],
+                  },
+        imgs=[elements.B4BImage('', 'desc.', page_path='vv_test/verification'),
+              elements.B4BImage('', 'desc.', page_path='vv_test/verification'),
+              ]
+    )
+
+    assert b4b._repr_html() == truth
+
+
+@LIVVkitOutput()
+def test_el_b4b_latex():
+    truth = '\\begin{table}[h!]\n' \
+            '    \\centering\n' \
+            '    \\begin{tabular}{ c  c  c  c  c }\n' \
+            '        Variable & Max Error & Index of Max Error & RMS Error & Plots \\\\\n' \
+            '        \\hline\n' \
+            '        velnorm & 5.07070e+00 & (2, 1, 24, 19) & 2.60977e-01 & \\begin{minipage}{0.3\\textwidth}\\begin{figure}[h]\n' \
+            '    \\centering\n' \
+            '    \\includegraphics[height=50px]{../imgs/b4b.png}\n' \
+            '    \\caption[]{desc.}\n' \
+            '\\end{figure}\\end{minipage} \\\\\n' \
+            '        thk & 3.76806e-01 & (3, 23, 23) & 3.54492e-02 & \\begin{minipage}{0.3\\textwidth}\\begin{figure}[h]\n' \
+            '    \\centering\n' \
+            '    \\includegraphics[height=50px]{../imgs/b4b.png}\n' \
+            '    \\caption[]{desc.}\n' \
+            '\\end{figure}\\end{minipage} \\\\\n' \
+            '        \\end{tabular}\n' \
+            '\\end{table}'
+
+    b4b = elements.BitForBit(
+        'title', {'Variable': ['velnorm', 'thk'],
+                  'Max Error': [5.07070, 0.376806],
+                  'Index of Max Error': [(2, 1, 24, 19), (3, 23, 23)],
+                  'RMS Error': [.260977, .0354492],
+                  },
+        imgs=[elements.B4BImage('', 'desc.', page_path='vv_test/verification'),
+              elements.B4BImage('', 'desc.', page_path='vv_test/verification'),
+              ]
+    )
+
+    assert b4b._repr_latex() == truth
+
+    livvkit.output_dir = None
+
+
+def test_el_gallery_json():
+    truth = '{\n' \
+            '    "Gallery": {\n' \
+            '        "elements": [\n' \
+            '            {\n' \
+            '                "Image": {\n' \
+            '                    "title": "The Image",\n' \
+            '                    "desc": "A very nice image.",\n' \
+            '                    "path": "imgs",\n' \
+            '                    "name": "image.png",\n' \
+            '                    "group": null,\n' \
+            '                    "height": null,\n' \
+            '                    "__module__": "livvkit.elements.elements",\n' \
+            '                    "_html_template": "image.html",\n' \
+            '                    "_latex_template": "image.tex"\n' \
+            '                }\n' \
+            '            }\n' \
+            '        ],\n' \
+            '        "title": "The Gallery",\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "gallery.html",\n' \
+            '        "_latex_template": "gallery.tex"\n' \
+            '    }\n' \
+            '}'
+
+    image = elements.Image('The Image', 'A very nice image.', 'imgs/image.png')
+    gallery = elements.Gallery('The Gallery', [image])
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(gallery._repr_json())
+
+    assert gallery._repr_json() == truth
+
+
+def test_el_gallery_html():
+    truth = '<div class="gallery">\n' \
+            '    <h3>The Gallery</h3>\n' \
+            '    <div>\n' \
+            '    <a href="imgs/image.png"\n' \
+            '       data-lightbox="The Image"\n' \
+            '       data-title="A very nice image."\n' \
+            '    >\n' \
+            '        <img class="thumbnail caption"\n' \
+            '             data-caption="The Image"\n' \
+            '             alt="The Image"\n' \
+            '             src="imgs/image.png"\n' \
+            '             style="height: 200px; overflow: hidden; position: relative;"\n' \
+            '        >\n' \
+            '    </a>\n' \
+            '</div>\n' \
+            '    \n' \
+            '</div>\n' \
+            '<div style="clear:both"></div>'
+
+    image = elements.Image('The Image', 'A very nice image.', 'imgs/image.png')
+    gallery = elements.Gallery('The Gallery', [image])
+
+    assert gallery._repr_html() == truth
+
+
+def test_el_gallery_latex():
+    truth = '\\levelstay{The Gallery}\n' \
+            '    \\begin{figure}[h]\n' \
+            '    \\centering\n' \
+            '    \\includegraphics[height=200px]{imgs/image.png}\n' \
+            '    \\caption[The Image]{A very nice image.}\n' \
+            '\\end{figure}\n'
+
+    image = elements.Image('The Image', 'A very nice image.', 'imgs/image.png')
+    gallery = elements.Gallery('The Gallery', [image])
+
+    assert gallery._repr_latex() == truth
+
+
+def test_el_image_json():
+    truth = '{\n' \
+            '    "Image": {\n' \
+            '        "title": "title",\n' \
+            '        "desc": "description",\n' \
+            '        "path": "imgs",\n' \
+            '        "name": "name.png",\n' \
+            '        "group": "group",\n        "height": 300,\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "image.html",\n' \
+            '        "_latex_template": "image.tex"\n' \
+            '    }\n' \
+            '}'
+
+    image = elements.Image('title', 'description', 'imgs/name.png', group='group', height=300)
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(image._repr_json())
+
+    assert image._repr_json() == truth
+
+
+def test_el_image_html():
+    truth = '<div>\n' \
+            '    <a href="imgs/name.png"\n' \
+            '       data-lightbox="group"\n' \
+            '       data-title="description"\n' \
+            '    >\n' \
+            '        <img class="thumbnail caption"\n' \
+            '             data-caption="title"\n' \
+            '             alt="title"\n' \
+            '             src="imgs/name.png"\n' \
+            '             style="height: 300px; overflow: hidden; position: relative;"\n' \
+            '        >\n' \
+            '    </a>\n' \
+            '</div>'
+
+    image = elements.Image('title', 'description', 'imgs/name.png', group='group', height=300)
+
+    assert image._repr_html() == truth
+
+
+def test_el_image_latex():
+    truth = "\\begin{figure}[h]\n" \
+            "    \\centering\n" \
+            "    \\includegraphics[height=300px]{imgs/name.png}\n" \
+            "    \\caption[title]{description}\n" \
+            "\\end{figure}"
+
+    image = elements.Image('title', 'description', 'imgs/name.png', group='group', height=300)
+
+    assert image._repr_latex() == truth
+
+
+def test_el_file_diff_json(diff_data):
+    from_file, to_file = diff_data
+
+    with open(from_file) as from_:
+        fromlines = from_.read().splitlines()
+
+    diff_diff = elements.FileDiff('Test JSON', from_file=from_file, to_file=to_file)
+    diff_same = elements.FileDiff('Test JSON', from_file=from_file, to_file=from_file)
+    errors = []
+
+    if diff_diff.diff_status is not True or diff_diff.diff == fromlines:
+        errors.append('Error: there was no difference between the files.')
+    if diff_same.diff_status is True or diff_same.diff != fromlines:
+        errors.append('Error: Self difference showed a difference.')
+
+    try:
+        _ = json.loads(diff_diff._repr_json())
+    except json.JSONDecodeError:
+        errors.append('Diff_diff element did not produce valid JSON.')
+
+    try:
+        _ = json.loads(diff_same._repr_json())
+    except json.JSONDecodeError:
+        errors.append('Diff_same element did not produce valid JSON.')
+
+    assert not errors, 'Errors occurred:\n{}'.format('\n'.join(errors))
+
+
+def test_el_file_diff_html(diff_data):
+    truth = '<h3>Test HTML</h3>\n' \
+            '<div class="diff">\n' \
+            '    <p class="old">--- </p>\n' \
+            '    <p class="new">+++ </p>\n' \
+            '    <p class="range">@@ -4,7 +4,7 @@</p>\n' \
+            '    <p> upn = 10</p>\n' \
+            '    <p> ewn = 31</p>\n' \
+            '    <p> nsn = 31</p>\n' \
+            '    <p class="old">-dew = 2000.0</p>\n' \
+            '    <p class="new">+dew = 200.0</p>\n' \
+            '    <p> dns = 2000.0</p>\n' \
+            '    <p> </p>\n' \
+            '    <p> [time]</p>\n' \
+            '</div>'
+
+    from_file, to_file = diff_data
+
+    diff = elements.FileDiff('Test HTML', from_file=from_file, to_file=to_file)
+
+    assert diff._repr_html() == truth
+
+
+def test_el_file_diff_latex(diff_data):
+    truth = '\\begin{minted}{diff}\n' \
+            '--- \n' \
+            '+++ \n' \
+            '@@ -4,7 +4,7 @@\n' \
+            ' upn = 10\n' \
+            ' ewn = 31\n' \
+            ' nsn = 31\n' \
+            '-dew = 2000.0\n' \
+            '+dew = 200.0\n' \
+            ' dns = 2000.0\n' \
+            ' \n' \
+            ' [time]\n' \
+            '\\end{minted}'
+
+    from_file, to_file = diff_data
+
+    diff = elements.FileDiff('Test HTML', from_file=from_file, to_file=to_file)
+
+    assert diff._repr_latex() == truth
+
+
+def test_el_error_json():
+    truth = '{\n' \
+            '    "Error": {\n' \
+            '        "title": "WOOPS",\n' \
+            '        "message": "Mistakes were made.",\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "err.html",\n' \
+            '        "_latex_template": "err.tex"\n' \
+            '    }\n' \
+            '}'
+
+    err = elements.Error('WOOPS', 'Mistakes were made.')
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(err._repr_json())
+
+    assert err._repr_json() == truth
+
+
+def test_el_error_html():
+    truth = '<div class="error">\n' \
+            '    <h3>WOOPS</h3>\n' \
+            '    <p>Mistakes were made.</p>\n' \
+            '</div>'
+
+    err = elements.Error('WOOPS', 'Mistakes were made.')
+
+    assert err._repr_html() == truth
+
+
+def test_el_error_latex():
+    truth = '\\colorbox{red}{\\parbox{\\textwidth}{\n' \
+            '    \\textbf{WOOPS}: Mistakes were made.\n' \
+            '}}'
+    err = elements.Error('WOOPS', 'Mistakes were made.')
+
+    assert err._repr_latex() == truth
+
+
+def test_el_raw_html_json():
+    truth = '{\n' \
+            '    "RawHTML": {\n' \
+            '        "html": "<div>Hi</div>",\n' \
+            '        "__module__": "livvkit.elements.elements",\n' \
+            '        "_html_template": "raw.html",\n' \
+            '        "_latex_template": "raw.tex"\n' \
+            '    }\n' \
+            '}'
+
+    html = elements.RawHTML('<div>Hi</div>')
+
+    # Will raise a JSONDecodeError if not valid JSON
+    _ = json.loads(html._repr_json())
+
+    assert html._repr_json() == truth
+
+
+def test_el_raw_html_html():
+    truth = '<div>\n    <div>Hi</div>\n</div>'
+
+    html = elements.RawHTML('<div>Hi</div>')
+
+    assert html._repr_html() == truth
+
+
+def test_el_raw_html_latex():
+    truth = '\\begin{minted}{html}\n    <div>Hi</div>\n\\end{minted}'
+
+    html = elements.RawHTML('<div>Hi</div>')
+
+    assert html._repr_latex() == truth
diff --git a/tests/test_util_bib.py b/tests/test_util_bib.py
new file mode 100644
index 0000000..90570d0
--- /dev/null
+++ b/tests/test_util_bib.py
@@ -0,0 +1,91 @@
+# coding=utf-8
+
+import os
+import pybtex.database
+
+import livvkit
+from livvkit.util import bib
+
+
+DATA_DIR = os.path.join(os.path.dirname(livvkit.__file__), 'data')
+
+KENNEDY2017_HTML = '<div class="bibliography"><h2>References</h2>' \
+                    '<p>LIVVkit is an open source project licensed under a BSD 3-clause License. ' \
+                    'We ask that you please acknowledge LIVVkit in any work it is used or supports. ' \
+                    'In any corresponding published work, please cite: </p>' \
+                    '<dl><dt>1</dt> <dd>Joseph&nbsp;H. Kennedy, ' \
+                   'Andrew&nbsp;R. Bennett, Katherine&nbsp;J. Evans, Stephen Price, ' \
+                   'Matthew Hoffman, William&nbsp;H. Lipscomb, Jeremy Fyke, Lauren Vargo, ' \
+                   'Adrianna Boghozian, Matthew Norman, and Patrick&nbsp;H. Worley. ' \
+                   'Livvkit: an extensible, python-based, land ice verification and validation ' \
+                   'toolkit for ice sheet models. <em>Journal of Advances in Modeling Earth ' \
+                   'Systems</em>, 9(2):854–869, 2017. ' \
+                   '<a href="https://doi.org/10.1002/2017MS000916">doi:10.1002/2017MS000916</a>.' \
+                   '</dd> </dl></div>'
+
+KENNEDY2017_EVANS2019_HTML = '<div class="bibliography"><h2>References</h2>' \
+                             '<p>LIVVkit is an open source project licensed under a BSD 3-clause License. ' \
+                             'We ask that you please acknowledge LIVVkit in any work it is used or supports. ' \
+                             'In any corresponding published work, please cite: </p>' \
+                             '<dl><dt>1</dt> <dd>K.&nbsp;J. Evans, ' \
+                             'J.&nbsp;H. Kennedy, D.&nbsp;Lu, M.&nbsp;M. Forrester, S.&nbsp;Price, ' \
+                             'J.&nbsp;Fyke, A.&nbsp;R. Bennett, M.&nbsp;J. Hoffman, I.&nbsp;Tezaur, ' \
+                             'C.&nbsp;S. Zender, and M.&nbsp;Vizca\\\'<span class="bibtex-protected">ı</span>no. ' \
+                             'Livvkit 2.1: automated and extensible ice sheet model validation. ' \
+                             '<em>Geoscientific Model Development</em>, 12(3):1067–1086, 2019. URL: ' \
+                             '<a href="https://www.geosci-model-dev.net/12/1067/2019/">' \
+                             'https://www.geosci-model-dev.net/12/1067/2019/</a>, ' \
+                             '<a href="https://doi.org/10.5194/gmd-12-1067-2019">' \
+                             'doi:10.5194/gmd-12-1067-2019</a>.</dd> <dt>2</dt> <dd>Joseph&nbsp;H. Kennedy, ' \
+                             'Andrew&nbsp;R. Bennett, Katherine&nbsp;J. Evans, Stephen Price, Matthew Hoffman, ' \
+                             'William&nbsp;H. Lipscomb, Jeremy Fyke, Lauren Vargo, Adrianna Boghozian, ' \
+                             'Matthew Norman, and Patrick&nbsp;H. Worley. Livvkit: an extensible, ' \
+                             'python-based, land ice verification and validation toolkit for ice sheet ' \
+                             'models. <em>Journal of Advances in Modeling Earth Systems</em>, 9(2):854–869, ' \
+                             '2017. <a href="https://doi.org/10.1002/2017MS000916">doi:10.1002/2017MS000916</a>.' \
+                             '</dd> </dl></div>'
+
+
+
+
+def test_bib2html_str():
+    html = bib.bib2html(os.path.join(DATA_DIR, 'Kennedy2017.bib'))
+    assert html == KENNEDY2017_HTML
+
+
+def test_bib2html_list():
+    html = bib.bib2html([
+        os.path.join(DATA_DIR, 'Kennedy2017.bib'), os.path.join(DATA_DIR, 'Evans2019.bib')
+    ])
+    assert html == KENNEDY2017_EVANS2019_HTML
+
+
+def test_bib2html_set():
+    html = bib.bib2html({
+        os.path.join(DATA_DIR, 'Kennedy2017.bib'), os.path.join(DATA_DIR, 'Evans2019.bib')
+    })
+
+    assert html == KENNEDY2017_EVANS2019_HTML
+
+
+def test_bib2html_tuple():
+    html = bib.bib2html((
+        os.path.join(DATA_DIR, 'Kennedy2017.bib'), os.path.join(DATA_DIR, 'Evans2019.bib')
+    ))
+
+    assert html == KENNEDY2017_EVANS2019_HTML
+
+
+def test_bib2html_bibliographydata():
+    bibliography = pybtex.database.parse_file(os.path.join(DATA_DIR, 'Kennedy2017.bib'))
+
+    html = bib.bib2html(bibliography)
+
+    assert html == KENNEDY2017_HTML
+
+
+def test_bib2html_duplicate_elements():
+    html = bib.bib2html([
+        os.path.join(DATA_DIR, 'Kennedy2017.bib'), os.path.join(DATA_DIR, 'Kennedy2017.bib')
+    ])
+    assert html == KENNEDY2017_HTML
diff --git a/tests/test_util.py b/tests/test_util_functions.py
similarity index 97%
rename from tests/test_util.py
rename to tests/test_util_functions.py
index b28e827..c404247 100644
--- a/tests/test_util.py
+++ b/tests/test_util_functions.py
@@ -1,7 +1,5 @@
 # coding=utf-8
 
-from __future__ import absolute_import, print_function, unicode_literals
-
 import pytest
 import numpy as np
 from collections import OrderedDict
@@ -130,6 +128,6 @@ def test_fn_setup_output(tmpdir):
     livvkit.index_dir = str(idir)
     functions.setup_output()
 
-    test = idir.join('data.txt').read_text('utf8').strip()
+    test = idir.join('data.txt').readlines()[0].strip()
 
     assert test == livvkit.timestamp