UNPKG

@ant-design/x-markdown

Version:

placeholder for @ant-design/x-markdown

146 lines (139 loc) 5.92 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = void 0; var _dompurify = _interopRequireDefault(require("dompurify")); var _htmlReactParser = _interopRequireWildcard(require("html-react-parser")); var _react = _interopRequireDefault(require("react")); var _AnimationText = _interopRequireDefault(require("../AnimationText")); function _getRequireWildcardCache(e) { if ("function" != typeof WeakMap) return null; var r = new WeakMap(), t = new WeakMap(); return (_getRequireWildcardCache = function (e) { return e ? t : r; })(e); } function _interopRequireWildcard(e, r) { if (!r && e && e.__esModule) return e; if (null === e || "object" != typeof e && "function" != typeof e) return { default: e }; var t = _getRequireWildcardCache(r); if (t && t.has(e)) return t.get(e); var n = { __proto__: null }, a = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var u in e) if ("default" !== u && Object.prototype.hasOwnProperty.call(e, u)) { var i = a ? Object.getOwnPropertyDescriptor(e, u) : null; i && (i.get || i.set) ? Object.defineProperty(n, u, i) : n[u] = e[u]; } return n.default = e, t && t.set(e, n), n; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } class Renderer { options; static NON_WHITESPACE_REGEX = /[^\r\n\s]+/; constructor(options) { this.options = options; } /** * Detect unclosed tags using regular expressions */ detectUnclosedTags(htmlString) { const unclosedTags = new Set(); const stack = []; const tagRegex = /<\/?([a-zA-Z][a-zA-Z0-9-]*)(?:\s[^>]*)?>/g; let match = tagRegex.exec(htmlString); while (match !== null) { const [fullMatch, tagName] = match; const isClosing = fullMatch.startsWith('</'); const isSelfClosing = fullMatch.endsWith('/>'); if (this.options.components?.[tagName.toLowerCase()]) { if (isClosing) { // Found closing tag, pop from stack const lastIndex = stack.lastIndexOf(tagName.toLowerCase()); if (lastIndex !== -1) { stack.splice(lastIndex, 1); } } else if (!isSelfClosing) { // Found opening tag, push to stack stack.push(tagName.toLowerCase()); } } match = tagRegex.exec(htmlString); } // Remaining tags in stack are unclosed stack.forEach(tag => { unclosedTags.add(tag); }); return unclosedTags; } /** * Configure DOMPurify to preserve components and target attributes, filter everything else */ configureDOMPurify() { const customComponents = Object.keys(this.options.components || {}); const userConfig = this.options.dompurifyConfig || {}; const allowedTags = Array.isArray(userConfig.ADD_TAGS) ? userConfig.ADD_TAGS : []; const addAttr = Array.isArray(userConfig.ADD_ATTR) ? userConfig.ADD_ATTR : []; return { ...userConfig, ADD_TAGS: Array.from(new Set([...customComponents, ...allowedTags])), ADD_ATTR: Array.from(new Set(['target', 'rel', ...addAttr])) }; } createReplaceElement(unclosedTags, cidRef) { const { enableAnimation, animationConfig } = this.options.streaming || {}; return domNode => { const key = `x-markdown-component-${cidRef.current++}`; // Check if it's a text node with data const isValidTextNode = domNode.type === 'text' && domNode.data && Renderer.NON_WHITESPACE_REGEX.test(domNode.data); // Skip animation for text nodes inside custom components to preserve their internal structure const parentTagName = domNode.parent?.name; const isParentCustomComponent = parentTagName && this.options.components?.[parentTagName]; const shouldReplaceText = enableAnimation && isValidTextNode && !isParentCustomComponent; if (shouldReplaceText) { return /*#__PURE__*/_react.default.createElement(_AnimationText.default, { text: domNode.data, key, animationConfig }); } if (!('name' in domNode)) return; const { name, attribs, children } = domNode; const renderElement = this.options.components?.[name]; if (renderElement) { const streamStatus = unclosedTags?.has(name) ? 'loading' : 'done'; const props = { domNode, streamStatus, key, ...attribs }; // Handle class and className merging const classes = [props.className, props.classname, props.class].filter(Boolean).join(' ').trim(); props.className = classes || ''; if (name === 'code') { const { 'data-block': block = 'false', 'data-state': codeStreamStatus = 'done' } = attribs || {}; props.block = block === 'true'; props.streamStatus = codeStreamStatus === 'loading' ? 'loading' : 'done'; } if (children) { props.children = this.processChildren(children, unclosedTags, cidRef); } return /*#__PURE__*/_react.default.createElement(renderElement, props); } }; } processChildren(children, unclosedTags, cidRef) { return (0, _htmlReactParser.domToReact)(children, { replace: this.createReplaceElement(unclosedTags, cidRef) }); } processHtml(htmlString) { const unclosedTags = this.detectUnclosedTags(htmlString); const cidRef = { current: 0 }; // Use DOMPurify to clean HTML while preserving custom components and target attributes const purifyConfig = this.configureDOMPurify(); const cleanHtml = _dompurify.default.sanitize(htmlString, purifyConfig); return (0, _htmlReactParser.default)(cleanHtml, { replace: this.createReplaceElement(unclosedTags, cidRef) }); } render(html) { return this.processHtml(html); } } var _default = exports.default = Renderer;