@ai-react-markdown/core 1.0.0 → 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +407 -0
- package/dist/index.cjs +147 -120
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +556 -19
- package/dist/index.d.ts +556 -19
- package/dist/index.js +143 -119
- package/dist/index.js.map +1 -1
- package/package.json +42 -14
package/README.md
ADDED
|
@@ -0,0 +1,407 @@
|
|
|
1
|
+
# @ai-react-markdown/core
|
|
2
|
+
|
|
3
|
+
A batteries-included React component for rendering AI-generated markdown with first-class support for LaTeX math, GFM, CJK text, and streaming content.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **GFM** -- tables, strikethrough, task lists, autolinks via `remark-gfm`
|
|
8
|
+
- **LaTeX math** -- inline and display math rendered with KaTeX; smart preprocessing handles currency `$` signs, bracket delimiters (`\[...\]`, `\(...\)`), pipe escaping, and mhchem commands
|
|
9
|
+
- **Emoji** -- shortcode support (`:smile:`) via `remark-emoji`
|
|
10
|
+
- **CJK-friendly** -- proper line breaking and spacing for Chinese, Japanese, and Korean text
|
|
11
|
+
- **Extra syntax** -- highlight (`==text==`), definition lists, superscript/subscript
|
|
12
|
+
- **Display optimizations** -- SmartyPants typography, pangu CJK spacing, HTML comment removal
|
|
13
|
+
- **Streaming-aware** -- built-in `streaming` flag propagated via context for custom components
|
|
14
|
+
- **Customizable** -- swap typography, color scheme, individual markdown element renderers, and inject extra style wrappers
|
|
15
|
+
- **Metadata context** -- pass arbitrary data to deeply nested custom components without prop drilling, isolated from render state to avoid unnecessary re-renders
|
|
16
|
+
- **TypeScript** -- full generic support for extended configs and metadata types
|
|
17
|
+
|
|
18
|
+
## Installation
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
# npm
|
|
22
|
+
npm install @ai-react-markdown/core
|
|
23
|
+
|
|
24
|
+
# pnpm
|
|
25
|
+
pnpm add @ai-react-markdown/core
|
|
26
|
+
|
|
27
|
+
# yarn
|
|
28
|
+
yarn add @ai-react-markdown/core
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Peer Dependencies
|
|
32
|
+
|
|
33
|
+
```json
|
|
34
|
+
{
|
|
35
|
+
"react": ">=19.0.0",
|
|
36
|
+
"react-dom": ">=19.0.0"
|
|
37
|
+
}
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
### CSS Dependencies
|
|
41
|
+
|
|
42
|
+
For LaTeX math rendering, include the KaTeX stylesheet:
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
import 'katex/dist/katex.min.css';
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
For the built-in default typography, include the typography CSS:
|
|
49
|
+
|
|
50
|
+
```tsx
|
|
51
|
+
import '@ai-react-markdown/core/typography/default.css';
|
|
52
|
+
// or import all typography variants at once:
|
|
53
|
+
import '@ai-react-markdown/core/typography/all.css';
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
## Quick Start
|
|
57
|
+
|
|
58
|
+
```tsx
|
|
59
|
+
import AIMarkdown from '@ai-react-markdown/core';
|
|
60
|
+
import 'katex/dist/katex.min.css';
|
|
61
|
+
import '@ai-react-markdown/core/typography/default.css';
|
|
62
|
+
|
|
63
|
+
function App() {
|
|
64
|
+
return <AIMarkdown content="Hello **world**! Math: $E = mc^2$" />;
|
|
65
|
+
}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
### Streaming Example
|
|
69
|
+
|
|
70
|
+
```tsx
|
|
71
|
+
function StreamingChat({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
72
|
+
return <AIMarkdown content={content} streaming={isStreaming} colorScheme="dark" />;
|
|
73
|
+
}
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Props API Reference
|
|
77
|
+
|
|
78
|
+
### `AIMarkdownProps<TConfig, TRenderData>`
|
|
79
|
+
|
|
80
|
+
| Prop | Type | Default | Description |
|
|
81
|
+
| ---------------------- | -------------------------------- | ------------------------------- | ---------------------------------------------------------------------- |
|
|
82
|
+
| `content` | `string` | **(required)** | Raw markdown content to render. |
|
|
83
|
+
| `streaming` | `boolean` | `false` | Whether content is actively being streamed (e.g. from an LLM). |
|
|
84
|
+
| `fontSize` | `number \| string` | `'0.875rem'` | Base font size. Numbers are treated as pixels. |
|
|
85
|
+
| `variant` | `AIMarkdownVariant` | `'default'` | Typography variant name. |
|
|
86
|
+
| `colorScheme` | `AIMarkdownColorScheme` | `'light'` | Color scheme name (`'light'`, `'dark'`, or custom). |
|
|
87
|
+
| `config` | `PartialDeep<TConfig>` | `undefined` | Partial render config, deep-merged with defaults. |
|
|
88
|
+
| `defaultConfig` | `TConfig` | `defaultAIMarkdownRenderConfig` | Base config to merge against. Sub-packages can pass extended defaults. |
|
|
89
|
+
| `metadata` | `TRenderData` | `undefined` | Arbitrary data passed to custom components via a dedicated context. |
|
|
90
|
+
| `contentPreprocessors` | `AIMDContentPreprocessor[]` | `[]` | Additional preprocessors run after the built-in LaTeX preprocessor. |
|
|
91
|
+
| `customComponents` | `AIMarkdownCustomComponents` | `undefined` | `react-markdown` component overrides for specific HTML elements. |
|
|
92
|
+
| `Typography` | `AIMarkdownTypographyComponent` | `DefaultTypography` | Typography wrapper component. |
|
|
93
|
+
| `ExtraStyles` | `AIMarkdownExtraStylesComponent` | `undefined` | Optional extra style wrapper rendered between typography and content. |
|
|
94
|
+
|
|
95
|
+
## Configuration
|
|
96
|
+
|
|
97
|
+
Rendering behavior is controlled by `AIMarkdownRenderConfig`, which has two configuration arrays:
|
|
98
|
+
|
|
99
|
+
### Extra Syntax Extensions
|
|
100
|
+
|
|
101
|
+
Enable via `config.extraSyntaxSupported`. All are enabled by default.
|
|
102
|
+
|
|
103
|
+
| Value | Description |
|
|
104
|
+
| --------------------------------------------- | ------------------------------------------------------------------------------------------------------- |
|
|
105
|
+
| `AIMarkdownRenderExtraSyntax.HIGHLIGHT` | `==Highlight==` syntax support |
|
|
106
|
+
| `AIMarkdownRenderExtraSyntax.DEFINITION_LIST` | Definition list syntax ([PHP Markdown Extra](https://michelf.ca/projects/php-markdown/extra/#def-list)) |
|
|
107
|
+
| `AIMarkdownRenderExtraSyntax.SUBSCRIPT` | Superscript (`^text^`) and subscript (`~text~`) |
|
|
108
|
+
|
|
109
|
+
### Display Optimization Abilities
|
|
110
|
+
|
|
111
|
+
Enable via `config.displayOptimizeAbilities`. All are enabled by default.
|
|
112
|
+
|
|
113
|
+
| Value | Description |
|
|
114
|
+
| -------------------------------------------------------- | -------------------------------------------------------- |
|
|
115
|
+
| `AIMarkdownRenderDisplayOptimizeAbility.REMOVE_COMMENTS` | Strip HTML comments |
|
|
116
|
+
| `AIMarkdownRenderDisplayOptimizeAbility.SMARTYPANTS` | Typographic enhancements (curly quotes, em-dashes, etc.) |
|
|
117
|
+
| `AIMarkdownRenderDisplayOptimizeAbility.PANGU` | Auto-insert spaces between CJK and half-width characters |
|
|
118
|
+
|
|
119
|
+
### Example: Selective Configuration
|
|
120
|
+
|
|
121
|
+
```tsx
|
|
122
|
+
import AIMarkdown, {
|
|
123
|
+
AIMarkdownRenderExtraSyntax,
|
|
124
|
+
AIMarkdownRenderDisplayOptimizeAbility,
|
|
125
|
+
} from '@ai-react-markdown/core';
|
|
126
|
+
|
|
127
|
+
<AIMarkdown
|
|
128
|
+
content={markdown}
|
|
129
|
+
config={{
|
|
130
|
+
extraSyntaxSupported: [AIMarkdownRenderExtraSyntax.HIGHLIGHT],
|
|
131
|
+
displayOptimizeAbilities: [AIMarkdownRenderDisplayOptimizeAbility.SMARTYPANTS],
|
|
132
|
+
}}
|
|
133
|
+
/>;
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
When you provide a partial `config`, it is deep-merged with the defaults. Array values (like `extraSyntaxSupported`) are **replaced entirely**, not merged by index -- so the example above enables only the highlight extension, disabling definition lists and subscript.
|
|
137
|
+
|
|
138
|
+
## Hooks
|
|
139
|
+
|
|
140
|
+
### `useAIMarkdownRenderState<TConfig>()`
|
|
141
|
+
|
|
142
|
+
Access the current render state from within any component rendered inside `<AIMarkdown>`. Throws if called outside the provider boundary.
|
|
143
|
+
|
|
144
|
+
```tsx
|
|
145
|
+
import { useAIMarkdownRenderState } from '@ai-react-markdown/core';
|
|
146
|
+
|
|
147
|
+
function CustomCodeBlock({ children }: PropsWithChildren) {
|
|
148
|
+
const { streaming, config, fontSize, variant, colorScheme } = useAIMarkdownRenderState();
|
|
149
|
+
|
|
150
|
+
if (streaming) {
|
|
151
|
+
return <pre className="streaming">{children}</pre>;
|
|
152
|
+
}
|
|
153
|
+
return <pre>{children}</pre>;
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Returns** `AIMarkdownRenderState<TConfig>`:
|
|
158
|
+
|
|
159
|
+
| Field | Type | Description |
|
|
160
|
+
| ------------- | ----------------------- | --------------------------------------------------- |
|
|
161
|
+
| `streaming` | `boolean` | Whether content is being streamed. |
|
|
162
|
+
| `fontSize` | `string` | Resolved CSS font-size value. |
|
|
163
|
+
| `variant` | `AIMarkdownVariant` | Active typography variant. |
|
|
164
|
+
| `colorScheme` | `AIMarkdownColorScheme` | Active color scheme. |
|
|
165
|
+
| `config` | `TConfig` | Active render configuration (merged with defaults). |
|
|
166
|
+
|
|
167
|
+
### `useAIMarkdownMetadata<TMetadata>()`
|
|
168
|
+
|
|
169
|
+
Access arbitrary metadata from within the `<AIMarkdown>` tree. Metadata lives in a **separate** React context from render state, so metadata changes do not trigger re-renders in components that only consume render state.
|
|
170
|
+
|
|
171
|
+
```tsx
|
|
172
|
+
import { useAIMarkdownMetadata } from '@ai-react-markdown/core';
|
|
173
|
+
|
|
174
|
+
interface MyMetadata {
|
|
175
|
+
onCopyCode: (code: string) => void;
|
|
176
|
+
messageId: string;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function CustomCodeBlock({ children }: PropsWithChildren) {
|
|
180
|
+
const metadata = useAIMarkdownMetadata<MyMetadata>();
|
|
181
|
+
return (
|
|
182
|
+
<pre>
|
|
183
|
+
<button onClick={() => metadata?.onCopyCode(String(children))}>Copy</button>
|
|
184
|
+
{children}
|
|
185
|
+
</pre>
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
```
|
|
189
|
+
|
|
190
|
+
**Returns** `TMetadata | undefined` -- `undefined` when no metadata was provided.
|
|
191
|
+
|
|
192
|
+
### `useStableValue<T>(value: T)`
|
|
193
|
+
|
|
194
|
+
Returns a referentially stable version of `value`. On each render the new value is deep-compared (via `lodash/isEqual`) against the previous one. If they are structurally equal, the previous reference is returned, preventing unnecessary re-renders in downstream `useMemo`/`useEffect` consumers.
|
|
195
|
+
|
|
196
|
+
```tsx
|
|
197
|
+
import { useStableValue } from '@ai-react-markdown/core';
|
|
198
|
+
|
|
199
|
+
const stableConfig = useStableValue(config);
|
|
200
|
+
// stableConfig keeps the same reference as long as config is deep-equal.
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
## Typography and Styling
|
|
204
|
+
|
|
205
|
+
The `<AIMarkdown>` component wraps its content in a typography component that controls font size, variant, and color scheme.
|
|
206
|
+
|
|
207
|
+
### Built-in Default Typography
|
|
208
|
+
|
|
209
|
+
The built-in `DefaultTypography` renders a `<div>` with CSS class names for the active variant and color scheme:
|
|
210
|
+
|
|
211
|
+
```html
|
|
212
|
+
<div class="aim-typography-root default light" style="width: 100%; font-size: 0.875rem">
|
|
213
|
+
<!-- markdown content -->
|
|
214
|
+
</div>
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
Import the corresponding CSS to activate styles:
|
|
218
|
+
|
|
219
|
+
```tsx
|
|
220
|
+
import '@ai-react-markdown/core/typography/default.css';
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
### Custom Typography Component
|
|
224
|
+
|
|
225
|
+
Replace the typography wrapper by passing a custom component:
|
|
226
|
+
|
|
227
|
+
```tsx
|
|
228
|
+
import type { AIMarkdownTypographyProps } from '@ai-react-markdown/core';
|
|
229
|
+
|
|
230
|
+
function MyTypography({ children, fontSize, variant, colorScheme }: AIMarkdownTypographyProps) {
|
|
231
|
+
return (
|
|
232
|
+
<div className={`my-markdown ${colorScheme}`} style={{ fontSize }}>
|
|
233
|
+
{children}
|
|
234
|
+
</div>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
<AIMarkdown content={markdown} Typography={MyTypography} />;
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
### Extra Styles Wrapper
|
|
242
|
+
|
|
243
|
+
The `ExtraStyles` prop accepts a component rendered between the typography wrapper and the markdown content. Useful for injecting additional CSS scope or theme providers:
|
|
244
|
+
|
|
245
|
+
```tsx
|
|
246
|
+
import type { AIMarkdownExtraStylesProps } from '@ai-react-markdown/core';
|
|
247
|
+
|
|
248
|
+
function MyExtraStyles({ children }: AIMarkdownExtraStylesProps) {
|
|
249
|
+
return <div className="my-extra-scope">{children}</div>;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
<AIMarkdown content={markdown} ExtraStyles={MyExtraStyles} />;
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
## Custom Components
|
|
256
|
+
|
|
257
|
+
Override the default renderers for specific HTML elements using the `customComponents` prop. This maps directly to `react-markdown`'s `Components` type:
|
|
258
|
+
|
|
259
|
+
```tsx
|
|
260
|
+
import type { AIMarkdownCustomComponents } from '@ai-react-markdown/core';
|
|
261
|
+
|
|
262
|
+
const components: AIMarkdownCustomComponents = {
|
|
263
|
+
a: ({ href, children }) => (
|
|
264
|
+
<a href={href} target="_blank" rel="noopener noreferrer">
|
|
265
|
+
{children}
|
|
266
|
+
</a>
|
|
267
|
+
),
|
|
268
|
+
img: ({ src, alt }) => <img src={src} alt={alt} loading="lazy" />,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
<AIMarkdown content={markdown} customComponents={components} />;
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Streaming Support
|
|
275
|
+
|
|
276
|
+
Pass `streaming={true}` when content is actively being generated (e.g. token-by-token from an LLM). The flag is propagated to all descendant components via `useAIMarkdownRenderState()`, allowing custom renderers to adapt their behavior (e.g. show a cursor, disable copy buttons, or skip animations).
|
|
277
|
+
|
|
278
|
+
```tsx
|
|
279
|
+
function ChatMessage({ content, isStreaming }: { content: string; isStreaming: boolean }) {
|
|
280
|
+
return <AIMarkdown content={content} streaming={isStreaming} />;
|
|
281
|
+
}
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
## Metadata
|
|
285
|
+
|
|
286
|
+
The `metadata` prop lets you pass arbitrary data to deeply nested custom components without prop drilling. Metadata is stored in a **separate React context** from the render state, so updating metadata does not cause re-renders in components that only read render state (like the core `MarkdownContent`).
|
|
287
|
+
|
|
288
|
+
```tsx
|
|
289
|
+
interface ChatMetadata {
|
|
290
|
+
messageId: string;
|
|
291
|
+
onCopyCode: (code: string) => void;
|
|
292
|
+
onRegenerate: () => void;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
<AIMarkdown<AIMarkdownRenderConfig, ChatMetadata>
|
|
296
|
+
content={markdown}
|
|
297
|
+
metadata={{
|
|
298
|
+
messageId: msg.id,
|
|
299
|
+
onCopyCode: handleCopy,
|
|
300
|
+
onRegenerate: handleRegenerate,
|
|
301
|
+
}}
|
|
302
|
+
/>;
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
## Content Preprocessors
|
|
306
|
+
|
|
307
|
+
The rendering pipeline runs a LaTeX preprocessor by default. You can append additional preprocessors that transform the raw markdown string before it enters the remark/rehype pipeline:
|
|
308
|
+
|
|
309
|
+
```tsx
|
|
310
|
+
import type { AIMDContentPreprocessor } from '@ai-react-markdown/core';
|
|
311
|
+
|
|
312
|
+
const stripFrontmatter: AIMDContentPreprocessor = (content) => content.replace(/^---[\s\S]*?---\n/, '');
|
|
313
|
+
|
|
314
|
+
<AIMarkdown content={markdown} contentPreprocessors={[stripFrontmatter]} />;
|
|
315
|
+
```
|
|
316
|
+
|
|
317
|
+
Preprocessors run in sequence: built-in LaTeX preprocessor first, then your custom ones in array order.
|
|
318
|
+
|
|
319
|
+
## TypeScript Generics
|
|
320
|
+
|
|
321
|
+
The component supports two generic type parameters for type-safe config and metadata:
|
|
322
|
+
|
|
323
|
+
```tsx
|
|
324
|
+
import AIMarkdown, { type AIMarkdownRenderConfig, type AIMarkdownMetadata } from '@ai-react-markdown/core';
|
|
325
|
+
|
|
326
|
+
// Extended config (e.g. adding code block options)
|
|
327
|
+
interface MyConfig extends AIMarkdownRenderConfig {
|
|
328
|
+
codeBlock: { defaultExpanded: boolean };
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Extended metadata
|
|
332
|
+
interface MyMetadata extends AIMarkdownMetadata {
|
|
333
|
+
messageId: string;
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
<AIMarkdown<MyConfig, MyMetadata>
|
|
337
|
+
content={markdown}
|
|
338
|
+
defaultConfig={myDefaultConfig}
|
|
339
|
+
config={{ codeBlock: { defaultExpanded: false } }}
|
|
340
|
+
metadata={{ messageId: '123' }}
|
|
341
|
+
/>;
|
|
342
|
+
```
|
|
343
|
+
|
|
344
|
+
Sub-packages like `@ai-react-markdown/mantine` use this pattern to extend the base config with additional options (e.g. `forceSameFontSize`, `codeBlock.autoDetectUnknownLanguage`) while inheriting all core functionality.
|
|
345
|
+
|
|
346
|
+
Similarly, hooks accept generic parameters for type-safe access:
|
|
347
|
+
|
|
348
|
+
```tsx
|
|
349
|
+
const { config } = useAIMarkdownRenderState<MyConfig>();
|
|
350
|
+
const metadata = useAIMarkdownMetadata<MyMetadata>();
|
|
351
|
+
```
|
|
352
|
+
|
|
353
|
+
## Architecture Overview
|
|
354
|
+
|
|
355
|
+
```text
|
|
356
|
+
<AIMarkdown>
|
|
357
|
+
<AIMarkdownMetadataProvider> // Separate context for metadata
|
|
358
|
+
<AIMarkdownRenderStateProvider> // Context for render state (streaming, config, etc.)
|
|
359
|
+
<Typography> // Configurable typography wrapper
|
|
360
|
+
<ExtraStyles?> // Optional extra style wrapper
|
|
361
|
+
<AIMarkdownContent /> // react-markdown with remark/rehype plugin chain
|
|
362
|
+
</ExtraStyles?>
|
|
363
|
+
</Typography>
|
|
364
|
+
</AIMarkdownRenderStateProvider>
|
|
365
|
+
</AIMarkdownMetadataProvider>
|
|
366
|
+
</AIMarkdown>
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
The metadata and render state providers are deliberately separated so that metadata changes (e.g. callback updates) do not trigger re-renders in `AIMarkdownContent`, which only consumes render state.
|
|
370
|
+
|
|
371
|
+
## Exported API
|
|
372
|
+
|
|
373
|
+
### Default Export
|
|
374
|
+
|
|
375
|
+
- `AIMarkdown` -- the main component (memoized)
|
|
376
|
+
|
|
377
|
+
### Types
|
|
378
|
+
|
|
379
|
+
- `AIMarkdownProps`
|
|
380
|
+
- `AIMarkdownCustomComponents`
|
|
381
|
+
- `AIMarkdownRenderConfig`
|
|
382
|
+
- `AIMarkdownRenderState`
|
|
383
|
+
- `AIMarkdownMetadata`
|
|
384
|
+
- `AIMarkdownTypographyProps`
|
|
385
|
+
- `AIMarkdownTypographyComponent`
|
|
386
|
+
- `AIMarkdownExtraStylesProps`
|
|
387
|
+
- `AIMarkdownExtraStylesComponent`
|
|
388
|
+
- `AIMarkdownVariant`
|
|
389
|
+
- `AIMarkdownColorScheme`
|
|
390
|
+
- `AIMDContentPreprocessor`
|
|
391
|
+
- `PartialDeep`
|
|
392
|
+
|
|
393
|
+
### Enums and Constants
|
|
394
|
+
|
|
395
|
+
- `AIMarkdownRenderExtraSyntax`
|
|
396
|
+
- `AIMarkdownRenderDisplayOptimizeAbility`
|
|
397
|
+
- `defaultAIMarkdownRenderConfig`
|
|
398
|
+
|
|
399
|
+
### Hooks (re-exported)
|
|
400
|
+
|
|
401
|
+
- `useAIMarkdownRenderState()`
|
|
402
|
+
- `useAIMarkdownMetadata()`
|
|
403
|
+
- `useStableValue()`
|
|
404
|
+
|
|
405
|
+
## License
|
|
406
|
+
|
|
407
|
+
MIT
|