@ant-design/x-markdown
Version:
placeholder for @ant-design/x-markdown
199 lines (194 loc) • 7.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.default = void 0;
var _react = require("react");
var _interface = require("../interface");
/* ------------ Type ------------ */
/* ------------ Constants ------------ */
const FENCED_CODE_REGEX = /^(`{3,}|~{3,})/;
// Validates whether a token is still incomplete in the streaming context.
// Returns true if the token is syntactically incomplete; false if it is complete or invalid.
const STREAM_INCOMPLETE_REGEX = {
image: [/^!\[[^\]\r\n]{0,1000}$/, /^!\[[^\r\n]{0,1000}\]\(*[^)\r\n]{0,1000}$/],
link: [/^\[[^\]\r\n]{0,1000}$/, /^\[[^\r\n]{0,1000}\]\(*[^)\r\n]{0,1000}$/],
html: [/^<\/$/, /^<\/?[a-zA-Z][a-zA-Z0-9-]{0,100}[^>\r\n]{0,1000}$/],
commonEmphasis: [/^(\*{1,3}|_{1,3})(?!\s)(?!.*\1$)[^\r\n]{0,1000}$/],
// regex2 matches cases like "- **"
list: [/^[-+*]\s{0,3}$/, /^[-+*]\s{1,3}(\*{1,3}|_{1,3})(?!\s)(?!.*\1$)[^\r\n]{0,1000}$/]
};
const isTableInComplete = markdown => {
if (markdown.includes('\n\n')) return false;
const lines = markdown.split('\n');
if (lines.length <= 1) return true;
const [header, separator] = lines;
const trimmedHeader = header.trim();
if (!/^\|.*\|$/.test(trimmedHeader)) return false;
const trimmedSeparator = separator.trim();
const columns = trimmedSeparator.split('|').map(col => col.trim()).filter(Boolean);
const separatorRegex = /^:?-+:?$/;
return columns.every((col, index) => index === columns.length - 1 ? col === ':' || separatorRegex.test(col) : separatorRegex.test(col));
};
const tokenRecognizerMap = {
[_interface.StreamCacheTokenType.Link]: {
tokenType: _interface.StreamCacheTokenType.Link,
isStartOfToken: markdown => markdown.startsWith('['),
isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.link.some(re => re.test(markdown))
},
[_interface.StreamCacheTokenType.Image]: {
tokenType: _interface.StreamCacheTokenType.Image,
isStartOfToken: markdown => markdown.startsWith('!'),
isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.image.some(re => re.test(markdown))
},
[_interface.StreamCacheTokenType.Html]: {
tokenType: _interface.StreamCacheTokenType.Html,
isStartOfToken: markdown => markdown.startsWith('<'),
isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.html.some(re => re.test(markdown))
},
[_interface.StreamCacheTokenType.Emphasis]: {
tokenType: _interface.StreamCacheTokenType.Emphasis,
isStartOfToken: markdown => markdown.startsWith('*') || markdown.startsWith('_'),
isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.commonEmphasis.some(re => re.test(markdown))
},
[_interface.StreamCacheTokenType.List]: {
tokenType: _interface.StreamCacheTokenType.List,
isStartOfToken: markdown => /^[-+*]/.test(markdown),
isStreamingValid: markdown => STREAM_INCOMPLETE_REGEX.list.some(re => re.test(markdown))
},
[_interface.StreamCacheTokenType.Table]: {
tokenType: _interface.StreamCacheTokenType.Table,
isStartOfToken: markdown => markdown.startsWith('|'),
isStreamingValid: isTableInComplete
}
};
const recognize = (cache, tokenType) => {
const recognizer = tokenRecognizerMap[tokenType];
if (!recognizer) return;
const {
token,
pending
} = cache;
if (token === _interface.StreamCacheTokenType.Text && recognizer.isStartOfToken(pending)) {
cache.token = tokenType;
return;
}
if (token === tokenType && !recognizer.isStreamingValid(pending)) {
commitCache(cache);
}
};
const recognizeHandlers = Object.values(tokenRecognizerMap).map(rec => ({
tokenType: rec.tokenType,
recognize: cache => recognize(cache, rec.tokenType)
}));
/* ------------ Utils ------------ */
const getInitialCache = () => ({
pending: '',
token: _interface.StreamCacheTokenType.Text,
processedLength: 0,
completeMarkdown: ''
});
const commitCache = cache => {
if (cache.pending) {
cache.completeMarkdown += cache.pending;
cache.pending = '';
}
cache.token = _interface.StreamCacheTokenType.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;
};
/* ------------ Main Hook ------------ */
const useStreaming = (input, config) => {
const {
hasNextChunk: enableCache = false,
incompleteMarkdownComponentMap
} = config || {};
const [output, setOutput] = (0, _react.useState)('');
const cacheRef = (0, _react.useRef)(getInitialCache());
const handleIncompleteMarkdown = (0, _react.useCallback)(cache => {
const {
token,
pending
} = cache;
if (token === _interface.StreamCacheTokenType.Text) return;
const componentMap = incompleteMarkdownComponentMap || {};
const encodedPending = encodeURIComponent(pending);
switch (token) {
case _interface.StreamCacheTokenType.Image:
return pending === '!' ? undefined : `<${componentMap.image || 'incomplete-image'} data-raw="${encodedPending}" />`;
case _interface.StreamCacheTokenType.Table:
return pending.split('\n').length <= 2 ? `<${componentMap.table || 'incomplete-table'} data-raw="${encodedPending}" />` : pending;
default:
return `<${componentMap[token] || `incomplete-${token}`} data-raw="${encodedPending}" />`;
}
}, [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);
for (const char of chunk) {
cache.pending += char;
// Skip processing if inside code block
if (isTextInBlock) {
commitCache(cache);
continue;
}
if (cache.token === _interface.StreamCacheTokenType.Text) {
for (const handler of recognizeHandlers) handler.recognize(cache);
} else {
const handler = recognizeHandlers.find(handler => handler.tokenType === cache.token);
handler?.recognize(cache);
}
if (cache.token === _interface.StreamCacheTokenType.Text) {
commitCache(cache);
}
}
const incompletePlaceholder = handleIncompleteMarkdown(cache);
setOutput(cache.completeMarkdown + (incompletePlaceholder || ''));
}, [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;