myst-to-react 0.1.15 → 0.1.16
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/package.json +3 -1
- package/src/admonitions.tsx +1 -1
- package/src/card.tsx +141 -0
- package/src/dropdown.tsx +69 -0
- package/src/footnotes.tsx +1 -1
- package/src/grid.tsx +127 -0
- package/src/image.tsx +27 -2
- package/src/index.tsx +6 -0
- package/src/myst.tsx +38 -2
- package/src/tabs.tsx +7 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "myst-to-react",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.16",
|
|
4
4
|
"main": "./src/index.tsx",
|
|
5
5
|
"types": "./src/index.tsx",
|
|
6
6
|
"files": [
|
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
"@popperjs/core": "^2.11.5",
|
|
22
22
|
"@remix-run/react": "^1.6.3",
|
|
23
23
|
"ansi-to-react": "^6.1.6",
|
|
24
|
+
"buffer": "^6.0.3",
|
|
24
25
|
"classnames": "^2.3.1",
|
|
25
26
|
"mermaid": "^9.1.6",
|
|
26
27
|
"myst-spec": "^0.0.4",
|
|
28
|
+
"myst-to-docx": "*",
|
|
27
29
|
"myst-to-tex": "*",
|
|
28
30
|
"myst-transforms": "*",
|
|
29
31
|
"mystjs": "^0.0.13",
|
package/src/admonitions.tsx
CHANGED
|
@@ -120,7 +120,7 @@ function Admonition({
|
|
|
120
120
|
return (
|
|
121
121
|
<aside
|
|
122
122
|
className={classNames(
|
|
123
|
-
'admonition rounded-md my-4 border-l-4 shadow-md dark:shadow-2xl dark:shadow-neutral-900
|
|
123
|
+
'admonition rounded-md my-4 border-l-4 shadow-md dark:shadow-2xl dark:shadow-neutral-900',
|
|
124
124
|
{
|
|
125
125
|
'border-blue-500': color === 'blue',
|
|
126
126
|
'border-green-600': color === 'green',
|
package/src/card.tsx
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NodeRenderer } from './types';
|
|
3
|
+
import classNames from 'classnames';
|
|
4
|
+
import { Link } from '@remix-run/react';
|
|
5
|
+
// import { AdmonitionKind } from 'mystjs';
|
|
6
|
+
|
|
7
|
+
type CardSpec = {
|
|
8
|
+
type: 'card';
|
|
9
|
+
url?: string;
|
|
10
|
+
};
|
|
11
|
+
type CardTitleSpec = {
|
|
12
|
+
type: 'cardTitle';
|
|
13
|
+
};
|
|
14
|
+
type HeaderSpec = {
|
|
15
|
+
type: 'header';
|
|
16
|
+
};
|
|
17
|
+
type FooterSpec = {
|
|
18
|
+
type: 'footer';
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
export const Header: NodeRenderer<HeaderSpec> = (node, children) => {
|
|
22
|
+
return (
|
|
23
|
+
<header
|
|
24
|
+
key={node.key}
|
|
25
|
+
className="m-0 py-1 pl-3 bg-gray-50 dark:bg-slate-900 border-b border-gray-100 dark:border-gray-800"
|
|
26
|
+
>
|
|
27
|
+
{children}
|
|
28
|
+
</header>
|
|
29
|
+
);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
export const Footer: NodeRenderer<FooterSpec> = (node, children) => {
|
|
33
|
+
return (
|
|
34
|
+
<footer
|
|
35
|
+
key={node.key}
|
|
36
|
+
className="m-0 py-1 pl-3 bg-gray-50 dark:bg-slate-900 border-t border-gray-100 dark:border-gray-800"
|
|
37
|
+
>
|
|
38
|
+
{children}
|
|
39
|
+
</footer>
|
|
40
|
+
);
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
export const CardTitle: NodeRenderer<CardTitleSpec> = (node, children) => {
|
|
44
|
+
return (
|
|
45
|
+
<div key={node.key} className="pt-3 font-bold group-hover:underline">
|
|
46
|
+
{children}
|
|
47
|
+
</div>
|
|
48
|
+
);
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
type Parts = {
|
|
52
|
+
header?: React.ReactNode;
|
|
53
|
+
body?: React.ReactNode;
|
|
54
|
+
footer?: React.ReactNode;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
function getParts(children: React.ReactNode): Parts {
|
|
58
|
+
const parts: Parts = {};
|
|
59
|
+
if (!Array.isArray(children)) return parts;
|
|
60
|
+
const next = [...children];
|
|
61
|
+
if (next[0]?.type === 'header') {
|
|
62
|
+
parts.header = next.splice(0, 1);
|
|
63
|
+
}
|
|
64
|
+
if (next[next.length - 1]?.type === 'footer') {
|
|
65
|
+
parts.footer = next.splice(-1, 1);
|
|
66
|
+
}
|
|
67
|
+
parts.body = next;
|
|
68
|
+
return parts;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function ExternalOrInternalLink({
|
|
72
|
+
to,
|
|
73
|
+
className,
|
|
74
|
+
prefetch = 'intent',
|
|
75
|
+
children,
|
|
76
|
+
}: {
|
|
77
|
+
to: string;
|
|
78
|
+
className?: string;
|
|
79
|
+
prefetch?: 'intent' | 'render' | 'none';
|
|
80
|
+
children: React.ReactNode;
|
|
81
|
+
}) {
|
|
82
|
+
if (to.startsWith('http')) {
|
|
83
|
+
return (
|
|
84
|
+
<a href={to} className={className} target="_blank" rel="noopener noreferrer">
|
|
85
|
+
{children}
|
|
86
|
+
</a>
|
|
87
|
+
);
|
|
88
|
+
}
|
|
89
|
+
return (
|
|
90
|
+
<Link to={to} className={className} prefetch={prefetch}>
|
|
91
|
+
{children}
|
|
92
|
+
</Link>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function Card({ children, url }: { children: React.ReactNode; url?: string }) {
|
|
97
|
+
const parts = getParts(children);
|
|
98
|
+
const link = !!url;
|
|
99
|
+
const sharedStyle =
|
|
100
|
+
'rounded-md shadow dark:shadow-neutral-800 overflow-hidden border border-gray-100 dark:border-gray-800 flex flex-col';
|
|
101
|
+
if (link) {
|
|
102
|
+
return (
|
|
103
|
+
<ExternalOrInternalLink
|
|
104
|
+
to={url}
|
|
105
|
+
className={classNames(
|
|
106
|
+
sharedStyle,
|
|
107
|
+
'block font-normal no-underline cursor-pointer group',
|
|
108
|
+
'hover:border-blue-500 dark:hover:border-blue-400',
|
|
109
|
+
)}
|
|
110
|
+
>
|
|
111
|
+
{parts.header}
|
|
112
|
+
<div className="py-2 px-4 flex-grow">{parts.body}</div>
|
|
113
|
+
{parts.footer}
|
|
114
|
+
</ExternalOrInternalLink>
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
return (
|
|
118
|
+
<div className={sharedStyle}>
|
|
119
|
+
{parts.header}
|
|
120
|
+
<div className="py-2 px-4 flex-grow">{parts.body}</div>
|
|
121
|
+
{parts.footer}
|
|
122
|
+
</div>
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export const CardRenderer: NodeRenderer<CardSpec> = (node, children) => {
|
|
127
|
+
return (
|
|
128
|
+
<Card key={node.key} url={node.url}>
|
|
129
|
+
{children}
|
|
130
|
+
</Card>
|
|
131
|
+
);
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const CARD_RENDERERS = {
|
|
135
|
+
card: CardRenderer,
|
|
136
|
+
cardTitle: CardTitle,
|
|
137
|
+
header: Header,
|
|
138
|
+
footer: Footer,
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
export default CARD_RENDERERS;
|
package/src/dropdown.tsx
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import type { NodeRenderer } from './types';
|
|
3
|
+
import { ChevronRightIcon } from '@heroicons/react/solid';
|
|
4
|
+
import classNames from 'classnames';
|
|
5
|
+
|
|
6
|
+
type DropdownSpec = {
|
|
7
|
+
type: 'details';
|
|
8
|
+
open?: boolean;
|
|
9
|
+
};
|
|
10
|
+
type SummarySpec = {
|
|
11
|
+
type: 'summary';
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
const iconClass = 'h-8 w-8 inline-block pl-2 mr-2 -translate-y-[1px]';
|
|
15
|
+
|
|
16
|
+
export const SummaryTitle: NodeRenderer<SummarySpec> = (node, children) => {
|
|
17
|
+
return children;
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
function Details({
|
|
21
|
+
title,
|
|
22
|
+
children,
|
|
23
|
+
open,
|
|
24
|
+
}: {
|
|
25
|
+
title: React.ReactNode;
|
|
26
|
+
children: React.ReactNode[];
|
|
27
|
+
open?: boolean;
|
|
28
|
+
}) {
|
|
29
|
+
return (
|
|
30
|
+
<details
|
|
31
|
+
className={classNames(
|
|
32
|
+
'rounded-md my-4 shadow dark:shadow-2xl dark:shadow-neutral-900 overflow-hidden',
|
|
33
|
+
)}
|
|
34
|
+
open={open}
|
|
35
|
+
>
|
|
36
|
+
<summary
|
|
37
|
+
className={classNames(
|
|
38
|
+
'm-0 text-lg font-medium py-1 min-h-[2em] pl-3',
|
|
39
|
+
'cursor-pointer hover:shadow-[inset_0_0_0px_20px_#00000003] dark:hover:shadow-[inset_0_0_0px_20px_#FFFFFF03]',
|
|
40
|
+
'bg-gray-100 dark:bg-slate-900',
|
|
41
|
+
)}
|
|
42
|
+
>
|
|
43
|
+
<span className="text-neutral-900 dark:text-white">
|
|
44
|
+
<span className="block float-right font-thin text-sm text-neutral-700 dark:text-neutral-200">
|
|
45
|
+
<ChevronRightIcon className={classNames(iconClass, 'transition-transform')} />
|
|
46
|
+
</span>
|
|
47
|
+
{title}
|
|
48
|
+
</span>
|
|
49
|
+
</summary>
|
|
50
|
+
<div className="px-4 py-1 bg-gray-50 dark:bg-stone-800">{children}</div>
|
|
51
|
+
</details>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export const DetailsRenderer: NodeRenderer<DropdownSpec> = (node, children) => {
|
|
56
|
+
const [title, ...rest] = children as any[];
|
|
57
|
+
return (
|
|
58
|
+
<Details key={node.key} title={title} open={node.open}>
|
|
59
|
+
{rest}
|
|
60
|
+
</Details>
|
|
61
|
+
);
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const DROPDOWN_RENDERERS = {
|
|
65
|
+
details: DetailsRenderer,
|
|
66
|
+
summary: SummaryTitle,
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
export default DROPDOWN_RENDERERS;
|
package/src/footnotes.tsx
CHANGED
|
@@ -18,7 +18,7 @@ export const FootnoteReference: NodeRenderer = (node) => {
|
|
|
18
18
|
card={<FootnoteDefinition identifier={node.identifier as string} />}
|
|
19
19
|
as="span"
|
|
20
20
|
>
|
|
21
|
-
<sup>[{node.identifier}]</sup>
|
|
21
|
+
<sup>[{node.number ?? node.identifier}]</sup>
|
|
22
22
|
</ClickPopover>
|
|
23
23
|
);
|
|
24
24
|
};
|
package/src/grid.tsx
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import classNames from 'classnames';
|
|
2
|
+
import React from 'react';
|
|
3
|
+
import type { NodeRenderer } from './types';
|
|
4
|
+
|
|
5
|
+
type GridSpec = {
|
|
6
|
+
type: 'grid';
|
|
7
|
+
columns: number[];
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
const gridClassNames = {
|
|
11
|
+
main: [
|
|
12
|
+
'grid-cols-1',
|
|
13
|
+
'grid-cols-2',
|
|
14
|
+
'grid-cols-3',
|
|
15
|
+
'grid-cols-4',
|
|
16
|
+
'grid-cols-5',
|
|
17
|
+
'grid-cols-6',
|
|
18
|
+
'grid-cols-7',
|
|
19
|
+
'grid-cols-8',
|
|
20
|
+
'grid-cols-9',
|
|
21
|
+
'grid-cols-10',
|
|
22
|
+
'grid-cols-11',
|
|
23
|
+
'grid-cols-12',
|
|
24
|
+
],
|
|
25
|
+
sm: [
|
|
26
|
+
'sm:grid-cols-1',
|
|
27
|
+
'sm:grid-cols-2',
|
|
28
|
+
'sm:grid-cols-3',
|
|
29
|
+
'sm:grid-cols-4',
|
|
30
|
+
'sm:grid-cols-5',
|
|
31
|
+
'sm:grid-cols-6',
|
|
32
|
+
'sm:grid-cols-7',
|
|
33
|
+
'sm:grid-cols-8',
|
|
34
|
+
'sm:grid-cols-9',
|
|
35
|
+
'sm:grid-cols-10',
|
|
36
|
+
'sm:grid-cols-11',
|
|
37
|
+
'sm:grid-cols-12',
|
|
38
|
+
],
|
|
39
|
+
md: [
|
|
40
|
+
'md:grid-cols-1',
|
|
41
|
+
'md:grid-cols-2',
|
|
42
|
+
'md:grid-cols-3',
|
|
43
|
+
'md:grid-cols-4',
|
|
44
|
+
'md:grid-cols-5',
|
|
45
|
+
'md:grid-cols-6',
|
|
46
|
+
'md:grid-cols-7',
|
|
47
|
+
'md:grid-cols-8',
|
|
48
|
+
'md:grid-cols-9',
|
|
49
|
+
'md:grid-cols-10',
|
|
50
|
+
'md:grid-cols-11',
|
|
51
|
+
'md:grid-cols-12',
|
|
52
|
+
],
|
|
53
|
+
lg: [
|
|
54
|
+
'lg:grid-cols-1',
|
|
55
|
+
'lg:grid-cols-2',
|
|
56
|
+
'lg:grid-cols-3',
|
|
57
|
+
'lg:grid-cols-4',
|
|
58
|
+
'lg:grid-cols-5',
|
|
59
|
+
'lg:grid-cols-6',
|
|
60
|
+
'lg:grid-cols-7',
|
|
61
|
+
'lg:grid-cols-8',
|
|
62
|
+
'lg:grid-cols-9',
|
|
63
|
+
'lg:grid-cols-10',
|
|
64
|
+
'lg:grid-cols-11',
|
|
65
|
+
'lg:grid-cols-12',
|
|
66
|
+
],
|
|
67
|
+
xl: [
|
|
68
|
+
'xl:grid-cols-1',
|
|
69
|
+
'xl:grid-cols-2',
|
|
70
|
+
'xl:grid-cols-3',
|
|
71
|
+
'xl:grid-cols-4',
|
|
72
|
+
'xl:grid-cols-5',
|
|
73
|
+
'xl:grid-cols-6',
|
|
74
|
+
'xl:grid-cols-7',
|
|
75
|
+
'xl:grid-cols-8',
|
|
76
|
+
'xl:grid-cols-9',
|
|
77
|
+
'xl:grid-cols-10',
|
|
78
|
+
'xl:grid-cols-11',
|
|
79
|
+
'xl:grid-cols-12',
|
|
80
|
+
],
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const DEFAULT_NUM_COLUMNS = 3;
|
|
84
|
+
|
|
85
|
+
function getColumnClassName(classes: string[], number?: string | number): string {
|
|
86
|
+
const num = Number(number);
|
|
87
|
+
if (!number || Number.isNaN(num)) {
|
|
88
|
+
return getColumnClassName(classes, DEFAULT_NUM_COLUMNS);
|
|
89
|
+
}
|
|
90
|
+
return classes[num - 1] ?? classes[DEFAULT_NUM_COLUMNS];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function gridColumnClasses(columns?: number[]): string {
|
|
94
|
+
if (!columns || columns.length <= 1) {
|
|
95
|
+
return getColumnClassName(gridClassNames.main, columns?.[0]);
|
|
96
|
+
}
|
|
97
|
+
if (columns.length !== 4) {
|
|
98
|
+
return getColumnClassName(gridClassNames.main, columns[0]);
|
|
99
|
+
}
|
|
100
|
+
return [
|
|
101
|
+
// getColumnClassName(gridClassNames.main, columns[0]),
|
|
102
|
+
getColumnClassName(gridClassNames.sm, columns[0]),
|
|
103
|
+
getColumnClassName(gridClassNames.md, columns[1]),
|
|
104
|
+
getColumnClassName(gridClassNames.lg, columns[2]),
|
|
105
|
+
getColumnClassName(gridClassNames.xl, columns[3]),
|
|
106
|
+
].join(' ');
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function Grid({ columns, children }: { columns?: number[]; children: React.ReactNode }) {
|
|
110
|
+
const gridClasses = gridColumnClasses(columns);
|
|
111
|
+
const gutterClasses = 'gap-4';
|
|
112
|
+
return <div className={classNames('grid', gridClasses, gutterClasses)}>{children}</div>;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export const GridRenderer: NodeRenderer<GridSpec> = (node, children) => {
|
|
116
|
+
return (
|
|
117
|
+
<Grid key={node.key} columns={node.columns}>
|
|
118
|
+
{children}
|
|
119
|
+
</Grid>
|
|
120
|
+
);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
const GRID_RENDERERS = {
|
|
124
|
+
grid: GridRenderer,
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
export default GRID_RENDERERS;
|
package/src/image.tsx
CHANGED
|
@@ -1,7 +1,28 @@
|
|
|
1
1
|
import type { Alignment } from '@curvenote/blocks';
|
|
2
|
-
import type { Image as
|
|
2
|
+
import type { Image as ImageNodeSpec } from 'myst-spec';
|
|
3
3
|
import type { NodeRenderer } from './types';
|
|
4
4
|
|
|
5
|
+
type ImageNode = ImageNodeSpec & { height?: string };
|
|
6
|
+
|
|
7
|
+
function getStyleValue(width?: number | string): string | number | undefined {
|
|
8
|
+
if (typeof width === 'number' && Number.isNaN(width)) {
|
|
9
|
+
// If it is nan, return undefined.
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
|
12
|
+
if (typeof width === 'string') {
|
|
13
|
+
if (width.endsWith('%')) {
|
|
14
|
+
return width;
|
|
15
|
+
} else if (width.endsWith('px')) {
|
|
16
|
+
return Number(width.replace('px', ''));
|
|
17
|
+
} else if (!Number.isNaN(Number(width))) {
|
|
18
|
+
return Number(width);
|
|
19
|
+
}
|
|
20
|
+
console.log(`Unknown width ${width} in getImageWidth`);
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
return width;
|
|
24
|
+
}
|
|
25
|
+
|
|
5
26
|
function alignToMargin(align: string) {
|
|
6
27
|
switch (align) {
|
|
7
28
|
case 'left':
|
|
@@ -22,18 +43,21 @@ function Picture({
|
|
|
22
43
|
align = 'center',
|
|
23
44
|
alt,
|
|
24
45
|
width,
|
|
46
|
+
height,
|
|
25
47
|
}: {
|
|
26
48
|
src: string;
|
|
27
49
|
srcOptimized?: string;
|
|
28
50
|
urlSource?: string;
|
|
29
51
|
alt?: string;
|
|
30
52
|
width?: string;
|
|
53
|
+
height?: string;
|
|
31
54
|
align?: Alignment;
|
|
32
55
|
}) {
|
|
33
56
|
const image = (
|
|
34
57
|
<img
|
|
35
58
|
style={{
|
|
36
|
-
width: width
|
|
59
|
+
width: getStyleValue(width),
|
|
60
|
+
height: getStyleValue(height),
|
|
37
61
|
...alignToMargin(align),
|
|
38
62
|
}}
|
|
39
63
|
src={src}
|
|
@@ -58,6 +82,7 @@ export const Image: NodeRenderer<ImageNode> = (node) => {
|
|
|
58
82
|
srcOptimized={(node as any).urlOptimized}
|
|
59
83
|
alt={node.alt || node.title}
|
|
60
84
|
width={node.width || undefined}
|
|
85
|
+
height={node.height || undefined}
|
|
61
86
|
align={node.align}
|
|
62
87
|
// Note that sourceUrl is for backwards compatibility
|
|
63
88
|
urlSource={(node as any).urlSource || (node as any).sourceUrl}
|
package/src/index.tsx
CHANGED
|
@@ -3,6 +3,9 @@ import type { GenericParent } from 'mystjs';
|
|
|
3
3
|
import { mystToReact } from './convertToReact';
|
|
4
4
|
import BASIC_RENDERERS from './basic';
|
|
5
5
|
import ADMONITION_RENDERERS from './admonitions';
|
|
6
|
+
import DROPDOWN_RENDERERS from './dropdown';
|
|
7
|
+
import CARD_RENDERERS from './card';
|
|
8
|
+
import GRID_RENDERERS from './grid';
|
|
6
9
|
import CITE_RENDERERS from './cite';
|
|
7
10
|
import FOOTNOTE_RENDERERS from './footnotes';
|
|
8
11
|
import CODE_RENDERERS from './code';
|
|
@@ -41,6 +44,9 @@ export const DEFAULT_RENDERERS: Record<string, NodeRenderer> = {
|
|
|
41
44
|
...CROSS_REFERENCE_RENDERERS,
|
|
42
45
|
...MYST_RENDERERS,
|
|
43
46
|
...MERMAID_RENDERERS,
|
|
47
|
+
...DROPDOWN_RENDERERS,
|
|
48
|
+
...CARD_RENDERERS,
|
|
49
|
+
...GRID_RENDERERS,
|
|
44
50
|
...EXT_RENDERERS,
|
|
45
51
|
};
|
|
46
52
|
|
package/src/myst.tsx
CHANGED
|
@@ -4,6 +4,7 @@ import type { LatexResult } from 'myst-to-tex'; // Only import the type!!
|
|
|
4
4
|
import type { VFileMessage } from 'vfile-message';
|
|
5
5
|
import yaml from 'js-yaml';
|
|
6
6
|
import type { References } from '@curvenote/site-common';
|
|
7
|
+
import type { DocxResult } from 'myst-to-docx';
|
|
7
8
|
import type { PageFrontmatter } from 'myst-frontmatter';
|
|
8
9
|
import type { NodeRenderer } from './types';
|
|
9
10
|
import React, { useEffect, useRef, useState } from 'react';
|
|
@@ -15,9 +16,33 @@ import { CopyIcon } from './components/CopyIcon';
|
|
|
15
16
|
import { CodeBlock } from './code';
|
|
16
17
|
import { ReferencesProvider } from '@curvenote/ui-providers';
|
|
17
18
|
|
|
19
|
+
function downloadBlob(filename: string, blob: Blob) {
|
|
20
|
+
const a = document.createElement('a');
|
|
21
|
+
const url = URL.createObjectURL(blob);
|
|
22
|
+
a.href = url;
|
|
23
|
+
a.download = filename;
|
|
24
|
+
a.click();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function saveDocxFile(filename: string, mdast: any, footnotes?: any) {
|
|
28
|
+
const { unified } = await import('unified');
|
|
29
|
+
const { mystToDocx, fetchImagesAsBuffers } = await import('myst-to-docx');
|
|
30
|
+
// Clone the tree
|
|
31
|
+
const tree = JSON.parse(JSON.stringify(mdast));
|
|
32
|
+
// Put the footnotes back in
|
|
33
|
+
if (footnotes) tree.children.push(...Object.values(footnotes));
|
|
34
|
+
const opts = await fetchImagesAsBuffers(tree);
|
|
35
|
+
const docxBlob = await (unified()
|
|
36
|
+
.use(mystToDocx, opts)
|
|
37
|
+
.stringify(tree as any).result as DocxResult);
|
|
38
|
+
downloadBlob(filename, docxBlob as Blob);
|
|
39
|
+
}
|
|
40
|
+
|
|
18
41
|
async function parse(text: string, defaultFrontmatter?: PageFrontmatter) {
|
|
19
42
|
// Ensure that any imports from myst are async and scoped to this function
|
|
20
|
-
const {
|
|
43
|
+
const { visit } = await import('unist-util-visit');
|
|
44
|
+
const { unified } = await import('unified');
|
|
45
|
+
const { MyST } = await import('mystjs');
|
|
21
46
|
const {
|
|
22
47
|
mathPlugin,
|
|
23
48
|
footnotesPlugin,
|
|
@@ -110,7 +135,7 @@ export function MySTRenderer({ value, numbering }: { value: string; numbering: a
|
|
|
110
135
|
}, [text]);
|
|
111
136
|
|
|
112
137
|
return (
|
|
113
|
-
<figure className="relative shadow-lg rounded
|
|
138
|
+
<figure className="relative shadow-lg rounded">
|
|
114
139
|
<div className="absolute right-0 p-1">
|
|
115
140
|
<CopyIcon text={text} />
|
|
116
141
|
</div>
|
|
@@ -144,6 +169,17 @@ export function MySTRenderer({ value, numbering }: { value: string; numbering: a
|
|
|
144
169
|
{show}
|
|
145
170
|
</button>
|
|
146
171
|
))}
|
|
172
|
+
<button
|
|
173
|
+
className={classnames(
|
|
174
|
+
'px-2',
|
|
175
|
+
'bg-white hover:bg-slate-200 dark:bg-slate-500 dark:hover:bg-slate-700',
|
|
176
|
+
)}
|
|
177
|
+
title={`Download Micorsoft Word`}
|
|
178
|
+
aria-label={`Download Micorsoft Word`}
|
|
179
|
+
onClick={() => saveDocxFile('demo.docx', references.article, references.footnotes)}
|
|
180
|
+
>
|
|
181
|
+
DOCX
|
|
182
|
+
</button>
|
|
147
183
|
</div>
|
|
148
184
|
{previewType === 'DEMO' && (
|
|
149
185
|
<ReferencesProvider references={references}>{content}</ReferencesProvider>
|
package/src/tabs.tsx
CHANGED
|
@@ -19,12 +19,13 @@ export const TabSetRenderer: NodeRenderer = (node, children) => {
|
|
|
19
19
|
onClick(items[0]?.sync || items[0]?.key);
|
|
20
20
|
}, []);
|
|
21
21
|
return (
|
|
22
|
-
<div className="">
|
|
22
|
+
<div key={node.key} className="">
|
|
23
23
|
<div className="flex flex-row border-b border-b-gray-100 overflow-x-auto">
|
|
24
24
|
{items.map((item) => {
|
|
25
25
|
const key = item.sync || item.key;
|
|
26
26
|
return (
|
|
27
27
|
<div
|
|
28
|
+
key={item.key}
|
|
28
29
|
className={classNames('flex-none px-3 py-1 font-semibold cursor-pointer', {
|
|
29
30
|
'text-blue-600 border-b-2 border-b-blue-600 dark:border-b-white dark:text-white':
|
|
30
31
|
active[key],
|
|
@@ -47,7 +48,11 @@ export const TabSetRenderer: NodeRenderer = (node, children) => {
|
|
|
47
48
|
|
|
48
49
|
export const TabItemRenderer: NodeRenderer<TabItem> = (node, children) => {
|
|
49
50
|
const open = useIsTabOpen(node.sync || node.key);
|
|
50
|
-
return
|
|
51
|
+
return (
|
|
52
|
+
<div key={node.key} className={classNames({ hidden: !open })}>
|
|
53
|
+
{children}
|
|
54
|
+
</div>
|
|
55
|
+
);
|
|
51
56
|
};
|
|
52
57
|
|
|
53
58
|
const TAB_RENDERERS: Record<string, NodeRenderer> = {
|