@grom.js/tgx 0.5.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) 2024-PRESENT Vladislav Deryabkin
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,53 @@
1
+ ![tgx](tgx.png)
2
+
3
+ [JSX](https://facebook.github.io/jsx/) runtime for composing Telegram messages.
4
+
5
+ ## Installation
6
+
7
+ ```sh
8
+ # Using npm
9
+ npm install @grom.js/tgx
10
+
11
+ # Using jsr
12
+ deno add jsr:@grom/tgx
13
+ ```
14
+
15
+ Then in your `tsconfig.json`:
16
+
17
+ ```jsonc
18
+ {
19
+ "compilerOptions": {
20
+ "jsx": "react-jsx",
21
+ "jsxImportSource": "@grom.js/tgx" // "@grom/tgx" for jsr
22
+ // ...
23
+ }
24
+ }
25
+ ```
26
+
27
+ ## Example
28
+
29
+ Usage with [grammY](https://grammy.dev):
30
+
31
+ ```tsx
32
+ import { html } from '@grom.js/tgx'
33
+ import { Bot } from 'grammy'
34
+
35
+ function Greeting(props: { name: string }) {
36
+ return <>Hello, <b>{props.name}</b>!</>
37
+ }
38
+
39
+ const bot = new Bot(/* TOKEN */)
40
+
41
+ bot.command('start', async (ctx) => {
42
+ await ctx.reply(
43
+ html(<Greeting name={ctx.from.first_name} />),
44
+ { parse_mode: 'HTML' }
45
+ )
46
+ })
47
+
48
+ bot.start()
49
+ ```
50
+
51
+ ## License
52
+
53
+ [MIT](./LICENSE)
@@ -0,0 +1,6 @@
1
+ export type StringWithSuggestions<S extends string> = S | (string & {});
2
+ /**
3
+ * Aliases of all languages that are supported by [libprisma](https://github.com/TelegramMessenger/libprisma)
4
+ * (library that Telegram uses for highlighting syntax of code blocks).
5
+ */
6
+ export type PrismLanguage = 'markup' | 'html' | 'xml' | 'svg' | 'mathml' | 'ssml' | 'atom' | 'rss' | 'css' | 'clike' | 'regex' | 'javascript' | 'js' | 'abap' | 'abnf' | 'actionscript' | 'ada' | 'agda' | 'al' | 'antlr4' | 'g4' | 'apacheconf' | 'sql' | 'apex' | 'apl' | 'applescript' | 'aql' | 'c' | 'cpp' | 'arduino' | 'ino' | 'arff' | 'armasm' | 'arm-asm' | 'bash' | 'sh' | 'shell' | 'yaml' | 'yml' | 'markdown' | 'md' | 'arturo' | 'art' | 'asciidoc' | 'adoc' | 'csharp' | 'cs' | 'dotnet' | 'aspnet' | 'asm6502' | 'asmatmel' | 'autohotkey' | 'autoit' | 'avisynth' | 'avs' | 'avro-idl' | 'avdl' | 'awk' | 'gawk' | 'basic' | 'batch' | 'bbcode' | 'shortcode' | 'bbj' | 'bicep' | 'birb' | 'bison' | 'bnf' | 'rbnf' | 'bqn' | 'brainfuck' | 'brightscript' | 'bro' | 'cfscript' | 'cfc' | 'chaiscript' | 'cil' | 'cilkc' | 'cilk-c' | 'cilkcpp' | 'cilk-cpp' | 'cilk' | 'clojure' | 'cmake' | 'cobol' | 'coffeescript' | 'coffee' | 'concurnas' | 'conc' | 'csp' | 'cooklang' | 'ruby' | 'rb' | 'crystal' | 'csv' | 'cue' | 'cypher' | 'd' | 'dart' | 'dataweave' | 'dax' | 'dhall' | 'diff' | 'markup-templating' | 'django' | 'jinja2' | 'dns-zone-file' | 'dns-zone' | 'docker' | 'dockerfile' | 'dot' | 'gv' | 'ebnf' | 'editorconfig' | 'eiffel' | 'ejs' | 'eta' | 'elixir' | 'elm' | 'lua' | 'etlua' | 'erb' | 'erlang' | 'excel-formula' | 'xlsx' | 'xls' | 'fsharp' | 'factor' | 'false' | 'fift' | 'firestore-security-rules' | 'flow' | 'fortran' | 'ftl' | 'func' | 'gml' | 'gamemakerlanguage' | 'gap' | 'gcode' | 'gdscript' | 'gedcom' | 'gettext' | 'po' | 'git' | 'glsl' | 'gn' | 'gni' | 'linker-script' | 'ld' | 'go' | 'go-module' | 'go-mod' | 'gradle' | 'graphql' | 'groovy' | 'less' | 'scss' | 'textile' | 'haml' | 'handlebars' | 'hbs' | 'mustache' | 'haskell' | 'hs' | 'haxe' | 'hcl' | 'hlsl' | 'hoon' | 'hpkp' | 'hsts' | 'json' | 'webmanifest' | 'uri' | 'url' | 'http' | 'ichigojam' | 'icon' | 'icu-message-format' | 'idris' | 'idr' | 'ignore' | 'gitignore' | 'hgignore' | 'npmignore' | 'inform7' | 'ini' | 'io' | 'j' | 'java' | 'scala' | 'php' | 'javadoclike' | 'javadoc' | 'javastacktrace' | 'jolie' | 'jq' | 'typescript' | 'ts' | 'jsdoc' | 'n4js' | 'n4jsd' | 'json5' | 'jsonp' | 'jsstacktrace' | 'julia' | 'keepalived' | 'keyman' | 'kotlin' | 'kt' | 'kts' | 'kusto' | 'latex' | 'tex' | 'context' | 'latte' | 'scheme' | 'lilypond' | 'ly' | 'liquid' | 'lisp' | 'emacs' | 'elisp' | 'emacs-lisp' | 'livescript' | 'llvm' | 'log' | 'lolcode' | 'magma' | 'makefile' | 'mata' | 'matlab' | 'maxscript' | 'mel' | 'mermaid' | 'metafont' | 'mizar' | 'mongodb' | 'monkey' | 'moonscript' | 'moon' | 'n1ql' | 'nand2tetris-hdl' | 'naniscript' | 'nani' | 'nasm' | 'neon' | 'nevod' | 'nginx' | 'nim' | 'nix' | 'nsis' | 'objectivec' | 'objc' | 'ocaml' | 'odin' | 'opencl' | 'openqasm' | 'qasm' | 'oz' | 'parigp' | 'parser' | 'pascal' | 'objectpascal' | 'pascaligo' | 'psl' | 'pcaxis' | 'px' | 'peoplecode' | 'pcode' | 'perl' | 'phpdoc' | 'plant-uml' | 'plantuml' | 'plsql' | 'powerquery' | 'pq' | 'mscript' | 'powershell' | 'processing' | 'prolog' | 'promql' | 'properties' | 'protobuf' | 'stylus' | 'twig' | 'pug' | 'puppet' | 'purebasic' | 'pbfasm' | 'python' | 'py' | 'qsharp' | 'qs' | 'q' | 'qml' | 'qore' | 'r' | 'racket' | 'rkt' | 'cshtml' | 'razor' | 'jsx' | 'tsx' | 'reason' | 'rego' | 'renpy' | 'rpy' | 'rescript' | 'res' | 'rest' | 'rip' | 'roboconf' | 'robotframework' | 'robot' | 'rust' | 'sas' | 'sass' | 'shell-session' | 'sh-session' | 'shellsession' | 'smali' | 'smalltalk' | 'smarty' | 'sml' | 'smlnj' | 'solidity' | 'sol' | 'solution-file' | 'sln' | 'soy' | 'splunk-spl' | 'sqf' | 'squirrel' | 'stan' | 'stata' | 'iecst' | 'supercollider' | 'sclang' | 'swift' | 'systemd' | 'tact' | 't4-templating' | 't4-cs' | 't4' | 'vbnet' | 't4-vb' | 'tap' | 'tcl' | 'tt2' | 'toml' | 'ttcn' | 'ttcn3' | 'ttcn-3' | 'turtle' | 'trickle' | 'typescript-jsdoc' | 'typoscript' | 'tsconfig' | 'unrealscript' | 'uscript' | 'uc' | 'uri' | 'v' | 'vala' | 'vba' | 'vbscript' | 'velocity' | 'verilog' | 'vhdl' | 'vim' | 'visual-basic' | 'vb' | 'warpscript' | 'wasm' | 'web-idl' | 'webidl' | 'wgsl' | 'wiki' | 'wolfram' | 'mathematica' | 'nb' | 'wl' | 'xeora' | 'xeoracube' | 'xml-doc' | 'xojo' | 'xquery' | 'yaml' | 'yml' | 'yang' | 'zig';
@@ -0,0 +1 @@
1
+ export {};
package/dist/html.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { TgxElement } from './types.ts';
2
+ /**
3
+ * Converts {@link TgxElement} to an HTML string formatted for Telegram's
4
+ * HTML parse mode.
5
+ */
6
+ export declare function html(tgx: TgxElement | TgxElement[]): string;
package/dist/html.js ADDED
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Converts {@link TgxElement} to an HTML string formatted for Telegram's
3
+ * HTML parse mode.
4
+ */
5
+ export function html(tgx) {
6
+ return (Array.isArray(tgx) ? tgx : [tgx])
7
+ .map((el) => {
8
+ switch (el.type) {
9
+ case 'plain': return htmlTgxPlainElement(el);
10
+ case 'fragment': return html(el.subelements);
11
+ case 'text': return htmlTgxTextElement(el);
12
+ }
13
+ throw new Error(`Unknown element: ${el}.`);
14
+ })
15
+ .join('');
16
+ }
17
+ function htmlTgxPlainElement({ value }) {
18
+ if (value == null || typeof value === 'boolean')
19
+ return '';
20
+ return sanitize(String(value));
21
+ }
22
+ function htmlTgxTextElement(el) {
23
+ switch (el.entity.type) {
24
+ case 'bold': return `<b>${html(el.subelements)}</b>`;
25
+ case 'italic': return `<i>${html(el.subelements)}</i>`;
26
+ case 'underline': return `<u>${html(el.subelements)}</u>`;
27
+ case 'strikethrough': return `<s>${html(el.subelements)}</s>`;
28
+ case 'spoiler': return `<tg-spoiler>${html(el.subelements)}</tg-spoiler>`;
29
+ // TODO: Shouldn't we urlencode this?
30
+ case 'link': return `<a href="${el.entity.url}">${html(el.subelements)}</a>`;
31
+ case 'custom-emoji': return `<tg-emoji emoji-id="${el.entity.id}">${el.entity.alt}</tg-emoji>`;
32
+ case 'code': return `<code>${html(el.subelements)}</code>`;
33
+ case 'codeblock': return (el.entity.language
34
+ ? `<pre><code class="language-${el.entity.language}">${html(el.subelements)}</code></pre>`
35
+ : `<pre>${html(el.subelements)}</pre>`);
36
+ case 'blockquote': return (el.entity.expandable
37
+ ? `<blockquote expandable>${html(el.subelements)}</blockquote>`
38
+ : `<blockquote>${html(el.subelements)}</blockquote>`);
39
+ }
40
+ }
41
+ function sanitize(unsafe) {
42
+ return unsafe
43
+ .replaceAll('&', '&amp;') // must be first
44
+ .replaceAll('<', '&lt;')
45
+ .replaceAll('>', '&gt;');
46
+ }
@@ -0,0 +1 @@
1
+ export { html } from './html.ts';
package/dist/index.js ADDED
@@ -0,0 +1 @@
1
+ export { html } from "./html.js";
package/dist/jsx.d.ts ADDED
@@ -0,0 +1,6 @@
1
+ import type { FunctionComponent, NativeElements, TgxElement, TgxFragmentElement, TgxNode } from './types.ts';
2
+ export declare function render<T extends keyof NativeElements>(type: T, props: NativeElements[T], children: TgxNode): TgxElement;
3
+ export declare function render(type: FunctionComponent, props: any, children: TgxNode): TgxElement;
4
+ export declare function Fragment(props?: {
5
+ children?: TgxNode;
6
+ }): TgxFragmentElement;
package/dist/jsx.js ADDED
@@ -0,0 +1,108 @@
1
+ export function render(type, props, children) {
2
+ if (typeof type === 'string' && isNativeTag(type)) {
3
+ return renderNativeElement({
4
+ tag: type,
5
+ props: { ...props, children },
6
+ });
7
+ }
8
+ if (typeof type === 'function')
9
+ return type({ ...props, children });
10
+ throw new Error(`Invalid JSX component: ${type}.`);
11
+ }
12
+ export function Fragment(props) {
13
+ return {
14
+ type: 'fragment',
15
+ subelements: elementsFromNode(props?.children ?? []),
16
+ };
17
+ }
18
+ function elementsFromNode(node) {
19
+ switch (typeof node) {
20
+ case 'string':
21
+ case 'number':
22
+ case 'boolean':
23
+ return [{ type: 'plain', value: node }];
24
+ }
25
+ if (node == null)
26
+ return [{ type: 'plain', value: node }];
27
+ if (Array.isArray(node))
28
+ return node.flatMap(child => elementsFromNode(child));
29
+ return [node];
30
+ }
31
+ function isNativeTag(tag) {
32
+ return ([
33
+ 'b',
34
+ 'i',
35
+ 'u',
36
+ 's',
37
+ 'spoiler',
38
+ 'a',
39
+ 'emoji',
40
+ 'code',
41
+ 'codeblock',
42
+ 'blockquote',
43
+ ]).includes(tag);
44
+ }
45
+ function renderNativeElement(options) {
46
+ switch (options.tag) {
47
+ case 'b':
48
+ return {
49
+ type: 'text',
50
+ entity: { type: 'bold' },
51
+ subelements: elementsFromNode(options.props.children ?? []),
52
+ };
53
+ case 'i':
54
+ return {
55
+ type: 'text',
56
+ entity: { type: 'italic' },
57
+ subelements: elementsFromNode(options.props.children ?? []),
58
+ };
59
+ case 'u':
60
+ return {
61
+ type: 'text',
62
+ entity: { type: 'underline' },
63
+ subelements: elementsFromNode(options.props.children ?? []),
64
+ };
65
+ case 's':
66
+ return {
67
+ type: 'text',
68
+ entity: { type: 'strikethrough' },
69
+ subelements: elementsFromNode(options.props.children ?? []),
70
+ };
71
+ case 'spoiler':
72
+ return {
73
+ type: 'text',
74
+ entity: { type: 'spoiler' },
75
+ subelements: elementsFromNode(options.props.children ?? []),
76
+ };
77
+ case 'a':
78
+ return {
79
+ type: 'text',
80
+ entity: { type: 'link', url: options.props.href },
81
+ subelements: elementsFromNode(options.props.children ?? []),
82
+ };
83
+ case 'emoji':
84
+ return {
85
+ type: 'text',
86
+ entity: { type: 'custom-emoji', id: options.props.id, alt: options.props.alt },
87
+ subelements: [],
88
+ };
89
+ case 'code':
90
+ return {
91
+ type: 'text',
92
+ entity: { type: 'code' },
93
+ subelements: elementsFromNode(options.props.children ?? []),
94
+ };
95
+ case 'codeblock':
96
+ return {
97
+ type: 'text',
98
+ entity: { type: 'codeblock', language: options.props.lang },
99
+ subelements: elementsFromNode(options.props.children ?? []),
100
+ };
101
+ case 'blockquote':
102
+ return {
103
+ type: 'text',
104
+ entity: { type: 'blockquote', expandable: !!options.props.expandable },
105
+ subelements: elementsFromNode(options.props.children ?? []),
106
+ };
107
+ }
108
+ }
@@ -0,0 +1,18 @@
1
+ import type { FunctionComponent, NativeElements, TgxElement } from './types.ts';
2
+ import { Fragment } from './jsx.ts';
3
+ declare function jsx(type: any, props: any, key: any): any;
4
+ export { Fragment, jsx, jsx as jsxDEV, jsx as jsxs, };
5
+ export declare namespace JSX {
6
+ type Element = TgxElement;
7
+ type ElementType = keyof NativeElements | FunctionComponent;
8
+ interface ElementAttributesProperty {
9
+ props: {};
10
+ }
11
+ interface ElementChildrenAttribute {
12
+ children: {};
13
+ }
14
+ interface IntrinsicElements extends NativeElements {
15
+ }
16
+ interface IntrinsicAttributes {
17
+ }
18
+ }
@@ -0,0 +1,10 @@
1
+ import { Fragment, render } from "./jsx.js";
2
+ function jsx(type, props, key) {
3
+ const { children } = props;
4
+ delete props.children;
5
+ if (arguments.length > 2) {
6
+ props.key = key;
7
+ }
8
+ return render(type, props, children);
9
+ }
10
+ export { Fragment, jsx, jsx as jsxDEV, jsx as jsxs, };
@@ -0,0 +1,120 @@
1
+ import type { PrismLanguage, StringWithSuggestions } from './_utils/types.ts';
2
+ export interface NativeElements {
3
+ /**
4
+ * Bold text.
5
+ */
6
+ b: PropsWithChildren;
7
+ /**
8
+ * Italic text.
9
+ */
10
+ i: PropsWithChildren;
11
+ /**
12
+ * Underlined text.
13
+ */
14
+ u: PropsWithChildren;
15
+ /**
16
+ * Strikethrough text.
17
+ */
18
+ s: PropsWithChildren;
19
+ /**
20
+ * Spoiler.
21
+ */
22
+ spoiler: PropsWithChildren;
23
+ /**
24
+ * Inline URL or Telegram (deep) link.
25
+ *
26
+ * Read more about Telegram deep links:
27
+ * https://core.telegram.org/api/links
28
+ */
29
+ a: PropsWithChildren<{
30
+ /**
31
+ * Link to open.
32
+ *
33
+ * @example "https://google.com"
34
+ * @example "tg://resolve?domain=BotFather"
35
+ */
36
+ href: string;
37
+ }>;
38
+ /**
39
+ * Custom Telegram emoji.
40
+ */
41
+ emoji: {
42
+ /**
43
+ * Unique identifier of the custom emoji.
44
+ */
45
+ id: string;
46
+ /**
47
+ * Alternative emoji that will be shown instead of the custom emoji in
48
+ * places where a custom emoji cannot be displayed.
49
+ */
50
+ alt: string;
51
+ };
52
+ /**
53
+ * Inline fixed-width code.
54
+ */
55
+ code: PropsWithChildren;
56
+ /**
57
+ * Fixed-width code block with the optional programming language.
58
+ */
59
+ codeblock: PropsWithChildren<{
60
+ /**
61
+ * Programming or markup language of the block.
62
+ *
63
+ * Telegram uses libprisma for code highlighting,
64
+ * so the following languages are supported:
65
+ * https://github.com/TelegramMessenger/libprisma#supported-languages
66
+ */
67
+ lang?: StringWithSuggestions<PrismLanguage>;
68
+ }>;
69
+ /**
70
+ * Block quotation. Can be expandable or not.
71
+ */
72
+ blockquote: PropsWithChildren<{
73
+ expandable?: boolean;
74
+ }>;
75
+ }
76
+ export type TgxNode = TgxNode[] | TgxElement | string | number | boolean | null | undefined;
77
+ export type PropsWithChildren<P = {}> = {
78
+ children?: TgxNode;
79
+ } & P;
80
+ export type FunctionComponent = (props: any) => TgxElement;
81
+ export type TgxElement = TgxPlainValueElement | TgxFragmentElement | TgxTextElement;
82
+ export interface TgxPlainValueElement {
83
+ type: 'plain';
84
+ value: string | number | boolean | null | undefined;
85
+ }
86
+ export interface TgxFragmentElement {
87
+ type: 'fragment';
88
+ subelements: TgxElement[];
89
+ }
90
+ export interface TgxTextElement {
91
+ type: 'text';
92
+ entity: TextEntity;
93
+ subelements: TgxElement[];
94
+ }
95
+ export type TextEntity = {
96
+ type: 'bold';
97
+ } | {
98
+ type: 'italic';
99
+ } | {
100
+ type: 'underline';
101
+ } | {
102
+ type: 'strikethrough';
103
+ } | {
104
+ type: 'spoiler';
105
+ } | {
106
+ type: 'link';
107
+ url: string;
108
+ } | {
109
+ type: 'custom-emoji';
110
+ id: string;
111
+ alt: string;
112
+ } | {
113
+ type: 'code';
114
+ } | {
115
+ type: 'codeblock';
116
+ language?: string;
117
+ } | {
118
+ type: 'blockquote';
119
+ expandable: boolean;
120
+ };
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,58 @@
1
+ {
2
+ "name": "@grom.js/tgx",
3
+ "type": "module",
4
+ "version": "0.5.0",
5
+ "description": "JSX runtime for composing Telegram messages",
6
+ "author": {
7
+ "name": "Vladislav Deryabkin",
8
+ "url": "https://evermake.me"
9
+ },
10
+ "license": "MIT",
11
+ "keywords": [
12
+ "jsx",
13
+ "telegram"
14
+ ],
15
+ "exports": {
16
+ ".": {
17
+ "types": "./dist/index.d.ts",
18
+ "import": "./dist/index.js"
19
+ },
20
+ "./types": {
21
+ "types": "./dist/types.d.ts"
22
+ },
23
+ "./jsx-runtime": {
24
+ "types": "./dist/jsx-runtime.d.ts",
25
+ "import": "./dist/jsx-runtime.js"
26
+ },
27
+ "./jsx-dev-runtime": {
28
+ "types": "./dist/jsx-dev-runtime.d.ts",
29
+ "import": "./dist/jsx-dev-runtime.js"
30
+ }
31
+ },
32
+ "files": [
33
+ "./dist/"
34
+ ],
35
+ "engines": {
36
+ "node": "22.x"
37
+ },
38
+ "devDependencies": {
39
+ "@antfu/eslint-config": "6.2.0",
40
+ "@types/node": "22.19.1",
41
+ "@vitest/coverage-v8": "4.0.10",
42
+ "bumpp": "10.3.1",
43
+ "eslint": "9.39.1",
44
+ "eslint-plugin-format": "1.0.2",
45
+ "taze": "19.9.0",
46
+ "vitest": "4.0.10"
47
+ },
48
+ "scripts": {
49
+ "build": "rm -rf ./dist/ && tsc --project ./tsconfig.lib.json",
50
+ "typecheck": "tsc --build --noEmit",
51
+ "lint": "eslint",
52
+ "lint:fix": "eslint --fix",
53
+ "test": "vitest --config ./vitest.config.ts",
54
+ "test:coverage": "vitest run --config ./vitest.config.ts --coverage",
55
+ "deps": "taze --interactive --write --include-locked",
56
+ "release": "bumpp"
57
+ }
58
+ }