@ant-design/x-markdown
Version:
placeholder for @ant-design/x-markdown
236 lines (231 loc) • 7.5 kB
JavaScript
;
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;