This commit is contained in:
SheetJS 2024-01-06 21:35:22 -05:00
parent d6abde0e8e
commit 9698f15b32
87 changed files with 45118 additions and 1 deletions

@ -0,0 +1,728 @@
---
title: Summary Statistics
sidebar_label: Summary Statistics
pagination_prev: demos/index
pagination_next: demos/frontend/index
---
import current from '/version.js';
import Tabs from '@theme/Tabs';
import TabItem from '@theme/TabItem';
import CodeBlock from '@theme/CodeBlock';
export const bs = ({borderStyle:"none", background:"none", textAlign:"left" });
Summary statistics help people quickly understand datasets and make informed
decisions. Many interesting datasets are stored in spreadsheet files.
[SheetJS](https://sheetjs.com) is a JavaScript library for reading and writing
data from spreadsheets.
This demo uses SheetJS to process data in spreadsheets. We'll explore how to
extract spreadsheet data and how to compute simple summary statistics. This
demo will focus on two general data representations:
- ["Arrays of Objects"](#arrays-of-objects) simplifies processing by translating
from the SheetJS data model to a more idiomatic data structure.
- ["Dense Worksheets"](#dense-worksheets) directly analyzes SheetJS worksheets.
:::tip pass
The [Import Tutorial](/docs/getting-started/examples/import) is a guided example
of extracting data from a workbook. It is strongly recommended to review the
tutorial first.
:::
:::note Tested Deployments
This browser demo was tested in the following environments:
| Browser | Date |
|:------------|:-----------|
| Chrome 119 | 2024-01-06 |
:::
## Data Representations
Many worksheets include one header row followed by a number of data rows. Each
row is an "observation" and each column is a "variable".
:::info pass
The "Array of Objects" explanations use more idiomatic JavaScript patterns. It
is suitable for smaller datasets.
The "Dense Worksheets" approach is more performant, but the code patterns are
reminiscent of C. The low-level approach is only encouraged when the traditional
patterns are prohibitively slow.
:::
### Arrays of Objects
The idiomatic JavaScript representation of the dataset is an array of objects.
Variable names are typically taken from the first row. Those names are used as
keys in each observation.
<table><thead><tr><th>Spreadsheet</th><th>JS Data</th></tr></thead><tbody><tr><td>
![`pres.xlsx` data](pathname:///pres.png)
</td><td>
```js
[
{ Name: "Bill Clinton", Index: 42 },
{ Name: "GeorgeW Bush", Index: 43 },
{ Name: "Barack Obama", Index: 44 },
{ Name: "Donald Trump", Index: 45 },
{ Name: "Joseph Biden", Index: 46 }
]
```
</td></tr></tbody></table>
The SheetJS `sheet_to_json` method[^1] can generate arrays of objects from a
worksheet object. For example, the following snippet fetches a test file and
creates an array of arrays from the first sheet:
```js
const url = "https://docs.sheetjs.com/typedarray/iris.xlsx";
/* fetch file and pull file data into an ArrayBuffer */
const file = await (await fetch(url)).arrayBuffer();
/* parse workbook */
const workbook = XLSX.read(file, {dense: true});
/* first worksheet */
const first_sheet = workbook.Sheets[workbook.SheetNames[0]];
/* generate array of arrays */
// highlight-next-line
const aoo = XLSX.utils.sheet_to_json(first_sheet);
```
### Dense Worksheets
SheetJS "dense" worksheets[^2] store cells in an array of arrays. The SheetJS
`read` method[^3] accepts a special `dense` option to create dense worksheets.
The following example fetches a file:
```js
/* fetch file and pull file data into an ArrayBuffer */
const url = "https://docs.sheetjs.com/typedarray/iris.xlsx";
const file = await (await fetch(url)).arrayBuffer();
/* parse workbook */
// highlight-next-line
const workbook = XLSX.read(file, {dense: true});
/* first worksheet */
const first_dense_sheet = workbook.Sheets[workbook.SheetNames[0]];
```
The `"!data"` property of a dense worksheet is an array of arrays of cell
objects[^4]. Cell objects include attributes including data type and value.
## Analyzing Variables
Individual variables can be extracted by looping through the array of objects
and accessing specific keys. For example, using the Iris dataset:
![Iris dataset](pathname:///typedarray/iris.png)
<Tabs groupId="style">
<TabItem name="aoo" value="Array of Objects">
The following snippet shows the first entry in the array of objects:
```js
{
"sepal length": 5.1,
"sepal width": 3.5,
"petal length": 1.4,
"petal width": 0.2,
"class ": "Iris-setosa"
}
```
The values for the `sepal length` variable can be extracted by indexing each
object. The following snippet prints the sepal lengths:
```js
for(let i = 0; i < aoo.length; ++i) {
const row = aoo[i];
const sepal_length = row["sepal length"];
console.log(sepal_length);
}
```
```jsx live
function SheetJSAoOExtractColumn() {
const [col, setCol] = React.useState([]);
React.useEffect(() => { (async() => {
const ab = await (await fetch("/typedarray/iris.xlsx")).arrayBuffer();
const wb = XLSX.read(ab, {dense: true});
const aoo = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
/* store first 5 sepal lengths in an array */
const col = [];
for(let i = 0; i < aoo.length; ++i) {
const row = aoo[i];
const sepal_length = row["sepal length"];
col.push(sepal_length); if(col.length >= 5) break;
}
setCol(col);
})(); }, []);
return ( <>
<b>First 5 Sepal Length Values</b><br/>
<table><tbody>
{col.map(sw => (<tr><td>{sw}</td></tr>))}
</tbody></table>
</>
);
}
```
</TabItem>
<TabItem name="ws" value="Dense Worksheet">
The column for the `sepal length` variable can be determined by testing the cell
values in the first row.
**Finding the column index for the variable**
The first row of cells will be the first row in the `"!data"` array:
```js
const first_row = first_dense_sheet["!data"][0];
```
When looping over the cells in the first row, the cell must be tested in the
following order:
- confirm the cell object exists (entry is not null)
- cell is a text cell (the `t` property will be `"s"`[^5])
- cell value (`v` property[^6]) matches `"sepal length"`
```js
let C = -1;
for(let i = 0; i < first_row.length; ++i) {
let cell = first_row[i];
/* confirm cell exists */
if(!cell) continue;
/* confirm cell is a text cell */
if(cell.t != "s") continue;
/* compare the text */
if(cell.v.localeCompare("sepal length") != 0) continue;
/* save column index */
C = i; break;
}
/* throw an error if the column cannot be found */
if(C == -1) throw new Error(`"sepal length" column cannot be found! `);
```
**Finding the values for the variable**
After finding the column index, the rest of the rows can be scanned. This time,
the cell type will be `"n"`[^7] (numeric). The following snippet prints values:
```js
const number_of_rows = first_dense_sheet["!data"].length;
for(let R = 1; R < number_of_rows; ++R) {
/* confirm row exists */
let row = first_dense_sheet["!data"][R];
if(!row) continue;
/* confirm cell exists */
let cell = row[C];
if(!cell) continue;
/* confirm cell is a numeric cell */
if(cell.t != "n") continue;
/* print raw value */
console.log(cell.v);
}
```
**Live Demo**
The following snippet prints the sepal lengths:
```js
for(let i = 0; i < aoo.length; ++i) {
const row = aoo[i];
const sepal_length = row["sepal length"];
console.log(sepal_length);
}
```
```jsx live
function SheetJSDensExtractColumn() {
const [msg, setMsg] = React.useState("");
const [col, setCol] = React.useState([]);
React.useEffect(() => { (async() => {
const ab = await (await fetch("/typedarray/iris.xlsx")).arrayBuffer();
const wb = XLSX.read(ab, {dense: true});
/* first worksheet */
const first_dense_sheet = wb.Sheets[wb.SheetNames[0]];
/* find column index */
const first_row = first_dense_sheet["!data"][0];
let C = -1;
for(let i = 0; i < first_row.length; ++i) {
let cell = first_row[i];
/* confirm cell exists */
if(!cell) continue;
/* confirm cell is a text cell */
if(cell.t != "s") continue;
/* compare the text */
if(cell.v.localeCompare("sepal length") != 0) continue;
/* save column index */
C = i; break;
}
/* throw an error if the column cannot be found */
if(C == -1) return setMsg(`"sepal length" column cannot be found! `);
/* store first 5 sepal lengths in an array */
const col = [];
const number_of_rows = first_dense_sheet["!data"].length;
for(let R = 1; R < number_of_rows; ++R) {
/* confirm row exists */
let row = first_dense_sheet["!data"][R];
if(!row) continue;
/* confirm cell exists */
let cell = row[C];
if(!cell) continue;
/* confirm cell is a numeric cell */
if(cell.t != "n") continue;
/* add raw value */
const sepal_length = cell.v;
col.push(sepal_length); if(col.length >= 5) break;
}
setCol(col);
setMsg("First 5 Sepal Length Values");
})(); }, []);
return ( <><b>{msg}</b><br/><table><tbody>
{col.map(sw => (<tr><td>{sw}</td></tr>))}
</tbody></table></> );
}
```
</TabItem>
</Tabs>
## Average (Mean)
For a given sequence of numbers $x_1\mathellipsis x_{count}$ the mean $M$ is
defined as the sum of the elements divided by the count:
$$
M[x;count] = \frac{1}{count}\sum_{i=1}^{count} x_i
$$
In JavaScript terms, the mean of an array is the sum of the numbers in the array
divided by the total number of numeric values.
Non-numeric elements and array holes do not affect the sum and do not contribute
to the count. Algorithms are expected to explicitly track the count and cannot
assume the array `length` property will be the correct count.
:::info pass
This definition aligns with the spreadsheet `AVERAGE` function.
`AVERAGEA` differs from `AVERAGE` in its treatment of string and Boolean values:
string values are treated as zeroes and Boolean values map to their coerced
numeric equivalent (`true` is `1` and `false` is `0`).
:::
:::note JavaScript Ecosystem
Some JavaScript libraries implement functions for computing array means.
| Library | Implementation |
|:------------------------|:----------------------------------------------|
| `jStat`[^8] | Textbook sum (divide at end) |
| `simple-statistics`[^9] | Neumaier compensated sum (divide at end) |
| `stdlib.js`[^10] | Trial mean (`mean`) / van Reeken (`incrmean`) |
:::
### Textbook Sum
The mean of a sequence of values can be calculated by computing the sum and
dividing by the count.
<Tabs groupId="style">
<TabItem name="aoo" value="Array of Objects">
The following function accepts an array of objects and a key.
```js
function aoa_average_of_key(aoo, key) {
let sum = 0, cnt = 0;
for(let R = 0; R < aoo.length; ++R) {
const row = aoo[R];
if(typeof row == "undefined") continue;
const field = row[key];
if(typeof field != "number") continue;
sum += field; ++cnt;
}
return cnt == 0 ? 0 : sum / cnt;
}
```
<details><summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSAoOAverageKey() {
const [avg, setAvg] = React.useState(NaN);
function aoa_average_of_key(aoo, key) {
let sum = 0, cnt = 0;
for(let R = 0; R < aoo.length; ++R) {
const row = aoo[R];
if(typeof row == "undefined") continue;
const field = row[key];
if(typeof field != "number") continue;
sum += field; ++cnt;
}
return cnt == 0 ? 0 : sum / cnt;
}
React.useEffect(() => { (async() => {
const ab = await (await fetch("/typedarray/iris.xlsx")).arrayBuffer();
const wb = XLSX.read(ab, {dense: true});
const aoo = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
setAvg(aoa_average_of_key(aoo, "sepal length"));
})(); }, []);
return ( <b>The average Sepal Length is {avg}</b> );
}
```
</details>
</TabItem>
<TabItem name="ws" value="Dense Worksheet">
The following function accepts a SheetJS worksheet and a column index.
```js
function ws_average_of_col(ws, C) {
const data = ws["!data"];
let sum = 0, cnt = 0;
for(let R = 1; R < data.length; ++R) {
const row = data[R];
if(typeof row == "undefined") continue;
const field = row[C];
if(!field || field.t != "n") continue;
sum += field.v; ++cnt;
}
return cnt == 0 ? 0 : sum / cnt;
}
```
<details><summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSDenseAverageKey() {
const [avg, setAvg] = React.useState(NaN);
function ws_average_of_col(ws, C) {
const data = ws["!data"];
let sum = 0, cnt = 0;
for(let R = 1; R < data.length; ++R) {
const row = data[R];
if(typeof row == "undefined") continue;
const field = row[C];
if(!field || field.t != "n") continue;
sum += field.v; ++cnt;
}
return cnt == 0 ? 0 : sum / cnt;
}
React.useEffect(() => { (async() => {
const ab = await (await fetch("/typedarray/iris.xlsx")).arrayBuffer();
const wb = XLSX.read(ab, {dense: true});
const ws = wb.Sheets[wb.SheetNames[0]];
/* find column index */
const first_row = ws["!data"][0];
let C = -1;
for(let i = 0; i < first_row.length; ++i) {
let cell = first_row[i];
/* confirm cell exists */
if(!cell) continue;
/* confirm cell is a text cell */
if(cell.t != "s") continue;
/* compare the text */
if(cell.v.localeCompare("sepal length") != 0) continue;
/* save column index */
C = i; break;
}
setAvg(ws_average_of_col(ws, C));
})(); }, []);
return ( <b>The average Sepal Length is {avg}</b> );
}
```
</details>
</TabItem>
</Tabs>
:::caution pass
The textbook method suffers from numerical issues when many values of similar
magnitude are summed. As the number of elements grows, the absolute value of the
sum grows to orders of magnitude larger than the absolute values of the
individual values and significant figures are lost.
:::
### van Reeken
Some of the issues in the textbook approach can be addressed with a differential
technique. Instead of computing the whole sum, it is possible to calculate and
update an estimate for the mean.
The van Reeken array mean can be implemented in one line of JavaScript code:
```js
for(var n = 1, mean = 0; n <= x.length; ++n) mean += (x[n-1] - mean)/n;
```
<details><summary><b>Math details</b> (click to show)</summary>
Let $M[x;m] = \frac{1}{m}\sum_{i=1}^{m}x_m$ be the mean of the first $m$ elements. Then:
<table style={bs}><tbody style={bs}><tr style={bs}><td style={bs}>
$M[x;m+1]$
</td><td style={bs}>
$= \frac{1}{m+1}\sum_{i=1}^{m+1} x_i$
</td></tr><tr style={bs}><td style={bs}>&nbsp;</td><td style={bs}>
$= \frac{1}{m+1}\sum_{i=1}^{m} x_i + \frac{x_{m+1}}{m+1}$
</td></tr><tr style={bs}><td style={bs}>&nbsp;</td><td style={bs}>
$= \frac{m}{m+1}(\frac{1}{m}\sum_{i=1}^{m} x_i) + \frac{x_{m+1}}{m+1}$
</td></tr><tr style={bs}><td style={bs}>&nbsp;</td><td style={bs}>
$= \frac{m}{m+1}M[x;m] + \frac{x_{m+1}}{m+1}$
</td></tr><tr style={bs}><td style={bs}>&nbsp;</td><td style={bs}>
$= (1 - \frac{1}{m+1})M[x;m] + \frac{x_{m+1}}{m+1}$
</td></tr><tr style={bs}><td style={bs}>&nbsp;</td><td style={bs}>
$= M[x;m] + \frac{x_{m+1}}{m+1} - \frac{1}{m+1}M[x;m]$
</td></tr><tr style={bs}><td style={bs}>&nbsp;</td><td style={bs}>
$= M[x;m] + \frac{1}{m+1}(x_{m+1}-M[x;m])$
</td></tr><tr style={bs}><td style={bs}>
$new\_mean$
</td><td style={bs}>
$= old\_mean + (x_{m+1}-old\_mean) / (m+1)$
</td></tr></tbody></table>
Switching to zero-based indexing, the relation matches the following expression:
```js
new_mean = old_mean + (x[m] - old_mean) / (m + 1);
```
This update can be succinctly implemented in JavaScript:
```js
mean += (x[m] - mean) / (m + 1);
```
</details>
<Tabs groupId="style">
<TabItem name="aoo" value="Array of Objects">
The following function accepts an array of objects and a key.
```js
function aoa_mean_of_key(aoo, key) {
let mean = 0, cnt = 0;
for(let R = 0; R < aoo.length; ++R) {
const row = aoo[R];
if(typeof row == "undefined") continue;
const field = row[key];
if(typeof field != "number") continue;
mean += (field - mean) / ++cnt;
}
return cnt == 0 ? 0 : mean;
}
```
<details><summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSAoOMeanKey() {
const [avg, setAvg] = React.useState(NaN);
function aoa_mean_of_key(aoo, key) {
let mean = 0, cnt = 0;
for(let R = 0; R < aoo.length; ++R) {
const row = aoo[R];
if(typeof row == "undefined") continue;
const field = row[key];
if(typeof field != "number") continue;
mean += (field - mean) / ++cnt;
}
return cnt == 0 ? 0 : mean;
}
React.useEffect(() => { (async() => {
const ab = await (await fetch("/typedarray/iris.xlsx")).arrayBuffer();
const wb = XLSX.read(ab, {dense: true});
const aoo = XLSX.utils.sheet_to_json(wb.Sheets[wb.SheetNames[0]]);
setAvg(aoa_mean_of_key(aoo, "sepal length"));
})(); }, []);
return ( <b>The average Sepal Length is {avg}</b> );
}
```
</details>
</TabItem>
<TabItem name="ws" value="Dense Worksheet">
The following function accepts a SheetJS worksheet and a column index.
```js
function ws_mean_of_col(ws, C) {
const data = ws["!data"];
let mean = 0, cnt = 0;
for(let R = 1; R < data.length; ++R) {
const row = data[R];
if(typeof row == "undefined") continue;
const field = row[C];
if(!field || field.t != "n") continue;
mean += (field.v - mean) / ++cnt;
}
return cnt == 0 ? 0 : mean;
}
```
<details><summary><b>Live Demo</b> (click to show)</summary>
```jsx live
function SheetJSDenseMeanKey() {
const [avg, setAvg] = React.useState(NaN);
function ws_mean_of_col(ws, C) {
const data = ws["!data"];
let mean = 0, cnt = 0;
for(let R = 1; R < data.length; ++R) {
const row = data[R];
if(typeof row == "undefined") continue;
const field = row[C];
if(!field || field.t != "n") continue;
mean += (field.v - mean) / ++cnt;
}
return cnt == 0 ? 0 : mean;
}
React.useEffect(() => { (async() => {
const ab = await (await fetch("/typedarray/iris.xlsx")).arrayBuffer();
const wb = XLSX.read(ab, {dense: true});
const ws = wb.Sheets[wb.SheetNames[0]];
/* find column index */
const first_row = ws["!data"][0];
let C = -1;
for(let i = 0; i < first_row.length; ++i) {
let cell = first_row[i];
/* confirm cell exists */
if(!cell) continue;
/* confirm cell is a text cell */
if(cell.t != "s") continue;
/* compare the text */
if(cell.v.localeCompare("sepal length") != 0) continue;
/* save column index */
C = i; break;
}
setAvg(ws_mean_of_col(ws, C));
})(); }, []);
return ( <b>The average Sepal Length is {avg}</b> );
}
```
</details>
</TabItem>
</Tabs>
:::note Historical Context
This algorithm is generally attributed to Welford[^11]. However, the original
paper does not propose this algorithm for calculating the mean!
Programmers including Neely[^12] attributed a different algorithm to Welford.
van Reeken[^13] reported success with the algorithm presented in this section.
Knuth[^14] erroneously attributed this implementation of the mean to Welford.
:::
[^1]: See [`sheet_to_json` in "Utilities"](/docs/api/utilities/array#array-output)
[^2]: See ["Dense Mode" in "Utilities"](/docs/csf/sheet#dense-mode)
[^3]: See [`read` in "Reading Files"](/docs/api/parse-options)
[^4]: See ["Dense Mode" in "Utilities"](/docs/csf/sheet#dense-mode)
[^5]: See ["Cell Types" in "Cell Objects"](/docs/csf/cell#cell-types)
[^6]: See ["Underlying Values" in "Cell Objects"](/docs/csf/cell#underlying-values)
[^7]: See ["Cell Types" in "Cell Objects"](/docs/csf/cell#cell-types)
[^8]: See [`mean()`](https://jstat.github.io/all.html#mean) in the `jStat` documentation.
[^9]: See [`mean`](http://simple-statistics.github.io/docs/#mean) in the `simple-statistics` documentation.
[^10]: See [`incrsum`](https://stdlib.io/docs/api/latest/@stdlib/stats/incr/sum) in the `stdlib.js` documentation.
[^11]: See "Note on a Method for Calculated Corrected Sums of Squares and Products" in Technometrics Vol 4 No 3 (1962 August).
[^12]: See "Comparison of Several Algorithms for Computation of Means, Standard Deviations and Correlation Coefficients" in CACM Vol 9 No 7 (1966 July).
[^13]: See "Dealing with Neely's Algorithms" in CACM Vol 11 No 3 (1968 March).
[^14]: See "The Art of Computer Programming: Seminumerical Algorithms" Third Edition page 232.

@ -378,7 +378,7 @@ func azure functionapp publish FUNCTION_NAME
After publishing, the process will print the "Invoke url":
```
```text pass
Functions in sheetjsazure:
SheetJSAzure - [httpTrigger]
// highlight-next-line

@ -4,6 +4,9 @@
const lightCodeTheme = require('prism-react-renderer/themes/github');
const darkCodeTheme = require('prism-react-renderer/themes/dracula');
const math = require('remark-math');
const katex = require('rehype-katex');
/** @type {import('@docusaurus/types').Config} */
const config = {
title: 'SheetJS Community Edition',
@ -31,6 +34,8 @@ const config = {
sidebarPath: require.resolve('./sidebars.js'),
showLastUpdateTime: true,
editUrl: 'https://git.sheetjs.com/sheetjs/docs.sheetjs.com/src/branch/master/docz',
remarkPlugins: [math],
rehypePlugins: [katex /* , { strict: false } */],
},
//blog: {
// showReadingTime: true,
@ -160,6 +165,13 @@ const config = {
async: true
}
],
stylesheets: [
{
href: '/katex/katex.min.css',
type: 'text/css',
crossorigin: 'anonymous',
},
],
plugins: [
require.resolve("@cmfcmf/docusaurus-search-local"),
[ '@docusaurus/plugin-client-redirects', {

@ -26,6 +26,8 @@
"prism-react-renderer": "1.3.5",
"react": "17.0.2",
"react-dom": "17.0.2",
"rehype-katex": "4.0.0",
"remark-math": "3.0.1",
"xlsx": "https://cdn.sheetjs.com/xlsx-0.20.1/xlsx-0.20.1.tgz"
},
"devDependencies": {

@ -0,0 +1,91 @@
# [<img src="https://katex.org/img/katex-logo-black.svg" width="130" alt="KaTeX">](https://katex.org/)
[![npm](https://img.shields.io/npm/v/katex.svg)](https://www.npmjs.com/package/katex)
[![CircleCI](https://circleci.com/gh/KaTeX/KaTeX.svg?style=shield)](https://circleci.com/gh/KaTeX/KaTeX)
[![codecov](https://codecov.io/gh/KaTeX/KaTeX/branch/master/graph/badge.svg)](https://codecov.io/gh/KaTeX/KaTeX)
[![Join the chat at https://gitter.im/KaTeX/KaTeX](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/KaTeX/KaTeX?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Dependabot Status](https://api.dependabot.com/badges/status?host=github&repo=KaTeX/KaTeX)](https://dependabot.com)
[![jsDelivr](https://data.jsdelivr.com/v1/package/npm/katex/badge?style=rounded)](https://www.jsdelivr.com/package/npm/katex)
![](https://img.badgesize.io/KaTeX/KaTeX/v0.12.0/dist/katex.min.js?compression=gzip)
KaTeX is a fast, easy-to-use JavaScript library for TeX math rendering on the web.
* **Fast:** KaTeX renders its math synchronously and doesn't need to reflow the page. See how it compares to a competitor in [this speed test](http://www.intmath.com/cg5/katex-mathjax-comparison.php).
* **Print quality:** KaTeX's layout is based on Donald Knuth's TeX, the gold standard for math typesetting.
* **Self contained:** KaTeX has no dependencies and can easily be bundled with your website resources.
* **Server side rendering:** KaTeX produces the same output regardless of browser or environment, so you can pre-render expressions using Node.js and send them as plain HTML.
KaTeX is compatible with all major browsers, including Chrome, Safari, Firefox, Opera, Edge, and IE 11.
KaTeX supports much (but not all) of LaTeX and many LaTeX packages. See the [list of supported functions](https://katex.org/docs/supported.html).
Try out KaTeX [on the demo page](https://katex.org/#demo)!
## Getting started
### Starter template
```html
<!DOCTYPE html>
<!-- KaTeX requires the use of the HTML5 doctype. Without it, KaTeX may not render properly -->
<html>
<head>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.css" integrity="sha384-AfEj0r4/OFrOo5t7NnNe46zW/tFgW6x/bCJG8FqQCEo3+Aro6EYUG4+cU+KJWu/X" crossorigin="anonymous">
<!-- The loading of KaTeX is deferred to speed up page rendering -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/katex.min.js" integrity="sha384-g7c+Jr9ZivxKLnZTDUhnkOnsh30B4H0rpLUpJ4jAIKs4fnJI+sEnkvrMWph2EDg4" crossorigin="anonymous"></script>
<!-- To automatically render math in text elements, include the auto-render extension: -->
<script defer src="https://cdn.jsdelivr.net/npm/katex@0.12.0/dist/contrib/auto-render.min.js" integrity="sha384-mll67QQFJfxn0IYznZYonOWZ644AWYC+Pt2cHqMaRhXVrursRwvLnLaebdGIlYNa" crossorigin="anonymous"
onload="renderMathInElement(document.body);"></script>
</head>
...
</html>
```
You can also [download KaTeX](https://github.com/KaTeX/KaTeX/releases) and host it yourself.
For details on how to configure auto-render extension, refer to [the documentation](https://katex.org/docs/autorender.html).
### API
Call `katex.render` to render a TeX expression directly into a DOM element.
For example:
```js
katex.render("c = \\pm\\sqrt{a^2 + b^2}", element, {
throwOnError: false
});
```
Call `katex.renderToString` to generate an HTML string of the rendered math,
e.g., for server-side rendering. For example:
```js
var html = katex.renderToString("c = \\pm\\sqrt{a^2 + b^2}", {
throwOnError: false
});
// '<span class="katex">...</span>'
```
Make sure to include the CSS and font files in both cases.
If you are doing all rendering on the server, there is no need to include the
JavaScript on the client.
The examples above use the `throwOnError: false` option, which renders invalid
inputs as the TeX source code in red (by default), with the error message as
hover text. For other available options, see the
[API documentation](https://katex.org/docs/api.html),
[options documentation](https://katex.org/docs/options.html), and
[handling errors documentation](https://katex.org/docs/error.html).
## Demo and Documentation
Learn more about using KaTeX [on the website](https://katex.org)!
## Contributing
See [CONTRIBUTING.md](CONTRIBUTING.md)
## License
KaTeX is licensed under the [MIT License](http://opensource.org/licenses/MIT).

@ -0,0 +1,350 @@
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory(require("katex"));
else if(typeof define === 'function' && define.amd)
define(["katex"], factory);
else if(typeof exports === 'object')
exports["renderMathInElement"] = factory(require("katex"));
else
root["renderMathInElement"] = factory(root["katex"]);
})((typeof self !== 'undefined' ? self : this), function(__WEBPACK_EXTERNAL_MODULE__0__) {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;
/******/ }
/******/ // Create a new module (and put it into the cache)
/******/ var module = installedModules[moduleId] = {
/******/ i: moduleId,
/******/ l: false,
/******/ exports: {}
/******/ };
/******/
/******/ // Execute the module function
/******/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__);
/******/
/******/ // Flag the module as loaded
/******/ module.l = true;
/******/
/******/ // Return the exports of the module
/******/ return module.exports;
/******/ }
/******/
/******/
/******/ // expose the modules object (__webpack_modules__)
/******/ __webpack_require__.m = modules;
/******/
/******/ // expose the module cache
/******/ __webpack_require__.c = installedModules;
/******/
/******/ // define getter function for harmony exports
/******/ __webpack_require__.d = function(exports, name, getter) {
/******/ if(!__webpack_require__.o(exports, name)) {
/******/ Object.defineProperty(exports, name, { enumerable: true, get: getter });
/******/ }
/******/ };
/******/
/******/ // define __esModule on exports
/******/ __webpack_require__.r = function(exports) {
/******/ if(typeof Symbol !== 'undefined' && Symbol.toStringTag) {
/******/ Object.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });
/******/ }
/******/ Object.defineProperty(exports, '__esModule', { value: true });
/******/ };
/******/
/******/ // create a fake namespace object
/******/ // mode & 1: value is a module id, require it
/******/ // mode & 2: merge all properties of value into the ns
/******/ // mode & 4: return value when already ns object
/******/ // mode & 8|1: behave like require
/******/ __webpack_require__.t = function(value, mode) {
/******/ if(mode & 1) value = __webpack_require__(value);
/******/ if(mode & 8) return value;
/******/ if((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;
/******/ var ns = Object.create(null);
/******/ __webpack_require__.r(ns);
/******/ Object.defineProperty(ns, 'default', { enumerable: true, value: value });
/******/ if(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));
/******/ return ns;
/******/ };
/******/
/******/ // getDefaultExport function for compatibility with non-harmony modules
/******/ __webpack_require__.n = function(module) {
/******/ var getter = module && module.__esModule ?
/******/ function getDefault() { return module['default']; } :
/******/ function getModuleExports() { return module; };
/******/ __webpack_require__.d(getter, 'a', getter);
/******/ return getter;
/******/ };
/******/
/******/ // Object.prototype.hasOwnProperty.call
/******/ __webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
/******/
/******/ // __webpack_public_path__
/******/ __webpack_require__.p = "";
/******/
/******/
/******/ // Load entry module and return exports
/******/ return __webpack_require__(__webpack_require__.s = 1);
/******/ })
/************************************************************************/
/******/ ([
/* 0 */
/***/ (function(module, exports) {
module.exports = __WEBPACK_EXTERNAL_MODULE__0__;
/***/ }),
/* 1 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
// EXTERNAL MODULE: external "katex"
var external_katex_ = __webpack_require__(0);
var external_katex_default = /*#__PURE__*/__webpack_require__.n(external_katex_);
// CONCATENATED MODULE: ./contrib/auto-render/splitAtDelimiters.js
/* eslint no-constant-condition:0 */
var findEndOfMath = function findEndOfMath(delimiter, text, startIndex) {
// Adapted from
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
var index = startIndex;
var braceLevel = 0;
var delimLength = delimiter.length;
while (index < text.length) {
var character = text[index];
if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) {
return index;
} else if (character === "\\") {
index++;
} else if (character === "{") {
braceLevel++;
} else if (character === "}") {
braceLevel--;
}
index++;
}
return -1;
};
var splitAtDelimiters = function splitAtDelimiters(startData, leftDelim, rightDelim, display) {
var finalData = [];
for (var i = 0; i < startData.length; i++) {
if (startData[i].type === "text") {
var text = startData[i].data;
var lookingForLeft = true;
var currIndex = 0;
var nextIndex = void 0;
nextIndex = text.indexOf(leftDelim);
if (nextIndex !== -1) {
currIndex = nextIndex;
finalData.push({
type: "text",
data: text.slice(0, currIndex)
});
lookingForLeft = false;
}
while (true) {
if (lookingForLeft) {
nextIndex = text.indexOf(leftDelim, currIndex);
if (nextIndex === -1) {
break;
}
finalData.push({
type: "text",
data: text.slice(currIndex, nextIndex)
});
currIndex = nextIndex;
} else {
nextIndex = findEndOfMath(rightDelim, text, currIndex + leftDelim.length);
if (nextIndex === -1) {
break;
}
finalData.push({
type: "math",
data: text.slice(currIndex + leftDelim.length, nextIndex),
rawData: text.slice(currIndex, nextIndex + rightDelim.length),
display: display
});
currIndex = nextIndex + rightDelim.length;
}
lookingForLeft = !lookingForLeft;
}
finalData.push({
type: "text",
data: text.slice(currIndex)
});
} else {
finalData.push(startData[i]);
}
}
return finalData;
};
/* harmony default export */ var auto_render_splitAtDelimiters = (splitAtDelimiters);
// CONCATENATED MODULE: ./contrib/auto-render/auto-render.js
/* eslint no-console:0 */
var auto_render_splitWithDelimiters = function splitWithDelimiters(text, delimiters) {
var data = [{
type: "text",
data: text
}];
for (var i = 0; i < delimiters.length; i++) {
var delimiter = delimiters[i];
data = auto_render_splitAtDelimiters(data, delimiter.left, delimiter.right, delimiter.display || false);
}
return data;
};
/* Note: optionsCopy is mutated by this method. If it is ever exposed in the
* API, we should copy it before mutating.
*/
var auto_render_renderMathInText = function renderMathInText(text, optionsCopy) {
var data = auto_render_splitWithDelimiters(text, optionsCopy.delimiters);
if (data.length === 1 && data[0].type === 'text') {
// There is no formula in the text.
// Let's return null which means there is no need to replace
// the current text node with a new one.
return null;
}
var fragment = document.createDocumentFragment();
for (var i = 0; i < data.length; i++) {
if (data[i].type === "text") {
fragment.appendChild(document.createTextNode(data[i].data));
} else {
var span = document.createElement("span");
var math = data[i].data; // Override any display mode defined in the settings with that
// defined by the text itself
optionsCopy.displayMode = data[i].display;
try {
if (optionsCopy.preProcess) {
math = optionsCopy.preProcess(math);
}
external_katex_default.a.render(math, span, optionsCopy);
} catch (e) {
if (!(e instanceof external_katex_default.a.ParseError)) {
throw e;
}
optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e);
fragment.appendChild(document.createTextNode(data[i].rawData));
continue;
}
fragment.appendChild(span);
}
}
return fragment;
};
var renderElem = function renderElem(elem, optionsCopy) {
for (var i = 0; i < elem.childNodes.length; i++) {
var childNode = elem.childNodes[i];
if (childNode.nodeType === 3) {
// Text node
var frag = auto_render_renderMathInText(childNode.textContent, optionsCopy);
if (frag) {
i += frag.childNodes.length - 1;
elem.replaceChild(frag, childNode);
}
} else if (childNode.nodeType === 1) {
(function () {
// Element node
var className = ' ' + childNode.className + ' ';
var shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(function (x) {
return className.indexOf(' ' + x + ' ') === -1;
});
if (shouldRender) {
renderElem(childNode, optionsCopy);
}
})();
} // Otherwise, it's something else, and ignore it.
}
};
var renderMathInElement = function renderMathInElement(elem, options) {
if (!elem) {
throw new Error("No element provided to render");
}
var optionsCopy = {}; // Object.assign(optionsCopy, option)
for (var option in options) {
if (options.hasOwnProperty(option)) {
optionsCopy[option] = options[option];
}
} // default options
optionsCopy.delimiters = optionsCopy.delimiters || [{
left: "$$",
right: "$$",
display: true
}, {
left: "\\(",
right: "\\)",
display: false
}, // LaTeX uses $…$, but it ruins the display of normal `$` in text:
// {left: "$", right: "$", display: false},
// \[…\] must come last in this array. Otherwise, renderMathInElement
// will search for \[ before it searches for $$ or \(
// That makes it susceptible to finding a \\[0.3em] row delimiter and
// treating it as if it were the start of a KaTeX math zone.
{
left: "\\[",
right: "\\]",
display: true
}];
optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"];
optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different
// math elements within a single call to `renderMathInElement`.
optionsCopy.macros = optionsCopy.macros || {};
renderElem(elem, optionsCopy);
};
/* harmony default export */ var auto_render = __webpack_exports__["default"] = (renderMathInElement);
/***/ })
/******/ ])["default"];
});

@ -0,0 +1 @@
!function(e,t){"object"==typeof exports&&"object"==typeof module?module.exports=t(require("katex")):"function"==typeof define&&define.amd?define(["katex"],t):"object"==typeof exports?exports.renderMathInElement=t(require("katex")):e.renderMathInElement=t(e.katex)}("undefined"!=typeof self?self:this,function(e){return function(e){var t={};function r(n){if(t[n])return t[n].exports;var o=t[n]={i:n,l:!1,exports:{}};return e[n].call(o.exports,o,o.exports,r),o.l=!0,o.exports}return r.m=e,r.c=t,r.d=function(e,t,n){r.o(e,t)||Object.defineProperty(e,t,{enumerable:!0,get:n})},r.r=function(e){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(e,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(e,"__esModule",{value:!0})},r.t=function(e,t){if(1&t&&(e=r(e)),8&t)return e;if(4&t&&"object"==typeof e&&e&&e.__esModule)return e;var n=Object.create(null);if(r.r(n),Object.defineProperty(n,"default",{enumerable:!0,value:e}),2&t&&"string"!=typeof e)for(var o in e)r.d(n,o,function(t){return e[t]}.bind(null,o));return n},r.n=function(e){var t=e&&e.__esModule?function(){return e.default}:function(){return e};return r.d(t,"a",t),t},r.o=function(e,t){return Object.prototype.hasOwnProperty.call(e,t)},r.p="",r(r.s=1)}([function(t,r){t.exports=e},function(e,t,r){"use strict";r.r(t);var n=r(0),o=r.n(n),a=function(e,t,r){for(var n=r,o=0,a=e.length;n<t.length;){var i=t[n];if(o<=0&&t.slice(n,n+a)===e)return n;"\\"===i?n++:"{"===i?o++:"}"===i&&o--,n++}return-1},i=function(e,t,r,n){for(var o=[],i=0;i<e.length;i++)if("text"===e[i].type){var l=e[i].data,d=!0,s=0,f=void 0;for(-1!==(f=l.indexOf(t))&&(s=f,o.push({type:"text",data:l.slice(0,s)}),d=!1);;){if(d){if(-1===(f=l.indexOf(t,s)))break;o.push({type:"text",data:l.slice(s,f)}),s=f}else{if(-1===(f=a(r,l,s+t.length)))break;o.push({type:"math",data:l.slice(s+t.length,f),rawData:l.slice(s,f+r.length),display:n}),s=f+r.length}d=!d}o.push({type:"text",data:l.slice(s)})}else o.push(e[i]);return o},l=function(e,t){var r=function(e,t){for(var r=[{type:"text",data:e}],n=0;n<t.length;n++){var o=t[n];r=i(r,o.left,o.right,o.display||!1)}return r}(e,t.delimiters);if(1===r.length&&"text"===r[0].type)return null;for(var n=document.createDocumentFragment(),a=0;a<r.length;a++)if("text"===r[a].type)n.appendChild(document.createTextNode(r[a].data));else{var l=document.createElement("span"),d=r[a].data;t.displayMode=r[a].display;try{t.preProcess&&(d=t.preProcess(d)),o.a.render(d,l,t)}catch(e){if(!(e instanceof o.a.ParseError))throw e;t.errorCallback("KaTeX auto-render: Failed to parse `"+r[a].data+"` with ",e),n.appendChild(document.createTextNode(r[a].rawData));continue}n.appendChild(l)}return n};t.default=function(e,t){if(!e)throw new Error("No element provided to render");var r={};for(var n in t)t.hasOwnProperty(n)&&(r[n]=t[n]);r.delimiters=r.delimiters||[{left:"$$",right:"$$",display:!0},{left:"\\(",right:"\\)",display:!1},{left:"\\[",right:"\\]",display:!0}],r.ignoredTags=r.ignoredTags||["script","noscript","style","textarea","pre","code","option"],r.ignoredClasses=r.ignoredClasses||[],r.errorCallback=r.errorCallback||console.error,r.macros=r.macros||{},function e(t,r){for(var n=0;n<t.childNodes.length;n++){var o=t.childNodes[n];if(3===o.nodeType){var a=l(o.textContent,r);a&&(n+=a.childNodes.length-1,t.replaceChild(a,o))}else 1===o.nodeType&&function(){var t=" "+o.className+" ";-1===r.ignoredTags.indexOf(o.nodeName.toLowerCase())&&r.ignoredClasses.every(function(e){return-1===t.indexOf(" "+e+" ")})&&e(o,r)}()}}(e,r)}}]).default});

@ -0,0 +1,226 @@
import katex from '../katex.mjs';
/* eslint no-constant-condition:0 */
const findEndOfMath = function findEndOfMath(delimiter, text, startIndex) {
// Adapted from
// https://github.com/Khan/perseus/blob/master/src/perseus-markdown.jsx
let index = startIndex;
let braceLevel = 0;
const delimLength = delimiter.length;
while (index < text.length) {
const character = text[index];
if (braceLevel <= 0 && text.slice(index, index + delimLength) === delimiter) {
return index;
} else if (character === "\\") {
index++;
} else if (character === "{") {
braceLevel++;
} else if (character === "}") {
braceLevel--;
}
index++;
}
return -1;
};
const splitAtDelimiters = function splitAtDelimiters(startData, leftDelim, rightDelim, display) {
const finalData = [];
for (let i = 0; i < startData.length; i++) {
if (startData[i].type === "text") {
const text = startData[i].data;
let lookingForLeft = true;
let currIndex = 0;
let nextIndex;
nextIndex = text.indexOf(leftDelim);
if (nextIndex !== -1) {
currIndex = nextIndex;
finalData.push({
type: "text",
data: text.slice(0, currIndex)
});
lookingForLeft = false;
}
while (true) {
if (lookingForLeft) {
nextIndex = text.indexOf(leftDelim, currIndex);
if (nextIndex === -1) {
break;
}
finalData.push({
type: "text",
data: text.slice(currIndex, nextIndex)
});
currIndex = nextIndex;
} else {
nextIndex = findEndOfMath(rightDelim, text, currIndex + leftDelim.length);
if (nextIndex === -1) {
break;
}
finalData.push({
type: "math",
data: text.slice(currIndex + leftDelim.length, nextIndex),
rawData: text.slice(currIndex, nextIndex + rightDelim.length),
display: display
});
currIndex = nextIndex + rightDelim.length;
}
lookingForLeft = !lookingForLeft;
}
finalData.push({
type: "text",
data: text.slice(currIndex)
});
} else {
finalData.push(startData[i]);
}
}
return finalData;
};
/* eslint no-console:0 */
const splitWithDelimiters = function splitWithDelimiters(text, delimiters) {
let data = [{
type: "text",
data: text
}];
for (let i = 0; i < delimiters.length; i++) {
const delimiter = delimiters[i];
data = splitAtDelimiters(data, delimiter.left, delimiter.right, delimiter.display || false);
}
return data;
};
/* Note: optionsCopy is mutated by this method. If it is ever exposed in the
* API, we should copy it before mutating.
*/
const renderMathInText = function renderMathInText(text, optionsCopy) {
const data = splitWithDelimiters(text, optionsCopy.delimiters);
if (data.length === 1 && data[0].type === 'text') {
// There is no formula in the text.
// Let's return null which means there is no need to replace
// the current text node with a new one.
return null;
}
const fragment = document.createDocumentFragment();
for (let i = 0; i < data.length; i++) {
if (data[i].type === "text") {
fragment.appendChild(document.createTextNode(data[i].data));
} else {
const span = document.createElement("span");
let math = data[i].data; // Override any display mode defined in the settings with that
// defined by the text itself
optionsCopy.displayMode = data[i].display;
try {
if (optionsCopy.preProcess) {
math = optionsCopy.preProcess(math);
}
katex.render(math, span, optionsCopy);
} catch (e) {
if (!(e instanceof katex.ParseError)) {
throw e;
}
optionsCopy.errorCallback("KaTeX auto-render: Failed to parse `" + data[i].data + "` with ", e);
fragment.appendChild(document.createTextNode(data[i].rawData));
continue;
}
fragment.appendChild(span);
}
}
return fragment;
};
const renderElem = function renderElem(elem, optionsCopy) {
for (let i = 0; i < elem.childNodes.length; i++) {
const childNode = elem.childNodes[i];
if (childNode.nodeType === 3) {
// Text node
const frag = renderMathInText(childNode.textContent, optionsCopy);
if (frag) {
i += frag.childNodes.length - 1;
elem.replaceChild(frag, childNode);
}
} else if (childNode.nodeType === 1) {
// Element node
const className = ' ' + childNode.className + ' ';
const shouldRender = optionsCopy.ignoredTags.indexOf(childNode.nodeName.toLowerCase()) === -1 && optionsCopy.ignoredClasses.every(x => className.indexOf(' ' + x + ' ') === -1);
if (shouldRender) {
renderElem(childNode, optionsCopy);
}
} // Otherwise, it's something else, and ignore it.
}
};
const renderMathInElement = function renderMathInElement(elem, options) {
if (!elem) {
throw new Error("No element provided to render");
}
const optionsCopy = {}; // Object.assign(optionsCopy, option)
for (const option in options) {
if (options.hasOwnProperty(option)) {
optionsCopy[option] = options[option];
}
} // default options
optionsCopy.delimiters = optionsCopy.delimiters || [{
left: "$$",
right: "$$",
display: true
}, {
left: "\\(",
right: "\\)",
display: false
}, // LaTeX uses $…$, but it ruins the display of normal `$` in text:
// {left: "$", right: "$", display: false},
// \[…\] must come last in this array. Otherwise, renderMathInElement
// will search for \[ before it searches for $$ or \(
// That makes it susceptible to finding a \\[0.3em] row delimiter and
// treating it as if it were the start of a KaTeX math zone.
{
left: "\\[",
right: "\\]",
display: true
}];
optionsCopy.ignoredTags = optionsCopy.ignoredTags || ["script", "noscript", "style", "textarea", "pre", "code", "option"];
optionsCopy.ignoredClasses = optionsCopy.ignoredClasses || [];
optionsCopy.errorCallback = optionsCopy.errorCallback || console.error; // Enable sharing of global macros defined via `\gdef` between different
// math elements within a single call to `renderMathInElement`.
optionsCopy.macros = optionsCopy.macros || {};
renderElem(elem, optionsCopy);
};
export default renderMathInElement;

@ -0,0 +1,14 @@
/* Force selection of entire .katex/.katex-display blocks, so that we can
* copy/paste the entire source code. If you omit this CSS, partial
* selections of a formula will work, but will copy the ugly HTML
* representation instead of the LaTeX source code. (Full selections will
* still produce the LaTeX source code.)
*/
.katex,
.katex-display {
user-select: all;
-moz-user-select: all;
-webkit-user-select: all;
-ms-user-select: all;
}

@ -0,0 +1,213 @@
(function webpackUniversalModuleDefinition(root, factory) {
if(typeof exports === 'object' && typeof module === 'object')
module.exports = factory();
else if(typeof define === 'function' && define.amd)
define([], factory);
else {
var a = factory();
for(var i in a) (typeof exports === 'object' ? exports : root)[i] = a[i];
}
})((typeof self !== 'undefined' ? self : this), function() {
return /******/ (function(modules) { // webpackBootstrap
/******/ // The module cache
/******/ var installedModules = {};
/******/
/******/ // The require function
/******/ function __webpack_require__(moduleId) {
/******/
/******/ // Check if module is in cache
/******/ if(installedModules[moduleId]) {
/******/ return installedModules[moduleId].exports;