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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "myst-to-react",
3
- "version": "0.1.15",
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",
@@ -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 overflow-hidden',
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;
@@ -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 ImageNode } from 'myst-spec';
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 || undefined,
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 { MyST, unified, visit } = await import('mystjs');
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 overflow-hidden">
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 <div className={classNames({ hidden: !open })}>{children}</div>;
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> = {