@mtcute/markdown-parser 0.16.3 → 0.16.9
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 +3 -3
- package/index.cjs +352 -0
- package/{cjs/index.d.ts → index.d.cts} +2 -2
- package/index.d.ts +47 -1
- package/index.js +347 -1
- package/markdown-parser.test.d.ts +1 -0
- package/package.json +28 -24
- package/cjs/index.js +0 -416
- package/cjs/index.js.map +0 -1
- package/cjs/package.json +0 -3
- package/esm/index.d.ts +0 -47
- package/esm/index.js +0 -404
- package/esm/index.js.map +0 -1
package/README.md
CHANGED
|
@@ -129,10 +129,10 @@ You can interpolate one of the following:
|
|
|
129
129
|
- `number` - will be converted to string and appended to plain text as-is
|
|
130
130
|
- `TextWithEntities` or `MessageEntity` - will add the text and its entities to the output. This is the type returned by `md` itself:
|
|
131
131
|
```ts
|
|
132
|
-
|
|
133
|
-
|
|
132
|
+
const bold = md`**bold**`
|
|
133
|
+
const text = md`Hello, ${bold}!`
|
|
134
134
|
```
|
|
135
135
|
- falsy value (i.e. `null`, `undefined`, `false`) - will be ignored
|
|
136
136
|
|
|
137
137
|
Because of interpolation, you almost never need to think about escaping anything,
|
|
138
|
-
since the values are not even parsed as Markdown, and are appended to the output as-is.
|
|
138
|
+
since the values are not even parsed as Markdown, and are appended to the output as-is.
|
package/index.cjs
ADDED
|
@@ -0,0 +1,352 @@
|
|
|
1
|
+
if (typeof globalThis !== "undefined" && !globalThis._MTCUTE_CJS_DEPRECATION_WARNED) {
|
|
2
|
+
globalThis._MTCUTE_CJS_DEPRECATION_WARNED = true;
|
|
3
|
+
console.warn("[@mtcute/markdown-parser] CommonJS support is deprecated and will be removed soon. Please consider switching to ESM, it's " + (/* @__PURE__ */ new Date()).getFullYear() + " already.");
|
|
4
|
+
console.warn("[@mtcute/markdown-parser] Learn more about switching to ESM: https://gist.github.com/sindresorhus/a39789f98801d908bbc7ff3ecc99d99c");
|
|
5
|
+
}
|
|
6
|
+
"use strict";
|
|
7
|
+
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
|
|
8
|
+
const Long = require("long");
|
|
9
|
+
const MENTION_REGEX = /^tg:\/\/user\?id=(\d+)(?:&hash=(-?[0-9a-fA-F]+)(?:&|$)|&|$)/;
|
|
10
|
+
const EMOJI_REGEX = /^tg:\/\/emoji\?id=(-?\d+)/;
|
|
11
|
+
const TAG_BOLD = "**";
|
|
12
|
+
const TAG_ITALIC = "__";
|
|
13
|
+
const TAG_UNDERLINE = "--";
|
|
14
|
+
const TAG_STRIKE = "~~";
|
|
15
|
+
const TAG_SPOILER = "||";
|
|
16
|
+
const TAG_CODE = "`";
|
|
17
|
+
const TAG_PRE = "```";
|
|
18
|
+
const TO_BE_ESCAPED = /[*_\-~`[\\\]|]/g;
|
|
19
|
+
function escape(str) {
|
|
20
|
+
return str.replace(TO_BE_ESCAPED, (s) => `\\${s}`);
|
|
21
|
+
}
|
|
22
|
+
function unparse(input) {
|
|
23
|
+
if (typeof input === "string") return escape(input);
|
|
24
|
+
let text = input.text;
|
|
25
|
+
const entities = input.entities ?? [];
|
|
26
|
+
const escaped = [];
|
|
27
|
+
text = text.replace(TO_BE_ESCAPED, (s, pos) => {
|
|
28
|
+
escaped.push(pos);
|
|
29
|
+
return `\\${s}`;
|
|
30
|
+
});
|
|
31
|
+
const hasEscaped = escaped.length > 0;
|
|
32
|
+
const insert = [];
|
|
33
|
+
for (const entity of entities) {
|
|
34
|
+
const type = entity._;
|
|
35
|
+
let start = entity.offset;
|
|
36
|
+
let end = start + entity.length;
|
|
37
|
+
if (start > text.length) continue;
|
|
38
|
+
if (start < 0) start = 0;
|
|
39
|
+
if (end > text.length) end = text.length;
|
|
40
|
+
if (hasEscaped) {
|
|
41
|
+
let escapedPos = 0;
|
|
42
|
+
while (escapedPos < escaped.length && escaped[escapedPos] < start) {
|
|
43
|
+
escapedPos += 1;
|
|
44
|
+
}
|
|
45
|
+
start += escapedPos;
|
|
46
|
+
while (escapedPos < escaped.length && escaped[escapedPos] <= end) {
|
|
47
|
+
escapedPos += 1;
|
|
48
|
+
}
|
|
49
|
+
end += escapedPos;
|
|
50
|
+
}
|
|
51
|
+
let startTag;
|
|
52
|
+
let endTag;
|
|
53
|
+
switch (type) {
|
|
54
|
+
case "messageEntityBold":
|
|
55
|
+
startTag = endTag = TAG_BOLD;
|
|
56
|
+
break;
|
|
57
|
+
case "messageEntityItalic":
|
|
58
|
+
startTag = endTag = TAG_ITALIC;
|
|
59
|
+
break;
|
|
60
|
+
case "messageEntityUnderline":
|
|
61
|
+
startTag = endTag = TAG_UNDERLINE;
|
|
62
|
+
break;
|
|
63
|
+
case "messageEntityStrike":
|
|
64
|
+
startTag = endTag = TAG_STRIKE;
|
|
65
|
+
break;
|
|
66
|
+
case "messageEntitySpoiler":
|
|
67
|
+
startTag = endTag = TAG_SPOILER;
|
|
68
|
+
break;
|
|
69
|
+
case "messageEntityCode":
|
|
70
|
+
startTag = endTag = TAG_CODE;
|
|
71
|
+
break;
|
|
72
|
+
case "messageEntityPre":
|
|
73
|
+
startTag = TAG_PRE;
|
|
74
|
+
if (entity.language) {
|
|
75
|
+
startTag += entity.language;
|
|
76
|
+
}
|
|
77
|
+
startTag += "\n";
|
|
78
|
+
endTag = `
|
|
79
|
+
${TAG_PRE}`;
|
|
80
|
+
break;
|
|
81
|
+
case "messageEntityTextUrl":
|
|
82
|
+
startTag = "[";
|
|
83
|
+
endTag = `](${entity.url})`;
|
|
84
|
+
break;
|
|
85
|
+
case "messageEntityMentionName":
|
|
86
|
+
startTag = "[";
|
|
87
|
+
endTag = `](tg://user?id=${entity.userId})`;
|
|
88
|
+
break;
|
|
89
|
+
case "messageEntityCustomEmoji":
|
|
90
|
+
startTag = "[";
|
|
91
|
+
endTag = `](tg://emoji?id=${entity.documentId.toString()})`;
|
|
92
|
+
break;
|
|
93
|
+
default:
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
insert.push([start, startTag]);
|
|
97
|
+
insert.push([end, endTag]);
|
|
98
|
+
}
|
|
99
|
+
insert.sort((a, b) => b[0] - a[0]);
|
|
100
|
+
for (const [offset, tag] of insert) {
|
|
101
|
+
text = text.substr(0, offset) + tag + text.substr(offset);
|
|
102
|
+
}
|
|
103
|
+
return text;
|
|
104
|
+
}
|
|
105
|
+
function parse(strings, ...sub) {
|
|
106
|
+
const entities = [];
|
|
107
|
+
let result = "";
|
|
108
|
+
const stacks = {};
|
|
109
|
+
let insideCode = false;
|
|
110
|
+
let insidePre = false;
|
|
111
|
+
let insideLink = false;
|
|
112
|
+
let insideLinkUrl = false;
|
|
113
|
+
let pendingLinkUrl = "";
|
|
114
|
+
function feed(text) {
|
|
115
|
+
const len = text.length;
|
|
116
|
+
let pos = 0;
|
|
117
|
+
while (pos < len) {
|
|
118
|
+
const c = text[pos];
|
|
119
|
+
if (c === "\\") {
|
|
120
|
+
if (insideLinkUrl) {
|
|
121
|
+
pendingLinkUrl += text[pos + 1];
|
|
122
|
+
} else {
|
|
123
|
+
result += text[pos + 1];
|
|
124
|
+
}
|
|
125
|
+
pos += 2;
|
|
126
|
+
continue;
|
|
127
|
+
}
|
|
128
|
+
if (insideCode) {
|
|
129
|
+
if (c === "`") {
|
|
130
|
+
const ent = stacks.code.pop();
|
|
131
|
+
ent.length = result.length - ent.offset;
|
|
132
|
+
entities.push(ent);
|
|
133
|
+
insideCode = false;
|
|
134
|
+
pos += 1;
|
|
135
|
+
} else {
|
|
136
|
+
pos += 1;
|
|
137
|
+
result += c;
|
|
138
|
+
}
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
if (insidePre) {
|
|
142
|
+
if (c === "`" || c === "\n" && text[pos + 1] === "`") {
|
|
143
|
+
if (c === "\n") pos += 1;
|
|
144
|
+
if (text[pos + 1] === "`" && text[pos + 2] === "`") {
|
|
145
|
+
const ent = stacks.pre.pop();
|
|
146
|
+
ent.length = result.length - ent.offset;
|
|
147
|
+
entities.push(ent);
|
|
148
|
+
insidePre = false;
|
|
149
|
+
pos += 3;
|
|
150
|
+
continue;
|
|
151
|
+
} else if (c === "\n") {
|
|
152
|
+
pos -= 1;
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
pos += 1;
|
|
156
|
+
result += c;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
if (insideLink && c === "]") {
|
|
160
|
+
const ent = stacks.link.pop();
|
|
161
|
+
if (text[pos + 1] !== "(") {
|
|
162
|
+
result = `${result.substr(0, ent.offset)}[${result.substr(ent.offset)}]`;
|
|
163
|
+
pos += 1;
|
|
164
|
+
insideLink = false;
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
pos += 2;
|
|
168
|
+
insideLink = false;
|
|
169
|
+
insideLinkUrl = true;
|
|
170
|
+
stacks.link.push(ent);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
if (insideLinkUrl) {
|
|
174
|
+
pos += 1;
|
|
175
|
+
if (c !== ")") {
|
|
176
|
+
pendingLinkUrl += c;
|
|
177
|
+
continue;
|
|
178
|
+
}
|
|
179
|
+
const ent = stacks.link.pop();
|
|
180
|
+
let url = pendingLinkUrl;
|
|
181
|
+
pendingLinkUrl = "";
|
|
182
|
+
insideLinkUrl = false;
|
|
183
|
+
if (!url.length) continue;
|
|
184
|
+
ent.length = result.length - ent.offset;
|
|
185
|
+
let m = url.match(MENTION_REGEX);
|
|
186
|
+
if (m) {
|
|
187
|
+
const userId = Number.parseInt(m[1]);
|
|
188
|
+
const accessHash = m[2];
|
|
189
|
+
if (accessHash) {
|
|
190
|
+
ent._ = "inputMessageEntityMentionName";
|
|
191
|
+
ent.userId = {
|
|
192
|
+
_: "inputUser",
|
|
193
|
+
userId,
|
|
194
|
+
accessHash: Long.fromString(accessHash, false, 16)
|
|
195
|
+
};
|
|
196
|
+
} else {
|
|
197
|
+
ent._ = "messageEntityMentionName";
|
|
198
|
+
ent.userId = userId;
|
|
199
|
+
}
|
|
200
|
+
} else if (m = EMOJI_REGEX.exec(url)) {
|
|
201
|
+
ent._ = "messageEntityCustomEmoji";
|
|
202
|
+
ent.documentId = Long.fromString(m[1]);
|
|
203
|
+
} else {
|
|
204
|
+
if (url.match(/^\/\//)) url = `http:${url}`;
|
|
205
|
+
ent._ = "messageEntityTextUrl";
|
|
206
|
+
ent.url = url;
|
|
207
|
+
}
|
|
208
|
+
entities.push(ent);
|
|
209
|
+
continue;
|
|
210
|
+
}
|
|
211
|
+
if (c === "[" && !insideLink) {
|
|
212
|
+
pos += 1;
|
|
213
|
+
insideLink = true;
|
|
214
|
+
if (!("link" in stacks)) stacks.link = [];
|
|
215
|
+
stacks.link.push({
|
|
216
|
+
offset: result.length,
|
|
217
|
+
length: 0
|
|
218
|
+
});
|
|
219
|
+
continue;
|
|
220
|
+
}
|
|
221
|
+
if (c === "`") {
|
|
222
|
+
const isPre = text[pos + 1] === "`" && text[pos + 2] === "`";
|
|
223
|
+
if (isPre) {
|
|
224
|
+
pos += 3;
|
|
225
|
+
let language = "";
|
|
226
|
+
while (pos < text.length && text[pos] !== "\n") {
|
|
227
|
+
language += text[pos++];
|
|
228
|
+
}
|
|
229
|
+
pos += 1;
|
|
230
|
+
if (pos > text.length) {
|
|
231
|
+
throw new Error("Malformed PRE entity, expected LF after ```");
|
|
232
|
+
}
|
|
233
|
+
if (!("pre" in stacks)) stacks.pre = [];
|
|
234
|
+
stacks.pre.push({
|
|
235
|
+
_: "messageEntityPre",
|
|
236
|
+
offset: result.length,
|
|
237
|
+
length: 0,
|
|
238
|
+
language
|
|
239
|
+
});
|
|
240
|
+
insidePre = true;
|
|
241
|
+
} else {
|
|
242
|
+
pos += 1;
|
|
243
|
+
if (!("code" in stacks)) stacks.code = [];
|
|
244
|
+
stacks.code.push({
|
|
245
|
+
_: "messageEntityCode",
|
|
246
|
+
offset: result.length,
|
|
247
|
+
length: 0
|
|
248
|
+
});
|
|
249
|
+
insideCode = true;
|
|
250
|
+
}
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
if (c === text[pos + 1]) {
|
|
254
|
+
let type = null;
|
|
255
|
+
switch (c) {
|
|
256
|
+
case "_":
|
|
257
|
+
type = "Italic";
|
|
258
|
+
break;
|
|
259
|
+
case "*":
|
|
260
|
+
type = "Bold";
|
|
261
|
+
break;
|
|
262
|
+
case "-":
|
|
263
|
+
type = "Underline";
|
|
264
|
+
break;
|
|
265
|
+
case "~":
|
|
266
|
+
type = "Strike";
|
|
267
|
+
break;
|
|
268
|
+
case "|":
|
|
269
|
+
type = "Spoiler";
|
|
270
|
+
break;
|
|
271
|
+
}
|
|
272
|
+
if (type) {
|
|
273
|
+
if (!(type in stacks)) stacks[type] = [];
|
|
274
|
+
const isBegin = stacks[type].length === 0;
|
|
275
|
+
if (isBegin) {
|
|
276
|
+
stacks[type].push({
|
|
277
|
+
_: `messageEntity${type}`,
|
|
278
|
+
offset: result.length,
|
|
279
|
+
length: 0
|
|
280
|
+
});
|
|
281
|
+
} else {
|
|
282
|
+
const ent = stacks[type].pop();
|
|
283
|
+
ent.length = result.length - ent.offset;
|
|
284
|
+
entities.push(ent);
|
|
285
|
+
}
|
|
286
|
+
pos += 2;
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (c === "\n") {
|
|
291
|
+
if (pos !== 0) {
|
|
292
|
+
result += "\n";
|
|
293
|
+
}
|
|
294
|
+
const nonWhitespace = text.slice(pos + 1).search(/[^ \t]/);
|
|
295
|
+
if (nonWhitespace !== -1) {
|
|
296
|
+
pos += nonWhitespace + 1;
|
|
297
|
+
} else {
|
|
298
|
+
pos = len;
|
|
299
|
+
result = result.trimEnd();
|
|
300
|
+
}
|
|
301
|
+
continue;
|
|
302
|
+
}
|
|
303
|
+
result += c;
|
|
304
|
+
pos += 1;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
if (typeof strings === "string") strings = [strings];
|
|
308
|
+
sub.forEach((it, idx) => {
|
|
309
|
+
feed(strings[idx]);
|
|
310
|
+
if (typeof it === "boolean" || !it) return;
|
|
311
|
+
if (insideLinkUrl) {
|
|
312
|
+
if (typeof it === "string" || typeof it === "number") {
|
|
313
|
+
pendingLinkUrl += it;
|
|
314
|
+
} else if (Long.isLong(it)) {
|
|
315
|
+
pendingLinkUrl += it.toString(10);
|
|
316
|
+
} else {
|
|
317
|
+
pendingLinkUrl += it.text;
|
|
318
|
+
}
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
if (typeof it === "string" || typeof it === "number") {
|
|
322
|
+
result += it;
|
|
323
|
+
} else if (Long.isLong(it)) {
|
|
324
|
+
result += it.toString(10);
|
|
325
|
+
} else {
|
|
326
|
+
const text = it.text;
|
|
327
|
+
const innerEntities = "raw" in it ? [it.raw] : it.entities;
|
|
328
|
+
const baseOffset = result.length;
|
|
329
|
+
result += text;
|
|
330
|
+
if (innerEntities) {
|
|
331
|
+
for (const ent of innerEntities) {
|
|
332
|
+
entities.push({ ...ent, offset: ent.offset + baseOffset });
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
});
|
|
337
|
+
feed(strings[strings.length - 1]);
|
|
338
|
+
for (const [name, stack] of Object.entries(stacks)) {
|
|
339
|
+
if (stack.length) {
|
|
340
|
+
throw new Error(`Unterminated ${name} entity`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return {
|
|
344
|
+
text: result,
|
|
345
|
+
entities
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
const md = Object.assign(parse, {
|
|
349
|
+
escape,
|
|
350
|
+
unparse
|
|
351
|
+
});
|
|
352
|
+
exports.md = md;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import Long from 'long';
|
|
2
|
-
import
|
|
1
|
+
import { default as Long } from 'long';
|
|
2
|
+
import { InputText, MessageEntity, TextWithEntities } from '@mtcute/core';
|
|
3
3
|
/**
|
|
4
4
|
* Escape a string to be safely used in Markdown.
|
|
5
5
|
*
|
package/index.d.ts
CHANGED
|
@@ -1 +1,47 @@
|
|
|
1
|
-
|
|
1
|
+
import { default as Long } from 'long';
|
|
2
|
+
import { InputText, MessageEntity, TextWithEntities } from '@mtcute/core';
|
|
3
|
+
/**
|
|
4
|
+
* Escape a string to be safely used in Markdown.
|
|
5
|
+
*
|
|
6
|
+
* > **Note**: this function is in most cases not needed, as `md` function
|
|
7
|
+
* > handles all `string`s passed to it automatically as plain text.
|
|
8
|
+
*/
|
|
9
|
+
declare function escape(str: string): string;
|
|
10
|
+
/**
|
|
11
|
+
* Add Markdown formatting to the text given the plain text and entities contained in it.
|
|
12
|
+
*/
|
|
13
|
+
declare function unparse(input: InputText): string;
|
|
14
|
+
export declare const md: {
|
|
15
|
+
/**
|
|
16
|
+
* Tagged template based Markdown-to-entities parser function
|
|
17
|
+
*
|
|
18
|
+
* Additionally, `md` function has two static methods:
|
|
19
|
+
* - `md.escape` - escape a string to be safely used in Markdown
|
|
20
|
+
* (should not be needed in most cases, as `md` function itself handles all `string`s
|
|
21
|
+
* passed to it automatically as plain text)
|
|
22
|
+
* - `md.unparse` - add Markdown formatting to the text given the plain text and entities contained in it
|
|
23
|
+
*
|
|
24
|
+
* @example
|
|
25
|
+
* ```typescript
|
|
26
|
+
* const text = md`**${user.displayName}**`
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
(strings: TemplateStringsArray, ...sub: (InputText | MessageEntity | Long | boolean | number | undefined | null)[]): TextWithEntities;
|
|
30
|
+
/**
|
|
31
|
+
* A variant taking a plain JS string as input
|
|
32
|
+
* and parsing it.
|
|
33
|
+
*
|
|
34
|
+
* Useful for cases when you already have a string
|
|
35
|
+
* (e.g. from some server) and want to parse it.
|
|
36
|
+
*
|
|
37
|
+
* @example
|
|
38
|
+
* ```typescript
|
|
39
|
+
* const string = '**hello**'
|
|
40
|
+
* const text = md(string)
|
|
41
|
+
* ```
|
|
42
|
+
*/
|
|
43
|
+
(string: string): TextWithEntities;
|
|
44
|
+
escape: typeof escape;
|
|
45
|
+
unparse: typeof unparse;
|
|
46
|
+
};
|
|
47
|
+
export {};
|