From a7f04a356a8f60877be0ca1414a9ef408b20a5f7 Mon Sep 17 00:00:00 2001 From: Trim21 Date: Wed, 30 Nov 2022 08:35:27 +0800 Subject: [PATCH 1/4] add test case --- packages/utils/bbcode/__test__/html.test.ts | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/packages/utils/bbcode/__test__/html.test.ts b/packages/utils/bbcode/__test__/html.test.ts index c2cc8baa7..2ac2bf491 100644 --- a/packages/utils/bbcode/__test__/html.test.ts +++ b/packages/utils/bbcode/__test__/html.test.ts @@ -239,4 +239,15 @@ describe('html render bbcode string', () => { '
ss[b]加粗\n换行了[/b](bgm38) [/fafa [code]
', ); }); + + test('render code by custom converter, nested', () => { + const input = '[b][url]qq[/url][/b]'; + expect( + render(input, { + url: (node) => { + return '[url]convert map[/url]'; + }, + }), + ).toBe('[url]convert map[/url]'); + }); }); From d729e4623a5757c45ef1a3e1132339905e8c2c0c Mon Sep 17 00:00:00 2001 From: 22earth Date: Wed, 30 Nov 2022 15:20:59 +0800 Subject: [PATCH 2/4] fix(utils): nested bbcode with custom converter feat(utils): allow Parser to ignore tag --- packages/utils/bbcode/__test__/html.test.ts | 17 +++++++-- packages/utils/bbcode/__test__/parser.test.ts | 36 +++++++++++++++++++ packages/utils/bbcode/convert.ts | 13 ++++--- packages/utils/bbcode/html.ts | 16 +++++++-- packages/utils/bbcode/index.ts | 2 +- packages/utils/bbcode/parser.ts | 21 ++++++++--- 6 files changed, 89 insertions(+), 16 deletions(-) diff --git a/packages/utils/bbcode/__test__/html.test.ts b/packages/utils/bbcode/__test__/html.test.ts index 2ac2bf491..8a968671c 100644 --- a/packages/utils/bbcode/__test__/html.test.ts +++ b/packages/utils/bbcode/__test__/html.test.ts @@ -1,5 +1,6 @@ import { STICKER_DOMAIN_URL } from '../constants'; -import { renderNodes, renderNode, render } from '../html'; +import { renderNodes, renderNode, render, renderWithParser } from '../html'; +import { Parser } from '../parser'; import type { VNode } from '../types'; describe('html render vnode', () => { @@ -241,7 +242,7 @@ describe('html render bbcode string', () => { }); test('render code by custom converter, nested', () => { - const input = '[b][url]qq[/url][/b]'; + const input = '[b][url]http://qq.com[/url][/b]'; expect( render(input, { url: (node) => { @@ -250,4 +251,16 @@ describe('html render bbcode string', () => { }), ).toBe('[url]convert map[/url]'); }); + test('render code with Parser, nested', () => { + const input = '[b]加粗[size=16]16px[img]http://chii.in/img/ico/bgm88-31.gif[/img][/size][/b]'; + expect( + renderWithParser(new Parser(input, [], ['img']), { + url: (node) => { + return '[url]convert map[/url]'; + }, + }), + ).toBe( + '加粗16px[img]http://chii.in/img/ico/bgm88-31.gif[/img]', + ); + }); }); diff --git a/packages/utils/bbcode/__test__/parser.test.ts b/packages/utils/bbcode/__test__/parser.test.ts index 6ae837c6a..f14ca3ca3 100644 --- a/packages/utils/bbcode/__test__/parser.test.ts +++ b/packages/utils/bbcode/__test__/parser.test.ts @@ -89,6 +89,15 @@ describe('bbcode parser', () => { ]; expect(getNodes(input)).toEqual(expect.arrayContaining(tests)); }); + test('ignore img bbcode', () => { + const input = `存放于其他网络服务器的图片: +[img]http://chii.in/img/ico/bgm88-31.gif[/img]`; + const tests: CodeNodeTypes[] = [ + '存放于其他网络服务器的图片:\n', + '[img]http://chii.in/img/ico/bgm88-31.gif[/img]', + ]; + expect(new Parser(input, [], ['img']).parse()).toEqual(expect.arrayContaining(tests)); + }); test('simple bbcode', () => { const input = `我是[mask]马赛克文字[/mask] [s]删除线文字[/s] @@ -320,6 +329,33 @@ describe('bbcode parser', () => { ]), ); }); + test('merge tags with ignore case', () => { + const fn = (): boolean => true; + const tags = mergeTags( + [ + 'i', + 'b', + { + name: 's', + schema: { + s: fn, + }, + }, + ], + [ + { + name: 'i', + schema: { + i: fn, + }, + }, + 's', + 'mybbcode', + ], + ['i', 's'], + ); + expect(tags).toEqual(expect.arrayContaining(['b', 'mybbcode'])); + }); test('subject bbcode', () => { const tests: Array<[string, CodeNodeTypes[]]> = [ [ diff --git a/packages/utils/bbcode/convert.ts b/packages/utils/bbcode/convert.ts index b2bccfc35..8eb914b8e 100644 --- a/packages/utils/bbcode/convert.ts +++ b/packages/utils/bbcode/convert.ts @@ -151,6 +151,12 @@ function toVNode( return vnode; } +let UserConverterFnMap: Record = {}; + +export function setUserConverter(map: Record): void { + UserConverterFnMap = map; +} + const CONVERTER_FN_MAP: Record = { b: (node) => toVNode(node, 'strong'), i: (node) => toVNode(node, 'em'), @@ -233,14 +239,11 @@ const CONVERTER_FN_MAP: Record = { user: convertUser, }; -export function convert( - node: CodeNodeTypes, - converterMap: Record = {}, -): NodeTypes { +export function convert(node: CodeNodeTypes): NodeTypes { if (typeof node === 'string') { return node; } - let converterFn = converterMap[node.type]; + let converterFn = UserConverterFnMap[node.type]; if (!converterFn) { converterFn = CONVERTER_FN_MAP[node.type]; } diff --git a/packages/utils/bbcode/html.ts b/packages/utils/bbcode/html.ts index 849709022..5986b215f 100644 --- a/packages/utils/bbcode/html.ts +++ b/packages/utils/bbcode/html.ts @@ -1,5 +1,5 @@ import { UnreadableCodeError } from '../index'; -import { convert } from './convert'; +import { convert, setUserConverter } from './convert'; import { Parser } from './parser'; import type { CodeNodeTypes, ConverterFn, NodeTypes, VNode } from './types'; @@ -84,10 +84,20 @@ export function renderNodes(nodes: NodeTypes[], parentNode?: VNode): string { } export function render(rawStr: string, converterMap: Record = {}): string { + return renderWithParser(new Parser(rawStr), converterMap); +} + +export function renderWithParser( + parser: Parser, + converterMap: Record = {}, +): string { let result = ''; - const nodes: CodeNodeTypes[] = new Parser(rawStr).parse(); + const nodes: CodeNodeTypes[] = parser.parse(); + setUserConverter(converterMap); nodes.forEach((node) => { - result += renderNode(convert(node, converterMap)); + result += renderNode(convert(node)); }); + // 重置为默认值 @TODO 改成类避免使用全局变量 + setUserConverter({}); return result; } diff --git a/packages/utils/bbcode/index.ts b/packages/utils/bbcode/index.ts index 9e4b939b8..4e6de7b76 100644 --- a/packages/utils/bbcode/index.ts +++ b/packages/utils/bbcode/index.ts @@ -1,4 +1,4 @@ -export { render } from './html'; +export { render, renderWithParser } from './html'; export * from './parser'; export { convert } from './convert'; export * from './types'; diff --git a/packages/utils/bbcode/parser.ts b/packages/utils/bbcode/parser.ts index 3bc02cd9f..669fbc624 100644 --- a/packages/utils/bbcode/parser.ts +++ b/packages/utils/bbcode/parser.ts @@ -123,15 +123,23 @@ const DEFAULT_TAGS: ITag[] = [ ]; // 合并 tag 配置。新的覆盖旧的 -export function mergeTags(tagList: ITag[], toMergeTags: ITag[]): ITag[] { - const results: ITag[] = [...toMergeTags]; - tagList.forEach((tag) => { +export function mergeTags( + tagList: ITag[], + toMergeTags: ITag[], + ignoreTagNames: string[] = [], +): ITag[] { + let results: ITag[] = [...toMergeTags]; + const getTagName = (tag: ITag) => { let name = ''; if (typeof tag === 'string') { name = tag; } else { name = tag.name; } + return name; + }; + tagList.forEach((tag) => { + const name = getTagName(tag); const idx = results.findIndex((t) => { if (typeof t === 'string') { return t === name; @@ -142,6 +150,9 @@ export function mergeTags(tagList: ITag[], toMergeTags: ITag[]): ITag[] { results.push(tag); } }); + results = results.filter((tag) => { + return !ignoreTagNames.includes(getTagName(tag)); + }); return results; } @@ -153,13 +164,13 @@ export class Parser { private readonly tagStack: string[]; private readonly validTags: ITag[]; - constructor(input: string, tags: ITag[] = []) { + constructor(input: string, tags: ITag[] = [], ignoreTagNames: string[] = []) { this.input = input; this.pos = 0; this.ctxStack = []; this.tagStack = []; // 解析器支持的 tag; sticker 用来表示 Bangumi 的表情,不是 bbcode - this.validTags = mergeTags(DEFAULT_TAGS, tags); + this.validTags = mergeTags(DEFAULT_TAGS, tags, ignoreTagNames); } parse(): CodeNodeTypes[] { From 673ab2ebc3da9c9e598350b79c7b0eebfcbc5e3e Mon Sep 17 00:00:00 2001 From: 22earth Date: Thu, 1 Dec 2022 10:59:58 +0800 Subject: [PATCH 3/4] refactor(bbcode): converter class --- packages/utils/bbcode/convert.ts | 314 ++++++++++++++++--------------- packages/utils/bbcode/html.ts | 7 +- 2 files changed, 164 insertions(+), 157 deletions(-) diff --git a/packages/utils/bbcode/convert.ts b/packages/utils/bbcode/convert.ts index de731bd7b..92b95cf92 100644 --- a/packages/utils/bbcode/convert.ts +++ b/packages/utils/bbcode/convert.ts @@ -39,37 +39,6 @@ function convertImgNode(node: CodeVNode): VNode { return vnode; } -function setVNodeChildren(vnode: VNode, node: CodeVNode): void { - if (node.children) { - vnode.children = node.children.map((c) => convert(c)); - } -} - -function convertUrlNode(node: CodeVNode): VNode { - let href = node.props?.url; - if (!href) { - href = node.children![0] as string; - } - const vnode: VNode = { - type: 'a', - props: { - href, - }, - className: 'l', - }; - if (node.children) { - vnode.children = node.children.map((c) => convert(c)); - } - if (isExternalLink(href)) { - vnode.props = { - ...vnode.props, - target: '_blank', - ref: 'nofollow external noopener noreferrer', - }; - } - return vnode; -} - function convertStickerNode(node: CodeVNode): string { const stickerId = node.props!.stickerId!; let id = -1; @@ -117,18 +86,6 @@ function convertStickerNode(node: CodeVNode): string { return stickerId; } -function convertQuote(node: CodeVNode): VNode { - const q: VNode = { - type: 'q', - }; - setVNodeChildren(q, node); - return { - type: 'div', - className: 'quote', - children: [q], - }; -} - function convertUser(node: CodeVNode): VNode { let userId = node.props?.user; if (!userId) { @@ -144,121 +101,174 @@ function convertUser(node: CodeVNode): VNode { }; } -function toVNode( - node: CodeVNode, - type: string, - props: Pick = {}, -): VNode { - const vnode: VNode = { - type, - ...props, - }; - setVNodeChildren(vnode, node); - return vnode; +export function convert( + node: CodeNodeTypes, + converterMap: Record = {}, +): NodeTypes { + const converter = new Converter(converterMap); + return converter.convert(node); } -let UserConverterFnMap: Record = {}; +export class Converter { + readonly fnMap: Record = {}; + readonly defaultFnMap: Record = { + b: (node) => this.toVNode(node, 'strong'), + i: (node) => this.toVNode(node, 'em'), + u: (node) => + this.toVNode(node, 'span', { + style: { + 'text-decoration': 'underline', + }, + }), + s: (node) => + this.toVNode(node, 'span', { + style: { + 'text-decoration': 'line-through', + }, + }), + mask: (node) => + this.toVNode(node, 'span', { + style: { + 'background-color': '#555', + color: '#555', + border: '1px solid #555', + }, + }), + color: (node) => + this.toVNode(node, 'span', { + style: { + color: node.props!.color!, + }, + }), + size: (node) => + this.toVNode(node, 'span', { + style: { + 'font-size': node.props!.size! + 'px', + 'line-height': node.props!.size! + 'px', + }, + }), + url: (node) => this.convertUrlNode(node), + img: convertImgNode, + sticker: convertStickerNode, + quote: (node) => this.convertQuote(node), + code: (node) => ({ + type: 'pre', + children: node.children, + }), + left: (node) => + this.toVNode(node, 'p', { + style: { + 'text-align': 'left', + }, + }), + right: (node) => + this.toVNode(node, 'p', { + style: { + 'text-align': 'right', + }, + }), + center: (node) => + this.toVNode(node, 'p', { + style: { + 'text-align': 'center', + }, + }), + indent: (node) => this.toVNode(node, 'blockquote', {}), + align: (node) => + this.toVNode(node, 'p', { + style: { + 'text-align': node.props!.align!, + }, + }), + float: (node) => + this.toVNode(node, 'span', { + style: { + float: node.props!.float!, + }, + }), + subject: (node) => + this.toVNode(node, 'a', { + className: 'l', + }), + user: convertUser, + }; -export function setUserConverter(map: Record): void { - UserConverterFnMap = map; -} + constructor(converterMap: Record) { + this.fnMap = converterMap; + } -const CONVERTER_FN_MAP: Record = { - b: (node) => toVNode(node, 'strong'), - i: (node) => toVNode(node, 'em'), - u: (node) => - toVNode(node, 'span', { - style: { - 'text-decoration': 'underline', - }, - }), - s: (node) => - toVNode(node, 'span', { - style: { - 'text-decoration': 'line-through', - }, - }), - mask: (node) => - toVNode(node, 'span', { - style: { - 'background-color': '#555', - color: '#555', - border: '1px solid #555', - }, - }), - color: (node) => - toVNode(node, 'span', { - style: { - color: node.props!.color!, - }, - }), - size: (node) => - toVNode(node, 'span', { - style: { - 'font-size': node.props!.size! + 'px', - 'line-height': node.props!.size! + 'px', - }, - }), - url: convertUrlNode, - img: convertImgNode, - sticker: convertStickerNode, - quote: convertQuote, - code: (node) => ({ - type: 'pre', - children: node.children, - }), - left: (node) => - toVNode(node, 'p', { - style: { - 'text-align': 'left', - }, - }), - right: (node) => - toVNode(node, 'p', { - style: { - 'text-align': 'right', - }, - }), - center: (node) => - toVNode(node, 'p', { - style: { - 'text-align': 'center', - }, - }), - indent: (node) => toVNode(node, 'blockquote', {}), - align: (node) => - toVNode(node, 'p', { - style: { - 'text-align': node.props!.align!, - }, - }), - float: (node) => - toVNode(node, 'span', { - style: { - float: node.props!.float!, - }, - }), - subject: (node) => - toVNode(node, 'a', { - className: 'l', - }), - user: convertUser, -}; + convert(node: CodeNodeTypes): NodeTypes { + if (typeof node === 'string') { + return node; + } + const converterFn = this.getConvertFn(node.type); + if (converterFn) { + return converterFn(node); + } + const vnode: VNode = { + type: node.type, + }; + this.setVNodeChildren(vnode, node); + return vnode; + } + + getConvertFn(type: string): ConverterFn | undefined { + let converterFn = this.fnMap[type]; + if (!converterFn) { + converterFn = this.defaultFnMap[type]; + } + return converterFn; + } -export function convert(node: CodeNodeTypes): NodeTypes { - if (typeof node === 'string') { - return node; + toVNode(node: CodeVNode, type: string, props: Pick = {}): VNode { + const vnode: VNode = { + type, + ...props, + }; + this.setVNodeChildren(vnode, node); + return vnode; } - let converterFn = UserConverterFnMap[node.type]; - if (!converterFn) { - converterFn = CONVERTER_FN_MAP[node.type]; + + setVNodeChildren(vnode: VNode, node: CodeVNode): void { + if (node.children) { + vnode.children = node.children.map((c) => this.convert(c)); + } } - if (converterFn) { - return converterFn(node); + + convertQuote(node: CodeVNode): VNode { + const q: VNode = { + type: 'q', + }; + this.setVNodeChildren(q, node); + return { + type: 'div', + className: 'quote', + children: [q], + }; + } + + convertUrlNode(node: CodeVNode): VNode { + let href = node.props?.url; + if (!href) { + href = node.children![0] as string; + } + const vnode: VNode = { + type: 'a', + props: { + href, + }, + className: 'l', + }; + if (node.children) { + vnode.children = node.children.map((c) => this.convert(c)); + } + if (isExternalLink(href)) { + vnode.props = { + ...vnode.props, + target: '_blank', + ref: 'nofollow external noopener noreferrer', + }; + } + return vnode; } - const vnode: VNode = { - type: node.type, - }; - setVNodeChildren(vnode, node); - return vnode; } diff --git a/packages/utils/bbcode/html.ts b/packages/utils/bbcode/html.ts index 5986b215f..9d3034d54 100644 --- a/packages/utils/bbcode/html.ts +++ b/packages/utils/bbcode/html.ts @@ -1,5 +1,5 @@ import { UnreadableCodeError } from '../index'; -import { convert, setUserConverter } from './convert'; +import { convert } from './convert'; import { Parser } from './parser'; import type { CodeNodeTypes, ConverterFn, NodeTypes, VNode } from './types'; @@ -93,11 +93,8 @@ export function renderWithParser( ): string { let result = ''; const nodes: CodeNodeTypes[] = parser.parse(); - setUserConverter(converterMap); nodes.forEach((node) => { - result += renderNode(convert(node)); + result += renderNode(convert(node, converterMap)); }); - // 重置为默认值 @TODO 改成类避免使用全局变量 - setUserConverter({}); return result; } From c3b9a8af9d61f1055b7966c2d0f17142f8aa8c5c Mon Sep 17 00:00:00 2001 From: 22earth Date: Thu, 1 Dec 2022 11:11:44 +0800 Subject: [PATCH 4/4] fixed(bbcode): create multiple converter --- packages/utils/bbcode/html.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/packages/utils/bbcode/html.ts b/packages/utils/bbcode/html.ts index 9d3034d54..3131ef16e 100644 --- a/packages/utils/bbcode/html.ts +++ b/packages/utils/bbcode/html.ts @@ -1,5 +1,5 @@ import { UnreadableCodeError } from '../index'; -import { convert } from './convert'; +import { Converter } from './convert'; import { Parser } from './parser'; import type { CodeNodeTypes, ConverterFn, NodeTypes, VNode } from './types'; @@ -93,8 +93,9 @@ export function renderWithParser( ): string { let result = ''; const nodes: CodeNodeTypes[] = parser.parse(); + const converter = new Converter(converterMap); nodes.forEach((node) => { - result += renderNode(convert(node, converterMap)); + result += renderNode(converter.convert(node)); }); return result; }