diff --git a/.github/dependabot.yml b/.github/dependabot.yml deleted file mode 100644 index 7f8c9fa..0000000 --- a/.github/dependabot.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Please see the documentation for all configuration options: -# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates - -version: 2 -updates: - - package-ecosystem: "npm" - directory: "/" - open-pull-requests-limit: 30 - schedule: - interval: "weekly" - day: "saturday" - time: "02:42" # UTC - commit-message: - prefix: "build(npm):" diff --git a/.github/workflows/nodejs.yml b/.github/workflows/deno.yml similarity index 53% rename from .github/workflows/nodejs.yml rename to .github/workflows/deno.yml index 8936899..d805f91 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/deno.yml @@ -1,44 +1,60 @@ -name: Node.js +name: Deno on: push: pull_request: schedule: - # Check if it works with current dependencies + # Check if it works with current versions - cron: '42 2 * * 6' # weekly on Saturday 2:42 UTC jobs: + denofmt-and-lint: + runs-on: ubuntu-latest + steps: + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x + - uses: actions/checkout@v4 + + - run: deno lint + - run: deno fmt --check + test: - name: Node.js runs-on: ubuntu-latest steps: - - uses: actions/setup-node@v4 + - uses: denoland/setup-deno@v1 with: - node-version: 'lts/*' + deno-version: v1.x - uses: actions/checkout@v4 - - run: npm install - - run: npm test - - run: npm pack - publish: + - run: deno cache source/*.ts + - run: deno check source/*.ts + - run: deno test + + npm: name: Publish on NPM - if: startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest - needs: test + needs: [denofmt-and-lint, test] permissions: contents: write id-token: write steps: - uses: actions/checkout@v4 + - uses: denoland/setup-deno@v1 + with: + deno-version: v1.x - uses: actions/setup-node@v4 with: node-version: 'lts/*' registry-url: 'https://registry.npmjs.org' - run: npm install + - run: npm pack - run: npm publish + if: startsWith(github.ref, 'refs/tags/v') env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} NPM_CONFIG_ACCESS: public NPM_CONFIG_PROVENANCE: true - name: Create GitHub release + if: startsWith(github.ref, 'refs/tags/v') uses: softprops/action-gh-release@v1 diff --git a/.gitignore b/.gitignore index 3148a6d..7866d79 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,3 @@ /*-*.*.*.tgz -coverage dist node_modules diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000..43df84f --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "deno.enable": true +} diff --git a/README.md b/README.md index 8a4bcdc..35c3f8e 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,9 @@ > Format Telegram message texts with Markdown or HTML -This library abstracts the [formatting options](https://core.telegram.org/bots/api#formatting-options) for you. +This library abstracts the +[formatting options](https://core.telegram.org/bots/api#formatting-options) for +you. ## Install @@ -16,44 +18,48 @@ npm install telegram-format ## Usage ```js -import {html as format} from 'telegram-format'; -import {markdownv2 as format} from 'telegram-format'; +import { html as format } from "telegram-format"; +import { markdownv2 as format } from "telegram-format"; -format.bold('hey'); +format.bold("hey"); //=> '*hey*' -format.italic('you'); +format.italic("you"); //=> '_you_' -format.bold(format.italic('they')) +format.bold(format.italic("they")); //=> '*_they_*' -format.url('me', 'https://edjopato.de'); +format.url("me", "https://edjopato.de"); //=> '[me](https://edjopato.de)' // Legacy but still works -import {markdown as format} from 'telegram-format'; +import { markdown as format } from "telegram-format"; ``` -When using `format` as an alias its easy to switch between Markdown and HTML fast. +When using `format` as an alias its easy to switch between Markdown and HTML +fast. ### Escaping Input -When you have something that might be unescaped you need to use `format.escape` before formatting it. +When you have something that might be unescaped you need to use `format.escape` +before formatting it. ```js -const username = 'master_yoda' -format.italic(format.escape(username)) +const username = "master_yoda"; +format.italic(format.escape(username)); ``` -`format.monospace` and `format.monospaceBlock` will escape on their own as they only need to escape specific characters. -You do not need to escape the input in this cases. +`format.monospace` and `format.monospaceBlock` will escape on their own as they +only need to escape specific characters. You do not need to escape the input in +this cases. -`MarkdownV2` and `HTML` are fairly similar in escaping inputs but `Markdown` is not. -`Markdown` is still supported by this library and by Telegram for legacy reasons but it behaves a bit differently. -`Markdown` still escapes inputs and does not need `format.escape` before. -Also nested formatting is not supported by telegram itself. -Consider switching to `MarkdownV2` or `HTML` for simplicity reasons. +`MarkdownV2` and `HTML` are fairly similar in escaping inputs but `Markdown` is +not. `Markdown` is still supported by this library and by Telegram for legacy +reasons but it behaves a bit differently. `Markdown` still escapes inputs and +does not need `format.escape` before. Also nested formatting is not supported by +telegram itself. Consider switching to `MarkdownV2` or `HTML` for simplicity +reasons. ## API diff --git a/deno.jsonc b/deno.jsonc new file mode 100644 index 0000000..de48e40 --- /dev/null +++ b/deno.jsonc @@ -0,0 +1,10 @@ +{ + "lock": false, + "fmt": { + "useTabs": true + }, + "exclude": [ + "./dist/", + "./node_modules/" + ] +} diff --git a/package.json b/package.json index f6b2a54..395968d 100644 --- a/package.json +++ b/package.json @@ -17,44 +17,19 @@ "url": "https://edjopato.de" }, "scripts": { - "build": "del-cli dist && tsc", - "prepack": "npm run build", - "test": "tsc --sourceMap && xo && c8 --all ava" + "prepare": "deno2node", + "test": "echo use deno test && exit 1" }, "type": "module", "engines": { "node": ">=14" }, "devDependencies": { - "@sindresorhus/tsconfig": "^5.0.0", - "ava": "^5.0.1", - "c8": "^8.0.1", - "del-cli": "^5.0.0", - "typescript": "^5.0.2", - "xo": "^0.56.0" + "deno2node": "1.10.1" }, "files": [ - "dist", - "!*.test.*" + "dist" ], - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "xo": { - "rules": { - "@typescript-eslint/naming-convention": "off", - "@typescript-eslint/prefer-readonly-parameter-types": "error", - "unicorn/prefer-string-replace-all": "off", - "ava/no-ignored-test-files": "off" - }, - "overrides": [ - { - "files": [ - "**/*.test.*" - ], - "rules": { - "@typescript-eslint/prefer-readonly-parameter-types": "off" - } - } - ] - } + "main": "./dist/mod.js", + "types": "./dist/mod.d.ts" } diff --git a/source/html.test.ts b/source/html.test.ts index 0cc3301..9261260 100644 --- a/source/html.test.ts +++ b/source/html.test.ts @@ -1,67 +1,70 @@ -import test from 'ava'; -import {html as format} from './html.js'; +import { assertEquals } from "https://deno.land/std@0.206.0/assert/mod.ts"; +import { html as format } from "./html.ts"; -test('bold', t => { - t.is(format.bold('bold'), 'bold'); +Deno.test("bold", () => { + assertEquals(format.bold("bold"), "bold"); }); -test('italic', t => { - t.is(format.italic('italic'), 'italic'); +Deno.test("italic", () => { + assertEquals(format.italic("italic"), "italic"); }); -test('strikethrough', t => { - t.is(format.strikethrough('strikethrough'), 'strikethrough'); +Deno.test("strikethrough", () => { + assertEquals(format.strikethrough("strikethrough"), "strikethrough"); }); -test('underline', t => { - t.is(format.underline('underline'), 'underline'); +Deno.test("underline", () => { + assertEquals(format.underline("underline"), "underline"); }); -test('spoiler', t => { - t.is(format.spoiler('spoiler'), 'spoiler'); +Deno.test("spoiler", () => { + assertEquals( + format.spoiler("spoiler"), + 'spoiler', + ); }); -test('url', t => { - t.is( - format.url('me', 'https://edjopato.de'), +Deno.test("url", () => { + assertEquals( + format.url("me", "https://edjopato.de"), 'me', ); }); -test('escape', t => { - t.is(format.escape('hy'), 'h<e>y'); +Deno.test("escape", () => { + assertEquals(format.escape("hy"), "h<e>y"); }); -test('bold italic', t => { - t.is(format.bold(format.italic('green')), 'green'); +Deno.test("bold italic", () => { + assertEquals(format.bold(format.italic("green")), "green"); }); -test('user mention', t => { - t.is( - format.userMention('inline mention of a user', 123_456_789), +Deno.test("user mention", () => { + assertEquals( + format.userMention("inline mention of a user", 123_456_789), 'inline mention of a user', ); }); -test('monospace', t => { - t.is( - format.monospace('inline fixed-width code'), - 'inline fixed-width code', +Deno.test("monospace", () => { + assertEquals( + format.monospace("inline fixed-width code"), + "inline fixed-width code", ); }); -test('monospaceBlock w/o language', t => { - t.is( - format.monospaceBlock('pre-formatted fixed-width code block'), - '
pre-formatted fixed-width code block
', +Deno.test("monospaceBlock w/o language", () => { + assertEquals( + format.monospaceBlock("pre-formatted fixed-width code block"), + "
pre-formatted fixed-width code block
", ); }); -test('monospaceBlock w/ language', t => { - t.is( +Deno.test("monospaceBlock w/ language", () => { + assertEquals( format.monospaceBlock( - 'pre-formatted fixed-width code block written in the Python programming language', - 'python', + "pre-formatted fixed-width code block written in the Python programming language", + "python", ), '
pre-formatted fixed-width code block written in the Python programming language
', ); diff --git a/source/html.ts b/source/html.ts index 174fe64..4206f36 100644 --- a/source/html.ts +++ b/source/html.ts @@ -1,10 +1,10 @@ -import type {Formatter} from './types.js'; +import type { Formatter } from "./types.ts"; function escape(text: string): string { return text - .replace(/&/g, '&') - .replace(//g, '>'); + .replace(/&/g, "&") + .replace(//g, ">"); } function bold(text: string): string { @@ -33,7 +33,9 @@ function monospace(text: string): string { function monospaceBlock(text: string, programmingLanguage?: string): string { if (programmingLanguage) { - return `
${escape(text)}
`; + return `
${
+			escape(text)
+		}
`; } return `
${escape(text)}
`; @@ -49,7 +51,7 @@ function userMention(label: string, userId: number): string { /** https://core.telegram.org/bots/api#html-style */ export const html = { - parse_mode: 'HTML', + parse_mode: "HTML", escape, bold, italic, diff --git a/source/index.ts b/source/index.ts deleted file mode 100644 index 41ea91b..0000000 --- a/source/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export type {Formatter} from './types.js'; - -export {html} from './html.js'; -export {markdown} from './markdown.js'; -export {markdownv2} from './markdownv2.js'; diff --git a/source/markdown.test.ts b/source/markdown.test.ts index 55a5630..1ea26d6 100644 --- a/source/markdown.test.ts +++ b/source/markdown.test.ts @@ -1,75 +1,75 @@ -import test from 'ava'; -import {markdown as format} from './markdown.js'; +import { + assertEquals, + assertThrows, +} from "https://deno.land/std@0.206.0/assert/mod.ts"; +import { markdown as format } from "./markdown.ts"; -test('bold', t => { - t.is(format.bold('bold'), '*bold*'); +Deno.test("bold", () => { + assertEquals(format.bold("bold"), "*bold*"); }); -test('italic', t => { - t.is(format.italic('italic'), '_italic_'); +Deno.test("italic", () => { + assertEquals(format.italic("italic"), "_italic_"); }); -test('url', t => { - t.is(format.url('me', 'https://edjopato.de'), '[me](https://edjopato.de)'); +Deno.test("url", () => { + assertEquals( + format.url("me", "https://edjopato.de"), + "[me](https://edjopato.de)", + ); }); -test('escape', t => { - t.is(format.escape('[h_]e(*y)`'), 'hey'); +Deno.test("escape", () => { + assertEquals(format.escape("[h_]e(*y)`"), "hey"); }); -test('bold malicious', t => { - t.is(format.bold('bo*ld'), '*bold*'); +Deno.test("bold malicious", () => { + assertEquals(format.bold("bo*ld"), "*bold*"); }); -test('italic malicious', t => { - t.is(format.italic('ita_lic'), '_italic_'); +Deno.test("italic malicious", () => { + assertEquals(format.italic("ita_lic"), "_italic_"); }); -test('user mention', t => { - t.is( - format.userMention('inline mention of a user', 123_456_789), - '[inline mention of a user](tg://user?id=123456789)', +Deno.test("user mention", () => { + assertEquals( + format.userMention("inline mention of a user", 123_456_789), + "[inline mention of a user](tg://user?id=123456789)", ); }); -test('monospace', t => { - t.is( - format.monospace('inline fixed-width code'), - '`inline fixed-width code`', +Deno.test("monospace", () => { + assertEquals( + format.monospace("inline fixed-width code"), + "`inline fixed-width code`", ); }); -test('monospaceBlock w/o language', t => { - t.is( - format.monospaceBlock('pre-formatted fixed-width code block'), - '```\npre-formatted fixed-width code block\n```', +Deno.test("monospaceBlock w/o language", () => { + assertEquals( + format.monospaceBlock("pre-formatted fixed-width code block"), + "```\npre-formatted fixed-width code block\n```", ); }); -test('monospaceBlock w/ language', t => { - t.is( +Deno.test("monospaceBlock w/ language", () => { + assertEquals( format.monospaceBlock( - 'pre-formatted fixed-width code block written in the Python programming language', - 'python', + "pre-formatted fixed-width code block written in the Python programming language", + "python", ), - '```python\npre-formatted fixed-width code block written in the Python programming language\n```', + "```python\npre-formatted fixed-width code block written in the Python programming language\n```", ); }); -test('strikethrough', t => { - t.throws(() => format.strikethrough('1337'), { - message: 'strikethrough is not supported by Markdown. Use MarkdownV2 instead.', - }); +Deno.test("strikethrough", () => { + assertThrows(() => format.strikethrough("1337")); }); -test('underline', t => { - t.throws(() => format.underline('1337'), { - message: 'underline is not supported by Markdown. Use MarkdownV2 instead.', - }); +Deno.test("underline", () => { + assertThrows(() => format.underline("1337")); }); -test('spoiler', t => { - t.throws(() => format.spoiler('1337'), { - message: 'spoiler is not supported by Markdown. Use MarkdownV2 instead.', - }); +Deno.test("spoiler", () => { + assertThrows(() => format.spoiler("1337")); }); diff --git a/source/markdown.ts b/source/markdown.ts index eaccff6..9155d69 100644 --- a/source/markdown.ts +++ b/source/markdown.ts @@ -1,50 +1,56 @@ -import type {Formatter} from './types.js'; +import type { Formatter } from "./types.ts"; function escape(text: string): string { - return text.replace(/[*_`[\]()]/g, ''); + return text.replace(/[*_`[\]()]/g, ""); } function bold(text: string): string { - return `*${text.replace(/\*/g, '')}*`; + return `*${text.replace(/\*/g, "")}*`; } function italic(text: string): string { - return `_${text.replace(/_/g, '')}_`; + return `_${text.replace(/_/g, "")}_`; } function strikethrough(_text: string): string { - throw new Error('strikethrough is not supported by Markdown. Use MarkdownV2 instead.'); + throw new Error( + "strikethrough is not supported by Markdown. Use MarkdownV2 instead.", + ); } function underline(_text: string): string { - throw new Error('underline is not supported by Markdown. Use MarkdownV2 instead.'); + throw new Error( + "underline is not supported by Markdown. Use MarkdownV2 instead.", + ); } function spoiler(_text: string): string { - throw new Error('spoiler is not supported by Markdown. Use MarkdownV2 instead.'); + throw new Error( + "spoiler is not supported by Markdown. Use MarkdownV2 instead.", + ); } function monospace(text: string): string { - return '`' + text.replace(/`/g, '') + '`'; + return "`" + text.replace(/`/g, "") + "`"; } function monospaceBlock(text: string, programmingLanguage?: string): string { - let result = ''; - result += '```'; + let result = ""; + result += "```"; if (programmingLanguage) { result += programmingLanguage; } - result += '\n'; - result += text.replace(/```/g, ''); - result += '\n'; - result += '```'; + result += "\n"; + result += text.replace(/```/g, ""); + result += "\n"; + result += "```"; return result; } function url(label: string, url: string): string { - return `[${label.replace(/]/, '')}](${url.replace(/\)/, '')})`; + return `[${label.replace(/]/, "")}](${url.replace(/\)/, "")})`; } function userMention(label: string, userId: number): string { @@ -53,7 +59,7 @@ function userMention(label: string, userId: number): string { /** https://core.telegram.org/bots/api#markdown-style */ export const markdown = { - parse_mode: 'Markdown', + parse_mode: "Markdown", escape, bold, italic, diff --git a/source/markdownv2.test.ts b/source/markdownv2.test.ts index 02a362a..c83cb9d 100644 --- a/source/markdownv2.test.ts +++ b/source/markdownv2.test.ts @@ -1,69 +1,72 @@ -import test from 'ava'; -import {markdownv2 as format} from './markdownv2.js'; +import { assertEquals } from "https://deno.land/std@0.206.0/assert/mod.ts"; +import { markdownv2 as format } from "./markdownv2.ts"; -test('bold', t => { - t.is(format.bold('bold'), '*bold*'); +Deno.test("bold", () => { + assertEquals(format.bold("bold"), "*bold*"); }); -test('italic', t => { - t.is(format.italic('italic'), '_italic_'); +Deno.test("italic", () => { + assertEquals(format.italic("italic"), "_italic_"); }); -test('strikethrough', t => { - t.is(format.strikethrough('strikethrough'), '~strikethrough~'); +Deno.test("strikethrough", () => { + assertEquals(format.strikethrough("strikethrough"), "~strikethrough~"); }); -test('underline', t => { - t.is(format.underline('underline'), '__underline__'); +Deno.test("underline", () => { + assertEquals(format.underline("underline"), "__underline__"); }); -test('spoiler', t => { - t.is(format.spoiler('spoiler'), '||spoiler||'); +Deno.test("spoiler", () => { + assertEquals(format.spoiler("spoiler"), "||spoiler||"); }); -test('url', t => { - t.is(format.url('me', 'https://edjopato.de'), '[me](https://edjopato.de)'); +Deno.test("url", () => { + assertEquals( + format.url("me", "https://edjopato.de"), + "[me](https://edjopato.de)", + ); }); -test('escape', t => { - t.is(format.escape('[h_]e(*y\\)`'), '\\[h\\_\\]e\\(\\*y\\\\\\)\\`'); +Deno.test("escape", () => { + assertEquals(format.escape("[h_]e(*y\\)`"), "\\[h\\_\\]e\\(\\*y\\\\\\)\\`"); }); -test('escape with number', t => { - t.is(format.escape('h1e2y'), 'h1e2y'); +Deno.test("escape with number", () => { + assertEquals(format.escape("h1e2y"), "h1e2y"); }); -test('bold italic', t => { - t.is(format.bold(format.italic('green')), '*_green_*'); +Deno.test("bold italic", () => { + assertEquals(format.bold(format.italic("green")), "*_green_*"); }); -test('user mention', t => { - t.is( - format.userMention('inline mention of a user', 123_456_789), - '[inline mention of a user](tg://user?id=123456789)', +Deno.test("user mention", () => { + assertEquals( + format.userMention("inline mention of a user", 123_456_789), + "[inline mention of a user](tg://user?id=123456789)", ); }); -test('monospace', t => { - t.is( - format.monospace('inline fixed-width code'), - '`inline fixed-width code`', +Deno.test("monospace", () => { + assertEquals( + format.monospace("inline fixed-width code"), + "`inline fixed-width code`", ); }); -test('monospaceBlock w/o language', t => { - t.is( - format.monospaceBlock('pre-formatted fixed-width code block'), - '```\npre-formatted fixed-width code block\n```', +Deno.test("monospaceBlock w/o language", () => { + assertEquals( + format.monospaceBlock("pre-formatted fixed-width code block"), + "```\npre-formatted fixed-width code block\n```", ); }); -test('monospaceBlock w/ language', t => { - t.is( +Deno.test("monospaceBlock w/ language", () => { + assertEquals( format.monospaceBlock( - 'pre-formatted fixed-width code block written in the Python programming language', - 'python', + "pre-formatted fixed-width code block written in the Python programming language", + "python", ), - '```python\npre-formatted fixed-width code block written in the Python programming language\n```', + "```python\npre-formatted fixed-width code block written in the Python programming language\n```", ); }); diff --git a/source/markdownv2.ts b/source/markdownv2.ts index e8afe72..5518660 100644 --- a/source/markdownv2.ts +++ b/source/markdownv2.ts @@ -1,11 +1,11 @@ -import type {Formatter} from './types.js'; +import type { Formatter } from "./types.ts"; function escapeInternal(text: string, escapeChars: string): string { - return text.replace(new RegExp(`[${escapeChars}\\\\]`, 'g'), '\\$&'); + return text.replace(new RegExp(`[${escapeChars}\\\\]`, "g"), "\\$&"); } function escape(text: string): string { - return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, '\\$&'); + return text.replace(/[_*[\]()~`>#+\-=|{}.!\\]/g, "\\$&"); } function bold(text: string): string { @@ -29,26 +29,26 @@ function spoiler(text: string): string { } function monospace(text: string): string { - return '`' + escapeInternal(text, '`') + '`'; + return "`" + escapeInternal(text, "`") + "`"; } function monospaceBlock(text: string, programmingLanguage?: string): string { - let result = ''; - result += '```'; + let result = ""; + result += "```"; if (programmingLanguage) { result += programmingLanguage; } - result += '\n'; - result += escapeInternal(text, '`'); - result += '\n'; - result += '```'; + result += "\n"; + result += escapeInternal(text, "`"); + result += "\n"; + result += "```"; return result; } function url(label: string, url: string): string { - return `[${label}](${escapeInternal(url, ')')})`; + return `[${label}](${escapeInternal(url, ")")})`; } function userMention(label: string, userId: number): string { @@ -57,7 +57,7 @@ function userMention(label: string, userId: number): string { /** https://core.telegram.org/bots/api#markdownv2-style */ export const markdownv2 = { - parse_mode: 'MarkdownV2', + parse_mode: "MarkdownV2", escape, bold, italic, diff --git a/source/mod.ts b/source/mod.ts new file mode 100644 index 0000000..a35b014 --- /dev/null +++ b/source/mod.ts @@ -0,0 +1,5 @@ +export type { Formatter } from "./types.ts"; + +export { html } from "./html.ts"; +export { markdown } from "./markdown.ts"; +export { markdownv2 } from "./markdownv2.ts"; diff --git a/source/types.ts b/source/types.ts index e14603f..7db82b5 100644 --- a/source/types.ts +++ b/source/types.ts @@ -1,5 +1,5 @@ export type Formatter = { - parse_mode: 'HTML' | 'Markdown' | 'MarkdownV2'; + parse_mode: "HTML" | "Markdown" | "MarkdownV2"; bold: (text: string) => string; escape: (text: string) => string; italic: (text: string) => string; diff --git a/tsconfig.json b/tsconfig.json index 431fb68..01394e4 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,13 @@ { - "extends": "@sindresorhus/tsconfig/tsconfig.json", "include": [ "source" ], + "exclude": [ + "**/*.test.*" + ], "compilerOptions": { + "declaration": true, "target": "ES2020", - "lib": ["ES2020"], "outDir": "dist" } }