Skip to content

Commit d021789

Browse files
authored
feat: support for rendering multiple examples in markdown (#35)
1 parent d02db0f commit d021789

File tree

6 files changed

+336
-216
lines changed

6 files changed

+336
-216
lines changed

__tests__/parseHTML-test.js

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
import parseHTML from '../src/utils/parseHTML';
2+
3+
const trim = str => {
4+
return str.replace(/[\n]+/g, '').trim();
5+
};
6+
7+
it('parse be null', () => {
8+
const result = parseHTML('');
9+
10+
expect(result).toBe(null);
11+
});
12+
13+
it('parse be html', () => {
14+
const result = parseHTML('<html><div></div></html>');
15+
16+
expect(result.length).toBe(1);
17+
expect(result[0].type).toBe('html');
18+
expect(result[0].content).toBe('<html><div></div></html>');
19+
});
20+
21+
it('Parse into one piece of code and two pieces of html', () => {
22+
const html = `<h1>header</h1>
23+
<!--start-code-->
24+
const a = 100;
25+
<!--end-code-->
26+
<p>footer</p>`;
27+
28+
const result = parseHTML(html);
29+
30+
expect(result.length).toBe(3);
31+
expect(result[0].type).toBe('html');
32+
expect(result[1].type).toBe('code');
33+
expect(result[2].type).toBe('html');
34+
expect(trim(result[0].content)).toContain('<h1>header</h1>');
35+
expect(trim(result[1].content)).toContain('const a = 100;');
36+
expect(trim(result[2].content)).toContain('<p>footer</p>');
37+
});
38+
39+
it('Parse into two pieces of code and three pieces of html', () => {
40+
const html = `<h1>header</h1>
41+
<!--start-code-->
42+
const a = 100;
43+
<!--end-code-->
44+
<h2>title</h2>
45+
<!--start-code-->
46+
const b = 200;
47+
<!--end-code-->
48+
<p>footer</p>`;
49+
50+
const result = parseHTML(html);
51+
52+
expect(result.length).toBe(5);
53+
expect(result[0].type).toBe('html');
54+
expect(result[1].type).toBe('code');
55+
expect(result[2].type).toBe('html');
56+
expect(result[3].type).toBe('code');
57+
expect(result[4].type).toBe('html');
58+
59+
expect(trim(result[0].content)).toBe('<h1>header</h1>');
60+
expect(trim(result[1].content)).toBe('const a = 100;');
61+
expect(trim(result[2].content)).toBe('<h2>title</h2>');
62+
expect(trim(result[3].content)).toBe('const b = 200;');
63+
expect(trim(result[4].content)).toBe('<p>footer</p>');
64+
});

docs/example.md

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ return <CodeView dependencies={{ Button }}>{require('./example.md')}</CodeView>;
2525
2626
## Example
2727

28+
### First example
29+
2830
<!--start-code-->
2931

3032
```js
@@ -35,12 +37,22 @@ import ReactDOM from 'react-dom';
3537
import { Button } from 'rsuite';
3638

3739
const App = () => {
38-
return <Button>Test</Button>;
40+
return <Button>First example</Button>;
3941
};
4042

4143
ReactDOM.render(<App />);
4244
```
4345

4446
<!--end-code-->
4547

48+
### Second example
49+
50+
<!--start-code-->
51+
52+
```js
53+
ReactDOM.render(<Button>Second example</Button>);
54+
```
55+
56+
<!--end-code-->
57+
4658
> Note: You can try changing the code above and see what changes.

src/CodeView.tsx

Lines changed: 29 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -1,89 +1,26 @@
11
/* eslint-disable @typescript-eslint/no-var-requires */
2-
import { useEffect, useState, useCallback, useRef } from 'react';
3-
import CodeIcon from '@rsuite/icons/Code';
4-
import classNames from 'classnames';
2+
import React from 'react';
53
import MarkdownRenderer from './MarkdownRenderer';
6-
import CodeEditor from './CodeEditor';
74
import parseHTML from './utils/parseHTML';
8-
import Preview from './Preview';
9-
import canUseDOM from './utils/canUseDOM';
10-
11-
const React = require('react');
12-
const ReactDOM = require('react-dom');
13-
14-
export interface CodeViewProps extends Omit<React.HTMLAttributes<HTMLElement>, 'onChange'> {
15-
/** Code editor theme, applied to CodeMirror */
16-
theme?: 'light' | 'dark';
5+
import Renderer, { RendererProps } from './Renderer';
176

7+
export interface CodeViewProps extends RendererProps {
188
/** The code to be rendered is executed. Usually imported via markdown-loader. */
199
children?: any;
2010

2111
/** The code to be rendered is executed */
2212
sourceCode?: string;
23-
24-
/** Dependent objects required by the executed code */
25-
dependencies?: object;
26-
27-
/** Renders a code editor that can modify the source code */
28-
editable?: boolean;
29-
30-
/** Editor properties */
31-
editor?: {
32-
className?: string;
33-
34-
/** Add a prefix to the className of the buttons on the toolbar */
35-
classPrefix?: string;
36-
37-
/** The className of the code button displayed on the toolbar */
38-
buttonClassName?: string;
39-
40-
/** Customize the code icon on the toolbar */
41-
icon?: React.ReactNode;
42-
};
43-
44-
/**
45-
* swc configuration
46-
* https://swc.rs/docs/configuration/compilation
47-
*/
48-
transformOptions?: object;
49-
50-
/** Customize the rendering toolbar */
51-
renderToolbar?: (buttons: React.ReactNode) => React.ReactNode;
52-
53-
/** Callback triggered after code change */
54-
onChange?: (code?: string) => void;
55-
56-
/**
57-
* A compiler that transforms the code. Use swc.transformSync by default
58-
* See https://swc.rs/docs/usage/wasm
59-
*/
60-
compiler?: (code: string) => string;
61-
62-
/** Executed before compiling the code */
63-
beforeCompile?: (code: string) => string;
64-
65-
/** Executed after compiling the code */
66-
afterCompile?: (code: string) => string;
6713
}
6814

69-
const defaultTransformOptions = {
70-
jsc: {
71-
parser: {
72-
syntax: 'ecmascript',
73-
jsx: true
74-
}
75-
}
76-
};
77-
7815
const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivElement>) => {
7916
const {
8017
children,
18+
sourceCode,
8119
dependencies,
8220
editor = {},
8321
theme = 'light',
84-
editable: isEditable = false,
85-
transformOptions = defaultTransformOptions,
86-
sourceCode,
22+
editable,
23+
transformOptions,
8724
renderToolbar,
8825
onChange,
8926
beforeCompile,
@@ -92,141 +29,33 @@ const CodeView = React.forwardRef((props: CodeViewProps, ref: React.Ref<HTMLDivE
9229
...rest
9330
} = props;
9431

95-
const {
96-
classPrefix,
97-
icon: codeIcon,
98-
className: editorClassName,
99-
buttonClassName,
100-
...editorProps
101-
} = editor;
102-
103-
const [initialized, setInitialized] = useState(false);
104-
const transfrom = useRef<any>(null);
105-
106-
useEffect(() => {
107-
if (!canUseDOM) {
108-
return;
109-
}
110-
111-
import('@swc/wasm-web').then(async module => {
112-
await module.default();
113-
transfrom.current = module.transformSync;
114-
setInitialized(true);
115-
});
116-
}, []);
117-
11832
const sourceStr: string = children?.__esModule ? children.default : sourceCode;
119-
const { code, beforeHTML, afterHTML } = parseHTML(sourceStr) || {};
120-
const [editable, setEditable] = useState(isEditable);
121-
const [hasError, setHasError] = useState(false);
122-
const [errorMessage, setErrorMessage] = useState(null);
123-
const [compiledReactNode, setCompiledReactNode] = useState(null);
124-
125-
const handleExpandEditor = useCallback(() => {
126-
setEditable(!editable);
127-
}, [editable]);
128-
129-
const handleError = useCallback(error => {
130-
setHasError(true);
131-
setErrorMessage(error.message);
132-
}, []);
133-
134-
const prefix = name => (classPrefix ? `${classPrefix}-${name}` : name);
135-
136-
const executeCode = useCallback(
137-
(pendCode: string = code) => {
138-
if (!canUseDOM) {
139-
return;
140-
}
141-
142-
const originalRender = ReactDOM.render;
143-
144-
// Redefine the render function, which will reset to the default value after `eval` is executed.
145-
ReactDOM.render = element => {
146-
setCompiledReactNode(element);
147-
};
148-
149-
try {
150-
const statement = dependencies
151-
? Object.keys(dependencies).map(key => `var ${key}= dependencies.${key};`)
152-
: [];
153-
154-
const beforeCompileCode = beforeCompile?.(pendCode) || pendCode;
155-
156-
if (beforeCompileCode) {
157-
const { code: compiledCode } = compiler
158-
? compiler(beforeCompileCode)
159-
: transfrom.current?.(beforeCompileCode, transformOptions);
160-
161-
eval(`${statement.join('\n')} ${afterCompile?.(compiledCode) || compiledCode}`);
162-
}
163-
} catch (err) {
164-
console.error(err);
165-
} finally {
166-
// Reset the render function to the original value.
167-
ReactDOM.render = originalRender;
168-
}
169-
},
170-
[code, dependencies, beforeCompile, compiler, transformOptions, afterCompile]
171-
);
172-
173-
useEffect(() => {
174-
if (initialized) {
175-
executeCode(code);
176-
}
177-
}, [initialized, code, executeCode]);
178-
179-
const handleCodeChange = useCallback(
180-
(code?: string) => {
181-
setHasError(false);
182-
setErrorMessage(null);
183-
onChange?.(code);
184-
185-
if (initialized) {
186-
executeCode(code);
187-
}
188-
},
189-
[executeCode, initialized, onChange]
190-
);
191-
192-
const codeButton = (
193-
<button
194-
role="switch"
195-
aria-checked={editable}
196-
aria-label="Show the full source"
197-
className={classNames(prefix('btn'), prefix('btn-xs'), buttonClassName)}
198-
onClick={handleExpandEditor}
199-
>
200-
{typeof codeIcon !== 'undefined' ? (
201-
codeIcon
202-
) : (
203-
<CodeIcon className={classNames(prefix('icon'), prefix('icon-code'))} />
204-
)}
205-
</button>
206-
);
207-
208-
const showCodeEditor = editable && code && initialized;
33+
const fragments = parseHTML(sourceStr);
20934

21035
return (
21136
<div ref={ref} {...rest}>
212-
<MarkdownRenderer>{beforeHTML}</MarkdownRenderer>
213-
<div className="rcv-container">
214-
<Preview hasError={hasError} errorMessage={errorMessage} onError={handleError}>
215-
{compiledReactNode}
216-
</Preview>
217-
<div className="rcv-toolbar">{renderToolbar ? renderToolbar(codeButton) : codeButton}</div>
218-
{showCodeEditor && (
219-
<CodeEditor
220-
{...editorProps}
221-
key="jsx"
222-
onChange={handleCodeChange}
223-
className={classNames(editorClassName, 'rcv-editor')}
224-
editorConfig={{ lineNumbers: true, theme: `base16-${theme}` }}
225-
code={code}
226-
/>
227-
)}
228-
</div>
229-
<MarkdownRenderer>{afterHTML}</MarkdownRenderer>
37+
{fragments?.map(fragment => {
38+
if (fragment.type === 'code') {
39+
return (
40+
<Renderer
41+
key={fragment.key}
42+
code={fragment.content}
43+
editable={editable}
44+
theme={theme}
45+
dependencies={dependencies}
46+
transformOptions={transformOptions}
47+
renderToolbar={renderToolbar}
48+
onChange={onChange}
49+
beforeCompile={beforeCompile}
50+
compiler={compiler}
51+
afterCompile={afterCompile}
52+
editor={editor}
53+
/>
54+
);
55+
} else if (fragment.type === 'html') {
56+
return <MarkdownRenderer key={fragment.key}>{fragment.content}</MarkdownRenderer>;
57+
}
58+
})}
23059
</div>
23160
);
23261
});

0 commit comments

Comments
 (0)