Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Major] Make fmt use intuitive #17

Draft
wants to merge 9 commits into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
{
"deno.enable": true,
"deno.lint": true,
"deno.unstable": true
KnightNiwrem marked this conversation as resolved.
Show resolved Hide resolved
}
48 changes: 13 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,31 @@
# Parse Mode plugin for grammY

This plugin provides a transformer for setting default `parse_mode`, and a middleware for hydrating `Context` with familiar `reply` variant methods - i.e. `replyWithHTML`, `replyWithMarkdown`, etc.
This plugin provides transformer that injects `entities` or `caption_entities`
if `text` or `caption` are FormattedString. It also provides a middleware that
installs this transformer on the `ctx.api` object.

## Usage (Using format)

```ts
import { Bot, Composer, Context } from 'grammy';
import { Bot, Context } from 'grammy';
import { bold, fmt, hydrateReply, italic } from '@grammyjs/parse-mode';
import type { ParseModeFlavor } from '@grammyjs/parse-mode';

const bot = new Bot<ParseModeFlavor<Context>>('');

// Install format reply variant to ctx
bot.use(hydrateReply);
// Install automatic entities inject from FormattedString transformer
bot.use(hydrateReply());

bot.command('demo', async ctx => {
await ctx.replyFmt(fmt`${bold('bold!')}
${bold(italic('bitalic!'))}
${bold(fmt`bold ${link('blink', 'example.com')} bold`)}`);
const boldText = fmt`This is a ${bold('bolded')} string`;
await ctx.reply(boldText);

// fmt can also be called like a regular function
await ctx.replyFmt(fmt(['', ' and ', ' and ', ''], fmt`${bold('bold')}`, fmt`${bold(italic('bitalic'))}`, fmt`${italic('italic')}`));
});

bot.start();
```

## Usage (Using default parse mode and utility reply methods)

```ts
import { Bot, Composer, Context } from 'grammy';
import { hydrateReply, parseMode } from '@grammyjs/parse-mode';
const underlineText = fmt`This is an ${underline('underlined')}`;
await ctx.reply(underlineText);

import type { ParseModeFlavor } from '@grammyjs/parse-mode';

const bot = new Bot<ParseModeFlavor<Context>>('');

// Install familiar reply variants to ctx
bot.use(hydrateReply);

// Sets default parse_mode for ctx.reply
bot.api.config.use(parseMode('MarkdownV2'));

bot.command('demo', async ctx => {
await ctx.reply('*This* is _the_ default `formatting`');
await ctx.replyWithHTML('<b>This</b> is <i>withHTML</i> <code>formatting</code>');
await ctx.replyWithMarkdown('*This* is _withMarkdown_ `formatting`');
await ctx.replyWithMarkdownV1('*This* is _withMarkdownV1_ `formatting`');
await ctx.replyWithMarkdownV2('*This* is _withMarkdownV2_ `formatting`');
// fmt can also be use to concat FormattedStrings
cosnt combinedText = fmt`${boldText}\n${underlineText}`
await ctx.reply(combinedText);
});

bot.start();
Expand Down
8 changes: 4 additions & 4 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,11 @@
"build": "deno2node tsconfig.json"
},
"devDependencies": {
"@grammyjs/types": "^3.0.3",
"@grammyjs/types": "^3.1.2",
KnightNiwrem marked this conversation as resolved.
Show resolved Hide resolved
"@tsconfig/node16": "^1.0.2",
"@types/node": "^16.6.1",
"deno2node": "^1.8.1",
"grammy": "^1.15.3"
"@types/node": "^20.4.7",
"deno2node": "^1.9.0",
"grammy": "^1.17.2"
},
"files": [
"dist/"
Expand Down
48 changes: 13 additions & 35 deletions src/README.md
Original file line number Diff line number Diff line change
@@ -1,53 +1,31 @@
# Parse Mode plugin for grammY

This plugin provides a transformer for setting default `parse_mode`, and a middleware for hydrating `Context` with familiar `reply` variant methods - i.e. `replyWithHTML`, `replyWithMarkdown`, etc.
This plugin provides transformer that injects `entities` or `caption_entities`
if `text` or `caption` are FormattedString. It also provides a middleware that
installs this transformer on the `ctx.api` object.

## Usage (Using format)

```ts
import { Bot, Composer, Context } from 'grammy';
import { Bot, Context } from 'grammy';
import { bold, fmt, hydrateReply, italic } from '@grammyjs/parse-mode';
import type { ParseModeFlavor } from '@grammyjs/parse-mode';

const bot = new Bot<ParseModeFlavor<Context>>('');

// Install format reply variant to ctx
bot.use(hydrateReply);
// Install automatic entities inject from FormattedString transformer
bot.use(hydrateReply());

bot.command('demo', async ctx => {
await ctx.replyFmt(fmt`${bold('bold!')}
${bold(italic('bitalic!'))}
${bold(fmt`bold ${link('blink', 'example.com')} bold`)}`);
const boldText = fmt`This is a ${bold('bolded')} string`;
await ctx.reply(boldText);

// fmt can also be called like a regular function
await ctx.replyFmt(fmt(['', ' and ', ' and ', ''], fmt`${bold('bold')}`, fmt`${bold(italic('bitalic'))}`, fmt`${italic('italic')}`));
});

