UNPKG

@ant-design/x-markdown

Version:

placeholder for @ant-design/x-markdown

236 lines (231 loc) 7.5 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.default = exports.TokenType = void 0; var _react = require("react"); /* ------------ Type ------------ */ let TokenType = exports.TokenType = /*#__PURE__*/function (TokenType) { TokenType[TokenType["Text"] = 0] = "Text"; TokenType[TokenType["IncompleteLink"] = 1] = "IncompleteLink"; TokenType[TokenType["IncompleteImage"] = 2] = "IncompleteImage"; TokenType[TokenType["IncompleteHeading"] = 3] = "IncompleteHeading"; TokenType[TokenType["IncompleteHtml"] = 4] = "IncompleteHtml"; TokenType[TokenType["IncompleteEmphasis"] = 5] = "IncompleteEmphasis"; TokenType[TokenType["IncompleteList"] = 6] = "IncompleteList"; TokenType[TokenType["MaybeImage"] = 7] = "MaybeImage"; return TokenType; }({}); /* ------------ Constants ------------ */ const INCOMPLETE_REGEX = { image: [/^!\[[^\]\r\n]*$/, /^!\[[^\r\n]*\]\(*[^)\r\n]*$/], link: [/^\[[^\]\r\n]*$/, /^\[[^\r\n]*\]\(*[^)\r\n]*$/], atxHeading: [/^#{1,6}(?=\s)*$/], html: [/^<[a-zA-Z][a-zA-Z0-9-]*[^>\r\n]*$/], commonEmphasis: [/^(\*+|_+)(?!\s)(?!.*\1$)[^\r\n]*$/], list: [/^[-+*]\s*$/, /^[-+*]\s*(\*+|_+)(?!\s)(?!.*\1$)[^\r\n]*$/] }; const FENCED_CODE_REGEX = /^(`{3,}|~{3,})/; /* ------------ Utils ------------ */ const getInitialCache = () => ({ pending: '', token: TokenType.Text, processedLength: 0, completeMarkdown: '' }); const commitCache = cache => { if (cache.pending) { cache.completeMarkdown += cache.pending; cache.pending = ''; } cache.token = TokenType.Text; }; const isInCodeBlock = text => { const lines = text.split('\n'); let inFenced = false; let fenceChar = ''; let fenceLen = 0; for (const rawLine of lines) { const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine; const fenceMatch = line.match(FENCED_CODE_REGEX); if (fenceMatch) { const currentFence = fenceMatch[1]; const char = currentFence[0]; const len = currentFence.length; if (!inFenced) { inFenced = true; fenceChar = char; fenceLen = len; } else if (char === fenceChar && len >= fenceLen) { inFenced = false; fenceChar = ''; fenceLen = 0; } } } return inFenced; }; /* ------------ Recognizers ------------ */ const isTokenIncomplete = { image: markdown => INCOMPLETE_REGEX.image.some(re => re.test(markdown)), link: markdown => INCOMPLETE_REGEX.link.some(re => re.test(markdown)), atxHeading: markdown => INCOMPLETE_REGEX.atxHeading.some(re => re.test(markdown)), html: markdown => INCOMPLETE_REGEX.html.some(re => re.test(markdown)), commonEmphasis: markdown => INCOMPLETE_REGEX.commonEmphasis.some(re => re.test(markdown)), list: markdown => INCOMPLETE_REGEX.list.some(re => re.test(markdown)) }; const recognizeImage = cache => { const { token, pending } = cache; if (token === TokenType.Text && pending.startsWith('!')) { cache.token = TokenType.MaybeImage; return; } if (token !== TokenType.IncompleteImage && token !== TokenType.MaybeImage) return; if (isTokenIncomplete.image(pending)) { cache.token = TokenType.IncompleteImage; } else { commitCache(cache); } }; const recognizeLink = cache => { const { token, pending } = cache; if (token === TokenType.Text && pending.startsWith('[')) { cache.token = TokenType.IncompleteLink; return; } if (token !== TokenType.IncompleteLink) return; if (!isTokenIncomplete.link(pending)) { commitCache(cache); } }; const recognizeAtxHeading = cache => { const { token, pending } = cache; if (token === TokenType.Text && pending.startsWith('#')) { cache.token = TokenType.IncompleteHeading; return; } if (token !== TokenType.IncompleteHeading) return; if (!isTokenIncomplete.atxHeading(pending)) { commitCache(cache); } }; const recognizeHtml = cache => { const { token, pending } = cache; if (token === TokenType.Text && pending.startsWith('<')) { cache.token = TokenType.IncompleteHtml; return; } if (token !== TokenType.IncompleteHtml) return; if (!isTokenIncomplete.html(pending)) { commitCache(cache); } }; const recognizeEmphasis = cache => { const { token, pending } = cache; const isEmphasisStart = pending.startsWith('*') || pending.startsWith('_'); if (token === TokenType.Text && isEmphasisStart) { cache.token = TokenType.IncompleteEmphasis; return; } if (token !== TokenType.IncompleteEmphasis) return; if (!isTokenIncomplete.commonEmphasis(pending)) { commitCache(cache); } }; const recognizeList = cache => { const { token, pending } = cache; if (token === TokenType.Text && /^[-+*]/.test(pending)) { cache.token = TokenType.IncompleteList; return; } if (token !== TokenType.IncompleteList) return; if (!isTokenIncomplete.list(pending)) { commitCache(cache); } }; const recognizeText = cache => { if (cache.token === TokenType.Text) { commitCache(cache); } }; /* ------------ Main Hook ------------ */ const useStreaming = (input, config) => { const { hasNextChunk: enableCache = false, incompleteMarkdownComponentMap } = config || {}; const [output, setOutput] = (0, _react.useState)(''); const cacheRef = (0, _react.useRef)(getInitialCache()); // Memoize recognizers to avoid recreation on each render const recognizers = (0, _react.useMemo)(() => [recognizeImage, recognizeLink, recognizeAtxHeading, recognizeEmphasis, recognizeHtml, recognizeList, recognizeText], []); const handleIncompleteMarkdown = (0, _react.useCallback)(cache => { if (cache.token === TokenType.Text) return; const componentMap = incompleteMarkdownComponentMap || {}; switch (cache.token) { case TokenType.IncompleteImage: return `<${componentMap.image || 'incomplete-image'} />`; case TokenType.IncompleteLink: return `<${componentMap.link || 'incomplete-link'} />`; default: return undefined; } }, [incompleteMarkdownComponentMap]); const processStreaming = (0, _react.useCallback)(text => { if (!text) { setOutput(''); cacheRef.current = getInitialCache(); return; } const cache = cacheRef.current; const expectedPrefix = cache.completeMarkdown + cache.pending; // Reset cache if input doesn't continue from previous state if (!text.startsWith(expectedPrefix)) { cacheRef.current = getInitialCache(); } const chunk = text.slice(cache.processedLength); if (!chunk) return; cache.processedLength += chunk.length; const isTextInBlock = isInCodeBlock(text); // Skip processing if inside code block for (const char of chunk) { cache.pending += char; if (isTextInBlock) { commitCache(cache); } else { recognizers.forEach(recognize => { recognize(cache); }); } } const incompletePlaceholder = handleIncompleteMarkdown(cache); setOutput(cache.completeMarkdown + (incompletePlaceholder || '')); }, [recognizers, handleIncompleteMarkdown]); (0, _react.useEffect)(() => { if (typeof input !== 'string') { console.error(`X-Markdown: input must be string, not ${typeof input}.`); setOutput(''); return; } enableCache ? processStreaming(input) : setOutput(input); }, [input, enableCache, processStreaming]); return output; }; var _default = exports.default = useStreaming;