Server IP : 80.87.202.40 / Your IP : 216.73.216.169 Web Server : Apache System : Linux rospirotorg.ru 5.14.0-539.el9.x86_64 #1 SMP PREEMPT_DYNAMIC Thu Dec 5 22:26:13 UTC 2024 x86_64 User : bitrix ( 600) PHP Version : 8.2.27 Disable Function : NONE MySQL : OFF | cURL : ON | WGET : ON | Perl : ON | Python : OFF | Sudo : ON | Pkexec : ON Directory : /home/bitrix/ext_www/rospirotorg.ru/bitrix/js/ui/bbcode/parser/src/ |
Upload File : |
import { Type } from 'main.core'; import { AstProcessor } from 'ui.bbcode.ast-processor'; import { getByIndex } from '../../shared'; import { BBCodeScheme, DefaultBBCodeScheme, BBCodeNode, typeof BBCodeRootNode, typeof BBCodeElementNode, typeof BBCodeTextNode, typeof BBCodeTagScheme, type BBCodeContentNode, type BBCodeSpecialCharNode, } from 'ui.bbcode.model'; import { BBCodeEncoder } from 'ui.bbcode.encoder'; import { Linkify } from 'ui.linkify'; import { ParserScheme } from './parser-scheme'; const TAG_REGEX: RegExp = /\[(\/)?(\w+|\*).*?]/; const TAG_REGEX_GS: RegExp = /\[(\/)?(\w+|\*)(.*?)]/gs; const LF = '\n'; const CRLF = '\r\n'; const TAB = '\t'; const isLinebreak = (symbol: string): boolean => { return [LF, CRLF].includes(symbol); }; const isTab = (symbol: string): boolean => { return symbol === TAB; }; const isSpecialChar = (symbol: string): boolean => { return isTab(symbol) || isLinebreak(symbol); }; const isList = (tagName: string): boolean => { return ['list', 'ul', 'ol'].includes(String(tagName).toLowerCase()); }; const isListItem = (tagName: string): boolean => { return ['*', 'li'].includes(String(tagName).toLowerCase()); }; const parserScheme = new ParserScheme(); type BBCodeParserOptions = { scheme?: BBCodeScheme, onUnknown?: (node: BBCodeContentNode, scheme: BBCodeScheme) => void, encoder?: BBCodeEncoder, linkify?: boolean, }; type NextTagResult = { tagName: string, isClosedTag: boolean, }; class BBCodeParser { scheme: BBCodeScheme; encoder: BBCodeEncoder; onUnknownHandler: () => any; allowedLinkify: boolean = true; constructor(options: BBCodeParserOptions = {}) { if (options.scheme) { this.setScheme(options.scheme); } else { this.setScheme(new DefaultBBCodeScheme()); } if (Type.isFunction(options.onUnknown)) { this.setOnUnknown(options.onUnknown); } else { this.setOnUnknown(BBCodeParser.defaultOnUnknownHandler); } if (options.encoder instanceof BBCodeEncoder) { this.setEncoder(options.encoder); } else { this.setEncoder(new BBCodeEncoder()); } if (Type.isBoolean(options.linkify)) { this.setIsAllowedLinkify(options.linkify); } } setScheme(scheme: BBCodeScheme) { this.scheme = scheme; } getScheme(): BBCodeScheme { return this.scheme; } setOnUnknown(handler: () => any) { if (!Type.isFunction(handler)) { throw new TypeError('handler is not a function'); } this.onUnknownHandler = handler; } getOnUnknownHandler(): () => any { return this.onUnknownHandler; } setEncoder(encoder: BBCodeEncoder) { if (encoder instanceof BBCodeEncoder) { this.encoder = encoder; } else { throw new TypeError('encoder is not BBCodeEncoder instance'); } } getEncoder(): BBCodeEncoder { return this.encoder; } setIsAllowedLinkify(value: boolean) { this.allowedLinkify = Boolean(value); } isAllowedLinkify(): boolean { return this.allowedLinkify; } canBeLinkified(node: BBCodeTextNode | BBCodeElementNode): boolean { if (node.getName() === '#text') { const notAllowedNodeNames = ['url', 'img', 'video', 'code']; const inNotAllowedNode = notAllowedNodeNames.some((name: string) => { return Boolean(AstProcessor.findParentNodeByName(node, name)); }); return !inNotAllowedNode; } return false; } static defaultOnUnknownHandler(node: BBCodeContentNode, scheme: BBCodeScheme): ?Array<BBCodeContentNode> { if (node.getType() === BBCodeNode.ELEMENT_NODE) { const nodeName: string = node.getName(); if (['left', 'center', 'right', 'justify'].includes(nodeName)) { const newNode = scheme.createElement({ name: 'p', }); node.replace(newNode); newNode.setChildren(node.getChildren()); } else if (['background', 'color', 'size'].includes(nodeName)) { const newNode = scheme.createElement({ name: 'b', }); node.replace(newNode); newNode.setChildren(node.getChildren()); } else if (['span', 'font'].includes(nodeName)) { const fragment = scheme.createFragment({ children: node.getChildren() }); node.replace(fragment); } else { const openingTag: string = node.getOpeningTag(); const closingTag: string = node.getClosingTag(); node.replace( scheme.createText(openingTag), ...node.getChildren(), scheme.createText(closingTag), ); } } } static toLowerCase(value: string): string { if (Type.isStringFilled(value)) { return value.toLowerCase(); } return value; } parseText(text: string): Array<BBCodeTextNode | BBCodeSpecialCharNode> { if (Type.isStringFilled(text)) { const regex = /\\r\\n|\\n|\\t|\\.|.|\r\n|\n|\t/g; return [...text.matchAll(regex)] .flatMap(([token]) => { if (isLinebreak(token)) { return token; } return [...token]; }) .reduce((acc: Array<BBCodeTextNode | BBCodeSpecialCharNode>, symbol: string) => { if (isSpecialChar(symbol)) { acc.push(symbol); } else { const lastItem: string = getByIndex(acc, -1); if (isSpecialChar(lastItem) || Type.isNil(lastItem)) { acc.push(symbol); } else { acc[acc.length - 1] += symbol; } } return acc; }, []) .map((fragment: string) => { if (isLinebreak(fragment)) { return parserScheme.createNewLine(); } if (isTab(fragment)) { return parserScheme.createTab(); } return parserScheme.createText({ content: this.getEncoder().decodeText(fragment), }); }); } return []; } static findNextTagIndex(bbcode: string, startIndex = 0): number { const nextContent: string = bbcode.slice(startIndex); const matchResult = nextContent.match(new RegExp(TAG_REGEX)); if (matchResult) { return matchResult.index + startIndex; } return -1; } static findNextTag(bbcode: string, startIndex = 0): ?NextTagResult { const nextContent: string = bbcode.slice(startIndex); const matchResult = nextContent.match(new RegExp(TAG_REGEX)); if (matchResult) { const [, slash, tagName] = matchResult; return { tagName, isClosedTag: slash === '\\', }; } return null; } static trimQuotes(value: string): string { const source = String(value); if ((/^["'].*["']$/g).test(source)) { return source.slice(1, -1); } return value; } parseAttributes(sourceAttributes: string): { value: ?string, attributes: Array<[string, string]> } { const result: {value: string, attributes: Array<Array<string, string>>} = { value: '', attributes: [] }; if (Type.isStringFilled(sourceAttributes)) { if (sourceAttributes.startsWith('=')) { result.value = this.getEncoder().decodeAttribute( BBCodeParser.trimQuotes( sourceAttributes.slice(1), ), ); return result; } return sourceAttributes .trim() .split(' ') .filter(Boolean) .reduce((acc: typeof result, item: string) => { const [key: string, value: string = ''] = item.split('='); acc.attributes.push([ BBCodeParser.toLowerCase(key), this.getEncoder().decodeAttribute( BBCodeParser.trimQuotes(value), ), ]); return acc; }, result); } return result; } parse(bbcode: string): BBCodeRootNode { const result: BBCodeRootNode = parserScheme.createRoot(); const firstTagIndex: number = BBCodeParser.findNextTagIndex(bbcode); if (firstTagIndex !== 0) { const textBeforeFirstTag: string = firstTagIndex === -1 ? bbcode : bbcode.slice(0, firstTagIndex); result.appendChild( ...this.parseText(textBeforeFirstTag), ); } const stack: Array<BBCodeElementNode> = [result]; const wasOpened: Array<string> = []; let current: ?BBCodeElementNode = null; let level: number = 0; bbcode.replace(TAG_REGEX_GS, (fullTag: string, slash: ?string, tagName: string, attrs: ?string, index: number) => { const isOpeningTag: boolean = Boolean(slash) === false; const startIndex: number = fullTag.length + index; const nextContent: string = bbcode.slice(startIndex); const attributes = this.parseAttributes(attrs); const lowerCaseTagName: string = BBCodeParser.toLowerCase(tagName); let parent: ?(BBCodeRootNode | BBCodeElementNode) = stack[level]; if (isOpeningTag) { const isPotentiallyVoid: boolean = !nextContent.includes(`[/${tagName}]`); if ( isPotentiallyVoid && !isListItem(lowerCaseTagName) ) { const tagScheme: BBCodeTagScheme = this.getScheme().getTagScheme(lowerCaseTagName); const isAllowedVoidTag: boolean = tagScheme && tagScheme.isVoid(); if (isAllowedVoidTag) { current = parserScheme.createElement({ name: lowerCaseTagName, value: attributes.value, attributes: Object.fromEntries(attributes.attributes), }); current.setScheme(this.getScheme()); parent.appendChild(current); } else { parent.appendChild( parserScheme.createText(fullTag), ); } const nextTagIndex: number = BBCodeParser.findNextTagIndex(bbcode, startIndex); if (nextTagIndex !== 0) { const content: string = nextTagIndex === -1 ? nextContent : bbcode.slice(startIndex, nextTagIndex); parent.appendChild( ...this.parseText(content), ); } } else { if (isListItem(lowerCaseTagName) && current && isListItem(current.getName())) { level--; parent = stack[level]; } current = parserScheme.createElement({ name: lowerCaseTagName, value: attributes.value, attributes: Object.fromEntries(attributes.attributes), }); const nextTagIndex: number = BBCodeParser.findNextTagIndex(bbcode, startIndex); if (nextTagIndex !== 0) { const content: string = nextTagIndex === -1 ? nextContent : bbcode.slice(startIndex, nextTagIndex); current.appendChild( ...this.parseText(content), ); } if (!parent) { level++; parent = stack[level]; } parent.appendChild(current); level++; stack[level] = current; wasOpened.push(lowerCaseTagName); } } else { if (wasOpened.includes(lowerCaseTagName)) { level--; const openedTagIndex: number = wasOpened.indexOf(lowerCaseTagName); wasOpened.splice(openedTagIndex, 1); } else { stack[level].appendChild( parserScheme.createText(fullTag), ); } if (isList(lowerCaseTagName) && level > 0) { level--; } const nextTagIndex: number = BBCodeParser.findNextTagIndex(bbcode, startIndex); if (nextTagIndex !== 0 && stack[level]) { const content: string = nextTagIndex === -1 ? nextContent : bbcode.slice(startIndex, nextTagIndex); stack[level].appendChild( ...this.parseText(content), ); } if (level > 0 && isListItem(stack[level].getName())) { const nextTag: ?NextTagResult = BBCodeParser.findNextTag(bbcode, startIndex); if (Type.isNull(nextTag) || isListItem(nextTag.tagName)) { level--; } } } }); const getFinalLineBreaksIndexes = (node: BBCodeContentNode) => { let skip = false; return node .getChildren() .reduceRight((acc: Array<BBCodeContentNode>, child: BBCodeContentNode, index: number) => { if (!skip && child.getName() === '#linebreak') { acc.push(index); } else if (!skip && child.getName() !== '#tab') { skip = true; } return acc; }, []); }; BBCodeNode.flattenAst(result).forEach((node: BBCodeContentNode) => { if (node.getName() === '*') { const finalLinebreaksIndexes: Array<number> = getFinalLineBreaksIndexes(node); if (finalLinebreaksIndexes.length === 1) { node.setChildren( node.getChildren().slice(0, getByIndex(finalLinebreaksIndexes, 0)), ); } if (finalLinebreaksIndexes.length > 1 && (finalLinebreaksIndexes & 2) === 0) { node.setChildren( node.getChildren().slice(0, getByIndex(finalLinebreaksIndexes, 0)), ); } } if ( this.isAllowedLinkify() && this.canBeLinkified(node) ) { const content = node.toString({ encode: false }); const tokens: Array<Linkify.MultiToken> = Linkify.tokenize(content); const nodes = tokens.map((token: Linkify.MultiToken) => { if (token.t === 'url') { return parserScheme.createElement({ name: 'url', value: token.toHref().replace(/^http:\/\//, 'https://'), children: [ parserScheme.createText(token.toString()), ], }); } if (token.t === 'email') { return parserScheme.createElement({ name: 'url', value: token.toHref(), children: [ parserScheme.createText(token.toString()), ], }); } return parserScheme.createText(token.toString()); }); node.replace(...nodes); } }); BBCodeNode.flattenAst(result).forEach((node: BBCodeNode) => { const tagScheme: BBCodeTagScheme = this.getScheme().getTagScheme(node); if (tagScheme) { tagScheme.runOnParseHandler(node); } }); result.setScheme( this.getScheme(), this.getOnUnknownHandler(), ); return result; } } export { BBCodeParser, };