225 lines
8.4 KiB
JavaScript
225 lines
8.4 KiB
JavaScript
export const RICH_MESSAGE_MAX_CHARS = 32768;
|
|
export const RICH_MESSAGE_MAX_BLOCKS = 500;
|
|
function normalizeTelegramNativeMarkdown(markdown) {
|
|
const lines = markdown.replace(/\r\n/g, '\n').split('\n');
|
|
let fence;
|
|
let displayMath = false;
|
|
const hasClosingDisplayMathDelimiter = (source, index) => {
|
|
for (let i = index + 1; i < source.length; i++) {
|
|
if (source[i].trim() === '$$')
|
|
return true;
|
|
if (/^ {0,3}(`{3,}|~{3,})/.test(source[i]))
|
|
return false;
|
|
}
|
|
return false;
|
|
};
|
|
return lines
|
|
.map((line, index) => {
|
|
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
const inFence = fence !== undefined;
|
|
if (!inFence && line.trim() === '$$') {
|
|
if (displayMath) {
|
|
displayMath = false;
|
|
return '```';
|
|
}
|
|
if (hasClosingDisplayMathDelimiter(lines, index)) {
|
|
displayMath = true;
|
|
return '```math';
|
|
}
|
|
}
|
|
if (displayMath)
|
|
return line;
|
|
if (!inFence && fenceMatch) {
|
|
const markerText = fenceMatch[1];
|
|
fence = {
|
|
marker: markerText[0],
|
|
length: markerText.length,
|
|
};
|
|
return line;
|
|
}
|
|
if (inFence
|
|
&& new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
|
|
fence = undefined;
|
|
return line;
|
|
}
|
|
return line;
|
|
})
|
|
.join('\n');
|
|
}
|
|
function countTelegramNativeMarkdownBlocks(block) {
|
|
if (/^ {0,3}(`{3,}|~{3,})/.test(block))
|
|
return 1;
|
|
const lines = block.split('\n').filter((line) => line.trim().length > 0);
|
|
if (lines.some((line) => /^\s*([-*+] |\d+\. |>|\|)/.test(line))) {
|
|
return Math.max(1, lines.length);
|
|
}
|
|
return 1;
|
|
}
|
|
function splitTelegramNativeMarkdownBlocks(markdown) {
|
|
const blocks = [];
|
|
const current = [];
|
|
let fence;
|
|
const flush = () => {
|
|
if (current.length === 0)
|
|
return;
|
|
blocks.push(current.join('\n'));
|
|
current.length = 0;
|
|
};
|
|
for (const line of markdown.split('\n')) {
|
|
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
if (!fence && line.trim().length === 0) {
|
|
flush();
|
|
continue;
|
|
}
|
|
current.push(line);
|
|
if (!fence && fenceMatch) {
|
|
const markerText = fenceMatch[1];
|
|
fence = { marker: markerText[0], length: markerText.length };
|
|
continue;
|
|
}
|
|
if (fence
|
|
&& new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
|
|
fence = undefined;
|
|
}
|
|
}
|
|
flush();
|
|
return blocks;
|
|
}
|
|
function splitTelegramNativeMarkdownCountedBlocks(block) {
|
|
if (countTelegramNativeMarkdownBlocks(block) <= RICH_MESSAGE_MAX_BLOCKS) {
|
|
return [block];
|
|
}
|
|
const chunks = [];
|
|
let current = [];
|
|
let fence;
|
|
for (const line of block.split('\n')) {
|
|
const fenceMatch = line.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
if (!fence && current.length >= RICH_MESSAGE_MAX_BLOCKS) {
|
|
chunks.push(current.join('\n'));
|
|
current = [];
|
|
}
|
|
current.push(line);
|
|
if (!fence && fenceMatch) {
|
|
const markerText = fenceMatch[1];
|
|
fence = { marker: markerText[0], length: markerText.length };
|
|
continue;
|
|
}
|
|
if (fence
|
|
&& new RegExp(`^ {0,3}${fence.marker}{${fence.length},}\\s*$`).test(line)) {
|
|
fence = undefined;
|
|
}
|
|
}
|
|
if (current.length > 0)
|
|
chunks.push(current.join('\n'));
|
|
return chunks;
|
|
}
|
|
function findTelegramNativeMarkdownSplitIndex(text, hardLimit = RICH_MESSAGE_MAX_CHARS) {
|
|
const paragraphIndex = text.lastIndexOf('\n\n', hardLimit);
|
|
if (paragraphIndex > 0)
|
|
return paragraphIndex + 2;
|
|
const lineIndex = text.lastIndexOf('\n', hardLimit);
|
|
if (lineIndex > 0)
|
|
return lineIndex + 1;
|
|
const spaceIndex = text.lastIndexOf(' ', hardLimit);
|
|
if (spaceIndex > 0)
|
|
return spaceIndex + 1;
|
|
return hardLimit;
|
|
}
|
|
function splitTelegramNativeMarkdownWrappedContent(content, maxContentLength, wrap) {
|
|
const chunks = [];
|
|
let remaining = content;
|
|
while (remaining.length > maxContentLength) {
|
|
const window = remaining.slice(0, maxContentLength + 1);
|
|
const splitIndex = findTelegramNativeMarkdownSplitIndex(window, maxContentLength);
|
|
chunks.push(wrap(remaining.slice(0, splitIndex)));
|
|
remaining = remaining.slice(splitIndex);
|
|
}
|
|
if (remaining.length > 0)
|
|
chunks.push(wrap(remaining));
|
|
return chunks;
|
|
}
|
|
function splitTelegramNativeMarkdownLongFenceBlock(block) {
|
|
const lines = block.split('\n');
|
|
const opening = lines[0] ?? '';
|
|
const closing = lines[lines.length - 1] ?? '';
|
|
const openingMatch = opening?.match(/^ {0,3}(`{3,}|~{3,})/);
|
|
if (!openingMatch || !closing || lines.length < 2)
|
|
return undefined;
|
|
const markerText = openingMatch[1] ?? '```';
|
|
const marker = markerText[0];
|
|
if (!new RegExp(`^ {0,3}${marker}{${markerText.length},}\\s*$`).test(closing)) {
|
|
return undefined;
|
|
}
|
|
const maxContentLength = RICH_MESSAGE_MAX_CHARS - opening.length - closing.length - 2;
|
|
if (maxContentLength <= 0)
|
|
return undefined;
|
|
const content = lines.slice(1, -1).join('\n');
|
|
return splitTelegramNativeMarkdownWrappedContent(content, maxContentLength, (chunk) => `${opening}\n${chunk}${chunk.endsWith('\n') ? '' : '\n'}${closing}`);
|
|
}
|
|
function splitTelegramNativeMarkdownLongWrappedInlineBlock(block) {
|
|
const delimiter = ['**', '__', '~~', '`', '*', '_'].find((candidate) => block.startsWith(candidate)
|
|
&& block.endsWith(candidate)
|
|
&& block.length > candidate.length * 2);
|
|
if (!delimiter)
|
|
return undefined;
|
|
const maxContentLength = RICH_MESSAGE_MAX_CHARS - delimiter.length * 2;
|
|
if (maxContentLength <= 0)
|
|
return undefined;
|
|
return splitTelegramNativeMarkdownWrappedContent(block.slice(delimiter.length, -delimiter.length), maxContentLength, (chunk) => `${delimiter}${chunk}${delimiter}`);
|
|
}
|
|
function splitTelegramNativeMarkdownLongPlainBlock(block) {
|
|
const chunks = [];
|
|
let remaining = block;
|
|
while (remaining.length > RICH_MESSAGE_MAX_CHARS) {
|
|
const window = remaining.slice(0, RICH_MESSAGE_MAX_CHARS + 1);
|
|
const splitIndex = findTelegramNativeMarkdownSplitIndex(window);
|
|
chunks.push(remaining.slice(0, splitIndex).trimEnd());
|
|
remaining = remaining.slice(splitIndex).trimStart();
|
|
}
|
|
if (remaining.length > 0)
|
|
chunks.push(remaining);
|
|
return chunks;
|
|
}
|
|
function splitTelegramNativeMarkdownLongBlock(block) {
|
|
return (splitTelegramNativeMarkdownLongFenceBlock(block)
|
|
?? splitTelegramNativeMarkdownLongWrappedInlineBlock(block)
|
|
?? splitTelegramNativeMarkdownLongPlainBlock(block));
|
|
}
|
|
export function splitRichMarkdown(markdown) {
|
|
const normalizedMarkdown = normalizeTelegramNativeMarkdown(markdown);
|
|
if (normalizedMarkdown.length <= RICH_MESSAGE_MAX_CHARS
|
|
&& countTelegramNativeMarkdownBlocks(normalizedMarkdown) <= RICH_MESSAGE_MAX_BLOCKS) {
|
|
return [normalizedMarkdown];
|
|
}
|
|
const chunks = [];
|
|
let current = '';
|
|
let currentBlockCount = 0;
|
|
for (const rawBlock of splitTelegramNativeMarkdownBlocks(normalizedMarkdown)) {
|
|
for (const block of splitTelegramNativeMarkdownCountedBlocks(rawBlock)) {
|
|
const blockCount = countTelegramNativeMarkdownBlocks(block);
|
|
const candidate = current ? `${current}\n\n${block}` : block;
|
|
const exceedsChars = candidate.length > RICH_MESSAGE_MAX_CHARS;
|
|
const exceedsBlocks = currentBlockCount + blockCount > RICH_MESSAGE_MAX_BLOCKS;
|
|
if (!exceedsChars && !exceedsBlocks) {
|
|
current = candidate;
|
|
currentBlockCount += blockCount;
|
|
continue;
|
|
}
|
|
if (current)
|
|
chunks.push(current.trimEnd());
|
|
if (block.length <= RICH_MESSAGE_MAX_CHARS
|
|
&& blockCount <= RICH_MESSAGE_MAX_BLOCKS) {
|
|
current = block;
|
|
currentBlockCount = blockCount;
|
|
continue;
|
|
}
|
|
chunks.push(...splitTelegramNativeMarkdownLongBlock(block));
|
|
current = '';
|
|
currentBlockCount = 0;
|
|
}
|
|
}
|
|
if (current)
|
|
chunks.push(current.trimEnd());
|
|
return chunks;
|
|
}
|