Files
dsh-telegram-channel/lib/rich-format.js
T

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;
}