bot.start();
```

## Usage (Using default parse mode and utility reply methods)

```ts
import { Bot, Composer, Context } from 'grammy';
import { hydrateReply, parseMode } from '@grammyjs/parse-mode';
const underlineText = fmt`This is an ${underline('underlined')}`;
await ctx.reply(underlineText);

import type { ParseModeFlavor } from '@grammyjs/parse-mode';

const bot = new Bot<ParseModeFlavor<Context>>('');

// Install familiar reply variants to ctx
bot.use(hydrateReply);

// Sets default parse_mode for ctx.reply
bot.api.config.use(parseMode('MarkdownV2'));

bot.command('demo', async ctx => {
await ctx.reply('*This* is _the_ default `formatting`');
await ctx.replyWithHTML('<b>This</b> is <i>withHTML</i> <code>formatting</code>');
await ctx.replyWithMarkdown('*This* is _withMarkdown_ `formatting`');
await ctx.replyWithMarkdownV1('*This* is _withMarkdownV1_ `formatting`');
await ctx.replyWithMarkdownV2('*This* is _withMarkdownV2_ `formatting`');
// fmt can also be use to concat FormattedStrings
cosnt combinedText = fmt`${boldText}\n${underlineText}`
await ctx.reply(combinedText);
});

bot.start();
Expand Down
35 changes: 23 additions & 12 deletions src/format.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import type { MessageEntity } from "./deps.deno.ts";

