@yaebal/rich 0.0.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/LICENSE +21 -0
- package/README.md +109 -0
- package/lib/blocks.d.ts +115 -0
- package/lib/blocks.d.ts.map +1 -0
- package/lib/blocks.js +149 -0
- package/lib/blocks.js.map +1 -0
- package/lib/draft.d.ts +44 -0
- package/lib/draft.d.ts.map +1 -0
- package/lib/draft.js +85 -0
- package/lib/draft.js.map +1 -0
- package/lib/escape.d.ts +5 -0
- package/lib/escape.d.ts.map +1 -0
- package/lib/escape.js +9 -0
- package/lib/escape.js.map +1 -0
- package/lib/guards.d.ts +106 -0
- package/lib/guards.d.ts.map +1 -0
- package/lib/guards.js +55 -0
- package/lib/guards.js.map +1 -0
- package/lib/index.d.ts +33 -0
- package/lib/index.d.ts.map +1 -0
- package/lib/index.js +32 -0
- package/lib/index.js.map +1 -0
- package/lib/index.test.d.ts +2 -0
- package/lib/index.test.d.ts.map +1 -0
- package/lib/index.test.js +130 -0
- package/lib/index.test.js.map +1 -0
- package/lib/inline.d.ts +79 -0
- package/lib/inline.d.ts.map +1 -0
- package/lib/inline.js +120 -0
- package/lib/inline.js.map +1 -0
- package/lib/message.d.ts +26 -0
- package/lib/message.d.ts.map +1 -0
- package/lib/message.js +31 -0
- package/lib/message.js.map +1 -0
- package/lib/plaintext.d.ts +16 -0
- package/lib/plaintext.d.ts.map +1 -0
- package/lib/plaintext.js +104 -0
- package/lib/plaintext.js.map +1 -0
- package/lib/send.d.ts +27 -0
- package/lib/send.d.ts.map +1 -0
- package/lib/send.js +52 -0
- package/lib/send.js.map +1 -0
- package/package.json +51 -0
- package/src/blocks.ts +250 -0
- package/src/draft.ts +116 -0
- package/src/escape.ts +9 -0
- package/src/guards.ts +169 -0
- package/src/index.test.ts +201 -0
- package/src/index.ts +199 -0
- package/src/inline.ts +152 -0
- package/src/message.ts +45 -0
- package/src/plaintext.ts +143 -0
- package/src/send.ts +81 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 neverlane
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
# @yaebal/rich
|
|
2
|
+
|
|
3
|
+
`sendRichMessage` / `sendRichMessageDraft` — telegram's block-tree message format. a typed builder for the extended html dialect, a draft/streaming session that owns the 30s ttl, and full read-side coverage (type guards + plain-text flattening) of everything telegram can hand back on `message.rich_message`.
|
|
4
|
+
|
|
5
|
+
unlike classic `parse_mode`/entities (see [`@yaebal/fmt`](https://www.npmjs.com/package/@yaebal/fmt)), a rich message isn't a flat `{ text, entities }` pair — it's a document: paragraphs, headings, tables, lists, collages, slideshows, block quotes, a collapsible `<details>`, even a `<tg-thinking>` placeholder for streaming an in-progress answer. you write extended html (or markdown), telegram parses it server-side into a `RichMessage.blocks` tree, and that same tree is what you read back.
|
|
6
|
+
|
|
7
|
+
## install
|
|
8
|
+
|
|
9
|
+
```sh
|
|
10
|
+
pnpm add @yaebal/rich
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## sending
|
|
14
|
+
|
|
15
|
+
```ts
|
|
16
|
+
import { document, heading, paragraph, bold, link, sendRichMessage } from "@yaebal/rich";
|
|
17
|
+
|
|
18
|
+
const input = document([
|
|
19
|
+
heading(1, "release notes"),
|
|
20
|
+
paragraph("yaebal ", bold("0.1"), " is out — see ", link("https://yaeb.al", "the docs"), "."),
|
|
21
|
+
]);
|
|
22
|
+
|
|
23
|
+
await sendRichMessage(ctx.api, ctx.chat.id, input);
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
or install the plugin for `ctx.send`-flavored ergonomics:
|
|
27
|
+
|
|
28
|
+
```ts
|
|
29
|
+
import { Bot } from "@yaebal/core";
|
|
30
|
+
import { rich, document, paragraph } from "@yaebal/rich";
|
|
31
|
+
|
|
32
|
+
const bot = new Bot(token)
|
|
33
|
+
.install(rich())
|
|
34
|
+
.command("hi", (ctx) => ctx.sendRichMessage(document([paragraph("hello!")])));
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## streaming a draft
|
|
38
|
+
|
|
39
|
+
`sendRichMessageDraft` streams a partial answer to a private chat — but the draft is **ephemeral**: telegram drops it 30 seconds after the last push, and it never becomes a real message on its own. `RichMessageDraft` owns that lifecycle: it re-pushes the latest draft on a timer so a slow generator doesn't lose it, and it refuses to push after you close it.
|
|
40
|
+
|
|
41
|
+
```ts
|
|
42
|
+
import { thinking, document, paragraph, sendRichMessage } from "@yaebal/rich";
|
|
43
|
+
|
|
44
|
+
bot.command("ask", async (ctx) => {
|
|
45
|
+
const draft = ctx.richMessageDraft(1); // draft_id, non-zero, per-message
|
|
46
|
+
|
|
47
|
+
await draft.push(thinking("thinking…")); // draft-only block, see below
|
|
48
|
+
|
|
49
|
+
let text = "";
|
|
50
|
+
for await (const chunk of streamAnswer(ctx.text)) {
|
|
51
|
+
text += chunk;
|
|
52
|
+
await draft.push(document([paragraph(text)]));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// required: a draft never persists on its own.
|
|
56
|
+
await draft.commit(document([paragraph(text)]));
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
call `draft.cancel()` instead of `commit()` to abandon a draft without persisting anything (it expires within 30s regardless).
|
|
61
|
+
|
|
62
|
+
## reading
|
|
63
|
+
|
|
64
|
+
```ts
|
|
65
|
+
import { richMessageToPlainText, isTable, isPhoto } from "@yaebal/rich";
|
|
66
|
+
|
|
67
|
+
bot.on("message:rich_message", (ctx) => {
|
|
68
|
+
const plain = richMessageToPlainText(ctx.message.rich_message);
|
|
69
|
+
const tables = ctx.message.rich_message.blocks.filter(isTable);
|
|
70
|
+
});
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
`richMessageToPlainText` / `richBlockToPlainText` / `richTextToPlainText` flatten the whole tree (or one node) to plain characters — useful for search indices, logs, or notification previews. every `RichBlock`/`RichText` variant has a matching `isX` type guard (`isParagraph`, `isTable`, `isCustomEmoji`, `isDateTime`, …).
|
|
74
|
+
|
|
75
|
+
## coverage
|
|
76
|
+
|
|
77
|
+
every one of telegram's ~50 `Rich*` types is covered on both sides:
|
|
78
|
+
|
|
79
|
+
- **write** — a builder function (or documented auto-detection) for every block and inline mark.
|
|
80
|
+
- **read** — an `isX` type guard and a plain-text flattening branch for every block and inline mark.
|
|
81
|
+
|
|
82
|
+
most tags are **confirmed** directly from telegram's schema (`<p>`, `<h1>`–`<h6>`, `<pre><code>`, `<hr/>`, `<footer>`, `<blockquote>`, `<aside>` for pull-quotes, `<details>`/`<summary>`, `<table>`, `<tg-collage>`, `<tg-slideshow>`, `<tg-map>`, `<tg-math-block>`, `<tg-thinking>`, `<img>`/`<video>`/`<audio>`, `<a name>`/`<a href="#…">`, `<cite>`, and the classic `<b>`/`<i>`/`<u>`/`<s>`/`<code>`/`<tg-spoiler>`/`<tg-emoji>`/`tg://user?id=…` set). `url`/`email_address`/`phone_number`/`bank_card_number`/`@mention`/`#hashtag`/`$cashtag`/`/bot_command` need **no explicit tag at all** — telegram auto-detects them from plain text unless you pass `skipEntityDetection: true`.
|
|
83
|
+
|
|
84
|
+
a handful of inline/block features have **no documented tag** in the schema at all (`marked`, `subscript`, `superscript`, `date_time`, inline `mathematical_expression`, `reference`/`reference_link`, table `is_bordered`/`is_striped`). those are implemented as a best-effort guess (standard html5 tags where one exists, a `tg-*`-style name otherwise) and flagged in their doc comments in `inline.ts`/`blocks.ts` — verify against the live "rich message formatting options" docs before relying on the exact spelling in production.
|
|
85
|
+
|
|
86
|
+
`sendRichMessage` has no `attach://`/multipart upload path (unlike `sendPhoto`) — media blocks (`image`/`video`/`audio`) take a hosted url, not a local file.
|
|
87
|
+
|
|
88
|
+
## api
|
|
89
|
+
|
|
90
|
+
| export | what |
|
|
91
|
+
|:-------------------------------------------------------------------------------------------------------|:--------------------------------------------------------------------------|
|
|
92
|
+
| `html` | tagged template for the extended-html dialect, auto-escaped interpolation |
|
|
93
|
+
| `bold`/`italic`/`underline`/`strikethrough`/`spoiler`/`code`/`marked`/`subscript`/`superscript` | inline marks |
|
|
94
|
+
| `link`/`textMention`/`anchor`/`anchorLink`/`customEmoji`/`dateTime`/`math`/`reference`/`referenceLink` | inline nodes with data |
|
|
95
|
+
| `paragraph`/`heading`/`preformatted`/`footer`/`divider`/`mathBlock`/`anchorBlock` | simple blocks |
|
|
96
|
+
| `blockquote`/`pullquote`/`details`/`list`/`item`/`table`/`cell` | structural blocks |
|
|
97
|
+
| `collage`/`slideshow`/`map`/`image`/`video`/`audio`/`thinking` | media & draft-only blocks |
|
|
98
|
+
| `document`/`markdown` | assemble blocks into an `InputRichMessage` |
|
|
99
|
+
| `sendRichMessage`/`sendRichMessageDraft` | standalone send functions, no plugin required |
|
|
100
|
+
| `rich()` | plugin: adds `ctx.sendRichMessage`/`ctx.richMessageDraft` |
|
|
101
|
+
| `RichMessageDraft` | the draft/streaming session class |
|
|
102
|
+
| `isParagraph`, `isTable`, `isCustomEmoji`, … | one type guard per `RichBlock`/`RichText` variant |
|
|
103
|
+
| `richTextToPlainText`/`richBlockToPlainText`/`richMessageToPlainText` | flatten to plain text |
|
|
104
|
+
|
|
105
|
+
plus the full generated type surface (`RichMessage`, `RichBlock`, `RichText`, and every `RichBlock*`/`RichText*` interface) re-exported from `@yaebal/types` for convenience.
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
part of [**yaebal**](https://github.com/neverlane/yaebal) — a type-safe, runtime-agnostic Telegram Bot API framework. MIT.
|
package/lib/blocks.d.ts
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import { type Insertable, type RichNode } from "./inline.js";
|
|
2
|
+
/** a caption + optional credit for a media/collage/slideshow/map block (`RichBlockCaption`). */
|
|
3
|
+
export interface Caption {
|
|
4
|
+
caption?: Insertable;
|
|
5
|
+
/** `RichBlockCaption.credit`, confirmed `<cite>`. */
|
|
6
|
+
credit?: Insertable;
|
|
7
|
+
}
|
|
8
|
+
/** `RichBlockParagraph`, `<p>`. */
|
|
9
|
+
export declare function paragraph(...children: Insertable[]): RichNode;
|
|
10
|
+
/** `RichBlockSectionHeading`, `<h1>`–`<h6>`. `level` is telegram's `size` (1 largest, 6 smallest). */
|
|
11
|
+
export declare function heading(level: 1 | 2 | 3 | 4 | 5 | 6, ...children: Insertable[]): RichNode;
|
|
12
|
+
/** `RichBlockPreformatted`, nested `<pre><code>`, matching classic parse_mode html. */
|
|
13
|
+
export declare function preformatted(text: string, language?: string): RichNode;
|
|
14
|
+
/** `RichBlockFooter`, `<footer>`. */
|
|
15
|
+
export declare function footer(...children: Insertable[]): RichNode;
|
|
16
|
+
/** `RichBlockDivider`, `<hr/>`. */
|
|
17
|
+
export declare function divider(): RichNode;
|
|
18
|
+
/** `RichBlockMathematicalExpression`, confirmed `<tg-math-block>` (LaTeX). */
|
|
19
|
+
export declare function mathBlock(expression: string): RichNode;
|
|
20
|
+
/** `RichBlockAnchor`, `<a name="…">` at block level. */
|
|
21
|
+
export declare function anchorBlock(name: string): RichNode;
|
|
22
|
+
/** `RichBlockBlockQuotation`, `<blockquote>`, with an optional `<cite>` credit. */
|
|
23
|
+
export declare function blockquote(children: Insertable[], credit?: Insertable): RichNode;
|
|
24
|
+
/** `RichBlockPullQuotation`, loosely `<aside>` per the schema, with an optional `<cite>` credit. */
|
|
25
|
+
export declare function pullquote(text: Insertable, credit?: Insertable): RichNode;
|
|
26
|
+
/** `RichBlockCollage`, confirmed custom tag `<tg-collage>`. */
|
|
27
|
+
export declare function collage(blocks: Insertable[], caption?: Caption): RichNode;
|
|
28
|
+
/** `RichBlockSlideshow`, confirmed custom tag `<tg-slideshow>`. */
|
|
29
|
+
export declare function slideshow(blocks: Insertable[], caption?: Caption): RichNode;
|
|
30
|
+
export interface TableCellOptions {
|
|
31
|
+
header?: boolean;
|
|
32
|
+
colspan?: number;
|
|
33
|
+
rowspan?: number;
|
|
34
|
+
/** default `"left"`, matching `RichBlockTableCell.align`. */
|
|
35
|
+
align?: "left" | "center" | "right";
|
|
36
|
+
/** default `"top"`, matching `RichBlockTableCell.valign`. */
|
|
37
|
+
valign?: "top" | "middle" | "bottom";
|
|
38
|
+
}
|
|
39
|
+
/** one `<td>`/`<th>` cell; `content` omitted ⇒ an invisible cell (per `RichBlockTableCell`). */
|
|
40
|
+
export declare function cell(content?: Insertable, options?: TableCellOptions): RichNode;
|
|
41
|
+
export interface TableOptions {
|
|
42
|
+
/** best-effort: rendered as the `border` attribute. */
|
|
43
|
+
bordered?: boolean;
|
|
44
|
+
/** best-effort: no standard html equivalent, rendered as `data-striped`. */
|
|
45
|
+
striped?: boolean;
|
|
46
|
+
caption?: Insertable;
|
|
47
|
+
}
|
|
48
|
+
/** `RichBlockTable`, confirmed `<table>`. build rows with `cell()`. */
|
|
49
|
+
export declare function table(rows: Insertable[][], options?: TableOptions): RichNode;
|
|
50
|
+
export interface DetailsOptions {
|
|
51
|
+
/** `RichBlockDetails.is_open` — content visible by default. defaults to `false`. */
|
|
52
|
+
open?: boolean;
|
|
53
|
+
}
|
|
54
|
+
/** `RichBlockDetails`, confirmed `<details>`/`<summary>`. */
|
|
55
|
+
export declare function details(summary: Insertable, blocks: Insertable[], options?: DetailsOptions): RichNode;
|
|
56
|
+
export interface ListItemOptions {
|
|
57
|
+
/** an unchecked/checked checkbox prefix (`RichBlockListItem.has_checkbox`/`is_checked`). */
|
|
58
|
+
checkbox?: boolean;
|
|
59
|
+
checked?: boolean;
|
|
60
|
+
/** ordered-list numeric override (`RichBlockListItem.value`) — standard `<li value>`. */
|
|
61
|
+
value?: number;
|
|
62
|
+
/** ordered-list label style override (`RichBlockListItem.type`) — standard `<li type>`. */
|
|
63
|
+
type?: "a" | "A" | "i" | "I";
|
|
64
|
+
}
|
|
65
|
+
/** one `<li>` for `list()`. */
|
|
66
|
+
export declare function item(blocks: Insertable[], options?: ListItemOptions): RichNode;
|
|
67
|
+
export interface ListOptions {
|
|
68
|
+
/** `<ol>` instead of `<ul>`. defaults to `false`. */
|
|
69
|
+
ordered?: boolean;
|
|
70
|
+
}
|
|
71
|
+
/** `RichBlockList`, `<ul>`/`<ol>` of `item()`s. */
|
|
72
|
+
export declare function list(items: Insertable[], options?: ListOptions): RichNode;
|
|
73
|
+
export interface MapOptions {
|
|
74
|
+
/** 13–20, per `RichBlockMap.zoom`. */
|
|
75
|
+
zoom: number;
|
|
76
|
+
width: number;
|
|
77
|
+
height: number;
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* `RichBlockMap`, confirmed custom tag `<tg-map>` — attribute names/encoding are a
|
|
81
|
+
* best-effort guess (lat/long/zoom/width/height are telegram's own field names).
|
|
82
|
+
*/
|
|
83
|
+
export declare function map(location: {
|
|
84
|
+
latitude: number;
|
|
85
|
+
longitude: number;
|
|
86
|
+
}, options: MapOptions, caption?: Caption): RichNode;
|
|
87
|
+
export interface MediaOptions extends Caption {
|
|
88
|
+
/** cover the media with a spoiler animation until tapped. */
|
|
89
|
+
spoiler?: boolean;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* `RichBlockPhoto`, confirmed `<img>`. media is referenced by url — `sendRichMessage`
|
|
93
|
+
* has no multipart/`attach://` upload path (unlike `sendPhoto`), so there is no
|
|
94
|
+
* `MediaSource` overload here; host the file and pass its url.
|
|
95
|
+
*/
|
|
96
|
+
export declare function image(src: string, options?: MediaOptions): RichNode;
|
|
97
|
+
/**
|
|
98
|
+
* `RichBlockVideo` **and** `RichBlockAnimation` both map to `<video>` per the
|
|
99
|
+
* schema (telegram tells the two apart server-side, e.g. by looping/silence —
|
|
100
|
+
* not by tag). there is no separate `animation()` builder for that reason.
|
|
101
|
+
*/
|
|
102
|
+
export declare function video(src: string, options?: MediaOptions): RichNode;
|
|
103
|
+
/**
|
|
104
|
+
* `RichBlockAudio` **and** `RichBlockVoiceNote` both map to `<audio>` per the
|
|
105
|
+
* schema (again disambiguated server-side, not by tag) — there is no separate
|
|
106
|
+
* `voiceNote()` builder for that reason.
|
|
107
|
+
*/
|
|
108
|
+
export declare function audio(src: string, options?: MediaOptions): RichNode;
|
|
109
|
+
/**
|
|
110
|
+
* `RichBlockThinking`, confirmed custom tag `<tg-thinking>`. **draft-only** — the
|
|
111
|
+
* schema states it can be used in `sendRichMessageDraft` payloads but is never
|
|
112
|
+
* received back on a real message. see `RichMessageDraft` in draft.ts.
|
|
113
|
+
*/
|
|
114
|
+
export declare function thinking(...children: Insertable[]): RichNode;
|
|
115
|
+
//# sourceMappingURL=blocks.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.d.ts","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"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"}
|
package/lib/blocks.js
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { escapeAttr, escapeText } from "./escape.js";
|
|
2
|
+
import { toHtml } from "./inline.js";
|
|
3
|
+
function join(children) {
|
|
4
|
+
return children.map(toHtml).join("");
|
|
5
|
+
}
|
|
6
|
+
function figcaption({ caption, credit }) {
|
|
7
|
+
if (caption === undefined && credit === undefined)
|
|
8
|
+
return "";
|
|
9
|
+
const creditHtml = credit === undefined ? "" : `<cite>${toHtml(credit)}</cite>`;
|
|
10
|
+
return `<figcaption>${toHtml(caption)}${creditHtml}</figcaption>`;
|
|
11
|
+
}
|
|
12
|
+
// --- confirmed tags ---
|
|
13
|
+
/** `RichBlockParagraph`, `<p>`. */
|
|
14
|
+
export function paragraph(...children) {
|
|
15
|
+
return { html: `<p>${join(children)}</p>` };
|
|
16
|
+
}
|
|
17
|
+
/** `RichBlockSectionHeading`, `<h1>`–`<h6>`. `level` is telegram's `size` (1 largest, 6 smallest). */
|
|
18
|
+
export function heading(level, ...children) {
|
|
19
|
+
return { html: `<h${level}>${join(children)}</h${level}>` };
|
|
20
|
+
}
|
|
21
|
+
/** `RichBlockPreformatted`, nested `<pre><code>`, matching classic parse_mode html. */
|
|
22
|
+
export function preformatted(text, language) {
|
|
23
|
+
const cls = language ? ` class="language-${escapeAttr(language)}"` : "";
|
|
24
|
+
return { html: `<pre><code${cls}>${escapeText(text)}</code></pre>` };
|
|
25
|
+
}
|
|
26
|
+
/** `RichBlockFooter`, `<footer>`. */
|
|
27
|
+
export function footer(...children) {
|
|
28
|
+
return { html: `<footer>${join(children)}</footer>` };
|
|
29
|
+
}
|
|
30
|
+
/** `RichBlockDivider`, `<hr/>`. */
|
|
31
|
+
export function divider() {
|
|
32
|
+
return { html: "<hr/>" };
|
|
33
|
+
}
|
|
34
|
+
/** `RichBlockMathematicalExpression`, confirmed `<tg-math-block>` (LaTeX). */
|
|
35
|
+
export function mathBlock(expression) {
|
|
36
|
+
return { html: `<tg-math-block>${escapeText(expression)}</tg-math-block>` };
|
|
37
|
+
}
|
|
38
|
+
/** `RichBlockAnchor`, `<a name="…">` at block level. */
|
|
39
|
+
export function anchorBlock(name) {
|
|
40
|
+
return { html: `<a name="${escapeAttr(name)}"></a>` };
|
|
41
|
+
}
|
|
42
|
+
/** `RichBlockBlockQuotation`, `<blockquote>`, with an optional `<cite>` credit. */
|
|
43
|
+
export function blockquote(children, credit) {
|
|
44
|
+
const creditHtml = credit === undefined ? "" : `<cite>${toHtml(credit)}</cite>`;
|
|
45
|
+
return { html: `<blockquote>${join(children)}${creditHtml}</blockquote>` };
|
|
46
|
+
}
|
|
47
|
+
/** `RichBlockPullQuotation`, loosely `<aside>` per the schema, with an optional `<cite>` credit. */
|
|
48
|
+
export function pullquote(text, credit) {
|
|
49
|
+
const creditHtml = credit === undefined ? "" : `<cite>${toHtml(credit)}</cite>`;
|
|
50
|
+
return { html: `<aside>${toHtml(text)}${creditHtml}</aside>` };
|
|
51
|
+
}
|
|
52
|
+
/** `RichBlockCollage`, confirmed custom tag `<tg-collage>`. */
|
|
53
|
+
export function collage(blocks, caption = {}) {
|
|
54
|
+
return { html: `<tg-collage>${join(blocks)}${figcaption(caption)}</tg-collage>` };
|
|
55
|
+
}
|
|
56
|
+
/** `RichBlockSlideshow`, confirmed custom tag `<tg-slideshow>`. */
|
|
57
|
+
export function slideshow(blocks, caption = {}) {
|
|
58
|
+
return { html: `<tg-slideshow>${join(blocks)}${figcaption(caption)}</tg-slideshow>` };
|
|
59
|
+
}
|
|
60
|
+
/** one `<td>`/`<th>` cell; `content` omitted ⇒ an invisible cell (per `RichBlockTableCell`). */
|
|
61
|
+
export function cell(content, options = {}) {
|
|
62
|
+
const tag = options.header ? "th" : "td";
|
|
63
|
+
const attrs = (options.colspan ? ` colspan="${options.colspan}"` : "") +
|
|
64
|
+
(options.rowspan ? ` rowspan="${options.rowspan}"` : "") +
|
|
65
|
+
// `align`/`valign` are telegram's own field names for this cell — the values
|
|
66
|
+
// (left/center/right, top/middle/bottom) match the classic html attributes exactly.
|
|
67
|
+
(options.align ? ` align="${options.align}"` : "") +
|
|
68
|
+
(options.valign ? ` valign="${options.valign}"` : "");
|
|
69
|
+
return {
|
|
70
|
+
html: content === undefined
|
|
71
|
+
? `<${tag}${attrs}></${tag}>`
|
|
72
|
+
: `<${tag}${attrs}>${toHtml(content)}</${tag}>`,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** `RichBlockTable`, confirmed `<table>`. build rows with `cell()`. */
|
|
76
|
+
export function table(rows, options = {}) {
|
|
77
|
+
const attrs = (options.bordered ? " border" : "") + (options.striped ? " data-striped" : "");
|
|
78
|
+
const body = rows.map((row) => `<tr>${join(row)}</tr>`).join("");
|
|
79
|
+
const captionHtml = options.caption === undefined ? "" : `<caption>${toHtml(options.caption)}</caption>`;
|
|
80
|
+
return { html: `<table${attrs}>${captionHtml}${body}</table>` };
|
|
81
|
+
}
|
|
82
|
+
/** `RichBlockDetails`, confirmed `<details>`/`<summary>`. */
|
|
83
|
+
export function details(summary, blocks, options = {}) {
|
|
84
|
+
return {
|
|
85
|
+
html: `<details${options.open ? " open" : ""}><summary>${toHtml(summary)}</summary>${join(blocks)}</details>`,
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
/** one `<li>` for `list()`. */
|
|
89
|
+
export function item(blocks, options = {}) {
|
|
90
|
+
const attrs = (options.value !== undefined ? ` value="${options.value}"` : "") +
|
|
91
|
+
(options.type ? ` type="${options.type}"` : "");
|
|
92
|
+
const checkboxHtml = options.checkbox === undefined
|
|
93
|
+
? ""
|
|
94
|
+
: `<input type="checkbox"${options.checked ? " checked" : ""}/> `;
|
|
95
|
+
return { html: `<li${attrs}>${checkboxHtml}${join(blocks)}</li>` };
|
|
96
|
+
}
|
|
97
|
+
/** `RichBlockList`, `<ul>`/`<ol>` of `item()`s. */
|
|
98
|
+
export function list(items, options = {}) {
|
|
99
|
+
const tag = options.ordered ? "ol" : "ul";
|
|
100
|
+
return { html: `<${tag}>${join(items)}</${tag}>` };
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* `RichBlockMap`, confirmed custom tag `<tg-map>` — attribute names/encoding are a
|
|
104
|
+
* best-effort guess (lat/long/zoom/width/height are telegram's own field names).
|
|
105
|
+
*/
|
|
106
|
+
export function map(location, options, caption = {}) {
|
|
107
|
+
const attrs = ` latitude="${location.latitude}" longitude="${location.longitude}"` +
|
|
108
|
+
` zoom="${options.zoom}" width="${options.width}" height="${options.height}"`;
|
|
109
|
+
return { html: `<tg-map${attrs}>${figcaption(caption)}</tg-map>` };
|
|
110
|
+
}
|
|
111
|
+
function figure(tag, src, options) {
|
|
112
|
+
const spoilerAttr = options.spoiler ? " data-media-spoiler" : "";
|
|
113
|
+
const media = `<${tag} src="${escapeAttr(src)}"${spoilerAttr}></${tag}>`;
|
|
114
|
+
const cap = figcaption(options);
|
|
115
|
+
return { html: cap ? `<figure>${media}${cap}</figure>` : media };
|
|
116
|
+
}
|
|
117
|
+
/**
|
|
118
|
+
* `RichBlockPhoto`, confirmed `<img>`. media is referenced by url — `sendRichMessage`
|
|
119
|
+
* has no multipart/`attach://` upload path (unlike `sendPhoto`), so there is no
|
|
120
|
+
* `MediaSource` overload here; host the file and pass its url.
|
|
121
|
+
*/
|
|
122
|
+
export function image(src, options = {}) {
|
|
123
|
+
return figure("img", src, options);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* `RichBlockVideo` **and** `RichBlockAnimation` both map to `<video>` per the
|
|
127
|
+
* schema (telegram tells the two apart server-side, e.g. by looping/silence —
|
|
128
|
+
* not by tag). there is no separate `animation()` builder for that reason.
|
|
129
|
+
*/
|
|
130
|
+
export function video(src, options = {}) {
|
|
131
|
+
return figure("video", src, options);
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* `RichBlockAudio` **and** `RichBlockVoiceNote` both map to `<audio>` per the
|
|
135
|
+
* schema (again disambiguated server-side, not by tag) — there is no separate
|
|
136
|
+
* `voiceNote()` builder for that reason.
|
|
137
|
+
*/
|
|
138
|
+
export function audio(src, options = {}) {
|
|
139
|
+
return figure("audio", src, options);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* `RichBlockThinking`, confirmed custom tag `<tg-thinking>`. **draft-only** — the
|
|
143
|
+
* schema states it can be used in `sendRichMessageDraft` payloads but is never
|
|
144
|
+
* received back on a real message. see `RichMessageDraft` in draft.ts.
|
|
145
|
+
*/
|
|
146
|
+
export function thinking(...children) {
|
|
147
|
+
return { html: `<tg-thinking>${join(children)}</tg-thinking>` };
|
|
148
|
+
}
|
|
149
|
+
//# sourceMappingURL=blocks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"blocks.js","sourceRoot":"","sources":["../src/blocks.ts"],"names":[],"mappings":"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"}
|
package/lib/draft.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import type { Api, Message } from "@yaebal/core";
|
|
2
|
+
import type { InputRichMessage } from "@yaebal/types";
|
|
3
|
+
export interface RichMessageDraftOptions {
|
|
4
|
+
/**
|
|
5
|
+
* how often to re-push the last draft to keep it alive, in ms. telegram drops
|
|
6
|
+
* a draft 30s after the last `sendRichMessageDraft` call for its `draft_id`
|
|
7
|
+
* ("ephemeral … temporary 30-second preview"); this must stay comfortably
|
|
8
|
+
* under that. defaults to 20_000.
|
|
9
|
+
*/
|
|
10
|
+
keepAliveMs?: number;
|
|
11
|
+
/** called when a background keep-alive push fails (e.g. network blip). */
|
|
12
|
+
onError?: (error: unknown) => void;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* a `sendRichMessageDraft` streaming session — the operationally hard part of the
|
|
16
|
+
* rich-message api. telegram's draft is ephemeral: it vanishes 30s after the last
|
|
17
|
+
* push, and it never turns into a real message on its own (per the schema:
|
|
18
|
+
* "once the output is finalized, you must call sendRichMessage with the complete
|
|
19
|
+
* message to persist it"). this class:
|
|
20
|
+
*
|
|
21
|
+
* - re-pushes the latest draft on a timer so a slow generator (e.g. an LLM
|
|
22
|
+
* stream) doesn't lose the draft between tokens;
|
|
23
|
+
* - refuses to push after `commit()`/`cancel()`, so a stray late token can't
|
|
24
|
+
* resurrect a closed draft;
|
|
25
|
+
* - requires an explicit `commit()` — there is no implicit "last push wins".
|
|
26
|
+
*
|
|
27
|
+
* @example
|
|
28
|
+
* const draft = ctx.richMessageDraft(1);
|
|
29
|
+
* await draft.push(thinking("…"));
|
|
30
|
+
* for await (const chunk of stream) await draft.push(document([paragraph(soFar)]));
|
|
31
|
+
* await draft.commit(document([paragraph(finalAnswer)]));
|
|
32
|
+
*/
|
|
33
|
+
export declare class RichMessageDraft {
|
|
34
|
+
#private;
|
|
35
|
+
constructor(api: Api, chatId: number, draftId: number, options?: RichMessageDraftOptions);
|
|
36
|
+
get closed(): boolean;
|
|
37
|
+
/** push a new partial draft; telegram animates the transition for a shared `draft_id`. */
|
|
38
|
+
push(input: InputRichMessage | string): Promise<void>;
|
|
39
|
+
/** finalize: persist the real message and stop the keep-alive. always call this or `cancel()`. */
|
|
40
|
+
commit(input: InputRichMessage | string, extra?: Record<string, unknown>): Promise<Message>;
|
|
41
|
+
/** abandon the draft without persisting anything — it expires within 30s on its own. */
|
|
42
|
+
cancel(): void;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=draft.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft.d.ts","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,GAAG,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AACjD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,eAAe,CAAC;AAEtD,MAAM,WAAW,uBAAuB;IACvC;;;;;OAKG;IACH,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,0EAA0E;IAC1E,OAAO,CAAC,EAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAI,CAAC;CACnC;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,qBAAa,gBAAgB;;gBAWhB,GAAG,EAAE,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,GAAE,uBAA4B;IAQ5F,IAAI,MAAM,IAAI,OAAO,CAEpB;IAED,0FAA0F;IACpF,IAAI,CAAC,KAAK,EAAE,gBAAgB,GAAG,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA8B3D,kGAAkG;IAC5F,MAAM,CACX,KAAK,EAAE,gBAAgB,GAAG,MAAM,EAChC,KAAK,GAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAM,GACjC,OAAO,CAAC,OAAO,CAAC;IAYnB,wFAAwF;IACxF,MAAM,IAAI,IAAI;CAUd"}
|
package/lib/draft.js
ADDED
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* a `sendRichMessageDraft` streaming session — the operationally hard part of the
|
|
3
|
+
* rich-message api. telegram's draft is ephemeral: it vanishes 30s after the last
|
|
4
|
+
* push, and it never turns into a real message on its own (per the schema:
|
|
5
|
+
* "once the output is finalized, you must call sendRichMessage with the complete
|
|
6
|
+
* message to persist it"). this class:
|
|
7
|
+
*
|
|
8
|
+
* - re-pushes the latest draft on a timer so a slow generator (e.g. an LLM
|
|
9
|
+
* stream) doesn't lose the draft between tokens;
|
|
10
|
+
* - refuses to push after `commit()`/`cancel()`, so a stray late token can't
|
|
11
|
+
* resurrect a closed draft;
|
|
12
|
+
* - requires an explicit `commit()` — there is no implicit "last push wins".
|
|
13
|
+
*
|
|
14
|
+
* @example
|
|
15
|
+
* const draft = ctx.richMessageDraft(1);
|
|
16
|
+
* await draft.push(thinking("…"));
|
|
17
|
+
* for await (const chunk of stream) await draft.push(document([paragraph(soFar)]));
|
|
18
|
+
* await draft.commit(document([paragraph(finalAnswer)]));
|
|
19
|
+
*/
|
|
20
|
+
export class RichMessageDraft {
|
|
21
|
+
#api;
|
|
22
|
+
#chatId;
|
|
23
|
+
#draftId;
|
|
24
|
+
#keepAliveMs;
|
|
25
|
+
#onError;
|
|
26
|
+
#timer;
|
|
27
|
+
#latest;
|
|
28
|
+
#closed = false;
|
|
29
|
+
constructor(api, chatId, draftId, options = {}) {
|
|
30
|
+
this.#api = api;
|
|
31
|
+
this.#chatId = chatId;
|
|
32
|
+
this.#draftId = draftId;
|
|
33
|
+
this.#keepAliveMs = options.keepAliveMs ?? 20_000;
|
|
34
|
+
this.#onError = options.onError;
|
|
35
|
+
}
|
|
36
|
+
get closed() {
|
|
37
|
+
return this.#closed;
|
|
38
|
+
}
|
|
39
|
+
/** push a new partial draft; telegram animates the transition for a shared `draft_id`. */
|
|
40
|
+
async push(input) {
|
|
41
|
+
if (this.#closed)
|
|
42
|
+
throw new Error("RichMessageDraft: push() after commit()/cancel()");
|
|
43
|
+
const resolved = typeof input === "string" ? { html: input } : input;
|
|
44
|
+
this.#latest = resolved;
|
|
45
|
+
await this.#push(resolved);
|
|
46
|
+
this.#arm();
|
|
47
|
+
}
|
|
48
|
+
#push(input) {
|
|
49
|
+
return this.#api.call("sendRichMessageDraft", {
|
|
50
|
+
chat_id: this.#chatId,
|
|
51
|
+
draft_id: this.#draftId,
|
|
52
|
+
rich_message: input,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
#arm() {
|
|
56
|
+
clearInterval(this.#timer);
|
|
57
|
+
this.#timer = setInterval(() => {
|
|
58
|
+
if (!this.#latest)
|
|
59
|
+
return;
|
|
60
|
+
this.#push(this.#latest).catch((error) => this.#onError?.(error));
|
|
61
|
+
}, this.#keepAliveMs);
|
|
62
|
+
this.#timer.unref?.();
|
|
63
|
+
}
|
|
64
|
+
/** finalize: persist the real message and stop the keep-alive. always call this or `cancel()`. */
|
|
65
|
+
async commit(input, extra = {}) {
|
|
66
|
+
this.#stop();
|
|
67
|
+
const resolved = typeof input === "string" ? { html: input } : input;
|
|
68
|
+
return this.#api.call("sendRichMessage", {
|
|
69
|
+
chat_id: this.#chatId,
|
|
70
|
+
rich_message: resolved,
|
|
71
|
+
...extra,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
/** abandon the draft without persisting anything — it expires within 30s on its own. */
|
|
75
|
+
cancel() {
|
|
76
|
+
this.#stop();
|
|
77
|
+
}
|
|
78
|
+
#stop() {
|
|
79
|
+
if (this.#closed)
|
|
80
|
+
return;
|
|
81
|
+
this.#closed = true;
|
|
82
|
+
clearInterval(this.#timer);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
//# sourceMappingURL=draft.js.map
|
package/lib/draft.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"draft.js","sourceRoot":"","sources":["../src/draft.ts"],"names":[],"mappings":"AAeA;;;;;;;;;;;;;;;;;;GAkBG;AACH,MAAM,OAAO,gBAAgB;IACnB,IAAI,CAAM;IACV,OAAO,CAAS;IAChB,QAAQ,CAAS;IACjB,YAAY,CAAS;IACrB,QAAQ,CAA4B;IAE7C,MAAM,CAAkC;IACxC,OAAO,CAA+B;IACtC,OAAO,GAAG,KAAK,CAAC;IAEhB,YAAY,GAAQ,EAAE,MAAc,EAAE,OAAe,EAAE,UAAmC,EAAE;QAC3F,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC;QAChB,IAAI,CAAC,OAAO,GAAG,MAAM,CAAC;QACtB,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC;QACxB,IAAI,CAAC,YAAY,GAAG,OAAO,CAAC,WAAW,IAAI,MAAM,CAAC;QAClD,IAAI,CAAC,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC;IACjC,CAAC;IAED,IAAI,MAAM;QACT,OAAO,IAAI,CAAC,OAAO,CAAC;IACrB,CAAC;IAED,0FAA0F;IAC1F,KAAK,CAAC,IAAI,CAAC,KAAgC;QAC1C,IAAI,IAAI,CAAC,OAAO;YAAE,MAAM,IAAI,KAAK,CAAC,kDAAkD,CAAC,CAAC;QAEtF,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAErE,IAAI,CAAC,OAAO,GAAG,QAAQ,CAAC;QACxB,MAAM,IAAI,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;QAC3B,IAAI,CAAC,IAAI,EAAE,CAAC;IACb,CAAC;IAED,KAAK,CAAC,KAAuB;QAC5B,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAU,sBAAsB,EAAE;YACtD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,QAAQ,EAAE,IAAI,CAAC,QAAQ;YACvB,YAAY,EAAE,KAAK;SACnB,CAAC,CAAC;IACJ,CAAC;IAED,IAAI;QACH,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;QAE3B,IAAI,CAAC,MAAM,GAAG,WAAW,CAAC,GAAG,EAAE;YAC9B,IAAI,CAAC,IAAI,CAAC,OAAO;gBAAE,OAAO;YAE1B,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,KAAK,CAAC,CAAC,KAAc,EAAE,EAAE,CAAC,IAAI,CAAC,QAAQ,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;QAC5E,CAAC,EAAE,IAAI,CAAC,YAAY,CAAC,CAAC;QAEtB,IAAI,CAAC,MAAM,CAAC,KAAK,EAAE,EAAE,CAAC;IACvB,CAAC;IAED,kGAAkG;IAClG,KAAK,CAAC,MAAM,CACX,KAAgC,EAChC,QAAiC,EAAE;QAEnC,IAAI,CAAC,KAAK,EAAE,CAAC;QAEb,MAAM,QAAQ,GAAG,OAAO,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,KAAK,CAAC;QAErE,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAU,iBAAiB,EAAE;YACjD,OAAO,EAAE,IAAI,CAAC,OAAO;YACrB,YAAY,EAAE,QAAQ;YACtB,GAAG,KAAK;SACR,CAAC,CAAC;IACJ,CAAC;IAED,wFAAwF;IACxF,MAAM;QACL,IAAI,CAAC,KAAK,EAAE,CAAC;IACd,CAAC;IAED,KAAK;QACJ,IAAI,IAAI,CAAC,OAAO;YAAE,OAAO;QAEzB,IAAI,CAAC,OAAO,GAAG,IAAI,CAAC;QACpB,aAAa,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC5B,CAAC;CACD"}
|
package/lib/escape.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
/** escape text so it can never be re-parsed as a tag or entity. */
|
|
2
|
+
export declare function escapeText(value: string): string;
|
|
3
|
+
/** escape a double-quoted attribute value. */
|
|
4
|
+
export declare function escapeAttr(value: string): string;
|
|
5
|
+
//# sourceMappingURL=escape.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape.d.ts","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD;AAED,8CAA8C;AAC9C,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAEhD"}
|
package/lib/escape.js
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/** escape text so it can never be re-parsed as a tag or entity. */
|
|
2
|
+
export function escapeText(value) {
|
|
3
|
+
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
|
4
|
+
}
|
|
5
|
+
/** escape a double-quoted attribute value. */
|
|
6
|
+
export function escapeAttr(value) {
|
|
7
|
+
return escapeText(value).replace(/"/g, """);
|
|
8
|
+
}
|
|
9
|
+
//# sourceMappingURL=escape.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"escape.js","sourceRoot":"","sources":["../src/escape.ts"],"names":[],"mappings":"AAAA,mEAAmE;AACnE,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,KAAK,CAAC,OAAO,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC;AACjF,CAAC;AAED,8CAA8C;AAC9C,MAAM,UAAU,UAAU,CAAC,KAAa;IACvC,OAAO,UAAU,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC;AAClD,CAAC"}
|