feat: add Telegram HTML format and split helpers
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,49 @@
|
|||||||
|
export function escapeHtml(text: string): string {
|
||||||
|
return text
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"')
|
||||||
|
.replace(/'/g, ''')
|
||||||
|
}
|
||||||
|
|
||||||
|
function inlineToHtml(text: string): string {
|
||||||
|
let html = escapeHtml(text)
|
||||||
|
html = html.replace(/`([^`\n]+)`/g, '<code>$1</code>')
|
||||||
|
html = html.replace(/\*\*([^*\n]+)\*\*/g, '<b>$1</b>')
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
export function markdownToHtml(text: string): string {
|
||||||
|
const parts = text.split(/```[\w-]*/)
|
||||||
|
if (parts.length % 2 === 0) return inlineToHtml(text)
|
||||||
|
let html = ''
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (i % 2 === 1) {
|
||||||
|
const code = parts[i]!.replace(/^\n/, '').replace(/\n$/, '')
|
||||||
|
html += `<pre>${escapeHtml(code)}</pre>`
|
||||||
|
} else {
|
||||||
|
html += inlineToHtml(parts[i]!)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return html
|
||||||
|
}
|
||||||
|
|
||||||
|
export function splitMessage(text: string, maxLength: number): string[] {
|
||||||
|
if (maxLength < 1) throw new Error('maxLength must be >= 1')
|
||||||
|
if (text.length <= maxLength) return [text]
|
||||||
|
const chunks: string[] = []
|
||||||
|
let rest = text
|
||||||
|
while (rest.length > maxLength) {
|
||||||
|
const window = rest.slice(0, maxLength)
|
||||||
|
const newline = window.lastIndexOf('\n')
|
||||||
|
const ideographic = window.lastIndexOf('。')
|
||||||
|
const sentence = window.lastIndexOf('. ')
|
||||||
|
const breakAt = Math.max(newline, ideographic, sentence)
|
||||||
|
const cut = breakAt > 0 ? (breakAt === sentence ? breakAt + 2 : breakAt + 1) : maxLength
|
||||||
|
chunks.push(rest.slice(0, cut))
|
||||||
|
rest = rest.slice(cut)
|
||||||
|
}
|
||||||
|
chunks.push(rest)
|
||||||
|
return chunks
|
||||||
|
}
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import assert from 'node:assert/strict'
|
||||||
|
import test from 'node:test'
|
||||||
|
import { escapeHtml, markdownToHtml, splitMessage } from '../src/format.ts'
|
||||||
|
|
||||||
|
test('escapeHtml escapes five special chars', () => {
|
||||||
|
assert.equal(escapeHtml(`&<>"'`), '&<>"'')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('markdownToHtml converts fence bold and inline code', () => {
|
||||||
|
const html = markdownToHtml('hi **x** and `y`\n```\nz\n```')
|
||||||
|
assert.match(html, /<b>x<\/b>/)
|
||||||
|
assert.match(html, /<code>y<\/code>/)
|
||||||
|
assert.match(html, /<pre>z<\/pre>/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('markdownToHtml keeps unbalanced fence without pre', () => {
|
||||||
|
const html = markdownToHtml('start ``` not closed')
|
||||||
|
assert.equal(html.includes('<pre>'), false)
|
||||||
|
assert.match(html, /start/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('splitMessage prefers newline within window', () => {
|
||||||
|
const text = 'aaaa\nbbbb\ncccc'
|
||||||
|
assert.deepEqual(splitMessage(text, 6), ['aaaa\n', 'bbbb\n', 'cccc'])
|
||||||
|
})
|
||||||
|
|
||||||
|
test('splitMessage hard-cuts when no break', () => {
|
||||||
|
assert.deepEqual(splitMessage('abcdefghij', 4), ['abcd', 'efgh', 'ij'])
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user