/**
* Objects that implement this interface implement a `.toString()`
* Objects that implement this interface implement a `.toString()`
* method that returns a `string` value representing the object.
*/
export interface Stringable {
Expand All @@ -26,11 +26,11 @@ class FormattedString implements Stringable {
entities: MessageEntity[];

/**
* Creates a new `FormattedString`. Useful for constructing a
* Creates a new `FormattedString`. Useful for constructing a
* `FormattedString` from user's formatted message
* @param text Plain text value
* @param entities Format entities
*
*
* ```ts
* // Constructing a new `FormattedString` from user's message
* const userMsg = new FormattedString(ctx.message.text, ctx.entities());
Expand Down Expand Up @@ -147,34 +147,45 @@ const customEmoji = (placeholder: Stringable, emoji: number) => {
* @param chatId The chat ID to link to.
* @param messageId The message ID to link to.
*/
const linkMessage = (stringLike: Stringable, chatId: number, messageId: number) => {
const linkMessage = (
stringLike: Stringable,
chatId: number,
messageId: number,
) => {
if (chatId > 0) {
console.warn("linkMessage can only be used for supergroups and channel messages. Refusing to transform into link.");
console.warn(
"linkMessage can only be used for supergroups and channel messages. Refusing to transform into link.",
);
return stringLike;
} else if (chatId < -1002147483647 || chatId > -1000000000000) {
console.warn("linkMessage is not able to link messages whose chatIds are greater than -1000000000000 or less than -1002147483647 at this moment. Refusing to transform into link.");
console.warn(
"linkMessage is not able to link messages whose chatIds are greater than -1000000000000 or less than -1002147483647 at this moment. Refusing to transform into link.",
);
return stringLike;
} else {
return link(stringLike, `https://t.me/c/${(chatId + 1000000000000) * -1}/${messageId}`);
return link(
stringLike,
`https://t.me/c/${(chatId + 1000000000000) * -1}/${messageId}`,
);
}
};

// === Format tagged template function

/**
* This is the format tagged template function. It accepts a template literal
* containing any mix of `Stringable` and `string` values, and constructs a
* This is the format tagged template function. It accepts a template literal
* containing any mix of `Stringable` and `string` values, and constructs a
* `FormattedString` that represents the combination of all the given values.
* The constructed `FormattedString` also implements Stringable, and can be used
* The constructed `FormattedString` also implements Stringable, and can be used
* in further `fmt` tagged templates.
* @param rawStringParts An array of `string` parts found in the tagged template
* @param stringLikes An array of `Stringable`s found in the tagged template
*
*
* ```ts
* // Using return values of fmt in fmt
* const left = fmt`${bold('>>>')} >>>`;
* const right = fmt`<<< ${bold('<<<')}`;
*
*
* const final = fmt`${left} ${ctx.msg.text} ${right}`;
* await ctx.replyFmt(final);
* ```
Expand Down
130 changes: 75 additions & 55 deletions src/hydrate.ts
Original file line number Diff line number Diff line change
@@ -1,75 +1,95 @@
import type {
Context,
MessageEntity,
NextFunction,
ParseMode,
} from "./deps.deno.ts";
import { FormattedString, type Stringable } from "./format.ts";
import type { Context, NextFunction } from "./deps.deno.ts";

type Tail<T extends Array<any>> = T extends [head: infer E1, ...tail: infer E2]
? E2
import { FormattedString } from "./format.ts";
import { parseMode } from "./transformer.ts";

type Head<T extends Array<unknown>> = T extends
[head: infer E1, ...tail: infer E2] ? E1
: never;

type Tail<T extends Array<unknown>> = T extends
[head: infer E1, ...tail: infer E2] ? E2
: [];

/**
* Context flavor for `Context` that will be hydrated with
* Context flavor for `Context` that will be hydrated with
* an additional set of reply methods from `hydrateReply`
*/
type ParseModeFlavor<C extends Context> = C & {
replyFmt: (
stringLike: Stringable,
editMessageCaption: (
other?: Head<Parameters<C["editMessageCaption"]>> & {
caption?: FormattedString;
},
...args: Tail<Parameters<C["editMessageText"]>>
) => ReturnType<C["editMessageCaption"]>;
editMessageMedia: (
media: { caption?: FormattedString },
...args: Tail<Parameters<C["editMessageMedia"]>>
) => ReturnType<C["editMessageMedia"]>;
editMessageText: (
text: FormattedString,
...args: Tail<Parameters<C["editMessageText"]>>
) => ReturnType<C["editMessageText"]>;
reply: (
text: FormattedString,
...args: Tail<Parameters<C["reply"]>>
) => ReturnType<C["reply"]>;
replyWithHTML: C["reply"];
replyWithMarkdown: C["reply"];
replyWithMarkdownV1: C["reply"];
replyWithMarkdownV2: C["reply"];
};

/**
* @deprecated Use ParseModeFlavor instead of ParseModeContext
*/
type ParseModeContext<C extends Context = Context> = ParseModeFlavor<C>;

const buildReplyWithParseMode = <C extends Context>(
parseMode: ParseMode,
ctx: ParseModeFlavor<C>,
) => {
return (...args: Parameters<C["reply"]>) => {
const [text, payload, ...rest] = args;
return ctx.reply(
text,
{ ...payload, parse_mode: parseMode },
...rest as any,
);
};
replyWithAnimation: (
animation: Head<Parameters<C["replyWithAnimation"]>>,
other?: Head<Tail<Parameters<C["replyWithAnimation"]>>> & {
caption?: FormattedString;
},
...args: Tail<Tail<Parameters<C["replyWithAnimation"]>>>
) => ReturnType<C["replyWithAnimation"]>;
replyWithAudio: (
audio: Head<Parameters<C["replyWithAudio"]>>,
other?: Head<Tail<Parameters<C["replyWithAudio"]>>> & {
caption?: FormattedString;
},
...args: Tail<Tail<Parameters<C["replyWithAudio"]>>>
) => ReturnType<C["replyWithAudio"]>;
replyWithDocument: (
document: Head<Parameters<C["replyWithDocument"]>>,
other?: Head<Tail<Parameters<C["replyWithDocument"]>>> & {
caption?: FormattedString;
},
...args: Tail<Tail<Parameters<C["replyWithDocument"]>>>
) => ReturnType<C["replyWithDocument"]>;
replyWithPhoto: (
photo: Head<Parameters<C["replyWithPhoto"]>>,
other?: Head<Tail<Parameters<C["replyWithPhoto"]>>> & {
caption?: FormattedString;
},
...args: Tail<Tail<Parameters<C["replyWithPhoto"]>>>
) => ReturnType<C["replyWithPhoto"]>;
replyWithVideo: (
photo: Head<Parameters<C["replyWithVideo"]>>,
other?: Head<Tail<Parameters<C["replyWithVideo"]>>> & {
caption?: FormattedString;
},
...args: Tail<Tail<Parameters<C["replyWithVideo"]>>>
) => ReturnType<C["replyWithVideo"]>;
replyWithVoice: (
photo: Head<Parameters<C["replyWithVoice"]>>,
other?: Head<Tail<Parameters<C["replyWithVoice"]>>> & {
caption?: FormattedString;
},
...args: Tail<Tail<Parameters<C["replyWithVoice"]>>>
) => ReturnType<C["replyWithVoice"]>;
};

/**
* Hydrates a context with an additional set of reply methods
* Hydrates a context with new reply method overloads
* @param ctx The context to hydrate
* @param next The next middleware function
*/
const middleware = async <C extends Context>(
const middleware = () =>
async <C extends Context>(
ctx: ParseModeFlavor<C>,
next: NextFunction,
) => {
ctx.replyFmt = (stringLike, ...args) => {
const [payload, ...rest] = args;
const entities = stringLike instanceof FormattedString
? { entities: stringLike.entities }
: undefined;
return ctx.reply(
stringLike.toString(),
{ ...payload, ...entities },
...rest as any,
) as ReturnType<C['reply']>;
};

ctx.replyWithHTML = buildReplyWithParseMode("HTML", ctx);
ctx.replyWithMarkdown = buildReplyWithParseMode("MarkdownV2", ctx);
ctx.replyWithMarkdownV1 = buildReplyWithParseMode("Markdown", ctx);
ctx.replyWithMarkdownV2 = buildReplyWithParseMode("MarkdownV2", ctx);
return next();
ctx.api.config.use(parseMode());
await next();
};

export { middleware as hydrateReply, type ParseModeFlavor, type ParseModeContext };
export { middleware as hydrateReply, type ParseModeFlavor };
Loading