tellegram 1.0.0

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 ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2021 Skoropad Aleksandr
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,80 @@
1
+ # teLLegraM
2
+
3
+ [![Build](https://img.shields.io/github/actions/workflow/status/skoropadas/telegramify-markdown/release.yml?branch=master)](https://github.com/skoropadas/telegramify-markdown/actions)
4
+ [![codecov](https://codecov.io/gh/skoropadas/telegramify-markdown/branch/master/graph/badge.svg?token=LxCmgGNUHl)](https://codecov.io/gh/skoropadas/telegramify-markdown)
5
+ ![License](https://img.shields.io/github/license/skoropadas/telegramify-markdown)
6
+
7
+ teLLegraM is a library designed to format LLM (Large Language Model) generated text into [Telegram-specific-markdown (MarkdownV2)](https://core.telegram.org/bots/api#formatting-options), based on [Unified](https://github.com/unifiedjs/unified) and [Remark](https://github.com/remarkjs/remark/). It ensures that complex markdown from AI responses is perfectly interpreted by Telegram clients.
8
+
9
+ ## Install
10
+
11
+ ```bash
12
+ npm install tellegram
13
+ ```
14
+
15
+ ## Usage
16
+
17
+ ```js
18
+ const teLLegraM = require('tellegram');
19
+ const markdown = `
20
+ # Header
21
+ ## Subheader
22
+
23
+ [1.0.0](http://version.com)
24
+
25
+ * item 1
26
+ * item 2
27
+ * item 3
28
+
29
+ And simple text with + some - symbols.
30
+ `;
31
+
32
+ teLLegraM(markdown);
33
+ /*
34
+ *Header*
35
+ *Subheader*
36
+
37
+ [1\.0\.0](http://version.com)
38
+
39
+ • item 1
40
+ • item 2
41
+ • item 3
42
+
43
+ And simple text with \+ some \- symbols\.
44
+ */
45
+ ```
46
+
47
+ ## Possible options
48
+
49
+ You can also add unsupported tags strategy as a second argument, which can be one of the following:
50
+
51
+ - `escape` - escape unsupported symbols for unsupported tags
52
+ - `remove` - remove unsupported tags
53
+ - `keep` - ignore unsupported tags (default)
54
+
55
+ ```js
56
+ const teLLegraM = require('teLLegraM');
57
+ const markdown = `
58
+ # Header
59
+
60
+ > Blockquote
61
+
62
+ <div>Text in div</div>
63
+ `;
64
+
65
+ teLLegraM(markdown, 'escape');
66
+ /*
67
+ *Header*
68
+
69
+ \> Blockquote
70
+
71
+ <div\>Text in div</div\>
72
+ */
73
+
74
+ teLLegraM(markdown, 'remove');
75
+ /*
76
+ *Header*
77
+ */
78
+ ```
79
+
80
+ [MIT Licence](LICENSE)
package/index.js ADDED
@@ -0,0 +1,2 @@
1
+ import convert from './lib/convert.js';
2
+ export default convert;
package/lib/convert.js ADDED
@@ -0,0 +1,25 @@
1
+ import gfm from 'remark-gfm';
2
+ import parse from 'remark-parse';
3
+ import stringify from 'remark-stringify';
4
+ import removeComments from 'remark-remove-comments';
5
+ import unified from 'unified';
6
+
7
+ import { collectDefinitions, removeDefinitions } from './definitions.js';
8
+ import createTelegramifyOptions from './telegramify.js';
9
+
10
+ export default (markdown, unsupportedTagsStrategy) => {
11
+ const definitions = {};
12
+
13
+ const telegramifyOptions = createTelegramifyOptions(definitions, unsupportedTagsStrategy);
14
+
15
+ return unified()
16
+ .use(parse)
17
+ .use(gfm)
18
+ .use(removeComments)
19
+ .use(collectDefinitions, definitions)
20
+ .use(removeDefinitions)
21
+ .use(stringify, telegramifyOptions)
22
+ .processSync(markdown)
23
+ .toString()
24
+ .replace(/<!---->\n/gi, '');
25
+ };
@@ -0,0 +1,27 @@
1
+ import remove from 'unist-util-remove';
2
+ import visit from 'unist-util-visit';
3
+
4
+ /**
5
+ * Fills the provided record with `Definition`s contained in the mdast.
6
+ * They are keyed by identifier for subsequent `Reference` lookups.
7
+ *
8
+ * @param {Record<string, { title: null | string, url: string }>} definitions
9
+ */
10
+ export const collectDefinitions = definitions => tree => {
11
+ visit(tree, 'definition', node => {
12
+ definitions[node.identifier] = {
13
+ title: node.title,
14
+ url: node.url,
15
+ };
16
+ });
17
+ };
18
+
19
+ /**
20
+ * Removes `Definition`s and their parent `Paragraph`s from the mdast.
21
+ * This avoids unwanted negative space in stringified output.
22
+ */
23
+ export const removeDefinitions = () => tree => {
24
+ remove(tree, { cascade: true }, 'definition');
25
+ };
26
+
27
+
@@ -0,0 +1,158 @@
1
+ import defaultHandlers from 'mdast-util-to-markdown/lib/handle/index.js';
2
+ import phrasing from 'mdast-util-to-markdown/lib/util/container-phrasing.js';
3
+ import {toMarkdown as gfmTableToMarkdown} from 'mdast-util-gfm-table';
4
+
5
+ import {wrap, isURL, escapeSymbols, processUnsupportedTags} from './utils.js';
6
+
7
+ /**
8
+ * Creates custom `mdast-util-to-markdown` handlers that tailor the output for
9
+ * Telegram Markdown.
10
+ *
11
+ * @param {Readonly<Record<string, { title: null | string, url: string }>>} definitions
12
+ * Record of `Definition`s in the Markdown document, keyed by identifier.
13
+ *
14
+ * @returns {import('mdast-util-to-markdown').Handlers}
15
+ */
16
+ const createHandlers = (definitions, unsupportedTagsStrategy) => ({
17
+ heading: (node, _parent, context) => {
18
+ // make headers to be just *strong*
19
+ const marker = '*';
20
+
21
+ const exit = context.enter('heading');
22
+ const value = phrasing(node, context, {before: marker, after: marker});
23
+ exit();
24
+
25
+ return wrap(value, marker);
26
+ },
27
+
28
+ strong: (node, _parent, context) => {
29
+ const marker = '*';
30
+
31
+ const exit = context.enter('strong');
32
+ const value = phrasing(node, context, {before: marker, after: marker});
33
+ exit();
34
+
35
+ return wrap(value, marker);
36
+ },
37
+
38
+ delete(node, _parent, context) {
39
+ const marker = '~';
40
+
41
+ const exit = context.enter('delete');
42
+ const value = phrasing(node, context, {before: marker, after: marker});
43
+ exit();
44
+
45
+ return wrap(value, marker);
46
+ },
47
+
48
+ emphasis: (node, _parent, context) => {
49
+ const marker = '_';
50
+
51
+ const exit = context.enter('emphasis');
52
+ const value = phrasing(node, context, {before: marker, after: marker});
53
+ exit();
54
+
55
+ return wrap(value, marker);
56
+ },
57
+
58
+ list: (...args) => defaultHandlers.list(...args).replace(/^(\d+)./gm, '$1\\.'),
59
+
60
+ listItem: (...args) => defaultHandlers.listItem(...args).replace(/^\*/, '•'),
61
+
62
+ code(node, _parent, context) {
63
+ const exit = context.enter('code');
64
+ // delete language prefix for deprecated markdown formatters (old Bitbucket Editor)
65
+ const content = node.value.replace(/^#![a-z]+\n/, ''); // ```\n#!javascript\ncode block\n```
66
+ exit();
67
+
68
+ const language = node.lang || '';
69
+ return `\`\`\`${language}\n${escapeSymbols(content, 'code')}\n\`\`\``;
70
+ },
71
+
72
+ link: (node, _parent, context) => {
73
+ const exit = context.enter('link');
74
+ const text = phrasing(node, context, {before: '|', after: '>'}) || escapeSymbols(node.title);
75
+ const isUrlEncoded = decodeURI(node.url) !== node.url;
76
+ const url = isUrlEncoded ? node.url : encodeURI(node.url);
77
+ exit();
78
+
79
+ if (!isURL(url)) return escapeSymbols(text) || escapeSymbols(url);
80
+
81
+ return text
82
+ ? `[${text}](${escapeSymbols(url, 'link')})`
83
+ : `[${escapeSymbols(url)}](${escapeSymbols(url, 'link')})`;
84
+ },
85
+
86
+ linkReference: (node, _parent, context) => {
87
+ const exit = context.enter('linkReference');
88
+ const definition = definitions[node.identifier];
89
+ const text = phrasing(node, context, {before: '|', after: '>'}) || (definition ? definition.title : null);
90
+ exit();
91
+
92
+ if (!definition || !isURL(definition.url)) return escapeSymbols(text);
93
+
94
+ return text
95
+ ? `[${text}](${escapeSymbols(definition.url, 'link')})`
96
+ : `[${escapeSymbols(definition.url)}](${escapeSymbols(definition.url, 'link')})`;
97
+ },
98
+
99
+ image: (node, _parent, context) => {
100
+ const exit = context.enter('image');
101
+ const text = node.alt || node.title;
102
+ const url = node.url
103
+ exit();
104
+
105
+ if (!isURL(url)) return escapeSymbols(text) || escapeSymbols(url);
106
+
107
+ return text
108
+ ? `[${escapeSymbols(text)}](${escapeSymbols(url, 'link')})`
109
+ : `[${escapeSymbols(url)}](${escapeSymbols(url, 'link')})`;
110
+ },
111
+
112
+ imageReference: (node, _parent, context) => {
113
+ const exit = context.enter('imageReference');
114
+ const definition = definitions[node.identifier];
115
+ const text = node.alt || (definition ? definition.title : null);
116
+ exit();
117
+
118
+ if (!definition || !isURL(definition.url)) return escapeSymbols(text);
119
+
120
+ return text
121
+ ? `[${escapeSymbols(text)}](${escapeSymbols(definition.url, 'link')})`
122
+ : `[${escapeSymbols(definition.url)}](${escapeSymbols(definition.url, 'link')})`;
123
+ },
124
+
125
+ text: (node, _parent, context) => {
126
+ const exit = context.enter('text');
127
+ const text = node.value;
128
+ exit();
129
+
130
+ return escapeSymbols(text);
131
+ },
132
+
133
+ blockquote: (node, _parent, context) =>
134
+ processUnsupportedTags(defaultHandlers.blockquote(node, _parent, context), unsupportedTagsStrategy),
135
+ html: (node, _parent, context) =>
136
+ processUnsupportedTags(defaultHandlers.html(node, _parent, context), unsupportedTagsStrategy),
137
+ table: (node, _parent, context) =>
138
+ processUnsupportedTags(gfmTableToMarkdown().handlers.table(node, _parent, context), unsupportedTagsStrategy),
139
+ thematicBreak: (_node, _parent, _context) =>
140
+ processUnsupportedTags('---', unsupportedTagsStrategy),
141
+ });
142
+
143
+ /**
144
+ * Creates options to be passed into a `remark-stringify` processor that tailor
145
+ * the output for Telegram Markdown.
146
+ *
147
+ * @param {Readonly<Record<string, { title: null | string, url: string }>>} definitions
148
+ * Record of `Definition`s in the Markdown document, keyed by identifier.
149
+ *
150
+ * @returns {import('remark-stringify').RemarkStringifyOptions}
151
+ */
152
+ const createOptions = (definitions, unsupportedTagsStrategy) => ({
153
+ bullet: '*',
154
+ tightDefinitions: true,
155
+ handlers: createHandlers(definitions, unsupportedTagsStrategy),
156
+ });
157
+
158
+ export default createOptions;
package/lib/utils.js ADDED
@@ -0,0 +1,76 @@
1
+ import { URL } from 'url';
2
+
3
+ export function wrap(string, ...wrappers) {
4
+ return [
5
+ ...wrappers,
6
+ string,
7
+ ...wrappers.reverse(),
8
+ ].join('');
9
+ }
10
+
11
+ export function isURL(string) {
12
+ try {
13
+ return Boolean(new URL(string));
14
+ } catch (error) {
15
+ return false;
16
+ }
17
+ }
18
+
19
+ export function escapeSymbols(text, textType = 'text') {
20
+ if (!text) {
21
+ return text;
22
+ }
23
+ switch (textType) {
24
+ case 'code':
25
+ return text
26
+ .replace(/\\/g, '\\\\')
27
+ .replace(/`/g, '\\`')
28
+ case 'link':
29
+ return text
30
+ .replace(/\\/g, '\\\\')
31
+ .replace(/\(/g, '\\(')
32
+ .replace(/\)/g, '\\)')
33
+ case 'ignore_escaped':
34
+ return text.replace(/(\\.)|([_*\[\]()~`>#+\-=|{}.!])/g, (match, escaped, char) => {
35
+ if (escaped) {
36
+ return escaped;
37
+ }
38
+ return '\\' + char;
39
+ });
40
+ default:
41
+ return text
42
+ .replace(/_/g, '\\_')
43
+ .replace(/\*/g, '\\*')
44
+ .replace(/\[/g, '\\[')
45
+ .replace(/]/g, '\\]')
46
+ .replace(/\(/g, '\\(')
47
+ .replace(/\)/g, '\\)')
48
+ .replace(/~/g, '\\~')
49
+ .replace(/`/g, '\\`')
50
+ .replace(/>/g, '\\>')
51
+ .replace(/#/g, '\\#')
52
+ .replace(/\+/g, '\\+')
53
+ .replace(/-/g, '\\-')
54
+ .replace(/=/g, '\\=')
55
+ .replace(/\|/g, '\\|')
56
+ .replace(/{/g, '\\{')
57
+ .replace(/}/g, '\\}')
58
+ .replace(/\./g, '\\.')
59
+ .replace(/!/g, '\\!');
60
+
61
+ }
62
+ }
63
+
64
+ export function processUnsupportedTags(content, strategy) {
65
+ switch (strategy) {
66
+ case 'escape':
67
+ return escapeSymbols(content, 'ignore_escaped');
68
+ case 'remove':
69
+ return '';
70
+ case 'keep':
71
+ default:
72
+ return content;
73
+ }
74
+ }
75
+
76
+
package/package.json ADDED
@@ -0,0 +1,70 @@
1
+ {
2
+ "name": "tellegram",
3
+ "version": "1.0.0",
4
+ "description": "Convert LLM-generated markdown into Telegram-specific markdown (MarkdownV2)",
5
+ "type": "module",
6
+ "main": "index.js",
7
+ "scripts": {
8
+ "test": "NODE_OPTIONS=--experimental-vm-modules npx jest --coverage",
9
+ "lint": "eslint",
10
+ "semantic-release": "semantic-release",
11
+ "prepare": "husky install",
12
+ "codecov": "codecov"
13
+ },
14
+ "files": [
15
+ "README.md",
16
+ "LICENSE",
17
+ "index.js",
18
+ "lib",
19
+ "types"
20
+ ],
21
+ "repository": {
22
+ "type": "git",
23
+ "url": "git+https://github.com/leask/tellegram.git"
24
+ },
25
+ "keywords": [
26
+ "telegram",
27
+ "markdown",
28
+ "telegramify",
29
+ "parser",
30
+ "remark",
31
+ "unified"
32
+ ],
33
+ "types": "types/index.d.ts",
34
+ "author": "Leask Wong",
35
+ "license": "MIT",
36
+ "bugs": {
37
+ "url": "https://github.com/leask/tellegram/issues"
38
+ },
39
+ "homepage": "https://github.com/leask/tellegram#readme",
40
+ "dependencies": {
41
+ "mdast-util-gfm-table": "^0.1.6",
42
+ "mdast-util-to-markdown": "^0.6.2",
43
+ "remark-gfm": "^1.0.0",
44
+ "remark-parse": "^9.0.0",
45
+ "remark-remove-comments": "^0.2.0",
46
+ "remark-stringify": "^9.0.1",
47
+ "unified": "^9.0.0",
48
+ "unist-util-remove": "^2.0.1",
49
+ "unist-util-visit": "^2.0.3"
50
+ },
51
+ "devDependencies": {
52
+ "@commitlint/cli": "^12.1.1",
53
+ "@commitlint/config-conventional": "^12.1.1",
54
+ "codecov": "^3.8.3",
55
+ "eslint": "^7.24.0",
56
+ "husky": "^6.0.0",
57
+ "jest": "^29.3.1",
58
+ "lint-staged": "^10.5.4",
59
+ "prettier": "2.2.1",
60
+ "semantic-release": "^17.4.2"
61
+ },
62
+ "lint-staged": {
63
+ "*.{js,json,md}": [
64
+ "prettier --write",
65
+ "git add"
66
+ ],
67
+ "*.{css,scss,less}": "stylelint --fix",
68
+ "*.js": "eslint --cache --fix"
69
+ }
70
+ }
@@ -0,0 +1,13 @@
1
+ type UnsupportedTagsStrategy = 'escape' | 'remove' | 'keep'
2
+
3
+ declare module 'tellegram' {
4
+
5
+ /**
6
+ * Converts markdown to Telegram's format.
7
+ * @param markdown The markdown to convert.
8
+ * @param unsupportedTagsStrategy The strategy to use for unsupported tags.
9
+ */
10
+ function convert(markdown: string, unsupportedTagsStrategy: UnsupportedTagsStrategy): string
11
+
12
+ export = convert
13
+ }