|
|
|
@ -0,0 +1,173 @@
|
|
|
|
|
import React, { ComponentType, ReactNode } from 'react';
|
|
|
|
|
import type { TagProps, AstNode } from './type';
|
|
|
|
|
import { parse } from '@bbob/parser';
|
|
|
|
|
import _last from 'lodash/last';
|
|
|
|
|
import _set from 'lodash/set';
|
|
|
|
|
import _get from 'lodash/get';
|
|
|
|
|
import _has from 'lodash/has';
|
|
|
|
|
import _isObject from 'lodash/isObject';
|
|
|
|
|
import _isArray from 'lodash/isArray';
|
|
|
|
|
import _isEmpty from 'lodash/isEmpty';
|
|
|
|
|
import _mapKeys from 'lodash/mapKeys';
|
|
|
|
|
import _toPairs from 'lodash/toPairs';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 通用的bbcode解释器
|
|
|
|
|
* 一个纯语言实现
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
type StringTagComponent = ComponentType<{ children?: string }> | string;
|
|
|
|
|
type ObjectTagComponent = ComponentType<TagProps>;
|
|
|
|
|
type TagMapComponent = StringTagComponent | ObjectTagComponent;
|
|
|
|
|
|
|
|
|
|
const tagMap: { [tag: string]: TagMapComponent } = {};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 注册一个组件到内部的tagMap中
|
|
|
|
|
* @param tagName 标签名
|
|
|
|
|
* @param component 组件
|
|
|
|
|
*/
|
|
|
|
|
export const registerBBCodeTag = (
|
|
|
|
|
tagName: string,
|
|
|
|
|
component: TagMapComponent
|
|
|
|
|
) => {
|
|
|
|
|
tagMap[tagName] = component;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const DefaultBBCodeComponent: React.FC<TagProps> = React.memo((props) => {
|
|
|
|
|
if (_has(tagMap, '_text')) {
|
|
|
|
|
const Component = tagMap['_text'] as StringTagComponent;
|
|
|
|
|
return <Component>{props.node.content.join('')}</Component>;
|
|
|
|
|
} else {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
DefaultBBCodeComponent.displayName = 'DefaultBBCodeComponent';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取BBCode标签组件
|
|
|
|
|
*/
|
|
|
|
|
export const getBBCodeTag = (tagName: string): TagMapComponent => {
|
|
|
|
|
return tagMap[tagName] ?? DefaultBBCodeComponent;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* BBCode 解析器
|
|
|
|
|
*/
|
|
|
|
|
class BBCodeParser {
|
|
|
|
|
options = {
|
|
|
|
|
onlyAllowTags: Object.keys(tagMap),
|
|
|
|
|
onError: (err) => {
|
|
|
|
|
console.warn(err.message, err.lineNumber, err.columnNumber);
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 将文本中没有被bbcode标签包裹住的部分进行预处理后重新拼装成bbcode字符串
|
|
|
|
|
*/
|
|
|
|
|
preProcessText(input: string, processFn: (text: string) => string): string {
|
|
|
|
|
const ast = parse(input, this.options) as AstNode[];
|
|
|
|
|
|
|
|
|
|
return ast
|
|
|
|
|
.map((node, index) => {
|
|
|
|
|
if (typeof node === 'string') {
|
|
|
|
|
// 此处进行预处理
|
|
|
|
|
const text = node;
|
|
|
|
|
return processFn(text);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const { tag, content, attrs } = node;
|
|
|
|
|
const attrsStr = _toPairs(attrs)
|
|
|
|
|
.map(([key, value]) => {
|
|
|
|
|
if (key === value) {
|
|
|
|
|
return `=${value}`;
|
|
|
|
|
} else {
|
|
|
|
|
return ` ${key}=${value}`;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
// NOTICE: 这里排序看起来好像有问题,但是attrs的顺序是有序的,所以没有问题
|
|
|
|
|
.join('');
|
|
|
|
|
return `[${tag}${attrsStr}]${content}[/${tag}]`;
|
|
|
|
|
})
|
|
|
|
|
.join('');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将bbcode字符串转化为AstNode
|
|
|
|
|
parse(input: string): AstNode[] {
|
|
|
|
|
try {
|
|
|
|
|
return parse(input, this.options).map((node: AstNode) => {
|
|
|
|
|
if (_isObject(node)) {
|
|
|
|
|
const content = _get(node, 'content');
|
|
|
|
|
const attrs = _get(node, 'attrs');
|
|
|
|
|
|
|
|
|
|
if (_isEmpty(attrs) && _isArray(content) && content.length === 0) {
|
|
|
|
|
// 如果是[text]这种格式的话会被误解析成一个节点
|
|
|
|
|
// 做一下特殊处理
|
|
|
|
|
// NOTICE: 这种处理方式会将[url][/url]解析成字符串[url]
|
|
|
|
|
// 最好的解决方案是自己写一个BBCode的词法解释器
|
|
|
|
|
const tag = _get(node, 'tag');
|
|
|
|
|
if (typeof tag === 'string') {
|
|
|
|
|
return `[${tag}]`;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 将[url=http://baidu.com] 解析出的attrs: { 'http://baidu.com': 'http://baidu.com' }
|
|
|
|
|
// 转换为attrs: { 'url': 'http://baidu.com' }
|
|
|
|
|
_set(
|
|
|
|
|
node,
|
|
|
|
|
'attrs',
|
|
|
|
|
_mapKeys(attrs, (value, key) => {
|
|
|
|
|
if (value === key) {
|
|
|
|
|
return node.tag;
|
|
|
|
|
} else {
|
|
|
|
|
return key;
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return node;
|
|
|
|
|
});
|
|
|
|
|
} catch (e) {
|
|
|
|
|
console.warn(e);
|
|
|
|
|
return [];
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
render(input: string): ReactNode[] {
|
|
|
|
|
const ast = this.parse(input);
|
|
|
|
|
|
|
|
|
|
return ast
|
|
|
|
|
.reduce<AstNode[]>((prev, curr) => {
|
|
|
|
|
if (typeof curr === 'string' && typeof _last(prev) === 'string') {
|
|
|
|
|
// 合并字符串, 使其渲染时能公用一个Text组件
|
|
|
|
|
prev[prev.length - 1] += curr;
|
|
|
|
|
} else {
|
|
|
|
|
prev.push(curr);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return prev;
|
|
|
|
|
}, [])
|
|
|
|
|
.map<ReactNode>((node, index) => {
|
|
|
|
|
if (typeof node === 'string') {
|
|
|
|
|
if (_has(tagMap, '_text')) {
|
|
|
|
|
const Component = tagMap['_text'] as StringTagComponent;
|
|
|
|
|
return <Component key={index}>{node}</Component>;
|
|
|
|
|
} else {
|
|
|
|
|
return node;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (typeof node === 'object') {
|
|
|
|
|
const Component = getBBCodeTag(node.tag);
|
|
|
|
|
return <Component key={index} node={node} />;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return null;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const bbcodeParser = new BBCodeParser();
|
|
|
|
|
|
|
|
|
|
export default bbcodeParser;
|