diff --git a/index.ts b/index.ts
index bf75c89..63ac922 100644
--- a/index.ts
+++ b/index.ts
@@ -683,34 +683,100 @@ function isMarkdownTableSeparator(line: string): boolean {
return /^\s*\|?(?:\s*:?-{3,}:?\s*\|)+\s*:?-{3,}:?\s*\|?\s*$/.test(line);
}
+function parseMarkdownFence(
+ line: string,
+): { marker: "`" | "~"; length: number; info?: string } | undefined {
+ const match = line.match(/^\s*([`~]{3,})(.*)$/);
+ if (!match) return undefined;
+ const fence = match[1] ?? "";
+ const marker = fence[0];
+ if ((marker !== "`" && marker !== "~") || /[^`~]/.test(fence)) {
+ return undefined;
+ }
+ if (!fence.split("").every((char) => char === marker)) return undefined;
+ return {
+ marker,
+ length: fence.length,
+ info: (match[2] ?? "").trim() || undefined,
+ };
+}
+
function isFencedCodeStart(line: string): boolean {
- return /^\s*```/.test(line);
+ return parseMarkdownFence(line) !== undefined;
+}
+
+function isMatchingMarkdownFence(
+ line: string,
+ fence: { marker: "`" | "~"; length: number },
+): boolean {
+ const match = line.match(/^\s*([`~]{3,})\s*$/);
+ if (!match) return false;
+ const candidate = match[1] ?? "";
+ return (
+ candidate.length >= fence.length &&
+ candidate[0] === fence.marker &&
+ candidate.split("").every((char) => char === fence.marker)
+ );
}
function isIndentedCodeLine(line: string): boolean {
return /^(?:\t| {4,})/.test(line);
}
+function isIndentedMarkdownStructureLine(line: string): boolean {
+ const trimmed = line.trimStart();
+ return (
+ /^(?:[-*+]|\d+\.)\s+\[([ xX])\]\s+/.test(trimmed) ||
+ /^(?:[-*+]|\d+\.)\s+/.test(trimmed) ||
+ /^>\s?/.test(trimmed) ||
+ /^#{1,6}\s+/.test(trimmed) ||
+ parseMarkdownFence(trimmed) !== undefined
+ );
+}
+
+function canStartIndentedCodeBlock(lines: string[], index: number): boolean {
+ const line = lines[index] ?? "";
+ if (!isIndentedCodeLine(line)) return false;
+ if (isIndentedMarkdownStructureLine(line)) return false;
+ if (index === 0) return true;
+ return (lines[index - 1] ?? "").trim().length === 0;
+}
+
+function stripIndentedCodePrefix(line: string): string {
+ if (line.startsWith("\t")) return line.slice(1);
+ if (line.startsWith(" ")) return line.slice(4);
+ return line;
+}
+
function renderMarkdownPreviewText(markdown: string): string {
const normalized = markdown.replace(/\r\n/g, "\n").trim();
if (normalized.length === 0) return "";
const output: string[] = [];
const lines = normalized.split("\n");
- let inFence = false;
+ let activeFence: { marker: "`" | "~"; length: number } | undefined;
for (const rawLine of lines) {
const line = rawLine ?? "";
- if (isFencedCodeStart(line)) {
- inFence = !inFence;
+ const fence = parseMarkdownFence(line);
+ if (activeFence) {
+ if (fence && isMatchingMarkdownFence(line, activeFence)) {
+ activeFence = undefined;
+ continue;
+ }
+ if (line.trim().length === 0) {
+ if (output.at(-1) !== "") output.push("");
+ continue;
+ }
+ output.push(line);
+ continue;
+ }
+ if (fence) {
+ activeFence = { marker: fence.marker, length: fence.length };
continue;
}
if (line.trim().length === 0) {
if (output.at(-1) !== "") output.push("");
continue;
}
- if (inFence) {
- output.push(line);
- continue;
- }
if (isMarkdownTableSeparator(line)) {
continue;
}
@@ -758,6 +824,24 @@ function renderMarkdownPreviewText(markdown: string): string {
// --- Rich Markdown Rendering ---
+function renderDelimitedInlineStyle(
+ text: string,
+ delimiter: string,
+ render: (content: string) => string,
+): string {
+ const escapedDelimiter = delimiter.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
+ const pattern = new RegExp(
+ `(^|[^\\p{L}\\p{N}\\\\])(${escapedDelimiter})(?=\\S)(.+?)(?<=\\S)\\2(?=[^\\p{L}\\p{N}]|$)`,
+ "gu",
+ );
+ return text.replace(
+ pattern,
+ (_match, prefix: string, _wrapped: string, content: string) => {
+ return `${prefix}${render(content)}`;
+ },
+ );
+}
+
function renderInlineMarkdown(text: string): string {
const tokens: string[] = [];
const makeToken = (html: string): string => {
@@ -789,10 +873,27 @@ function renderInlineMarkdown(text: string): string {
return makeToken(`${escapeHtml(code)}`);
});
result = escapeHtml(result);
- result = result.replace(/(\*\*\*|___)(.+?)\1/g, "$2");
- result = result.replace(/~~(.+?)~~/g, "$1");
- result = result.replace(/(\*\*|__)(.+?)\1/g, "$2");
- result = result.replace(/(\*|_)(.+?)\1/g, "$2");
+ result = renderDelimitedInlineStyle(result, "***", (content) => {
+ return `${content}`;
+ });
+ result = renderDelimitedInlineStyle(result, "___", (content) => {
+ return `${content}`;
+ });
+ result = renderDelimitedInlineStyle(result, "~~", (content) => {
+ return `${content}`;
+ });
+ result = renderDelimitedInlineStyle(result, "**", (content) => {
+ return `${content}`;
+ });
+ result = renderDelimitedInlineStyle(result, "__", (content) => {
+ return `${content}`;
+ });
+ result = renderDelimitedInlineStyle(result, "*", (content) => {
+ return `${content}`;
+ });
+ result = renderDelimitedInlineStyle(result, "_", (content) => {
+ return `${content}`;
+ });
result = result.replace(
/(^|[\s>(])(\[(?: |x|X)\])(?=($|[\s<).,:;!?]))/g,
(_match, prefix: string, checkbox: string) => {
@@ -808,7 +909,7 @@ function renderInlineMarkdown(text: string): string {
}
function buildListIndent(level: number): string {
- return "\u00A0".repeat(Math.max(0, Math.min(12, level * 2)));
+ return "\u00A0".repeat(Math.max(0, level) * 2);
}
function parseMarkdownTableRow(line: string): string[] {
@@ -945,10 +1046,50 @@ function renderMarkdownTableBlock(lines: string[]): string[] {
return renderMarkdownCodeBlock(tableLines.join("\n"), "markdown");
}
+function chunkRenderedHtmlLines(
+ lines: string[],
+ wrapper?: { open: string; close: string },
+): string[] {
+ if (lines.length === 0) return [];
+ const open = wrapper?.open ?? "";
+ const close = wrapper?.close ?? "";
+ const maxContentLength = MAX_MESSAGE_LENGTH - open.length - close.length;
+ const chunks: string[] = [];
+ let current = "";
+ const pushCurrent = (): void => {
+ if (current.length === 0) return;
+ chunks.push(`${open}${current}${close}`);
+ current = "";
+ };
+ for (const line of lines) {
+ const candidate = current.length === 0 ? line : `${current}\n${line}`;
+ if (candidate.length <= maxContentLength) {
+ current = candidate;
+ continue;
+ }
+ pushCurrent();
+ if (line.length <= maxContentLength) {
+ current = line;
+ continue;
+ }
+ for (let i = 0; i < line.length; i += maxContentLength) {
+ chunks.push(`${open}${line.slice(i, i + maxContentLength)}${close}`);
+ }
+ }
+ pushCurrent();
+ return chunks;
+}
+
+function renderMarkdownTextBlock(block: string): string[] {
+ return chunkRenderedHtmlLines(renderMarkdownTextLines(block));
+}
+
function renderMarkdownQuoteBlock(lines: string[]): string[] {
const inner = lines.map((line) => line.replace(/^\s*>\s?/, "")).join("\n");
- const rendered = renderMarkdownTextLines(inner).join("\n");
- return rendered.length > 0 ? [`
${rendered}`] : []; + return chunkRenderedHtmlLines(renderMarkdownTextLines(inner), { + open: "
", + close: "", + }); } function renderMarkdownToTelegramHtmlChunks(markdown: string): string[] { @@ -960,11 +1101,14 @@ function renderMarkdownToTelegramHtmlChunks(markdown: string): string[] { while (index < lines.length) { const line = lines[index] ?? ""; const nextLine = lines[index + 1] ?? ""; - if (isFencedCodeStart(line)) { - const language = line.trim().slice(3).trim() || undefined; + const fence = parseMarkdownFence(line); + if (fence) { index += 1; const codeLines: string[] = []; - while (index < lines.length && !isFencedCodeStart(lines[index] ?? "")) { + while ( + index < lines.length && + !isMatchingMarkdownFence(lines[index] ?? "", fence) + ) { codeLines.push(lines[index] ?? ""); index += 1; } @@ -972,7 +1116,7 @@ function renderMarkdownToTelegramHtmlChunks(markdown: string): string[] { index += 1; } renderedBlocks.push( - ...renderMarkdownCodeBlock(codeLines.join("\n"), language), + ...renderMarkdownCodeBlock(codeLines.join("\n"), fence.info), ); while (index < lines.length && (lines[index] ?? "").trim().length === 0) { index += 1; @@ -997,13 +1141,17 @@ function renderMarkdownToTelegramHtmlChunks(markdown: string): string[] { renderedBlocks.push(...renderMarkdownTableBlock(tableLines)); continue; } - if (isIndentedCodeLine(line)) { + if (canStartIndentedCodeBlock(lines, index)) { const codeLines: string[] = []; - while (index < lines.length && isIndentedCodeLine(lines[index] ?? "")) { + while (index < lines.length) { const rawLine = lines[index] ?? ""; - codeLines.push( - rawLine.startsWith("\t") ? rawLine.slice(1) : rawLine.slice(4), - ); + if (rawLine.trim().length === 0) { + codeLines.push(""); + index += 1; + continue; + } + if (!isIndentedCodeLine(rawLine)) break; + codeLines.push(stripIndentedCodePrefix(rawLine)); index += 1; } renderedBlocks.push(...renderMarkdownCodeBlock(codeLines.join("\n"))); @@ -1025,7 +1173,7 @@ function renderMarkdownToTelegramHtmlChunks(markdown: string): string[] { if (current.trim().length === 0) break; if ( isFencedCodeStart(current) || - isIndentedCodeLine(current) || + canStartIndentedCodeBlock(lines, index) || /^\s*>/.test(current) ) break; @@ -1033,12 +1181,7 @@ function renderMarkdownToTelegramHtmlChunks(markdown: string): string[] { textLines.push(current); index += 1; } - const renderedTextBlock = renderMarkdownTextLines( - textLines.join("\n"), - ).join("\n"); - if (renderedTextBlock.length > 0) { - renderedBlocks.push(renderedTextBlock); - } + renderedBlocks.push(...renderMarkdownTextBlock(textLines.join("\n"))); } const chunks: string[] = []; let current = ""; @@ -1179,9 +1322,11 @@ export default function (pi: ExtensionAPI) { let nextQueuedTelegramTurnOrder = 0; let nextPriorityReactionOrder = 0; let activeTelegramTurn: ActiveTelegramTurn | undefined; + let telegramTurnDispatchPending = false; let typingInterval: ReturnType