Skip to content

Commit

Permalink
feat: add support for math blocks using $$
Browse files Browse the repository at this point in the history
  • Loading branch information
OEvgeny committed Nov 15, 2024
1 parent 7f9f47e commit 4f818a4
Show file tree
Hide file tree
Showing 6 changed files with 206 additions and 40 deletions.
125 changes: 125 additions & 0 deletions __tests__/html2/markdown/math5.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
<!doctype html>
<html lang="en-US">

<head>
<link href="/assets/index.css" rel="stylesheet" type="text/css" />
<script crossorigin="anonymous" src="/test-harness.js"></script>
<script crossorigin="anonymous" src="/test-page-object.js"></script>
<script crossorigin="anonymous" src="/__dist__/webchat-es5.js"></script>
<style>
[data-math-type=error] {
color: #9d0000;
border: 1px dashed currentColor;
padding: 0.2em 0.4em;
margin: 0 0.2em;
border-radius: 2px;
}
</style>
</head>

<body>
<template id="messages">
<x-message>
## Display Math with ($$)

1. Basic summation:
$$ \sum\_{n=1}^{\infty} \frac{1}{n^2} = \frac{\pi^2}{6} $$

2. Matrix representation:

$$
\begin{pmatrix}
a & b \\
c & d
\end{pmatrix}
\begin{pmatrix}
x \\
y
\end{pmatrix} =
\begin{pmatrix}
ax + by \\
cx + dy
\end{pmatrix}
$$

3. Equation system:
$$
\begin{cases}
x + y + z = 1 \\
2x - y + z = 3 \\
x + 2y - z = 2
\end{cases}
$$
</x-message>
<x-message>
## Inline Math with \\(...\\)

4. Physics formulas:

- Energy: \(E = mc^2\)
- Force: \(F = ma\)
- Gravitational force: \(F = G\frac{m_1m_2}{r^2}\)

5. Complex numbers:

- Euler's formula: \(e^{ix} = \cos(x) + i\sin(x)\)
- De Moivre's formula: \((\cos\theta + i\sin\theta)^n = \cos(n\theta) + i\sin(n\theta)\)

</x-message>
<x-message>
## Display Math with \\[...\\]

