diff --git a/src/format.ts b/src/format.ts new file mode 100644 index 0000000..4cb7b6d --- /dev/null +++ b/src/format.ts @@ -0,0 +1,49 @@ +export function escapeHtml(text: string): string { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, ''') +} + +function inlineToHtml(text: string): string { + let html = escapeHtml(text) + html = html.replace(/`([^`\n]+)`/g, '$1') + html = html.replace(/\*\*([^*\n]+)\*\*/g, '$1') + 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 += `
${escapeHtml(code)}
` + } 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 +} diff --git a/tests/format.test.ts b/tests/format.test.ts new file mode 100644 index 0000000..1448ac5 --- /dev/null +++ b/tests/format.test.ts @@ -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, /x<\/b>/) + assert.match(html, /y<\/code>/) + assert.match(html, /
z<\/pre>/)
+})
+
+test('markdownToHtml keeps unbalanced fence without pre', () => {
+  const html = markdownToHtml('start ``` not closed')
+  assert.equal(html.includes('
'), 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'])
+})