├── .github └── workflows │ ├── ci.yml │ └── gh-pages.yml ├── .gitignore ├── CHANGELOG.md ├── CUSTOM_SCHEMA.md ├── Cargo.lock ├── Cargo.toml ├── LICENSE-APACHE.md ├── LICENSE-MIT.md ├── README.md ├── V2_CHANGES.md ├── gh-pages-generator ├── Cargo.toml ├── base-schema.yml └── src │ ├── custom.rs │ ├── custom2.rs │ ├── main.rs │ └── openapi.rs ├── rustfmt.toml └── src ├── extractor.rs ├── lib.rs ├── parser.rs ├── parser ├── sentence.rs └── tags.rs └── util.rs /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches-ignore: 6 | - gh-pages 7 | pull_request: 8 | branches-ignore: 9 | - gh-pages 10 | 11 | jobs: 12 | test: 13 | runs-on: ubuntu-latest 14 | steps: 15 | - uses: actions/checkout@v2 16 | - uses: actions-rs/toolchain@v1 17 | with: 18 | profile: minimal 19 | toolchain: stable 20 | override: true 21 | - uses: actions-rs/cargo@v1 22 | with: 23 | command: generate-lockfile 24 | - name: Cache cargo registry 25 | uses: actions/cache@v1 26 | with: 27 | path: ~/.cargo/registry 28 | key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }} 29 | restore-keys: | 30 | ${{ runner.os }}-cargo-registry- 31 | - name: Cache cargo index 32 | uses: actions/cache@v1 33 | with: 34 | path: ~/.cargo/git 35 | key: ${{ runner.os }}-cargo-index-${{ hashFiles('**/Cargo.lock') }} 36 | restore-keys: | 37 | ${{ runner.os }}-cargo-index- 38 | - name: Cache cargo build 39 | uses: actions/cache@v1 40 | with: 41 | path: target 42 | key: ${{ runner.os }}-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} 43 | restore-keys: | 44 | ${{ runner.os }}-cargo-build-target- 45 | - uses: actions-rs/cargo@v1 46 | with: 47 | command: build 48 | args: --all --all-features --all-targets 49 | - uses: actions-rs/cargo@v1 50 | with: 51 | command: test 52 | args: --all --all-features 53 | rustfmt: 54 | runs-on: ubuntu-latest 55 | steps: 56 | - uses: actions/checkout@v2 57 | - name: Install toolchain 58 | uses: actions-rs/toolchain@v1 59 | with: 60 | profile: minimal 61 | toolchain: nightly 62 | override: true 63 | components: rustfmt 64 | - name: cargo fmt 65 | uses: actions-rs/cargo@v1 66 | with: 67 | command: fmt 68 | args: --all -- --check 69 | clippy: 70 | runs-on: ubuntu-latest 71 | steps: 72 | - uses: actions/checkout@v2 73 | - run: rustup component add clippy 74 | - uses: actions-rs/clippy-check@v1 75 | with: 76 | token: ${{ secrets.GITHUB_TOKEN }} 77 | args: --all --all-targets --all-features -- -D warnings 78 | deploy: 79 | runs-on: ubuntu-latest 80 | needs: test 81 | steps: 82 | - uses: actions/checkout@v2 83 | - name: Build generator 84 | uses: actions-rs/cargo@v1 85 | with: 86 | command: build 87 | args: --package gh-pages-generator --bin gh-pages-generator 88 | - name: Generate schemas 89 | uses: actions-rs/cargo@v1 90 | with: 91 | command: run 92 | args: --package gh-pages-generator --bin gh-pages-generator -- dev 93 | - name: Deploy 94 | uses: peaceiris/actions-gh-pages@v3 95 | with: 96 | github_token: ${{ secrets.GITHUB_TOKEN }} 97 | publish_dir: ./public 98 | publish_branch: gh-pages 99 | keep_files: true 100 | -------------------------------------------------------------------------------- /.github/workflows/gh-pages.yml: -------------------------------------------------------------------------------- 1 | name: GitHub Pages 2 | 3 | on: 4 | repository_dispatch: 5 | types: telegram_bot_api 6 | push: 7 | branches: 8 | - master 9 | schedule: 10 | - cron: '0 0 * * *' # every day at midnight 11 | 12 | jobs: 13 | deploy: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | with: 18 | ref: 'master' 19 | - name: Build generator 20 | uses: actions-rs/cargo@v1 21 | with: 22 | command: build 23 | args: --package gh-pages-generator --bin gh-pages-generator 24 | - name: Generate schemas 25 | uses: actions-rs/cargo@v1 26 | with: 27 | command: run 28 | args: --package gh-pages-generator --bin gh-pages-generator -- production 29 | - name: Generate (dev) schemas 30 | uses: actions-rs/cargo@v1 31 | with: 32 | command: run 33 | args: --package gh-pages-generator --bin gh-pages-generator -- dev 34 | - name: Deploy 35 | uses: peaceiris/actions-gh-pages@v3 36 | with: 37 | github_token: ${{ secrets.GITHUB_TOKEN }} 38 | publish_dir: ./public 39 | publish_branch: gh-pages 40 | keep_files: true 41 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target/ 2 | public/ 3 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | Format: vX.Y.Z (DD-MM-YYYY) 2 | 3 | # v0.6.0 (09.03.2024) 4 | 5 | Parse `icon_color` field enumeration 6 | 7 | # v0.5.0 (26.02.2024) 8 | 9 | * Update dependencies 10 | * Resolve #16 11 | 12 | # v0.4.4 (20.11.2022) 13 | 14 | Fix library tries to parse `default to the` as boolean 15 | 16 | # v0.4.3 (15.05.2022) 17 | 18 | Fix `Choose one` "one of" pattern is not recognized 19 | 20 | # v0.4.2 (17.04.2022) 21 | 22 | Fix links like `/bots/webapps` are not handled 23 | 24 | # v0.4.1 (16.03.2022) 25 | 26 | * Fix `deleteMessage` cannot be extracted 27 | * Fix `" base quote` sentence cannot be parsed 28 | * Fix description is not full for types like `PassportElementError` 29 | 30 | # v0.4.0 (05.07.2021) 31 | 32 | Yank v0.3.1 because of breaking changes 33 | 34 | # v0.3.1 (05.07.2021) 35 | 36 | * Add `min-max characters` min-max pattern for strings 37 | * Add `always "something"` default pattern 38 | 39 | # v0.3.0 (26.05.2021) 40 | 41 | * Yank v0.2.6 because of breaking changes 42 | * Return lexer error instead of panicking 43 | 44 | # v0.2.6 (24.05.2021) 45 | 46 | * Fix issue #11 47 | * Fix issue #9 48 | 49 | # v0.2.5 (09.05.2021) 50 | 51 | * Fix issue #7 52 | * Fix issue #8 53 | 54 | # v0.2.4 (25.04.2021) 55 | 56 | Update README.md (new domain for links) 57 | 58 | # v0.2.3 (01.12.2020) 59 | 60 | Add `Can be` pattern for enumeration detection 61 | Fix `
` tag ignored when converting HTML to plain text 62 | 63 | # v0.2.2 (30.11.2020) 64 | 65 | Fix type parsing like `Array of Array of PhotoSize` 66 | 67 | # v0.2.1 (28.11.2020) 68 | 69 | Add `either` pattern for enumeration detection 70 | 71 | # v0.2.0 (20.11.2020) 72 | 73 | Add `Unknown` variant for `ObjectData` to cope better with future changes 74 | -------------------------------------------------------------------------------- /CUSTOM_SCHEMA.md: -------------------------------------------------------------------------------- 1 | # Custom schema description 2 | 3 | ## Root 4 | 5 | ```json5 6 | { 7 | // semver of bot API 8 | "version": { 9 | "major": 5, 10 | "minor": 6, 11 | "patch": 0 12 | }, 13 | // when recent changes was made to bot API 14 | "recent_changes": { 15 | "year": 2021, 16 | "month": 12, 17 | "day": 30 18 | }, 19 | // contains list of Method objects 20 | "methods": [ 21 | ... 22 | ], 23 | // contains list of Object 24 | "objects": [ 25 | ... 26 | ] 27 | } 28 | ``` 29 | 30 | ## Type 31 | 32 | Basic structure: 33 | 34 | ```json5 35 | { 36 | "type": "string", 37 | // and some additional fields if applicable 38 | } 39 | ``` 40 | 41 | ### "type": "integer" 42 | 43 | ```json5 44 | { 45 | "type": "integer", 46 | // optional 47 | "default": 123, 48 | // optional 49 | "min": 0, 50 | // optional 51 | "max": 255, 52 | // there can be variants or list is empty 53 | "enumeration": [ 54 | 123, 55 | 23, 56 | 1423423 57 | ] 58 | } 59 | ``` 60 | 61 | ### "type": "string" 62 | 63 | ```json5 64 | { 65 | "type": "string", 66 | // optional 67 | "default": "apple", 68 | // optional 69 | "min_len": 0, 70 | // optional 71 | "max_len": 100, 72 | // there can be variants or list is empty 73 | "enumeration": [ 74 | "apple", 75 | "orange" 76 | ] 77 | } 78 | ``` 79 | 80 | ### "type": "bool" 81 | 82 | ```json5 83 | { 84 | "type": "bool", 85 | // optional 86 | "default": "false", 87 | } 88 | ``` 89 | 90 | ### "type": "float" 91 | 92 | No additional fields. 93 | 94 | ### "type": "any_of" 95 | 96 | ```json5 97 | { 98 | "type": "any_of", 99 | // list of Type objects 100 | "any_of": [ 101 | ... 102 | ] 103 | } 104 | ``` 105 | 106 | ### "type": "reference" 107 | 108 | ```json5 109 | { 110 | "type": "reference", 111 | // refers to some Object 112 | "reference": "Update" 113 | } 114 | ``` 115 | 116 | ### "type": "array" 117 | 118 | ```json5 119 | { 120 | "type": "bool", 121 | // refers to Type object 122 | "array": { 123 | ... 124 | } 125 | } 126 | ``` 127 | 128 | ## Method 129 | 130 | ```json5 131 | { 132 | "name": "getUpdates", 133 | "description": "Use this method to receive incoming updates using long polling ([wiki](https://en.wikipedia.org/wiki/Push_technology#Long_polling)). An Array of [Update](https://core.telegram.org/bots/api/#update) objects is returned.", 134 | // see Argument heading 135 | "arguments": [ 136 | ... 137 | ], 138 | // arguments may contain some Input* types (like InputMedia, InputFile, etc) 139 | // so it is recommended to send request using multipart/form-data 140 | "maybe_multipart": false, 141 | // see Type heading 142 | "return_type": { 143 | ... 144 | }, 145 | "documentation_link": "https://core.telegram.org/bots/api/#getupdates" 146 | } 147 | ``` 148 | 149 | ## Argument 150 | 151 | ```json5 152 | { 153 | "name": "allowed_updates", 154 | "description": "A JSON-serialized list of the update types you want your bot to receive. For example, specify [“message”, “edited\\_channel\\_post”, “callback\\_query”] to only receive updates of these types. See [Update](https://core.telegram.org/bots/api/#update) for a complete list of available update types. Specify an empty list to receive all update types except *chat\\_member* (default). If not specified, the previous setting will be used. \n\nPlease note that this parameter doesn't affect updates created before the call to the getUpdates, so unwanted updates may be received for a short period of time.", 155 | // is argument required to be presented? 156 | "required": false, 157 | // see Type heading 158 | "type_info": { 159 | ... 160 | } 161 | } 162 | ``` 163 | 164 | ## Object 165 | 166 | ```json5 167 | { 168 | "name": "GameHighScore", 169 | "description": "This object represents one row of the high scores table for a game.", 170 | // type of Object. Can be "properties", "any_of" or "unknown" 171 | "type": "properties", 172 | "documentation_link": "https://core.telegram.org/bots/api/#gamehighscore" 173 | } 174 | ``` 175 | 176 | Explanation of Object's `type` field: 177 | 178 | ### "type": "properties" 179 | 180 | ```json5 181 | { 182 | "name": "Update", 183 | "description": "This [object](https://core.telegram.org/bots/api/#available-types) represents an incoming update. \nAt most **one** of the optional parameters can be present in any given update.", 184 | "type": "properties", 185 | // see Property heading 186 | "properties": [ 187 | ... 188 | ], 189 | "documentation_link": "https://core.telegram.org/bots/api/#update" 190 | } 191 | ``` 192 | 193 | ### "type": "any_of" 194 | 195 | This field means that Object is consists of other Type objects. 196 | 197 | ```json5 198 | { 199 | "name": "PassportElementError", 200 | "description": "This object represents an error in the Telegram Passport element which was submitted that should be resolved by the user. It should be one of:", 201 | "type": "any_of", 202 | // list of Type objects 203 | "any_of": [ 204 | { 205 | "type": "reference", 206 | "reference": "PassportElementErrorDataField" 207 | }, 208 | { 209 | "type": "reference", 210 | "reference": "PassportElementErrorFrontSide" 211 | }, 212 | ... 213 | ], 214 | "documentation_link": "https://core.telegram.org/bots/api/#passportelementerror" 215 | } 216 | ``` 217 | 218 | ### "type": "unknown" 219 | 220 | This field means that Object nor has properties neither consists of other types 221 | 222 | ```json5 223 | { 224 | "name": "CallbackGame", 225 | "description": "A placeholder, currently holds no information. Use [BotFather](https://t.me/botfather) to set up your game.", 226 | "type": "unknown", 227 | "documentation_link": "https://core.telegram.org/bots/api/#callbackgame" 228 | } 229 | ``` 230 | 231 | ## Property 232 | 233 | ```json5 234 | { 235 | "name": "position", 236 | "description": "Position in high score table for the game", 237 | // is property required to be presented? 238 | "required": true, 239 | // see Type heading 240 | "type_info": { 241 | ... 242 | } 243 | } 244 | ``` 245 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "ahash" 7 | version = "0.8.9" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "d713b3834d76b85304d4d525563c1276e2e30dc97cc67bfb4585a4a29fc2c89f" 10 | dependencies = [ 11 | "cfg-if 1.0.0", 12 | "getrandom", 13 | "once_cell", 14 | "version_check", 15 | "zerocopy", 16 | ] 17 | 18 | [[package]] 19 | name = "aho-corasick" 20 | version = "1.1.2" 21 | source = "registry+https://github.com/rust-lang/crates.io-index" 22 | checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" 23 | dependencies = [ 24 | "memchr", 25 | ] 26 | 27 | [[package]] 28 | name = "android-tzdata" 29 | version = "0.1.1" 30 | source = "registry+https://github.com/rust-lang/crates.io-index" 31 | checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" 32 | 33 | [[package]] 34 | name = "android_system_properties" 35 | version = "0.1.5" 36 | source = "registry+https://github.com/rust-lang/crates.io-index" 37 | checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" 38 | dependencies = [ 39 | "libc", 40 | ] 41 | 42 | [[package]] 43 | name = "ansi_term" 44 | version = "0.12.1" 45 | source = "registry+https://github.com/rust-lang/crates.io-index" 46 | checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2" 47 | dependencies = [ 48 | "winapi 0.3.9", 49 | ] 50 | 51 | [[package]] 52 | name = "anyhow" 53 | version = "1.0.80" 54 | source = "registry+https://github.com/rust-lang/crates.io-index" 55 | checksum = "5ad32ce52e4161730f7098c077cd2ed6229b5804ccf99e5366be1ab72a98b4e1" 56 | 57 | [[package]] 58 | name = "atty" 59 | version = "0.2.14" 60 | source = "registry+https://github.com/rust-lang/crates.io-index" 61 | checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" 62 | dependencies = [ 63 | "hermit-abi 0.1.19", 64 | "libc", 65 | "winapi 0.3.9", 66 | ] 67 | 68 | [[package]] 69 | name = "autocfg" 70 | version = "1.1.0" 71 | source = "registry+https://github.com/rust-lang/crates.io-index" 72 | checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" 73 | 74 | [[package]] 75 | name = "base64" 76 | version = "0.13.1" 77 | source = "registry+https://github.com/rust-lang/crates.io-index" 78 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 79 | 80 | [[package]] 81 | name = "beef" 82 | version = "0.5.2" 83 | source = "registry+https://github.com/rust-lang/crates.io-index" 84 | checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" 85 | 86 | [[package]] 87 | name = "bitflags" 88 | version = "1.3.2" 89 | source = "registry+https://github.com/rust-lang/crates.io-index" 90 | checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" 91 | 92 | [[package]] 93 | name = "bitflags" 94 | version = "2.4.2" 95 | source = "registry+https://github.com/rust-lang/crates.io-index" 96 | checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" 97 | 98 | [[package]] 99 | name = "bumpalo" 100 | version = "3.15.3" 101 | source = "registry+https://github.com/rust-lang/crates.io-index" 102 | checksum = "8ea184aa71bb362a1157c896979544cc23974e08fd265f29ea96b59f0b4a555b" 103 | 104 | [[package]] 105 | name = "byteorder" 106 | version = "1.5.0" 107 | source = "registry+https://github.com/rust-lang/crates.io-index" 108 | checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" 109 | 110 | [[package]] 111 | name = "bytes" 112 | version = "0.5.6" 113 | source = "registry+https://github.com/rust-lang/crates.io-index" 114 | checksum = "0e4cec68f03f32e44924783795810fa50a7035d8c8ebe78580ad7e6c703fba38" 115 | 116 | [[package]] 117 | name = "bytes" 118 | version = "1.5.0" 119 | source = "registry+https://github.com/rust-lang/crates.io-index" 120 | checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" 121 | 122 | [[package]] 123 | name = "cc" 124 | version = "1.0.88" 125 | source = "registry+https://github.com/rust-lang/crates.io-index" 126 | checksum = "02f341c093d19155a6e41631ce5971aac4e9a868262212153124c15fa22d1cdc" 127 | 128 | [[package]] 129 | name = "cesu8" 130 | version = "1.1.0" 131 | source = "registry+https://github.com/rust-lang/crates.io-index" 132 | checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" 133 | 134 | [[package]] 135 | name = "cfg-if" 136 | version = "0.1.10" 137 | source = "registry+https://github.com/rust-lang/crates.io-index" 138 | checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" 139 | 140 | [[package]] 141 | name = "cfg-if" 142 | version = "1.0.0" 143 | source = "registry+https://github.com/rust-lang/crates.io-index" 144 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 145 | 146 | [[package]] 147 | name = "chrono" 148 | version = "0.4.34" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "5bc015644b92d5890fab7489e49d21f879d5c990186827d42ec511919404f38b" 151 | dependencies = [ 152 | "android-tzdata", 153 | "iana-time-zone", 154 | "js-sys", 155 | "num-traits", 156 | "wasm-bindgen", 157 | "windows-targets 0.52.3", 158 | ] 159 | 160 | [[package]] 161 | name = "clap" 162 | version = "2.34.0" 163 | source = "registry+https://github.com/rust-lang/crates.io-index" 164 | checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c" 165 | dependencies = [ 166 | "ansi_term", 167 | "atty", 168 | "bitflags 1.3.2", 169 | "strsim", 170 | "textwrap", 171 | "unicode-width", 172 | "vec_map", 173 | ] 174 | 175 | [[package]] 176 | name = "combine" 177 | version = "4.6.6" 178 | source = "registry+https://github.com/rust-lang/crates.io-index" 179 | checksum = "35ed6e9d84f0b51a7f52daf1c7d71dd136fd7a3f41a8462b8cdb8c78d920fad4" 180 | dependencies = [ 181 | "bytes 1.5.0", 182 | "memchr", 183 | ] 184 | 185 | [[package]] 186 | name = "core-foundation" 187 | version = "0.9.4" 188 | source = "registry+https://github.com/rust-lang/crates.io-index" 189 | checksum = "91e195e091a93c46f7102ec7818a2aa394e1e1771c3ab4825963fa03e45afb8f" 190 | dependencies = [ 191 | "core-foundation-sys", 192 | "libc", 193 | ] 194 | 195 | [[package]] 196 | name = "core-foundation-sys" 197 | version = "0.8.6" 198 | source = "registry+https://github.com/rust-lang/crates.io-index" 199 | checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" 200 | 201 | [[package]] 202 | name = "cssparser" 203 | version = "0.31.2" 204 | source = "registry+https://github.com/rust-lang/crates.io-index" 205 | checksum = "5b3df4f93e5fbbe73ec01ec8d3f68bba73107993a5b1e7519273c32db9b0d5be" 206 | dependencies = [ 207 | "cssparser-macros", 208 | "dtoa-short", 209 | "itoa 1.0.10", 210 | "phf", 211 | "smallvec", 212 | ] 213 | 214 | [[package]] 215 | name = "cssparser-macros" 216 | version = "0.6.1" 217 | source = "registry+https://github.com/rust-lang/crates.io-index" 218 | checksum = "13b588ba4ac1a99f7f2964d24b3d896ddc6bf847ee3855dbd4366f058cfcd331" 219 | dependencies = [ 220 | "quote", 221 | "syn 2.0.50", 222 | ] 223 | 224 | [[package]] 225 | name = "derive_more" 226 | version = "0.99.17" 227 | source = "registry+https://github.com/rust-lang/crates.io-index" 228 | checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" 229 | dependencies = [ 230 | "proc-macro2", 231 | "quote", 232 | "syn 1.0.109", 233 | ] 234 | 235 | [[package]] 236 | name = "dtoa" 237 | version = "1.0.9" 238 | source = "registry+https://github.com/rust-lang/crates.io-index" 239 | checksum = "dcbb2bf8e87535c23f7a8a321e364ce21462d0ff10cb6407820e8e96dfff6653" 240 | 241 | [[package]] 242 | name = "dtoa-short" 243 | version = "0.3.4" 244 | source = "registry+https://github.com/rust-lang/crates.io-index" 245 | checksum = "dbaceec3c6e4211c79e7b1800fb9680527106beb2f9c51904a3210c03a448c74" 246 | dependencies = [ 247 | "dtoa", 248 | ] 249 | 250 | [[package]] 251 | name = "dyn-clone" 252 | version = "1.0.16" 253 | source = "registry+https://github.com/rust-lang/crates.io-index" 254 | checksum = "545b22097d44f8a9581187cdf93de7a71e4722bf51200cfaba810865b49a495d" 255 | 256 | [[package]] 257 | name = "ego-tree" 258 | version = "0.6.2" 259 | source = "registry+https://github.com/rust-lang/crates.io-index" 260 | checksum = "3a68a4904193147e0a8dec3314640e6db742afd5f6e634f428a6af230d9b3591" 261 | 262 | [[package]] 263 | name = "either" 264 | version = "1.10.0" 265 | source = "registry+https://github.com/rust-lang/crates.io-index" 266 | checksum = "11157ac094ffbdde99aa67b23417ebdd801842852b500e395a45a9c0aac03e4a" 267 | 268 | [[package]] 269 | name = "encoding_rs" 270 | version = "0.8.33" 271 | source = "registry+https://github.com/rust-lang/crates.io-index" 272 | checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" 273 | dependencies = [ 274 | "cfg-if 1.0.0", 275 | ] 276 | 277 | [[package]] 278 | name = "env_logger" 279 | version = "0.7.1" 280 | source = "registry+https://github.com/rust-lang/crates.io-index" 281 | checksum = "44533bbbb3bb3c1fa17d9f2e4e38bbbaf8396ba82193c4cb1b6445d711445d36" 282 | dependencies = [ 283 | "atty", 284 | "humantime", 285 | "log", 286 | "regex", 287 | "termcolor", 288 | ] 289 | 290 | [[package]] 291 | name = "errno" 292 | version = "0.3.8" 293 | source = "registry+https://github.com/rust-lang/crates.io-index" 294 | checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" 295 | dependencies = [ 296 | "libc", 297 | "windows-sys", 298 | ] 299 | 300 | [[package]] 301 | name = "fastrand" 302 | version = "2.0.1" 303 | source = "registry+https://github.com/rust-lang/crates.io-index" 304 | checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" 305 | 306 | [[package]] 307 | name = "fnv" 308 | version = "1.0.7" 309 | source = "registry+https://github.com/rust-lang/crates.io-index" 310 | checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" 311 | 312 | [[package]] 313 | name = "foreign-types" 314 | version = "0.3.2" 315 | source = "registry+https://github.com/rust-lang/crates.io-index" 316 | checksum = "f6f339eb8adc052cd2ca78910fda869aefa38d22d5cb648e6485e4d3fc06f3b1" 317 | dependencies = [ 318 | "foreign-types-shared", 319 | ] 320 | 321 | [[package]] 322 | name = "foreign-types-shared" 323 | version = "0.1.1" 324 | source = "registry+https://github.com/rust-lang/crates.io-index" 325 | checksum = "00b0228411908ca8685dba7fc2cdd70ec9990a6e753e89b6ac91a84c40fbaf4b" 326 | 327 | [[package]] 328 | name = "form_urlencoded" 329 | version = "1.2.1" 330 | source = "registry+https://github.com/rust-lang/crates.io-index" 331 | checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" 332 | dependencies = [ 333 | "percent-encoding", 334 | ] 335 | 336 | [[package]] 337 | name = "fuchsia-zircon" 338 | version = "0.3.3" 339 | source = "registry+https://github.com/rust-lang/crates.io-index" 340 | checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82" 341 | dependencies = [ 342 | "bitflags 1.3.2", 343 | "fuchsia-zircon-sys", 344 | ] 345 | 346 | [[package]] 347 | name = "fuchsia-zircon-sys" 348 | version = "0.3.3" 349 | source = "registry+https://github.com/rust-lang/crates.io-index" 350 | checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7" 351 | 352 | [[package]] 353 | name = "futf" 354 | version = "0.1.5" 355 | source = "registry+https://github.com/rust-lang/crates.io-index" 356 | checksum = "df420e2e84819663797d1ec6544b13c5be84629e7bb00dc960d6917db2987843" 357 | dependencies = [ 358 | "mac", 359 | "new_debug_unreachable", 360 | ] 361 | 362 | [[package]] 363 | name = "futures-channel" 364 | version = "0.3.30" 365 | source = "registry+https://github.com/rust-lang/crates.io-index" 366 | checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" 367 | dependencies = [ 368 | "futures-core", 369 | ] 370 | 371 | [[package]] 372 | name = "futures-core" 373 | version = "0.3.30" 374 | source = "registry+https://github.com/rust-lang/crates.io-index" 375 | checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" 376 | 377 | [[package]] 378 | name = "futures-io" 379 | version = "0.3.30" 380 | source = "registry+https://github.com/rust-lang/crates.io-index" 381 | checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" 382 | 383 | [[package]] 384 | name = "futures-sink" 385 | version = "0.3.30" 386 | source = "registry+https://github.com/rust-lang/crates.io-index" 387 | checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" 388 | 389 | [[package]] 390 | name = "futures-task" 391 | version = "0.3.30" 392 | source = "registry+https://github.com/rust-lang/crates.io-index" 393 | checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" 394 | 395 | [[package]] 396 | name = "futures-util" 397 | version = "0.3.30" 398 | source = "registry+https://github.com/rust-lang/crates.io-index" 399 | checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" 400 | dependencies = [ 401 | "futures-core", 402 | "futures-io", 403 | "futures-task", 404 | "memchr", 405 | "pin-project-lite 0.2.13", 406 | "pin-utils", 407 | "slab", 408 | ] 409 | 410 | [[package]] 411 | name = "fxhash" 412 | version = "0.2.1" 413 | source = "registry+https://github.com/rust-lang/crates.io-index" 414 | checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" 415 | dependencies = [ 416 | "byteorder", 417 | ] 418 | 419 | [[package]] 420 | name = "getopts" 421 | version = "0.2.21" 422 | source = "registry+https://github.com/rust-lang/crates.io-index" 423 | checksum = "14dbbfd5c71d70241ecf9e6f13737f7b5ce823821063188d7e46c41d371eebd5" 424 | dependencies = [ 425 | "unicode-width", 426 | ] 427 | 428 | [[package]] 429 | name = "getrandom" 430 | version = "0.2.12" 431 | source = "registry+https://github.com/rust-lang/crates.io-index" 432 | checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" 433 | dependencies = [ 434 | "cfg-if 1.0.0", 435 | "libc", 436 | "wasi", 437 | ] 438 | 439 | [[package]] 440 | name = "gh-pages-generator" 441 | version = "0.1.0" 442 | dependencies = [ 443 | "anyhow", 444 | "chrono", 445 | "indexmap", 446 | "openapiv3", 447 | "pretty_env_logger", 448 | "pulldown-cmark", 449 | "reqwest", 450 | "schemars", 451 | "serde", 452 | "serde_json", 453 | "serde_yaml", 454 | "structopt", 455 | "tg-bot-api", 456 | ] 457 | 458 | [[package]] 459 | name = "h2" 460 | version = "0.2.7" 461 | source = "registry+https://github.com/rust-lang/crates.io-index" 462 | checksum = "5e4728fd124914ad25e99e3d15a9361a879f6620f63cb56bbb08f95abb97a535" 463 | dependencies = [ 464 | "bytes 0.5.6", 465 | "fnv", 466 | "futures-core", 467 | "futures-sink", 468 | "futures-util", 469 | "http", 470 | "indexmap", 471 | "slab", 472 | "tokio", 473 | "tokio-util", 474 | "tracing", 475 | "tracing-futures", 476 | ] 477 | 478 | [[package]] 479 | name = "hashbrown" 480 | version = "0.12.3" 481 | source = "registry+https://github.com/rust-lang/crates.io-index" 482 | checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" 483 | 484 | [[package]] 485 | name = "heck" 486 | version = "0.3.3" 487 | source = "registry+https://github.com/rust-lang/crates.io-index" 488 | checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" 489 | dependencies = [ 490 | "unicode-segmentation", 491 | ] 492 | 493 | [[package]] 494 | name = "hermit-abi" 495 | version = "0.1.19" 496 | source = "registry+https://github.com/rust-lang/crates.io-index" 497 | checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" 498 | dependencies = [ 499 | "libc", 500 | ] 501 | 502 | [[package]] 503 | name = "hermit-abi" 504 | version = "0.3.8" 505 | source = "registry+https://github.com/rust-lang/crates.io-index" 506 | checksum = "379dada1584ad501b383485dd706b8afb7a70fcbc7f4da7d780638a5a6124a60" 507 | 508 | [[package]] 509 | name = "html2md" 510 | version = "0.2.14" 511 | source = "registry+https://github.com/rust-lang/crates.io-index" 512 | checksum = "be92446e11d68f5d71367d571c229d09ced1f24ab6d08ea0bff329d5f6c0b2a3" 513 | dependencies = [ 514 | "html5ever", 515 | "jni", 516 | "lazy_static", 517 | "markup5ever_rcdom", 518 | "percent-encoding", 519 | "regex", 520 | ] 521 | 522 | [[package]] 523 | name = "html5ever" 524 | version = "0.26.0" 525 | source = "registry+https://github.com/rust-lang/crates.io-index" 526 | checksum = "bea68cab48b8459f17cf1c944c67ddc572d272d9f2b274140f223ecb1da4a3b7" 527 | dependencies = [ 528 | "log", 529 | "mac", 530 | "markup5ever", 531 | "proc-macro2", 532 | "quote", 533 | "syn 1.0.109", 534 | ] 535 | 536 | [[package]] 537 | name = "http" 538 | version = "0.2.11" 539 | source = "registry+https://github.com/rust-lang/crates.io-index" 540 | checksum = "8947b1a6fad4393052c7ba1f4cd97bed3e953a95c79c92ad9b051a04611d9fbb" 541 | dependencies = [ 542 | "bytes 1.5.0", 543 | "fnv", 544 | "itoa 1.0.10", 545 | ] 546 | 547 | [[package]] 548 | name = "http-body" 549 | version = "0.3.1" 550 | source = "registry+https://github.com/rust-lang/crates.io-index" 551 | checksum = "13d5ff830006f7646652e057693569bfe0d51760c0085a071769d142a205111b" 552 | dependencies = [ 553 | "bytes 0.5.6", 554 | "http", 555 | ] 556 | 557 | [[package]] 558 | name = "httparse" 559 | version = "1.8.0" 560 | source = "registry+https://github.com/rust-lang/crates.io-index" 561 | checksum = "d897f394bad6a705d5f4104762e116a75639e470d80901eed05a860a95cb1904" 562 | 563 | [[package]] 564 | name = "httpdate" 565 | version = "0.3.2" 566 | source = "registry+https://github.com/rust-lang/crates.io-index" 567 | checksum = "494b4d60369511e7dea41cf646832512a94e542f68bb9c49e54518e0f468eb47" 568 | 569 | [[package]] 570 | name = "humantime" 571 | version = "1.3.0" 572 | source = "registry+https://github.com/rust-lang/crates.io-index" 573 | checksum = "df004cfca50ef23c36850aaaa59ad52cc70d0e90243c3c7737a4dd32dc7a3c4f" 574 | dependencies = [ 575 | "quick-error", 576 | ] 577 | 578 | [[package]] 579 | name = "hyper" 580 | version = "0.13.10" 581 | source = "registry+https://github.com/rust-lang/crates.io-index" 582 | checksum = "8a6f157065790a3ed2f88679250419b5cdd96e714a0d65f7797fd337186e96bb" 583 | dependencies = [ 584 | "bytes 0.5.6", 585 | "futures-channel", 586 | "futures-core", 587 | "futures-util", 588 | "h2", 589 | "http", 590 | "http-body", 591 | "httparse", 592 | "httpdate", 593 | "itoa 0.4.8", 594 | "pin-project", 595 | "socket2", 596 | "tokio", 597 | "tower-service", 598 | "tracing", 599 | "want", 600 | ] 601 | 602 | [[package]] 603 | name = "hyper-tls" 604 | version = "0.4.3" 605 | source = "registry+https://github.com/rust-lang/crates.io-index" 606 | checksum = "d979acc56dcb5b8dddba3917601745e877576475aa046df3226eabdecef78eed" 607 | dependencies = [ 608 | "bytes 0.5.6", 609 | "hyper", 610 | "native-tls", 611 | "tokio", 612 | "tokio-tls", 613 | ] 614 | 615 | [[package]] 616 | name = "iana-time-zone" 617 | version = "0.1.60" 618 | source = "registry+https://github.com/rust-lang/crates.io-index" 619 | checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141" 620 | dependencies = [ 621 | "android_system_properties", 622 | "core-foundation-sys", 623 | "iana-time-zone-haiku", 624 | "js-sys", 625 | "wasm-bindgen", 626 | "windows-core", 627 | ] 628 | 629 | [[package]] 630 | name = "iana-time-zone-haiku" 631 | version = "0.1.2" 632 | source = "registry+https://github.com/rust-lang/crates.io-index" 633 | checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" 634 | dependencies = [ 635 | "cc", 636 | ] 637 | 638 | [[package]] 639 | name = "idna" 640 | version = "0.5.0" 641 | source = "registry+https://github.com/rust-lang/crates.io-index" 642 | checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" 643 | dependencies = [ 644 | "unicode-bidi", 645 | "unicode-normalization", 646 | ] 647 | 648 | [[package]] 649 | name = "indexmap" 650 | version = "1.9.3" 651 | source = "registry+https://github.com/rust-lang/crates.io-index" 652 | checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" 653 | dependencies = [ 654 | "autocfg", 655 | "hashbrown", 656 | "serde", 657 | ] 658 | 659 | [[package]] 660 | name = "iovec" 661 | version = "0.1.4" 662 | source = "registry+https://github.com/rust-lang/crates.io-index" 663 | checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e" 664 | dependencies = [ 665 | "libc", 666 | ] 667 | 668 | [[package]] 669 | name = "ipnet" 670 | version = "2.9.0" 671 | source = "registry+https://github.com/rust-lang/crates.io-index" 672 | checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" 673 | 674 | [[package]] 675 | name = "itertools" 676 | version = "0.12.1" 677 | source = "registry+https://github.com/rust-lang/crates.io-index" 678 | checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" 679 | dependencies = [ 680 | "either", 681 | ] 682 | 683 | [[package]] 684 | name = "itoa" 685 | version = "0.4.8" 686 | source = "registry+https://github.com/rust-lang/crates.io-index" 687 | checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" 688 | 689 | [[package]] 690 | name = "itoa" 691 | version = "1.0.10" 692 | source = "registry+https://github.com/rust-lang/crates.io-index" 693 | checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" 694 | 695 | [[package]] 696 | name = "jni" 697 | version = "0.19.0" 698 | source = "registry+https://github.com/rust-lang/crates.io-index" 699 | checksum = "c6df18c2e3db7e453d3c6ac5b3e9d5182664d28788126d39b91f2d1e22b017ec" 700 | dependencies = [ 701 | "cesu8", 702 | "combine", 703 | "jni-sys", 704 | "log", 705 | "thiserror", 706 | "walkdir", 707 | ] 708 | 709 | [[package]] 710 | name = "jni-sys" 711 | version = "0.3.0" 712 | source = "registry+https://github.com/rust-lang/crates.io-index" 713 | checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" 714 | 715 | [[package]] 716 | name = "js-sys" 717 | version = "0.3.68" 718 | source = "registry+https://github.com/rust-lang/crates.io-index" 719 | checksum = "406cda4b368d531c842222cf9d2600a9a4acce8d29423695379c6868a143a9ee" 720 | dependencies = [ 721 | "wasm-bindgen", 722 | ] 723 | 724 | [[package]] 725 | name = "kernel32-sys" 726 | version = "0.2.2" 727 | source = "registry+https://github.com/rust-lang/crates.io-index" 728 | checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d" 729 | dependencies = [ 730 | "winapi 0.2.8", 731 | "winapi-build", 732 | ] 733 | 734 | [[package]] 735 | name = "lazy_static" 736 | version = "1.4.0" 737 | source = "registry+https://github.com/rust-lang/crates.io-index" 738 | checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" 739 | 740 | [[package]] 741 | name = "libc" 742 | version = "0.2.153" 743 | source = "registry+https://github.com/rust-lang/crates.io-index" 744 | checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd" 745 | 746 | [[package]] 747 | name = "linked-hash-map" 748 | version = "0.5.6" 749 | source = "registry+https://github.com/rust-lang/crates.io-index" 750 | checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" 751 | 752 | [[package]] 753 | name = "linux-raw-sys" 754 | version = "0.4.13" 755 | source = "registry+https://github.com/rust-lang/crates.io-index" 756 | checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" 757 | 758 | [[package]] 759 | name = "lock_api" 760 | version = "0.4.11" 761 | source = "registry+https://github.com/rust-lang/crates.io-index" 762 | checksum = "3c168f8615b12bc01f9c17e2eb0cc07dcae1940121185446edc3744920e8ef45" 763 | dependencies = [ 764 | "autocfg", 765 | "scopeguard", 766 | ] 767 | 768 | [[package]] 769 | name = "log" 770 | version = "0.4.20" 771 | source = "registry+https://github.com/rust-lang/crates.io-index" 772 | checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" 773 | 774 | [[package]] 775 | name = "logos" 776 | version = "0.14.0" 777 | source = "registry+https://github.com/rust-lang/crates.io-index" 778 | checksum = "161971eb88a0da7ae0c333e1063467c5b5727e7fb6b710b8db4814eade3a42e8" 779 | dependencies = [ 780 | "logos-derive", 781 | ] 782 | 783 | [[package]] 784 | name = "logos-codegen" 785 | version = "0.14.0" 786 | source = "registry+https://github.com/rust-lang/crates.io-index" 787 | checksum = "8e31badd9de5131fdf4921f6473d457e3dd85b11b7f091ceb50e4df7c3eeb12a" 788 | dependencies = [ 789 | "beef", 790 | "fnv", 791 | "lazy_static", 792 | "proc-macro2", 793 | "quote", 794 | "regex-syntax", 795 | "syn 2.0.50", 796 | ] 797 | 798 | [[package]] 799 | name = "logos-derive" 800 | version = "0.14.0" 801 | source = "registry+https://github.com/rust-lang/crates.io-index" 802 | checksum = "1c2a69b3eb68d5bd595107c9ee58d7e07fe2bb5e360cc85b0f084dedac80de0a" 803 | dependencies = [ 804 | "logos-codegen", 805 | ] 806 | 807 | [[package]] 808 | name = "mac" 809 | version = "0.1.1" 810 | source = "registry+https://github.com/rust-lang/crates.io-index" 811 | checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4" 812 | 813 | [[package]] 814 | name = "markup5ever" 815 | version = "0.11.0" 816 | source = "registry+https://github.com/rust-lang/crates.io-index" 817 | checksum = "7a2629bb1404f3d34c2e921f21fd34ba00b206124c81f65c50b43b6aaefeb016" 818 | dependencies = [ 819 | "log", 820 | "phf", 821 | "phf_codegen", 822 | "string_cache", 823 | "string_cache_codegen", 824 | "tendril", 825 | ] 826 | 827 | [[package]] 828 | name = "markup5ever_rcdom" 829 | version = "0.2.0" 830 | source = "registry+https://github.com/rust-lang/crates.io-index" 831 | checksum = "b9521dd6750f8e80ee6c53d65e2e4656d7de37064f3a7a5d2d11d05df93839c2" 832 | dependencies = [ 833 | "html5ever", 834 | "markup5ever", 835 | "tendril", 836 | "xml5ever", 837 | ] 838 | 839 | [[package]] 840 | name = "memchr" 841 | version = "2.7.1" 842 | source = "registry+https://github.com/rust-lang/crates.io-index" 843 | checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" 844 | 845 | [[package]] 846 | name = "mime" 847 | version = "0.3.17" 848 | source = "registry+https://github.com/rust-lang/crates.io-index" 849 | checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" 850 | 851 | [[package]] 852 | name = "mime_guess" 853 | version = "2.0.4" 854 | source = "registry+https://github.com/rust-lang/crates.io-index" 855 | checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" 856 | dependencies = [ 857 | "mime", 858 | "unicase", 859 | ] 860 | 861 | [[package]] 862 | name = "mio" 863 | version = "0.6.23" 864 | source = "registry+https://github.com/rust-lang/crates.io-index" 865 | checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4" 866 | dependencies = [ 867 | "cfg-if 0.1.10", 868 | "fuchsia-zircon", 869 | "fuchsia-zircon-sys", 870 | "iovec", 871 | "kernel32-sys", 872 | "libc", 873 | "log", 874 | "miow", 875 | "net2", 876 | "slab", 877 | "winapi 0.2.8", 878 | ] 879 | 880 | [[package]] 881 | name = "miow" 882 | version = "0.2.2" 883 | source = "registry+https://github.com/rust-lang/crates.io-index" 884 | checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d" 885 | dependencies = [ 886 | "kernel32-sys", 887 | "net2", 888 | "winapi 0.2.8", 889 | "ws2_32-sys", 890 | ] 891 | 892 | [[package]] 893 | name = "native-tls" 894 | version = "0.2.11" 895 | source = "registry+https://github.com/rust-lang/crates.io-index" 896 | checksum = "07226173c32f2926027b63cce4bcd8076c3552846cbe7925f3aaffeac0a3b92e" 897 | dependencies = [ 898 | "lazy_static", 899 | "libc", 900 | "log", 901 | "openssl", 902 | "openssl-probe", 903 | "openssl-sys", 904 | "schannel", 905 | "security-framework", 906 | "security-framework-sys", 907 | "tempfile", 908 | ] 909 | 910 | [[package]] 911 | name = "net2" 912 | version = "0.2.39" 913 | source = "registry+https://github.com/rust-lang/crates.io-index" 914 | checksum = "b13b648036a2339d06de780866fbdfda0dde886de7b3af2ddeba8b14f4ee34ac" 915 | dependencies = [ 916 | "cfg-if 0.1.10", 917 | "libc", 918 | "winapi 0.3.9", 919 | ] 920 | 921 | [[package]] 922 | name = "new_debug_unreachable" 923 | version = "1.0.4" 924 | source = "registry+https://github.com/rust-lang/crates.io-index" 925 | checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54" 926 | 927 | [[package]] 928 | name = "num-traits" 929 | version = "0.2.18" 930 | source = "registry+https://github.com/rust-lang/crates.io-index" 931 | checksum = "da0df0e5185db44f69b44f26786fe401b6c293d1907744beaa7fa62b2e5a517a" 932 | dependencies = [ 933 | "autocfg", 934 | ] 935 | 936 | [[package]] 937 | name = "num_cpus" 938 | version = "1.16.0" 939 | source = "registry+https://github.com/rust-lang/crates.io-index" 940 | checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" 941 | dependencies = [ 942 | "hermit-abi 0.3.8", 943 | "libc", 944 | ] 945 | 946 | [[package]] 947 | name = "once_cell" 948 | version = "1.19.0" 949 | source = "registry+https://github.com/rust-lang/crates.io-index" 950 | checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" 951 | 952 | [[package]] 953 | name = "openapiv3" 954 | version = "0.5.0" 955 | source = "registry+https://github.com/rust-lang/crates.io-index" 956 | checksum = "90228d34f20d9fff3174d19b0d41e67f52711045270b540a2a2c2dc41ccb4085" 957 | dependencies = [ 958 | "indexmap", 959 | "serde", 960 | "serde_json", 961 | "serde_yaml", 962 | ] 963 | 964 | [[package]] 965 | name = "openssl" 966 | version = "0.10.64" 967 | source = "registry+https://github.com/rust-lang/crates.io-index" 968 | checksum = "95a0481286a310808298130d22dd1fef0fa571e05a8f44ec801801e84b216b1f" 969 | dependencies = [ 970 | "bitflags 2.4.2", 971 | "cfg-if 1.0.0", 972 | "foreign-types", 973 | "libc", 974 | "once_cell", 975 | "openssl-macros", 976 | "openssl-sys", 977 | ] 978 | 979 | [[package]] 980 | name = "openssl-macros" 981 | version = "0.1.1" 982 | source = "registry+https://github.com/rust-lang/crates.io-index" 983 | checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" 984 | dependencies = [ 985 | "proc-macro2", 986 | "quote", 987 | "syn 2.0.50", 988 | ] 989 | 990 | [[package]] 991 | name = "openssl-probe" 992 | version = "0.1.5" 993 | source = "registry+https://github.com/rust-lang/crates.io-index" 994 | checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" 995 | 996 | [[package]] 997 | name = "openssl-sys" 998 | version = "0.9.101" 999 | source = "registry+https://github.com/rust-lang/crates.io-index" 1000 | checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" 1001 | dependencies = [ 1002 | "cc", 1003 | "libc", 1004 | "pkg-config", 1005 | "vcpkg", 1006 | ] 1007 | 1008 | [[package]] 1009 | name = "parking_lot" 1010 | version = "0.12.1" 1011 | source = "registry+https://github.com/rust-lang/crates.io-index" 1012 | checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" 1013 | dependencies = [ 1014 | "lock_api", 1015 | "parking_lot_core", 1016 | ] 1017 | 1018 | [[package]] 1019 | name = "parking_lot_core" 1020 | version = "0.9.9" 1021 | source = "registry+https://github.com/rust-lang/crates.io-index" 1022 | checksum = "4c42a9226546d68acdd9c0a280d17ce19bfe27a46bf68784e4066115788d008e" 1023 | dependencies = [ 1024 | "cfg-if 1.0.0", 1025 | "libc", 1026 | "redox_syscall", 1027 | "smallvec", 1028 | "windows-targets 0.48.5", 1029 | ] 1030 | 1031 | [[package]] 1032 | name = "percent-encoding" 1033 | version = "2.3.1" 1034 | source = "registry+https://github.com/rust-lang/crates.io-index" 1035 | checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" 1036 | 1037 | [[package]] 1038 | name = "phf" 1039 | version = "0.10.1" 1040 | source = "registry+https://github.com/rust-lang/crates.io-index" 1041 | checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259" 1042 | dependencies = [ 1043 | "phf_macros", 1044 | "phf_shared", 1045 | "proc-macro-hack", 1046 | ] 1047 | 1048 | [[package]] 1049 | name = "phf_codegen" 1050 | version = "0.10.0" 1051 | source = "registry+https://github.com/rust-lang/crates.io-index" 1052 | checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd" 1053 | dependencies = [ 1054 | "phf_generator", 1055 | "phf_shared", 1056 | ] 1057 | 1058 | [[package]] 1059 | name = "phf_generator" 1060 | version = "0.10.0" 1061 | source = "registry+https://github.com/rust-lang/crates.io-index" 1062 | checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6" 1063 | dependencies = [ 1064 | "phf_shared", 1065 | "rand", 1066 | ] 1067 | 1068 | [[package]] 1069 | name = "phf_macros" 1070 | version = "0.10.0" 1071 | source = "registry+https://github.com/rust-lang/crates.io-index" 1072 | checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0" 1073 | dependencies = [ 1074 | "phf_generator", 1075 | "phf_shared", 1076 | "proc-macro-hack", 1077 | "proc-macro2", 1078 | "quote", 1079 | "syn 1.0.109", 1080 | ] 1081 | 1082 | [[package]] 1083 | name = "phf_shared" 1084 | version = "0.10.0" 1085 | source = "registry+https://github.com/rust-lang/crates.io-index" 1086 | checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096" 1087 | dependencies = [ 1088 | "siphasher", 1089 | ] 1090 | 1091 | [[package]] 1092 | name = "pin-project" 1093 | version = "1.1.4" 1094 | source = "registry+https://github.com/rust-lang/crates.io-index" 1095 | checksum = "0302c4a0442c456bd56f841aee5c3bfd17967563f6fadc9ceb9f9c23cf3807e0" 1096 | dependencies = [ 1097 | "pin-project-internal", 1098 | ] 1099 | 1100 | [[package]] 1101 | name = "pin-project-internal" 1102 | version = "1.1.4" 1103 | source = "registry+https://github.com/rust-lang/crates.io-index" 1104 | checksum = "266c042b60c9c76b8d53061e52b2e0d1116abc57cefc8c5cd671619a56ac3690" 1105 | dependencies = [ 1106 | "proc-macro2", 1107 | "quote", 1108 | "syn 2.0.50", 1109 | ] 1110 | 1111 | [[package]] 1112 | name = "pin-project-lite" 1113 | version = "0.1.12" 1114 | source = "registry+https://github.com/rust-lang/crates.io-index" 1115 | checksum = "257b64915a082f7811703966789728173279bdebb956b143dbcd23f6f970a777" 1116 | 1117 | [[package]] 1118 | name = "pin-project-lite" 1119 | version = "0.2.13" 1120 | source = "registry+https://github.com/rust-lang/crates.io-index" 1121 | checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" 1122 | 1123 | [[package]] 1124 | name = "pin-utils" 1125 | version = "0.1.0" 1126 | source = "registry+https://github.com/rust-lang/crates.io-index" 1127 | checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" 1128 | 1129 | [[package]] 1130 | name = "pkg-config" 1131 | version = "0.3.30" 1132 | source = "registry+https://github.com/rust-lang/crates.io-index" 1133 | checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" 1134 | 1135 | [[package]] 1136 | name = "ppv-lite86" 1137 | version = "0.2.17" 1138 | source = "registry+https://github.com/rust-lang/crates.io-index" 1139 | checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" 1140 | 1141 | [[package]] 1142 | name = "precomputed-hash" 1143 | version = "0.1.1" 1144 | source = "registry+https://github.com/rust-lang/crates.io-index" 1145 | checksum = "925383efa346730478fb4838dbe9137d2a47675ad789c546d150a6e1dd4ab31c" 1146 | 1147 | [[package]] 1148 | name = "pretty_env_logger" 1149 | version = "0.4.0" 1150 | source = "registry+https://github.com/rust-lang/crates.io-index" 1151 | checksum = "926d36b9553851b8b0005f1275891b392ee4d2d833852c417ed025477350fb9d" 1152 | dependencies = [ 1153 | "env_logger", 1154 | "log", 1155 | ] 1156 | 1157 | [[package]] 1158 | name = "proc-macro-error" 1159 | version = "1.0.4" 1160 | source = "registry+https://github.com/rust-lang/crates.io-index" 1161 | checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" 1162 | dependencies = [ 1163 | "proc-macro-error-attr", 1164 | "proc-macro2", 1165 | "quote", 1166 | "syn 1.0.109", 1167 | "version_check", 1168 | ] 1169 | 1170 | [[package]] 1171 | name = "proc-macro-error-attr" 1172 | version = "1.0.4" 1173 | source = "registry+https://github.com/rust-lang/crates.io-index" 1174 | checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" 1175 | dependencies = [ 1176 | "proc-macro2", 1177 | "quote", 1178 | "version_check", 1179 | ] 1180 | 1181 | [[package]] 1182 | name = "proc-macro-hack" 1183 | version = "0.5.20+deprecated" 1184 | source = "registry+https://github.com/rust-lang/crates.io-index" 1185 | checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068" 1186 | 1187 | [[package]] 1188 | name = "proc-macro2" 1189 | version = "1.0.78" 1190 | source = "registry+https://github.com/rust-lang/crates.io-index" 1191 | checksum = "e2422ad645d89c99f8f3e6b88a9fdeca7fabeac836b1002371c4367c8f984aae" 1192 | dependencies = [ 1193 | "unicode-ident", 1194 | ] 1195 | 1196 | [[package]] 1197 | name = "pulldown-cmark" 1198 | version = "0.8.0" 1199 | source = "registry+https://github.com/rust-lang/crates.io-index" 1200 | checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8" 1201 | dependencies = [ 1202 | "bitflags 1.3.2", 1203 | "getopts", 1204 | "memchr", 1205 | "unicase", 1206 | ] 1207 | 1208 | [[package]] 1209 | name = "quick-error" 1210 | version = "1.2.3" 1211 | source = "registry+https://github.com/rust-lang/crates.io-index" 1212 | checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" 1213 | 1214 | [[package]] 1215 | name = "quote" 1216 | version = "1.0.35" 1217 | source = "registry+https://github.com/rust-lang/crates.io-index" 1218 | checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" 1219 | dependencies = [ 1220 | "proc-macro2", 1221 | ] 1222 | 1223 | [[package]] 1224 | name = "rand" 1225 | version = "0.8.5" 1226 | source = "registry+https://github.com/rust-lang/crates.io-index" 1227 | checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" 1228 | dependencies = [ 1229 | "libc", 1230 | "rand_chacha", 1231 | "rand_core", 1232 | ] 1233 | 1234 | [[package]] 1235 | name = "rand_chacha" 1236 | version = "0.3.1" 1237 | source = "registry+https://github.com/rust-lang/crates.io-index" 1238 | checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" 1239 | dependencies = [ 1240 | "ppv-lite86", 1241 | "rand_core", 1242 | ] 1243 | 1244 | [[package]] 1245 | name = "rand_core" 1246 | version = "0.6.4" 1247 | source = "registry+https://github.com/rust-lang/crates.io-index" 1248 | checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" 1249 | dependencies = [ 1250 | "getrandom", 1251 | ] 1252 | 1253 | [[package]] 1254 | name = "redox_syscall" 1255 | version = "0.4.1" 1256 | source = "registry+https://github.com/rust-lang/crates.io-index" 1257 | checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" 1258 | dependencies = [ 1259 | "bitflags 1.3.2", 1260 | ] 1261 | 1262 | [[package]] 1263 | name = "regex" 1264 | version = "1.10.3" 1265 | source = "registry+https://github.com/rust-lang/crates.io-index" 1266 | checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" 1267 | dependencies = [ 1268 | "aho-corasick", 1269 | "memchr", 1270 | "regex-automata", 1271 | "regex-syntax", 1272 | ] 1273 | 1274 | [[package]] 1275 | name = "regex-automata" 1276 | version = "0.4.5" 1277 | source = "registry+https://github.com/rust-lang/crates.io-index" 1278 | checksum = "5bb987efffd3c6d0d8f5f89510bb458559eab11e4f869acb20bf845e016259cd" 1279 | dependencies = [ 1280 | "aho-corasick", 1281 | "memchr", 1282 | "regex-syntax", 1283 | ] 1284 | 1285 | [[package]] 1286 | name = "regex-syntax" 1287 | version = "0.8.2" 1288 | source = "registry+https://github.com/rust-lang/crates.io-index" 1289 | checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" 1290 | 1291 | [[package]] 1292 | name = "reqwest" 1293 | version = "0.10.10" 1294 | source = "registry+https://github.com/rust-lang/crates.io-index" 1295 | checksum = "0718f81a8e14c4dbb3b34cf23dc6aaf9ab8a0dfec160c534b3dbca1aaa21f47c" 1296 | dependencies = [ 1297 | "base64", 1298 | "bytes 0.5.6", 1299 | "encoding_rs", 1300 | "futures-core", 1301 | "futures-util", 1302 | "http", 1303 | "http-body", 1304 | "hyper", 1305 | "hyper-tls", 1306 | "ipnet", 1307 | "js-sys", 1308 | "lazy_static", 1309 | "log", 1310 | "mime", 1311 | "mime_guess", 1312 | "native-tls", 1313 | "percent-encoding", 1314 | "pin-project-lite 0.2.13", 1315 | "serde", 1316 | "serde_urlencoded", 1317 | "tokio", 1318 | "tokio-tls", 1319 | "url", 1320 | "wasm-bindgen", 1321 | "wasm-bindgen-futures", 1322 | "web-sys", 1323 | "winreg", 1324 | ] 1325 | 1326 | [[package]] 1327 | name = "rustix" 1328 | version = "0.38.31" 1329 | source = "registry+https://github.com/rust-lang/crates.io-index" 1330 | checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" 1331 | dependencies = [ 1332 | "bitflags 2.4.2", 1333 | "errno", 1334 | "libc", 1335 | "linux-raw-sys", 1336 | "windows-sys", 1337 | ] 1338 | 1339 | [[package]] 1340 | name = "ryu" 1341 | version = "1.0.17" 1342 | source = "registry+https://github.com/rust-lang/crates.io-index" 1343 | checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1" 1344 | 1345 | [[package]] 1346 | name = "same-file" 1347 | version = "1.0.6" 1348 | source = "registry+https://github.com/rust-lang/crates.io-index" 1349 | checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" 1350 | dependencies = [ 1351 | "winapi-util", 1352 | ] 1353 | 1354 | [[package]] 1355 | name = "schannel" 1356 | version = "0.1.23" 1357 | source = "registry+https://github.com/rust-lang/crates.io-index" 1358 | checksum = "fbc91545643bcf3a0bbb6569265615222618bdf33ce4ffbbd13c4bbd4c093534" 1359 | dependencies = [ 1360 | "windows-sys", 1361 | ] 1362 | 1363 | [[package]] 1364 | name = "schemars" 1365 | version = "0.8.16" 1366 | source = "registry+https://github.com/rust-lang/crates.io-index" 1367 | checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" 1368 | dependencies = [ 1369 | "dyn-clone", 1370 | "schemars_derive", 1371 | "serde", 1372 | "serde_json", 1373 | ] 1374 | 1375 | [[package]] 1376 | name = "schemars_derive" 1377 | version = "0.8.16" 1378 | source = "registry+https://github.com/rust-lang/crates.io-index" 1379 | checksum = "c767fd6fa65d9ccf9cf026122c1b555f2ef9a4f0cea69da4d7dbc3e258d30967" 1380 | dependencies = [ 1381 | "proc-macro2", 1382 | "quote", 1383 | "serde_derive_internals", 1384 | "syn 1.0.109", 1385 | ] 1386 | 1387 | [[package]] 1388 | name = "scopeguard" 1389 | version = "1.2.0" 1390 | source = "registry+https://github.com/rust-lang/crates.io-index" 1391 | checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" 1392 | 1393 | [[package]] 1394 | name = "scraper" 1395 | version = "0.18.1" 1396 | source = "registry+https://github.com/rust-lang/crates.io-index" 1397 | checksum = "585480e3719b311b78a573db1c9d9c4c1f8010c2dee4cc59c2efe58ea4dbc3e1" 1398 | dependencies = [ 1399 | "ahash", 1400 | "cssparser", 1401 | "ego-tree", 1402 | "getopts", 1403 | "html5ever", 1404 | "once_cell", 1405 | "selectors", 1406 | "tendril", 1407 | ] 1408 | 1409 | [[package]] 1410 | name = "security-framework" 1411 | version = "2.9.2" 1412 | source = "registry+https://github.com/rust-lang/crates.io-index" 1413 | checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" 1414 | dependencies = [ 1415 | "bitflags 1.3.2", 1416 | "core-foundation", 1417 | "core-foundation-sys", 1418 | "libc", 1419 | "security-framework-sys", 1420 | ] 1421 | 1422 | [[package]] 1423 | name = "security-framework-sys" 1424 | version = "2.9.1" 1425 | source = "registry+https://github.com/rust-lang/crates.io-index" 1426 | checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" 1427 | dependencies = [ 1428 | "core-foundation-sys", 1429 | "libc", 1430 | ] 1431 | 1432 | [[package]] 1433 | name = "selectors" 1434 | version = "0.25.0" 1435 | source = "registry+https://github.com/rust-lang/crates.io-index" 1436 | checksum = "4eb30575f3638fc8f6815f448d50cb1a2e255b0897985c8c59f4d37b72a07b06" 1437 | dependencies = [ 1438 | "bitflags 2.4.2", 1439 | "cssparser", 1440 | "derive_more", 1441 | "fxhash", 1442 | "log", 1443 | "new_debug_unreachable", 1444 | "phf", 1445 | "phf_codegen", 1446 | "precomputed-hash", 1447 | "servo_arc", 1448 | "smallvec", 1449 | ] 1450 | 1451 | [[package]] 1452 | name = "semver" 1453 | version = "1.0.22" 1454 | source = "registry+https://github.com/rust-lang/crates.io-index" 1455 | checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" 1456 | 1457 | [[package]] 1458 | name = "serde" 1459 | version = "1.0.197" 1460 | source = "registry+https://github.com/rust-lang/crates.io-index" 1461 | checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2" 1462 | dependencies = [ 1463 | "serde_derive", 1464 | ] 1465 | 1466 | [[package]] 1467 | name = "serde_derive" 1468 | version = "1.0.197" 1469 | source = "registry+https://github.com/rust-lang/crates.io-index" 1470 | checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" 1471 | dependencies = [ 1472 | "proc-macro2", 1473 | "quote", 1474 | "syn 2.0.50", 1475 | ] 1476 | 1477 | [[package]] 1478 | name = "serde_derive_internals" 1479 | version = "0.26.0" 1480 | source = "registry+https://github.com/rust-lang/crates.io-index" 1481 | checksum = "85bf8229e7920a9f636479437026331ce11aa132b4dde37d121944a44d6e5f3c" 1482 | dependencies = [ 1483 | "proc-macro2", 1484 | "quote", 1485 | "syn 1.0.109", 1486 | ] 1487 | 1488 | [[package]] 1489 | name = "serde_json" 1490 | version = "1.0.114" 1491 | source = "registry+https://github.com/rust-lang/crates.io-index" 1492 | checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" 1493 | dependencies = [ 1494 | "itoa 1.0.10", 1495 | "ryu", 1496 | "serde", 1497 | ] 1498 | 1499 | [[package]] 1500 | name = "serde_urlencoded" 1501 | version = "0.7.1" 1502 | source = "registry+https://github.com/rust-lang/crates.io-index" 1503 | checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" 1504 | dependencies = [ 1505 | "form_urlencoded", 1506 | "itoa 1.0.10", 1507 | "ryu", 1508 | "serde", 1509 | ] 1510 | 1511 | [[package]] 1512 | name = "serde_yaml" 1513 | version = "0.8.26" 1514 | source = "registry+https://github.com/rust-lang/crates.io-index" 1515 | checksum = "578a7433b776b56a35785ed5ce9a7e777ac0598aac5a6dd1b4b18a307c7fc71b" 1516 | dependencies = [ 1517 | "indexmap", 1518 | "ryu", 1519 | "serde", 1520 | "yaml-rust", 1521 | ] 1522 | 1523 | [[package]] 1524 | name = "servo_arc" 1525 | version = "0.3.0" 1526 | source = "registry+https://github.com/rust-lang/crates.io-index" 1527 | checksum = "d036d71a959e00c77a63538b90a6c2390969f9772b096ea837205c6bd0491a44" 1528 | dependencies = [ 1529 | "stable_deref_trait", 1530 | ] 1531 | 1532 | [[package]] 1533 | name = "siphasher" 1534 | version = "0.3.11" 1535 | source = "registry+https://github.com/rust-lang/crates.io-index" 1536 | checksum = "38b58827f4464d87d377d175e90bf58eb00fd8716ff0a62f80356b5e61555d0d" 1537 | 1538 | [[package]] 1539 | name = "slab" 1540 | version = "0.4.9" 1541 | source = "registry+https://github.com/rust-lang/crates.io-index" 1542 | checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" 1543 | dependencies = [ 1544 | "autocfg", 1545 | ] 1546 | 1547 | [[package]] 1548 | name = "smallvec" 1549 | version = "1.13.1" 1550 | source = "registry+https://github.com/rust-lang/crates.io-index" 1551 | checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" 1552 | 1553 | [[package]] 1554 | name = "socket2" 1555 | version = "0.3.19" 1556 | source = "registry+https://github.com/rust-lang/crates.io-index" 1557 | checksum = "122e570113d28d773067fab24266b66753f6ea915758651696b6e35e49f88d6e" 1558 | dependencies = [ 1559 | "cfg-if 1.0.0", 1560 | "libc", 1561 | "winapi 0.3.9", 1562 | ] 1563 | 1564 | [[package]] 1565 | name = "stable_deref_trait" 1566 | version = "1.2.0" 1567 | source = "registry+https://github.com/rust-lang/crates.io-index" 1568 | checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" 1569 | 1570 | [[package]] 1571 | name = "string_cache" 1572 | version = "0.8.7" 1573 | source = "registry+https://github.com/rust-lang/crates.io-index" 1574 | checksum = "f91138e76242f575eb1d3b38b4f1362f10d3a43f47d182a5b359af488a02293b" 1575 | dependencies = [ 1576 | "new_debug_unreachable", 1577 | "once_cell", 1578 | "parking_lot", 1579 | "phf_shared", 1580 | "precomputed-hash", 1581 | "serde", 1582 | ] 1583 | 1584 | [[package]] 1585 | name = "string_cache_codegen" 1586 | version = "0.5.2" 1587 | source = "registry+https://github.com/rust-lang/crates.io-index" 1588 | checksum = "6bb30289b722be4ff74a408c3cc27edeaad656e06cb1fe8fa9231fa59c728988" 1589 | dependencies = [ 1590 | "phf_generator", 1591 | "phf_shared", 1592 | "proc-macro2", 1593 | "quote", 1594 | ] 1595 | 1596 | [[package]] 1597 | name = "strsim" 1598 | version = "0.8.0" 1599 | source = "registry+https://github.com/rust-lang/crates.io-index" 1600 | checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a" 1601 | 1602 | [[package]] 1603 | name = "structopt" 1604 | version = "0.3.26" 1605 | source = "registry+https://github.com/rust-lang/crates.io-index" 1606 | checksum = "0c6b5c64445ba8094a6ab0c3cd2ad323e07171012d9c98b0b15651daf1787a10" 1607 | dependencies = [ 1608 | "clap", 1609 | "lazy_static", 1610 | "structopt-derive", 1611 | ] 1612 | 1613 | [[package]] 1614 | name = "structopt-derive" 1615 | version = "0.4.18" 1616 | source = "registry+https://github.com/rust-lang/crates.io-index" 1617 | checksum = "dcb5ae327f9cc13b68763b5749770cb9e048a99bd9dfdfa58d0cf05d5f64afe0" 1618 | dependencies = [ 1619 | "heck", 1620 | "proc-macro-error", 1621 | "proc-macro2", 1622 | "quote", 1623 | "syn 1.0.109", 1624 | ] 1625 | 1626 | [[package]] 1627 | name = "syn" 1628 | version = "1.0.109" 1629 | source = "registry+https://github.com/rust-lang/crates.io-index" 1630 | checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" 1631 | dependencies = [ 1632 | "proc-macro2", 1633 | "quote", 1634 | "unicode-ident", 1635 | ] 1636 | 1637 | [[package]] 1638 | name = "syn" 1639 | version = "2.0.50" 1640 | source = "registry+https://github.com/rust-lang/crates.io-index" 1641 | checksum = "74f1bdc9872430ce9b75da68329d1c1746faf50ffac5f19e02b71e37ff881ffb" 1642 | dependencies = [ 1643 | "proc-macro2", 1644 | "quote", 1645 | "unicode-ident", 1646 | ] 1647 | 1648 | [[package]] 1649 | name = "tempfile" 1650 | version = "3.10.0" 1651 | source = "registry+https://github.com/rust-lang/crates.io-index" 1652 | checksum = "a365e8cd18e44762ef95d87f284f4b5cd04107fec2ff3052bd6a3e6069669e67" 1653 | dependencies = [ 1654 | "cfg-if 1.0.0", 1655 | "fastrand", 1656 | "rustix", 1657 | "windows-sys", 1658 | ] 1659 | 1660 | [[package]] 1661 | name = "tendril" 1662 | version = "0.4.3" 1663 | source = "registry+https://github.com/rust-lang/crates.io-index" 1664 | checksum = "d24a120c5fc464a3458240ee02c299ebcb9d67b5249c8848b09d639dca8d7bb0" 1665 | dependencies = [ 1666 | "futf", 1667 | "mac", 1668 | "utf-8", 1669 | ] 1670 | 1671 | [[package]] 1672 | name = "termcolor" 1673 | version = "1.4.1" 1674 | source = "registry+https://github.com/rust-lang/crates.io-index" 1675 | checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" 1676 | dependencies = [ 1677 | "winapi-util", 1678 | ] 1679 | 1680 | [[package]] 1681 | name = "textwrap" 1682 | version = "0.11.0" 1683 | source = "registry+https://github.com/rust-lang/crates.io-index" 1684 | checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060" 1685 | dependencies = [ 1686 | "unicode-width", 1687 | ] 1688 | 1689 | [[package]] 1690 | name = "tg-bot-api" 1691 | version = "0.6.0" 1692 | dependencies = [ 1693 | "chrono", 1694 | "ego-tree", 1695 | "html2md", 1696 | "itertools", 1697 | "log", 1698 | "logos", 1699 | "percent-encoding", 1700 | "scraper", 1701 | "semver", 1702 | "tendril", 1703 | "thiserror", 1704 | ] 1705 | 1706 | [[package]] 1707 | name = "thiserror" 1708 | version = "1.0.57" 1709 | source = "registry+https://github.com/rust-lang/crates.io-index" 1710 | checksum = "1e45bcbe8ed29775f228095caf2cd67af7a4ccf756ebff23a306bf3e8b47b24b" 1711 | dependencies = [ 1712 | "thiserror-impl", 1713 | ] 1714 | 1715 | [[package]] 1716 | name = "thiserror-impl" 1717 | version = "1.0.57" 1718 | source = "registry+https://github.com/rust-lang/crates.io-index" 1719 | checksum = "a953cb265bef375dae3de6663da4d3804eee9682ea80d8e2542529b73c531c81" 1720 | dependencies = [ 1721 | "proc-macro2", 1722 | "quote", 1723 | "syn 2.0.50", 1724 | ] 1725 | 1726 | [[package]] 1727 | name = "tinyvec" 1728 | version = "1.6.0" 1729 | source = "registry+https://github.com/rust-lang/crates.io-index" 1730 | checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" 1731 | dependencies = [ 1732 | "tinyvec_macros", 1733 | ] 1734 | 1735 | [[package]] 1736 | name = "tinyvec_macros" 1737 | version = "0.1.1" 1738 | source = "registry+https://github.com/rust-lang/crates.io-index" 1739 | checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" 1740 | 1741 | [[package]] 1742 | name = "tokio" 1743 | version = "0.2.25" 1744 | source = "registry+https://github.com/rust-lang/crates.io-index" 1745 | checksum = "6703a273949a90131b290be1fe7b039d0fc884aa1935860dfcbe056f28cd8092" 1746 | dependencies = [ 1747 | "bytes 0.5.6", 1748 | "fnv", 1749 | "futures-core", 1750 | "iovec", 1751 | "lazy_static", 1752 | "memchr", 1753 | "mio", 1754 | "num_cpus", 1755 | "pin-project-lite 0.1.12", 1756 | "slab", 1757 | ] 1758 | 1759 | [[package]] 1760 | name = "tokio-tls" 1761 | version = "0.3.1" 1762 | source = "registry+https://github.com/rust-lang/crates.io-index" 1763 | checksum = "9a70f4fcd7b3b24fb194f837560168208f669ca8cb70d0c4b862944452396343" 1764 | dependencies = [ 1765 | "native-tls", 1766 | "tokio", 1767 | ] 1768 | 1769 | [[package]] 1770 | name = "tokio-util" 1771 | version = "0.3.1" 1772 | source = "registry+https://github.com/rust-lang/crates.io-index" 1773 | checksum = "be8242891f2b6cbef26a2d7e8605133c2c554cd35b3e4948ea892d6d68436499" 1774 | dependencies = [ 1775 | "bytes 0.5.6", 1776 | "futures-core", 1777 | "futures-sink", 1778 | "log", 1779 | "pin-project-lite 0.1.12", 1780 | "tokio", 1781 | ] 1782 | 1783 | [[package]] 1784 | name = "tower-service" 1785 | version = "0.3.2" 1786 | source = "registry+https://github.com/rust-lang/crates.io-index" 1787 | checksum = "b6bc1c9ce2b5135ac7f93c72918fc37feb872bdc6a5533a8b85eb4b86bfdae52" 1788 | 1789 | [[package]] 1790 | name = "tracing" 1791 | version = "0.1.40" 1792 | source = "registry+https://github.com/rust-lang/crates.io-index" 1793 | checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" 1794 | dependencies = [ 1795 | "log", 1796 | "pin-project-lite 0.2.13", 1797 | "tracing-core", 1798 | ] 1799 | 1800 | [[package]] 1801 | name = "tracing-core" 1802 | version = "0.1.32" 1803 | source = "registry+https://github.com/rust-lang/crates.io-index" 1804 | checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" 1805 | dependencies = [ 1806 | "once_cell", 1807 | ] 1808 | 1809 | [[package]] 1810 | name = "tracing-futures" 1811 | version = "0.2.5" 1812 | source = "registry+https://github.com/rust-lang/crates.io-index" 1813 | checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" 1814 | dependencies = [ 1815 | "pin-project", 1816 | "tracing", 1817 | ] 1818 | 1819 | [[package]] 1820 | name = "try-lock" 1821 | version = "0.2.5" 1822 | source = "registry+https://github.com/rust-lang/crates.io-index" 1823 | checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" 1824 | 1825 | [[package]] 1826 | name = "unicase" 1827 | version = "2.7.0" 1828 | source = "registry+https://github.com/rust-lang/crates.io-index" 1829 | checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" 1830 | dependencies = [ 1831 | "version_check", 1832 | ] 1833 | 1834 | [[package]] 1835 | name = "unicode-bidi" 1836 | version = "0.3.15" 1837 | source = "registry+https://github.com/rust-lang/crates.io-index" 1838 | checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" 1839 | 1840 | [[package]] 1841 | name = "unicode-ident" 1842 | version = "1.0.12" 1843 | source = "registry+https://github.com/rust-lang/crates.io-index" 1844 | checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" 1845 | 1846 | [[package]] 1847 | name = "unicode-normalization" 1848 | version = "0.1.23" 1849 | source = "registry+https://github.com/rust-lang/crates.io-index" 1850 | checksum = "a56d1686db2308d901306f92a263857ef59ea39678a5458e7cb17f01415101f5" 1851 | dependencies = [ 1852 | "tinyvec", 1853 | ] 1854 | 1855 | [[package]] 1856 | name = "unicode-segmentation" 1857 | version = "1.11.0" 1858 | source = "registry+https://github.com/rust-lang/crates.io-index" 1859 | checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" 1860 | 1861 | [[package]] 1862 | name = "unicode-width" 1863 | version = "0.1.11" 1864 | source = "registry+https://github.com/rust-lang/crates.io-index" 1865 | checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" 1866 | 1867 | [[package]] 1868 | name = "url" 1869 | version = "2.5.0" 1870 | source = "registry+https://github.com/rust-lang/crates.io-index" 1871 | checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" 1872 | dependencies = [ 1873 | "form_urlencoded", 1874 | "idna", 1875 | "percent-encoding", 1876 | ] 1877 | 1878 | [[package]] 1879 | name = "utf-8" 1880 | version = "0.7.6" 1881 | source = "registry+https://github.com/rust-lang/crates.io-index" 1882 | checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" 1883 | 1884 | [[package]] 1885 | name = "vcpkg" 1886 | version = "0.2.15" 1887 | source = "registry+https://github.com/rust-lang/crates.io-index" 1888 | checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" 1889 | 1890 | [[package]] 1891 | name = "vec_map" 1892 | version = "0.8.2" 1893 | source = "registry+https://github.com/rust-lang/crates.io-index" 1894 | checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191" 1895 | 1896 | [[package]] 1897 | name = "version_check" 1898 | version = "0.9.4" 1899 | source = "registry+https://github.com/rust-lang/crates.io-index" 1900 | checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" 1901 | 1902 | [[package]] 1903 | name = "walkdir" 1904 | version = "2.4.0" 1905 | source = "registry+https://github.com/rust-lang/crates.io-index" 1906 | checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" 1907 | dependencies = [ 1908 | "same-file", 1909 | "winapi-util", 1910 | ] 1911 | 1912 | [[package]] 1913 | name = "want" 1914 | version = "0.3.1" 1915 | source = "registry+https://github.com/rust-lang/crates.io-index" 1916 | checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" 1917 | dependencies = [ 1918 | "try-lock", 1919 | ] 1920 | 1921 | [[package]] 1922 | name = "wasi" 1923 | version = "0.11.0+wasi-snapshot-preview1" 1924 | source = "registry+https://github.com/rust-lang/crates.io-index" 1925 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 1926 | 1927 | [[package]] 1928 | name = "wasm-bindgen" 1929 | version = "0.2.91" 1930 | source = "registry+https://github.com/rust-lang/crates.io-index" 1931 | checksum = "c1e124130aee3fb58c5bdd6b639a0509486b0338acaaae0c84a5124b0f588b7f" 1932 | dependencies = [ 1933 | "cfg-if 1.0.0", 1934 | "serde", 1935 | "serde_json", 1936 | "wasm-bindgen-macro", 1937 | ] 1938 | 1939 | [[package]] 1940 | name = "wasm-bindgen-backend" 1941 | version = "0.2.91" 1942 | source = "registry+https://github.com/rust-lang/crates.io-index" 1943 | checksum = "c9e7e1900c352b609c8488ad12639a311045f40a35491fb69ba8c12f758af70b" 1944 | dependencies = [ 1945 | "bumpalo", 1946 | "log", 1947 | "once_cell", 1948 | "proc-macro2", 1949 | "quote", 1950 | "syn 2.0.50", 1951 | "wasm-bindgen-shared", 1952 | ] 1953 | 1954 | [[package]] 1955 | name = "wasm-bindgen-futures" 1956 | version = "0.4.41" 1957 | source = "registry+https://github.com/rust-lang/crates.io-index" 1958 | checksum = "877b9c3f61ceea0e56331985743b13f3d25c406a7098d45180fb5f09bc19ed97" 1959 | dependencies = [ 1960 | "cfg-if 1.0.0", 1961 | "js-sys", 1962 | "wasm-bindgen", 1963 | "web-sys", 1964 | ] 1965 | 1966 | [[package]] 1967 | name = "wasm-bindgen-macro" 1968 | version = "0.2.91" 1969 | source = "registry+https://github.com/rust-lang/crates.io-index" 1970 | checksum = "b30af9e2d358182b5c7449424f017eba305ed32a7010509ede96cdc4696c46ed" 1971 | dependencies = [ 1972 | "quote", 1973 | "wasm-bindgen-macro-support", 1974 | ] 1975 | 1976 | [[package]] 1977 | name = "wasm-bindgen-macro-support" 1978 | version = "0.2.91" 1979 | source = "registry+https://github.com/rust-lang/crates.io-index" 1980 | checksum = "642f325be6301eb8107a83d12a8ac6c1e1c54345a7ef1a9261962dfefda09e66" 1981 | dependencies = [ 1982 | "proc-macro2", 1983 | "quote", 1984 | "syn 2.0.50", 1985 | "wasm-bindgen-backend", 1986 | "wasm-bindgen-shared", 1987 | ] 1988 | 1989 | [[package]] 1990 | name = "wasm-bindgen-shared" 1991 | version = "0.2.91" 1992 | source = "registry+https://github.com/rust-lang/crates.io-index" 1993 | checksum = "4f186bd2dcf04330886ce82d6f33dd75a7bfcf69ecf5763b89fcde53b6ac9838" 1994 | 1995 | [[package]] 1996 | name = "web-sys" 1997 | version = "0.3.68" 1998 | source = "registry+https://github.com/rust-lang/crates.io-index" 1999 | checksum = "96565907687f7aceb35bc5fc03770a8a0471d82e479f25832f54a0e3f4b28446" 2000 | dependencies = [ 2001 | "js-sys", 2002 | "wasm-bindgen", 2003 | ] 2004 | 2005 | [[package]] 2006 | name = "winapi" 2007 | version = "0.2.8" 2008 | source = "registry+https://github.com/rust-lang/crates.io-index" 2009 | checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a" 2010 | 2011 | [[package]] 2012 | name = "winapi" 2013 | version = "0.3.9" 2014 | source = "registry+https://github.com/rust-lang/crates.io-index" 2015 | checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" 2016 | dependencies = [ 2017 | "winapi-i686-pc-windows-gnu", 2018 | "winapi-x86_64-pc-windows-gnu", 2019 | ] 2020 | 2021 | [[package]] 2022 | name = "winapi-build" 2023 | version = "0.1.1" 2024 | source = "registry+https://github.com/rust-lang/crates.io-index" 2025 | checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc" 2026 | 2027 | [[package]] 2028 | name = "winapi-i686-pc-windows-gnu" 2029 | version = "0.4.0" 2030 | source = "registry+https://github.com/rust-lang/crates.io-index" 2031 | checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" 2032 | 2033 | [[package]] 2034 | name = "winapi-util" 2035 | version = "0.1.6" 2036 | source = "registry+https://github.com/rust-lang/crates.io-index" 2037 | checksum = "f29e6f9198ba0d26b4c9f07dbe6f9ed633e1f3d5b8b414090084349e46a52596" 2038 | dependencies = [ 2039 | "winapi 0.3.9", 2040 | ] 2041 | 2042 | [[package]] 2043 | name = "winapi-x86_64-pc-windows-gnu" 2044 | version = "0.4.0" 2045 | source = "registry+https://github.com/rust-lang/crates.io-index" 2046 | checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" 2047 | 2048 | [[package]] 2049 | name = "windows-core" 2050 | version = "0.52.0" 2051 | source = "registry+https://github.com/rust-lang/crates.io-index" 2052 | checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" 2053 | dependencies = [ 2054 | "windows-targets 0.52.3", 2055 | ] 2056 | 2057 | [[package]] 2058 | name = "windows-sys" 2059 | version = "0.52.0" 2060 | source = "registry+https://github.com/rust-lang/crates.io-index" 2061 | checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" 2062 | dependencies = [ 2063 | "windows-targets 0.52.3", 2064 | ] 2065 | 2066 | [[package]] 2067 | name = "windows-targets" 2068 | version = "0.48.5" 2069 | source = "registry+https://github.com/rust-lang/crates.io-index" 2070 | checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" 2071 | dependencies = [ 2072 | "windows_aarch64_gnullvm 0.48.5", 2073 | "windows_aarch64_msvc 0.48.5", 2074 | "windows_i686_gnu 0.48.5", 2075 | "windows_i686_msvc 0.48.5", 2076 | "windows_x86_64_gnu 0.48.5", 2077 | "windows_x86_64_gnullvm 0.48.5", 2078 | "windows_x86_64_msvc 0.48.5", 2079 | ] 2080 | 2081 | [[package]] 2082 | name = "windows-targets" 2083 | version = "0.52.3" 2084 | source = "registry+https://github.com/rust-lang/crates.io-index" 2085 | checksum = "d380ba1dc7187569a8a9e91ed34b8ccfc33123bbacb8c0aed2d1ad7f3ef2dc5f" 2086 | dependencies = [ 2087 | "windows_aarch64_gnullvm 0.52.3", 2088 | "windows_aarch64_msvc 0.52.3", 2089 | "windows_i686_gnu 0.52.3", 2090 | "windows_i686_msvc 0.52.3", 2091 | "windows_x86_64_gnu 0.52.3", 2092 | "windows_x86_64_gnullvm 0.52.3", 2093 | "windows_x86_64_msvc 0.52.3", 2094 | ] 2095 | 2096 | [[package]] 2097 | name = "windows_aarch64_gnullvm" 2098 | version = "0.48.5" 2099 | source = "registry+https://github.com/rust-lang/crates.io-index" 2100 | checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" 2101 | 2102 | [[package]] 2103 | name = "windows_aarch64_gnullvm" 2104 | version = "0.52.3" 2105 | source = "registry+https://github.com/rust-lang/crates.io-index" 2106 | checksum = "68e5dcfb9413f53afd9c8f86e56a7b4d86d9a2fa26090ea2dc9e40fba56c6ec6" 2107 | 2108 | [[package]] 2109 | name = "windows_aarch64_msvc" 2110 | version = "0.48.5" 2111 | source = "registry+https://github.com/rust-lang/crates.io-index" 2112 | checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" 2113 | 2114 | [[package]] 2115 | name = "windows_aarch64_msvc" 2116 | version = "0.52.3" 2117 | source = "registry+https://github.com/rust-lang/crates.io-index" 2118 | checksum = "8dab469ebbc45798319e69eebf92308e541ce46760b49b18c6b3fe5e8965b30f" 2119 | 2120 | [[package]] 2121 | name = "windows_i686_gnu" 2122 | version = "0.48.5" 2123 | source = "registry+https://github.com/rust-lang/crates.io-index" 2124 | checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" 2125 | 2126 | [[package]] 2127 | name = "windows_i686_gnu" 2128 | version = "0.52.3" 2129 | source = "registry+https://github.com/rust-lang/crates.io-index" 2130 | checksum = "2a4e9b6a7cac734a8b4138a4e1044eac3404d8326b6c0f939276560687a033fb" 2131 | 2132 | [[package]] 2133 | name = "windows_i686_msvc" 2134 | version = "0.48.5" 2135 | source = "registry+https://github.com/rust-lang/crates.io-index" 2136 | checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" 2137 | 2138 | [[package]] 2139 | name = "windows_i686_msvc" 2140 | version = "0.52.3" 2141 | source = "registry+https://github.com/rust-lang/crates.io-index" 2142 | checksum = "28b0ec9c422ca95ff34a78755cfa6ad4a51371da2a5ace67500cf7ca5f232c58" 2143 | 2144 | [[package]] 2145 | name = "windows_x86_64_gnu" 2146 | version = "0.48.5" 2147 | source = "registry+https://github.com/rust-lang/crates.io-index" 2148 | checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" 2149 | 2150 | [[package]] 2151 | name = "windows_x86_64_gnu" 2152 | version = "0.52.3" 2153 | source = "registry+https://github.com/rust-lang/crates.io-index" 2154 | checksum = "704131571ba93e89d7cd43482277d6632589b18ecf4468f591fbae0a8b101614" 2155 | 2156 | [[package]] 2157 | name = "windows_x86_64_gnullvm" 2158 | version = "0.48.5" 2159 | source = "registry+https://github.com/rust-lang/crates.io-index" 2160 | checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" 2161 | 2162 | [[package]] 2163 | name = "windows_x86_64_gnullvm" 2164 | version = "0.52.3" 2165 | source = "registry+https://github.com/rust-lang/crates.io-index" 2166 | checksum = "42079295511643151e98d61c38c0acc444e52dd42ab456f7ccfd5152e8ecf21c" 2167 | 2168 | [[package]] 2169 | name = "windows_x86_64_msvc" 2170 | version = "0.48.5" 2171 | source = "registry+https://github.com/rust-lang/crates.io-index" 2172 | checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" 2173 | 2174 | [[package]] 2175 | name = "windows_x86_64_msvc" 2176 | version = "0.52.3" 2177 | source = "registry+https://github.com/rust-lang/crates.io-index" 2178 | checksum = "0770833d60a970638e989b3fa9fd2bb1aaadcf88963d1659fd7d9990196ed2d6" 2179 | 2180 | [[package]] 2181 | name = "winreg" 2182 | version = "0.7.0" 2183 | source = "registry+https://github.com/rust-lang/crates.io-index" 2184 | checksum = "0120db82e8a1e0b9fb3345a539c478767c0048d842860994d96113d5b667bd69" 2185 | dependencies = [ 2186 | "winapi 0.3.9", 2187 | ] 2188 | 2189 | [[package]] 2190 | name = "ws2_32-sys" 2191 | version = "0.2.1" 2192 | source = "registry+https://github.com/rust-lang/crates.io-index" 2193 | checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e" 2194 | dependencies = [ 2195 | "winapi 0.2.8", 2196 | "winapi-build", 2197 | ] 2198 | 2199 | [[package]] 2200 | name = "xml5ever" 2201 | version = "0.17.0" 2202 | source = "registry+https://github.com/rust-lang/crates.io-index" 2203 | checksum = "4034e1d05af98b51ad7214527730626f019682d797ba38b51689212118d8e650" 2204 | dependencies = [ 2205 | "log", 2206 | "mac", 2207 | "markup5ever", 2208 | ] 2209 | 2210 | [[package]] 2211 | name = "yaml-rust" 2212 | version = "0.4.5" 2213 | source = "registry+https://github.com/rust-lang/crates.io-index" 2214 | checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" 2215 | dependencies = [ 2216 | "linked-hash-map", 2217 | ] 2218 | 2219 | [[package]] 2220 | name = "zerocopy" 2221 | version = "0.7.32" 2222 | source = "registry+https://github.com/rust-lang/crates.io-index" 2223 | checksum = "74d4d3961e53fa4c9a25a8637fc2bfaf2595b3d3ae34875568a5cf64787716be" 2224 | dependencies = [ 2225 | "zerocopy-derive", 2226 | ] 2227 | 2228 | [[package]] 2229 | name = "zerocopy-derive" 2230 | version = "0.7.32" 2231 | source = "registry+https://github.com/rust-lang/crates.io-index" 2232 | checksum = "9ce1b18ccd8e73a9321186f97e46f9f04b778851177567b1975109d26a08d2a6" 2233 | dependencies = [ 2234 | "proc-macro2", 2235 | "quote", 2236 | "syn 2.0.50", 2237 | ] 2238 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "tg-bot-api" 3 | version = "0.6.0" 4 | authors = ["Arsenii Lyashenko "] 5 | edition = "2018" 6 | license = "MIT OR Apache-2.0" 7 | description = "Telegram Bot API parser" 8 | repository = "https://github.com/ark0f/tg-bot-api" 9 | documentation = "https://docs.rs/ark0f/tg-bot-api" 10 | readme = "README.md" 11 | include = ["Cargo.toml", "LICENSE-*.md", "src/**/*"] 12 | keywords = ["telegram", "telegram-bot-api", "api", "parser"] 13 | categories = ["development-tools", "parsing"] 14 | 15 | [workspace] 16 | members = ["gh-pages-generator"] 17 | 18 | [dependencies] 19 | scraper = "0.18.1" 20 | ego-tree = "0.6.2" 21 | thiserror = "1.0.22" 22 | itertools = "0.12.1" 23 | chrono = "0.4.19" 24 | html2md = "0.2.10" 25 | semver = "1.0.22" 26 | percent-encoding = "2.1.0" 27 | logos = "0.14.0" 28 | log = "0.4.14" 29 | tendril = "0.4.2" 30 | -------------------------------------------------------------------------------- /LICENSE-APACHE.md: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /LICENSE-MIT.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Arsenii "ark0f" Lyashenko 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tg-bot-api 2 | 3 | [![Actions Status](https://github.com/ark0f/tg-bot-api/workflows/CI/badge.svg)](https://github.com/ark0f/tg-bot-api/actions) 4 | ![License](https://img.shields.io/crates/l/tg_bot_api.svg) 5 | [![crates.io](https://img.shields.io/crates/v/tg-bot-api.svg)](https://crates.io/crates/tg-bot-api) 6 | [![Documentation](https://docs.rs/tg-bot-api/badge.svg)](https://docs.rs/tg-bot-api) 7 | 8 | Telegram Bot API parser in Rust 9 | 10 | Generated schemas can be found at `ark0f.github.io/tg-bot-api`: 11 | 12 | OpenAPI: 13 | 14 | * [`/openapi.yml`](https://ark0f.github.io/tg-bot-api/openapi.yml) or 15 | [`/openapi.json`](https://ark0f.github.io/tg-bot-api/openapi.json) 16 | 17 | Custom schema thar more convenient to work with: 18 | 19 | * [`/custom_v2.json`](https://ark0f.github.io/tg-bot-api/custom_v2.json) 20 | * [`/custom_v2.schema.json`](https://ark0f.github.io/tg-bot-api/custom_v2.schema.json) - JSON Schema Draft #7 21 | for `/custom_v2.json` 22 | 23 | Documentation can be found at [CUSTOM_SCHEMA.md](CUSTOM_SCHEMA.md). 24 | 25 | `.min.json` suffix can be used to fetch minimized JSON. For example: `openapi.min.json`, `custom_v2.min.json`, etc. 26 | 27 | ## Automatic deploy 28 | 29 | Schemas are deployed automatically every midnight at UTC+0 and when there is a new commit 30 | in [tdlib/telegram-bot-api](https://github.com/tdlib/telegram-bot-api). 31 | 32 | ## Custom custom schema v1 33 | 34 | This is a note for old users. 35 | 36 | Schema still remains and updates at old URLs as earlier: 37 | 38 | * [`/custom.json`](https://ark0f.github.io/tg-bot-api/custom.json) 39 | * [`/custom.schema.json`](https://ark0f.github.io/tg-bot-api/custom.schema.json) 40 | 41 | See [v2 changes](V2_CHANGES.md) for more details. 42 | -------------------------------------------------------------------------------- /V2_CHANGES.md: -------------------------------------------------------------------------------- 1 | # The 2nd version changes 2 | 3 | ## Lists 4 | 5 | Lists are not skipped during serialization anymore, so, for example, instead of 6 | 7 | ```json 8 | { 9 | "type": "string" 10 | } 11 | ``` 12 | 13 | you will see 14 | 15 | ```json 16 | { 17 | "type": "string", 18 | "enumeration": [] 19 | } 20 | ``` 21 | 22 | because empty list and absent field are semantically same. 23 | 24 | ## Field/argument types 25 | 26 | Type objects are separated everywhere (not flattened anymore). 27 | 28 | Was: 29 | 30 | ```json 31 | { 32 | "name": "reply_to_message_id", 33 | "description": "If the message is a reply, ID of the original message", 34 | "required": false, 35 | "type": "integer" 36 | } 37 | ``` 38 | 39 | Become: 40 | 41 | ```json 42 | { 43 | "name": "reply_to_message_id", 44 | "description": "If the message is a reply, ID of the original message", 45 | "required": false, 46 | "type_info": { 47 | "type": "integer" 48 | } 49 | } 50 | ``` 51 | 52 | Because it's hard to deserialize such structures without powerful libraries like Rust's serde. 53 | 54 | ## Object types 55 | 56 | Here you can see `type` is `properties`: 57 | 58 | ```json 59 | { 60 | "name": "VoiceChatScheduled", 61 | "description": "This object represents a service message about a voice chat scheduled in the chat.", 62 | "type": "properties", 63 | "properties": [ 64 | ... 65 | ], 66 | "documentation_link": "https://core.telegram.org/bots/api/#voicechatscheduled" 67 | } 68 | ``` 69 | 70 | But here `type` is not exists because it nor has properties neither consists of other types: 71 | 72 | ```json 73 | { 74 | "name": "VoiceChatStarted", 75 | "description": "This object represents a service message about a voice chat started in the chat. Currently holds no information.", 76 | "documentation_link": "https://core.telegram.org/bots/api/#voicechatstarted" 77 | } 78 | ``` 79 | 80 | So I added `unknown` value for consistency: 81 | 82 | ```json 83 | { 84 | "name": "VoiceChatStarted", 85 | "description": "This object represents a service message about a voice chat started in the chat. Currently holds no information.", 86 | "type": "unknown", 87 | "documentation_link": "https://core.telegram.org/bots/api/#voicechatstarted" 88 | } 89 | ``` 90 | 91 | ## "maybe_multipart" 92 | 93 | Field `only_multipart` from `Method` object was renamed to `maybe_multipart` to be clearer about its meaning. 94 | -------------------------------------------------------------------------------- /gh-pages-generator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "gh-pages-generator" 3 | version = "0.1.0" 4 | authors = ["Arsenii Lyashenko "] 5 | edition = "2018" 6 | 7 | [dependencies] 8 | reqwest = { version = "0.10.8", features = ["blocking"] } 9 | anyhow = "1.0.34" 10 | schemars = "0.8.0" 11 | openapiv3 = "0.5.0" 12 | serde = { version = "1.0.117", features = ["derive"] } 13 | serde_json = "1.0.59" 14 | serde_yaml = "0.8.14" 15 | tg-bot-api = { path = ".." } 16 | chrono = "0.4.19" 17 | indexmap = "1.6.0" 18 | pulldown-cmark = "0.8.0" 19 | structopt = "0.3.20" 20 | pretty_env_logger = "0.4.0" 21 | -------------------------------------------------------------------------------- /gh-pages-generator/base-schema.yml: -------------------------------------------------------------------------------- 1 | openapi: "3.0.0" 2 | info: 3 | title: Telegram Bot API 4 | description: Auto-generated OpenAPI schema 5 | version: 0.0.0 6 | components: 7 | schemas: 8 | Success: 9 | type: object 10 | required: 11 | - ok 12 | - result 13 | properties: 14 | ok: 15 | type: boolean 16 | default: true 17 | result: 18 | type: object 19 | Error: 20 | type: object 21 | required: 22 | - ok 23 | - error_code 24 | - description 25 | properties: 26 | ok: 27 | type: boolean 28 | default: false 29 | error_code: 30 | type: integer 31 | description: 32 | type: string 33 | parameters: 34 | $ref: "#/components/schemas/ResponseParameters" 35 | paths: {} 36 | servers: 37 | - url: https://api.telegram.org/bot{token} 38 | variables: 39 | token: 40 | description: Each bot is given a unique authentication token when it is created. 41 | default: 123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11 42 | externalDocs: 43 | description: The Bot API is an HTTP-based interface created for developers keen on building bots for Telegram. 44 | url: https://core.telegram.org/bots/api 45 | -------------------------------------------------------------------------------- /gh-pages-generator/src/custom.rs: -------------------------------------------------------------------------------- 1 | use chrono::Datelike; 2 | use schemars::{gen::SchemaGenerator, schema::RootSchema, schema_for, JsonSchema}; 3 | use serde::{Serialize, Serializer}; 4 | use tg_bot_api::{MethodArgs, Parsed, Type}; 5 | 6 | pub fn generate(parsed: Parsed) -> (Schema, RootSchema) { 7 | let methods = parsed.methods.into_iter().map(Method::from).collect(); 8 | let objects = parsed.objects.into_iter().map(Object::from).collect(); 9 | 10 | ( 11 | Schema { 12 | version: Version { 13 | major: parsed.version.major, 14 | minor: parsed.version.minor, 15 | patch: parsed.version.patch, 16 | }, 17 | recent_changes: Date { 18 | year: parsed.recent_changes.year(), 19 | month: parsed.recent_changes.month(), 20 | day: parsed.recent_changes.day(), 21 | }, 22 | methods, 23 | objects, 24 | }, 25 | schema_for!(Schema), 26 | ) 27 | } 28 | 29 | #[derive(Serialize, JsonSchema)] 30 | pub struct Schema { 31 | version: Version, 32 | recent_changes: Date, 33 | methods: Vec, 34 | objects: Vec, 35 | } 36 | 37 | #[derive(Serialize, JsonSchema)] 38 | struct Version { 39 | major: u64, 40 | minor: u64, 41 | patch: u64, 42 | } 43 | 44 | #[derive(Serialize, JsonSchema)] 45 | struct Date { 46 | year: i32, 47 | month: u32, 48 | day: u32, 49 | } 50 | 51 | #[derive(Debug, Serialize, JsonSchema)] 52 | #[serde(tag = "type")] 53 | #[serde(rename_all = "snake_case")] 54 | enum Kind { 55 | Integer { 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | default: Option, 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | min: Option, 60 | #[serde(skip_serializing_if = "Option::is_none")] 61 | max: Option, 62 | #[schemars(with = "Option>")] 63 | #[serde(skip_serializing_if = "Vec::is_empty")] 64 | enumeration: Vec, 65 | }, 66 | String { 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | default: Option, 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | min_len: Option, 71 | #[serde(skip_serializing_if = "Option::is_none")] 72 | max_len: Option, 73 | #[schemars(with = "Option>")] 74 | #[serde(skip_serializing_if = "Vec::is_empty")] 75 | enumeration: Vec, 76 | }, 77 | Bool { 78 | #[serde(skip_serializing_if = "Option::is_none")] 79 | default: Option, 80 | }, 81 | Float, 82 | AnyOf { 83 | #[schemars(with = "Option>")] 84 | #[serde(skip_serializing_if = "Vec::is_empty")] 85 | any_of: Vec, 86 | }, 87 | Reference { 88 | reference: String, 89 | }, 90 | Array { 91 | array: Box, 92 | }, 93 | } 94 | 95 | // this type used to avoid recursion type 96 | // because serde and schemars don't support such types 97 | #[derive(Debug, Serialize, JsonSchema)] 98 | #[serde(transparent)] 99 | struct KindWrapper(Kind); 100 | 101 | impl From for KindWrapper { 102 | fn from(ty: tg_bot_api::Type) -> Self { 103 | let base = match ty { 104 | Type::Integer { 105 | default, 106 | min, 107 | max, 108 | one_of, 109 | } => Kind::Integer { 110 | default, 111 | min, 112 | max, 113 | enumeration: one_of, 114 | }, 115 | Type::String { 116 | default, 117 | min_len, 118 | max_len, 119 | one_of, 120 | } => Kind::String { 121 | default, 122 | min_len, 123 | max_len, 124 | enumeration: one_of, 125 | }, 126 | Type::Bool { default } => Kind::Bool { default }, 127 | Type::Float => Kind::Float, 128 | Type::Or(types) => Kind::AnyOf { 129 | any_of: types.into_iter().map(KindWrapper::from).collect(), 130 | }, 131 | Type::Object(object) => Kind::Reference { reference: object }, 132 | Type::Array(ty) => Kind::Array { 133 | array: Box::new(KindWrapper::from(*ty)), 134 | }, 135 | }; 136 | KindWrapper(base) 137 | } 138 | } 139 | 140 | #[derive(Serialize, JsonSchema)] 141 | struct Method { 142 | name: String, 143 | description: String, 144 | #[schemars(with = "Option>")] 145 | #[serde(skip_serializing_if = "Vec::is_empty")] 146 | arguments: Vec, 147 | multipart_only: bool, 148 | return_type: KindWrapper, 149 | documentation_link: String, 150 | } 151 | 152 | impl From for Method { 153 | fn from(method: tg_bot_api::Method) -> Self { 154 | let (multipart_only, args) = match method.args { 155 | MethodArgs::No => (false, vec![]), 156 | MethodArgs::Yes(args) => (false, args), 157 | MethodArgs::WithMultipart(args) => (true, args), 158 | }; 159 | Self { 160 | name: method.name, 161 | description: method.description, 162 | arguments: args.into_iter().map(Argument::from).collect(), 163 | multipart_only, 164 | return_type: KindWrapper::from(method.return_type), 165 | documentation_link: method.docs_link, 166 | } 167 | } 168 | } 169 | 170 | #[derive(Serialize, JsonSchema)] 171 | struct Argument { 172 | name: String, 173 | description: String, 174 | required: bool, 175 | #[serde(flatten)] 176 | kind: KindWrapper, 177 | } 178 | 179 | impl From for Argument { 180 | fn from(arg: tg_bot_api::Argument) -> Self { 181 | Self { 182 | name: arg.name, 183 | description: arg.description, 184 | required: arg.required, 185 | kind: KindWrapper::from(arg.kind), 186 | } 187 | } 188 | } 189 | 190 | #[derive(Serialize, JsonSchema)] 191 | struct Object { 192 | name: String, 193 | description: String, 194 | #[serde(flatten)] 195 | data: ObjectData, 196 | documentation_link: String, 197 | } 198 | 199 | impl From for Object { 200 | fn from(object: tg_bot_api::Object) -> Self { 201 | Self { 202 | name: object.name, 203 | description: object.description, 204 | data: ObjectData::from(object.data), 205 | documentation_link: object.docs_link, 206 | } 207 | } 208 | } 209 | 210 | enum ObjectData { 211 | Properties { properties: Vec }, 212 | AnyOf { any_of: Vec }, 213 | Unknown, 214 | } 215 | 216 | impl From for ObjectData { 217 | fn from(object_data: tg_bot_api::ObjectData) -> Self { 218 | match object_data { 219 | tg_bot_api::ObjectData::Fields(fields) => ObjectData::Properties { 220 | properties: fields.into_iter().map(Property::from).collect(), 221 | }, 222 | tg_bot_api::ObjectData::Elements(types) => ObjectData::AnyOf { 223 | any_of: types.into_iter().map(KindWrapper::from).collect(), 224 | }, 225 | tg_bot_api::ObjectData::Unknown => ObjectData::Unknown, 226 | } 227 | } 228 | } 229 | 230 | impl Serialize for ObjectData { 231 | fn serialize(&self, serializer: S) -> Result 232 | where 233 | S: Serializer, 234 | { 235 | #[derive(Serialize)] 236 | #[serde(rename_all = "snake_case")] 237 | #[serde(tag = "type")] 238 | enum Inner<'a> { 239 | Properties { properties: &'a Vec }, 240 | AnyOf { any_of: &'a Vec }, 241 | } 242 | 243 | match self { 244 | ObjectData::Properties { properties } => Inner::Properties { properties }, 245 | ObjectData::AnyOf { any_of } => Inner::AnyOf { any_of }, 246 | ObjectData::Unknown => return ().serialize(serializer), 247 | } 248 | .serialize(serializer) 249 | } 250 | } 251 | 252 | impl JsonSchema for ObjectData { 253 | fn schema_name() -> String { 254 | "ObjectData".to_string() 255 | } 256 | 257 | fn json_schema(gen: &mut SchemaGenerator) -> schemars::schema::Schema { 258 | #[allow(dead_code)] 259 | #[derive(Serialize, JsonSchema)] 260 | #[serde(untagged)] 261 | enum Inner<'a> { 262 | Properties { 263 | #[serde(rename = "type")] 264 | kind: String, 265 | properties: &'a Vec, 266 | }, 267 | AnyOf { 268 | #[serde(rename = "type")] 269 | kind: String, 270 | any_of: &'a Vec, 271 | }, 272 | Unknown {}, 273 | } 274 | 275 | Inner::json_schema(gen) 276 | } 277 | } 278 | 279 | #[derive(Serialize, JsonSchema)] 280 | struct Property { 281 | name: String, 282 | description: String, 283 | required: bool, 284 | #[serde(flatten)] 285 | kind: KindWrapper, 286 | } 287 | 288 | impl From for Property { 289 | fn from(field: tg_bot_api::Field) -> Self { 290 | Self { 291 | name: field.name, 292 | description: field.description, 293 | required: field.required, 294 | kind: KindWrapper::from(field.kind), 295 | } 296 | } 297 | } 298 | -------------------------------------------------------------------------------- /gh-pages-generator/src/custom2.rs: -------------------------------------------------------------------------------- 1 | use chrono::Datelike; 2 | use schemars::{schema::RootSchema, schema_for, JsonSchema}; 3 | use serde::Serialize; 4 | use tg_bot_api::{MethodArgs, Parsed, Type}; 5 | 6 | pub fn generate(parsed: Parsed) -> (Schema, RootSchema) { 7 | let methods = parsed.methods.into_iter().map(Method::from).collect(); 8 | let objects = parsed.objects.into_iter().map(Object::from).collect(); 9 | 10 | ( 11 | Schema { 12 | version: Version { 13 | major: parsed.version.major, 14 | minor: parsed.version.minor, 15 | patch: parsed.version.patch, 16 | }, 17 | recent_changes: Date { 18 | year: parsed.recent_changes.year(), 19 | month: parsed.recent_changes.month(), 20 | day: parsed.recent_changes.day(), 21 | }, 22 | methods, 23 | objects, 24 | }, 25 | schema_for!(Schema), 26 | ) 27 | } 28 | 29 | #[derive(Serialize, JsonSchema)] 30 | pub struct Schema { 31 | version: Version, 32 | recent_changes: Date, 33 | methods: Vec, 34 | objects: Vec, 35 | } 36 | 37 | #[derive(Serialize, JsonSchema)] 38 | struct Version { 39 | major: u64, 40 | minor: u64, 41 | patch: u64, 42 | } 43 | 44 | #[derive(Serialize, JsonSchema)] 45 | struct Date { 46 | year: i32, 47 | month: u32, 48 | day: u32, 49 | } 50 | 51 | #[derive(Debug, Serialize, JsonSchema)] 52 | #[serde(tag = "type")] 53 | #[serde(rename_all = "snake_case")] 54 | enum Kind { 55 | Integer { 56 | #[serde(skip_serializing_if = "Option::is_none")] 57 | default: Option, 58 | #[serde(skip_serializing_if = "Option::is_none")] 59 | min: Option, 60 | #[serde(skip_serializing_if = "Option::is_none")] 61 | max: Option, 62 | enumeration: Vec, 63 | }, 64 | String { 65 | #[serde(skip_serializing_if = "Option::is_none")] 66 | default: Option, 67 | #[serde(skip_serializing_if = "Option::is_none")] 68 | min_len: Option, 69 | #[serde(skip_serializing_if = "Option::is_none")] 70 | max_len: Option, 71 | enumeration: Vec, 72 | }, 73 | Bool { 74 | #[serde(skip_serializing_if = "Option::is_none")] 75 | default: Option, 76 | }, 77 | Float, 78 | AnyOf { 79 | any_of: Vec, 80 | }, 81 | Reference { 82 | reference: String, 83 | }, 84 | Array { 85 | array: Box, 86 | }, 87 | } 88 | 89 | // this type used to avoid recursion type 90 | // because serde and schemars don't support such types 91 | #[derive(Debug, Serialize, JsonSchema)] 92 | #[serde(transparent)] 93 | struct KindWrapper(Kind); 94 | 95 | impl From for KindWrapper { 96 | fn from(ty: tg_bot_api::Type) -> Self { 97 | let base = match ty { 98 | Type::Integer { 99 | default, 100 | min, 101 | max, 102 | one_of, 103 | } => Kind::Integer { 104 | default, 105 | min, 106 | max, 107 | enumeration: one_of, 108 | }, 109 | Type::String { 110 | default, 111 | min_len, 112 | max_len, 113 | one_of, 114 | } => Kind::String { 115 | default, 116 | min_len, 117 | max_len, 118 | enumeration: one_of, 119 | }, 120 | Type::Bool { default } => Kind::Bool { default }, 121 | Type::Float => Kind::Float, 122 | Type::Or(types) => Kind::AnyOf { 123 | any_of: types.into_iter().map(KindWrapper::from).collect(), 124 | }, 125 | Type::Object(object) => Kind::Reference { reference: object }, 126 | Type::Array(ty) => Kind::Array { 127 | array: Box::new(KindWrapper::from(*ty)), 128 | }, 129 | }; 130 | KindWrapper(base) 131 | } 132 | } 133 | 134 | #[derive(Serialize, JsonSchema)] 135 | struct Method { 136 | name: String, 137 | description: String, 138 | arguments: Vec, 139 | maybe_multipart: bool, 140 | return_type: KindWrapper, 141 | documentation_link: String, 142 | } 143 | 144 | impl From for Method { 145 | fn from(method: tg_bot_api::Method) -> Self { 146 | let (maybe_multipart, args) = match method.args { 147 | MethodArgs::No => (false, vec![]), 148 | MethodArgs::Yes(args) => (false, args), 149 | MethodArgs::WithMultipart(args) => (true, args), 150 | }; 151 | Self { 152 | name: method.name, 153 | description: method.description, 154 | arguments: args.into_iter().map(Argument::from).collect(), 155 | maybe_multipart, 156 | return_type: KindWrapper::from(method.return_type), 157 | documentation_link: method.docs_link, 158 | } 159 | } 160 | } 161 | 162 | #[derive(Serialize, JsonSchema)] 163 | struct Argument { 164 | name: String, 165 | description: String, 166 | required: bool, 167 | #[serde(rename = "type_info")] 168 | kind: KindWrapper, 169 | } 170 | 171 | impl From for Argument { 172 | fn from(arg: tg_bot_api::Argument) -> Self { 173 | Self { 174 | name: arg.name, 175 | description: arg.description, 176 | required: arg.required, 177 | kind: KindWrapper::from(arg.kind), 178 | } 179 | } 180 | } 181 | 182 | #[derive(Serialize, JsonSchema)] 183 | struct Object { 184 | name: String, 185 | description: String, 186 | #[serde(flatten)] 187 | data: ObjectData, 188 | documentation_link: String, 189 | } 190 | 191 | impl From for Object { 192 | fn from(object: tg_bot_api::Object) -> Self { 193 | Self { 194 | name: object.name, 195 | description: object.description, 196 | data: ObjectData::from(object.data), 197 | documentation_link: object.docs_link, 198 | } 199 | } 200 | } 201 | 202 | #[derive(Serialize, JsonSchema)] 203 | #[serde(tag = "type")] 204 | #[serde(rename_all = "snake_case")] 205 | enum ObjectData { 206 | Properties { properties: Vec }, 207 | AnyOf { any_of: Vec }, 208 | Unknown, 209 | } 210 | 211 | impl From for ObjectData { 212 | fn from(object_data: tg_bot_api::ObjectData) -> Self { 213 | match object_data { 214 | tg_bot_api::ObjectData::Fields(fields) => ObjectData::Properties { 215 | properties: fields.into_iter().map(Property::from).collect(), 216 | }, 217 | tg_bot_api::ObjectData::Elements(types) => ObjectData::AnyOf { 218 | any_of: types.into_iter().map(KindWrapper::from).collect(), 219 | }, 220 | tg_bot_api::ObjectData::Unknown => ObjectData::Unknown, 221 | } 222 | } 223 | } 224 | 225 | #[derive(Serialize, JsonSchema)] 226 | struct Property { 227 | name: String, 228 | description: String, 229 | required: bool, 230 | #[serde(rename = "type_info")] 231 | kind: KindWrapper, 232 | } 233 | 234 | impl From for Property { 235 | fn from(field: tg_bot_api::Field) -> Self { 236 | Self { 237 | name: field.name, 238 | description: field.description, 239 | required: field.required, 240 | kind: KindWrapper::from(field.kind), 241 | } 242 | } 243 | } 244 | -------------------------------------------------------------------------------- /gh-pages-generator/src/main.rs: -------------------------------------------------------------------------------- 1 | mod custom; 2 | mod custom2; 3 | mod openapi; 4 | 5 | use serde::Serialize; 6 | use std::{fs, path::PathBuf}; 7 | use structopt::StructOpt; 8 | use tg_bot_api::BOT_API_DOCS_URL; 9 | 10 | fn md_to_html(md: &str) -> String { 11 | let parser = pulldown_cmark::Parser::new(md); 12 | let mut buf = String::new(); 13 | pulldown_cmark::html::push_html(&mut buf, parser); 14 | buf 15 | } 16 | 17 | struct Serialized { 18 | content: String, 19 | path: String, 20 | } 21 | 22 | #[derive(Default)] 23 | struct Indexer { 24 | publish_dir: PathBuf, 25 | inner: Vec, 26 | } 27 | 28 | impl Indexer { 29 | fn new(publish_dir: &str) -> Self { 30 | Self { 31 | publish_dir: PathBuf::from(publish_dir), 32 | inner: vec![], 33 | } 34 | } 35 | 36 | fn add(&mut self, api: &T, formats: Vec) -> anyhow::Result<()> { 37 | for format in formats { 38 | let (path, content) = match format { 39 | Format::Json(path) => (path, serde_json::to_string_pretty(api)?), 40 | Format::MinimizedJson(path) => (path, serde_json::to_string(api)?), 41 | Format::Yaml(path) => (path, serde_yaml::to_string(api)?), 42 | }; 43 | self.inner.push(Serialized { 44 | content, 45 | path: path.to_string(), 46 | }); 47 | } 48 | 49 | Ok(()) 50 | } 51 | 52 | fn gen(self) -> anyhow::Result<()> { 53 | if !self.publish_dir.exists() { 54 | fs::create_dir_all(&self.publish_dir)?; 55 | } 56 | 57 | let mut index = String::new(); 58 | 59 | for Serialized { content, path } in self.inner { 60 | fs::write(self.publish_dir.join(&path), content)?; 61 | index += &format!("* [{path}]({path})\n", path = path); 62 | } 63 | 64 | let html = md_to_html(&index); 65 | fs::write(self.publish_dir.join("index.html"), html)?; 66 | 67 | Ok(()) 68 | } 69 | } 70 | 71 | enum Format { 72 | Json(&'static str), 73 | MinimizedJson(&'static str), 74 | Yaml(&'static str), 75 | } 76 | 77 | #[derive(StructOpt)] 78 | enum Path { 79 | /// Write files to `public` directory 80 | Production, 81 | /// Write files to `public/dev` directory 82 | Dev, 83 | } 84 | 85 | impl Path { 86 | fn into_str(self) -> &'static str { 87 | match self { 88 | Path::Dev => "public/dev", 89 | Path::Production => "public", 90 | } 91 | } 92 | } 93 | 94 | fn main() -> anyhow::Result<()> { 95 | pretty_env_logger::init(); 96 | 97 | let path = Path::from_args(); 98 | 99 | let api = reqwest::blocking::get(BOT_API_DOCS_URL)?.text()?; 100 | let parsed = tg_bot_api::get(&api)?; 101 | 102 | let mut indexer = Indexer::new(path.into_str()); 103 | let api = openapi::generate(parsed.clone()); 104 | indexer.add( 105 | &api, 106 | vec![ 107 | Format::Json("openapi.json"), 108 | Format::MinimizedJson("openapi.min.json"), 109 | Format::Yaml("openapi.yml"), 110 | ], 111 | )?; 112 | 113 | let (custom_schema, json_schema) = custom::generate(parsed.clone()); 114 | indexer.add( 115 | &custom_schema, 116 | vec![ 117 | Format::Json("custom.json"), 118 | Format::MinimizedJson("custom.min.json"), 119 | ], 120 | )?; 121 | indexer.add( 122 | &&json_schema, 123 | vec![ 124 | Format::Json("custom.schema.json"), 125 | Format::MinimizedJson("custom.schema.min.json"), 126 | ], 127 | )?; 128 | 129 | let (custom_schema, json_schema) = custom2::generate(parsed); 130 | indexer.add( 131 | &custom_schema, 132 | vec![ 133 | Format::Json("custom_v2.json"), 134 | Format::MinimizedJson("custom_v2.min.json"), 135 | ], 136 | )?; 137 | indexer.add( 138 | &&json_schema, 139 | vec![ 140 | Format::Json("custom_v2.schema.json"), 141 | Format::MinimizedJson("custom_v2.schema.min.json"), 142 | ], 143 | )?; 144 | 145 | indexer.gen()?; 146 | 147 | Ok(()) 148 | } 149 | -------------------------------------------------------------------------------- /gh-pages-generator/src/openapi.rs: -------------------------------------------------------------------------------- 1 | use indexmap::{indexmap, IndexMap}; 2 | use openapiv3::{ 3 | AnySchema, ArrayType, ExternalDocumentation, IntegerType, MediaType, NumberType, ObjectType, 4 | OpenAPI, Operation, PathItem, ReferenceOr, RequestBody, Response, Responses, Schema, 5 | SchemaData, SchemaKind, StatusCode, StringType, Type, 6 | }; 7 | use tg_bot_api::{Argument, Field, MethodArgs, ObjectData, Parsed, Type as ParserType}; 8 | 9 | const BASE_SCHEMA: &str = include_str!("../base-schema.yml"); 10 | const FORM_URL_ENCODED: &str = "application/x-www-form-urlencoded"; 11 | const JSON: &str = "application/json"; 12 | const FORM_DATA: &str = "multipart/form-data"; 13 | 14 | pub fn generate(parsed: Parsed) -> OpenAPI { 15 | let mut api: OpenAPI = serde_yaml::from_str(BASE_SCHEMA).expect("Base schema is invalid"); 16 | 17 | let success = api 18 | .components 19 | .as_mut() 20 | .unwrap() 21 | .schemas 22 | .remove("Success") 23 | .unwrap(); 24 | 25 | let mut schemas = indexmap![]; 26 | for object in parsed.objects { 27 | let schema_kind = match object.data { 28 | ObjectData::Fields(fields) => { 29 | let (properties, required) = make_properties_and_required(fields); 30 | SchemaKind::Type(Type::Object(ObjectType { 31 | properties, 32 | required, 33 | ..ObjectType::default() 34 | })) 35 | } 36 | ObjectData::Elements(elements) => { 37 | let any_of = elements 38 | .into_iter() 39 | .map(ParserType::into_schema) 40 | .map(ReferenceOr::unbox) 41 | .collect(); 42 | SchemaKind::AnyOf { any_of } 43 | } 44 | ObjectData::Unknown => SchemaKind::Any(AnySchema::default()), 45 | }; 46 | 47 | schemas.insert( 48 | object.name, 49 | ReferenceOr::Item(Schema { 50 | schema_data: SchemaData { 51 | description: Some(object.description), 52 | external_docs: Some(ExternalDocumentation { 53 | url: object.docs_link, 54 | ..ExternalDocumentation::default() 55 | }), 56 | ..SchemaData::default() 57 | }, 58 | schema_kind, 59 | }), 60 | ); 61 | } 62 | 63 | let mut paths = indexmap![]; 64 | for method in parsed.methods { 65 | let (file_uploading, has_args) = match method.args { 66 | MethodArgs::No => (false, false), 67 | MethodArgs::Yes(_) => (false, true), 68 | MethodArgs::WithMultipart(_) => (true, true), 69 | }; 70 | 71 | let mut content = indexmap![]; 72 | let (properties, required) = match method.args { 73 | MethodArgs::Yes(args) | MethodArgs::WithMultipart(args) => { 74 | make_properties_and_required(args) 75 | } 76 | _ => (indexmap![], vec![]), 77 | }; 78 | 79 | for content_type in [ 80 | Some(FORM_URL_ENCODED).filter(|_| !file_uploading), 81 | Some(FORM_DATA), 82 | Some(JSON).filter(|_| !file_uploading), 83 | ] 84 | .iter() 85 | .flatten() 86 | { 87 | content.insert( 88 | content_type.to_string(), 89 | MediaType { 90 | schema: Some(ReferenceOr::Item(Schema { 91 | schema_data: Default::default(), 92 | schema_kind: SchemaKind::Type(Type::Object(ObjectType { 93 | properties: properties.clone(), 94 | required: required.clone(), 95 | ..ObjectType::default() 96 | })), 97 | })), 98 | ..MediaType::default() 99 | }, 100 | ); 101 | } 102 | 103 | let mut success = success.clone(); 104 | if let ReferenceOr::Item(item) = &mut success { 105 | if let SchemaKind::Type(Type::Object(object)) = &mut item.schema_kind { 106 | object 107 | .properties 108 | .insert("result".to_string(), method.return_type.into_schema()); 109 | } 110 | } 111 | 112 | let operation = Operation { 113 | description: Some(method.description), 114 | request_body: if has_args { 115 | Some(ReferenceOr::Item(RequestBody { 116 | content, 117 | required: true, 118 | ..RequestBody::default() 119 | })) 120 | } else { 121 | None 122 | }, 123 | responses: Responses { 124 | default: Some(ReferenceOr::Item(Response { 125 | content: indexmap! { 126 | JSON.to_string() => MediaType { 127 | schema: Some(ReferenceOr::Reference { reference: "#/components/schemas/Error".to_string() }), 128 | ..MediaType::default() 129 | } 130 | }, 131 | ..Response::default() 132 | })), 133 | responses: indexmap! { 134 | StatusCode::Code(200) => ReferenceOr::Item(Response { 135 | content: indexmap! { 136 | JSON.to_string() => MediaType { 137 | schema: Some(success), 138 | ..MediaType::default() 139 | } 140 | }, 141 | ..Response::default() 142 | }), 143 | }, 144 | }, 145 | external_docs: Some(ExternalDocumentation { 146 | url: method.docs_link, 147 | ..ExternalDocumentation::default() 148 | }), 149 | ..Operation::default() 150 | }; 151 | 152 | let item = PathItem { 153 | post: Some(operation), 154 | ..PathItem::default() 155 | }; 156 | 157 | paths.insert(format!("/{}", method.name), ReferenceOr::Item(item)); 158 | } 159 | 160 | api.info.version = parsed.version.to_string(); 161 | api.paths = paths; 162 | if let Some(components) = &mut api.components { 163 | components.schemas.extend(schemas) 164 | } 165 | 166 | api 167 | } 168 | 169 | fn make_properties_and_required( 170 | common: Vec, 171 | ) -> (IndexMap>>, Vec) 172 | where 173 | T: Into, 174 | { 175 | common.into_iter().map(Into::into).fold( 176 | (indexmap![], vec![]), 177 | |(mut properties, mut required), content| { 178 | if content.required { 179 | required.push(content.name.clone()); 180 | } 181 | 182 | let ref_or_schema_parts = content.kind.into_ref_or_schema_parts(); 183 | let ref_or_schema = match ref_or_schema_parts { 184 | ReferenceOr::Item(SchemaParts { 185 | default, 186 | kind: schema_kind, 187 | }) => ReferenceOr::Item(Box::new(Schema { 188 | schema_data: SchemaData { 189 | description: Some(content.description), 190 | default, 191 | ..SchemaData::default() 192 | }, 193 | schema_kind, 194 | })), 195 | ReferenceOr::Reference { reference } => ReferenceOr::Reference { reference }, 196 | }; 197 | properties.insert(content.name, ref_or_schema); 198 | 199 | (properties, required) 200 | }, 201 | ) 202 | } 203 | 204 | trait TypeExt: Sized { 205 | fn into_ref_or_schema_parts(self) -> ReferenceOr; 206 | 207 | fn into_schema(self) -> ReferenceOr>; 208 | } 209 | 210 | impl TypeExt for ParserType { 211 | fn into_ref_or_schema_parts(self) -> ReferenceOr { 212 | let default = match &self { 213 | ParserType::Bool { default } => default.map(Into::into), 214 | ParserType::Integer { default, .. } => default.map(Into::into), 215 | ParserType::String { default, .. } => default.clone().map(Into::into), 216 | _ => None, 217 | }; 218 | 219 | let schema_kind = match self { 220 | this @ ParserType::Integer { .. } 221 | | this @ ParserType::String { .. } 222 | | this @ ParserType::Bool { .. } 223 | | this @ ParserType::Float 224 | | this @ ParserType::Array(_) => { 225 | let schema_type = match this { 226 | ParserType::Integer { 227 | min, max, one_of, .. 228 | } => Type::Integer(IntegerType { 229 | minimum: min, 230 | maximum: max, 231 | enumeration: one_of, 232 | ..IntegerType::default() 233 | }), 234 | ParserType::String { 235 | one_of, 236 | min_len, 237 | max_len, 238 | .. 239 | } => Type::String(StringType { 240 | min_length: min_len.map(|x| x as usize), 241 | max_length: max_len.map(|x| x as usize), 242 | enumeration: one_of, 243 | ..StringType::default() 244 | }), 245 | ParserType::Bool { .. } => Type::Boolean {}, 246 | ParserType::Float => Type::Number(NumberType::default()), 247 | ParserType::Array(array) => Type::Array(ArrayType { 248 | items: array.into_schema(), 249 | min_items: None, 250 | max_items: None, 251 | unique_items: false, 252 | }), 253 | _ => unreachable!(), 254 | }; 255 | SchemaKind::Type(schema_type) 256 | } 257 | ParserType::Or(types) => SchemaKind::AnyOf { 258 | any_of: types 259 | .into_iter() 260 | .map(ParserType::into_schema) 261 | .map(ReferenceOr::unbox) 262 | .collect(), 263 | }, 264 | ParserType::Object(reference) => { 265 | return ReferenceOr::Reference { 266 | reference: format!("#/components/schemas/{}", reference), 267 | } 268 | } 269 | }; 270 | 271 | ReferenceOr::Item(SchemaParts { 272 | default, 273 | kind: schema_kind, 274 | }) 275 | } 276 | 277 | fn into_schema(self) -> ReferenceOr> { 278 | match self.into_ref_or_schema_parts() { 279 | ReferenceOr::Item(parts) => ReferenceOr::Item(Box::new(Schema { 280 | schema_data: SchemaData { 281 | default: parts.default, 282 | ..SchemaData::default() 283 | }, 284 | schema_kind: parts.kind, 285 | })), 286 | ReferenceOr::Reference { reference } => ReferenceOr::Reference { reference }, 287 | } 288 | } 289 | } 290 | 291 | struct SchemaParts { 292 | default: Option, 293 | kind: SchemaKind, 294 | } 295 | 296 | struct CommonContent { 297 | name: String, 298 | description: String, 299 | required: bool, 300 | kind: ParserType, 301 | } 302 | 303 | impl From for CommonContent { 304 | fn from(arg: Argument) -> Self { 305 | CommonContent { 306 | name: arg.name, 307 | description: arg.description, 308 | required: arg.required, 309 | kind: arg.kind, 310 | } 311 | } 312 | } 313 | 314 | impl From for CommonContent { 315 | fn from(field: Field) -> Self { 316 | CommonContent { 317 | name: field.name, 318 | description: field.description, 319 | required: field.required, 320 | kind: field.kind, 321 | } 322 | } 323 | } 324 | -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | imports_granularity = "Crate" 2 | -------------------------------------------------------------------------------- /src/extractor.rs: -------------------------------------------------------------------------------- 1 | use crate::util::{ElementRefExt, StrExt}; 2 | use itertools::Itertools; 3 | use scraper::{ElementRef, Html, Selector}; 4 | 5 | #[derive(Debug, thiserror::Error)] 6 | pub enum ExtractorError { 7 | #[error("No `Recent changes` found in document")] 8 | NoRecentChanges, 9 | #[error("No version string found in document")] 10 | NoVersion, 11 | } 12 | 13 | pub struct Extractor { 14 | doc: Html, 15 | } 16 | 17 | impl Extractor { 18 | pub fn from_str(s: &str) -> Self { 19 | Self { 20 | doc: Html::parse_document(s), 21 | } 22 | } 23 | 24 | pub fn extract(&self) -> Result, ExtractorError> { 25 | let mut recent_changes = None; 26 | let mut version = None; 27 | let mut objects = Vec::new(); 28 | let mut methods = Vec::new(); 29 | 30 | let h3 = Selector::parse("h3").unwrap(); 31 | let h4 = Selector::parse("h4").unwrap(); 32 | let table = Selector::parse("table").unwrap(); 33 | let td = Selector::parse("td").unwrap(); 34 | let p = Selector::parse("p").unwrap(); 35 | let ul = Selector::parse("ul").unwrap(); 36 | let li = Selector::parse("li").unwrap(); 37 | let any = Selector::parse("h3, h4, p, table, ul").unwrap(); 38 | 39 | let mut state = State::SearchRecentChanges; 40 | let mut select_any = self.doc.select(&any).peekable(); 41 | while let Some(elem) = select_any.next() { 42 | let new_state = match state { 43 | State::SearchRecentChanges 44 | if h3.matches(&elem) && elem.plain_text() == "Recent changes" => 45 | { 46 | State::GetRecentChange 47 | } 48 | State::GetRecentChange if h4.matches(&elem) => { 49 | recent_changes = Some(elem.plain_text()); 50 | State::GetVersion 51 | } 52 | State::GetVersion if p.matches(&elem) => { 53 | version = Some(elem); 54 | State::SearchGettingUpdates 55 | } 56 | State::SearchGettingUpdates 57 | if h3.matches(&elem) && elem.plain_text() == "Getting updates" => 58 | { 59 | State::GetName 60 | } 61 | State::GetName if h4.matches(&elem) => { 62 | let name = elem.plain_text(); 63 | // get rid of elements like `Formatting options`, `Sending files` that are not objects or methods 64 | if name.chars().any(char::is_whitespace) { 65 | State::GetName 66 | } else if select_any.peek().matches(&table) { 67 | State::GetObjectFields { 68 | name: elem, 69 | description: RawDescription::default(), 70 | } 71 | } else { 72 | State::GetDescription { 73 | name: elem, 74 | description: RawDescription::default(), 75 | } 76 | } 77 | } 78 | State::GetDescription { 79 | name, 80 | mut description, 81 | } if p.matches(&elem) || ul.matches(&elem) => { 82 | description.push(elem); 83 | 84 | let has_p = select_any.peek().matches(&p); 85 | let is_method = name.plain_text().is_first_letter_lowercase(); 86 | let has_table = select_any.peek().matches(&table); 87 | let has_ul = select_any.peek().matches(&ul); 88 | 89 | if has_p || (has_ul && is_method) { 90 | State::GetDescription { name, description } 91 | } else { 92 | if has_ul && !is_method { 93 | let ul_elem = select_any.peek().cloned().unwrap(); 94 | description.push(ul_elem); 95 | } 96 | 97 | match (is_method, has_table, has_ul) { 98 | (true, true, false) => State::GetMethodFields { name, description }, 99 | (false, true, false) => State::GetObjectFields { name, description }, 100 | (false, false, true) => State::GetObjectElements { name, description }, 101 | (true, false, false) => { 102 | methods.push(RawMethod { 103 | name, 104 | description, 105 | args: vec![], 106 | }); 107 | State::GetName 108 | } 109 | (false, false, false) => { 110 | objects.push(RawObject { 111 | name, 112 | description, 113 | data: RawObjectData::Fields(vec![]), 114 | }); 115 | State::GetName 116 | } 117 | _ => unreachable!(), 118 | } 119 | } 120 | } 121 | State::GetObjectFields { name, description } if table.matches(&elem) => { 122 | objects.push(RawObject { 123 | name, 124 | description, 125 | data: RawObjectData::Fields(extract_fields(&td, elem)), 126 | }); 127 | 128 | State::GetName 129 | } 130 | State::GetMethodFields { name, description } if table.matches(&elem) => { 131 | methods.push(RawMethod { 132 | name, 133 | description, 134 | args: extract_args(&td, elem), 135 | }); 136 | 137 | State::GetName 138 | } 139 | State::GetObjectElements { name, description } if ul.matches(&elem) => { 140 | let elements = extract_elements(&li, elem); 141 | objects.push(RawObject { 142 | name, 143 | description, 144 | data: RawObjectData::Elements(elements), 145 | }); 146 | State::GetName 147 | } 148 | x => x, 149 | }; 150 | state = new_state; 151 | } 152 | 153 | Ok(Extracted { 154 | recent_changes: recent_changes.ok_or(ExtractorError::NoRecentChanges)?, 155 | version: version.ok_or(ExtractorError::NoVersion)?, 156 | methods, 157 | objects, 158 | }) 159 | } 160 | } 161 | 162 | fn extract_fields<'a>(td: &Selector, elem: ElementRef<'a>) -> Vec> { 163 | elem.select(td) 164 | .chunks(3) 165 | .into_iter() 166 | .filter_map(|mut tds| { 167 | let name = tds.next()?.plain_text(); 168 | let kind = tds.next()?.plain_text(); 169 | let description = tds.next()?; 170 | Some(RawField { 171 | name, 172 | kind, 173 | description, 174 | }) 175 | }) 176 | .collect() 177 | } 178 | 179 | fn extract_args<'a>(td: &Selector, elem: ElementRef<'a>) -> Vec> { 180 | elem.select(td) 181 | .chunks(4) 182 | .into_iter() 183 | .filter_map(|mut tds| { 184 | let name = tds.next()?.plain_text(); 185 | let kind = tds.next()?.plain_text(); 186 | let required = tds.next()?.plain_text(); 187 | let description = tds.next()?; 188 | 189 | Some(RawArgument { 190 | name, 191 | kind, 192 | required, 193 | description, 194 | }) 195 | }) 196 | .collect() 197 | } 198 | 199 | fn extract_elements<'a>(li: &Selector, elem: ElementRef<'a>) -> Vec> { 200 | elem.select(li).collect() 201 | } 202 | 203 | pub struct Extracted<'a> { 204 | pub recent_changes: String, 205 | pub version: ElementRef<'a>, 206 | pub methods: Vec>, 207 | pub objects: Vec>, 208 | } 209 | 210 | #[derive(Debug)] 211 | enum State<'a> { 212 | SearchRecentChanges, 213 | GetRecentChange, 214 | GetVersion, 215 | SearchGettingUpdates, 216 | GetName, 217 | GetDescription { 218 | name: ElementRef<'a>, 219 | description: RawDescription<'a>, 220 | }, 221 | GetObjectFields { 222 | name: ElementRef<'a>, 223 | description: RawDescription<'a>, 224 | }, 225 | GetMethodFields { 226 | name: ElementRef<'a>, 227 | description: RawDescription<'a>, 228 | }, 229 | GetObjectElements { 230 | name: ElementRef<'a>, 231 | description: RawDescription<'a>, 232 | }, 233 | } 234 | 235 | #[derive(Debug, Default)] 236 | pub struct RawDescription<'a>(pub Vec>); 237 | 238 | impl<'a> RawDescription<'a> { 239 | fn push(&mut self, element: ElementRef<'a>) { 240 | self.0.push(element); 241 | } 242 | } 243 | 244 | pub struct RawMethod<'a> { 245 | pub name: ElementRef<'a>, 246 | pub description: RawDescription<'a>, 247 | pub args: Vec>, 248 | } 249 | 250 | pub struct RawArgument<'a> { 251 | pub name: String, 252 | pub kind: String, 253 | pub required: String, 254 | pub description: ElementRef<'a>, 255 | } 256 | 257 | pub struct RawObject<'a> { 258 | pub name: ElementRef<'a>, 259 | pub description: RawDescription<'a>, 260 | pub data: RawObjectData<'a>, 261 | } 262 | 263 | pub enum RawObjectData<'a> { 264 | Fields(Vec>), 265 | Elements(Vec>), 266 | } 267 | 268 | pub struct RawField<'a> { 269 | pub name: String, 270 | pub kind: String, 271 | pub description: ElementRef<'a>, 272 | } 273 | 274 | trait OptionExt { 275 | fn matches(&self, selector: &Selector) -> bool; 276 | } 277 | 278 | impl OptionExt for Option<&ElementRef<'_>> { 279 | fn matches(&self, selector: &Selector) -> bool { 280 | self.map(|elem| selector.matches(elem)).unwrap_or(false) 281 | } 282 | } 283 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | #[macro_use] 2 | extern crate ego_tree; 3 | 4 | mod extractor; 5 | mod parser; 6 | mod util; 7 | 8 | pub use extractor::ExtractorError; 9 | pub use parser::{ 10 | Argument, Field, Method, MethodArgs, Object, ObjectData, ParseError, Parsed, Type, 11 | }; 12 | 13 | pub const CORE_TELEGRAM_URL: &str = "https://core.telegram.org"; 14 | pub const BOT_API_DOCS_URL: &str = "https://core.telegram.org/bots/api/"; 15 | 16 | use extractor::Extractor; 17 | 18 | #[derive(Debug, thiserror::Error)] 19 | pub enum Error { 20 | #[error("Extractor: {0}")] 21 | Extractor( 22 | #[from] 23 | #[source] 24 | ExtractorError, 25 | ), 26 | #[error("Parser: {0}")] 27 | Parse( 28 | #[from] 29 | #[source] 30 | ParseError, 31 | ), 32 | } 33 | 34 | pub fn get(html_doc: &str) -> Result { 35 | let extractor = Extractor::from_str(html_doc); 36 | let extracted = extractor.extract()?; 37 | let parsed = parser::parse(extracted)?; 38 | Ok(parsed) 39 | } 40 | -------------------------------------------------------------------------------- /src/parser.rs: -------------------------------------------------------------------------------- 1 | mod sentence; 2 | mod tags; 3 | 4 | use crate::{ 5 | extractor::{ 6 | Extracted, RawArgument, RawDescription, RawField, RawMethod, RawObject, RawObjectData, 7 | }, 8 | parser::sentence::Sentence, 9 | util::{ElementRefExt, StrExt}, 10 | BOT_API_DOCS_URL, 11 | }; 12 | use chrono::NaiveDate; 13 | use ego_tree::iter::Edge; 14 | use itertools::Itertools; 15 | use logos::Span; 16 | use scraper::{node::Element, ElementRef, Node}; 17 | use semver::Version; 18 | use sentence::{Pattern, SentenceRef, Sentences}; 19 | use std::{num::ParseIntError, ops::Deref, str::ParseBoolError}; 20 | use tags::TagsHandlerFactory; 21 | 22 | type Result = std::result::Result; 23 | 24 | #[derive(Debug, thiserror::Error)] 25 | pub enum ParseError { 26 | #[error("Invalid Required: {0}")] 27 | InvalidRequired(String), 28 | #[error("Failed to extract type from description: {0:?}")] 29 | TypeExtractionFailed(String), 30 | #[error("chrono: {0}")] 31 | ChronoParse( 32 | #[from] 33 | #[source] 34 | chrono::ParseError, 35 | ), 36 | #[error("Missing `href` attribute")] 37 | MissingHref, 38 | #[error("Missing `alt` attribute")] 39 | MissingAlt, 40 | #[error("SemVer: {0}")] 41 | SemVer( 42 | #[from] 43 | #[source] 44 | semver::Error, 45 | ), 46 | #[error("Integer parsing: {0}")] 47 | ParseInt( 48 | #[from] 49 | #[source] 50 | ParseIntError, 51 | ), 52 | #[error("Boolean parsing: {0}")] 53 | ParseBool( 54 | #[from] 55 | #[source] 56 | ParseBoolError, 57 | ), 58 | #[error("Lexer error: {lexeme:?} ({span:?}) in {input:?}")] 59 | Lexer { 60 | input: String, 61 | lexeme: String, 62 | span: Span, 63 | }, 64 | } 65 | 66 | pub fn parse(raw: Extracted) -> Result { 67 | let recent_changes = NaiveDate::parse_from_str(&raw.recent_changes, "%B %e, %Y")?; 68 | let version = parse_version(raw.version)?; 69 | let objects = raw 70 | .objects 71 | .into_iter() 72 | .map(parse_object) 73 | .collect::>()?; 74 | let methods = raw 75 | .methods 76 | .into_iter() 77 | .map(parse_method) 78 | .collect::>()?; 79 | 80 | Ok(Parsed { 81 | recent_changes, 82 | version, 83 | methods, 84 | objects, 85 | }) 86 | } 87 | 88 | fn parse_version(version: ElementRef) -> Result { 89 | let version = version 90 | .plain_text() 91 | .chars() 92 | .skip_while(|c| !c.is_ascii_digit()) 93 | .collect::() 94 | .trim_end_matches('.') 95 | .to_string() 96 | + ".0"; 97 | Ok(Version::parse(&version)?) 98 | } 99 | 100 | fn parse_object(raw_object: RawObject) -> Result { 101 | let name = raw_object.name.plain_text(); 102 | let description = raw_object.description.markdown(); 103 | let data = match raw_object.data { 104 | RawObjectData::Fields(fields) if !fields.is_empty() => { 105 | ObjectData::Fields(fields.into_iter().map(parse_field).collect::>()?) 106 | } 107 | RawObjectData::Fields(_) => ObjectData::Unknown, 108 | RawObjectData::Elements(elements) => ObjectData::Elements( 109 | elements 110 | .into_iter() 111 | .map(|elem| elem.plain_text()) 112 | .map(|s| Type::new(&s)) 113 | .collect(), 114 | ), 115 | }; 116 | let docs_link = raw_object.name.a_href().map(make_url_from_fragment)?; 117 | Ok(Object { 118 | name, 119 | description, 120 | data, 121 | docs_link, 122 | }) 123 | } 124 | 125 | fn parse_field(raw_field: RawField) -> Result { 126 | let plain_description = raw_field.description.plain_text(); 127 | let required = !plain_description.starts_with("Optional."); 128 | let kind = Type::new_with_description( 129 | &raw_field.kind, 130 | TypeParsingUnit::Element(&raw_field.description), 131 | )?; 132 | 133 | Ok(Field { 134 | name: raw_field.name, 135 | kind, 136 | required, 137 | description: raw_field.description.markdown(), 138 | }) 139 | } 140 | 141 | fn parse_method(raw_method: RawMethod) -> Result { 142 | let name = raw_method.name.plain_text(); 143 | let docs_link = raw_method.name.a_href().map(make_url_from_fragment)?; 144 | let return_type = 145 | Type::extract_from_text(TypeParsingUnit::Description(&raw_method.description))?; 146 | let args = raw_method 147 | .args 148 | .into_iter() 149 | .map(parse_argument) 150 | .collect::>()?; 151 | Ok(Method { 152 | name, 153 | description: raw_method.description.markdown(), 154 | args: MethodArgs::new(args), 155 | return_type, 156 | docs_link, 157 | }) 158 | } 159 | 160 | fn parse_argument(raw_arg: RawArgument) -> Result { 161 | let kind = Type::new_with_description( 162 | &raw_arg.kind, 163 | TypeParsingUnit::Element(&raw_arg.description), 164 | )?; 165 | let required = parse_required(raw_arg.required)?; 166 | Ok(Argument { 167 | name: raw_arg.name, 168 | kind, 169 | required, 170 | description: raw_arg.description.markdown(), 171 | }) 172 | } 173 | 174 | fn parse_required(s: String) -> Result { 175 | match s.as_str() { 176 | "Yes" => Ok(true), 177 | "Optional" => Ok(false), 178 | _ => Err(ParseError::InvalidRequired(s)), 179 | } 180 | } 181 | 182 | #[derive(Debug, Clone)] 183 | pub struct Parsed { 184 | pub recent_changes: NaiveDate, 185 | pub version: Version, 186 | pub methods: Vec, 187 | pub objects: Vec, 188 | } 189 | 190 | #[derive(Debug, Clone, Eq, PartialEq)] 191 | pub enum Type { 192 | Integer { 193 | default: Option, 194 | min: Option, 195 | max: Option, 196 | one_of: Vec, 197 | }, 198 | String { 199 | default: Option, 200 | min_len: Option, 201 | max_len: Option, 202 | one_of: Vec, 203 | }, 204 | Bool { 205 | default: Option, 206 | }, 207 | Float, 208 | Or(Vec), 209 | Array(Box), 210 | Object(String), 211 | } 212 | 213 | impl Type { 214 | // this function parses types from `Type` column in docs 215 | fn new(s: &str) -> Self { 216 | const ARRAY_OF: &[&str] = &["Array", "of"]; 217 | 218 | fn types_from_sentence_ref(sentence: &SentenceRef) -> Vec { 219 | sentence 220 | .parts() 221 | .iter() 222 | .filter(|part| !part.as_inner().is_first_letter_lowercase()) 223 | .map(|part| part.as_inner().as_str()) 224 | .map(Type::new) 225 | .collect() 226 | } 227 | 228 | match s { 229 | "Integer" | "Int" => Self::Integer { 230 | default: None, 231 | min: None, 232 | max: None, 233 | one_of: vec![], 234 | }, 235 | "String" => Self::String { 236 | default: None, 237 | min_len: None, 238 | max_len: None, 239 | one_of: vec![], 240 | }, 241 | "Boolean" => Self::Bool { default: None }, 242 | "True" => Self::Bool { 243 | default: Some(true), 244 | }, 245 | "Float" | "Float number" => Self::Float, 246 | _ => { 247 | let parser = Sentences::parse(s); 248 | if let Some(sentence) = parser.find(&["or"]) { 249 | let types = types_from_sentence_ref(sentence); 250 | Self::Or(types) 251 | } else if let Some(sentence) = parser.find_and_crop(ARRAY_OF) { 252 | let sentence = &sentence[2..]; 253 | let ty = if sentence.len() == 1 { 254 | Self::new(sentence.parts()[0].as_inner()) 255 | } else if sentence.starts_with(ARRAY_OF) { 256 | Self::new( 257 | &sentence 258 | .parts() 259 | .iter() 260 | .map(|part| part.as_inner()) 261 | .join(" "), 262 | ) 263 | } else { 264 | Self::Or(types_from_sentence_ref(sentence)) 265 | }; 266 | Self::Array(Box::new(ty)) 267 | } else { 268 | Self::Object(s.to_string()) 269 | } 270 | } 271 | } 272 | } 273 | 274 | fn new_with_description(s: &str, description: TypeParsingUnit) -> Result { 275 | let default = sentence::parse_type_custom(Pattern::Default, description, |sentence| { 276 | sentence.parts().first().map(|part| part.as_inner().clone()) 277 | })?; 278 | let min_max = sentence::parse_type_custom(Pattern::MinMax, description, |sentence| { 279 | let values = sentence.parts().first()?.as_inner(); 280 | let mut split = values.split('-'); 281 | let min = split.next()?.to_string(); 282 | let max = split.next()?.to_string(); 283 | Some((min, max)) 284 | })?; 285 | let one_of = sentence::parse_type_custom(Pattern::OneOf, description, |sentence| { 286 | Some( 287 | sentence 288 | .parts() 289 | .iter() 290 | .filter(|part| { 291 | part.has_quotes() 292 | || part.is_italic() 293 | || part.as_inner().chars().all(|c| c.is_ascii_digit()) 294 | }) 295 | .map(|part| part.as_inner()) 296 | .cloned() 297 | .dedup() 298 | .collect::>(), 299 | ) 300 | })?; 301 | 302 | let (min, max) = if let Some((min, max)) = min_max { 303 | (Some(min), Some(max)) 304 | } else { 305 | (None, None) 306 | }; 307 | 308 | let ty = match Type::new(s) { 309 | Type::Integer { 310 | default: type_default, 311 | min: type_min, 312 | max: type_max, 313 | one_of: type_one_of, 314 | } => { 315 | let one_of = if let Some(one_of) = one_of { 316 | one_of 317 | .into_iter() 318 | .map(|x| x.parse::()) 319 | .collect::>()? 320 | } else { 321 | type_one_of 322 | }; 323 | 324 | Type::Integer { 325 | default: default 326 | .as_deref() 327 | .map(str::parse) 328 | .transpose()? 329 | .or(type_default), 330 | min: min.as_deref().map(str::parse).transpose()?.or(type_min), 331 | max: max.as_deref().map(str::parse).transpose()?.or(type_max), 332 | one_of, 333 | } 334 | } 335 | Type::Bool { 336 | default: type_default, 337 | } => Type::Bool { 338 | default: default 339 | .as_deref() 340 | .map(str::to_lowercase) 341 | .as_deref() 342 | .map(str::parse) 343 | .transpose()? 344 | .or(type_default), 345 | }, 346 | Type::String { 347 | default: type_default, 348 | min_len: type_min_len, 349 | max_len: type_max_len, 350 | one_of: type_one_if, 351 | } if default.is_some() || min.is_some() || max.is_some() || one_of.is_some() => { 352 | Type::String { 353 | default: default.or(type_default), 354 | min_len: min.as_deref().map(str::parse).transpose()?.or(type_min_len), 355 | max_len: max.as_deref().map(str::parse).transpose()?.or(type_max_len), 356 | one_of: one_of.unwrap_or(type_one_if), 357 | } 358 | } 359 | x => x, 360 | }; 361 | 362 | Ok(ty) 363 | } 364 | 365 | pub fn extract_from_text(text: TypeParsingUnit) -> Result { 366 | fn strip_plural_ending(mut s: &str) -> &str { 367 | if s.ends_with("es") { 368 | s = s.strip_suffix('s').unwrap_or(s); 369 | } 370 | 371 | s 372 | } 373 | 374 | fn extract_type(sentence: &SentenceRef) -> Option { 375 | const ARRAY: &str = "Array"; 376 | const AN_ARRAY_OF: &[&str] = &["an", "array", "of"]; 377 | const OTHERWISE: &[&str] = &["otherwise"]; 378 | 379 | if sentence.contains(OTHERWISE) { 380 | let types = sentence 381 | .parts() 382 | .iter() 383 | .filter(|part| !part.as_inner().is_first_letter_lowercase()) 384 | .map(SentenceRef::from_part) 385 | .map(extract_type) 386 | .collect::>()?; 387 | Some(Type::Or(types)) 388 | } else { 389 | let (pos, part) = sentence 390 | .parts() 391 | .iter() 392 | .find_position(|part| !part.as_inner().is_first_letter_lowercase())?; 393 | let ty = part.as_inner(); 394 | let ty = strip_plural_ending(ty); 395 | 396 | if ty == ARRAY { 397 | let sentence = &sentence[pos + 1..]; 398 | let ty = extract_type(sentence)?; 399 | Some(Type::Array(Box::new(ty))) 400 | } else if sentence[pos.saturating_sub(AN_ARRAY_OF.len())..].starts_with(AN_ARRAY_OF) 401 | { 402 | let sentence = &sentence[pos..]; 403 | let ty = extract_type(sentence)?; 404 | Some(Type::Array(Box::new(ty))) 405 | } else { 406 | Some(Type::new(ty)) 407 | } 408 | } 409 | } 410 | 411 | sentence::parse_type_custom(Pattern::ReturnType, text, extract_type) 412 | .transpose() 413 | .ok_or_else(|| ParseError::TypeExtractionFailed(text.plain_text()))? 414 | } 415 | 416 | pub fn maybe_file_to_send(&self) -> bool { 417 | match self { 418 | Type::Integer { .. } | Type::String { .. } | Type::Bool { .. } | Type::Float => false, 419 | Type::Or(types) => types.iter().any(Self::maybe_file_to_send), 420 | Type::Array(ty) => ty.maybe_file_to_send(), 421 | // Kinda bad, but the alternative is hardcoding every value 422 | Type::Object(object) => object.starts_with("Input") && object != "InputPollOption", 423 | } 424 | } 425 | } 426 | 427 | #[derive(Debug, Copy, Clone)] 428 | pub enum TypeParsingUnit<'a> { 429 | Element(&'a ElementRef<'a>), 430 | Description(&'a RawDescription<'a>), 431 | } 432 | 433 | impl TypeParsingUnit<'_> { 434 | fn sentences(self) -> Result> { 435 | match self { 436 | TypeParsingUnit::Element(elem) => elem.sentences(), 437 | TypeParsingUnit::Description(description) => description.sentences(), 438 | } 439 | } 440 | 441 | fn plain_text(self) -> String { 442 | match self { 443 | TypeParsingUnit::Element(elem) => elem.plain_text(), 444 | TypeParsingUnit::Description(description) => description.plain_text(), 445 | } 446 | } 447 | } 448 | 449 | #[derive(Debug, Clone)] 450 | pub struct Object { 451 | pub name: String, 452 | pub description: String, 453 | pub data: ObjectData, 454 | pub docs_link: String, 455 | } 456 | 457 | #[derive(Debug, Clone)] 458 | pub enum ObjectData { 459 | Fields(Vec), 460 | Elements(Vec), 461 | /// Object without fields or elements 462 | /// So we don't know what it will be in the future 463 | Unknown, 464 | } 465 | 466 | #[derive(Debug, Clone)] 467 | pub struct Field { 468 | pub name: String, 469 | pub kind: Type, 470 | pub required: bool, 471 | pub description: String, 472 | } 473 | 474 | #[derive(Debug, Clone)] 475 | pub struct Method { 476 | pub name: String, 477 | pub description: String, 478 | pub args: MethodArgs, 479 | pub return_type: Type, 480 | pub docs_link: String, 481 | } 482 | 483 | #[derive(Debug, Clone)] 484 | pub enum MethodArgs { 485 | No, 486 | Yes(Vec), 487 | WithMultipart(Vec), 488 | } 489 | 490 | impl MethodArgs { 491 | fn new(args: Vec) -> Self { 492 | if args.iter().any(|arg| arg.kind.maybe_file_to_send()) { 493 | Self::WithMultipart(args) 494 | } else if args.is_empty() { 495 | Self::No 496 | } else { 497 | Self::Yes(args) 498 | } 499 | } 500 | } 501 | 502 | #[derive(Debug, Clone)] 503 | pub struct Argument { 504 | pub name: String, 505 | pub kind: Type, 506 | pub required: bool, 507 | pub description: String, 508 | } 509 | 510 | fn make_url_from_fragment(fragment: String) -> String { 511 | assert!(fragment.starts_with('#')); 512 | format!("{}{}", BOT_API_DOCS_URL, fragment) 513 | } 514 | 515 | trait RawDescriptionExt { 516 | fn sentences(&self) -> Result>; 517 | 518 | fn markdown(&self) -> String; 519 | 520 | fn plain_text(&self) -> String; 521 | } 522 | 523 | impl RawDescriptionExt for RawDescription<'_> { 524 | fn sentences(&self) -> Result> { 525 | self.0 526 | .iter() 527 | .map(ElementRef::sentences) 528 | .try_fold(Vec::new(), |mut acc, x| { 529 | acc.extend(x?); 530 | Ok(acc) 531 | }) 532 | } 533 | 534 | fn markdown(&self) -> String { 535 | html2md::parse_html_custom( 536 | &self.0.iter().map(ElementRef::html).join("\n"), 537 | &TagsHandlerFactory::new_in_map(), 538 | ) 539 | } 540 | 541 | fn plain_text(&self) -> String { 542 | self.0.iter().map(ElementRef::plain_text).join("\n") 543 | } 544 | } 545 | 546 | trait ElementRefParserExt { 547 | fn sentences(&self) -> Result>; 548 | 549 | fn markdown(&self) -> String; 550 | 551 | fn a_href(&self) -> Result; 552 | } 553 | 554 | impl ElementRefParserExt for ElementRef<'_> { 555 | fn sentences(&self) -> Result> { 556 | sentence::parse_node(*self.deref()) 557 | } 558 | 559 | fn markdown(&self) -> String { 560 | html2md::parse_html_custom(&self.html(), &TagsHandlerFactory::new_in_map()) 561 | } 562 | 563 | fn a_href(&self) -> Result { 564 | for edge in self.traverse() { 565 | if let Edge::Open(node) = edge { 566 | if let Node::Element(elem) = node.value() { 567 | if elem.name() == "a" { 568 | return elem.a_href(); 569 | } 570 | } 571 | } 572 | } 573 | 574 | Err(ParseError::MissingHref) 575 | } 576 | } 577 | 578 | trait ElementExt { 579 | fn a_href(&self) -> Result; 580 | } 581 | 582 | impl ElementExt for Element { 583 | fn a_href(&self) -> Result { 584 | self.attr("href") 585 | .map(str::to_string) 586 | .ok_or(ParseError::MissingHref) 587 | } 588 | } 589 | 590 | #[cfg(test)] 591 | mod tests { 592 | use super::*; 593 | 594 | #[test] 595 | fn or_type() { 596 | let ty = Type::new("Integer or String"); 597 | assert_eq!( 598 | ty, 599 | Type::Or(vec![ 600 | Type::Integer { 601 | default: None, 602 | min: None, 603 | max: None, 604 | one_of: vec![], 605 | }, 606 | Type::String { 607 | default: None, 608 | min_len: None, 609 | max_len: None, 610 | one_of: vec![] 611 | } 612 | ]) 613 | ) 614 | } 615 | 616 | #[test] 617 | fn array_of_type() { 618 | let ty = Type::new("Array of PhotoSize"); 619 | assert_eq!( 620 | ty, 621 | Type::Array(Box::new(Type::Object("PhotoSize".to_string()))) 622 | ); 623 | } 624 | 625 | #[test] 626 | fn array_of_array_type() { 627 | let ty = Type::new("Array of Array of PhotoSize"); 628 | assert_eq!( 629 | ty, 630 | Type::Array(Box::new(Type::Array(Box::new(Type::Object( 631 | "PhotoSize".to_string() 632 | ))))) 633 | ); 634 | } 635 | } 636 | -------------------------------------------------------------------------------- /src/parser/sentence.rs: -------------------------------------------------------------------------------- 1 | use super::{ParseError, TypeParsingUnit}; 2 | use crate::parser::ElementExt; 3 | use ego_tree::NodeRef; 4 | use itertools::Itertools; 5 | use logos::Logos; 6 | use scraper::{node::Text, Node}; 7 | use std::{mem, ops::Index, ptr, slice::SliceIndex}; 8 | use tendril::StrTendril; 9 | 10 | #[derive(Debug, PartialEq, Eq, Copy, Clone)] 11 | pub enum Pattern { 12 | ReturnType, 13 | Default, 14 | MinMax, 15 | OneOf, 16 | } 17 | 18 | impl Pattern { 19 | fn parts(self) -> Vec { 20 | match self { 21 | Pattern::ReturnType => vec![ 22 | SearcherPattern::default() 23 | .by_word("Returns") 24 | .by_word("the") 25 | .by_word("bot's") 26 | .by_word("Telegram") 27 | .exclude(), 28 | SearcherPattern::default() 29 | .by_word("Returns") 30 | .by_word("the") 31 | .by_word("list") 32 | .by_word("of") 33 | .exclude(), 34 | SearcherPattern::default().by_word("On").by_word("success"), 35 | SearcherPattern::default().by_word("Returns"), 36 | SearcherPattern::default().by_word("returns"), 37 | SearcherPattern::default().by_word("An"), 38 | ], 39 | Pattern::Default => vec![ 40 | SearcherPattern::default().by_word("Defaults").by_word("to"), 41 | SearcherPattern::default() 42 | .by_word("defaults") 43 | .by_word("to") 44 | .exclude(), 45 | SearcherPattern::default().by_word("defaults").by_word("to"), 46 | SearcherPattern::default() 47 | .by_word("must") 48 | .by_word("be") 49 | .by_kind(PartKind::Italic) 50 | .with_offset(-1), 51 | SearcherPattern::default() 52 | .by_word("always") 53 | .by_quotes() 54 | .with_offset(-1), 55 | ], 56 | Pattern::MinMax => vec![ 57 | SearcherPattern::default() 58 | .by_word("Values") 59 | .by_word("between"), 60 | SearcherPattern::default() 61 | .by_word("characters") 62 | .with_offset(-2), 63 | ], 64 | Pattern::OneOf => { 65 | vec![ 66 | SearcherPattern::default().by_word("either"), 67 | SearcherPattern::default().by_word("One").by_word("of"), 68 | SearcherPattern::default().by_word("one").by_word("of"), 69 | SearcherPattern::default().by_word("Can").by_word("be"), 70 | SearcherPattern::default() 71 | .by_word("can") 72 | .by_word("be") 73 | .by_quotes() 74 | .with_offset(-1), 75 | SearcherPattern::default() 76 | .by_quotes() 77 | .by_word("or") 78 | .by_quotes() 79 | .with_offset(-3), 80 | SearcherPattern::default().by_word("Choose").by_word("one"), 81 | ] 82 | } 83 | } 84 | } 85 | } 86 | 87 | #[derive(Debug, Default)] 88 | struct SearcherPattern { 89 | parts: Vec, 90 | offset: isize, 91 | exclude: bool, 92 | } 93 | 94 | impl SearcherPattern { 95 | fn by_word>(mut self, inner: T) -> Self { 96 | self.parts.push(SearchBy::word(inner)); 97 | self 98 | } 99 | 100 | fn by_kind(mut self, kind: PartKind) -> Self { 101 | self.parts.push(SearchBy::kind(kind)); 102 | self 103 | } 104 | 105 | fn by_quotes(mut self) -> Self { 106 | self.parts.push(SearchBy::quotes()); 107 | self 108 | } 109 | 110 | /// Useful for partial matching 111 | fn with_offset(mut self, offset: isize) -> Self { 112 | self.offset = offset; 113 | self 114 | } 115 | 116 | fn exclude(mut self) -> Self { 117 | self.exclude = true; 118 | self 119 | } 120 | } 121 | 122 | impl PartialEq<&[Part]> for SearcherPattern { 123 | fn eq(&self, other: &&[Part]) -> bool { 124 | self.parts == *other 125 | } 126 | } 127 | 128 | #[derive(Debug, Clone, Logos, Eq, PartialEq)] 129 | #[logos(skip r"[, ]")] 130 | #[logos(skip "\n")] 131 | enum SentenceLexer { 132 | #[regex(r#"[^, "“”\(\)\.\n]+"#)] 133 | Word, 134 | #[token(".")] 135 | Dot, 136 | #[token("\"")] 137 | #[token("“")] 138 | #[token("”")] 139 | /// In case line break between text and tag 140 | Quote, 141 | #[token("(")] 142 | LParen, 143 | #[token(")")] 144 | RParen, 145 | } 146 | 147 | #[derive(Debug)] 148 | pub(crate) struct Sentences { 149 | inner: Vec, 150 | } 151 | 152 | impl Sentences { 153 | pub(crate) fn parse(text: &str) -> Self { 154 | let tree = ego_tree::tree! { 155 | Node::Document => { 156 | Node::Text(Text { 157 | text: StrTendril::from_slice(text), 158 | }) 159 | } 160 | }; 161 | 162 | let sentences = parse_node(tree.root()).unwrap(); 163 | Self { inner: sentences } 164 | } 165 | 166 | pub fn find(&self, words: &[&str]) -> Option<&SentenceRef> { 167 | self.inner.iter().find_map(|sentence| { 168 | sentence.parts.windows(words.len()).find_map(|window| { 169 | if window == words { 170 | Some(sentence.as_ref()) 171 | } else { 172 | None 173 | } 174 | }) 175 | }) 176 | } 177 | 178 | pub(crate) fn find_and_crop(&self, words: &[&str]) -> Option<&SentenceRef> { 179 | self.inner.iter().find_map(|sentence| { 180 | sentence 181 | .parts 182 | .windows(words.len()) 183 | .position(|window| window == words) 184 | .map(|pos| &sentence[pos..]) 185 | }) 186 | } 187 | } 188 | 189 | #[derive(Debug, Clone, Hash, Default, PartialEq, Eq)] 190 | pub struct Part { 191 | inner: String, 192 | has_quotes: bool, 193 | kind: PartKind, 194 | } 195 | 196 | impl Part { 197 | fn new(inner: String) -> Self { 198 | Self { 199 | inner, 200 | ..Self::default() 201 | } 202 | } 203 | 204 | fn link(inner: String, link: String) -> Self { 205 | Self { 206 | inner, 207 | kind: PartKind::Link(link), 208 | ..Self::default() 209 | } 210 | } 211 | 212 | fn italic(inner: String) -> Self { 213 | Self { 214 | inner, 215 | kind: PartKind::Italic, 216 | ..Self::default() 217 | } 218 | } 219 | 220 | fn code(inner: String) -> Self { 221 | Self { 222 | inner, 223 | kind: PartKind::Code, 224 | ..Self::default() 225 | } 226 | } 227 | 228 | fn bold(inner: String) -> Self { 229 | Self { 230 | inner, 231 | kind: PartKind::Bold, 232 | ..Self::default() 233 | } 234 | } 235 | 236 | fn with_quotes(mut self, has_quotes: bool) -> Self { 237 | self.has_quotes = has_quotes; 238 | self 239 | } 240 | 241 | pub fn has_quotes(&self) -> bool { 242 | self.has_quotes 243 | } 244 | 245 | pub fn is_italic(&self) -> bool { 246 | matches!(self.kind, PartKind::Italic) 247 | } 248 | 249 | pub fn as_inner(&self) -> &String { 250 | &self.inner 251 | } 252 | } 253 | 254 | impl PartialEq<&str> for Part { 255 | fn eq(&self, other: &&str) -> bool { 256 | self.inner == *other 257 | } 258 | } 259 | 260 | #[derive(Debug, Clone, Hash, PartialEq, Eq)] 261 | enum PartKind { 262 | Word, 263 | Link(String), 264 | Bold, 265 | Italic, 266 | Code, 267 | } 268 | 269 | impl Default for PartKind { 270 | fn default() -> Self { 271 | Self::Word 272 | } 273 | } 274 | 275 | #[derive(Debug, Clone, PartialEq)] 276 | enum SearchBy { 277 | Word(String), 278 | Kind(PartKind), 279 | Quotes, 280 | } 281 | 282 | impl SearchBy { 283 | fn word>(inner: T) -> Self { 284 | Self::Word(inner.into()) 285 | } 286 | 287 | fn kind(kind: PartKind) -> Self { 288 | Self::Kind(kind) 289 | } 290 | 291 | fn quotes() -> Self { 292 | Self::Quotes 293 | } 294 | } 295 | 296 | impl PartialEq for SearchBy { 297 | fn eq(&self, other: &Part) -> bool { 298 | match self { 299 | SearchBy::Word(inner) => other.inner == *inner, 300 | SearchBy::Kind(kind) => other.kind == *kind, 301 | SearchBy::Quotes => other.has_quotes, 302 | } 303 | } 304 | } 305 | 306 | impl PartialEq<&str> for SearchBy { 307 | fn eq(&self, other: &&str) -> bool { 308 | match self { 309 | SearchBy::Word(s) => s == other, 310 | _ => false, 311 | } 312 | } 313 | } 314 | 315 | #[derive(Debug, Hash, PartialEq, Eq)] 316 | pub struct Sentence { 317 | parts: Vec, 318 | } 319 | 320 | impl Index for Sentence 321 | where 322 | I: SliceIndex<[Part], Output = [Part]>, 323 | { 324 | type Output = SentenceRef; 325 | 326 | fn index(&self, index: I) -> &Self::Output { 327 | &self.as_ref()[index] 328 | } 329 | } 330 | 331 | impl AsRef for Sentence { 332 | fn as_ref(&self) -> &SentenceRef { 333 | unsafe { &*(self.parts.as_slice() as *const [Part] as *const SentenceRef) } 334 | } 335 | } 336 | 337 | #[derive(Debug)] 338 | pub struct SentenceRef { 339 | parts: [Part], 340 | } 341 | 342 | impl SentenceRef { 343 | pub(crate) fn from_part(part: &Part) -> &Self { 344 | unsafe { &*(ptr::slice_from_raw_parts(part as *const Part, 1) as *const SentenceRef) } 345 | } 346 | 347 | pub(crate) fn len(&self) -> usize { 348 | self.parts.len() 349 | } 350 | 351 | pub(crate) fn starts_with(&self, words: &[&str]) -> bool { 352 | self.parts 353 | .get(..words.len()) 354 | .map(|slice| slice.iter().zip(words).all(|(l, &r)| l.inner == r)) 355 | .unwrap_or(false) 356 | } 357 | 358 | pub(crate) fn contains(&self, words: &[&str]) -> bool { 359 | self.parts.windows(words.len()).any(|parts| parts == words) 360 | } 361 | 362 | pub fn parts(&self) -> &[Part] { 363 | &self.parts 364 | } 365 | } 366 | 367 | impl Index for SentenceRef 368 | where 369 | I: SliceIndex<[Part], Output = [Part]>, 370 | { 371 | type Output = SentenceRef; 372 | 373 | fn index(&self, index: I) -> &Self::Output { 374 | unsafe { &*(&self.parts[index] as *const [Part] as *const SentenceRef) } 375 | } 376 | } 377 | 378 | #[derive(Debug, Copy, Clone, Eq, PartialEq)] 379 | enum QuoteState { 380 | Left, 381 | Right, 382 | None, 383 | } 384 | 385 | impl QuoteState { 386 | fn next_state(self) -> Self { 387 | match self { 388 | QuoteState::Left => QuoteState::Right, 389 | QuoteState::Right => QuoteState::None, 390 | QuoteState::None => QuoteState::Left, 391 | } 392 | } 393 | } 394 | 395 | pub(crate) fn parse_node(elem: NodeRef) -> Result, ParseError> { 396 | let mut sentences = vec![]; 397 | let mut parts = vec![]; 398 | let mut quote = QuoteState::None; 399 | let mut quote_part_start = 0; 400 | let mut paren = false; 401 | 402 | for node in elem.children() { 403 | match node.value() { 404 | Node::Text(text) => { 405 | let lexer = SentenceLexer::lexer(text); 406 | for (token, span) in lexer.spanned() { 407 | let lexeme = &text[span.start..span.end]; 408 | 409 | let token = match token { 410 | Ok(token) => token, 411 | Err(()) => { 412 | return Err(ParseError::Lexer { 413 | lexeme: lexeme.to_string(), 414 | input: text.to_string(), 415 | span, 416 | }); 417 | } 418 | }; 419 | 420 | match token { 421 | SentenceLexer::Word if !paren => { 422 | let part = Part::new(lexeme.to_string()); 423 | parts.push(part); 424 | } 425 | SentenceLexer::Dot if !paren && quote != QuoteState::Left => { 426 | sentences.push(Sentence { 427 | parts: mem::take(&mut parts), 428 | }); 429 | } 430 | SentenceLexer::Quote if !paren => { 431 | quote = quote.next_state(); 432 | 433 | match quote { 434 | QuoteState::Left => { 435 | quote_part_start = parts.len(); 436 | } 437 | QuoteState::Right => { 438 | let part = parts 439 | .drain(quote_part_start..) 440 | .map(|part| part.inner) 441 | .join(" "); 442 | let part = Part::new(part).with_quotes(true); 443 | parts.push(part); 444 | 445 | quote_part_start = 0; 446 | quote = QuoteState::None; 447 | } 448 | QuoteState::None => unreachable!(), 449 | } 450 | } 451 | SentenceLexer::LParen => paren = true, 452 | SentenceLexer::RParen => paren = false, 453 | _ => continue, 454 | } 455 | } 456 | } 457 | Node::Element(elem) if !paren => { 458 | let inner = node.first_child(); 459 | let text = inner 460 | .as_ref() 461 | .and_then(|node| node.value().as_text()) 462 | .map(|text| text.to_string()); 463 | 464 | let part = match (elem.name(), text) { 465 | ("a", Some(text)) => { 466 | let link = elem.a_href()?; 467 | Some(Part::link(text, link.to_string())) 468 | } 469 | ("a", None) => { 470 | let link = elem.a_href()?; 471 | Some(Part::new(link.to_string())) 472 | } 473 | ("em", Some(text)) => Some(Part::italic(text)), 474 | ("code", Some(text)) => Some(Part::code(text)), 475 | ("strong", Some(text)) => Some(Part::bold(text)), 476 | ("img", _) => { 477 | let alt = elem.attr("alt").ok_or(ParseError::MissingAlt)?; 478 | Some(Part::new(alt.to_string()).with_quotes(quote == QuoteState::Left)) 479 | } 480 | ("br", _) => None, 481 | ("li", _) => { 482 | if !parts.is_empty() { 483 | sentences.push(Sentence { 484 | parts: mem::take(&mut parts), 485 | }); 486 | } 487 | 488 | sentences.extend(parse_node(node)?); 489 | None 490 | } 491 | _ => { 492 | log::warn!("Tag {} skipped", elem.name()); 493 | None 494 | } 495 | }; 496 | parts.extend(part); 497 | } 498 | _ => continue, 499 | } 500 | } 501 | 502 | if !parts.is_empty() { 503 | sentences.push(Sentence { parts }); 504 | } 505 | 506 | Ok(sentences) 507 | } 508 | 509 | pub fn parse_type_custom( 510 | pattern: Pattern, 511 | text: TypeParsingUnit, 512 | extractor: E, 513 | ) -> Result, ParseError> 514 | where 515 | E: Fn(&SentenceRef) -> Option, 516 | { 517 | let sentences = text.sentences()?; 518 | let mut result = None; 519 | let patterns = pattern.parts(); 520 | 521 | 'sentences: for sentence in &sentences { 522 | for pattern in &patterns { 523 | for (word_idx, words) in sentence.parts.windows(pattern.parts.len()).enumerate() { 524 | if *pattern == words { 525 | if pattern.exclude { 526 | continue 'sentences; 527 | } 528 | 529 | let offset = (word_idx as isize + pattern.parts.len() as isize + pattern.offset) 530 | as usize; 531 | 532 | let sentence = &sentence[offset..]; 533 | result = Some(sentence); 534 | break 'sentences; 535 | } 536 | } 537 | } 538 | } 539 | 540 | Ok(result.and_then(extractor)) 541 | } 542 | 543 | #[cfg(test)] 544 | mod tests { 545 | use super::*; 546 | 547 | #[test] 548 | fn sentence_lexer_quote_and_words() { 549 | let mut sentence = SentenceLexer::lexer("\" base quote"); 550 | assert_eq!(sentence.next(), Some(Ok(SentenceLexer::Quote))); 551 | assert_eq!(sentence.next(), Some(Ok(SentenceLexer::Word))); 552 | assert_eq!(sentence.next(), Some(Ok(SentenceLexer::Word))); 553 | } 554 | 555 | #[test] 556 | fn sentence_parser_parentheses_ignored() { 557 | let sentences = Sentences::parse("Hello (really?), world!"); 558 | itertools::assert_equal( 559 | sentences.inner[0].parts.iter().map(|part| part.as_inner()), 560 | vec!["Hello", "world!"], 561 | ); 562 | } 563 | 564 | #[test] 565 | fn sentence_parser_one_word() { 566 | let sentences = Sentences::parse("One"); 567 | assert_eq!(sentences.inner.len(), 1); 568 | assert_eq!(sentences.inner[0].parts.len(), 1); 569 | assert_eq!(sentences.inner[0].parts[0], "One"); 570 | } 571 | 572 | #[test] 573 | fn sentence_parser_parts() { 574 | let sentences = Sentences::parse( 575 | r#"Emoji on which the dice throw animation is based. Currently, must be one of “🎲”, “🎯”, “🏀”, “⚽”, or “🎰”. Dice can have values 1-6 for “🎲” and “🎯”, values 1-5 for “🏀” and “⚽”, and values 1-64 for “🎰”. Defaults to “🎲”."#, 576 | ); 577 | assert_eq!(sentences.inner.len(), 4); 578 | assert_eq!(sentences.inner[0].parts.len(), 9); 579 | assert_eq!(sentences.inner[1].parts.len(), 11); 580 | assert_eq!(sentences.inner[2].parts.len(), 20); 581 | assert_eq!(sentences.inner[3].parts.len(), 3); 582 | } 583 | 584 | #[test] 585 | fn sentence_parser_quotes() { 586 | let sentences = Sentences::parse( 587 | r#"The section of the user's Telegram Passport which has the issue, one of “passport”, “driver_license”, “identity_card”, “internal_passport”."#, 588 | ); 589 | assert_eq!( 590 | sentences 591 | .inner 592 | .into_iter() 593 | .next() 594 | .unwrap() 595 | .parts 596 | .into_iter() 597 | .filter(|part| part.has_quotes) 598 | .map(|part| part.inner) 599 | .collect::>(), 600 | vec![ 601 | "passport", 602 | "driver_license", 603 | "identity_card", 604 | "internal_passport" 605 | ] 606 | ); 607 | } 608 | } 609 | -------------------------------------------------------------------------------- /src/parser/tags.rs: -------------------------------------------------------------------------------- 1 | use crate::{parser::make_url_from_fragment, CORE_TELEGRAM_URL}; 2 | use html2md::{common::get_tag_attr, Handle, StructuredPrinter, TagHandler, TagHandlerFactory}; 3 | use std::collections::HashMap; 4 | 5 | pub(crate) enum TagsHandlerFactory { 6 | Anchor, 7 | Image, 8 | } 9 | 10 | impl TagsHandlerFactory { 11 | pub(crate) fn new_in_map() -> HashMap> { 12 | let mut map = HashMap::new(); 13 | map.insert("a".to_string(), Box::new(TagsHandlerFactory::Anchor) as _); 14 | map.insert("img".to_string(), Box::new(TagsHandlerFactory::Image) as _); 15 | map 16 | } 17 | } 18 | 19 | impl TagHandlerFactory for TagsHandlerFactory { 20 | fn instantiate(&self) -> Box { 21 | match self { 22 | TagsHandlerFactory::Anchor => Box::::default(), 23 | TagsHandlerFactory::Image => Box::new(ImageHandler), 24 | } 25 | } 26 | } 27 | 28 | #[derive(Default)] 29 | struct AnchorHandler { 30 | inner: Option<(usize, String)>, 31 | } 32 | 33 | impl TagHandler for AnchorHandler { 34 | fn handle(&mut self, tag: &Handle, printer: &mut StructuredPrinter) { 35 | self.inner = get_tag_attr(tag, "href") 36 | .map(|value| { 37 | if value.starts_with('#') { 38 | make_url_from_fragment(value) 39 | } else if value.starts_with('/') { 40 | [CORE_TELEGRAM_URL, &value].concat() 41 | } else { 42 | value 43 | } 44 | }) 45 | .map(|value| (printer.data.len(), value)) 46 | } 47 | 48 | fn after_handle(&mut self, printer: &mut StructuredPrinter) { 49 | let (pos, value) = self.inner.as_ref().unwrap(); 50 | if *pos != printer.data.len() { 51 | printer.insert_str(*pos, "["); 52 | printer.append_str(&format!("]({})", value)); 53 | } 54 | } 55 | } 56 | 57 | struct ImageHandler; 58 | 59 | impl TagHandler for ImageHandler { 60 | fn handle(&mut self, tag: &Handle, printer: &mut StructuredPrinter) { 61 | let alt = get_tag_attr(tag, "alt"); 62 | 63 | if let Some(alt) = alt { 64 | printer.append_str(&alt); 65 | } else { 66 | html2md::images::ImgHandler::default().handle(tag, printer) 67 | } 68 | } 69 | 70 | fn after_handle(&mut self, _printer: &mut StructuredPrinter) {} 71 | } 72 | 73 | #[cfg(test)] 74 | mod tests { 75 | use super::*; 76 | use crate::BOT_API_DOCS_URL; 77 | 78 | #[test] 79 | fn empty_link_skipped() { 80 | let map = TagsHandlerFactory::new_in_map(); 81 | let md = html2md::parse_html_custom(r#""#, &map); 82 | assert_eq!(md, ""); 83 | } 84 | 85 | #[test] 86 | fn make_absolute_a_href() { 87 | let map = TagsHandlerFactory::new_in_map(); 88 | let md = html2md::parse_html_custom(r##"This is a link"##, &map); 89 | assert_eq!( 90 | md, 91 | format!("[This is a link]({}#fragment)", BOT_API_DOCS_URL) 92 | ); 93 | let md = 94 | html2md::parse_html_custom(r##"This is a link"##, &map); 95 | assert_eq!( 96 | md, 97 | format!("[This is a link]({}/bots/webapps)", CORE_TELEGRAM_URL) 98 | ) 99 | } 100 | 101 | #[test] 102 | fn extract_img_alt() { 103 | let map = TagsHandlerFactory::new_in_map(); 104 | let md = html2md::parse_html_custom( 105 | r#"🎲, 🎯"#, 106 | &map, 107 | ); 108 | assert_eq!(md, "🎲, 🎯"); 109 | 110 | const TAGS: &str = r#", "#; 111 | let md = html2md::parse_html_custom(TAGS, &map); 112 | assert_eq!(md, TAGS); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/util.rs: -------------------------------------------------------------------------------- 1 | use ego_tree::iter::Edge; 2 | use scraper::{ElementRef, Node}; 3 | 4 | pub trait StrExt { 5 | #[allow(clippy::wrong_self_convention)] 6 | fn is_first_letter_lowercase(self) -> bool; 7 | } 8 | 9 | impl<'a> StrExt for &'a str { 10 | fn is_first_letter_lowercase(self) -> bool { 11 | self.chars().next().map(|c| c.is_lowercase()).unwrap() 12 | } 13 | } 14 | 15 | pub trait ElementRefExt { 16 | fn plain_text(&self) -> String; 17 | } 18 | 19 | impl ElementRefExt for ElementRef<'_> { 20 | fn plain_text(&self) -> String { 21 | self.traverse() 22 | .filter_map(|edge| { 23 | if let Edge::Open(node) = edge { 24 | return match node.value() { 25 | Node::Text(text) => Some(text.as_ref()), 26 | Node::Element(elem) if elem.name() == "img" => elem.attr("alt"), 27 | Node::Element(elem) if elem.name() == "br" => Some("\n"), 28 | _ => None, 29 | }; 30 | } 31 | 32 | None 33 | }) 34 | .collect() 35 | } 36 | } 37 | --------------------------------------------------------------------------------