6. Calculus expressions:
\[\lim\_{h \to 0} \frac{f(x + h) - f(x)}{h} = f'(x)\]

7. Double integral:
\[\iint_D \left(\frac{\partial Q}{\partial x} - \frac{\partial P}{\partial y}\right) dx\,dy = \oint_C P\,dx + Q\,dy\]

8. Taylor series:
\[f(x) = \sum\_{n=0}^{\infty} \frac{f^{(n)}(a)}{n!}(x-a)^n\]
</x-message>
<x-message>
## Mixed Usage Examples

9. Quantum mechanics:
The Schrödinger equation \(\hat{H}\Psi = E\Psi\) can be written in position basis as:
$$ -\frac{\hbar^2}{2m}\frac{d^2\Psi}{dx^2} + V(x)\Psi = E\Psi $$

10. Statistics:
If \(X \sim N(\mu, \sigma^2)\), then its probability density function is:
$$ f(x) = \frac{1}{\sigma\sqrt{2\pi}} e^{-\frac{(x-\mu)^2}{2\sigma^2}} $$

11. Linear Algebra:
For a matrix \(A\), its determinant can be computed as:
\[\det(A) = \sum*{\sigma \in S_n} \text{sgn}(\sigma) \prod*{i=1}^n a\_{i,\sigma(i)}\]
</x-message>
</template>
<main id="webchat"></main>
<script>
run(async function () {
await host.windowSize(640, 720, document.getElementById('webchat'));

const {
WebChat: { renderWebChat }
} = window; // Imports in UMD fashion.

const { directLine, store } = testHelpers.createDirectLineEmulator();

renderWebChat({ directLine, store }, document.getElementById('webchat'));

await pageConditions.uiConnected();

const messages = Array.from(window.messages.content.querySelectorAll('x-message')).map(el => el.innerText)
for (const message of messages) {
await directLine.emulateIncomingActivity({
text: message,
type: 'message'
});
await host.snapshot('local');
await pageConditions.numActivitiesShown(messages.indexOf(message) + 1);
}
});
</script>
</body>

</html>
1 change: 1 addition & 0 deletions packages/bundle/src/markdown/mathExtension/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@ export const OPEN_PAREN = 40; // '('
export const CLOSE_PAREN = 41; // ')'
export const OPEN_BRACKET = 91; // '['
export const CLOSE_BRACKET = 93; // ']'
export const DOLLAR = 36; // '$'
19 changes: 2 additions & 17 deletions packages/bundle/src/markdown/mathExtension/index.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,2 @@
import { BACKSLASH } from './constants';
import { createTokenizer } from './tokenizer';
import { type Extension } from 'micromark-util-types';

export function math(): Extension {
const construct = {
name: 'math',
tokenize: createTokenizer
};

return {
text: { [BACKSLASH]: construct },
flow: { [BACKSLASH]: construct }
} as any;
}

export { type CreateHtmlRendererOptions as mathHtmlOptions, default as mathHtml } from './htmlRenderer';
export { default as math } from './math';
export { default as mathHtml, type CreateHtmlRendererOptions as mathHtmlOptions } from './mathHtml';
21 changes: 21 additions & 0 deletions packages/bundle/src/markdown/mathExtension/math.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import type { Extension } from 'micromark-util-types';
import { BACKSLASH, DOLLAR } from './constants';
import { createTokenizer } from './tokenizer';

export default function math(): Extension {
const construct = {
name: 'math',
tokenize: createTokenizer
};

return {
text: {
[BACKSLASH]: construct,
[DOLLAR]: construct
},
flow: {
[BACKSLASH]: construct,
[DOLLAR]: construct
}
} as any;
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,17 +4,25 @@ export type CreateHtmlRendererOptions = {
renderMath?: ((content: string, isDisplay: boolean) => string) | undefined;
};

function extractMathContent(value) {
const isDisplay = value.startsWith('\\[');
const start = value.indexOf(isDisplay ? '[' : '(') + 1;
const end = value.lastIndexOf(isDisplay ? ']' : ')') - 1;
const delimeters = {
PAREN: ['\\(', '\\)'],
BRACKET: ['\\[', '\\]'],
DOLLAR: ['$$', '$$']
} as const;

function extractMathContent(value: string) {
const [mode, [startDelimiter, endDelimiter]] = Object.entries(delimeters).find(([, [start]]) =>
value.startsWith(start)
);
const start = value.indexOf(startDelimiter) + startDelimiter.length;
const end = value.lastIndexOf(endDelimiter);
return {
content: value.slice(start, end).trim(),
isDisplay
content: value.substring(start, end).trim(),
isDisplay: mode === 'BRACKET' || mode === 'DOLLAR'
};
}

export default function createHtmlRenderer(options: CreateHtmlRendererOptions = {}): HtmlExtension {
export default function mathHtml(options: CreateHtmlRendererOptions = {}): HtmlExtension {
return {
exit: {
math(token: Token) {
Expand Down
58 changes: 42 additions & 16 deletions packages/bundle/src/markdown/mathExtension/tokenizer.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
/* eslint-disable no-magic-numbers */
/* eslint-disable @typescript-eslint/no-use-before-define */
import { BACKSLASH, OPEN_PAREN, CLOSE_PAREN, OPEN_BRACKET, CLOSE_BRACKET } from './constants';
import { BACKSLASH, OPEN_PAREN, CLOSE_PAREN, OPEN_BRACKET, CLOSE_BRACKET, DOLLAR } from './constants';
import { markdownLineEnding } from 'micromark-util-character';
import { type Code, type Effects, type State } from 'micromark-util-types';

Expand All @@ -11,36 +12,53 @@ type MathEffects = Omit<Effects, 'enter' | 'exit'> & {
};

export function createTokenizer(effects: MathEffects, ok: State, nok: State) {
let isDisplay = false;
let expectedCloseDelimiter: number;
let dollarDelimiterCount = 0;

return start;

function start(code: Code): State {
if (code !== BACKSLASH) {
return nok(code);
if (code === BACKSLASH || code === DOLLAR) {
effects.enter('math');
effects.enter('mathChunk');
effects.consume(code);
dollarDelimiterCount = code === DOLLAR ? 1 : 0;
return openDelimiter;
}
effects.enter('math');
effects.consume(code);
return openDelimiter;

return nok(code);
}

function openDelimiter(code: Code): State {
if (code === OPEN_PAREN || code === OPEN_BRACKET) {
isDisplay = code === OPEN_BRACKET;
effects.consume(code);
effects.enter('mathChunk');
return content;
switch (code) {
case OPEN_PAREN:
expectedCloseDelimiter = CLOSE_PAREN;
break;
case OPEN_BRACKET:
expectedCloseDelimiter = CLOSE_BRACKET;
break;
case DOLLAR:
expectedCloseDelimiter = DOLLAR;
dollarDelimiterCount++;
if (dollarDelimiterCount !== 2) {
return nok(code);
}
break;
default:
return nok(code);
}
return nok(code);
effects.consume(code);
return content;
}

function content(code: Code): State {
if (code === null) {
return nok(code);
}

if (code === BACKSLASH) {
if (code === BACKSLASH || (dollarDelimiterCount && code === DOLLAR)) {
effects.consume(code);
code === DOLLAR && dollarDelimiterCount--;
return maybeCloseDelimiter;
}

Expand All @@ -55,11 +73,19 @@ export function createTokenizer(effects: MathEffects, ok: State, nok: State) {
}

function maybeCloseDelimiter(code: Code): State {
if ((!isDisplay && code === CLOSE_PAREN) || (isDisplay && code === CLOSE_BRACKET)) {
if (code === expectedCloseDelimiter) {
code === DOLLAR && dollarDelimiterCount--;
if (dollarDelimiterCount !== 0) {
return nok(code);
}

effects.consume(code);
effects.exit('mathChunk');
effects.exit('math');
return ok(code);

dollarDelimiterCount = 0;
expectedCloseDelimiter = undefined;
return ok;
}

return content(code);
Expand Down

0 comments on commit 4f818a4

Please sign in to comment.