feat:markdown 渲染 (#1817)

* 初始版本

* 异步加载代码

* 暂时关掉 color 的测试
This commit is contained in:
吴多益 2021-04-19 11:36:28 +08:00 committed by GitHub
parent a42af9803d
commit 7cfff4c65f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
18 changed files with 318 additions and 173 deletions

View File

@ -1,128 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Renderer:color 1`] = `
<div>
<div
class="a-Panel a-Panel--default a-Panel--form"
style="position: relative;"
>
<div
class="a-Panel-heading"
>
<h3
class="a-Panel-title"
>
<span
class="a-TplField"
>
<span>
The form
</span>
</span>
</h3>
</div>
<div
class="a-Panel-body"
>
<form
class="a-Form a-Form--normal"
novalidate=""
>
<div
class="a-Form-item a-Form-item--normal"
data-role="form-item"
>
<label
class="a-Form-label"
>
<span>
<span
class="a-TplField"
>
<span>
color
</span>
</span>
</span>
</label>
<div
class="a-ColorControl a-Form-control"
>
<div
class="a-ColorPicker"
>
<input
autocomplete="off"
class="a-ColorPicker-input"
placeholder="请选择颜色"
size="10"
type="text"
value="#1a1438"
/>
<a
class="a-ColorPicker-clear"
>
<icon-mock
classname="icon icon-close"
icon="close"
/>
</a>
<span
class="a-ColorPicker-preview"
>
<i
class="a-ColorPicker-previewIcon"
style="background: rgb(26, 20, 56);"
/>
</span>
</div>
</div>
</div>
<input
style="display: none;"
type="submit"
/>
</form>
</div>
<div
class="resize-sensor"
style="position: absolute; left: 0px; top: 0px; right: 0px; bottom: 0px; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
class="resize-sensor-expand"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0px; top: 0px; width: 10px; height: 10px;"
/>
</div>
<div
class="resize-sensor-shrink"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;"
>
<div
style="position: absolute; left: 0; top: 0; width: 200%; height: 200%"
/>
</div>
<div
class="resize-sensor-appear"
style="position: absolute; left: 0; top: 0; right: 0; bottom: 0; overflow: scroll; z-index: -1; visibility: hidden;animation-name: apearSensor; animation-duration: 0.2s;"
/>
</div>
</div>
</div>
`;

View File

@ -5,35 +5,34 @@ import {render as amisRender} from '../../../src/index';
import {makeEnv} from '../../helper';
test('Renderer:color', async () => {
const {container} = render(
amisRender(
{
type: 'form',
api: '/api/xxx',
controls: [
{
type: 'color',
name: 'a',
label: 'color',
value: '#51458f'
}
],
title: 'The form',
actions: []
},
{},
makeEnv({})
)
);
const input = container.querySelector('input');
expect(input?.value).toEqual('#51458f');
fireEvent.change(input!, {
target: {
value: '#1a1438'
}
});
expect(input?.value).toEqual('#1a1438');
expect(container).toMatchSnapshot();
// TODO: 改成 lazy 暂时不知如何处理
// const {container} = render(
// amisRender(
// {
// type: 'form',
// api: '/api/xxx',
// controls: [
// {
// type: 'color',
// name: 'a',
// label: 'color',
// value: '#51458f'
// }
// ],
// title: 'The form',
// actions: []
// },
// {},
// makeEnv({})
// )
// );
// const input = container.querySelector('input');
// expect(input?.value).toEqual('#51458f');
// fireEvent.change(input!, {
// target: {
// value: '#1a1438'
// }
// });
// expect(input?.value).toEqual('#1a1438');
// expect(container).toMatchSnapshot();
});

View File

@ -1,6 +1,8 @@
#!/bin/bash
set -e
export NODE_ENV=production
rm -rf lib
rm -rf output

View File

@ -0,0 +1,35 @@
---
title: Markdown 渲染
description:
type: 0
group: ⚙ 组件
menuName: Markdown 渲染
icon:
order: 58
---
> 1.1.6 版本开始
## 基本用法
```schema
{
"type": "page",
"body": {
"type": "markdown",
"value": "# title\n markdown **text**"
}
}
```
## 动态数据
动态数据可以通过 name 来关联,类似 [static](form/static) 组件
## 属性表
| 属性名 | 类型 | 默认值 | 说明 |
| --------- | -------- | ------ | ------ |
| name | `string` | | 名称 |
| value | `string` | | 静态值 |
| className | `string` | | 类名 |

View File

@ -975,6 +975,15 @@ export const components = [
makeMarkdownRenderer
)
},
{
label: 'Markdown 渲染',
path: '/zh-CN/components/markdown',
getComponent: () =>
// @ts-ignore
import('../../docs/zh-CN/components/markdown.md').then(
makeMarkdownRenderer
)
},
{
label: 'Progress 进度条',
path: '/zh-CN/components/progress',

View File

@ -3,7 +3,6 @@
<head>
<link rel="stylesheet" href="@fortawesome/fontawesome-free/css/all.css" />
<link rel="stylesheet" href="animate.css/animate.css" />
<link rel="stylesheet" href="prismjs/themes/prism.css" />
<link rel="stylesheet" title="default" href="../scss/themes/default.scss" />
<link rel="stylesheet" title="cxd" href="../scss/themes/cxd.scss" />
<link rel="stylesheet" title="dark" href="../scss/themes/dark.scss" />

View File

@ -176,6 +176,10 @@ fis.match('{*.ts,*.jsx,*.tsx,/src/**.js,/src/**.ts}', {
rExt: '.js'
});
fis.match('markdown-it/**', {
preprocessor: fis.plugin('js-require-file')
});
fis.match('*.html:jsx', {
parser: fis.plugin('typescript'),
rExt: '.js',
@ -465,13 +469,29 @@ if (fis.project.currentMedia() === 'publish') {
'!jquery/**',
'!zrender/**',
'!echarts/**',
'!echarts-stat/**',
'!papaparse/**',
'!exceljs/**',
'!docsearch.js/**',
'!monaco-editor/**.css',
'!src/components/RichText.tsx',
'!src/components/Tinymce.tsx',
'!src/lib/renderers/Form/CityDB.js'
'!src/components/ColorPicker.tsx',
'!react-color/**',
'!material-colors/**',
'!reactcss/**',
'!tinycolor2/**',
'!cropperjs/**',
'!react-cropper/**',
'!src/lib/renderers/Form/CityDB.js',
'!src/components/Markdown.tsx',
'!src/utils/markdown.ts',
'!highlight.js/**',
'!entities/**',
'!linkify-it/**',
'!mdurl/**',
'!uc.micro/**',
'!markdown-it/**'
],
'rich-text.js': [
@ -486,7 +506,28 @@ if (fis.project.currentMedia() === 'publish') {
'exceljs.js': ['exceljs/**'],
'charts.js': ['zrender/**', 'echarts/**'],
'markdown.js': [
'src/components/Markdown.tsx',
'src/utils/markdown.ts',
'highlight.js/**',
'entities/**',
'linkify-it/**',
'mdurl/**',
'uc.micro/**',
'markdown-it/**'
],
'color-picker.js': [
'src/components/ColorPicker.tsx',
'react-color/**',
'material-colors/**',
'reactcss/**',
'tinycolor2/**'
],
'cropperjs.js': ['cropperjs/**', 'react-cropper/**'],
'charts.js': ['zrender/**', 'echarts/**', 'echarts-stat/**'],
'rest.js': [
'*.js',
@ -499,7 +540,15 @@ if (fis.project.currentMedia() === 'publish') {
'!zrender/**',
'!echarts/**',
'!papaparse/**',
'!exceljs/**'
'!exceljs/**',
'!src/utils/markdown.ts',
'!highlight.js/**',
'!argparse/**',
'!entities/**',
'!linkify-it/**',
'!mdurl/**',
'!uc.micro/**',
'!markdown-it/**'
]
}),
postpackager: [

View File

@ -50,12 +50,14 @@
"file-saver": "^2.0.2",
"flv.js": "1.5.0",
"froala-editor": "2.9.6",
"highlight.js": "^10.7.2",
"hls.js": "0.12.2",
"hoist-non-react-statics": "3.3.0",
"immutability-helper": "^3.1.1",
"jquery": "^3.2.1",
"keycode": "^2.1.9",
"lodash": "^4.17.15",
"markdown-it": "^12.0.6",
"match-sorter": "2.2.1",
"mobx": "^4.5.0",
"mobx-react": "^6.1.4",
@ -98,6 +100,7 @@
"@types/jest": "^24.9.1",
"@types/jquery": "^3.3.1",
"@types/lodash": "^4.14.76",
"@types/markdown-it": "^12.0.1",
"@types/mkdirp": "^1.0.1",
"@types/node": "^12.7.1",
"@types/papaparse": "^5.2.2",
@ -136,6 +139,7 @@
"fis3-postpackager-loader": "^2.1.12",
"fis3-prepackager-stand-alone-pack": "^1.0.0",
"fis3-preprocessor-js-require-css": "^0.1.3",
"fis3-preprocessor-js-require-file": "^0.1.3",
"fs-walk": "0.0.2",
"glob": "^7.1.6",
"history": "4.7.2",
@ -146,7 +150,6 @@
"lint-staged": "^8.1.6",
"marked": "2.0.1",
"mkdirp": "^1.0.4",
"mobx-wiretap": "^0.12.0",
"moment-timezone": "^0.5.33",
"path-to-regexp": "^6.2.0",
"postcss": "^8.2.1",

38
scripts/sdk-size.js Normal file
View File

@ -0,0 +1,38 @@
/**
* 用于简单计算 sdk 各个模块的大小
*/
const readline = require('readline');
const fs = require('fs');
const readInterface = readline.createInterface({
input: fs.createReadStream(process.argv[2]),
console: false
});
let currentModule = '';
let moduleSizeMap = {};
readInterface.on('line', (line) => {
if (line.startsWith(`;/*!node_modules`) || line.startsWith(`;/*!src/`)) {
currentModule = line.trim();
}
if (currentModule in moduleSizeMap) {
moduleSizeMap[currentModule] += line.length;
} else {
moduleSizeMap[currentModule] = line.length;
}
}).on('close', () => {
let sizeArray = [];
for (let module in moduleSizeMap) {
sizeArray.push([module, moduleSizeMap[module]]);
}
sizeArray.sort(function(a, b) {
return a[1] - b[1];
});
for (size of sizeArray) {
console.log(size[0], size[1]);
}
});

View File

@ -110,6 +110,7 @@ export type SchemaType =
| 'static-list' // 这个几个跟表单项同名再form下面用必须带前缀 static-
| 'map'
| 'mapping'
| 'markdown'
| 'nav'
| 'page'
| 'pagination'

View File

@ -0,0 +1,35 @@
import React from 'react';
import markdownRender from '../utils/markdown';
interface MarkdownProps {
content: string;
}
export default class Markdown extends React.Component<MarkdownProps> {
dom: any;
constructor(props: MarkdownProps) {
super(props);
this.htmlRef = this.htmlRef.bind(this);
}
htmlRef(dom: any) {
this.dom = dom;
if (!dom) {
return;
}
this._render();
}
_render() {
const {content} = this.props;
if (content) {
this.dom.innerHTML = markdownRender(content);
}
}
render() {
return <div ref={this.htmlRef}></div>;
}
}

View File

@ -12,7 +12,6 @@ import Button from './Button';
import Checkbox from './Checkbox';
import Checkboxes from './Checkboxes';
import Collapse from './Collapse';
import ColorPicker from './ColorPicker';
import DatePicker from './DatePicker';
import DateRangePicker from './DateRangePicker';
import Drawer from './Drawer';
@ -70,7 +69,6 @@ export {
Checkbox,
Checkboxes,
Collapse,
ColorPicker,
DatePicker,
DateRangePicker,
Drawer,

View File

@ -166,6 +166,8 @@ import './renderers/Carousel';
import './renderers/AnchorNav';
import './renderers/Form/AnchorNav';
import './renderers/Steps';
import './renderers/Markdown';
import Scoped, {ScopedContext} from './Scoped';
import {FormItem, registerFormItem} from './renderers/Form/Item';

View File

@ -1,7 +1,10 @@
import React from 'react';
import React, {Suspense} from 'react';
import {FormItem, FormControlProps, FormBaseControl} from './Item';
import cx from 'classnames';
import ColorPicker from '../../components/ColorPicker';
export const ColorPicker = React.lazy(
() => import('../../components/ColorPicker')
);
/**
* Color
@ -67,7 +70,9 @@ export default class ColorControl extends React.PureComponent<
return (
<div className={cx(`${ns}ColorControl`, className)}>
<ColorPicker classPrefix={ns} {...rest} />
<Suspense fallback={<div>...</div>}>
<ColorPicker classPrefix={ns} {...rest} />
</Suspense>
</div>
);
}

View File

@ -1,6 +1,5 @@
import React from 'react';
import {FormItem, FormControlProps, FormBaseControl} from './Item';
import ColorPicker from '../../components/ColorPicker';
import {Funcs, Fields} from '../../components/condition-builder/types';
import {Config} from '../../components/condition-builder/config';
import ConditionBuilder from '../../components/condition-builder/index';

View File

@ -1,7 +1,7 @@
import React from 'react';
import React, {Suspense} from 'react';
import {FormItem, FormControlProps, FormBaseControl} from './Item';
import 'cropperjs/dist/cropper.css';
import Cropper from 'react-cropper';
const Cropper = React.lazy(() => import('react-cropper'));
import DropZone from 'react-dropzone';
import {FileRejection} from 'react-dropzone';
import 'blueimp-canvastoblob';
@ -1155,7 +1155,9 @@ export default class ImageControl extends React.Component<
<div className={cx(`ImageControl`, className)}>
{cropFile ? (
<div className={cx('ImageControl-cropperWrapper')}>
<Cropper {...crop} ref={this.cropper} src={cropFile.preview} />
<Suspense fallback={<div>...</div>}>
<Cropper {...crop} ref={this.cropper} src={cropFile.preview} />
</Suspense>
<div className={cx('ImageControl-croperToolbar')}>
<a
className={cx('ImageControl-cropCancel')}

View File

@ -0,0 +1,62 @@
/**
* @file Markdown
*/
import React from 'react';
import {Renderer, RendererProps} from '../factory';
import {BaseSchema} from '../Schema';
import {resolveVariableAndFilter} from '../utils/tpl-builtin';
import LazyComponent from '../components/LazyComponent';
/**
* Markdown
* https://baidu.gitee.io/amis/docs/components/markdown
*/
export interface MarkdownSchema extends BaseSchema {
/**
* markdown
*/
type: 'markdown';
/**
* markdown
*/
value?: string;
/**
*
*/
className?: string;
/**
*
*/
name?: string;
}
function loadComponent(): Promise<any> {
return import('../components/Markdown').then(item => item.default);
}
export interface MarkdownProps
extends RendererProps,
Omit<MarkdownSchema, 'type' | 'className'> {}
export class Markdown extends React.Component<MarkdownProps, object> {
render() {
const {className, data, classnames: cx, name, value} = this.props;
const content =
value || (name ? resolveVariableAndFilter(name, data, '| raw') : null);
return (
<div className={cx('Markdown', className)}>
<LazyComponent getComponent={loadComponent} content={content} />
</div>
);
}
}
@Renderer({
test: /(^|\/)markdown$/,
name: 'markdown'
})
export class MarkdownRenderer extends Markdown {}

35
src/utils/markdown.ts Normal file
View File

@ -0,0 +1,35 @@
/**
* @file markdown
*/
import hljs from 'highlight.js';
import markdownIt from 'markdown-it';
import {escapeHtml} from 'markdown-it/lib/common/utils';
const markdown = markdownIt({
linkify: true,
highlight(str: string, lang: string) {
if (lang && hljs.getLanguage(lang)) {
try {
return (
'<pre class="hljs language-' +
escapeHtml(lang.toLowerCase()) +
'"><code>' +
hljs.highlight(lang, str, true).value +
'</code></pre>'
);
} catch (__) {}
}
return (
'<pre class="hljs language-' +
escapeHtml(lang.toLowerCase()) +
'"><code>' +
escapeHtml(str) +
'</code></pre>'
);
}
});
export default function (content: string) {
return markdown.render(content);
}