@yaebal/rich 0.0.1 → 0.0.3
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 +109 -35
- package/lib/blocks.d.ts +93 -32
- package/lib/blocks.d.ts.map +1 -1
- package/lib/blocks.js +227 -71
- package/lib/blocks.js.map +1 -1
- package/lib/blocks.test.d.ts +2 -0
- package/lib/blocks.test.d.ts.map +1 -0
- package/lib/blocks.test.js +142 -0
- package/lib/blocks.test.js.map +1 -0
- package/lib/document.d.ts +28 -0
- package/lib/document.d.ts.map +1 -0
- package/lib/document.js +46 -0
- package/lib/document.js.map +1 -0
- package/lib/draft.d.ts +46 -9
- package/lib/draft.d.ts.map +1 -1
- package/lib/draft.js +115 -19
- package/lib/draft.js.map +1 -1
- package/lib/draft.test.d.ts +2 -0
- package/lib/draft.test.d.ts.map +1 -0
- package/lib/draft.test.js +122 -0
- package/lib/draft.test.js.map +1 -0
- package/lib/escape.d.ts +8 -0
- package/lib/escape.d.ts.map +1 -1
- package/lib/escape.js +17 -0
- package/lib/escape.js.map +1 -1
- package/lib/escape.test.d.ts +2 -0
- package/lib/escape.test.d.ts.map +1 -0
- package/lib/escape.test.js +28 -0
- package/lib/escape.test.js.map +1 -0
- package/lib/guards.test.d.ts +2 -0
- package/lib/guards.test.d.ts.map +1 -0
- package/lib/guards.test.js +85 -0
- package/lib/guards.test.js.map +1 -0
- package/lib/index.d.ts +18 -9
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +17 -8
- package/lib/index.js.map +1 -1
- package/lib/index.test.js +73 -103
- package/lib/index.test.js.map +1 -1
- package/lib/inline.d.ts +48 -59
- package/lib/inline.d.ts.map +1 -1
- package/lib/inline.js +87 -82
- package/lib/inline.js.map +1 -1
- package/lib/inline.test.d.ts +2 -0
- package/lib/inline.test.d.ts.map +1 -0
- package/lib/inline.test.js +84 -0
- package/lib/inline.test.js.map +1 -0
- package/lib/node.d.ts +25 -0
- package/lib/node.d.ts.map +1 -0
- package/lib/node.js +21 -0
- package/lib/node.js.map +1 -0
- package/lib/plaintext.test.d.ts +2 -0
- package/lib/plaintext.test.d.ts.map +1 -0
- package/lib/plaintext.test.js +100 -0
- package/lib/plaintext.test.js.map +1 -0
- package/lib/render.d.ts +17 -0
- package/lib/render.d.ts.map +1 -0
- package/lib/render.js +30 -0
- package/lib/render.js.map +1 -0
- package/lib/send.d.ts +6 -3
- package/lib/send.d.ts.map +1 -1
- package/lib/send.js +12 -3
- package/lib/send.js.map +1 -1
- package/lib/template.d.ts +46 -0
- package/lib/template.d.ts.map +1 -0
- package/lib/template.js +82 -0
- package/lib/template.js.map +1 -0
- package/lib/template.test.d.ts +2 -0
- package/lib/template.test.d.ts.map +1 -0
- package/lib/template.test.js +78 -0
- package/lib/template.test.js.map +1 -0
- package/package.json +5 -4
- package/src/blocks.test.ts +233 -0
- package/src/blocks.ts +304 -86
- package/src/document.ts +51 -0
- package/src/draft.test.ts +167 -0
- package/src/draft.ts +148 -21
- package/src/escape.test.ts +38 -0
- package/src/escape.ts +20 -0
- package/src/guards.test.ts +138 -0
- package/src/index.test.ts +79 -140
- package/src/index.ts +26 -11
- package/src/inline.test.ts +125 -0
- package/src/inline.ts +131 -97
- package/src/node.ts +43 -0
- package/src/plaintext.test.ts +141 -0
- package/src/render.ts +54 -0
- package/src/send.ts +17 -10
- package/src/template.test.ts +100 -0
- package/src/template.ts +115 -0
- package/lib/message.d.ts +0 -26
- package/lib/message.d.ts.map +0 -1
- package/lib/message.js +0 -31
- package/lib/message.js.map +0 -1
- package/src/message.ts +0 -45
package/src/index.test.ts
CHANGED
|
@@ -1,122 +1,64 @@
|
|
|
1
1
|
import assert from "node:assert/strict";
|
|
2
2
|
import test from "node:test";
|
|
3
|
-
import {
|
|
4
|
-
import
|
|
3
|
+
import { Composer, Context } from "@yaebal/core";
|
|
4
|
+
import { mockApi } from "@yaebal/test";
|
|
5
5
|
import {
|
|
6
6
|
bold,
|
|
7
|
-
cell,
|
|
8
|
-
details,
|
|
9
7
|
document,
|
|
10
|
-
|
|
11
|
-
html,
|
|
12
|
-
image,
|
|
13
|
-
isPhoto,
|
|
14
|
-
isTable,
|
|
15
|
-
item,
|
|
16
|
-
link,
|
|
17
|
-
list,
|
|
8
|
+
md,
|
|
18
9
|
paragraph,
|
|
19
10
|
RichMessageDraft,
|
|
20
11
|
rich,
|
|
21
|
-
richMessageToPlainText,
|
|
22
12
|
sendRichMessage,
|
|
23
13
|
sendRichMessageDraft,
|
|
24
|
-
table,
|
|
25
|
-
thinking,
|
|
26
14
|
} from "./index.js";
|
|
27
15
|
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
// integration-level coverage for the parts that tie the package together: the raw
|
|
17
|
+
// send/draft functions, and the `rich()` plugin's ctx decoration. per-builder output
|
|
18
|
+
// (blocks.test.ts/inline.test.ts/markdown.test.ts), guards (guards.test.ts),
|
|
19
|
+
// plaintext flattening (plaintext.test.ts), and RichMessageDraft's rewrite/write/send
|
|
20
|
+
// state machine (draft.test.ts) all have their own dedicated test files.
|
|
30
21
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
call: async (method: string, params?: Record<string, unknown>) => {
|
|
34
|
-
calls.push([method, params]);
|
|
35
|
-
return {} as never;
|
|
36
|
-
},
|
|
37
|
-
} as unknown as Api & { calls: [string, Record<string, unknown> | undefined][] };
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
test("html template escapes interpolations and splices RichNode subs", () => {
|
|
41
|
-
const node = html`<p>hi ${"<b>hax</b>"} — ${bold("safe")}</p>`;
|
|
42
|
-
|
|
43
|
-
assert.equal(node.html, "<p>hi <b>hax</b> — <b>safe</b></p>");
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
test("inline builders emit the documented tags", () => {
|
|
47
|
-
assert.equal(bold("x").html, "<b>x</b>");
|
|
48
|
-
assert.equal(link("https://yaeb.al", "docs").html, '<a href="https://yaeb.al">docs</a>');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
test("block builders compose into a document", () => {
|
|
52
|
-
const input = document([
|
|
53
|
-
heading(1, "title"),
|
|
54
|
-
paragraph("hello ", bold("world")),
|
|
55
|
-
list([item(["a"]), item(["b"], { checkbox: true, checked: true })]),
|
|
56
|
-
details("more", [paragraph("hidden")], { open: false }),
|
|
57
|
-
]);
|
|
58
|
-
|
|
59
|
-
assert.equal(
|
|
60
|
-
input.html,
|
|
61
|
-
"<h1>title</h1>" +
|
|
62
|
-
"<p>hello <b>world</b></p>" +
|
|
63
|
-
'<ul><li>a</li><li><input type="checkbox" checked/> b</li></ul>' +
|
|
64
|
-
"<details><summary>more</summary><p>hidden</p></details>",
|
|
65
|
-
);
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
test("table() and cell() render alignment attributes telegram's schema documents", () => {
|
|
69
|
-
const node = table([
|
|
70
|
-
[cell("a", { header: true, align: "center" }), cell("b", { valign: "middle" })],
|
|
71
|
-
]);
|
|
72
|
-
|
|
73
|
-
assert.equal(
|
|
74
|
-
node.html,
|
|
75
|
-
'<table><tr><th align="center">a</th><td valign="middle">b</td></tr></table>',
|
|
76
|
-
);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
test("image() wraps caption/credit in figure/figcaption/cite", () => {
|
|
80
|
-
const node = image("https://example.com/x.jpg", { caption: "a cat", credit: "photographer" });
|
|
22
|
+
test("sendRichMessage posts chat_id + rich_message to the raw api", async () => {
|
|
23
|
+
const { api, calls } = mockApi();
|
|
81
24
|
|
|
82
|
-
|
|
83
|
-
node.html,
|
|
84
|
-
'<figure><img src="https://example.com/x.jpg"></img><figcaption>a cat<cite>photographer</cite></figcaption></figure>',
|
|
85
|
-
);
|
|
86
|
-
});
|
|
25
|
+
await sendRichMessage(api, 42, document([paragraph("hi")]), { reply_markup: { x: 1 } });
|
|
87
26
|
|
|
88
|
-
|
|
89
|
-
assert.
|
|
27
|
+
assert.equal(calls[0]?.method, "sendRichMessage");
|
|
28
|
+
assert.deepEqual(calls[0]?.params, {
|
|
29
|
+
chat_id: 42,
|
|
30
|
+
rich_message: { html: "<p>hi</p>" },
|
|
31
|
+
reply_markup: { x: 1 },
|
|
32
|
+
});
|
|
90
33
|
});
|
|
91
34
|
|
|
92
|
-
test("sendRichMessage
|
|
93
|
-
const api = mockApi();
|
|
35
|
+
test("sendRichMessage unwraps a markdown RichDocument with its fluent flags", async () => {
|
|
36
|
+
const { api, calls } = mockApi();
|
|
94
37
|
|
|
95
|
-
await sendRichMessage(api, 42,
|
|
38
|
+
await sendRichMessage(api, 42, md`# hi ${bold("there")}`.rtl());
|
|
96
39
|
|
|
97
|
-
assert.
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
]);
|
|
40
|
+
assert.equal(calls[0]?.method, "sendRichMessage");
|
|
41
|
+
assert.deepEqual(calls[0]?.params, {
|
|
42
|
+
chat_id: 42,
|
|
43
|
+
rich_message: { markdown: "# hi **there**", is_rtl: true },
|
|
44
|
+
});
|
|
103
45
|
});
|
|
104
46
|
|
|
105
47
|
test("sendRichMessageDraft posts draft_id alongside chat_id + rich_message", async () => {
|
|
106
|
-
const api = mockApi();
|
|
48
|
+
const { api, calls } = mockApi();
|
|
107
49
|
|
|
108
50
|
await sendRichMessageDraft(api, 42, 7, "<tg-thinking>…</tg-thinking>");
|
|
109
51
|
|
|
110
|
-
assert.
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
52
|
+
assert.equal(calls[0]?.method, "sendRichMessageDraft");
|
|
53
|
+
assert.deepEqual(calls[0]?.params, {
|
|
54
|
+
chat_id: 42,
|
|
55
|
+
draft_id: 7,
|
|
56
|
+
rich_message: { html: "<tg-thinking>…</tg-thinking>" },
|
|
57
|
+
});
|
|
116
58
|
});
|
|
117
59
|
|
|
118
|
-
test("rich() plugin decorates ctx.sendRichMessage bound to the current chat", async () => {
|
|
119
|
-
const api = mockApi();
|
|
60
|
+
test("rich() plugin decorates ctx.sendRichMessage/ctx.richMessageDraft bound to the current chat", async () => {
|
|
61
|
+
const { api, calls } = mockApi();
|
|
120
62
|
const composer = new Composer().install(rich());
|
|
121
63
|
|
|
122
64
|
const ctx = new Context({
|
|
@@ -136,66 +78,63 @@ test("rich() plugin decorates ctx.sendRichMessage bound to the current chat", as
|
|
|
136
78
|
};
|
|
137
79
|
|
|
138
80
|
await decorated.sendRichMessage(document([paragraph("hi")]));
|
|
139
|
-
assert.
|
|
140
|
-
|
|
141
|
-
{ chat_id: 5, rich_message: { html: "<p>hi</p>" } },
|
|
142
|
-
]);
|
|
81
|
+
assert.equal(calls[0]?.method, "sendRichMessage");
|
|
82
|
+
assert.deepEqual(calls[0]?.params, { chat_id: 5, rich_message: { html: "<p>hi</p>" } });
|
|
143
83
|
|
|
144
84
|
const draft = decorated.richMessageDraft(1);
|
|
145
85
|
assert.ok(draft instanceof RichMessageDraft);
|
|
146
86
|
});
|
|
147
87
|
|
|
148
|
-
test("
|
|
149
|
-
const api = mockApi();
|
|
150
|
-
const
|
|
151
|
-
|
|
152
|
-
await draft.push(thinking("…"));
|
|
153
|
-
assert.equal(api.calls[0]?.[0], "sendRichMessageDraft");
|
|
154
|
-
|
|
155
|
-
await draft.commit(document([paragraph("done")]));
|
|
156
|
-
assert.equal(api.calls[1]?.[0], "sendRichMessage");
|
|
157
|
-
assert.equal(draft.closed, true);
|
|
88
|
+
test("rich() plugin routes ctx.sendRichMessage through the business connection/topic", async () => {
|
|
89
|
+
const { api, calls } = mockApi();
|
|
90
|
+
const composer = new Composer().install(rich());
|
|
158
91
|
|
|
159
|
-
|
|
160
|
-
|
|
92
|
+
const ctx = new Context({
|
|
93
|
+
api,
|
|
94
|
+
update: {
|
|
95
|
+
update_id: 1,
|
|
96
|
+
business_message: {
|
|
97
|
+
message_id: 1,
|
|
98
|
+
date: 0,
|
|
99
|
+
chat: { id: 5, type: "private" },
|
|
100
|
+
business_connection_id: "bc1",
|
|
101
|
+
message_thread_id: 9,
|
|
102
|
+
},
|
|
103
|
+
} as never,
|
|
104
|
+
updateType: "business_message",
|
|
105
|
+
});
|
|
161
106
|
|
|
162
|
-
|
|
163
|
-
const api = mockApi();
|
|
164
|
-
const draft = new RichMessageDraft(api, 1, 2);
|
|
107
|
+
await composer.toMiddleware()(ctx as never, async () => {});
|
|
165
108
|
|
|
166
|
-
|
|
167
|
-
|
|
109
|
+
const decorated = ctx as unknown as { sendRichMessage: (input: unknown) => Promise<unknown> };
|
|
110
|
+
await decorated.sendRichMessage(document([paragraph("hi")]));
|
|
168
111
|
|
|
169
|
-
assert.equal(
|
|
170
|
-
assert.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
112
|
+
assert.equal(calls[0]?.method, "sendRichMessage");
|
|
113
|
+
assert.deepEqual(calls[0]?.params, {
|
|
114
|
+
chat_id: 5,
|
|
115
|
+
rich_message: { html: "<p>hi</p>" },
|
|
116
|
+
business_connection_id: "bc1",
|
|
117
|
+
message_thread_id: 9,
|
|
118
|
+
});
|
|
174
119
|
});
|
|
175
120
|
|
|
176
|
-
test("
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
{ type: "photo", photo: [] },
|
|
180
|
-
] as unknown as RichBlock[];
|
|
121
|
+
test("sendRichMessage()/richMessageDraft() reject when the update has no chat", async () => {
|
|
122
|
+
const { api } = mockApi();
|
|
123
|
+
const composer = new Composer().install(rich());
|
|
181
124
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
125
|
+
const ctx = new Context({
|
|
126
|
+
api,
|
|
127
|
+
update: { update_id: 1 } as never,
|
|
128
|
+
updateType: "message",
|
|
129
|
+
});
|
|
186
130
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
{
|
|
194
|
-
type: "list",
|
|
195
|
-
items: [{ label: "1", blocks: [{ type: "paragraph", text: "one" }] }],
|
|
196
|
-
},
|
|
197
|
-
],
|
|
198
|
-
} as unknown as RichMessage;
|
|
131
|
+
await composer.toMiddleware()(ctx as never, async () => {});
|
|
132
|
+
|
|
133
|
+
const decorated = ctx as unknown as {
|
|
134
|
+
sendRichMessage: (input: unknown) => Promise<unknown>;
|
|
135
|
+
richMessageDraft: (draftId: number) => RichMessageDraft;
|
|
136
|
+
};
|
|
199
137
|
|
|
200
|
-
assert.
|
|
138
|
+
await assert.rejects(() => decorated.sendRichMessage("x"), /no chat in this update/);
|
|
139
|
+
assert.throws(() => decorated.richMessageDraft(1), /no chat in this update/);
|
|
201
140
|
});
|
package/src/index.ts
CHANGED
|
@@ -6,12 +6,17 @@
|
|
|
6
6
|
* parses it server-side, and you get the same tree back on `message.rich_message`.
|
|
7
7
|
* this package covers all three parts of that surface:
|
|
8
8
|
*
|
|
9
|
-
* - **write**:
|
|
10
|
-
*
|
|
9
|
+
* - **write**: one dual-dialect builder set. every builder (`bold`, `paragraph`,
|
|
10
|
+
* `table`, …) returns a dialect-agnostic `RichNode`; the `html`/`md` template
|
|
11
|
+
* tags (or `document()`) pick the wire dialect once, at the edge, and emit a
|
|
12
|
+
* `RichDocument` — the `rich_message` envelope with fluent `.rtl()` /
|
|
13
|
+
* `.noEntityDetection()` flags. interpolated text is always dialect-escaped,
|
|
14
|
+
* so user input can never inject markup in either dialect.
|
|
11
15
|
* - **stream**: `RichMessageDraft` (draft.ts) owns the fiddly part of
|
|
12
16
|
* `sendRichMessageDraft` — the draft is ephemeral (telegram drops it 30s after
|
|
13
|
-
* the last push) and must be closed with a real `sendRichMessage` or
|
|
14
|
-
*
|
|
17
|
+
* the last push) and must be closed with a real `sendRichMessage` (`send()`) or
|
|
18
|
+
* explicitly `cancel()`led. `rewrite()` replaces the whole draft, `write()`
|
|
19
|
+
* appends to it.
|
|
15
20
|
* - **read**: type guards (guards.ts) and plain-text flattening (plaintext.ts)
|
|
16
21
|
* cover every `RichBlock`/`RichText` variant telegram can hand back.
|
|
17
22
|
*
|
|
@@ -95,9 +100,17 @@ export {
|
|
|
95
100
|
details,
|
|
96
101
|
divider,
|
|
97
102
|
footer,
|
|
103
|
+
h1,
|
|
104
|
+
h2,
|
|
105
|
+
h3,
|
|
106
|
+
h4,
|
|
107
|
+
h5,
|
|
108
|
+
h6,
|
|
98
109
|
heading,
|
|
99
110
|
image,
|
|
100
111
|
item,
|
|
112
|
+
join,
|
|
113
|
+
type ListItem,
|
|
101
114
|
type ListItemOptions,
|
|
102
115
|
type ListOptions,
|
|
103
116
|
list,
|
|
@@ -109,13 +122,16 @@ export {
|
|
|
109
122
|
preformatted,
|
|
110
123
|
pullquote,
|
|
111
124
|
slideshow,
|
|
125
|
+
type TableCell,
|
|
112
126
|
type TableCellOptions,
|
|
113
127
|
type TableOptions,
|
|
114
128
|
table,
|
|
115
129
|
thinking,
|
|
116
130
|
video,
|
|
117
131
|
} from "./blocks.js";
|
|
132
|
+
export { RichDocument } from "./document.js";
|
|
118
133
|
export { RichMessageDraft, type RichMessageDraftOptions } from "./draft.js";
|
|
134
|
+
export { escapeMarkdown, escapeMarkdownUrl } from "./escape.js";
|
|
119
135
|
export {
|
|
120
136
|
isAnchorBlock,
|
|
121
137
|
isAnchorLink,
|
|
@@ -137,6 +153,7 @@ export {
|
|
|
137
153
|
isFooter,
|
|
138
154
|
isHashtag,
|
|
139
155
|
isHeading,
|
|
156
|
+
isItalic,
|
|
140
157
|
isList,
|
|
141
158
|
isMap,
|
|
142
159
|
isMarked,
|
|
@@ -167,17 +184,14 @@ export {
|
|
|
167
184
|
anchor,
|
|
168
185
|
anchorLink,
|
|
169
186
|
bold,
|
|
187
|
+
br,
|
|
170
188
|
code,
|
|
171
189
|
customEmoji,
|
|
172
190
|
dateTime,
|
|
173
|
-
html,
|
|
174
|
-
type Insertable,
|
|
175
|
-
isRichNode,
|
|
176
191
|
italic,
|
|
177
192
|
link,
|
|
178
193
|
marked,
|
|
179
194
|
math,
|
|
180
|
-
type RichNode,
|
|
181
195
|
reference,
|
|
182
196
|
referenceLink,
|
|
183
197
|
spoiler,
|
|
@@ -185,15 +199,16 @@ export {
|
|
|
185
199
|
subscript,
|
|
186
200
|
superscript,
|
|
187
201
|
textMention,
|
|
188
|
-
toHtml,
|
|
189
202
|
underline,
|
|
190
203
|
} from "./inline.js";
|
|
191
|
-
export { type
|
|
192
|
-
|
|
204
|
+
export { type Dialect, isRichNode, type Level, RichError, type RichNode } from "./node.js";
|
|
193
205
|
export { richBlockToPlainText, richMessageToPlainText, richTextToPlainText } from "./plaintext.js";
|
|
206
|
+
export { escapeFor, type Insertable, render } from "./render.js";
|
|
194
207
|
export {
|
|
195
208
|
type RichContext,
|
|
209
|
+
type RichSource,
|
|
196
210
|
rich,
|
|
197
211
|
sendRichMessage,
|
|
198
212
|
sendRichMessageDraft,
|
|
199
213
|
} from "./send.js";
|
|
214
|
+
export { type DocumentOptions, document, html, md, type RichTemplate } from "./template.js";
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import assert from "node:assert/strict";
|
|
2
|
+
import test from "node:test";
|
|
3
|
+
import {
|
|
4
|
+
anchor,
|
|
5
|
+
anchorLink,
|
|
6
|
+
bold,
|
|
7
|
+
br,
|
|
8
|
+
code,
|
|
9
|
+
customEmoji,
|
|
10
|
+
dateTime,
|
|
11
|
+
italic,
|
|
12
|
+
link,
|
|
13
|
+
marked,
|
|
14
|
+
math,
|
|
15
|
+
reference,
|
|
16
|
+
referenceLink,
|
|
17
|
+
spoiler,
|
|
18
|
+
strikethrough,
|
|
19
|
+
subscript,
|
|
20
|
+
superscript,
|
|
21
|
+
textMention,
|
|
22
|
+
underline,
|
|
23
|
+
} from "./inline.js";
|
|
24
|
+
import type { RichNode } from "./node.js";
|
|
25
|
+
|
|
26
|
+
// every inline builder is dual-dialect: one node, two renders.
|
|
27
|
+
function both(node: RichNode): [html: string, md: string] {
|
|
28
|
+
return [node.render("html"), node.render("markdown")];
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test("wrapper marks render their token pair in each dialect", () => {
|
|
32
|
+
assert.deepEqual(both(bold("x")), ["<b>x</b>", "**x**"]);
|
|
33
|
+
assert.deepEqual(both(italic("x")), ["<i>x</i>", "*x*"]);
|
|
34
|
+
assert.deepEqual(both(strikethrough("x")), ["<s>x</s>", "~~x~~"]);
|
|
35
|
+
assert.deepEqual(both(spoiler("x")), ["<tg-spoiler>x</tg-spoiler>", "||x||"]);
|
|
36
|
+
assert.deepEqual(both(code("x")), ["<code>x</code>", "`x`"]);
|
|
37
|
+
assert.deepEqual(both(marked("x")), ["<mark>x</mark>", "==x=="]);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
test("marks with no markdown token embed the raw html tag in both dialects", () => {
|
|
41
|
+
assert.deepEqual(both(underline("x")), ["<u>x</u>", "<u>x</u>"]);
|
|
42
|
+
assert.deepEqual(both(subscript("x")), ["<sub>x</sub>", "<sub>x</sub>"]);
|
|
43
|
+
assert.deepEqual(both(superscript("x")), ["<sup>x</sup>", "<sup>x</sup>"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test("string children are escaped per dialect, so user input cannot inject markup", () => {
|
|
47
|
+
assert.equal(bold("<i>&</i>").render("html"), "<b><i>&</i></b>");
|
|
48
|
+
assert.equal(bold("a*b_c").render("markdown"), "**a\\*b\\_c**");
|
|
49
|
+
// html-significant chars become numeric entities in markdown (a backslash would show).
|
|
50
|
+
assert.equal(bold("<&>").render("markdown"), "**<&>**");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
test("nested nodes render into the chosen dialect; siblings concatenate", () => {
|
|
54
|
+
const node = bold("a ", italic("b"), " c");
|
|
55
|
+
|
|
56
|
+
assert.equal(node.render("html"), "<b>a <i>b</i> c</b>");
|
|
57
|
+
assert.equal(node.render("markdown"), "**a *b* c**");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
test("null/undefined/false children render as nothing — `cond && node` composes", () => {
|
|
61
|
+
const cond = false as boolean;
|
|
62
|
+
|
|
63
|
+
assert.equal(bold("a", null, undefined, cond && italic("b")).render("html"), "<b>a</b>");
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test("array children are flattened and concatenated", () => {
|
|
67
|
+
assert.equal(bold(["a", ["b", italic("c")]]).render("html"), "<b>ab<i>c</i></b>");
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test("number and bigint children are stringified then escaped", () => {
|
|
71
|
+
assert.equal(bold(42, "|", 10n).render("markdown"), "**42\\|10**");
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
test("br() is a hard newline in markdown, <br/> in html", () => {
|
|
75
|
+
assert.deepEqual(both(br()), ["<br/>", "\n"]);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
test("link() escapes the url per dialect so a hostile url cannot break out", () => {
|
|
79
|
+
assert.equal(
|
|
80
|
+
link('https://x.dev/?a=1&b="2"', "t").render("html"),
|
|
81
|
+
'<a href="https://x.dev/?a=1&b="2"">t</a>',
|
|
82
|
+
);
|
|
83
|
+
assert.equal(link("https://x.dev/a) b", "t").render("markdown"), "[t](https://x.dev/a\\)%20b)");
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
test("textMention() links tg://user?id=… in both dialects", () => {
|
|
87
|
+
assert.deepEqual(both(textMention({ id: 7 }, "dave")), [
|
|
88
|
+
'<a href="tg://user?id=7">dave</a>',
|
|
89
|
+
"[dave](tg://user?id=7)",
|
|
90
|
+
]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
test("anchor()/anchorLink() — jump target and #link", () => {
|
|
94
|
+
assert.deepEqual(both(anchor("top")), ['<a name="top"></a>', '<a name="top"></a>']);
|
|
95
|
+
assert.deepEqual(both(anchorLink("top", "up")), ['<a href="#top">up</a>', "[up](#top)"]);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
test("customEmoji() — tg-emoji tag / tg://emoji image link", () => {
|
|
99
|
+
assert.deepEqual(both(customEmoji("5368324170671202286", "👍")), [
|
|
100
|
+
'<tg-emoji emoji-id="5368324170671202286">👍</tg-emoji>',
|
|
101
|
+
"",
|
|
102
|
+
]);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
test("math() keeps LaTeX raw in markdown, html-escapes it in html", () => {
|
|
106
|
+
assert.deepEqual(both(math("a_1 < b")), ["<tg-math>a_1 < b</tg-math>", "$a_1 < b$"]);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
test("dateTime() — best-effort <time> / tg://time image link", () => {
|
|
110
|
+
assert.deepEqual(both(dateTime(1735689600, "R", "soon")), [
|
|
111
|
+
'<time datetime="1735689600" data-format="R">soon</time>',
|
|
112
|
+
"",
|
|
113
|
+
]);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
test("reference()/referenceLink() — tg-reference tags / markdown footnote syntax", () => {
|
|
117
|
+
assert.deepEqual(both(reference("1", "the fine print")), [
|
|
118
|
+
'<tg-reference name="1">the fine print</tg-reference>',
|
|
119
|
+
"[^1]: the fine print",
|
|
120
|
+
]);
|
|
121
|
+
assert.deepEqual(both(referenceLink("1", "see note")), [
|
|
122
|
+
'<tg-reference-link name="1">see note</tg-reference-link>',
|
|
123
|
+
"[^1]",
|
|
124
|
+
]);
|
|
125
|
+
});
|