mirror of
https://github.com/danog/telegram-entities.git
synced 2024-11-26 12:14:44 +01:00
First commit
This commit is contained in:
commit
6fd7154c55
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@ -0,0 +1 @@
|
||||
github: danog
|
73
.github/workflows/main.yml
vendored
Normal file
73
.github/workflows/main.yml
vendored
Normal file
@ -0,0 +1,73 @@
|
||||
name: build
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
jobs:
|
||||
run:
|
||||
runs-on: ${{ matrix.os }}
|
||||
strategy:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
php-versions: ["8.2", "8.3"]
|
||||
name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.os }}
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
|
||||
- name: Setup PHP
|
||||
uses: shivammathur/setup-php@v2
|
||||
with:
|
||||
php-version: ${{ matrix.php-versions }}
|
||||
extensions: mbstring, intl, sockets
|
||||
coverage: xdebug
|
||||
|
||||
- name: Check environment
|
||||
run: |
|
||||
php --version
|
||||
composer --version
|
||||
|
||||
- name: Get composer cache directory
|
||||
id: composercache
|
||||
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
|
||||
|
||||
- name: Cache dependencies
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ${{ steps.composercache.outputs.dir }}
|
||||
key: ${{ matrix.os }}-composer-${{ matrix.php-versions }}-${{ hashFiles('**/composer.lock') }}
|
||||
restore-keys: ${{ matrix.os }}-composer-${{ matrix.php-versions }}-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
composer install --prefer-dist
|
||||
wget https://github.com/infection/infection/releases/download/0.27.0/infection.phar -O /usr/local/bin/infection
|
||||
chmod +x /usr/local/bin/infection
|
||||
|
||||
- name: Run codestyle check
|
||||
env:
|
||||
PHP_CS_FIXER_IGNORE_ENV: 1
|
||||
run: |
|
||||
vendor/bin/php-cs-fixer --diff --dry-run -v fix
|
||||
|
||||
- name: Run unit tests
|
||||
env:
|
||||
TOKEN: ${{ secrets.TOKEN }}
|
||||
DEST: ${{ secrets.DEST }}
|
||||
run: |
|
||||
vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml
|
||||
|
||||
#- name: Run mutation tests
|
||||
# env:
|
||||
# STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }}
|
||||
# run: |
|
||||
# infection --show-mutations
|
||||
|
||||
- name: Run Psalm analysis
|
||||
run: |
|
||||
vendor/bin/psalm --shepherd
|
||||
|
||||
- name: Upload coverage reports to Codecov
|
||||
uses: codecov/codecov-action@v4.0.1
|
||||
with:
|
||||
token: ${{ secrets.CODECOV_TOKEN }}
|
||||
slug: danog/telegram-entities
|
9
.gitignore
vendored
Normal file
9
.gitignore
vendored
Normal file
@ -0,0 +1,9 @@
|
||||
.vscode
|
||||
/infection/
|
||||
a.php
|
||||
.phpunit.cache
|
||||
/vendor/
|
||||
*.cache
|
||||
composer.lock
|
||||
/coverage/
|
||||
/.infection-cache
|
14
.php-cs-fixer.dist.php
Normal file
14
.php-cs-fixer.dist.php
Normal file
@ -0,0 +1,14 @@
|
||||
<?php
|
||||
|
||||
$config = new Amp\CodeStyle\Config;
|
||||
|
||||
$config->getFinder()
|
||||
->in(__DIR__ . '/src')
|
||||
->in(__DIR__ . '/tests')
|
||||
->in(__DIR__ . '/examples');
|
||||
|
||||
$cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__;
|
||||
|
||||
$config->setCacheFile($cacheDir . '/.php_cs.cache');
|
||||
|
||||
return $config;
|
203
LICENSE
Normal file
203
LICENSE
Normal file
@ -0,0 +1,203 @@
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
5
NOTICE
Normal file
5
NOTICE
Normal file
@ -0,0 +1,5 @@
|
||||
TelegramEntities - A library to work with Telegram styled text entities.
|
||||
|
||||
Copyright 2024 Daniil Gentili <daniil@daniil.it>
|
||||
|
||||
Homepage: https://github.com/danog/telegram-entities
|
83
README.md
Normal file
83
README.md
Normal file
@ -0,0 +1,83 @@
|
||||
# Async ORM
|
||||
|
||||
[![codecov](https://codecov.io/gh/danog/telegram-entities/branch/master/graph/badge.svg)](https://codecov.io/gh/danog/telegram-entities)
|
||||
[![Psalm coverage](https://shepherd.dev/github/danog/telegram-entities/coverage.svg)](https://shepherd.dev/github/danog/telegram-entities)
|
||||
[![Psalm level 1](https://shepherd.dev/github/danog/telegram-entities/level.svg)](https://shepherd.dev/github/danog/telegram-entities)
|
||||
![License](https://img.shields.io/github/license/danog/telegram-entities)
|
||||
|
||||
A library to work with Telegram UTF-16 styled text entities, created by Daniil Gentili (https://daniil.it).
|
||||
|
||||
This library can be used to modify entities returned by the Telegram Bot API, or even locally generate them using a custom MarkdownV2 and HTML parser inside of the library.
|
||||
|
||||
This ORM library was initially created for [MadelineProto](https://docs.madelineproto.xyz), an async PHP client API for the telegram MTProto protocol.
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
composer require danog/async-orm
|
||||
```
|
||||
|
||||
## Usage
|
||||
|
||||
```php
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use Amp\Http\Client\HttpClientBuilder;
|
||||
use Amp\Http\Client\Request;
|
||||
use danog\TelegramEntities\Entities;
|
||||
use danog\TelegramEntities\EntityTools;
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
$token = getenv('TOKEN');
|
||||
if (!$token) {
|
||||
throw new AssertionError("A TOKEN environment variable must be specified!");
|
||||
}
|
||||
|
||||
$dest = getenv('DEST');
|
||||
if (!$dest) {
|
||||
throw new AssertionError("A DEST environment variable must be specified!");
|
||||
}
|
||||
|
||||
$client = HttpClientBuilder::buildDefault();
|
||||
|
||||
$sm = function (string $message, string $parse_mode = '', array $entities = []) use ($token, $dest, $client): array {
|
||||
$res = $client->request(new Request("https://api.telegram.org/bot$token/sendMessage?".http_build_query([
|
||||
'text' => $message,
|
||||
'parse_mode' => $parse_mode,
|
||||
'entities' => json_encode($entities),
|
||||
'chat_id' => $dest
|
||||
])));
|
||||
|
||||
return json_decode($res->getBody()->buffer(), true)['result'];
|
||||
};
|
||||
|
||||
$result = $sm("*This is a ❤️ test*", parse_mode: "MarkdownV2");
|
||||
|
||||
// Convert a message+entities back to HTML
|
||||
$entities = new Entities($result['text'], $result['entities']);
|
||||
var_dump($entities->toHTML()); // <b>This is a ❤️ test</b>
|
||||
|
||||
// Modify $entities as needed
|
||||
$entities->message = "A message with ❤️ emojis";
|
||||
|
||||
// EntityTools::mb* methods compute the length in UTF-16 code units, as required by the bot API.
|
||||
$entities->entities[0]['length'] = EntityTools::mbStrlen($entities->message);
|
||||
|
||||
// then resend:
|
||||
$sm($entities->message, entities: $entities->entities);
|
||||
|
||||
// Convert HTML to an array of entities locally
|
||||
$entities = Entities::fromHtml("<b>This is <i>a ❤️ nested</i> test</b>");
|
||||
$sm($entities->message, entities: $entities->entities);
|
||||
|
||||
// Convert markdown to an array of entities locally
|
||||
$entities = Entities::fromHtml("<b>This is <i>a ❤️ nested</i> test</b>");
|
||||
$sm($entities->message, entities: $entities->entities);
|
||||
```
|
||||
|
||||
Many more methods are available, see the [API documentation](https://github.com/danog/telegram-entities/blob/master/docs/docs/index.md) for the full list!
|
||||
|
||||
## API Documentation
|
||||
|
||||
Click [here »](https://github.com/danog/telegram-entities/blob/master/docs/docs/index.md) to view the API documentation.
|
46
composer.json
Normal file
46
composer.json
Normal file
@ -0,0 +1,46 @@
|
||||
{
|
||||
"name": "danog/telegram-entities",
|
||||
"description": "A library to work with Telegram UTF-16 styled text entities.",
|
||||
"type": "library",
|
||||
"license": "Apache-2.0",
|
||||
"autoload": {
|
||||
"psr-4": {
|
||||
"danog\\TelegramEntities\\": "src/"
|
||||
}
|
||||
},
|
||||
"autoload-dev": {
|
||||
"psr-4": {
|
||||
"danog\\TestTelegramEntities\\": "tests/"
|
||||
}
|
||||
},
|
||||
"authors": [
|
||||
{
|
||||
"name": "Daniil Gentili",
|
||||
"email": "daniil@daniil.it"
|
||||
}
|
||||
],
|
||||
"require": {
|
||||
"php-64bit": ">=8.2.4",
|
||||
"webmozart/assert": "^1.11",
|
||||
"symfony/polyfill-mbstring": "*"
|
||||
},
|
||||
"require-dev": {
|
||||
"vimeo/psalm": "dev-master",
|
||||
"phpunit/phpunit": "^11.0.9",
|
||||
"amphp/php-cs-fixer-config": "^2.0.1",
|
||||
"friendsofphp/php-cs-fixer": "^3.52.1",
|
||||
"infection/infection": "^0.28.1",
|
||||
"danog/phpdoc": "^0.1.22",
|
||||
"amphp/http-client": "^5.0"
|
||||
},
|
||||
"scripts": {
|
||||
"cs-fix": "PHP_CS_FIXER_IGNORE_ENV=1 php -d pcre.jit=0 vendor/bin/php-cs-fixer fix -v"
|
||||
},
|
||||
"config": {
|
||||
"allow-plugins": {
|
||||
"dealerdirect/phpcodesniffer-composer-installer": true,
|
||||
"infection/extension-installer": true
|
||||
}
|
||||
}
|
||||
|
||||
}
|
77
docs/docs/danog/TelegramEntities/Entities.md
Normal file
77
docs/docs/danog/TelegramEntities/Entities.md
Normal file
@ -0,0 +1,77 @@
|
||||
---
|
||||
title: "danog\\TelegramEntities\\Entities: Class that represents a message + set of Telegram entities."
|
||||
description: ""
|
||||
|
||||
---
|
||||
# `danog\TelegramEntities\Entities`
|
||||
[Back to index](../../index.md)
|
||||
|
||||
> Author: Daniil Gentili <daniil@daniil.it>
|
||||
|
||||
|
||||
Class that represents a message + set of Telegram entities.
|
||||
|
||||
|
||||
|
||||
## Properties
|
||||
* `$message`: `string` Converted message
|
||||
* `$entities`: `list<TEntity>` Converted entities.
|
||||
|
||||
## Method list:
|
||||
* [`__construct(string $message, array $entities)`](#__construct)
|
||||
* [`fromMarkdown(string $markdown): \danog\TelegramEntities\Entities`](#fromMarkdown)
|
||||
* [`fromHtml(string $html): \danog\TelegramEntities\Entities`](#fromHtml)
|
||||
* [`toHTML(bool $allowTelegramTags = false): string`](#toHTML)
|
||||
|
||||
## Methods:
|
||||
### <a name="__construct"></a> `__construct(string $message, array $entities)`
|
||||
|
||||
Creates an Entities container using a message and a list of entities.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$message`: `string`
|
||||
* `$entities`: `array`
|
||||
|
||||
|
||||
|
||||
### <a name="fromMarkdown"></a> `fromMarkdown(string $markdown): \danog\TelegramEntities\Entities`
|
||||
|
||||
Manually convert markdown to a message and a set of entities.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$markdown`: `string`
|
||||
|
||||
|
||||
Return value: Object containing message and entities
|
||||
|
||||
|
||||
### <a name="fromHtml"></a> `fromHtml(string $html): \danog\TelegramEntities\Entities`
|
||||
|
||||
Manually convert HTML to a message and a set of entities.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$html`: `string`
|
||||
|
||||
|
||||
Return value: Object containing message and entities
|
||||
|
||||
|
||||
### <a name="toHTML"></a> `toHTML(bool $allowTelegramTags = false): string`
|
||||
|
||||
Convert a message and a set of entities to HTML.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$allowTelegramTags`: `bool` Whether to allow telegram-specific tags like tg-spoiler, tg-emoji, mention links and so on...
|
||||
|
||||
|
||||
|
||||
---
|
||||
Generated by [danog/phpdoc](https://phpdoc.daniil.it)
|
125
docs/docs/danog/TelegramEntities/EntityTools.md
Normal file
125
docs/docs/danog/TelegramEntities/EntityTools.md
Normal file
@ -0,0 +1,125 @@
|
||||
---
|
||||
title: "danog\\TelegramEntities\\EntityTools: Telegram UTF-16 styled text entity tools."
|
||||
description: ""
|
||||
|
||||
---
|
||||
# `danog\TelegramEntities\EntityTools`
|
||||
[Back to index](../../index.md)
|
||||
|
||||
> Author: Daniil Gentili <daniil@daniil.it>
|
||||
|
||||
|
||||
Telegram UTF-16 styled text entity tools.
|
||||
|
||||
|
||||
|
||||
|
||||
## Method list:
|
||||
* [`mbStrlen(string $text): int`](#mbStrlen)
|
||||
* [`mbSubstr(string $text, integer $offset, (null|int) $length = NULL): string`](#mbSubstr)
|
||||
* [`mbStrSplit(string $text, integer<0, max> $length): list<string>`](#mbStrSplit)
|
||||
* [`htmlEscape(string $what): string`](#htmlEscape)
|
||||
* [`markdownEscape(string $what): string`](#markdownEscape)
|
||||
* [`markdownCodeblockEscape(string $what): string`](#markdownCodeblockEscape)
|
||||
* [`markdownCodeEscape(string $what): string`](#markdownCodeEscape)
|
||||
* [`markdownUrlEscape(string $what): string`](#markdownUrlEscape)
|
||||
|
||||
## Methods:
|
||||
### <a name="mbStrlen"></a> `mbStrlen(string $text): int`
|
||||
|
||||
Get length of string in UTF-16 code points.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$text`: `string` Text
|
||||
|
||||
|
||||
|
||||
### <a name="mbSubstr"></a> `mbSubstr(string $text, integer $offset, (null|int) $length = NULL): string`
|
||||
|
||||
Telegram UTF-16 multibyte substring.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$text`: `string` Text to substring
|
||||
* `$offset`: `integer` Offset
|
||||
* `$length`: `(null|int)` Length
|
||||
|
||||
|
||||
|
||||
### <a name="mbStrSplit"></a> `mbStrSplit(string $text, integer<0, max> $length): list<string>`
|
||||
|
||||
Telegram UTF-16 multibyte split.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$text`: `string` Text
|
||||
* `$length`: `integer<0, max>` Length
|
||||
|
||||
|
||||
#### See also:
|
||||
* `max`
|
||||
|
||||
|
||||
|
||||
|
||||
### <a name="htmlEscape"></a> `htmlEscape(string $what): string`
|
||||
|
||||
Escape string for this library's HTML entity converter.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$what`: `string` String to escape
|
||||
|
||||
|
||||
|
||||
### <a name="markdownEscape"></a> `markdownEscape(string $what): string`
|
||||
|
||||
Escape string for markdown.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$what`: `string` String to escape
|
||||
|
||||
|
||||
|
||||
### <a name="markdownCodeblockEscape"></a> `markdownCodeblockEscape(string $what): string`
|
||||
|
||||
Escape string for markdown codeblock.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$what`: `string` String to escape
|
||||
|
||||
|
||||
|
||||
### <a name="markdownCodeEscape"></a> `markdownCodeEscape(string $what): string`
|
||||
|
||||
Escape string for markdown code section.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$what`: `string` String to escape
|
||||
|
||||
|
||||
|
||||
### <a name="markdownUrlEscape"></a> `markdownUrlEscape(string $what): string`
|
||||
|
||||
Escape string for URL.
|
||||
|
||||
|
||||
Parameters:
|
||||
|
||||
* `$what`: `string` String to escape
|
||||
|
||||
|
||||
|
||||
---
|
||||
Generated by [danog/phpdoc](https://phpdoc.daniil.it)
|
20
docs/docs/index.md
Normal file
20
docs/docs/index.md
Normal file
@ -0,0 +1,20 @@
|
||||
---
|
||||
description: "A library to work with Telegram UTF-16 styled text entities."
|
||||
title: "danog/telegram-entities"
|
||||
|
||||
---
|
||||
# `danog/telegram-entities`
|
||||
|
||||
A library to work with Telegram UTF-16 styled text entities.
|
||||
|
||||
|
||||
|
||||
|
||||
## Classes
|
||||
* [\danog\TelegramEntities\Entities: Class that represents a message + set of Telegram entities.](danog/TelegramEntities/Entities.md)
|
||||
* [\danog\TelegramEntities\EntityTools: Telegram UTF-16 styled text entity tools.](danog/TelegramEntities/EntityTools.md)
|
||||
|
||||
|
||||
|
||||
---
|
||||
Generated by [danog/phpdoc](https://phpdoc.daniil.it).
|
1
docs/index.md
Symbolic link
1
docs/index.md
Symbolic link
@ -0,0 +1 @@
|
||||
../README.md
|
56
examples/1-all.php
Normal file
56
examples/1-all.php
Normal file
@ -0,0 +1,56 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
use Amp\Http\Client\HttpClientBuilder;
|
||||
use Amp\Http\Client\Request;
|
||||
use danog\TelegramEntities\Entities;
|
||||
use danog\TelegramEntities\EntityTools;
|
||||
|
||||
require __DIR__.'/../vendor/autoload.php';
|
||||
|
||||
$token = getenv('TOKEN');
|
||||
if (!$token) {
|
||||
throw new AssertionError("A TOKEN environment variable must be specified!");
|
||||
}
|
||||
|
||||
$dest = getenv('DEST');
|
||||
if (!$dest) {
|
||||
throw new AssertionError("A DEST environment variable must be specified!");
|
||||
}
|
||||
|
||||
$client = HttpClientBuilder::buildDefault();
|
||||
|
||||
$sm = function (string $message, string $parse_mode = '', array $entities = []) use ($token, $dest, $client): array {
|
||||
$res = $client->request(new Request("https://api.telegram.org/bot$token/sendMessage?".http_build_query([
|
||||
'text' => $message,
|
||||
'parse_mode' => $parse_mode,
|
||||
'entities' => json_encode($entities),
|
||||
'chat_id' => $dest
|
||||
])));
|
||||
|
||||
return json_decode($res->getBody()->buffer(), true)['result'];
|
||||
};
|
||||
|
||||
$result = $sm("*This is a ❤️ test*", parse_mode: "MarkdownV2");
|
||||
|
||||
// Convert a message+entities back to HTML
|
||||
$entities = new Entities($result['text'], $result['entities']);
|
||||
var_dump($entities->toHTML()); // <b>This is a ❤️ test</b>
|
||||
|
||||
// Modify $entities as needed
|
||||
$entities->message = "A message with ❤️ emojis";
|
||||
|
||||
// EntityTools::mb* methods compute the length in UTF-16 code units, as required by the bot API.
|
||||
$entities->entities[0]['length'] = EntityTools::mbStrlen($entities->message);
|
||||
|
||||
// then resend:
|
||||
$sm($entities->message, entities: $entities->entities);
|
||||
|
||||
// Convert HTML to an array of entities locally
|
||||
$entities = Entities::fromHtml("<b>This is <i>a ❤️ nested</i> test</b>");
|
||||
$sm($entities->message, entities: $entities->entities);
|
||||
|
||||
// Convert markdown to an array of entities locally
|
||||
$entities = Entities::fromHtml("<b>This is <i>a ❤️ nested</i> test</b>");
|
||||
$sm($entities->message, entities: $entities->entities);
|
||||
|
||||
// See https://github.com/danog/telegram-entities for the full list of available methods!
|
18
phpunit.xml
Normal file
18
phpunit.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/11.1/phpunit.xsd"
|
||||
bootstrap="vendor/autoload.php" cacheDirectory=".phpunit.cache"
|
||||
beStrictAboutCoverageMetadata="true" beStrictAboutOutputDuringTests="true"
|
||||
requireCoverageMetadata="false" failOnRisky="true" executionOrder="random" failOnWarning="true"
|
||||
displayDetailsOnTestsThatTriggerWarnings="true">
|
||||
<testsuites>
|
||||
<testsuite name="default">
|
||||
<directory>tests</directory>
|
||||
</testsuite>
|
||||
</testsuites>
|
||||
<source restrictNotices="true" restrictWarnings="true" ignoreIndirectDeprecations="true">
|
||||
<include>
|
||||
<directory>src</directory>
|
||||
</include>
|
||||
</source>
|
||||
</phpunit>
|
18
psalm.xml
Normal file
18
psalm.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0"?>
|
||||
<psalm
|
||||
errorLevel="1"
|
||||
resolveFromConfigFile="true"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xmlns="https://getpsalm.org/schema/config"
|
||||
xsi:schemaLocation="https://getpsalm.org/schema/config vendor/vimeo/psalm/config.xsd"
|
||||
findUnusedBaselineEntry="true"
|
||||
findUnusedPsalmSuppress="true"
|
||||
findUnusedCode="true"
|
||||
>
|
||||
<projectFiles>
|
||||
<directory name="src" />
|
||||
<ignoreFiles>
|
||||
<directory name="vendor" />
|
||||
</ignoreFiles>
|
||||
</projectFiles>
|
||||
</psalm>
|
416
src/Entities.php
Normal file
416
src/Entities.php
Normal file
@ -0,0 +1,416 @@
|
||||
<?php declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Copyright 2024 Daniil Gentili.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2024 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/license/apache-2-0 Apache 2.0
|
||||
* @link https://github.com/danog/telegram-entities TelegramEntities documentation
|
||||
*/
|
||||
|
||||
namespace danog\TelegramEntities;
|
||||
|
||||
use AssertionError;
|
||||
use DOMDocument;
|
||||
use DOMElement;
|
||||
use DOMNode;
|
||||
use DOMText;
|
||||
|
||||
/**
|
||||
* Class that represents a message + set of Telegram entities.
|
||||
*
|
||||
* @api
|
||||
*
|
||||
* @psalm-type TEntity=(
|
||||
* array{
|
||||
* type: "bold"|"italic"|"code"|"strikethrough"|"underline"|"block_quote"|"url"|"email"|"phone"|"spoiler"|"mention",
|
||||
* offset: int<0, max>,
|
||||
* length: int<0, max>
|
||||
* }
|
||||
* |array{type: "text_mention", user: array{id: int, ...}, offset: int, length: int}
|
||||
* |array{type: "custom_emoji", custom_emoji_id: int, offset: int, length: int}
|
||||
* |array{type: "pre", language?: string, offset: int, length: int}
|
||||
* |array{type: "text_link", url: string, offset: int, length: int}
|
||||
* )
|
||||
*/
|
||||
final class Entities
|
||||
{
|
||||
/**
|
||||
* Creates an Entities container using a message and a list of entities.
|
||||
*/
|
||||
public function __construct(
|
||||
/** Converted message */
|
||||
public string $message,
|
||||
/**
|
||||
* Converted entities.
|
||||
*
|
||||
* @var list<TEntity>
|
||||
*/
|
||||
public array $entities,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually convert markdown to a message and a set of entities.
|
||||
*
|
||||
* @return Entities Object containing message and entities
|
||||
*/
|
||||
public static function fromMarkdown(string $markdown): self
|
||||
{
|
||||
$markdown = \str_replace("\r\n", "\n", $markdown);
|
||||
$message = '';
|
||||
$messageLen = 0;
|
||||
$entities = [];
|
||||
$offset = 0;
|
||||
$stack = [];
|
||||
while ($offset < \strlen($markdown)) {
|
||||
$len = \strcspn($markdown, '*_~`[]|!\\', $offset);
|
||||
$piece = \substr($markdown, $offset, $len);
|
||||
$offset += $len;
|
||||
if ($offset === \strlen($markdown)) {
|
||||
$message .= $piece;
|
||||
break;
|
||||
}
|
||||
|
||||
$char = $markdown[$offset++];
|
||||
$next = $markdown[$offset] ?? '';
|
||||
if ($char === '\\') {
|
||||
$message .= $piece.$next;
|
||||
$messageLen += EntityTools::mbStrlen($piece)+1;
|
||||
$offset++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($char === '_' && $next === '_') {
|
||||
$offset++;
|
||||
$char = '__';
|
||||
} elseif ($char === '|') {
|
||||
if ($next === '|') {
|
||||
$offset++;
|
||||
$char = '||';
|
||||
} else {
|
||||
$message .= $piece.$char;
|
||||
$messageLen += EntityTools::mbStrlen($piece)+1;
|
||||
continue;
|
||||
}
|
||||
} elseif ($char === '!') {
|
||||
if ($next === '[') {
|
||||
$offset++;
|
||||
$char = '](';
|
||||
} else {
|
||||
$message .= $piece.$char;
|
||||
$messageLen += EntityTools::mbStrlen($piece)+1;
|
||||
continue;
|
||||
}
|
||||
} elseif ($char === '[') {
|
||||
$char = '](';
|
||||
} elseif ($char === ']') {
|
||||
if (!$stack || \end($stack)[0] !== '](') {
|
||||
$message .= $piece.$char;
|
||||
$messageLen += EntityTools::mbStrlen($piece)+1;
|
||||
continue;
|
||||
}
|
||||
if ($next !== '(') {
|
||||
\array_pop($stack);
|
||||
$message .= '['.$piece.$char;
|
||||
$messageLen += EntityTools::mbStrlen($piece)+2;
|
||||
continue;
|
||||
}
|
||||
$offset++;
|
||||
$char = "](";
|
||||
} elseif ($char === '`') {
|
||||
$message .= $piece;
|
||||
$messageLen += EntityTools::mbStrlen($piece);
|
||||
|
||||
$token = '`';
|
||||
$language = null;
|
||||
if ($next === '`' && ($markdown[$offset+1] ?? '') === '`') {
|
||||
$token = '```';
|
||||
|
||||
$offset += 2;
|
||||
$langLen = \strcspn($markdown, "\n ", $offset);
|
||||
$language = \substr($markdown, $offset, $langLen);
|
||||
$offset += $langLen;
|
||||
if (($markdown[$offset] ?? '') === "\n") {
|
||||
$offset++;
|
||||
}
|
||||
}
|
||||
|
||||
$piece = '';
|
||||
$posClose = $offset;
|
||||
while (($posClose = \strpos($markdown, $token, $posClose)) !== false) {
|
||||
if ($markdown[$posClose-1] === '\\') {
|
||||
$piece .= \substr($markdown, $offset, ($posClose-$offset)-1).$token;
|
||||
$posClose += \strlen($token);
|
||||
$offset = $posClose;
|
||||
continue;
|
||||
}
|
||||
break;
|
||||
}
|
||||
/** @var int|false $posClose */
|
||||
if ($posClose === false) {
|
||||
throw new AssertionError("Unclosed ``` opened @ pos $offset!");
|
||||
}
|
||||
$piece .= \substr($markdown, $offset, $posClose-$offset);
|
||||
|
||||
$start = $messageLen;
|
||||
|
||||
$message .= $piece;
|
||||
$pieceLen = EntityTools::mbStrlen($piece);
|
||||
$messageLen += $pieceLen;
|
||||
|
||||
for ($x = \strlen($piece)-1; $x >= 0; $x--) {
|
||||
if (!(
|
||||
$piece[$x] === ' '
|
||||
|| $piece[$x] === "\r"
|
||||
|| $piece[$x] === "\n"
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
$pieceLen--;
|
||||
}
|
||||
if ($pieceLen > 0) {
|
||||
\assert($start >= 0);
|
||||
$tmp = [
|
||||
'type' => match ($token) {
|
||||
'```' => 'pre',
|
||||
'`' => 'code',
|
||||
},
|
||||
'offset' => $start,
|
||||
'length' => $pieceLen,
|
||||
];
|
||||
if ($language !== null) {
|
||||
$tmp['language'] = $language;
|
||||
}
|
||||
$entities []= $tmp;
|
||||
unset($tmp);
|
||||
}
|
||||
|
||||
$offset = $posClose+\strlen($token);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($stack && \end($stack)[0] === $char) {
|
||||
[, $start] = \array_pop($stack);
|
||||
if ($char === '](') {
|
||||
$posClose = $offset;
|
||||
$link = '';
|
||||
while (($posClose = \strpos($markdown, ')', $posClose)) !== false) {
|
||||
if ($markdown[$posClose-1] === '\\') {
|
||||
$link .= \substr($markdown, $offset, ($posClose-$offset)-1);
|
||||
$offset = $posClose++;
|
||||
continue;
|
||||
}
|
||||
$link .= \substr($markdown, $offset, ($posClose-$offset));
|
||||
break;
|
||||
}
|
||||
/** @var int|false $posClose */
|
||||
if ($posClose === false) {
|
||||
throw new AssertionError("Unclosed ) opened @ pos $offset!");
|
||||
}
|
||||
$entity = self::handleLink($link);
|
||||
$offset = $posClose+1;
|
||||
} else {
|
||||
$entity = match ($char) {
|
||||
'*' => ['type' => 'bold'],
|
||||
'_' => ['type' => 'italic'],
|
||||
'__' => ['type' => 'underline'],
|
||||
'`' => ['type' => 'code'],
|
||||
'~' => ['type' => 'strikethrough'],
|
||||
'||' => ['type' => 'spoiler'],
|
||||
default => throw new AssertionError("Unknown char $char @ pos $offset!")
|
||||
};
|
||||
}
|
||||
$message .= $piece;
|
||||
$messageLen += EntityTools::mbStrlen($piece);
|
||||
|
||||
$lengthReal = $messageLen-$start;
|
||||
for ($x = \strlen($message)-1; $x >= 0; $x--) {
|
||||
if (!(
|
||||
$message[$x] === ' '
|
||||
|| $message[$x] === "\r"
|
||||
|| $message[$x] === "\n"
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
$lengthReal--;
|
||||
}
|
||||
if ($lengthReal > 0) {
|
||||
$entities []= $entity + ['offset' => $start, 'length' => $lengthReal];
|
||||
}
|
||||
} else {
|
||||
$message .= $piece;
|
||||
$messageLen += EntityTools::mbStrlen($piece);
|
||||
$stack []= [$char, $messageLen];
|
||||
}
|
||||
}
|
||||
if ($stack) {
|
||||
throw new AssertionError("Found unclosed markdown elements ".\implode(', ', \array_column($stack, 0)));
|
||||
}
|
||||
/** @psalm-suppress MixedArgumentTypeCoercion Psalm bug to fix */
|
||||
return new Entities(
|
||||
\trim($message),
|
||||
$entities,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Manually convert HTML to a message and a set of entities.
|
||||
*
|
||||
* @return Entities Object containing message and entities
|
||||
*/
|
||||
public static function fromHtml(string $html): Entities
|
||||
{
|
||||
$dom = new DOMDocument();
|
||||
$html = \preg_replace('/\<br(\s*)?\/?\>/i', "\n", $html);
|
||||
\assert($html !== null);
|
||||
$dom->loadxml('<body>' . \trim($html) . '</body>');
|
||||
$message = '';
|
||||
$entities = [];
|
||||
/** @psalm-suppress PossiblyNullArgument Ignore, will throw anyway */
|
||||
self::parseNode($dom->getElementsByTagName('body')->item(0), 0, $message, $entities);
|
||||
return new Entities(\trim($message), $entities);
|
||||
}
|
||||
/**
|
||||
* @return integer Length of the node
|
||||
*
|
||||
* @psalm-suppress UnusedReturnValue
|
||||
*
|
||||
* @param-out list<TEntity> $entities
|
||||
* @param list<TEntity> $entities
|
||||
*/
|
||||
private static function parseNode(DOMNode|DOMText $node, int $offset, string &$message, array &$entities): int
|
||||
{
|
||||
if ($node instanceof DOMText) {
|
||||
$message .= $node->wholeText;
|
||||
return EntityTools::mbStrlen($node->wholeText);
|
||||
}
|
||||
// @codeCoverageIgnoreStart
|
||||
if ($node->nodeName === 'br') {
|
||||
$message .= "\n";
|
||||
return 1;
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
/** @var DOMElement $node */
|
||||
$entity = match ($node->nodeName) {
|
||||
's', 'strike', 'del' => ['type' => 'strikethrough'],
|
||||
'u' => ['type' => 'underline'],
|
||||
'blockquote' => ['type' => 'block_quote'],
|
||||
'b', 'strong' => ['type' => 'bold'],
|
||||
'i', 'em' => ['type' => 'italic'],
|
||||
'code' => ['type' => 'code'],
|
||||
'spoiler', 'tg-spoiler' => ['type' => 'spoiler'],
|
||||
'pre' => $node->hasAttribute('language')
|
||||
? ['type' => 'pre', 'language' => $node->getAttribute('language')]
|
||||
: ['type' => 'pre'],
|
||||
'tg-emoji' => ['type' => 'custom_emoji', 'custom_emoji_id' => (int) $node->getAttribute('emoji-id')],
|
||||
'emoji' => ['type' => 'custom_emoji', 'custom_emoji_id' => (int) $node->getAttribute('id')],
|
||||
'a' => self::handleLink($node->getAttribute('href')),
|
||||
default => null,
|
||||
};
|
||||
$length = 0;
|
||||
/** @var DOMNode|DOMText */
|
||||
foreach ($node->childNodes as $sub) {
|
||||
$length += self::parseNode($sub, $offset+$length, $message, $entities);
|
||||
}
|
||||
if ($entity !== null) {
|
||||
$lengthReal = $length;
|
||||
for ($x = \strlen($message)-1; $x >= 0; $x--) {
|
||||
if (!(
|
||||
$message[$x] === ' '
|
||||
|| $message[$x] === "\r"
|
||||
|| $message[$x] === "\n"
|
||||
)) {
|
||||
break;
|
||||
}
|
||||
$lengthReal--;
|
||||
}
|
||||
if ($lengthReal > 0) {
|
||||
\assert($offset >= 0);
|
||||
$entity['offset'] = $offset;
|
||||
$entity['length'] = $lengthReal;
|
||||
/** @psalm-check-type $entity = TEntity */
|
||||
$entities []= $entity;
|
||||
}
|
||||
}
|
||||
return $length;
|
||||
}
|
||||
/** @return array{type: "text_mention", user: array{id: int}}|array{type: "custom_emoji", custom_emoji_id: int}|array{type: "text_link", url: string} */
|
||||
private static function handleLink(string $href): array
|
||||
{
|
||||
if (\preg_match('|^mention:(.+)|', $href, $matches) || \preg_match('|^tg://user\\?id=(.+)|', $href, $matches)) {
|
||||
return ['type' => 'text_mention', 'user' => ['id' => (int) $matches[1]]];
|
||||
}
|
||||
if (\preg_match('|^emoji:(\d+)$|', $href, $matches) || \preg_match('|^tg://emoji\\?id=(.+)|', $href, $matches)) {
|
||||
return ['type' => 'custom_emoji', 'custom_emoji_id' => (int) $matches[1]];
|
||||
}
|
||||
return ['type' => 'text_link', 'url' => $href];
|
||||
}
|
||||
/**
|
||||
* Convert a message and a set of entities to HTML.
|
||||
*
|
||||
* @param bool $allowTelegramTags Whether to allow telegram-specific tags like tg-spoiler, tg-emoji, mention links and so on...
|
||||
*/
|
||||
public function toHTML(bool $allowTelegramTags = false): string
|
||||
{
|
||||
$insertions = [];
|
||||
foreach ($this->entities as $entity) {
|
||||
['offset' => $offset, 'length' => $length] = $entity;
|
||||
$insertions[$offset] ??= '';
|
||||
/** @psalm-suppress PossiblyUndefinedArrayOffset */
|
||||
$insertions[$offset] .= match ($entity['type']) {
|
||||
'bold' => '<b>',
|
||||
'italic' => '<i>',
|
||||
'code' => '<code>',
|
||||
'pre' => isset($entity['language']) && $entity['language'] !== '' ? '<pre language="'.$entity['language'].'">' : '<pre>',
|
||||
'text_link' => '<a href="'.$entity['url'].'">',
|
||||
'strikethrough' => '<s>',
|
||||
"underline" => '<u>',
|
||||
"block_quote" => '<blockquote>',
|
||||
"url" => '<a href="'.EntityTools::htmlEscape(EntityTools::mbSubstr($this->message, $offset, $length)).'">',
|
||||
"email" => '<a href="mailto:'.EntityTools::htmlEscape(EntityTools::mbSubstr($this->message, $offset, $length)).'">',
|
||||
"phone" => '<a href="phone:'.EntityTools::htmlEscape(EntityTools::mbSubstr($this->message, $offset, $length)).'">',
|
||||
"mention" => '<a href="https://t.me/'.EntityTools::htmlEscape(EntityTools::mbSubstr($this->message, $offset+1, $length-1)).'">',
|
||||
"spoiler" => $allowTelegramTags ? '<tg-spoiler>' : '',
|
||||
"custom_emoji" => $allowTelegramTags ? '<tg-emoji emoji-id="'.$entity['custom_emoji_id'].'">' : '',
|
||||
"text_mention" => $allowTelegramTags ? '<a href="tg://user?id='.$entity['user']['id'].'">' : '',
|
||||
};
|
||||
$offset += $length;
|
||||
$insertions[$offset] = match ($entity['type']) {
|
||||
"bold" => '</b>',
|
||||
"italic" => '</i>',
|
||||
"code" => '</code>',
|
||||
"pre" => '</pre>',
|
||||
"text_link", "url", "email", "mention", "phone" => '</a>',
|
||||
"strikethrough" => '</s>',
|
||||
"underline" => '</u>',
|
||||
"block_quote" => '</blockquote>',
|
||||
"spoiler" => $allowTelegramTags ? '</tg-spoiler>' : '',
|
||||
"custom_emoji" => $allowTelegramTags ? "</tg-emoji>" : '',
|
||||
"text_mention" => $allowTelegramTags ? '</a>' : '',
|
||||
} . ($insertions[$offset] ?? '');
|
||||
}
|
||||
\ksort($insertions);
|
||||
$final = '';
|
||||
$pos = 0;
|
||||
foreach ($insertions as $offset => $insertion) {
|
||||
$final .= EntityTools::htmlEscape(EntityTools::mbSubstr($this->message, $pos, $offset-$pos));
|
||||
$final .= $insertion;
|
||||
$pos = $offset;
|
||||
}
|
||||
return \str_replace("\n", "<br>", $final.EntityTools::htmlEscape(EntityTools::mbSubstr($this->message, $pos)));
|
||||
}
|
||||
}
|
199
src/EntityTools.php
Normal file
199
src/EntityTools.php
Normal file
@ -0,0 +1,199 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* Tools module.
|
||||
*
|
||||
* Copyright 2024 Daniil Gentili.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*
|
||||
* @author Daniil Gentili <daniil@daniil.it>
|
||||
* @copyright 2016-2024 Daniil Gentili <daniil@daniil.it>
|
||||
* @license https://opensource.org/license/apache-2-0 Apache 2.0
|
||||
* @link https://github.com/danog/telegram-entities TelegramEntities documentation
|
||||
*/
|
||||
|
||||
namespace danog\TelegramEntities;
|
||||
|
||||
use Webmozart\Assert\Assert;
|
||||
|
||||
/**
|
||||
* Telegram UTF-16 styled text entity tools.
|
||||
*
|
||||
* @api
|
||||
*/
|
||||
final class EntityTools
|
||||
{
|
||||
// @codeCoverageIgnoreStart
|
||||
/**
|
||||
* @psalm-suppress UnusedConstructor
|
||||
*
|
||||
* @internal Can only be used statically.
|
||||
*/
|
||||
private function __construct()
|
||||
{
|
||||
}
|
||||
// @codeCoverageIgnoreEnd
|
||||
|
||||
/**
|
||||
* Get length of string in UTF-16 code points.
|
||||
*
|
||||
* @param string $text Text
|
||||
*/
|
||||
public static function mbStrlen(string $text): int
|
||||
{
|
||||
$length = 0;
|
||||
$textlength = \strlen($text);
|
||||
for ($x = 0; $x < $textlength; $x++) {
|
||||
$char = \ord($text[$x]);
|
||||
if (($char & 0xc0) != 0x80) {
|
||||
$length += 1 + ($char >= 0xf0 ? 1 : 0);
|
||||
}
|
||||
}
|
||||
return $length;
|
||||
}
|
||||
/**
|
||||
* Telegram UTF-16 multibyte substring.
|
||||
*
|
||||
* @param string $text Text to substring
|
||||
* @param integer $offset Offset
|
||||
* @param null|int $length Length
|
||||
*/
|
||||
public static function mbSubstr(string $text, int $offset, ?int $length = null): string
|
||||
{
|
||||
/** @var string */
|
||||
$converted = \mb_convert_encoding($text, 'UTF-16');
|
||||
/** @var string */
|
||||
return \mb_convert_encoding(
|
||||
\substr(
|
||||
$converted,
|
||||
$offset<<1,
|
||||
$length === null ? null : ($length<<1),
|
||||
),
|
||||
'UTF-8',
|
||||
'UTF-16',
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Telegram UTF-16 multibyte split.
|
||||
*
|
||||
* @param string $text Text
|
||||
* @param integer<0, max> $length Length
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function mbStrSplit(string $text, int $length): array
|
||||
{
|
||||
$result = [];
|
||||
/** @var string */
|
||||
$text = \mb_convert_encoding($text, 'UTF-16');
|
||||
/** @psalm-suppress ArgumentTypeCoercion */
|
||||
foreach (\str_split($text, $length<<1) as $chunk) {
|
||||
$chunk = \mb_convert_encoding($chunk, 'UTF-8', 'UTF-16');
|
||||
Assert::string($chunk);
|
||||
$result []= $chunk;
|
||||
}
|
||||
/** @var list<string> */
|
||||
return $result;
|
||||
}
|
||||
/**
|
||||
* Escape string for this library's HTML entity converter.
|
||||
*
|
||||
* @param string $what String to escape
|
||||
*/
|
||||
public static function htmlEscape(string $what): string
|
||||
{
|
||||
return \htmlspecialchars($what, ENT_QUOTES|ENT_SUBSTITUTE|ENT_XML1);
|
||||
}
|
||||
/**
|
||||
* Escape string for markdown.
|
||||
*
|
||||
* @param string $what String to escape
|
||||
*/
|
||||
public static function markdownEscape(string $what): string
|
||||
{
|
||||
return \str_replace(
|
||||
[
|
||||
'\\',
|
||||
'_',
|
||||
'*',
|
||||
'[',
|
||||
']',
|
||||
'(',
|
||||
')',
|
||||
'~',
|
||||
'`',
|
||||
'>',
|
||||
'#',
|
||||
'+',
|
||||
'-',
|
||||
'=',
|
||||
'|',
|
||||
'{',
|
||||
'}',
|
||||
'.',
|
||||
'!',
|
||||
],
|
||||
[
|
||||
'\\\\',
|
||||
'\\_',
|
||||
'\\*',
|
||||
'\\[',
|
||||
'\\]',
|
||||
'\\(',
|
||||
'\\)',
|
||||
'\\~',
|
||||
'\\`',
|
||||
'\\>',
|
||||
'\\#',
|
||||
'\\+',
|
||||
'\\-',
|
||||
'\\=',
|
||||
'\\|',
|
||||
'\\{',
|
||||
'\\}',
|
||||
'\\.',
|
||||
'\\!',
|
||||
],
|
||||
$what
|
||||
);
|
||||
}
|
||||
/**
|
||||
* Escape string for markdown codeblock.
|
||||
*
|
||||
* @param string $what String to escape
|
||||
*/
|
||||
public static function markdownCodeblockEscape(string $what): string
|
||||
{
|
||||
return \str_replace('```', '\\```', $what);
|
||||
}
|
||||
/**
|
||||
* Escape string for markdown code section.
|
||||
*
|
||||
* @param string $what String to escape
|
||||
*/
|
||||
public static function markdownCodeEscape(string $what): string
|
||||
{
|
||||
return \str_replace('`', '\\`', $what);
|
||||
}
|
||||
/**
|
||||
* Escape string for URL.
|
||||
*
|
||||
* @param string $what String to escape
|
||||
*/
|
||||
public static function markdownUrlEscape(string $what): string
|
||||
{
|
||||
return \str_replace(')', '\\)', $what);
|
||||
}
|
||||
}
|
638
tests/EntitiesTest.php
Normal file
638
tests/EntitiesTest.php
Normal file
@ -0,0 +1,638 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace danog\TestTelegramEntities;
|
||||
|
||||
use Amp\Http\Client\HttpClient;
|
||||
use Amp\Http\Client\HttpClientBuilder;
|
||||
use Amp\Http\Client\Request;
|
||||
use AssertionError;
|
||||
use danog\TelegramEntities\Entities;
|
||||
use danog\TelegramEntities\EntityTools;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
/** @internal */
|
||||
class EntitiesTest extends TestCase
|
||||
{
|
||||
private static HttpClient $client;
|
||||
public static function setUpBeforeClass(): void
|
||||
{
|
||||
self::$client = HttpClientBuilder::buildDefault();
|
||||
}
|
||||
public function testMb(): void
|
||||
{
|
||||
$this->assertEquals(1, EntityTools::mbStrlen('t'));
|
||||
$this->assertEquals(1, EntityTools::mbStrlen('я'));
|
||||
$this->assertEquals(2, EntityTools::mbStrlen('👍'));
|
||||
$this->assertEquals(4, EntityTools::mbStrlen('🇺🇦'));
|
||||
|
||||
$this->assertEquals('st', EntityTools::mbSubstr('test', 2));
|
||||
$this->assertEquals('aя', EntityTools::mbSubstr('aяaя', 2));
|
||||
$this->assertEquals('a👍', EntityTools::mbSubstr('a👍a👍', 3));
|
||||
$this->assertEquals('🇺🇦', EntityTools::mbSubstr('🇺🇦🇺🇦', 4));
|
||||
|
||||
$this->assertEquals(['te', 'st'], EntityTools::mbStrSplit('test', 2));
|
||||
$this->assertEquals(['aя', 'aя'], EntityTools::mbStrSplit('aяaя', 2));
|
||||
$this->assertEquals(['a👍', 'a👍'], EntityTools::mbStrSplit('a👍a👍', 3));
|
||||
$this->assertEquals(['🇺🇦', '🇺🇦'], EntityTools::mbStrSplit('🇺🇦🇺🇦', 4));
|
||||
}
|
||||
private static function render(string $message, string $parse_mode): Entities
|
||||
{
|
||||
return match ($parse_mode) {
|
||||
'html' => Entities::fromHtml($message),
|
||||
'markdown' => Entities::fromMarkdown($message),
|
||||
};
|
||||
}
|
||||
public function testEntities(): void
|
||||
{
|
||||
foreach ($this->provideEntities() as $params) {
|
||||
$this->testEntitiesInner(...$params);
|
||||
}
|
||||
}
|
||||
public function testUnclosed(): void
|
||||
{
|
||||
$this->expectExceptionMessage("Found unclosed markdown elements ](");
|
||||
Entities::fromMarkdown('[');
|
||||
}
|
||||
public function testUnclosedLink(): void
|
||||
{
|
||||
$this->expectExceptionMessage("Unclosed ) opened @ pos 7!");
|
||||
Entities::fromMarkdown('[test](https://google.com');
|
||||
}
|
||||
public function testUnclosedCode(): void
|
||||
{
|
||||
$this->expectExceptionMessage('Unclosed ``` opened @ pos 3!');
|
||||
Entities::fromMarkdown('```');
|
||||
}
|
||||
public function testStandalone(): void
|
||||
{
|
||||
$test = Entities::fromMarkdown(']');
|
||||
$this->assertEmpty($test->entities);
|
||||
$this->assertSame(']', $test->message);
|
||||
|
||||
$test = Entities::fromMarkdown('!!');
|
||||
$this->assertEmpty($test->entities);
|
||||
$this->assertSame('!!', $test->message);
|
||||
|
||||
$test = Entities::fromMarkdown('|');
|
||||
$this->assertEmpty($test->entities);
|
||||
$this->assertSame('|', $test->message);
|
||||
}
|
||||
private function testEntitiesInner(string $mode, string $html, string $bare, array $entities, ?string $htmlReverse = null): void
|
||||
{
|
||||
$result = self::render(message: $html, parse_mode: $mode);
|
||||
$this->assertEquals($bare, $result->message);
|
||||
$this->assertEquals($entities, $result->entities);
|
||||
|
||||
if (
|
||||
!\str_contains($html, 'tg://emoji')
|
||||
&& !\str_contains($html, '<br')
|
||||
&& !\str_contains($html, 'mention:')
|
||||
&& $html !== '[not a link]'
|
||||
&& $bare !== "a_b\n\\ ```"
|
||||
) {
|
||||
$token = \getenv("TOKEN");
|
||||
$dest = \getenv("DEST");
|
||||
if (!$token) {
|
||||
throw new AssertionError("A TOKEN environment variable must be defined to run the tests!");
|
||||
}
|
||||
if (!$dest) {
|
||||
throw new AssertionError("A DEST environment variable must be defined to run the tests!");
|
||||
}
|
||||
$resultApi = \json_decode(self::$client->request(new Request(
|
||||
"https://api.telegram.org/bot{$token}/sendMessage?".\http_build_query([
|
||||
'chat_id'=> $dest,
|
||||
'parse_mode'=> match ($mode) {
|
||||
'markdown' => 'MarkdownV2',
|
||||
'html' => 'html'
|
||||
},
|
||||
'text' => $html
|
||||
])
|
||||
))->getBody()->buffer(), true);
|
||||
|
||||
if (!isset($resultApi['result'])) {
|
||||
throw new AssertionError(\json_encode($resultApi));
|
||||
}
|
||||
|
||||
$entities = $resultApi['result']['entities'] ?? [];
|
||||
$entities = \array_map(function (array $e): array {
|
||||
if (isset($e['user'])) {
|
||||
$e['user'] = ['id' => $e['user']['id']];
|
||||
}
|
||||
return $e;
|
||||
}, $entities);
|
||||
$this->assertEquals($bare, $resultApi['result']['text']);
|
||||
$this->assertEquals($entities, $entities);
|
||||
}
|
||||
|
||||
if (\strtolower($mode) === 'html') {
|
||||
$this->assertEquals(
|
||||
\trim(\str_replace(['<br/>', ' </b>', 'mention:'], ['<br>', '</b> ', 'tg://user?id='], $htmlReverse ?? $html)),
|
||||
$result->toHTML(true)
|
||||
);
|
||||
$result = self::render(message: EntityTools::htmlEscape($html), parse_mode: $mode);
|
||||
$this->assertEquals($html, $result->message);
|
||||
$this->assertNoRelevantEntities($result->entities);
|
||||
} else {
|
||||
$result = self::render(message: EntityTools::markdownEscape($html), parse_mode: $mode);
|
||||
$this->assertEquals($html, $result->message);
|
||||
$this->assertNoRelevantEntities($result->entities);
|
||||
|
||||
$result = self::render(message: "```\n".EntityTools::markdownCodeblockEscape($html)."\n```", parse_mode: $mode);
|
||||
$this->assertEquals($html, \rtrim($result->message));
|
||||
$this->assertEquals([['offset' => 0, 'length' => EntityTools::mbStrlen($html), 'language' => '', 'type' => 'pre']], $result->entities);
|
||||
}
|
||||
}
|
||||
|
||||
private function assertNoRelevantEntities(array $entities): void
|
||||
{
|
||||
$entities = \array_filter($entities, static fn (array $e) => !\in_array(
|
||||
$e['type'],
|
||||
['url', 'email', 'phone_number', 'mention', 'bot_command'],
|
||||
true
|
||||
));
|
||||
$this->assertEmpty($entities);
|
||||
}
|
||||
|
||||
private function provideEntities(): array
|
||||
{
|
||||
return [
|
||||
[
|
||||
'html',
|
||||
'<b>test</b>',
|
||||
'test',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<b>test</b><br>test',
|
||||
"test\ntest",
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<b>test</b><br/>test',
|
||||
"test\ntest",
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'🇺🇦<b>🇺🇦</b>',
|
||||
'🇺🇦🇺🇦',
|
||||
[
|
||||
[
|
||||
'offset' => 4,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'test<b>test </b>',
|
||||
'testtest',
|
||||
[
|
||||
[
|
||||
'offset' => 4,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'è»test<b>test </b>test',
|
||||
'è»testtest test',
|
||||
[
|
||||
[
|
||||
'offset' => 6,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'test<b> test</b>',
|
||||
'test test',
|
||||
[
|
||||
[
|
||||
'offset' => 4,
|
||||
'length' => 5,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'test* test*',
|
||||
'test test',
|
||||
[
|
||||
[
|
||||
'offset' => 4,
|
||||
'length' => 5,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<b>test</b><br><i>test</i> <code>test</code> <pre language="html">test</pre> <a href="https://example.com/">test</a> <s>strikethrough</s> <u>underline</u> <blockquote>blockquote</blockquote> https://google.com daniil@daniil.it +39398172758722 @daniilgentili <tg-spoiler>spoiler</tg-spoiler> <b>not_bold</b>',
|
||||
"test\ntest test test test strikethrough underline blockquote https://google.com daniil@daniil.it +39398172758722 @daniilgentili spoiler <b>not_bold</b>",
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
[
|
||||
'offset' => 5,
|
||||
'length' => 4,
|
||||
'type' => 'italic',
|
||||
],
|
||||
[
|
||||
'offset' => 10,
|
||||
'length' => 4,
|
||||
'type' => 'code',
|
||||
],
|
||||
[
|
||||
'offset' => 15,
|
||||
'length' => 4,
|
||||
'language' => 'html',
|
||||
'type' => 'pre',
|
||||
],
|
||||
[
|
||||
'offset' => 20,
|
||||
'length' => 4,
|
||||
'url' => 'https://example.com/',
|
||||
'type' => 'text_link',
|
||||
],
|
||||
[
|
||||
'offset' => 25,
|
||||
'length' => 13,
|
||||
'type' => 'strikethrough',
|
||||
],
|
||||
[
|
||||
'offset' => 39,
|
||||
'length' => 9,
|
||||
'type' => 'underline',
|
||||
],
|
||||
[
|
||||
'offset' => 49,
|
||||
'length' => 10,
|
||||
'type' => 'block_quote',
|
||||
],
|
||||
[
|
||||
'offset' => 127,
|
||||
'length' => 7,
|
||||
'type' => 'spoiler',
|
||||
],
|
||||
],
|
||||
'<b>test</b><br><i>test</i> <code>test</code> <pre language="html">test</pre> <a href="https://example.com/">test</a> <s>strikethrough</s> <u>underline</u> <blockquote>blockquote</blockquote> https://google.com daniil@daniil.it +39398172758722 @daniilgentili <tg-spoiler>spoiler</tg-spoiler> <b>not_bold</b>',
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'test *bold _bold and italic_ bold*',
|
||||
'test bold bold and italic bold',
|
||||
[
|
||||
[
|
||||
'offset' => 10,
|
||||
'length' => 15,
|
||||
'type' => 'italic',
|
||||
],
|
||||
[
|
||||
'offset' => 5,
|
||||
'length' => 25,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
"a\nb\nc",
|
||||
"a\nb\nc",
|
||||
[],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
"a\n\nb\n\nc",
|
||||
"a\n\nb\n\nc",
|
||||
[],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
"a\n\n\nb\n\n\nc",
|
||||
"a\n\n\nb\n\n\nc",
|
||||
[],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
"a\n```php\n<?php\necho 'yay';\n```",
|
||||
"a\n<?php\necho 'yay';",
|
||||
[
|
||||
[
|
||||
'offset' => 2,
|
||||
'length' => 17,
|
||||
'type' => 'pre',
|
||||
'language' => 'php',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<b>\'"</b>',
|
||||
'\'"',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 2,
|
||||
'type' => 'bold',
|
||||
],
|
||||
],
|
||||
'<b>'"</b>',
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<a href="mention:101374607">mention1</a> <a href="tg://user?id=101374607">mention2</a>',
|
||||
'mention1 mention2',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 8,
|
||||
'type' => 'text_mention',
|
||||
'user' => ['id' => 101374607],
|
||||
],
|
||||
[
|
||||
'offset' => 9,
|
||||
'length' => 8,
|
||||
'type' => 'text_mention',
|
||||
'user' => ['id' => 101374607],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<a href="tg://user?id=101374607">mention1</a> <a href="tg://user?id=101374607">mention2</a>',
|
||||
'mention1 mention2',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 8,
|
||||
'type' => 'text_mention',
|
||||
'user' => ['id' => 101374607],
|
||||
],
|
||||
[
|
||||
'offset' => 9,
|
||||
'length' => 8,
|
||||
'type' => 'text_mention',
|
||||
'user' => ['id' => 101374607],
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'_a b c <b\> & " \' \_ \* \~ \\__',
|
||||
'a b c <b> & " \' _ * ~ _',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 23,
|
||||
'type' => 'italic',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
EntityTools::markdownEscape('\\ test testovich _*~'),
|
||||
'\\ test testovich _*~',
|
||||
[],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
"```\na_b\n".EntityTools::markdownCodeblockEscape('\\ ```').'```',
|
||||
"a_b\n\\ ```",
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 9,
|
||||
'type' => 'pre',
|
||||
'language' => '',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'`c_d '.EntityTools::markdownCodeEscape('`').'`',
|
||||
'c_d `',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 5,
|
||||
'type' => 'code',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'[link ](https://google.com/)test',
|
||||
'link test',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://google.com/',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'[link]('.EntityTools::markdownUrlEscape('https://transfer.sh/(/test/test.PNG,/test/test.MP4).zip').')',
|
||||
'link',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://transfer.sh/(/test/test.PNG,/test/test.MP4).zip',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'[link]('.EntityTools::markdownUrlEscape('https://google.com/').')',
|
||||
'link',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://google.com/',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'[link]('.EntityTools::markdownUrlEscape('https://google.com/?v=\\test').')',
|
||||
'link',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://google.com/?v=\\test',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'[link ](https://google.com/)',
|
||||
'link',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://google.com/',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'![link ](tg://emoji?id=5368324170671202286)',
|
||||
'link',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'custom_emoji',
|
||||
'custom_emoji_id' => 5368324170671202286,
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'[not a link]',
|
||||
'[not a link]',
|
||||
[],
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<a href="https://google.com/">link </a>test',
|
||||
'link test',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://google.com/',
|
||||
],
|
||||
],
|
||||
'<a href="https://google.com/">link</a> test',
|
||||
],
|
||||
[
|
||||
'html',
|
||||
'<a href="https://google.com/">link </a>',
|
||||
'link',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 4,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://google.com/',
|
||||
],
|
||||
],
|
||||
'<a href="https://google.com/">link</a> ',
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'test _italic_ *bold* __underlined__ ~strikethrough~ ```test pre``` `code` ||spoiler||',
|
||||
'test italic bold underlined strikethrough pre code spoiler',
|
||||
[
|
||||
[
|
||||
'offset' => 5,
|
||||
'length' => 6,
|
||||
'type' => 'italic',
|
||||
],
|
||||
[
|
||||
'offset' => 12,
|
||||
'length' => 4,
|
||||
'type' => 'bold',
|
||||
],
|
||||
[
|
||||
'offset' => 17,
|
||||
'length' => 10,
|
||||
'type' => 'underline',
|
||||
],
|
||||
[
|
||||
'offset' => 28,
|
||||
'length' => 13,
|
||||
'type' => 'strikethrough',
|
||||
],
|
||||
[
|
||||
'offset' => 42,
|
||||
'length' => 4,
|
||||
'type' => 'pre',
|
||||
'language' => 'test',
|
||||
],
|
||||
[
|
||||
'offset' => 47,
|
||||
'length' => 4,
|
||||
'type' => 'code',
|
||||
],
|
||||
[
|
||||
'offset' => 52,
|
||||
'length' => 7,
|
||||
'type' => 'spoiler',
|
||||
],
|
||||
],
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'[special link]('.EntityTools::markdownUrlEscape('https://google.com/)').')',
|
||||
'special link',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 12,
|
||||
'type' => 'text_link',
|
||||
'url' => 'https://google.com/)',
|
||||
],
|
||||
],
|
||||
'<a href="https://google.com/)">link</a> ',
|
||||
],
|
||||
[
|
||||
'markdown',
|
||||
'`'.EntityTools::markdownCodeEscape('``').'`',
|
||||
'``',
|
||||
[
|
||||
[
|
||||
'offset' => 0,
|
||||
'length' => 2,
|
||||
'type' => 'code',
|
||||
],
|
||||
],
|
||||
'`\`\``',
|
||||
],
|
||||
];
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue
Block a user