fix: Code组件部分情况无法渲染问题,自定义高亮失效问题 (#5642)

This commit is contained in:
RUNZE LU 2022-10-31 22:33:30 +08:00 committed by GitHub
parent 6d26ee61d1
commit 0dbc79e464
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 223 additions and 92 deletions

View File

@ -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 个配置项:

View 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` 代表加粗'
}
]
};

View File

@ -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',

View File

@ -0,0 +1,10 @@
.#{$ns}Code {
&--dark {
background-color: #1e1e1e;
border-radius: var(--borderRadius);
}
&-pre-wrap {
padding: var(--sizes-size-5);
}
}

View File

@ -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';

View File

@ -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
* 使colorizecode构建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>
);
}
}

View File

@ -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();
}