@moxxy/plugin-telegram 0.36.1 → 0.37.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channel/html.d.ts.map +1 -1
- package/dist/channel/html.js +19 -2
- package/dist/channel/html.js.map +1 -1
- package/dist/format.d.ts.map +1 -1
- package/dist/format.js +57 -16
- package/dist/format.js.map +1 -1
- package/package.json +9 -9
- package/src/channel/html.test.ts +4 -0
- package/src/channel/html.ts +20 -2
- package/src/format.test.ts +11 -0
- package/src/format.ts +60 -16
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/channel/html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAWxD;AAED;;;;;;;gFAOgF;AAChF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"html.d.ts","sourceRoot":"","sources":["../../src/channel/html.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,cAAc,CAAC;AAElD;;;;;;;;;;GAUG;AACH,wBAAgB,YAAY,CAAC,IAAI,EAAE,aAAa,GAAG,MAAM,CAWxD;AAED;;;;;;;gFAOgF;AAChF,wBAAgB,SAAS,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAQ9C;AAgCD,wBAAgB,QAAQ,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAErD"}
|
package/dist/channel/html.js
CHANGED
|
@@ -35,8 +35,7 @@ export function composeFrame(snap) {
|
|
|
35
35
|
* (`'`, `'`) are handled too, since escaped user/code content can
|
|
36
36
|
* carry them and they'd otherwise leak literally into the fallback message. */
|
|
37
37
|
export function stripHtml(html) {
|
|
38
|
-
return html
|
|
39
|
-
.replace(/<\/?[a-z][^>]*>/gi, '')
|
|
38
|
+
return stripTags(html)
|
|
40
39
|
.replace(/&#(\d+);/g, (_, dec) => safeFromCodePoint(parseInt(dec, 10)))
|
|
41
40
|
.replace(/&#x([0-9a-f]+);/gi, (_, hex) => safeFromCodePoint(parseInt(hex, 16)))
|
|
42
41
|
.replace(/</g, '<')
|
|
@@ -44,6 +43,24 @@ export function stripHtml(html) {
|
|
|
44
43
|
.replace(/"/g, '"')
|
|
45
44
|
.replace(/&/g, '&');
|
|
46
45
|
}
|
|
46
|
+
function stripTags(html) {
|
|
47
|
+
const out = [];
|
|
48
|
+
let cursor = 0;
|
|
49
|
+
while (cursor < html.length) {
|
|
50
|
+
if (html[cursor] !== '<') {
|
|
51
|
+
out.push(html[cursor] ?? '');
|
|
52
|
+
cursor += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const end = html.indexOf('>', cursor + 1);
|
|
56
|
+
if (end === -1) {
|
|
57
|
+
out.push(html.slice(cursor));
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
cursor = end + 1;
|
|
61
|
+
}
|
|
62
|
+
return out.join('');
|
|
63
|
+
}
|
|
47
64
|
/** Decode a numeric entity's code point, leaving an out-of-range/invalid value
|
|
48
65
|
* untouched (return empty) rather than throwing. */
|
|
49
66
|
function safeFromCodePoint(cp) {
|
package/dist/channel/html.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/channel/html.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,sBAAsB,EAAE,MAAM,cAAc,CAAC;AAGtD;;;;;;;;;;GAUG;AACH,MAAM,UAAU,YAAY,CAAC,IAAmB;IAC9C,MAAM,KAAK,GAAa,EAAE,CAAC;IAC3B,IAAI,IAAI,CAAC,YAAY;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;IACrD,IAAI,IAAI,CAAC,IAAI;QAAE,KAAK,CAAC,IAAI,CAAC,sBAAsB,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC;IAC7D,+DAA+D;IAC/D,sEAAsE;IACtE,wEAAwE;IACxE,qEAAqE;IACrE,IAAI,IAAI,CAAC,QAAQ;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;IAC7C,IAAI,IAAI,CAAC,SAAS;QAAE,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,SAAS,CAAC,CAAC;IAC/C,OAAO,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;AAC5B,CAAC;AAED;;;;;;;gFAOgF;AAChF,MAAM,UAAU,SAAS,CAAC,IAAY;IACpC,OAAO,
|
|
1
|
+
{"version":3,"file":"html.js","sourceRoot":"","sources":["../../src/channel/html.ts"],"names":[],"mappings":"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"}
|
package/dist/format.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"format.d.ts","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,wBAAgB,sBAAsB,CAAC,EAAE,EAAE,MAAM,GAAG,MAAM,CA6FzD"}
|
package/dist/format.js
CHANGED
|
@@ -38,20 +38,28 @@ export function markdownToTelegramHtml(md) {
|
|
|
38
38
|
// Pull fenced code blocks out FIRST so their contents skip inline
|
|
39
39
|
// markdown processing. Replace each with a placeholder, render the
|
|
40
40
|
// rest, then splice them back.
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
const html = `<pre><code${lang ? ` class="language-${escapeHtml(
|
|
44
|
-
escapeHtml(
|
|
41
|
+
const extracted = extractFences(md);
|
|
42
|
+
const fences = extracted.fences.map(({ lang, body }) => {
|
|
43
|
+
const html = `<pre><code${lang ? ` class="language-${escapeHtml(lang)}"` : ''}>` +
|
|
44
|
+
escapeHtml(trimTrailingNewlines(body)) +
|
|
45
45
|
'</code></pre>';
|
|
46
|
-
|
|
47
|
-
return ` FENCE${fences.length - 1} `;
|
|
46
|
+
return html;
|
|
48
47
|
});
|
|
48
|
+
let working = extracted.working;
|
|
49
49
|
// Pull inline code spans out next, same reason.
|
|
50
50
|
const inlines = [];
|
|
51
51
|
working = working.replace(/`([^`\n]+)`/g, (_, body) => {
|
|
52
52
|
inlines.push(`<code>${escapeHtml(String(body))}</code>`);
|
|
53
53
|
return ` INLINE${inlines.length - 1} `;
|
|
54
54
|
});
|
|
55
|
+
// Pull link destinations out before HTML escaping. The visible label stays
|
|
56
|
+
// in the Markdown pipeline, while the raw target is validated and escaped
|
|
57
|
+
// exactly once when the final anchor is emitted.
|
|
58
|
+
const linkTargets = [];
|
|
59
|
+
working = working.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, text, url) => {
|
|
60
|
+
linkTargets.push(String(url));
|
|
61
|
+
return `[${String(text)}](LINKTARGET${linkTargets.length - 1})`;
|
|
62
|
+
});
|
|
55
63
|
// Now safe to escape HTML special chars in everything else.
|
|
56
64
|
working = escapeHtml(working);
|
|
57
65
|
// Headings → bold. Telegram has no heading element; bold + line
|
|
@@ -86,15 +94,12 @@ export function markdownToTelegramHtml(md) {
|
|
|
86
94
|
// mid-word underscores (`some_var_name`) and stray bullets.
|
|
87
95
|
working = working.replace(/(^|[\s(,.!?:;])\*([^*\n]+)\*(?=$|[\s),.!?:;])/g, (_m, pre, body) => `${pre}<i>${String(body)}</i>`);
|
|
88
96
|
working = working.replace(/(^|[\s(,.!?:;])_([^_\n]+)_(?=$|[\s),.!?:;])/g, (_m, pre, body) => `${pre}<i>${String(body)}</i>`);
|
|
89
|
-
// Links [text](url)
|
|
90
|
-
//
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
// for the typical case; `<>` shouldn't appear in a URL but unescape
|
|
96
|
-
// them defensively.
|
|
97
|
-
const cleanUrl = String(url).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
97
|
+
// Links [text](url). Targets were removed before escaping, so they cannot
|
|
98
|
+
// be double-decoded and are escaped once at this attribute boundary.
|
|
99
|
+
working = working.replace(/\[([^\]]+)\]\(LINKTARGET(\d+)\)/g, (_match, text, index) => {
|
|
100
|
+
const cleanUrl = linkTargets[Number(index)];
|
|
101
|
+
if (cleanUrl === undefined)
|
|
102
|
+
return String(text);
|
|
98
103
|
// The anchor target comes from arbitrary model output (and, via prompt
|
|
99
104
|
// injection, untrusted data the model echoes). Don't rely on Telegram's
|
|
100
105
|
// server-side parser as the sole sanitizer: emit a clickable <a> only for
|
|
@@ -103,7 +108,7 @@ export function markdownToTelegramHtml(md) {
|
|
|
103
108
|
// the original `[text](url)` text (already HTML-escaped upstream) so it
|
|
104
109
|
// can't navigate the client.
|
|
105
110
|
if (!isAllowedUrl(cleanUrl))
|
|
106
|
-
return String(
|
|
111
|
+
return `[${String(text)}](${escapeHtml(cleanUrl)})`;
|
|
107
112
|
return `<a href="${escapeAttr(cleanUrl)}">${String(text)}</a>`;
|
|
108
113
|
});
|
|
109
114
|
// Splice fenced + inline code back in.
|
|
@@ -111,6 +116,42 @@ export function markdownToTelegramHtml(md) {
|
|
|
111
116
|
working = working.replace(/ FENCE(\d+) /g, (_, i) => fences[Number(i)] ?? '');
|
|
112
117
|
return working;
|
|
113
118
|
}
|
|
119
|
+
function extractFences(md) {
|
|
120
|
+
const output = [];
|
|
121
|
+
const fences = [];
|
|
122
|
+
let cursor = 0;
|
|
123
|
+
while (cursor < md.length) {
|
|
124
|
+
const start = md.indexOf('```', cursor);
|
|
125
|
+
if (start === -1)
|
|
126
|
+
break;
|
|
127
|
+
let bodyStart = start + 3;
|
|
128
|
+
while (bodyStart < md.length && isFenceLanguageChar(md[bodyStart]))
|
|
129
|
+
bodyStart += 1;
|
|
130
|
+
const lang = md.slice(start + 3, bodyStart);
|
|
131
|
+
if (md[bodyStart] === '\n')
|
|
132
|
+
bodyStart += 1;
|
|
133
|
+
const close = md.indexOf('```', bodyStart);
|
|
134
|
+
if (close === -1)
|
|
135
|
+
break;
|
|
136
|
+
output.push(md.slice(cursor, start), ` FENCE${fences.length} `);
|
|
137
|
+
fences.push({ lang, body: md.slice(bodyStart, close) });
|
|
138
|
+
cursor = close + 3;
|
|
139
|
+
}
|
|
140
|
+
output.push(md.slice(cursor));
|
|
141
|
+
return { working: output.join(''), fences };
|
|
142
|
+
}
|
|
143
|
+
function isFenceLanguageChar(char) {
|
|
144
|
+
return char !== undefined && ((char >= 'a' && char <= 'z') ||
|
|
145
|
+
(char >= 'A' && char <= 'Z') ||
|
|
146
|
+
(char >= '0' && char <= '9') ||
|
|
147
|
+
char === '_' || char === '+' || char === '-');
|
|
148
|
+
}
|
|
149
|
+
function trimTrailingNewlines(value) {
|
|
150
|
+
let end = value.length;
|
|
151
|
+
while (end > 0 && value[end - 1] === '\n')
|
|
152
|
+
end -= 1;
|
|
153
|
+
return value.slice(0, end);
|
|
154
|
+
}
|
|
114
155
|
/**
|
|
115
156
|
* GitHub / Obsidian-style callout types we recognise as the first line of a
|
|
116
157
|
* blockquote (`[!note]`, `[!warning]`, …). Each maps to a leading emoji, a
|
package/dist/format.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmCG;AACH,MAAM,UAAU,sBAAsB,CAAC,EAAU;IAC/C,kEAAkE;IAClE,mEAAmE;IACnE,+BAA+B;IAC/B,MAAM,
|
|
1
|
+
{"version":3,"file":"format.js","sourceRoot":"","sources":["../src/format.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@moxxy/plugin-telegram",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.37.1",
|
|
4
4
|
"description": "Telegram channel for moxxy via grammy. Treats Telegram chats as a bidirectional channel: prompts in, assistant + tool prompts out, inline-keyboard permission flow, QR deep-link chat pairing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"moxxy",
|
|
@@ -68,19 +68,19 @@
|
|
|
68
68
|
"@clack/prompts": "^1.4.0",
|
|
69
69
|
"grammy": "^1.30.0",
|
|
70
70
|
"qrcode": "^1.5.4",
|
|
71
|
-
"@moxxy/
|
|
72
|
-
"@moxxy/
|
|
73
|
-
"@moxxy/
|
|
74
|
-
"@moxxy/
|
|
75
|
-
"@moxxy/sdk": "0.
|
|
71
|
+
"@moxxy/config": "0.37.1",
|
|
72
|
+
"@moxxy/channel-kit": "0.37.1",
|
|
73
|
+
"@moxxy/core": "0.37.1",
|
|
74
|
+
"@moxxy/plugin-vault": "0.37.1",
|
|
75
|
+
"@moxxy/sdk": "0.37.1"
|
|
76
76
|
},
|
|
77
77
|
"devDependencies": {
|
|
78
78
|
"@types/node": "^22.10.0",
|
|
79
79
|
"@types/qrcode": "^1.5.5",
|
|
80
80
|
"typescript": "^5.7.3",
|
|
81
|
-
"vitest": "^2.
|
|
82
|
-
"@moxxy/
|
|
83
|
-
"@moxxy/
|
|
81
|
+
"vitest": "^3.2.6",
|
|
82
|
+
"@moxxy/vitest-preset": "0.0.0",
|
|
83
|
+
"@moxxy/tsconfig": "0.0.0"
|
|
84
84
|
},
|
|
85
85
|
"scripts": {
|
|
86
86
|
"build": "tsc -p tsconfig.json",
|
package/src/channel/html.test.ts
CHANGED
|
@@ -6,6 +6,10 @@ describe('stripHtml', () => {
|
|
|
6
6
|
expect(stripHtml('<b>bold</b> and <code>code</code>')).toBe('bold and code');
|
|
7
7
|
});
|
|
8
8
|
|
|
9
|
+
it('does not create a fresh tag when adjacent markup is removed', () => {
|
|
10
|
+
expect(stripHtml('<scr<script>hidden</script>ipt>visible</script>')).toBe('hiddenipt>visible');
|
|
11
|
+
});
|
|
12
|
+
|
|
9
13
|
it('decodes the four named entities', () => {
|
|
10
14
|
expect(stripHtml('a < b > c & d "e"')).toBe('a < b > c & d "e"');
|
|
11
15
|
});
|
package/src/channel/html.ts
CHANGED
|
@@ -34,8 +34,7 @@ export function composeFrame(snap: RenderedFrame): string {
|
|
|
34
34
|
* (`'`, `'`) are handled too, since escaped user/code content can
|
|
35
35
|
* carry them and they'd otherwise leak literally into the fallback message. */
|
|
36
36
|
export function stripHtml(html: string): string {
|
|
37
|
-
return html
|
|
38
|
-
.replace(/<\/?[a-z][^>]*>/gi, '')
|
|
37
|
+
return stripTags(html)
|
|
39
38
|
.replace(/&#(\d+);/g, (_, dec: string) => safeFromCodePoint(parseInt(dec, 10)))
|
|
40
39
|
.replace(/&#x([0-9a-f]+);/gi, (_, hex: string) => safeFromCodePoint(parseInt(hex, 16)))
|
|
41
40
|
.replace(/</g, '<')
|
|
@@ -44,6 +43,25 @@ export function stripHtml(html: string): string {
|
|
|
44
43
|
.replace(/&/g, '&');
|
|
45
44
|
}
|
|
46
45
|
|
|
46
|
+
function stripTags(html: string): string {
|
|
47
|
+
const out: string[] = [];
|
|
48
|
+
let cursor = 0;
|
|
49
|
+
while (cursor < html.length) {
|
|
50
|
+
if (html[cursor] !== '<') {
|
|
51
|
+
out.push(html[cursor] ?? '');
|
|
52
|
+
cursor += 1;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
const end = html.indexOf('>', cursor + 1);
|
|
56
|
+
if (end === -1) {
|
|
57
|
+
out.push(html.slice(cursor));
|
|
58
|
+
break;
|
|
59
|
+
}
|
|
60
|
+
cursor = end + 1;
|
|
61
|
+
}
|
|
62
|
+
return out.join('');
|
|
63
|
+
}
|
|
64
|
+
|
|
47
65
|
/** Decode a numeric entity's code point, leaving an out-of-range/invalid value
|
|
48
66
|
* untouched (return empty) rather than throwing. */
|
|
49
67
|
function safeFromCodePoint(cp: number): string {
|
package/src/format.test.ts
CHANGED
|
@@ -34,11 +34,22 @@ describe('markdownToTelegramHtml', () => {
|
|
|
34
34
|
expect(out).toContain('**not bold**');
|
|
35
35
|
});
|
|
36
36
|
|
|
37
|
+
it('leaves a long unterminated fence literal', () => {
|
|
38
|
+
const input = `\`\`\`${'a'.repeat(50_000)}`;
|
|
39
|
+
expect(markdownToTelegramHtml(input)).toBe(input);
|
|
40
|
+
});
|
|
41
|
+
|
|
37
42
|
it('converts links to <a href="...">', () => {
|
|
38
43
|
const out = markdownToTelegramHtml('See [docs](https://example.com).');
|
|
39
44
|
expect(out).toContain('<a href="https://example.com">docs</a>');
|
|
40
45
|
});
|
|
41
46
|
|
|
47
|
+
it('escapes a link target exactly once', () => {
|
|
48
|
+
const out = markdownToTelegramHtml('[search](https://example.com/?a=1&b=%3Ctwo%3E)');
|
|
49
|
+
expect(out).toContain('href="https://example.com/?a=1&b=%3Ctwo%3E"');
|
|
50
|
+
expect(out).not.toContain('&amp;');
|
|
51
|
+
});
|
|
52
|
+
|
|
42
53
|
it('allows http, mailto, and tel scheme links', () => {
|
|
43
54
|
expect(markdownToTelegramHtml('[a](http://x.com)')).toContain('<a href="http://x.com">');
|
|
44
55
|
expect(markdownToTelegramHtml('[m](mailto:a@b.com)')).toContain('<a href="mailto:a@b.com">');
|
package/src/format.ts
CHANGED
|
@@ -38,15 +38,15 @@ export function markdownToTelegramHtml(md: string): string {
|
|
|
38
38
|
// Pull fenced code blocks out FIRST so their contents skip inline
|
|
39
39
|
// markdown processing. Replace each with a placeholder, render the
|
|
40
40
|
// rest, then splice them back.
|
|
41
|
-
const
|
|
42
|
-
|
|
41
|
+
const extracted = extractFences(md);
|
|
42
|
+
const fences = extracted.fences.map(({ lang, body }) => {
|
|
43
43
|
const html =
|
|
44
|
-
`<pre><code${lang ? ` class="language-${escapeHtml(
|
|
45
|
-
escapeHtml(
|
|
44
|
+
`<pre><code${lang ? ` class="language-${escapeHtml(lang)}"` : ''}>` +
|
|
45
|
+
escapeHtml(trimTrailingNewlines(body)) +
|
|
46
46
|
'</code></pre>';
|
|
47
|
-
|
|
48
|
-
return ` FENCE${fences.length - 1} `;
|
|
47
|
+
return html;
|
|
49
48
|
});
|
|
49
|
+
let working = extracted.working;
|
|
50
50
|
|
|
51
51
|
// Pull inline code spans out next, same reason.
|
|
52
52
|
const inlines: string[] = [];
|
|
@@ -55,6 +55,15 @@ export function markdownToTelegramHtml(md: string): string {
|
|
|
55
55
|
return ` INLINE${inlines.length - 1} `;
|
|
56
56
|
});
|
|
57
57
|
|
|
58
|
+
// Pull link destinations out before HTML escaping. The visible label stays
|
|
59
|
+
// in the Markdown pipeline, while the raw target is validated and escaped
|
|
60
|
+
// exactly once when the final anchor is emitted.
|
|
61
|
+
const linkTargets: string[] = [];
|
|
62
|
+
working = working.replace(/\[([^\]]+)\]\(([^)\s]+)\)/g, (_match, text, url) => {
|
|
63
|
+
linkTargets.push(String(url));
|
|
64
|
+
return `[${String(text)}](LINKTARGET${linkTargets.length - 1})`;
|
|
65
|
+
});
|
|
66
|
+
|
|
58
67
|
// Now safe to escape HTML special chars in everything else.
|
|
59
68
|
working = escapeHtml(working);
|
|
60
69
|
|
|
@@ -97,15 +106,11 @@ export function markdownToTelegramHtml(md: string): string {
|
|
|
97
106
|
working = working.replace(/(^|[\s(,.!?:;])\*([^*\n]+)\*(?=$|[\s),.!?:;])/g, (_m, pre, body) => `${pre}<i>${String(body)}</i>`);
|
|
98
107
|
working = working.replace(/(^|[\s(,.!?:;])_([^_\n]+)_(?=$|[\s),.!?:;])/g, (_m, pre, body) => `${pre}<i>${String(body)}</i>`);
|
|
99
108
|
|
|
100
|
-
// Links [text](url)
|
|
101
|
-
//
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
// actually points where it should. Only `&` → `&` is needed
|
|
106
|
-
// for the typical case; `<>` shouldn't appear in a URL but unescape
|
|
107
|
-
// them defensively.
|
|
108
|
-
const cleanUrl = String(url).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
|
109
|
+
// Links [text](url). Targets were removed before escaping, so they cannot
|
|
110
|
+
// be double-decoded and are escaped once at this attribute boundary.
|
|
111
|
+
working = working.replace(/\[([^\]]+)\]\(LINKTARGET(\d+)\)/g, (_match, text, index) => {
|
|
112
|
+
const cleanUrl = linkTargets[Number(index)];
|
|
113
|
+
if (cleanUrl === undefined) return String(text);
|
|
109
114
|
// The anchor target comes from arbitrary model output (and, via prompt
|
|
110
115
|
// injection, untrusted data the model echoes). Don't rely on Telegram's
|
|
111
116
|
// server-side parser as the sole sanitizer: emit a clickable <a> only for
|
|
@@ -113,7 +118,7 @@ export function markdownToTelegramHtml(md: string): string {
|
|
|
113
118
|
// else — `javascript:`, `data:`, `file:`, `tg://` deep links — is left as
|
|
114
119
|
// the original `[text](url)` text (already HTML-escaped upstream) so it
|
|
115
120
|
// can't navigate the client.
|
|
116
|
-
if (!isAllowedUrl(cleanUrl)) return String(
|
|
121
|
+
if (!isAllowedUrl(cleanUrl)) return `[${String(text)}](${escapeHtml(cleanUrl)})`;
|
|
117
122
|
return `<a href="${escapeAttr(cleanUrl)}">${String(text)}</a>`;
|
|
118
123
|
});
|
|
119
124
|
|
|
@@ -124,6 +129,45 @@ export function markdownToTelegramHtml(md: string): string {
|
|
|
124
129
|
return working;
|
|
125
130
|
}
|
|
126
131
|
|
|
132
|
+
function extractFences(md: string): {
|
|
133
|
+
readonly working: string;
|
|
134
|
+
readonly fences: ReadonlyArray<{ readonly lang: string; readonly body: string }>;
|
|
135
|
+
} {
|
|
136
|
+
const output: string[] = [];
|
|
137
|
+
const fences: Array<{ lang: string; body: string }> = [];
|
|
138
|
+
let cursor = 0;
|
|
139
|
+
while (cursor < md.length) {
|
|
140
|
+
const start = md.indexOf('```', cursor);
|
|
141
|
+
if (start === -1) break;
|
|
142
|
+
let bodyStart = start + 3;
|
|
143
|
+
while (bodyStart < md.length && isFenceLanguageChar(md[bodyStart])) bodyStart += 1;
|
|
144
|
+
const lang = md.slice(start + 3, bodyStart);
|
|
145
|
+
if (md[bodyStart] === '\n') bodyStart += 1;
|
|
146
|
+
const close = md.indexOf('```', bodyStart);
|
|
147
|
+
if (close === -1) break;
|
|
148
|
+
output.push(md.slice(cursor, start), ` FENCE${fences.length} `);
|
|
149
|
+
fences.push({ lang, body: md.slice(bodyStart, close) });
|
|
150
|
+
cursor = close + 3;
|
|
151
|
+
}
|
|
152
|
+
output.push(md.slice(cursor));
|
|
153
|
+
return { working: output.join(''), fences };
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
function isFenceLanguageChar(char: string | undefined): boolean {
|
|
157
|
+
return char !== undefined && (
|
|
158
|
+
(char >= 'a' && char <= 'z') ||
|
|
159
|
+
(char >= 'A' && char <= 'Z') ||
|
|
160
|
+
(char >= '0' && char <= '9') ||
|
|
161
|
+
char === '_' || char === '+' || char === '-'
|
|
162
|
+
);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function trimTrailingNewlines(value: string): string {
|
|
166
|
+
let end = value.length;
|
|
167
|
+
while (end > 0 && value[end - 1] === '\n') end -= 1;
|
|
168
|
+
return value.slice(0, end);
|
|
169
|
+
}
|
|
170
|
+
|
|
127
171
|
/**
|
|
128
172
|
* GitHub / Obsidian-style callout types we recognise as the first line of a
|
|
129
173
|
* blockquote (`[!note]`, `[!warning]`, …). Each maps to a leading emoji, a
|