@adriangalilea/utils 2.2.0 → 2.3.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/README.md +15 -0
- package/dist/tg-md/index.d.ts +73 -0
- package/dist/tg-md/index.d.ts.map +1 -0
- package/dist/tg-md/index.js +251 -0
- package/dist/tg-md/index.js.map +1 -0
- package/package.json +5 -1
package/README.md
CHANGED
|
@@ -371,6 +371,21 @@ transform('<h1>Title</h1><ul><li>Point one</li><li>Point two</li></ul>')
|
|
|
371
371
|
// '<b><u>Title</u></b>\n\n• Point one\n• Point two'
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
+
### Telegram Markdown (`tg-md`)
|
|
375
|
+
|
|
376
|
+
The MARKDOWN twin of `tg-html`, for the markdown LLMs actually emit (grown in xtldr's bot, moved here verbatim). One opinionated dialect — `_` is NEVER emphasis so `snake_case` survives, every tag balanced by construction so Telegram can't reject a render, pure whole-string functions so full-frame draft repaints (`bot/draft`) degrade malformed mid-stream markdown to literal text — rendered to every shape a bot sends:
|
|
377
|
+
|
|
378
|
+
```typescript
|
|
379
|
+
import { markdownToRichHtml, markdownToTelegramHtml, toPlainText, tidyRichMarkdown } from '@adriangalilea/utils/tg-md'
|
|
380
|
+
|
|
381
|
+
markdownToRichHtml(md, coverUrl) // rich-message HTML (Bot API 10.1): real <h1>/<ul>, cover <img> spliced after the title
|
|
382
|
+
markdownToTelegramHtml(md) // strict parse_mode=HTML subset: bold-underlined title, breathing headings, • bullets, ###### footer stays quiet
|
|
383
|
+
toPlainText(md) // last-ditch fallback; quotes keep their > marker
|
|
384
|
+
tidyRichMarkdown(md) // strip the stray ```fence``` models wrap replies in
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
Zero dependencies, Worker-safe. Typography (heading air, bullet glyph, title underline) IS the opinion; it gets options when a second consumer needs a different look, not before.
|
|
388
|
+
|
|
374
389
|
### Telegram bot plugins (GramIO)
|
|
375
390
|
|
|
376
391
|
Plugins for personal Telegram bots built on [GramIO](https://gramio.dev). Each plugin lives at its own subpath; peer deps (`gramio`, `@gramio/storage`, `@gramio/session`, `@gramio/format`, `marked`) are **all optional** — install only what you import.
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tg-md — the opinionated Telegram-Markdown formatter (xtldr's house style,
|
|
3
|
+
* grown from theSummaryBot's tg-html lineage). ONE input dialect, the
|
|
4
|
+
* markdown LLMs actually emit, rendered to every shape a bot sends:
|
|
5
|
+
*
|
|
6
|
+
* markdownToRichHtml(md, coverUrl?) rich-message HTML (Bot API 10.1): real
|
|
7
|
+
* headings/lists, optional cover <img>
|
|
8
|
+
* spliced after the title
|
|
9
|
+
* markdownToTelegramHtml(md) strict parse_mode=HTML subset: # title
|
|
10
|
+
* -> bold underlined line, ## headings ->
|
|
11
|
+
* bold lines with breathing room, ######
|
|
12
|
+
* footer -> quiet plain line, bullets -> •
|
|
13
|
+
* toPlainText(md) last-ditch fallback (quotes keep their
|
|
14
|
+
* > marker so they still read as quotes)
|
|
15
|
+
* tidyRichMarkdown(md) strip the stray ```fence``` models add
|
|
16
|
+
*
|
|
17
|
+
* The dialect contract, load-bearing and deliberate:
|
|
18
|
+
* - `_` is NEVER emphasis — snake_case in model output survives verbatim.
|
|
19
|
+
* Only `*italic*` / `**bold**` are honored.
|
|
20
|
+
* - Every tag is balanced by construction: a render can never produce a
|
|
21
|
+
* message Telegram rejects; unmatched markers stay literal.
|
|
22
|
+
* - Pure functions of the whole string — safe for full-frame draft repaints
|
|
23
|
+
* (bot/draft): malformed mid-stream markdown degrades to literal text
|
|
24
|
+
* this frame and renders correctly the next.
|
|
25
|
+
*
|
|
26
|
+
* Typography (heading air, bullet glyph, the title underline) IS the opinion;
|
|
27
|
+
* it becomes configurable when a second consumer needs a different look, not
|
|
28
|
+
* before. Zero deps, worker-safe.
|
|
29
|
+
*/
|
|
30
|
+
/** Unwrap a leading/trailing ```…``` code fence (some models add one) and trim. */
|
|
31
|
+
export declare function tidyRichMarkdown(raw: string): string;
|
|
32
|
+
/**
|
|
33
|
+
* Render the model's rich Markdown into Telegram's strict `parse_mode=HTML` subset,
|
|
34
|
+
* which has no headings or lists:
|
|
35
|
+
* `#` title -> a bold UNDERLINED line + blank line (the wordmark look)
|
|
36
|
+
* `##`..`#####` headings -> a bold line + blank line after (air between head and body)
|
|
37
|
+
* `######` footer heading -> a bold line, no forced air (it's the last line)
|
|
38
|
+
* bullets (`- `/`* `/`+ `) -> a `• ` prefix
|
|
39
|
+
* blockquote (`> `) -> `<blockquote>` (consecutive lines merged)
|
|
40
|
+
* `**bold**`/`*italic*` -> `<b>`/`<i>`; `` `code` `` -> `<code>`; links -> `<a>`
|
|
41
|
+
* Only `*`-based emphasis is honored (never `_`), so `snake_case` survives. Every tag
|
|
42
|
+
* is balanced, so Telegram can't reject the message; unmatched markers stay literal.
|
|
43
|
+
* The spacing is the legacy tghtml contract (now @adriangalilea/utils/tg-html): every
|
|
44
|
+
* heading is followed by a blank line so sections breathe — a heading hugging its
|
|
45
|
+
* bullets reads as a wall.
|
|
46
|
+
*/
|
|
47
|
+
export declare function markdownToTelegramHtml(markdown: string): string;
|
|
48
|
+
/**
|
|
49
|
+
* Render the model's rich Markdown into rich-message HTML (the `rich_message.html`
|
|
50
|
+
* field, Bot API 10.1), which supports tags `parse_mode=HTML` lacks, so headings and
|
|
51
|
+
* lists survive as real structure instead of flattening:
|
|
52
|
+
* headings (`#`..`######`) -> `<h1>`..`<h6>` (level = count of `#`)
|
|
53
|
+
* bullets (`- `/`* `/`+ `) -> `<ul><li>…</li></ul>` (consecutive items merged)
|
|
54
|
+
* blockquote (`> `) -> `<blockquote>` (consecutive lines merged)
|
|
55
|
+
* Inline emphasis/code/links reuse {@link inlineMd}. Every tag is balanced, so Telegram
|
|
56
|
+
* can't reject the message; unmatched markers stay literal.
|
|
57
|
+
*
|
|
58
|
+
* `coverUrl` (rich path only) embeds a cover image as a standalone `<img>` block right after
|
|
59
|
+
* the title heading (title, cover, then body), matching the rich-message photo block, which
|
|
60
|
+
* accepts HTTP(S) URLs only. Ignored unless it's a well-formed http(s) URL. The strict-HTML and
|
|
61
|
+
* plain fallbacks have no image support, so they never carry a cover.
|
|
62
|
+
*/
|
|
63
|
+
export declare function markdownToRichHtml(markdown: string, coverUrl?: string): string;
|
|
64
|
+
/** Escape text/code content for Telegram HTML. */
|
|
65
|
+
export declare function escapeHtml(s: string): string;
|
|
66
|
+
/**
|
|
67
|
+
* Strip Markdown to plain text: the fallback when Telegram rejects the rich message.
|
|
68
|
+
* Removes heading/bullet markers and emphasis, unwraps links to their text, but KEEPS
|
|
69
|
+
* the `>` blockquote marker on every quote line so quotes still read as quotes with no
|
|
70
|
+
* formatting to lean on. Best-effort, not a full parser.
|
|
71
|
+
*/
|
|
72
|
+
export declare function toPlainText(markdown: string): string;
|
|
73
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/tg-md/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA4BG;AAEH,mFAAmF;AACnF,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAIpD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,sBAAsB,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAkD/D;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,kBAAkB,CACjC,QAAQ,EAAE,MAAM,EAChB,QAAQ,CAAC,EAAE,MAAM,GACf,MAAM,CA+DR;AAwED,kDAAkD;AAClD,wBAAgB,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,MAAM,CAE5C;AAWD;;;;;GAKG;AACH,wBAAgB,WAAW,CAAC,QAAQ,EAAE,MAAM,GAAG,MAAM,CAYpD"}
|
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tg-md — the opinionated Telegram-Markdown formatter (xtldr's house style,
|
|
3
|
+
* grown from theSummaryBot's tg-html lineage). ONE input dialect, the
|
|
4
|
+
* markdown LLMs actually emit, rendered to every shape a bot sends:
|
|
5
|
+
*
|
|
6
|
+
* markdownToRichHtml(md, coverUrl?) rich-message HTML (Bot API 10.1): real
|
|
7
|
+
* headings/lists, optional cover <img>
|
|
8
|
+
* spliced after the title
|
|
9
|
+
* markdownToTelegramHtml(md) strict parse_mode=HTML subset: # title
|
|
10
|
+
* -> bold underlined line, ## headings ->
|
|
11
|
+
* bold lines with breathing room, ######
|
|
12
|
+
* footer -> quiet plain line, bullets -> •
|
|
13
|
+
* toPlainText(md) last-ditch fallback (quotes keep their
|
|
14
|
+
* > marker so they still read as quotes)
|
|
15
|
+
* tidyRichMarkdown(md) strip the stray ```fence``` models add
|
|
16
|
+
*
|
|
17
|
+
* The dialect contract, load-bearing and deliberate:
|
|
18
|
+
* - `_` is NEVER emphasis — snake_case in model output survives verbatim.
|
|
19
|
+
* Only `*italic*` / `**bold**` are honored.
|
|
20
|
+
* - Every tag is balanced by construction: a render can never produce a
|
|
21
|
+
* message Telegram rejects; unmatched markers stay literal.
|
|
22
|
+
* - Pure functions of the whole string — safe for full-frame draft repaints
|
|
23
|
+
* (bot/draft): malformed mid-stream markdown degrades to literal text
|
|
24
|
+
* this frame and renders correctly the next.
|
|
25
|
+
*
|
|
26
|
+
* Typography (heading air, bullet glyph, the title underline) IS the opinion;
|
|
27
|
+
* it becomes configurable when a second consumer needs a different look, not
|
|
28
|
+
* before. Zero deps, worker-safe.
|
|
29
|
+
*/
|
|
30
|
+
/** Unwrap a leading/trailing ```…``` code fence (some models add one) and trim. */
|
|
31
|
+
export function tidyRichMarkdown(raw) {
|
|
32
|
+
const trimmed = raw.trim();
|
|
33
|
+
const fenced = /^```[a-zA-Z]*\n([\s\S]*?)\n?```$/.exec(trimmed);
|
|
34
|
+
return (fenced ? fenced[1] : trimmed).trim();
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Render the model's rich Markdown into Telegram's strict `parse_mode=HTML` subset,
|
|
38
|
+
* which has no headings or lists:
|
|
39
|
+
* `#` title -> a bold UNDERLINED line + blank line (the wordmark look)
|
|
40
|
+
* `##`..`#####` headings -> a bold line + blank line after (air between head and body)
|
|
41
|
+
* `######` footer heading -> a bold line, no forced air (it's the last line)
|
|
42
|
+
* bullets (`- `/`* `/`+ `) -> a `• ` prefix
|
|
43
|
+
* blockquote (`> `) -> `<blockquote>` (consecutive lines merged)
|
|
44
|
+
* `**bold**`/`*italic*` -> `<b>`/`<i>`; `` `code` `` -> `<code>`; links -> `<a>`
|
|
45
|
+
* Only `*`-based emphasis is honored (never `_`), so `snake_case` survives. Every tag
|
|
46
|
+
* is balanced, so Telegram can't reject the message; unmatched markers stay literal.
|
|
47
|
+
* The spacing is the legacy tghtml contract (now @adriangalilea/utils/tg-html): every
|
|
48
|
+
* heading is followed by a blank line so sections breathe — a heading hugging its
|
|
49
|
+
* bullets reads as a wall.
|
|
50
|
+
*/
|
|
51
|
+
export function markdownToTelegramHtml(markdown) {
|
|
52
|
+
const lines = tidyRichMarkdown(markdown).split(/\r?\n/);
|
|
53
|
+
const out = [];
|
|
54
|
+
let quote = [];
|
|
55
|
+
const flushQuote = () => {
|
|
56
|
+
if (quote.length) {
|
|
57
|
+
out.push(`<blockquote>${quote.join("\n")}</blockquote>`);
|
|
58
|
+
quote = [];
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
for (const raw of lines) {
|
|
62
|
+
const line = raw.replace(/\s+$/, "");
|
|
63
|
+
const q = /^\s{0,3}>\s?(.*)$/.exec(line);
|
|
64
|
+
if (q) {
|
|
65
|
+
quote.push(inlineMd(q[1]));
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
flushQuote();
|
|
69
|
+
if (!line.trim()) {
|
|
70
|
+
out.push("");
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
const heading = /^\s{0,3}(#{1,6})\s+(.*)$/.exec(line);
|
|
74
|
+
if (heading) {
|
|
75
|
+
const level = heading[1].length;
|
|
76
|
+
const text = inlineMd(heading[2]);
|
|
77
|
+
// ###### is the source footer: metadata, not a heading — it renders as a quiet
|
|
78
|
+
// plain line (the legacy look), never bold.
|
|
79
|
+
if (level === 1)
|
|
80
|
+
out.push(`<b><u>${text}</u></b>`, "");
|
|
81
|
+
else if (level === 6)
|
|
82
|
+
out.push(text);
|
|
83
|
+
else
|
|
84
|
+
out.push(`<b>${text}</b>`, "");
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
const bullet = /^\s{0,3}[-*+]\s+(.*)$/.exec(line);
|
|
88
|
+
if (bullet) {
|
|
89
|
+
out.push(`• ${inlineMd(bullet[1])}`);
|
|
90
|
+
continue;
|
|
91
|
+
}
|
|
92
|
+
out.push(inlineMd(line));
|
|
93
|
+
}
|
|
94
|
+
flushQuote();
|
|
95
|
+
return tidyHtml(out.join("\n"));
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* Render the model's rich Markdown into rich-message HTML (the `rich_message.html`
|
|
99
|
+
* field, Bot API 10.1), which supports tags `parse_mode=HTML` lacks, so headings and
|
|
100
|
+
* lists survive as real structure instead of flattening:
|
|
101
|
+
* headings (`#`..`######`) -> `<h1>`..`<h6>` (level = count of `#`)
|
|
102
|
+
* bullets (`- `/`* `/`+ `) -> `<ul><li>…</li></ul>` (consecutive items merged)
|
|
103
|
+
* blockquote (`> `) -> `<blockquote>` (consecutive lines merged)
|
|
104
|
+
* Inline emphasis/code/links reuse {@link inlineMd}. Every tag is balanced, so Telegram
|
|
105
|
+
* can't reject the message; unmatched markers stay literal.
|
|
106
|
+
*
|
|
107
|
+
* `coverUrl` (rich path only) embeds a cover image as a standalone `<img>` block right after
|
|
108
|
+
* the title heading (title, cover, then body), matching the rich-message photo block, which
|
|
109
|
+
* accepts HTTP(S) URLs only. Ignored unless it's a well-formed http(s) URL. The strict-HTML and
|
|
110
|
+
* plain fallbacks have no image support, so they never carry a cover.
|
|
111
|
+
*/
|
|
112
|
+
export function markdownToRichHtml(markdown, coverUrl) {
|
|
113
|
+
const lines = tidyRichMarkdown(markdown).split(/\r?\n/);
|
|
114
|
+
const out = [];
|
|
115
|
+
let quote = [];
|
|
116
|
+
let bullets = [];
|
|
117
|
+
const flushQuote = () => {
|
|
118
|
+
if (quote.length) {
|
|
119
|
+
out.push(`<blockquote>${quote.join("\n")}</blockquote>`);
|
|
120
|
+
quote = [];
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
const flushBullets = () => {
|
|
124
|
+
if (bullets.length) {
|
|
125
|
+
out.push(`<ul>${bullets.map((b) => `<li>${b}</li>`).join("")}</ul>`);
|
|
126
|
+
bullets = [];
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
for (const raw of lines) {
|
|
130
|
+
const line = raw.replace(/\s+$/, "");
|
|
131
|
+
const q = /^\s{0,3}>\s?(.*)$/.exec(line);
|
|
132
|
+
if (q) {
|
|
133
|
+
flushBullets();
|
|
134
|
+
quote.push(inlineMd(q[1]));
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const bullet = /^\s{0,3}[-*+]\s+(.*)$/.exec(line);
|
|
138
|
+
if (bullet) {
|
|
139
|
+
flushQuote();
|
|
140
|
+
bullets.push(inlineMd(bullet[1]));
|
|
141
|
+
continue;
|
|
142
|
+
}
|
|
143
|
+
flushQuote();
|
|
144
|
+
flushBullets();
|
|
145
|
+
if (!line.trim()) {
|
|
146
|
+
out.push("");
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
const heading = /^\s{0,3}(#{1,6})\s+(.*)$/.exec(line);
|
|
150
|
+
if (heading) {
|
|
151
|
+
const level = heading[1].length;
|
|
152
|
+
out.push(`<h${level}>${inlineMd(heading[2])}</h${level}>`);
|
|
153
|
+
continue;
|
|
154
|
+
}
|
|
155
|
+
out.push(inlineMd(line));
|
|
156
|
+
}
|
|
157
|
+
flushQuote();
|
|
158
|
+
flushBullets();
|
|
159
|
+
insertCover(out, coverUrl);
|
|
160
|
+
return out
|
|
161
|
+
.join("\n")
|
|
162
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
163
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
164
|
+
.trim();
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* Insert a cover `<img>` block into the rendered rich-HTML lines: right after the title heading
|
|
168
|
+
* (so it reads title → cover → body), or at the very top when there's no title. Anchors on `<h1>`
|
|
169
|
+
* only, since the summary's title is `#` (h1) while section headings are `##` (h2) and the source
|
|
170
|
+
* footer is `######` (h6): matching any `<h[1-6]>` would splice the cover below the footer when the
|
|
171
|
+
* model omits the title. A blank line surrounds it so it stays its own block (media blocks can't be
|
|
172
|
+
* inline). No-op unless `url` is a well-formed http(s) URL, since the rich photo block accepts only those.
|
|
173
|
+
*/
|
|
174
|
+
function insertCover(out, url) {
|
|
175
|
+
if (!url || !/^https?:\/\/\S+$/i.test(url.trim()))
|
|
176
|
+
return;
|
|
177
|
+
const img = `<img src="${escapeAttr(url.trim())}"/>`;
|
|
178
|
+
const titleAt = out.findIndex((line) => /^<h1>/.test(line));
|
|
179
|
+
const at = titleAt === -1 ? 0 : titleAt + 1;
|
|
180
|
+
out.splice(at, 0, "", img, "");
|
|
181
|
+
}
|
|
182
|
+
/** Normalize rendered HTML: strip trailing whitespace, collapse blank-line runs. Blank lines
|
|
183
|
+
* around lists stay — that air is the layout (the legacy look); only runs of 3+ collapse. */
|
|
184
|
+
function tidyHtml(html) {
|
|
185
|
+
return html
|
|
186
|
+
.replace(/[ \t]+\n/g, "\n")
|
|
187
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
188
|
+
.trim();
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Inline Markdown to Telegram HTML for one line. Code spans and links are pulled out
|
|
192
|
+
* first (so their contents aren't escaped twice or treated as emphasis) and stashed;
|
|
193
|
+
* the rest is escaped, `**bold**`/`*italic*` become tags, then the fragments are restored.
|
|
194
|
+
*/
|
|
195
|
+
// Placeholder sentinel for stashed fragments: U+F8FF, a private-use codepoint
|
|
196
|
+
// that renders INVISIBLY in most editors — declared here as an escape so the
|
|
197
|
+
// load-bearing character can never be destroyed by a well-meaning hand edit.
|
|
198
|
+
// It cannot occur in model output the way any printable marker could.
|
|
199
|
+
const STASH = "\uf8ff";
|
|
200
|
+
function inlineMd(text) {
|
|
201
|
+
const tokens = [];
|
|
202
|
+
const stash = (html) => {
|
|
203
|
+
tokens.push(html);
|
|
204
|
+
return `${STASH}${tokens.length - 1}${STASH}`;
|
|
205
|
+
};
|
|
206
|
+
// Code spans first: their contents are literal (no emphasis, no nested links).
|
|
207
|
+
let s = text.replace(/`([^`]+)`/g, (_, code) => stash(`<code>${escapeHtml(code)}</code>`));
|
|
208
|
+
// Then links: drop the href unless it's a trusted scheme, keeping the label.
|
|
209
|
+
s = s.replace(/\[([^\]]*)\]\(([^)\s]+)\)/g, (_, label, url) => {
|
|
210
|
+
const safe = /^(https?:|tg:|mailto:)/i.test(url) ? url : null;
|
|
211
|
+
return stash(safe
|
|
212
|
+
? `<a href="${escapeAttr(safe)}">${escapeHtml(label)}</a>`
|
|
213
|
+
: escapeHtml(label));
|
|
214
|
+
});
|
|
215
|
+
s = escapeHtml(s);
|
|
216
|
+
s = s.replace(/\*\*([^*]+)\*\*/g, "<b>$1</b>"); // bold before italic so `**` wins over `*`
|
|
217
|
+
s = s.replace(/(^|[^*])\*(?!\s)([^*]+?)\*/g, "$1<i>$2</i>");
|
|
218
|
+
return s.replace(new RegExp(`${STASH}(\\d+)${STASH}`, "g"), (_, i) => tokens[Number(i)]);
|
|
219
|
+
}
|
|
220
|
+
/** Escape text/code content for Telegram HTML. */
|
|
221
|
+
export function escapeHtml(s) {
|
|
222
|
+
return s.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
223
|
+
}
|
|
224
|
+
/** Escape an href for a Telegram HTML attribute. */
|
|
225
|
+
function escapeAttr(s) {
|
|
226
|
+
return s
|
|
227
|
+
.replace(/&/g, "&")
|
|
228
|
+
.replace(/"/g, """)
|
|
229
|
+
.replace(/</g, "<")
|
|
230
|
+
.replace(/>/g, ">");
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Strip Markdown to plain text: the fallback when Telegram rejects the rich message.
|
|
234
|
+
* Removes heading/bullet markers and emphasis, unwraps links to their text, but KEEPS
|
|
235
|
+
* the `>` blockquote marker on every quote line so quotes still read as quotes with no
|
|
236
|
+
* formatting to lean on. Best-effort, not a full parser.
|
|
237
|
+
*/
|
|
238
|
+
export function toPlainText(markdown) {
|
|
239
|
+
return tidyRichMarkdown(markdown)
|
|
240
|
+
.replace(/```[a-zA-Z]*\n?([\s\S]*?)```/g, "$1") // fenced code -> its contents
|
|
241
|
+
.replace(/^\s{0,3}#{1,6}\s+/gm, "") // heading markers
|
|
242
|
+
.replace(/^\s{0,3}>\s?/gm, "> ") // normalize blockquote markers, keep them
|
|
243
|
+
.replace(/^(\s*)[-*+]\s+/gm, "$1• ") // bullet markers -> •
|
|
244
|
+
.replace(/\[([^\]]*)\]\([^)]*\)/g, "$1") // [text](url) -> text
|
|
245
|
+
.replace(/(\*\*|__)(.*?)\1/g, "$2") // bold
|
|
246
|
+
.replace(/(\*|_)(.*?)\1/g, "$2") // italic
|
|
247
|
+
.replace(/`([^`]*)`/g, "$1") // inline code
|
|
248
|
+
.replace(/\n{3,}/g, "\n\n")
|
|
249
|
+
.trim();
|
|
250
|
+
}
|
|
251
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/tg-md/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adriangalilea/utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.1",
|
|
4
4
|
"description": "TypeScript utilities - logger, currency, formatter, GramIO bot plugins, and more",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -44,6 +44,10 @@
|
|
|
44
44
|
"types": "./dist/tg-html/index.d.ts",
|
|
45
45
|
"default": "./dist/tg-html/index.js"
|
|
46
46
|
},
|
|
47
|
+
"./tg-md": {
|
|
48
|
+
"types": "./dist/tg-md/index.d.ts",
|
|
49
|
+
"default": "./dist/tg-md/index.js"
|
|
50
|
+
},
|
|
47
51
|
"./bot": {
|
|
48
52
|
"types": "./dist/bot/index.d.ts",
|
|
49
53
|
"default": "./dist/bot/index.js"
|