mirror of
https://gitee.com/baidu/amis.git
synced 2024-11-29 18:48:45 +08:00
fix: Code组件部分情况无法渲染问题,自定义高亮失效问题 (#5642)
This commit is contained in:
parent
6d26ee61d1
commit
0dbc79e464
@ -104,40 +104,7 @@ language 支持从上下文获取数据
|
||||
|
||||
## 自定义语言高亮
|
||||
|
||||
还可以通过 `customLang` 参数来自定义高亮,比如下面这个示例
|
||||
|
||||
```schema: scope="body"
|
||||
{
|
||||
"type": "code",
|
||||
"customLang": {
|
||||
"name": "myLog",
|
||||
"tokens": [
|
||||
{
|
||||
"name": "custom-error",
|
||||
"regex": "\\[error.*",
|
||||
"color": "#ff0000",
|
||||
"fontStyle": "bold"
|
||||
},
|
||||
{
|
||||
"name": "custom-notice",
|
||||
"regex": "\\[notice.*",
|
||||
"color": "#FFA500"
|
||||
},
|
||||
{
|
||||
"name": "custom-info",
|
||||
"regex": "\\[info.*",
|
||||
"color": "#808080"
|
||||
},
|
||||
{
|
||||
"name": "custom-date",
|
||||
"regex": "\\[[a-zA-Z 0-9:]+\\]",
|
||||
"color": "#008800"
|
||||
}
|
||||
]
|
||||
},
|
||||
"value": "[Sun Mar 7 16:02:00 2021] [notice] Apache/1.3.29 (Unix) configured -- resuming normal operations\n[Sun Mar 7 16:02:00 2021] [info] Server built: Feb 27 2021 13:56:37\n[Sun Mar 7 16:02:00 2021] [notice] Accept mutex: sysvsem (Default: sysvsem)\n[Sun Mar 7 17:21:44 2021] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection\n[Sun Mar 7 17:23:53 2021] statistics: Use of uninitialized value in concatenation (.) or string at /home/httpd line 528.\n[Sun Mar 7 17:23:53 2021] statistics: Can't create file /home/httpd/twiki/data/Main/WebStatistics.txt - Permission denied\n[Sun Mar 7 17:27:37 2021] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection\n[Sun Mar 7 17:31:39 2021] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection\n[Sun Mar 7 21:16:17 2021] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome"
|
||||
}
|
||||
```
|
||||
还可以通过 `customLang` 参数来自定义高亮,详情参考[示例](../../../examples/code)。
|
||||
|
||||
`customLang` 中主要是 `tokens` 设置,这里是语言词法配置,它有 4 个配置项:
|
||||
|
||||
|
42
examples/components/Code.jsx
Normal file
42
examples/components/Code.jsx
Normal file
@ -0,0 +1,42 @@
|
||||
export default {
|
||||
type: 'page',
|
||||
title: 'Code组件自定义语言高亮',
|
||||
body: [
|
||||
{
|
||||
type: 'code',
|
||||
customLang: {
|
||||
name: 'myLog',
|
||||
tokens: [
|
||||
{
|
||||
name: 'custom-error',
|
||||
regex: '\\[error.*',
|
||||
color: '#ff0000',
|
||||
fontStyle: 'bold'
|
||||
},
|
||||
{
|
||||
name: 'custom-notice',
|
||||
regex: '\\[notice.*',
|
||||
color: '#FFA500'
|
||||
},
|
||||
{
|
||||
name: 'custom-info',
|
||||
regex: '\\[info.*',
|
||||
color: '#808080'
|
||||
},
|
||||
{
|
||||
name: 'custom-date',
|
||||
regex: '\\[[a-zA-Z 0-9:]+\\]',
|
||||
color: '#008800'
|
||||
}
|
||||
]
|
||||
},
|
||||
value:
|
||||
"[Sun Mar 7 16:02:00 2021] [notice] Apache/1.3.29 (Unix) configured -- resuming normal operations\n[Sun Mar 7 16:02:00 2021] [info] Server built: Feb 27 2021 13:56:37\n[Sun Mar 7 16:02:00 2021] [notice] Accept mutex: sysvsem (Default: sysvsem)\n[Sun Mar 7 17:21:44 2021] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection\n[Sun Mar 7 17:23:53 2021] statistics: Use of uninitialized value in concatenation (.) or string at /home/httpd line 528.\n[Sun Mar 7 17:23:53 2021] statistics: Can't create file /home/httpd/twiki/data/Main/WebStatistics.txt - Permission denied\n[Sun Mar 7 17:27:37 2021] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection\n[Sun Mar 7 17:31:39 2021] [info] [client xx.xx.xx.xx] (104)Connection reset by peer: client stopped connection\n[Sun Mar 7 21:16:17 2021] [error] [client xx.xx.xx.xx] File does not exist: /home/httpd/twiki/view/Main/WebHome"
|
||||
},
|
||||
{
|
||||
type: 'markdown',
|
||||
value:
|
||||
'`customLang` 中主要是 `tokens` 设置,这里是语言词法配置,它有 4 个配置项:\n- `name`:词法名称\n- `regex`:词法的正则匹配,注意因为是在字符串中,这里正则中如果遇到 `\\` 需要写成 `\\\\`\n- `regexFlags`: 可选,正则的标志参数\n- `color`:颜色\n- `fontStyle`: 可选,字体样式,比如 `bold` 代表加粗'
|
||||
}
|
||||
]
|
||||
};
|
@ -121,6 +121,7 @@ import Tab1Schema from './Tabs/Tab1';
|
||||
import Tab2Schema from './Tabs/Tab2';
|
||||
import Tab3Schema from './Tabs/Tab3';
|
||||
import Loading from './Loading';
|
||||
import CodeSchema from './Code';
|
||||
|
||||
import {Switch} from 'react-router-dom';
|
||||
import {navigations2route} from './App';
|
||||
@ -771,6 +772,13 @@ export const examples = [
|
||||
component: makeSchemaRenderer(ThemeSchema)
|
||||
},
|
||||
|
||||
{
|
||||
label: '代码高亮',
|
||||
icon: 'fa fa-code',
|
||||
path: '/examples/code',
|
||||
component: makeSchemaRenderer(CodeSchema)
|
||||
},
|
||||
|
||||
{
|
||||
label: '向导',
|
||||
icon: 'fa fa-desktop',
|
||||
|
10
packages/amis-ui/scss/components/_code.scss
Normal file
10
packages/amis-ui/scss/components/_code.scss
Normal file
@ -0,0 +1,10 @@
|
||||
.#{$ns}Code {
|
||||
&--dark {
|
||||
background-color: #1e1e1e;
|
||||
border-radius: var(--borderRadius);
|
||||
}
|
||||
|
||||
&-pre-wrap {
|
||||
padding: var(--sizes-size-5);
|
||||
}
|
||||
}
|
@ -46,6 +46,7 @@
|
||||
@import '../components/color';
|
||||
@import '../components/condition-builder';
|
||||
@import '../components/context-menu';
|
||||
@import '../components/code';
|
||||
@import '../components/wizard';
|
||||
@import '../components/crud';
|
||||
@import '../components/crud2';
|
||||
|
@ -2,11 +2,21 @@
|
||||
* @file 代码高亮
|
||||
*/
|
||||
import React from 'react';
|
||||
import isEqual from 'lodash/isEqual';
|
||||
import isPlainObject from 'lodash/isPlainObject';
|
||||
import {BaseSchema} from '../Schema';
|
||||
import {Renderer, RendererProps} from 'amis-core';
|
||||
import {detectPropValueChanged, getPropValue} from 'amis-core';
|
||||
import {Renderer, RendererProps, anyChanged} from 'amis-core';
|
||||
import {getPropValue} from 'amis-core';
|
||||
import {isPureVariable, resolveVariableAndFilter} from 'amis-core';
|
||||
import type {editor as EditorNamespace} from 'monaco-editor';
|
||||
|
||||
export type MonacoEditor = typeof EditorNamespace;
|
||||
|
||||
export type CodeBuiltinTheme = EditorNamespace.BuiltinTheme;
|
||||
|
||||
export type IMonaco = {
|
||||
editor: MonacoEditor;
|
||||
[propName: string]: any;
|
||||
};
|
||||
|
||||
// 自定义语言的 token
|
||||
export interface Token {
|
||||
@ -52,6 +62,11 @@ export interface CustomLang {
|
||||
* token
|
||||
*/
|
||||
tokens: Token[];
|
||||
|
||||
/**
|
||||
* 编辑器颜色相关配置,不传使用内置默认值
|
||||
*/
|
||||
colors?: EditorNamespace.IColors;
|
||||
}
|
||||
|
||||
/**
|
||||
@ -106,7 +121,7 @@ export interface CodeSchema extends BaseSchema {
|
||||
| 'yaml'
|
||||
| string;
|
||||
|
||||
editorTheme?: string;
|
||||
editorTheme?: CodeBuiltinTheme;
|
||||
|
||||
/**
|
||||
* tab 大小
|
||||
@ -122,13 +137,28 @@ export interface CodeSchema extends BaseSchema {
|
||||
* 自定义语言
|
||||
*/
|
||||
customLang?: CustomLang;
|
||||
|
||||
/**
|
||||
* 使用的标签,默认多行使用pre,单行使用code
|
||||
*/
|
||||
wrapperComponent?: string;
|
||||
}
|
||||
|
||||
export interface CodeProps
|
||||
extends RendererProps,
|
||||
Omit<CodeSchema, 'type' | 'className'> {}
|
||||
Omit<CodeSchema, 'type' | 'className' | 'wrapperComponent'> {
|
||||
wrapperComponent?: any;
|
||||
}
|
||||
|
||||
export default class Code extends React.Component<CodeProps> {
|
||||
static propsList: string[] = [
|
||||
'language',
|
||||
'editorTheme',
|
||||
'tabSize',
|
||||
'wordWrap',
|
||||
'customLang'
|
||||
];
|
||||
|
||||
static defaultProps: Partial<CodeProps> = {
|
||||
language: 'plaintext',
|
||||
editorTheme: 'vs',
|
||||
@ -136,7 +166,7 @@ export default class Code extends React.Component<CodeProps> {
|
||||
wordWrap: true
|
||||
};
|
||||
|
||||
monaco: any;
|
||||
monaco: IMonaco;
|
||||
toDispose: Array<Function> = [];
|
||||
codeRef = React.createRef<HTMLElement>();
|
||||
customLang: CustomLang;
|
||||
@ -146,59 +176,112 @@ export default class Code extends React.Component<CodeProps> {
|
||||
super(props);
|
||||
}
|
||||
|
||||
shouldComponentUpdate(nextProps: CodeProps) {
|
||||
return (
|
||||
anyChanged(Code.propsList, this.props, nextProps) ||
|
||||
this.resolveLanguage(this.props) !== this.resolveLanguage(nextProps) ||
|
||||
getPropValue(this.props) !== getPropValue(nextProps)
|
||||
);
|
||||
}
|
||||
|
||||
componentDidMount() {
|
||||
import('monaco-editor').then(monaco => this.handleMonaco(monaco));
|
||||
}
|
||||
|
||||
componentDidUpdate(preProps: CodeProps) {
|
||||
async componentDidUpdate(preProps: CodeProps) {
|
||||
const props = this.props;
|
||||
const dom = this.codeRef.current;
|
||||
|
||||
const sourceCode = getPropValue(this.props);
|
||||
const preSourceCode = getPropValue(this.props);
|
||||
|
||||
if (
|
||||
sourceCode !== preSourceCode ||
|
||||
(props.customLang && !isEqual(props.customLang, preProps.customLang))
|
||||
) {
|
||||
const dom = this.codeRef.current!;
|
||||
dom.innerHTML = sourceCode;
|
||||
const theme = this.registTheme() || this.props.editorTheme || 'vs';
|
||||
setTimeout(() => {
|
||||
this.monaco.editor.colorizeElement(dom, {
|
||||
tabSize: this.props.tabSize,
|
||||
theme
|
||||
});
|
||||
}, 16);
|
||||
if (this?.monaco?.editor && dom) {
|
||||
const {tabSize} = props;
|
||||
const sourceCode = getPropValue(this.props);
|
||||
const language = this.resolveLanguage();
|
||||
const theme = this.registerAndGetTheme();
|
||||
/**
|
||||
* FIXME: https://github.com/microsoft/monaco-editor/issues/338
|
||||
* 已知问题:变量的样式存储在顶层,所以同页面中存在多个editor时,切换主题对所有editor生效
|
||||
* 每个组件单独实例化一个editor可以处理,但是成本较高,目前官方的处理方式是iframe嵌套隔离
|
||||
*/
|
||||
this.monaco.editor.setTheme(theme);
|
||||
/**
|
||||
* colorizeElement可能会存在延迟加载的editor触发更新,导致sourceCode覆盖已经处理的innerHTML
|
||||
* 使用colorize,每次基于code构建HTML, 保证一致性
|
||||
*/
|
||||
const colorizedHtml = await this.monaco.editor.colorize(
|
||||
sourceCode,
|
||||
language,
|
||||
{
|
||||
tabSize
|
||||
}
|
||||
);
|
||||
dom.innerHTML = colorizedHtml;
|
||||
}
|
||||
}
|
||||
|
||||
handleMonaco(monaco: any) {
|
||||
this.monaco = monaco;
|
||||
if (this.codeRef.current) {
|
||||
const dom = this.codeRef.current;
|
||||
const theme = this.registTheme() || this.props.editorTheme || 'vs';
|
||||
// 这里必须是异步才能准确,可能是因为 monaco 里注册主题是异步的
|
||||
setTimeout(() => {
|
||||
monaco.editor.colorizeElement(dom, {
|
||||
tabSize: this.props.tabSize,
|
||||
theme
|
||||
});
|
||||
}, 16);
|
||||
}
|
||||
}
|
||||
|
||||
registTheme() {
|
||||
const monaco = this.monaco;
|
||||
async handleMonaco(monaco: any) {
|
||||
if (!monaco) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
this.monaco = monaco;
|
||||
const {tabSize} = this.props;
|
||||
const sourceCode = getPropValue(this.props);
|
||||
const language = this.resolveLanguage();
|
||||
const dom = this.codeRef.current;
|
||||
|
||||
if (dom && this.monaco?.editor) {
|
||||
const theme = this.registerAndGetTheme();
|
||||
// 这里必须是异步才能准确,可能是因为 monaco 里注册主题是异步的
|
||||
this.monaco.editor.setTheme(theme);
|
||||
const colorizedHtml = await this.monaco.editor.colorize(
|
||||
sourceCode,
|
||||
language,
|
||||
{
|
||||
tabSize
|
||||
}
|
||||
);
|
||||
dom.innerHTML = colorizedHtml;
|
||||
}
|
||||
}
|
||||
|
||||
resolveLanguage(props?: CodeProps) {
|
||||
const currentProps = props ?? this.props;
|
||||
const {customLang, data} = currentProps;
|
||||
let {language = 'plaintext'} = currentProps;
|
||||
|
||||
if (isPureVariable(language)) {
|
||||
language = resolveVariableAndFilter(language, data);
|
||||
}
|
||||
|
||||
if (customLang) {
|
||||
if (customLang.name) {
|
||||
language = customLang.name;
|
||||
}
|
||||
}
|
||||
|
||||
return language;
|
||||
}
|
||||
|
||||
/** 注册并返回当前主题名称,如果未自定义主题,则范围editorTheme值,默认为'vs' */
|
||||
registerAndGetTheme() {
|
||||
const monaco = this.monaco;
|
||||
const {editorTheme = 'vs'} = this.props;
|
||||
|
||||
if (!monaco) {
|
||||
return editorTheme;
|
||||
}
|
||||
|
||||
if (
|
||||
this.customLang &&
|
||||
this.customLang.name &&
|
||||
this.customLang.tokens &&
|
||||
Array.isArray(this.customLang.tokens) &&
|
||||
this.customLang.tokens.length
|
||||
) {
|
||||
const langName = this.customLang.name;
|
||||
const colors =
|
||||
this.customLang?.colors && isPlainObject(this.customLang?.colors)
|
||||
? this.customLang.colors
|
||||
: {};
|
||||
monaco.languages.register({id: langName});
|
||||
|
||||
const tokenizers = [];
|
||||
@ -222,37 +305,53 @@ export default class Code extends React.Component<CodeProps> {
|
||||
monaco.editor.defineTheme(langName, {
|
||||
base: 'vs',
|
||||
inherit: false,
|
||||
rules: rules
|
||||
rules: rules,
|
||||
colors
|
||||
});
|
||||
|
||||
return langName;
|
||||
}
|
||||
return null;
|
||||
|
||||
return editorTheme;
|
||||
}
|
||||
|
||||
render() {
|
||||
const {className, classnames: cx, data, customLang, wordWrap} = this.props;
|
||||
let language = this.props.language;
|
||||
const sourceCode = getPropValue(this.props);
|
||||
if (isPureVariable(language)) {
|
||||
language = resolveVariableAndFilter(language, data);
|
||||
}
|
||||
const {
|
||||
className,
|
||||
classnames: cx,
|
||||
editorTheme,
|
||||
customLang,
|
||||
wordWrap,
|
||||
wrapperComponent
|
||||
} = this.props;
|
||||
const language = this.resolveLanguage();
|
||||
const isMultiLine =
|
||||
typeof sourceCode === 'string' && sourceCode.split(/\r?\n/).length > 1;
|
||||
const Component = wrapperComponent || (isMultiLine ? 'pre' : 'code');
|
||||
|
||||
if (customLang) {
|
||||
if (customLang.name) {
|
||||
language = customLang.name;
|
||||
}
|
||||
this.customLang = customLang;
|
||||
}
|
||||
|
||||
return (
|
||||
<code
|
||||
<Component
|
||||
ref={this.codeRef}
|
||||
className={cx(`Code`, {'word-break': wordWrap}, className)}
|
||||
className={cx(
|
||||
'Code',
|
||||
{
|
||||
// 使用内置暗色主题时设置一下背景,避免看不清
|
||||
'Code--dark':
|
||||
editorTheme && ['vs-dark', 'hc-black'].includes(editorTheme),
|
||||
'Code-pre-wrap': Component === 'pre',
|
||||
'word-break': wordWrap
|
||||
},
|
||||
className
|
||||
)}
|
||||
data-lang={language}
|
||||
>
|
||||
{sourceCode}
|
||||
</code>
|
||||
</Component>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1429,7 +1429,11 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
multiLine ? `Combo--ver` : `Combo--hor`,
|
||||
noBorder ? `Combo--noBorder` : '',
|
||||
disabled ? 'is-disabled' : '',
|
||||
!isStatic && !disabled && draggable && Array.isArray(value) && value.length > 1
|
||||
!isStatic &&
|
||||
!disabled &&
|
||||
draggable &&
|
||||
Array.isArray(value) &&
|
||||
value.length > 1
|
||||
? 'is-draggable'
|
||||
: ''
|
||||
)}
|
||||
@ -1668,8 +1672,8 @@ export default class ComboControl extends React.Component<ComboProps> {
|
||||
// 当有staticSchema 或 type = input-kv | input-kvs
|
||||
// 才拦截处理,其他情况交给子表单项处理即可
|
||||
if (
|
||||
isStatic
|
||||
&& (staticSchema || ['input-kv', 'input-kvs'].includes(type))
|
||||
isStatic &&
|
||||
(staticSchema || ['input-kv', 'input-kvs'].includes(type))
|
||||
) {
|
||||
return this.renderStatic();
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user