├── .python-version ├── CHANGELOG.md ├── LICENSE ├── OpenUri.sublime-settings ├── README.md ├── bindings ├── Default (Linux).sublime-keymap ├── Default (OSX).sublime-keymap └── Default (Windows).sublime-keymap ├── boot.py ├── dependencies.json ├── images ├── FontAwesome │ ├── Font Awesome Free License.md │ ├── external-link-square.png │ ├── external-link.png │ ├── link.png │ ├── share-square.png │ └── star.png ├── package-lock.json ├── package.json └── self-made │ ├── go.png │ └── open.png ├── menus ├── Context.sublime-menu ├── Default.sublime-commands └── Main.sublime-menu ├── messages.json ├── messages └── update_message.md └── plugin ├── __init__.py ├── commands ├── __init__.py ├── abstract.py ├── copy_uri.py ├── open_uri.py └── select_uri.py ├── constants.py ├── helpers.py ├── libs ├── imagesize.py ├── png.py └── triegex │ ├── LICENSE │ └── __init__.py ├── listener.py ├── logger.py ├── renderer.py ├── settings.py ├── shared.py ├── types.py ├── ui ├── image.py ├── phantom_set.py ├── phatom_sets_manager.py ├── popup.py └── region_drawing.py └── utils.py /.python-version: -------------------------------------------------------------------------------- 1 | 3.8 2 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # OpenUri Changelog 2 | 3 | ## 8.0.2 4 | 5 | - fix: wrong results after `undo` command 6 | - refactor: tidy codes 7 | 8 | ## 8.0.1 9 | 10 | Package Control v4 is required due to the use of `more-itertools` py38 dependency. 11 | 12 | - refactor: tidy codes 13 | - chore: add `more-itertools` as a dependency 14 | 15 | ## 8.0.0 16 | 17 | - refactor: tidy codes (ST>=4152) 18 | 19 | ## 7.2.3 20 | 21 | - refactor: add `NO_UNDO` flag to `view.add_regions` 22 | - refactor: sort merged regex by scheme length DESC 23 | - refactor: use py38 syntax 24 | - chore: format `pyproject.toml` with taplo 25 | 26 | ## 7.2.2 27 | 28 | - refactor: tidy codes 29 | 30 | ## 7.2.1 31 | 32 | - fix: occasionally startup exception 33 | 34 | ## 7.2.0 35 | 36 | - feat: capture URLs starting with "www" without a scheme 37 | 38 | ## 7.1.5 39 | 40 | - fix: wrong link in "Copy ..." context menu 41 | 42 | ## 7.1.4 43 | 44 | - refactor: for ST >= 4132, utilize `view.expand_to_scope` API 45 | 46 | ## 7.1.3 47 | 48 | - refactor: simplify `boot.py` 49 | 50 | ## 7.1.2 51 | 52 | - fix: modules should be reloaded when update plugin 53 | - fix: RuntimeError: dictionary changed size during iteration 54 | - refactor: tweak directory structure 55 | 56 | ## 7.1.1 57 | 58 | - fix: wrong result if selected region's `.b > .a` 59 | - chore: improve type annotations 60 | 61 | ## 7.1.0 62 | 63 | - feat: add commands to copy URIs 64 | - feat: copy/select URIs from context menu 65 | 66 | ## 7.0.2 67 | 68 | - fix: URI ends with "." in markdown image/link context (#8) 69 | 70 | This fix also introduces a new setting: `expand_uri_regions_selectors` 71 | 72 | ## 7.0.1 73 | 74 | - feat: check all foreground views for updating 75 | 76 | Previously, OpenUri only checks the current activated view. 77 | Since OpenUri also checks whether the view is dirty before perform a update, 78 | I think this shouldn't be resource consuming. 79 | 80 | ## 7.0.0 81 | 82 | - refactor: drop ST 3 support 83 | - refactor: default mouse binding has been removed 84 | 85 | If you need the mouse binding, add 86 | 87 | ```js 88 | [ 89 | // open URL via: alt + right click 90 | { 91 | button: 'button2', 92 | modifiers: ['alt'], 93 | command: 'open_context_url', 94 | }, 95 | ] 96 | ``` 97 | 98 | ## 6.4.1 99 | 100 | - refactor: tidy codes 101 | 102 | ## 6.4.0 103 | 104 | Added 105 | 106 | - Use strings in setting `draw_uri_regions.flags` 107 | 108 | Previously, it's simply an integer, which is less self-explanatory. 109 | Now, you can use a list of strings (flag names): 110 | 111 | ```js 112 | "draw_uri_regions": { 113 | // the draw flags used, see https://www.sublimetext.com/docs/api_reference.html 114 | "flags": [ 115 | "HIDE_ON_MINIMAP", 116 | "DRAW_SOLID_UNDERLINE", 117 | "DRAW_NO_FILL", 118 | "DRAW_NO_OUTLINE", 119 | ], 120 | }, 121 | ``` 122 | 123 | Changed 124 | 125 | - make `plugin_loaded()` async to speed up ST startup 126 | - make plugin folder structure simpler 127 | 128 | Documentation 129 | 130 | - Update settings comments for recent ST 4 release 131 | 132 | ## 6.3.0 133 | 134 | Added 135 | 136 | - Add setting: `work_for_transient_view` 137 | 138 | This setting controls whether this plugin should work for a transient view 139 | such as file preview via "Goto Anything". 140 | 141 | ## 6.2.16 142 | 143 | Fixed 144 | 145 | - Plugin is not working for transient view. (#5) 146 | 147 | ## 6.2.15 148 | 149 | Fixed 150 | 151 | - Replace `open_uri_from_cursors` with `open_context_url` in mousemap. 152 | 153 | ## 6.2.14 154 | 155 | Fixed 156 | 157 | - Phantom is not generated when there is `&` in the URI. 158 | 159 | ## 6.2.13 160 | 161 | Fixed 162 | 163 | - As of ST 4, remove a phantom position workaround. 164 | 165 | ## 6.2.12 166 | 167 | Fixed 168 | 169 | - Prevent from frequently calling `sublime.load_settings()`. 170 | 171 | ## 6.2.11 172 | 173 | Fixed 174 | 175 | - Delete phantoms when plugin unloaded. 176 | 177 | ## 6.2.10 178 | 179 | Fixed 180 | 181 | - Phantom padding for some color schemes. 182 | 183 | Some color schemes such as Material Theme's have large 184 | unneeded padding for phantoms somehow. 185 | 186 | ## 6.2.9 187 | 188 | Changed 189 | 190 | - Run with Python 3.8 in ST 4. 191 | 192 | ## 6.2.8 193 | 194 | Changed 195 | 196 | - `file://` scheme is enabled by default now. 197 | - Update icon `FontAwesome/external-link.svg`. 198 | 199 | Fixed 200 | 201 | - `file://` scheme is not working for URL-encoded URIs. 202 | 203 | ## 6.2.7 204 | 205 | Changed 206 | 207 | - Bound renderer interval with a minimum value. 208 | So if you accidentally use a tiny value like `0` will not causing ST unresponsive. 209 | 210 | ## 6.2.6 211 | 212 | Added 213 | 214 | - Introduce `mypy` for static analysis. 215 | 216 | ## 6.2.5 217 | 218 | Added 219 | 220 | - Add and utilize the `typing` module. 221 | 222 | Changed 223 | 224 | - Make `get_package_name()` not hard-coded. 225 | 226 | ## 6.2.4 227 | 228 | Changed 229 | 230 | - The default mouse binding has been changed to `Ctrl + Right Click`. 231 | Because the `Alt` key seems not working under Linux and I would like to 232 | provide a binding that hopefully works under all platform. 233 | 234 | ## 6.2.3 235 | 236 | Fixed 237 | 238 | - Do not let exceptions terminate the rendering thread. 239 | 240 | ## 6.2.2 241 | 242 | Fixed 243 | 244 | - Fix renderer thread crashes when viewing an image file with ST. 245 | 246 | ## 6.2.1 247 | 248 | Changed 249 | 250 | - Just some code structure tweaks. 251 | 252 | Fixed 253 | 254 | - Prevent from weird `phantom_set_id` KeyError. 255 | 256 | ## 6.2.0 257 | 258 | Added 259 | 260 | - Add new log level: DEBUG_LOW 261 | 262 | Changed 263 | 264 | - Render phantoms/regions with another background thread. 265 | - Rename `on_modified_typing_period` to `typing_period`. 266 | 267 | Fixed 268 | 269 | - Fix and remove workaround for `is_view_too_large()`. 270 | 271 | ## 6.1.1 272 | 273 | Fixed 274 | 275 | - Optimize default URI regex for BBCode mismatches. 276 | 277 | ## 6.1.0 278 | 279 | Added 280 | 281 | - Allow drawing URI regions on hovering. 282 | - Make text in the hovering popup configurable. (`popup_text_html`) 283 | - Add log to show activated schemes. 284 | 285 | Changed 286 | 287 | - Allow drawing URI regions even if `show_open_button` is `"never"`. 288 | - Optimize the generated URI-matching regex. 289 | - Change default key binding to `alt+o`, `alt+u`. 290 | - Some minor mathematical optimizations. 291 | 292 | ## 6.0.0 293 | 294 | Changed 295 | 296 | - Plugin has been renamed from `OpenUriInBrowser` to `OpenUri`. 297 | - `use_show_open_button_fallback_if_file_larger_than` defaults to `1MB`. 298 | 299 | ## 5.7.0 300 | 301 | Added 302 | 303 | - Allow using different path regexes for different schemes. 304 | 305 | Changed 306 | 307 | - Change `uri_path_regex` to prevent from some HTML problem. 308 | Escaped HTML entity may be trailing in a URL. 309 | Disallow `<...>` in URI because it's ambiguous with HTML tags. 310 | 311 | Fixed 312 | 313 | - Fix scheme for `mailto:`. 314 | 315 | ## 5.6.0 316 | 317 | Added 318 | 319 | - Better logging messages with `log_level`. 320 | - Better fitting for light/dark images via inverting gray scale. 321 | 322 | Changed 323 | 324 | - Auto refresh after saving the settings file to reflect changes. 325 | 326 | Fixed 327 | 328 | - Fix URL matching regex keeps getting compiled in `find_uri_regions_by_regions()`. 329 | 330 | ## 5.5.0 331 | 332 | Added 333 | 334 | - Add new command: `open_uri_from_view`. 335 | - Add default key/mouse bindings. 336 | - Allow setting a different image and color for popup. 337 | 338 | Changed 339 | 340 | - Hovered behavior now uses popup rather than phantom. 341 | - Image-related user settings have been restructured. 342 | - Command names have been changed. 343 | 344 | - `open_uri_in_browser_from_cursor` -> `open_uri_from_cursors` 345 | - `select_uri_from_cursor` -> `select_uri_from_cursors` 346 | - `select_uri` -> `select_uri_from_view` 347 | 348 | ## 5.4.0 349 | 350 | Added 351 | 352 | - Add new FontAwesome imagaes: `link`, `share-square`, `star`. 353 | - Add new `image_new_window_color` values: `"@scope"`, `"@scope_inverted"`. 354 | This will make the phantom be the same color with the corresponding URI. 355 | 356 | Changed 357 | 358 | - Change default "on_modified_typing_period" to 150. 359 | - Downscaling FontAwesome images to 48x48. 360 | 361 | ## 5.3.0 362 | 363 | Added 364 | 365 | - Colored phantom PNG images are now generated in-memory. 366 | So you are able to use any color for those images. 367 | See setting `image_new_window_color`. 368 | 369 | Changed 370 | 371 | - Use icons from FontAwesome. 372 | - Default `uri_path_regex` now matches Unicode URIs. 373 | 374 | Fixed 375 | 376 | - Fix phantom may break "scope brackets" `` ` `` for BracketHilighter. 377 | - Fix scaling ratio when using non-square images in phantoms. 378 | 379 | ## 5.2.1 380 | 381 | Added 382 | 383 | - Add setting `use_show_open_button_fallback_if_file_larger_than`. 384 | - Add setting `show_open_button_fallback`. 385 | - Add new `show_open_button` values: `never`. 386 | 387 | Fixed 388 | 389 | - `on_hover` now draws URI regions if `draw_uri_regions` is enabled. 390 | 391 | ## 5.1.2 392 | 393 | Fixed 394 | 395 | - Fix that I misunderstand how `sublime.Settings.add_on_change()` works. 396 | 397 | ## 5.1.1 398 | 399 | nits 400 | 401 | ## 5.1.0 402 | 403 | Added 404 | 405 | - Add config the regex to match URI's path part. (`uri_path_regex`) 406 | - Add new schemes: `mms://` and `sftp://`. 407 | 408 | ## 5.0.0 409 | 410 | Changed 411 | 412 | - The settings of `detect_schemes` is now plain text rather than regex. 413 | This allows the generated regex to be further optimized. 414 | 415 | ## 4.1.0 416 | 417 | Added 418 | 419 | - Add the ability to highlight URI regions. (see `draw_uri_regions`) 420 | 421 | Changed 422 | 423 | - Remove detection for websocket scheme. 424 | 425 | ## 4.0.5 426 | 427 | Changed 428 | 429 | - Better phantom looking. 430 | 431 | ## 4.0.4 432 | 433 | Changed 434 | 435 | - Change default "on_modified_typing_period" to 150. 436 | 437 | ## 4.0.3 438 | 439 | Fixed 440 | 441 | - Fix circular import. 442 | 443 | ## 4.0.2 444 | 445 | Changed 446 | 447 | - Cache the "new_window" image content in memory. 448 | 449 | ## 4.0.1 450 | 451 | Changed 452 | 453 | - Setting `on_hover` is now changed to `show_open_button`. 454 | `"on_hover": true` is the same with `"show_open_button": "hover"`. 455 | `"on_hover": false` is the same with `"show_open_button": "always"`. 456 | 457 | ## 3.2.3 458 | 459 | Fixed 460 | 461 | - Fixed old phantoms are not removed after reloading plugin. 462 | 463 | ## 3.2.2 464 | 465 | Added 466 | 467 | - Add commands: `select_uri` and `select_uri_from_cursor`. 468 | - Add some menus. 469 | 470 | ## 3.1.0 471 | 472 | Added 473 | 474 | - Add config: `on_modified_typing_period`. 475 | 476 | ## 3.0.3 477 | 478 | Changed 479 | 480 | - Plugin name has been changed to `OpenUriInBrowser`. 481 | - Command `open_url_in_browser_from_cursor` has been changed to `open_uri_in_browser_from_cursor`. 482 | - All other `URL`-related things have been renamed to `URI` if not mentioned here. 483 | 484 | ## 2.0.4 485 | 486 | Added 487 | 488 | - Allow multiple cursors to open multiple URLs at once via a command (`open_url_in_browser_from_cursor`). 489 | 490 | Changed 491 | 492 | - Technically a total rewrite. 493 | - Simplified URL-finding REGEX. 494 | - Self-managed phantom set. Do not clear phantoms when a view is deactivated. 495 | - Use binary searching to find URLs which should be opened. 496 | 497 | ## 1.2.7 498 | 499 | Added 500 | 501 | - User level settings. 502 | 503 | ## 1.2.6 504 | 505 | Changed 506 | 507 | - Getting phantom icon from a function. 508 | 509 | ## 1.2.5 510 | 511 | Changed 512 | 513 | - Fix to read phantom icon. 514 | 515 | ## 1.2.4 516 | 517 | Changed 518 | 519 | - Fix to read phantom icon. 520 | 521 | ## 1.2.3 522 | 523 | Changed 524 | 525 | - Fix settings file. 526 | 527 | ## 1.2.2 528 | 529 | Added 530 | 531 | - Add image to README.md. 532 | 533 | Changed 534 | 535 | - Fix settings file. 536 | - Update README.md. 537 | 538 | ## 1.2.1 539 | 540 | Changed 541 | 542 | - Remove firefox as default browser. 543 | - Update README.md. 544 | 545 | ## 1.2.0 546 | 547 | Added 548 | 549 | - Regex to detect URLs. 550 | - Option to display links permanently or on hover. 551 | 552 | Removed 553 | 554 | - Support for opening files was removed as the usage is very limited. 555 | 556 | ## 1.1.1 557 | 558 | Added 559 | 560 | - Command option to open settings. 561 | 562 | Fixed 563 | 564 | - Fix for URLs with special characters. 565 | 566 | ## 1.1.0 567 | 568 | Added 569 | 570 | - Provision to specify custom browser to open links. 571 | - Option to disable plugin. 572 | - Open files with their default application on system. 573 | 574 | Changed 575 | 576 | - Popup was replaced by phantom as suggested by @FichteFoll. 577 | 578 | ## 1.0.0 579 | 580 | Added 581 | 582 | - "Open in Browser" popup while hovering over links or filepaths surrounded by quotes or whitespaces. 583 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019-2024 Jack Cherng 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. 22 | -------------------------------------------------------------------------------- /OpenUri.sublime-settings: -------------------------------------------------------------------------------- 1 | { 2 | // how detailed log messages should be? 3 | // values can be 4 | // - "NOTHING" (do not show anything) 5 | // - "CRITICAL" (something makes plugin not workable) 6 | // - "ERROR" (something is wrong) 7 | // - "WARNING" (something may be wrong) 8 | // - "INFO" (those that users may interest in) 9 | // - "DEBUG" (for developer) 10 | // - "DEBUG_LOW" (for developer, even more detailed than DEBUG) 11 | "log_level": "INFO", 12 | // browser used to open a URI. leave this empty to use a default browser. 13 | // available values could be found on https://docs.python.org/3.8/library/webbrowser.html#webbrowser.get 14 | "browser": "", 15 | // when to show a phantom/popup button beside a URI? 16 | // values can be 17 | // - "always" (always show buttons) 18 | // - "hover" (only when the URI is hovered) 19 | // - "never" (never show buttons) 20 | "show_open_button": "always", 21 | // if the file is too large, this setting will be used as the fallback setting of "show_open_button" 22 | "show_open_button_fallback": "hover", 23 | // should this plugin works for transient view such as "Go to Anywhere" preview? 24 | "work_for_transient_view": false, 25 | // if the file size is larger than the given one, it will uses "show_open_button_fallback" as the fallback mode 26 | "large_file_threshold": 1000000, // 1MB 27 | // the period (in millisecond) that consecutive modifications are treated as typing 28 | // phantoms will be updated only when the user is not considered typing 29 | "typing_period": 250, 30 | // the interval (in millisecond) for checking whether to render the current view 31 | // 500 means the background thread will check the current view should be re-rendered or not, every 500ms 32 | "renderer_interval": 500, 33 | // scope selectors used to expand regions of URIs 34 | "expand_uri_regions_selectors": ["markup.underline.link"], 35 | // the text HTML used in the hovering popup 36 | "popup_text_html": "Open this URI", 37 | // images used in this plugin (only supports PNG format) 38 | // for other plugin-shipped images, visit https://github.com/jfcherng/Sublime-OpenUri/tree/st4/images 39 | // if you don't like them, you can even define your own image path. 40 | "image_files": { 41 | "phantom": "Packages/${package_name}/images/FontAwesome/external-link-square.png", 42 | "popup": "Packages/${package_name}/images/FontAwesome/share-square.png", 43 | }, 44 | // colors which used to color corresponding images 45 | // values can be 46 | // - "" (empty string, use the original color of the image) 47 | // - "@scope" (use the same color with the corresponding URI's) 48 | // - "@scope_inverted" (use the inverted color of the corresponding URI's) 49 | // - ST's scope (use the color of the scope) 50 | // - color code in the form of "#RGB", "#RRGGBB" or "#RRGGBBAA" 51 | "image_colors": { 52 | "phantom": "#fa8c00", 53 | "popup": "#fa8c00", 54 | }, 55 | // draw URI regions such as adding a underline? 56 | "draw_uri_regions": { 57 | // when to draw URI regions? 58 | // values can be 59 | // - "always" (always draw) 60 | // - "hover" (only when the URI is hovered) 61 | // - "never" (never draw) 62 | "enabled": "never", 63 | // the scope used to highlight URI regions (you may customize it with your color scheme) 64 | "scope": "string", 65 | // icon in the gutter: "dot", "circle", "bookmark" or empty string for nothing 66 | "icon": "", 67 | // the draw flags used, see https://www.sublimetext.com/docs/api_reference.html 68 | "flags": [ 69 | "HIDE_ON_MINIMAP", 70 | "DRAW_SOLID_UNDERLINE", 71 | "DRAW_NO_FILL", 72 | "DRAW_NO_OUTLINE", 73 | ], 74 | }, 75 | // defined schemes (case-insensitive) that wants to be detected 76 | // you may add your own new schemes to be detected 77 | // key / value = scheme / enabled 78 | "detect_schemes": { 79 | // URLs starting with "www" without a scheme 80 | "": {"enabled": true, "path_regex": "www"}, 81 | // basic 82 | "file://": {"enabled": true, "path_regex": "@default"}, 83 | "ftp://": {"enabled": true, "path_regex": "@default"}, 84 | "ftps://": {"enabled": true, "path_regex": "@default"}, 85 | "http://": {"enabled": true, "path_regex": "@default"}, 86 | "https://": {"enabled": true, "path_regex": "@default"}, 87 | "mailto:": {"enabled": true, "path_regex": "@default"}, 88 | // server 89 | "sftp://": {"enabled": false, "path_regex": "@default"}, 90 | "ssh://": {"enabled": false, "path_regex": "@default"}, 91 | "telnet://": {"enabled": false, "path_regex": "@default"}, 92 | // P2P 93 | "ed2k://": {"enabled": false, "path_regex": "@default"}, 94 | "freenet://": {"enabled": false, "path_regex": "@default"}, 95 | "magnet:?": {"enabled": false, "path_regex": "@default"}, 96 | // messenger 97 | "irc://": {"enabled": false, "path_regex": "@default"}, 98 | "line://": {"enabled": false, "path_regex": "@default"}, 99 | "skype:": {"enabled": false, "path_regex": "@default"}, 100 | "tencent://": {"enabled": false, "path_regex": "@default"}, 101 | "tg://": {"enabled": false, "path_regex": "@default"}, 102 | }, 103 | // regexes (case-insensitive) used to match the URI's path part 104 | "uri_path_regexes": { 105 | // this is the default path regex for all schemes 106 | "@default": "(?:[^\\s()\\[\\]{}<>`^*'\"“”‘’]|\\((?![/\\\\])[^\\s)]*\\)|\\[(?![/\\\\])[^\\s\\]]*\\]|\\{(?![/\\\\])[^\\s}]*\\})+(?`^*'\"“”‘’]|\\((?![/\\\\])[^\\s)]*\\)|\\[(?![/\\\\])[^\\s\\]]*\\]|\\{(?![/\\\\])[^\\s}]*\\})+(?Alt + o, Alt + u 35 | (`o, u` is mnemonic for `Open, URI`) 36 | 37 | ### Mouse Bindings 38 | 39 | There is no mouse binding but you can add one if you need. 40 | 41 | Create `Packages/OpenUri/Default.sublime-mousemap` with the following content. 42 | 43 | ```js 44 | [ 45 | // open URL via: alt + right click 46 | { 47 | button: 'button2', 48 | modifiers: ['alt'], 49 | command: 'open_context_url', 50 | }, 51 | ] 52 | ``` 53 | 54 | ## Commands 55 | 56 | These commands are always available no matter what `show_open_button` is or how large the file is. 57 | 58 | | Command | Functionality | 59 | | ----------------------- | --------------------------------- | 60 | | open_uri_from_cursors | Open URIs from cursors | 61 | | open_uri_from_view | Open URIs from the current view | 62 | | copy_uri_from_cursors | Copy URIs from cursors | 63 | | copy_uri_from_view | Copy URIs from the current view | 64 | | select_uri_from_cursors | Select URIs from cursors | 65 | | select_uri_from_view | Select URIs from the current view | 66 | 67 | [openuri]: https://packagecontrol.io/packages/OpenUri 68 | [package-control]: https://packagecontrol.io 69 | [settings-file]: https://github.com/jfcherng-sublime/ST-OpenUri/blob/st4/OpenUri.sublime-settings 70 | -------------------------------------------------------------------------------- /bindings/Default (Linux).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["alt+o", "alt+u"], 4 | "command": "open_uri_from_cursors" 5 | // "args": {"browser": ""}, // if you want to force using a specific browser 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /bindings/Default (OSX).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["alt+o", "alt+u"], 4 | "command": "open_uri_from_cursors" 5 | // "args": {"browser": ""}, // if you want to force using a specific browser 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /bindings/Default (Windows).sublime-keymap: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "keys": ["alt+o", "alt+u"], 4 | "command": "open_uri_from_cursors" 5 | // "args": {"browser": ""}, // if you want to force using a specific browser 6 | } 7 | ] 8 | -------------------------------------------------------------------------------- /boot.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | 4 | def reload_plugin() -> None: 5 | import sys 6 | 7 | # remove all previously loaded plugin modules. 8 | prefix = f"{__package__}." 9 | for module_name in tuple(filter(lambda m: m.startswith(prefix) and m != __name__, sys.modules)): 10 | del sys.modules[module_name] 11 | 12 | 13 | reload_plugin() 14 | 15 | from .plugin import * # noqa: F401, F403 16 | -------------------------------------------------------------------------------- /dependencies.json: -------------------------------------------------------------------------------- 1 | { 2 | "*": { 3 | "*": [ 4 | "more-itertools" 5 | ] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /images/FontAwesome/Font Awesome Free License.md: -------------------------------------------------------------------------------- 1 | Font Awesome Free License 2 | ------------------------- 3 | 4 | Font Awesome Free is free, open source, and GPL friendly. You can use it for 5 | commercial projects, open source projects, or really almost whatever you want. 6 | Full Font Awesome Free license: https://fontawesome.com/license/free. 7 | 8 | # Icons: CC BY 4.0 License (https://creativecommons.org/licenses/by/4.0/) 9 | In the Font Awesome Free download, the CC BY 4.0 license applies to all icons 10 | packaged as SVG and JS file types. 11 | 12 | # Fonts: SIL OFL 1.1 License (https://scripts.sil.org/OFL) 13 | In the Font Awesome Free download, the SIL OFL license applies to all icons 14 | packaged as web and desktop font files. 15 | 16 | # Code: MIT License (https://opensource.org/licenses/MIT) 17 | In the Font Awesome Free download, the MIT license applies to all non-font and 18 | non-icon files. 19 | 20 | # Attribution 21 | Attribution is required by MIT, SIL OFL, and CC BY licenses. Downloaded Font 22 | Awesome Free files already contain embedded comments with sufficient 23 | attribution, so you shouldn't need to do anything additional when using these 24 | files normally. 25 | 26 | We've kept attribution comments terse, so we ask that you do not actively work 27 | to remove them from files, especially code. They're a great way for folks to 28 | learn about Font Awesome. 29 | 30 | # Brand Icons 31 | All brand icons are trademarks of their respective owners. The use of these 32 | trademarks does not indicate endorsement of the trademark holder by Font 33 | Awesome, nor vice versa. **Please do not use brand logos for any purpose except 34 | to represent the company, product, or service to which they refer.** 35 | -------------------------------------------------------------------------------- /images/FontAwesome/external-link-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/images/FontAwesome/external-link-square.png -------------------------------------------------------------------------------- /images/FontAwesome/external-link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/images/FontAwesome/external-link.png -------------------------------------------------------------------------------- /images/FontAwesome/link.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/images/FontAwesome/link.png -------------------------------------------------------------------------------- /images/FontAwesome/share-square.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/images/FontAwesome/share-square.png -------------------------------------------------------------------------------- /images/FontAwesome/star.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/images/FontAwesome/star.png -------------------------------------------------------------------------------- /images/package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "images", 3 | "lockfileVersion": 3, 4 | "requires": true, 5 | "packages": { 6 | "": { 7 | "dependencies": { 8 | "svg2png": "^4.1.1" 9 | } 10 | }, 11 | "node_modules/ajv": { 12 | "version": "6.12.6", 13 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", 14 | "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", 15 | "dependencies": { 16 | "fast-deep-equal": "^3.1.1", 17 | "fast-json-stable-stringify": "^2.0.0", 18 | "json-schema-traverse": "^0.4.1", 19 | "uri-js": "^4.2.2" 20 | }, 21 | "funding": { 22 | "type": "github", 23 | "url": "https://github.com/sponsors/epoberezkin" 24 | } 25 | }, 26 | "node_modules/ansi-regex": { 27 | "version": "2.1.1", 28 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 29 | "integrity": "sha512-TIGnTpdo+E3+pCyAluZvtED5p5wCqLdezCyhPZzKPcxvFplEt4i+W7OONCKgeZFT3+y5NZZfOOS/Bdcanm1MYA==", 30 | "engines": { 31 | "node": ">=0.10.0" 32 | } 33 | }, 34 | "node_modules/asn1": { 35 | "version": "0.2.6", 36 | "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz", 37 | "integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==", 38 | "dependencies": { 39 | "safer-buffer": "~2.1.0" 40 | } 41 | }, 42 | "node_modules/assert-plus": { 43 | "version": "1.0.0", 44 | "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz", 45 | "integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw==", 46 | "engines": { 47 | "node": ">=0.8" 48 | } 49 | }, 50 | "node_modules/asynckit": { 51 | "version": "0.4.0", 52 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", 53 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" 54 | }, 55 | "node_modules/aws-sign2": { 56 | "version": "0.7.0", 57 | "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz", 58 | "integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA==", 59 | "engines": { 60 | "node": "*" 61 | } 62 | }, 63 | "node_modules/aws4": { 64 | "version": "1.12.0", 65 | "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.12.0.tgz", 66 | "integrity": "sha512-NmWvPnx0F1SfrQbYwOi7OeaNGokp9XhzNioJ/CSBs8Qa4vxug81mhJEAVZwxXuBmYB5KDRfMq/F3RR0BIU7sWg==" 67 | }, 68 | "node_modules/bcrypt-pbkdf": { 69 | "version": "1.0.2", 70 | "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz", 71 | "integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==", 72 | "dependencies": { 73 | "tweetnacl": "^0.14.3" 74 | } 75 | }, 76 | "node_modules/buffer-crc32": { 77 | "version": "0.2.13", 78 | "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", 79 | "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", 80 | "engines": { 81 | "node": "*" 82 | } 83 | }, 84 | "node_modules/buffer-from": { 85 | "version": "1.1.2", 86 | "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", 87 | "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==" 88 | }, 89 | "node_modules/camelcase": { 90 | "version": "3.0.0", 91 | "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-3.0.0.tgz", 92 | "integrity": "sha512-4nhGqUkc4BqbBBB4Q6zLuD7lzzrHYrjKGeYaEji/3tFR5VdJu9v+LilhGIVe8wxEJPPOeWo7eg8dwY13TZ1BNg==", 93 | "engines": { 94 | "node": ">=0.10.0" 95 | } 96 | }, 97 | "node_modules/caseless": { 98 | "version": "0.12.0", 99 | "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz", 100 | "integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw==" 101 | }, 102 | "node_modules/cliui": { 103 | "version": "3.2.0", 104 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-3.2.0.tgz", 105 | "integrity": "sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==", 106 | "dependencies": { 107 | "string-width": "^1.0.1", 108 | "strip-ansi": "^3.0.1", 109 | "wrap-ansi": "^2.0.0" 110 | } 111 | }, 112 | "node_modules/code-point-at": { 113 | "version": "1.1.0", 114 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 115 | "integrity": "sha512-RpAVKQA5T63xEj6/giIbUEtZwJ4UFIc3ZtvEkiaUERylqe8xb5IvqcgOurZLahv93CLKfxcw5YI+DZcUBRyLXA==", 116 | "engines": { 117 | "node": ">=0.10.0" 118 | } 119 | }, 120 | "node_modules/combined-stream": { 121 | "version": "1.0.8", 122 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", 123 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", 124 | "dependencies": { 125 | "delayed-stream": "~1.0.0" 126 | }, 127 | "engines": { 128 | "node": ">= 0.8" 129 | } 130 | }, 131 | "node_modules/concat-stream": { 132 | "version": "1.6.2", 133 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.2.tgz", 134 | "integrity": "sha512-27HBghJxjiZtIk3Ycvn/4kbJk/1uZuJFfuPEns6LaEvpvG1f0hTea8lilrouyo9mVc2GWdcEZ8OLoGmSADlrCw==", 135 | "engines": [ 136 | "node >= 0.8" 137 | ], 138 | "dependencies": { 139 | "buffer-from": "^1.0.0", 140 | "inherits": "^2.0.3", 141 | "readable-stream": "^2.2.2", 142 | "typedarray": "^0.0.6" 143 | } 144 | }, 145 | "node_modules/core-util-is": { 146 | "version": "1.0.3", 147 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", 148 | "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" 149 | }, 150 | "node_modules/dashdash": { 151 | "version": "1.14.1", 152 | "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz", 153 | "integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==", 154 | "dependencies": { 155 | "assert-plus": "^1.0.0" 156 | }, 157 | "engines": { 158 | "node": ">=0.10" 159 | } 160 | }, 161 | "node_modules/debug": { 162 | "version": "2.6.9", 163 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", 164 | "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", 165 | "dependencies": { 166 | "ms": "2.0.0" 167 | } 168 | }, 169 | "node_modules/decamelize": { 170 | "version": "1.2.0", 171 | "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", 172 | "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", 173 | "engines": { 174 | "node": ">=0.10.0" 175 | } 176 | }, 177 | "node_modules/delayed-stream": { 178 | "version": "1.0.0", 179 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", 180 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", 181 | "engines": { 182 | "node": ">=0.4.0" 183 | } 184 | }, 185 | "node_modules/ecc-jsbn": { 186 | "version": "0.1.2", 187 | "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz", 188 | "integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==", 189 | "dependencies": { 190 | "jsbn": "~0.1.0", 191 | "safer-buffer": "^2.1.0" 192 | } 193 | }, 194 | "node_modules/error-ex": { 195 | "version": "1.3.2", 196 | "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", 197 | "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", 198 | "dependencies": { 199 | "is-arrayish": "^0.2.1" 200 | } 201 | }, 202 | "node_modules/es6-promise": { 203 | "version": "4.2.8", 204 | "resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz", 205 | "integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w==" 206 | }, 207 | "node_modules/extend": { 208 | "version": "3.0.2", 209 | "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", 210 | "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==" 211 | }, 212 | "node_modules/extract-zip": { 213 | "version": "1.7.0", 214 | "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-1.7.0.tgz", 215 | "integrity": "sha512-xoh5G1W/PB0/27lXgMQyIhP5DSY/LhoCsOyZgb+6iMmRtCwVBo55uKaMoEYrDCKQhWvqEip5ZPKAc6eFNyf/MA==", 216 | "dependencies": { 217 | "concat-stream": "^1.6.2", 218 | "debug": "^2.6.9", 219 | "mkdirp": "^0.5.4", 220 | "yauzl": "^2.10.0" 221 | }, 222 | "bin": { 223 | "extract-zip": "cli.js" 224 | } 225 | }, 226 | "node_modules/extsprintf": { 227 | "version": "1.3.0", 228 | "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz", 229 | "integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g==", 230 | "engines": [ 231 | "node >=0.6.0" 232 | ] 233 | }, 234 | "node_modules/fast-deep-equal": { 235 | "version": "3.1.3", 236 | "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", 237 | "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" 238 | }, 239 | "node_modules/fast-json-stable-stringify": { 240 | "version": "2.1.0", 241 | "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", 242 | "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==" 243 | }, 244 | "node_modules/fd-slicer": { 245 | "version": "1.1.0", 246 | "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", 247 | "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", 248 | "dependencies": { 249 | "pend": "~1.2.0" 250 | } 251 | }, 252 | "node_modules/file-url": { 253 | "version": "2.0.2", 254 | "resolved": "https://registry.npmjs.org/file-url/-/file-url-2.0.2.tgz", 255 | "integrity": "sha512-x3989K8a1jM6vulMigE8VngH7C5nci0Ks5d9kVjUXmNF28gmiZUNujk5HjwaS8dAzN2QmUfX56riJKgN00dNRw==", 256 | "engines": { 257 | "node": ">=4" 258 | } 259 | }, 260 | "node_modules/find-up": { 261 | "version": "1.1.2", 262 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", 263 | "integrity": "sha512-jvElSjyuo4EMQGoTwo1uJU5pQMwTW5lS1x05zzfJuTIyLR3zwO27LYrxNg+dlvKpGOuGy/MzBdXh80g0ve5+HA==", 264 | "dependencies": { 265 | "path-exists": "^2.0.0", 266 | "pinkie-promise": "^2.0.0" 267 | }, 268 | "engines": { 269 | "node": ">=0.10.0" 270 | } 271 | }, 272 | "node_modules/forever-agent": { 273 | "version": "0.6.1", 274 | "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz", 275 | "integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw==", 276 | "engines": { 277 | "node": "*" 278 | } 279 | }, 280 | "node_modules/form-data": { 281 | "version": "2.3.3", 282 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz", 283 | "integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==", 284 | "dependencies": { 285 | "asynckit": "^0.4.0", 286 | "combined-stream": "^1.0.6", 287 | "mime-types": "^2.1.12" 288 | }, 289 | "engines": { 290 | "node": ">= 0.12" 291 | } 292 | }, 293 | "node_modules/fs-extra": { 294 | "version": "1.0.0", 295 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-1.0.0.tgz", 296 | "integrity": "sha512-VerQV6vEKuhDWD2HGOybV6v5I73syoc/cXAbKlgTC7M/oFVEtklWlp9QH2Ijw3IaWDOQcMkldSPa7zXy79Z/UQ==", 297 | "dependencies": { 298 | "graceful-fs": "^4.1.2", 299 | "jsonfile": "^2.1.0", 300 | "klaw": "^1.0.0" 301 | } 302 | }, 303 | "node_modules/function-bind": { 304 | "version": "1.1.2", 305 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", 306 | "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", 307 | "funding": { 308 | "url": "https://github.com/sponsors/ljharb" 309 | } 310 | }, 311 | "node_modules/get-caller-file": { 312 | "version": "1.0.3", 313 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-1.0.3.tgz", 314 | "integrity": "sha512-3t6rVToeoZfYSGd8YoLFR2DJkiQrIiUrGcjvFX2mDw3bn6k2OtwHN0TNCLbBO+w8qTvimhDkv+LSscbJY1vE6w==" 315 | }, 316 | "node_modules/getpass": { 317 | "version": "0.1.7", 318 | "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz", 319 | "integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==", 320 | "dependencies": { 321 | "assert-plus": "^1.0.0" 322 | } 323 | }, 324 | "node_modules/graceful-fs": { 325 | "version": "4.2.11", 326 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", 327 | "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==" 328 | }, 329 | "node_modules/har-schema": { 330 | "version": "2.0.0", 331 | "resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz", 332 | "integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q==", 333 | "engines": { 334 | "node": ">=4" 335 | } 336 | }, 337 | "node_modules/har-validator": { 338 | "version": "5.1.5", 339 | "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz", 340 | "integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==", 341 | "deprecated": "this library is no longer supported", 342 | "dependencies": { 343 | "ajv": "^6.12.3", 344 | "har-schema": "^2.0.0" 345 | }, 346 | "engines": { 347 | "node": ">=6" 348 | } 349 | }, 350 | "node_modules/hasha": { 351 | "version": "2.2.0", 352 | "resolved": "https://registry.npmjs.org/hasha/-/hasha-2.2.0.tgz", 353 | "integrity": "sha512-jZ38TU/EBiGKrmyTNNZgnvCZHNowiRI4+w/I9noMlekHTZH3KyGgvJLmhSgykeAQ9j2SYPDosM0Bg3wHfzibAQ==", 354 | "dependencies": { 355 | "is-stream": "^1.0.1", 356 | "pinkie-promise": "^2.0.0" 357 | }, 358 | "engines": { 359 | "node": ">=0.10.0" 360 | } 361 | }, 362 | "node_modules/hasown": { 363 | "version": "2.0.2", 364 | "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", 365 | "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", 366 | "dependencies": { 367 | "function-bind": "^1.1.2" 368 | }, 369 | "engines": { 370 | "node": ">= 0.4" 371 | } 372 | }, 373 | "node_modules/hosted-git-info": { 374 | "version": "2.8.9", 375 | "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", 376 | "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" 377 | }, 378 | "node_modules/http-signature": { 379 | "version": "1.2.0", 380 | "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz", 381 | "integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==", 382 | "dependencies": { 383 | "assert-plus": "^1.0.0", 384 | "jsprim": "^1.2.2", 385 | "sshpk": "^1.7.0" 386 | }, 387 | "engines": { 388 | "node": ">=0.8", 389 | "npm": ">=1.3.7" 390 | } 391 | }, 392 | "node_modules/inherits": { 393 | "version": "2.0.4", 394 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", 395 | "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" 396 | }, 397 | "node_modules/invert-kv": { 398 | "version": "1.0.0", 399 | "resolved": "https://registry.npmjs.org/invert-kv/-/invert-kv-1.0.0.tgz", 400 | "integrity": "sha512-xgs2NH9AE66ucSq4cNG1nhSFghr5l6tdL15Pk+jl46bmmBapgoaY/AacXyaDznAqmGL99TiLSQgO/XazFSKYeQ==", 401 | "engines": { 402 | "node": ">=0.10.0" 403 | } 404 | }, 405 | "node_modules/is-arrayish": { 406 | "version": "0.2.1", 407 | "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", 408 | "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==" 409 | }, 410 | "node_modules/is-core-module": { 411 | "version": "2.13.1", 412 | "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.1.tgz", 413 | "integrity": "sha512-hHrIjvZsftOsvKSn2TRYl63zvxsgE0K+0mYMoH6gD4omR5IWB2KynivBQczo3+wF1cCkjzvptnI9Q0sPU66ilw==", 414 | "dependencies": { 415 | "hasown": "^2.0.0" 416 | }, 417 | "funding": { 418 | "url": "https://github.com/sponsors/ljharb" 419 | } 420 | }, 421 | "node_modules/is-fullwidth-code-point": { 422 | "version": "1.0.0", 423 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 424 | "integrity": "sha512-1pqUqRjkhPJ9miNq9SwMfdvi6lBJcd6eFxvfaivQhaH3SgisfiuudvFntdKOmxuee/77l+FPjKrQjWvmPjWrRw==", 425 | "dependencies": { 426 | "number-is-nan": "^1.0.0" 427 | }, 428 | "engines": { 429 | "node": ">=0.10.0" 430 | } 431 | }, 432 | "node_modules/is-stream": { 433 | "version": "1.1.0", 434 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz", 435 | "integrity": "sha512-uQPm8kcs47jx38atAcWTVxyltQYoPT68y9aWYdV6yWXSyW8mzSat0TL6CiWdZeCdF3KrAvpVtnHbTv4RN+rqdQ==", 436 | "engines": { 437 | "node": ">=0.10.0" 438 | } 439 | }, 440 | "node_modules/is-typedarray": { 441 | "version": "1.0.0", 442 | "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", 443 | "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==" 444 | }, 445 | "node_modules/is-utf8": { 446 | "version": "0.2.1", 447 | "resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz", 448 | "integrity": "sha512-rMYPYvCzsXywIsldgLaSoPlw5PfoB/ssr7hY4pLfcodrA5M/eArza1a9VmTiNIBNMjOGr1Ow9mTyU2o69U6U9Q==" 449 | }, 450 | "node_modules/isarray": { 451 | "version": "1.0.0", 452 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 453 | "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" 454 | }, 455 | "node_modules/isexe": { 456 | "version": "2.0.0", 457 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 458 | "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" 459 | }, 460 | "node_modules/isstream": { 461 | "version": "0.1.2", 462 | "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz", 463 | "integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g==" 464 | }, 465 | "node_modules/jsbn": { 466 | "version": "0.1.1", 467 | "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz", 468 | "integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg==" 469 | }, 470 | "node_modules/json-schema": { 471 | "version": "0.4.0", 472 | "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz", 473 | "integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==" 474 | }, 475 | "node_modules/json-schema-traverse": { 476 | "version": "0.4.1", 477 | "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", 478 | "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==" 479 | }, 480 | "node_modules/json-stringify-safe": { 481 | "version": "5.0.1", 482 | "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz", 483 | "integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA==" 484 | }, 485 | "node_modules/jsonfile": { 486 | "version": "2.4.0", 487 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-2.4.0.tgz", 488 | "integrity": "sha512-PKllAqbgLgxHaj8TElYymKCAgrASebJrWpTnEkOaTowt23VKXXN0sUeriJ+eh7y6ufb/CC5ap11pz71/cM0hUw==", 489 | "optionalDependencies": { 490 | "graceful-fs": "^4.1.6" 491 | } 492 | }, 493 | "node_modules/jsprim": { 494 | "version": "1.4.2", 495 | "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz", 496 | "integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==", 497 | "dependencies": { 498 | "assert-plus": "1.0.0", 499 | "extsprintf": "1.3.0", 500 | "json-schema": "0.4.0", 501 | "verror": "1.10.0" 502 | }, 503 | "engines": { 504 | "node": ">=0.6.0" 505 | } 506 | }, 507 | "node_modules/kew": { 508 | "version": "0.7.0", 509 | "resolved": "https://registry.npmjs.org/kew/-/kew-0.7.0.tgz", 510 | "integrity": "sha512-IG6nm0+QtAMdXt9KvbgbGdvY50RSrw+U4sGZg+KlrSKPJEwVE5JVoI3d7RWfSMdBQneRheeAOj3lIjX5VL/9RQ==" 511 | }, 512 | "node_modules/klaw": { 513 | "version": "1.3.1", 514 | "resolved": "https://registry.npmjs.org/klaw/-/klaw-1.3.1.tgz", 515 | "integrity": "sha512-TED5xi9gGQjGpNnvRWknrwAB1eL5GciPfVFOt3Vk1OJCVDQbzuSfrF3hkUQKlsgKrG1F+0t5W0m+Fje1jIt8rw==", 516 | "optionalDependencies": { 517 | "graceful-fs": "^4.1.9" 518 | } 519 | }, 520 | "node_modules/lcid": { 521 | "version": "1.0.0", 522 | "resolved": "https://registry.npmjs.org/lcid/-/lcid-1.0.0.tgz", 523 | "integrity": "sha512-YiGkH6EnGrDGqLMITnGjXtGmNtjoXw9SVUzcaos8RBi7Ps0VBylkq+vOcY9QE5poLasPCR849ucFUkl0UzUyOw==", 524 | "dependencies": { 525 | "invert-kv": "^1.0.0" 526 | }, 527 | "engines": { 528 | "node": ">=0.10.0" 529 | } 530 | }, 531 | "node_modules/load-json-file": { 532 | "version": "1.1.0", 533 | "resolved": "https://registry.npmjs.org/load-json-file/-/load-json-file-1.1.0.tgz", 534 | "integrity": "sha512-cy7ZdNRXdablkXYNI049pthVeXFurRyb9+hA/dZzerZ0pGTx42z+y+ssxBaVV2l70t1muq5IdKhn4UtcoGUY9A==", 535 | "dependencies": { 536 | "graceful-fs": "^4.1.2", 537 | "parse-json": "^2.2.0", 538 | "pify": "^2.0.0", 539 | "pinkie-promise": "^2.0.0", 540 | "strip-bom": "^2.0.0" 541 | }, 542 | "engines": { 543 | "node": ">=0.10.0" 544 | } 545 | }, 546 | "node_modules/mime-db": { 547 | "version": "1.52.0", 548 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", 549 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", 550 | "engines": { 551 | "node": ">= 0.6" 552 | } 553 | }, 554 | "node_modules/mime-types": { 555 | "version": "2.1.35", 556 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", 557 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", 558 | "dependencies": { 559 | "mime-db": "1.52.0" 560 | }, 561 | "engines": { 562 | "node": ">= 0.6" 563 | } 564 | }, 565 | "node_modules/minimist": { 566 | "version": "1.2.8", 567 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", 568 | "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", 569 | "funding": { 570 | "url": "https://github.com/sponsors/ljharb" 571 | } 572 | }, 573 | "node_modules/mkdirp": { 574 | "version": "0.5.6", 575 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", 576 | "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", 577 | "dependencies": { 578 | "minimist": "^1.2.6" 579 | }, 580 | "bin": { 581 | "mkdirp": "bin/cmd.js" 582 | } 583 | }, 584 | "node_modules/ms": { 585 | "version": "2.0.0", 586 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 587 | "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" 588 | }, 589 | "node_modules/normalize-package-data": { 590 | "version": "2.5.0", 591 | "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", 592 | "integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==", 593 | "dependencies": { 594 | "hosted-git-info": "^2.1.4", 595 | "resolve": "^1.10.0", 596 | "semver": "2 || 3 || 4 || 5", 597 | "validate-npm-package-license": "^3.0.1" 598 | } 599 | }, 600 | "node_modules/number-is-nan": { 601 | "version": "1.0.1", 602 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 603 | "integrity": "sha512-4jbtZXNAsfZbAHiiqjLPBiCl16dES1zI4Hpzzxw61Tk+loF+sBDBKx1ICKKKwIqQ7M0mFn1TmkN7euSncWgHiQ==", 604 | "engines": { 605 | "node": ">=0.10.0" 606 | } 607 | }, 608 | "node_modules/oauth-sign": { 609 | "version": "0.9.0", 610 | "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz", 611 | "integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ==", 612 | "engines": { 613 | "node": "*" 614 | } 615 | }, 616 | "node_modules/os-locale": { 617 | "version": "1.4.0", 618 | "resolved": "https://registry.npmjs.org/os-locale/-/os-locale-1.4.0.tgz", 619 | "integrity": "sha512-PRT7ZORmwu2MEFt4/fv3Q+mEfN4zetKxufQrkShY2oGvUms9r8otu5HfdyIFHkYXjO7laNsoVGmM2MANfuTA8g==", 620 | "dependencies": { 621 | "lcid": "^1.0.0" 622 | }, 623 | "engines": { 624 | "node": ">=0.10.0" 625 | } 626 | }, 627 | "node_modules/parse-json": { 628 | "version": "2.2.0", 629 | "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-2.2.0.tgz", 630 | "integrity": "sha512-QR/GGaKCkhwk1ePQNYDRKYZ3mwU9ypsKhB0XyFnLQdomyEqk3e8wpW3V5Jp88zbxK4n5ST1nqo+g9juTpownhQ==", 631 | "dependencies": { 632 | "error-ex": "^1.2.0" 633 | }, 634 | "engines": { 635 | "node": ">=0.10.0" 636 | } 637 | }, 638 | "node_modules/path-exists": { 639 | "version": "2.1.0", 640 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", 641 | "integrity": "sha512-yTltuKuhtNeFJKa1PiRzfLAU5182q1y4Eb4XCJ3PBqyzEDkAZRzBrKKBct682ls9reBVHf9udYLN5Nd+K1B9BQ==", 642 | "dependencies": { 643 | "pinkie-promise": "^2.0.0" 644 | }, 645 | "engines": { 646 | "node": ">=0.10.0" 647 | } 648 | }, 649 | "node_modules/path-parse": { 650 | "version": "1.0.7", 651 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", 652 | "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==" 653 | }, 654 | "node_modules/path-type": { 655 | "version": "1.1.0", 656 | "resolved": "https://registry.npmjs.org/path-type/-/path-type-1.1.0.tgz", 657 | "integrity": "sha512-S4eENJz1pkiQn9Znv33Q+deTOKmbl+jj1Fl+qiP/vYezj+S8x+J3Uo0ISrx/QoEvIlOaDWJhPaRd1flJ9HXZqg==", 658 | "dependencies": { 659 | "graceful-fs": "^4.1.2", 660 | "pify": "^2.0.0", 661 | "pinkie-promise": "^2.0.0" 662 | }, 663 | "engines": { 664 | "node": ">=0.10.0" 665 | } 666 | }, 667 | "node_modules/pend": { 668 | "version": "1.2.0", 669 | "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", 670 | "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==" 671 | }, 672 | "node_modules/performance-now": { 673 | "version": "2.1.0", 674 | "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", 675 | "integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow==" 676 | }, 677 | "node_modules/phantomjs-prebuilt": { 678 | "version": "2.1.16", 679 | "resolved": "https://registry.npmjs.org/phantomjs-prebuilt/-/phantomjs-prebuilt-2.1.16.tgz", 680 | "integrity": "sha512-PIiRzBhW85xco2fuj41FmsyuYHKjKuXWmhjy3A/Y+CMpN/63TV+s9uzfVhsUwFe0G77xWtHBG8xmXf5BqEUEuQ==", 681 | "deprecated": "this package is now deprecated", 682 | "hasInstallScript": true, 683 | "dependencies": { 684 | "es6-promise": "^4.0.3", 685 | "extract-zip": "^1.6.5", 686 | "fs-extra": "^1.0.0", 687 | "hasha": "^2.2.0", 688 | "kew": "^0.7.0", 689 | "progress": "^1.1.8", 690 | "request": "^2.81.0", 691 | "request-progress": "^2.0.1", 692 | "which": "^1.2.10" 693 | }, 694 | "bin": { 695 | "phantomjs": "bin/phantomjs" 696 | } 697 | }, 698 | "node_modules/pify": { 699 | "version": "2.3.0", 700 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 701 | "integrity": "sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==", 702 | "engines": { 703 | "node": ">=0.10.0" 704 | } 705 | }, 706 | "node_modules/pinkie": { 707 | "version": "2.0.4", 708 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 709 | "integrity": "sha512-MnUuEycAemtSaeFSjXKW/aroV7akBbY+Sv+RkyqFjgAe73F+MR0TBWKBRDkmfWq/HiFmdavfZ1G7h4SPZXaCSg==", 710 | "engines": { 711 | "node": ">=0.10.0" 712 | } 713 | }, 714 | "node_modules/pinkie-promise": { 715 | "version": "2.0.1", 716 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 717 | "integrity": "sha512-0Gni6D4UcLTbv9c57DfxDGdr41XfgUjqWZu492f0cIGr16zDU06BWP/RAEvOuo7CQ0CNjHaLlM59YJJFm3NWlw==", 718 | "dependencies": { 719 | "pinkie": "^2.0.0" 720 | }, 721 | "engines": { 722 | "node": ">=0.10.0" 723 | } 724 | }, 725 | "node_modules/pn": { 726 | "version": "1.1.0", 727 | "resolved": "https://registry.npmjs.org/pn/-/pn-1.1.0.tgz", 728 | "integrity": "sha512-2qHaIQr2VLRFoxe2nASzsV6ef4yOOH+Fi9FBOVH6cqeSgUnoyySPZkxzLuzd+RYOQTRpROA0ztTMqxROKSb/nA==" 729 | }, 730 | "node_modules/process-nextick-args": { 731 | "version": "2.0.1", 732 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", 733 | "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" 734 | }, 735 | "node_modules/progress": { 736 | "version": "1.1.8", 737 | "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", 738 | "integrity": "sha512-UdA8mJ4weIkUBO224tIarHzuHs4HuYiJvsuGT7j/SPQiUJVjYvNDBIPa0hAorduOfjGohB/qHWRa/lrrWX/mXw==", 739 | "engines": { 740 | "node": ">=0.4.0" 741 | } 742 | }, 743 | "node_modules/psl": { 744 | "version": "1.9.0", 745 | "resolved": "https://registry.npmjs.org/psl/-/psl-1.9.0.tgz", 746 | "integrity": "sha512-E/ZsdU4HLs/68gYzgGTkMicWTLPdAftJLfJFlLUAAKZGkStNU72sZjT66SnMDVOfOWY/YAoiD7Jxa9iHvngcag==" 747 | }, 748 | "node_modules/punycode": { 749 | "version": "2.3.1", 750 | "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", 751 | "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", 752 | "engines": { 753 | "node": ">=6" 754 | } 755 | }, 756 | "node_modules/qs": { 757 | "version": "6.5.3", 758 | "resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz", 759 | "integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA==", 760 | "engines": { 761 | "node": ">=0.6" 762 | } 763 | }, 764 | "node_modules/read-pkg": { 765 | "version": "1.1.0", 766 | "resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-1.1.0.tgz", 767 | "integrity": "sha512-7BGwRHqt4s/uVbuyoeejRn4YmFnYZiFl4AuaeXHlgZf3sONF0SOGlxs2Pw8g6hCKupo08RafIO5YXFNOKTfwsQ==", 768 | "dependencies": { 769 | "load-json-file": "^1.0.0", 770 | "normalize-package-data": "^2.3.2", 771 | "path-type": "^1.0.0" 772 | }, 773 | "engines": { 774 | "node": ">=0.10.0" 775 | } 776 | }, 777 | "node_modules/read-pkg-up": { 778 | "version": "1.0.1", 779 | "resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-1.0.1.tgz", 780 | "integrity": "sha512-WD9MTlNtI55IwYUS27iHh9tK3YoIVhxis8yKhLpTqWtml739uXc9NWTpxoHkfZf3+DkCCsXox94/VWZniuZm6A==", 781 | "dependencies": { 782 | "find-up": "^1.0.0", 783 | "read-pkg": "^1.0.0" 784 | }, 785 | "engines": { 786 | "node": ">=0.10.0" 787 | } 788 | }, 789 | "node_modules/readable-stream": { 790 | "version": "2.3.8", 791 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", 792 | "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", 793 | "dependencies": { 794 | "core-util-is": "~1.0.0", 795 | "inherits": "~2.0.3", 796 | "isarray": "~1.0.0", 797 | "process-nextick-args": "~2.0.0", 798 | "safe-buffer": "~5.1.1", 799 | "string_decoder": "~1.1.1", 800 | "util-deprecate": "~1.0.1" 801 | } 802 | }, 803 | "node_modules/request": { 804 | "version": "2.88.2", 805 | "resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz", 806 | "integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==", 807 | "deprecated": "request has been deprecated, see https://github.com/request/request/issues/3142", 808 | "dependencies": { 809 | "aws-sign2": "~0.7.0", 810 | "aws4": "^1.8.0", 811 | "caseless": "~0.12.0", 812 | "combined-stream": "~1.0.6", 813 | "extend": "~3.0.2", 814 | "forever-agent": "~0.6.1", 815 | "form-data": "~2.3.2", 816 | "har-validator": "~5.1.3", 817 | "http-signature": "~1.2.0", 818 | "is-typedarray": "~1.0.0", 819 | "isstream": "~0.1.2", 820 | "json-stringify-safe": "~5.0.1", 821 | "mime-types": "~2.1.19", 822 | "oauth-sign": "~0.9.0", 823 | "performance-now": "^2.1.0", 824 | "qs": "~6.5.2", 825 | "safe-buffer": "^5.1.2", 826 | "tough-cookie": "~2.5.0", 827 | "tunnel-agent": "^0.6.0", 828 | "uuid": "^3.3.2" 829 | }, 830 | "engines": { 831 | "node": ">= 6" 832 | } 833 | }, 834 | "node_modules/request-progress": { 835 | "version": "2.0.1", 836 | "resolved": "https://registry.npmjs.org/request-progress/-/request-progress-2.0.1.tgz", 837 | "integrity": "sha512-dxdraeZVUNEn9AvLrxkgB2k6buTlym71dJk1fk4v8j3Ou3RKNm07BcgbHdj2lLgYGfqX71F+awb1MR+tWPFJzA==", 838 | "dependencies": { 839 | "throttleit": "^1.0.0" 840 | } 841 | }, 842 | "node_modules/require-directory": { 843 | "version": "2.1.1", 844 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", 845 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", 846 | "engines": { 847 | "node": ">=0.10.0" 848 | } 849 | }, 850 | "node_modules/require-main-filename": { 851 | "version": "1.0.1", 852 | "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-1.0.1.tgz", 853 | "integrity": "sha512-IqSUtOVP4ksd1C/ej5zeEh/BIP2ajqpn8c5x+q99gvcIG/Qf0cud5raVnE/Dwd0ua9TXYDoDc0RE5hBSdz22Ug==" 854 | }, 855 | "node_modules/resolve": { 856 | "version": "1.22.8", 857 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", 858 | "integrity": "sha512-oKWePCxqpd6FlLvGV1VU0x7bkPmmCNolxzjMf4NczoDnQcIWrAF+cPtZn5i6n+RfD2d9i0tzpKnG6Yk168yIyw==", 859 | "dependencies": { 860 | "is-core-module": "^2.13.0", 861 | "path-parse": "^1.0.7", 862 | "supports-preserve-symlinks-flag": "^1.0.0" 863 | }, 864 | "bin": { 865 | "resolve": "bin/resolve" 866 | }, 867 | "funding": { 868 | "url": "https://github.com/sponsors/ljharb" 869 | } 870 | }, 871 | "node_modules/safe-buffer": { 872 | "version": "5.1.2", 873 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", 874 | "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" 875 | }, 876 | "node_modules/safer-buffer": { 877 | "version": "2.1.2", 878 | "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", 879 | "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" 880 | }, 881 | "node_modules/semver": { 882 | "version": "5.7.2", 883 | "resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz", 884 | "integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==", 885 | "bin": { 886 | "semver": "bin/semver" 887 | } 888 | }, 889 | "node_modules/set-blocking": { 890 | "version": "2.0.0", 891 | "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", 892 | "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==" 893 | }, 894 | "node_modules/spdx-correct": { 895 | "version": "3.2.0", 896 | "resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz", 897 | "integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==", 898 | "dependencies": { 899 | "spdx-expression-parse": "^3.0.0", 900 | "spdx-license-ids": "^3.0.0" 901 | } 902 | }, 903 | "node_modules/spdx-exceptions": { 904 | "version": "2.5.0", 905 | "resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.5.0.tgz", 906 | "integrity": "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w==" 907 | }, 908 | "node_modules/spdx-expression-parse": { 909 | "version": "3.0.1", 910 | "resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz", 911 | "integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==", 912 | "dependencies": { 913 | "spdx-exceptions": "^2.1.0", 914 | "spdx-license-ids": "^3.0.0" 915 | } 916 | }, 917 | "node_modules/spdx-license-ids": { 918 | "version": "3.0.17", 919 | "resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.17.tgz", 920 | "integrity": "sha512-sh8PWc/ftMqAAdFiBu6Fy6JUOYjqDJBJvIhpfDMyHrr0Rbp5liZqd4TjtQ/RgfLjKFZb+LMx5hpml5qOWy0qvg==" 921 | }, 922 | "node_modules/sshpk": { 923 | "version": "1.18.0", 924 | "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.18.0.tgz", 925 | "integrity": "sha512-2p2KJZTSqQ/I3+HX42EpYOa2l3f8Erv8MWKsy2I9uf4wA7yFIkXRffYdsx86y6z4vHtV8u7g+pPlr8/4ouAxsQ==", 926 | "dependencies": { 927 | "asn1": "~0.2.3", 928 | "assert-plus": "^1.0.0", 929 | "bcrypt-pbkdf": "^1.0.0", 930 | "dashdash": "^1.12.0", 931 | "ecc-jsbn": "~0.1.1", 932 | "getpass": "^0.1.1", 933 | "jsbn": "~0.1.0", 934 | "safer-buffer": "^2.0.2", 935 | "tweetnacl": "~0.14.0" 936 | }, 937 | "bin": { 938 | "sshpk-conv": "bin/sshpk-conv", 939 | "sshpk-sign": "bin/sshpk-sign", 940 | "sshpk-verify": "bin/sshpk-verify" 941 | }, 942 | "engines": { 943 | "node": ">=0.10.0" 944 | } 945 | }, 946 | "node_modules/string_decoder": { 947 | "version": "1.1.1", 948 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", 949 | "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", 950 | "dependencies": { 951 | "safe-buffer": "~5.1.0" 952 | } 953 | }, 954 | "node_modules/string-width": { 955 | "version": "1.0.2", 956 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 957 | "integrity": "sha512-0XsVpQLnVCXHJfyEs8tC0zpTVIr5PKKsQtkT29IwupnPTjtPmQ3xT/4yCREF9hYkV/3M3kzcUTSAZT6a6h81tw==", 958 | "dependencies": { 959 | "code-point-at": "^1.0.0", 960 | "is-fullwidth-code-point": "^1.0.0", 961 | "strip-ansi": "^3.0.0" 962 | }, 963 | "engines": { 964 | "node": ">=0.10.0" 965 | } 966 | }, 967 | "node_modules/strip-ansi": { 968 | "version": "3.0.1", 969 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 970 | "integrity": "sha512-VhumSSbBqDTP8p2ZLKj40UjBCV4+v8bUSEpUb4KjRgWk9pbqGF4REFj6KEagidb2f/M6AzC0EmFyDNGaw9OCzg==", 971 | "dependencies": { 972 | "ansi-regex": "^2.0.0" 973 | }, 974 | "engines": { 975 | "node": ">=0.10.0" 976 | } 977 | }, 978 | "node_modules/strip-bom": { 979 | "version": "2.0.0", 980 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-2.0.0.tgz", 981 | "integrity": "sha512-kwrX1y7czp1E69n2ajbG65mIo9dqvJ+8aBQXOGVxqwvNbsXdFM6Lq37dLAY3mknUwru8CfcCbfOLL/gMo+fi3g==", 982 | "dependencies": { 983 | "is-utf8": "^0.2.0" 984 | }, 985 | "engines": { 986 | "node": ">=0.10.0" 987 | } 988 | }, 989 | "node_modules/supports-preserve-symlinks-flag": { 990 | "version": "1.0.0", 991 | "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", 992 | "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", 993 | "engines": { 994 | "node": ">= 0.4" 995 | }, 996 | "funding": { 997 | "url": "https://github.com/sponsors/ljharb" 998 | } 999 | }, 1000 | "node_modules/svg2png": { 1001 | "version": "4.1.1", 1002 | "resolved": "https://registry.npmjs.org/svg2png/-/svg2png-4.1.1.tgz", 1003 | "integrity": "sha512-9tOp9Ugjlunuf1ugqkhiYboTmTaTI7p48dz5ZjNA5NQJ5xS1NLTZZ1tF8vkJOIBb/ZwxGJsKZvRWqVpo4q9z9Q==", 1004 | "dependencies": { 1005 | "file-url": "^2.0.0", 1006 | "phantomjs-prebuilt": "^2.1.14", 1007 | "pn": "^1.0.0", 1008 | "yargs": "^6.5.0" 1009 | }, 1010 | "bin": { 1011 | "svg2png": "bin/svg2png-cli.js" 1012 | } 1013 | }, 1014 | "node_modules/throttleit": { 1015 | "version": "1.0.1", 1016 | "resolved": "https://registry.npmjs.org/throttleit/-/throttleit-1.0.1.tgz", 1017 | "integrity": "sha512-vDZpf9Chs9mAdfY046mcPt8fg5QSZr37hEH4TXYBnDF+izxgrbRGUAAaBvIk/fJm9aOFCGFd1EsNg5AZCbnQCQ==", 1018 | "funding": { 1019 | "url": "https://github.com/sponsors/sindresorhus" 1020 | } 1021 | }, 1022 | "node_modules/tough-cookie": { 1023 | "version": "2.5.0", 1024 | "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", 1025 | "integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==", 1026 | "dependencies": { 1027 | "psl": "^1.1.28", 1028 | "punycode": "^2.1.1" 1029 | }, 1030 | "engines": { 1031 | "node": ">=0.8" 1032 | } 1033 | }, 1034 | "node_modules/tunnel-agent": { 1035 | "version": "0.6.0", 1036 | "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", 1037 | "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", 1038 | "dependencies": { 1039 | "safe-buffer": "^5.0.1" 1040 | }, 1041 | "engines": { 1042 | "node": "*" 1043 | } 1044 | }, 1045 | "node_modules/tweetnacl": { 1046 | "version": "0.14.5", 1047 | "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz", 1048 | "integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==" 1049 | }, 1050 | "node_modules/typedarray": { 1051 | "version": "0.0.6", 1052 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1053 | "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==" 1054 | }, 1055 | "node_modules/uri-js": { 1056 | "version": "4.4.1", 1057 | "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", 1058 | "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", 1059 | "dependencies": { 1060 | "punycode": "^2.1.0" 1061 | } 1062 | }, 1063 | "node_modules/util-deprecate": { 1064 | "version": "1.0.2", 1065 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1066 | "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" 1067 | }, 1068 | "node_modules/uuid": { 1069 | "version": "3.4.0", 1070 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", 1071 | "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==", 1072 | "deprecated": "Please upgrade to version 7 or higher. Older versions may use Math.random() in certain circumstances, which is known to be problematic. See https://v8.dev/blog/math-random for details.", 1073 | "bin": { 1074 | "uuid": "bin/uuid" 1075 | } 1076 | }, 1077 | "node_modules/validate-npm-package-license": { 1078 | "version": "3.0.4", 1079 | "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", 1080 | "integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==", 1081 | "dependencies": { 1082 | "spdx-correct": "^3.0.0", 1083 | "spdx-expression-parse": "^3.0.0" 1084 | } 1085 | }, 1086 | "node_modules/verror": { 1087 | "version": "1.10.0", 1088 | "resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz", 1089 | "integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==", 1090 | "engines": [ 1091 | "node >=0.6.0" 1092 | ], 1093 | "dependencies": { 1094 | "assert-plus": "^1.0.0", 1095 | "core-util-is": "1.0.2", 1096 | "extsprintf": "^1.2.0" 1097 | } 1098 | }, 1099 | "node_modules/verror/node_modules/core-util-is": { 1100 | "version": "1.0.2", 1101 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 1102 | "integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ==" 1103 | }, 1104 | "node_modules/which": { 1105 | "version": "1.3.1", 1106 | "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", 1107 | "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", 1108 | "dependencies": { 1109 | "isexe": "^2.0.0" 1110 | }, 1111 | "bin": { 1112 | "which": "bin/which" 1113 | } 1114 | }, 1115 | "node_modules/which-module": { 1116 | "version": "1.0.0", 1117 | "resolved": "https://registry.npmjs.org/which-module/-/which-module-1.0.0.tgz", 1118 | "integrity": "sha512-F6+WgncZi/mJDrammbTuHe1q0R5hOXv/mBaiNA2TCNT/LTHusX0V+CJnj9XT8ki5ln2UZyyddDgHfCzyrOH7MQ==" 1119 | }, 1120 | "node_modules/wrap-ansi": { 1121 | "version": "2.1.0", 1122 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-2.1.0.tgz", 1123 | "integrity": "sha512-vAaEaDM946gbNpH5pLVNR+vX2ht6n0Bt3GXwVB1AuAqZosOvHNF3P7wDnh8KLkSqgUh0uh77le7Owgoz+Z9XBw==", 1124 | "dependencies": { 1125 | "string-width": "^1.0.1", 1126 | "strip-ansi": "^3.0.1" 1127 | }, 1128 | "engines": { 1129 | "node": ">=0.10.0" 1130 | } 1131 | }, 1132 | "node_modules/y18n": { 1133 | "version": "3.2.2", 1134 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-3.2.2.tgz", 1135 | "integrity": "sha512-uGZHXkHnhF0XeeAPgnKfPv1bgKAYyVvmNL1xlKsPYZPaIHxGti2hHqvOCQv71XMsLxu1QjergkqogUnms5D3YQ==" 1136 | }, 1137 | "node_modules/yargs": { 1138 | "version": "6.6.0", 1139 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-6.6.0.tgz", 1140 | "integrity": "sha512-6/QWTdisjnu5UHUzQGst+UOEuEVwIzFVGBjq3jMTFNs5WJQsH/X6nMURSaScIdF5txylr1Ao9bvbWiKi2yXbwA==", 1141 | "dependencies": { 1142 | "camelcase": "^3.0.0", 1143 | "cliui": "^3.2.0", 1144 | "decamelize": "^1.1.1", 1145 | "get-caller-file": "^1.0.1", 1146 | "os-locale": "^1.4.0", 1147 | "read-pkg-up": "^1.0.1", 1148 | "require-directory": "^2.1.1", 1149 | "require-main-filename": "^1.0.1", 1150 | "set-blocking": "^2.0.0", 1151 | "string-width": "^1.0.2", 1152 | "which-module": "^1.0.0", 1153 | "y18n": "^3.2.1", 1154 | "yargs-parser": "^4.2.0" 1155 | } 1156 | }, 1157 | "node_modules/yargs-parser": { 1158 | "version": "4.2.1", 1159 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-4.2.1.tgz", 1160 | "integrity": "sha512-+QQWqC2xeL0N5/TE+TY6OGEqyNRM+g2/r712PDNYgiCdXYCApXf1vzfmDSLBxfGRwV+moTq/V8FnMI24JCm2Yg==", 1161 | "dependencies": { 1162 | "camelcase": "^3.0.0" 1163 | } 1164 | }, 1165 | "node_modules/yauzl": { 1166 | "version": "2.10.0", 1167 | "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", 1168 | "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", 1169 | "dependencies": { 1170 | "buffer-crc32": "~0.2.3", 1171 | "fd-slicer": "~1.1.0" 1172 | } 1173 | } 1174 | } 1175 | } 1176 | -------------------------------------------------------------------------------- /images/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "svg2png": "^4.1.1" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /images/self-made/go.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/images/self-made/go.png -------------------------------------------------------------------------------- /images/self-made/open.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/images/self-made/open.png -------------------------------------------------------------------------------- /menus/Context.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "-", 4 | "id": "clipboard" 5 | }, 6 | { 7 | "command": "copy_uri_from_context_menu" 8 | }, 9 | { 10 | "caption": "Copy All URIs", 11 | "command": "copy_uri_from_view" 12 | }, 13 | { 14 | "caption": "-", 15 | "id": "selection" 16 | }, 17 | { 18 | "caption": "Select All URIs", 19 | "command": "select_uri_from_view" 20 | } 21 | ] 22 | -------------------------------------------------------------------------------- /menus/Default.sublime-commands: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "caption": "OpenUri: Open URIs from Cursors", 4 | "command": "open_uri_from_cursors" 5 | }, 6 | { 7 | "caption": "OpenUri: Open URIs from the Current View", 8 | "command": "open_uri_from_view" 9 | }, 10 | { 11 | "caption": "OpenUri: Copy URIs from Cursors", 12 | "command": "copy_uri_from_cursors" 13 | }, 14 | { 15 | "caption": "OpenUri: Copy URIs from the Current View", 16 | "command": "copy_uri_from_view" 17 | }, 18 | { 19 | "caption": "OpenUri: Select URIs from Cursors", 20 | "command": "select_uri_from_cursors" 21 | }, 22 | { 23 | "caption": "OpenUri: Select URIs from the Current View", 24 | "command": "select_uri_from_view" 25 | } 26 | ] 27 | -------------------------------------------------------------------------------- /menus/Main.sublime-menu: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "id": "selection", 4 | "children": [ 5 | { 6 | "caption": "OpenUri", 7 | "children": [ 8 | { 9 | "caption": "-" 10 | }, 11 | { 12 | "caption": "Open URIs from Cursors", 13 | "command": "open_uri_from_cursors" 14 | }, 15 | { 16 | "caption": "Open URIs from the Current View", 17 | "command": "open_uri_from_view" 18 | }, 19 | { 20 | "caption": "-" 21 | }, 22 | { 23 | "caption": "Copy URIs from Cursors", 24 | "command": "copy_uri_from_cursors" 25 | }, 26 | { 27 | "caption": "Copy URIs from the Current View", 28 | "command": "copy_uri_from_view" 29 | }, 30 | { 31 | "caption": "-" 32 | }, 33 | { 34 | "caption": "Select URIs from Cursors", 35 | "command": "select_uri_from_cursors" 36 | }, 37 | { 38 | "caption": "Select URIs from the Current View", 39 | "command": "select_uri_from_view" 40 | }, 41 | { 42 | "caption": "-" 43 | } 44 | ] 45 | } 46 | ] 47 | }, 48 | { 49 | "id": "preferences", 50 | "children": [ 51 | { 52 | "id": "package-settings", 53 | "children": [ 54 | { 55 | "caption": "OpenUri", 56 | "children": [ 57 | { 58 | "caption": "-" 59 | }, 60 | { 61 | "caption": "Settings", 62 | "command": "edit_settings", 63 | "args": { 64 | "base_file": "${packages}/OpenUri/OpenUri.sublime-settings", 65 | "default": "{\n\t$0\n}\n" 66 | } 67 | }, 68 | { 69 | "caption": "-" 70 | }, 71 | { 72 | "caption": "README", 73 | "command": "open_file", 74 | "args": { 75 | "file": "${packages}/OpenUri/README.md" 76 | } 77 | }, 78 | { 79 | "caption": "CHANGELOG", 80 | "command": "open_file", 81 | "args": { 82 | "file": "${packages}/OpenUri/CHANGELOG.md" 83 | } 84 | }, 85 | { 86 | "caption": "-" 87 | } 88 | ] 89 | } 90 | ] 91 | } 92 | ] 93 | } 94 | ] 95 | -------------------------------------------------------------------------------- /messages.json: -------------------------------------------------------------------------------- 1 | { 2 | "7.0.1": "messages/update_message.md", 3 | "install": "README.md" 4 | } 5 | -------------------------------------------------------------------------------- /messages/update_message.md: -------------------------------------------------------------------------------- 1 | OpenUri has been updated. To see the changelog, visit 2 | Preferences » Package Settings » OpenUri » CHANGELOG 3 | 4 | ## [7.0.1] - 2021-07-24 5 | 6 | - feat: check all foreground views for updating 7 | 8 | Previously, OpenUri only checks the current activated view. 9 | Since OpenUri also checks whether the view is dirty before perform a update, 10 | I think this shouldn't be resource consuming. 11 | -------------------------------------------------------------------------------- /plugin/__init__.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | # import all listeners and commands 4 | from .commands.copy_uri import CopyUriFromContextMenuCommand, CopyUriFromCursorsCommand, CopyUriFromViewCommand 5 | from .commands.open_uri import OpenUriFromCursorsCommand, OpenUriFromViewCommand 6 | from .commands.select_uri import SelectUriFromCursorsCommand, SelectUriFromViewCommand 7 | from .constants import PLUGIN_NAME 8 | from .helpers import compile_uri_regex 9 | from .listener import OpenUriViewEventListener 10 | from .logger import apply_user_log_level, init_plugin_logger, log 11 | from .renderer import RendererThread 12 | from .settings import get_image_info, get_setting_renderer_interval, get_settings_object 13 | from .shared import global_get, global_set 14 | from .ui.phatom_sets_manager import PhatomSetsManager 15 | from .utils import is_processable_view, list_all_views, view_is_dirty_val 16 | 17 | __all__ = ( 18 | # ST: core 19 | "plugin_loaded", 20 | "plugin_unloaded", 21 | # ST: commands 22 | "CopyUriFromContextMenuCommand", 23 | "CopyUriFromCursorsCommand", 24 | "CopyUriFromViewCommand", 25 | "OpenUriFromCursorsCommand", 26 | "OpenUriFromViewCommand", 27 | "SelectUriFromCursorsCommand", 28 | "SelectUriFromViewCommand", 29 | # ST: listeners 30 | "OpenUriViewEventListener", 31 | ) 32 | 33 | 34 | def plugin_loaded() -> None: 35 | global_set("settings", get_settings_object()) 36 | global_set("logger", init_plugin_logger()) 37 | global_set("renderer_thread", RendererThread()) 38 | _settings_changed_callback() 39 | 40 | global_get("settings").add_on_change(PLUGIN_NAME, _settings_changed_callback) 41 | global_get("renderer_thread").start() 42 | 43 | 44 | def plugin_unloaded() -> None: 45 | global_get("settings").clear_on_change(PLUGIN_NAME) 46 | global_get("renderer_thread").cancel() 47 | PhatomSetsManager.clear() 48 | 49 | 50 | def _settings_changed_callback() -> None: 51 | apply_user_log_level(global_get("logger")) 52 | global_get("renderer_thread").set_interval(get_setting_renderer_interval()) 53 | 54 | uri_regex_obj, activated_schemes = compile_uri_regex() 55 | global_set("activated_schemes", activated_schemes) 56 | global_set("uri_regex_obj", uri_regex_obj) 57 | log("info", f"Activated schemes: {activated_schemes}") 58 | 59 | _init_images() 60 | _set_is_dirty_for_all_views(True) 61 | 62 | 63 | def _init_images() -> None: 64 | for img_name in global_get("images"): 65 | if not img_name.startswith("@"): 66 | global_set(f"images.{img_name}", get_image_info(img_name)) 67 | 68 | 69 | def _set_is_dirty_for_all_views(is_dirty: bool) -> None: 70 | """ 71 | @brief Set is_dirty for all views. 72 | 73 | @param is_dirty Indicate if views are dirty 74 | """ 75 | for view in list_all_views(): 76 | if is_processable_view(view): 77 | view_is_dirty_val(view, is_dirty) 78 | -------------------------------------------------------------------------------- /plugin/commands/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jfcherng-sublime/ST-OpenUri/5ee1125cf349f88f7429b4f267a30472063dc9c5/plugin/commands/__init__.py -------------------------------------------------------------------------------- /plugin/commands/abstract.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import enum 4 | from abc import ABC 5 | from collections.abc import Iterable 6 | 7 | import sublime 8 | import sublime_plugin 9 | 10 | from ..helpers import find_uri_regions_by_regions 11 | from ..shared import is_plugin_ready 12 | from ..types import EventDict, RegionLike 13 | 14 | 15 | class UriSource(enum.Enum): 16 | CONTEXT_MENU = enum.auto() 17 | CURSORS = enum.auto() 18 | FILE = enum.auto() 19 | NONE = enum.auto() 20 | 21 | 22 | class AbstractUriCommand(sublime_plugin.TextCommand, ABC): 23 | source = UriSource.NONE 24 | 25 | def is_enabled(self) -> bool: 26 | return is_plugin_ready() 27 | 28 | def is_visible(self) -> bool: 29 | return is_plugin_ready() 30 | 31 | def get_uri_regions(self, event: EventDict | None = None) -> list[sublime.Region]: 32 | regions: Iterable[RegionLike] = tuple() 33 | if self.source == UriSource.NONE: 34 | pass 35 | elif self.source == UriSource.CONTEXT_MENU: 36 | if event: 37 | point = self.view.window_to_text((event["x"], event["y"])) 38 | regions = ((point, point),) 39 | elif self.source == UriSource.CURSORS: 40 | regions = self.view.sel() 41 | elif self.source == UriSource.FILE: 42 | regions = ((0, self.view.size()),) 43 | else: 44 | raise RuntimeError(f"Invalid UriSource type: {self.source}") 45 | return find_uri_regions_by_regions(self.view, regions) 46 | -------------------------------------------------------------------------------- /plugin/commands/copy_uri.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | from collections.abc import Iterable 5 | 6 | import sublime 7 | 8 | from ..types import EventDict 9 | from .abstract import AbstractUriCommand, UriSource 10 | 11 | 12 | class AbstractCopyUriCommand(AbstractUriCommand, ABC): 13 | def run( 14 | self, 15 | _: sublime.Edit, 16 | event: EventDict | None = None, 17 | unique: bool = True, 18 | sort: bool = True, 19 | ) -> None: 20 | if uri_regions := self.get_uri_regions(event): 21 | uris: Iterable[str] = map(self.view.substr, uri_regions) 22 | if unique: 23 | uris = set(uris) 24 | if sort: 25 | uris = sorted(uris) 26 | sublime.set_clipboard("\n".join(uris)) 27 | 28 | 29 | class CopyUriFromViewCommand(AbstractCopyUriCommand): 30 | source = UriSource.FILE 31 | 32 | 33 | class CopyUriFromCursorsCommand(AbstractCopyUriCommand): 34 | source = UriSource.CURSORS 35 | 36 | 37 | class CopyUriFromContextMenuCommand(AbstractCopyUriCommand): 38 | source = UriSource.CONTEXT_MENU 39 | 40 | def description(self, event: EventDict | None = None) -> str: # type: ignore 41 | return f"Copy {self._find_url(event)}" 42 | 43 | def is_visible(self, event: EventDict | None = None) -> bool: # type: ignore 44 | return super().is_visible() and bool(self.get_uri_regions(event)) 45 | 46 | def want_event(self) -> bool: 47 | return True 48 | 49 | def _find_url(self, event: EventDict | None = None) -> str: 50 | uri_region = next(iter(self.get_uri_regions(event)), None) 51 | return self.view.substr(uri_region) if uri_region else "" 52 | -------------------------------------------------------------------------------- /plugin/commands/open_uri.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | 5 | import sublime 6 | 7 | from ..helpers import open_uri_with_browser 8 | from ..types import EventDict 9 | from .abstract import AbstractUriCommand, UriSource 10 | 11 | 12 | class AbstractOpenUriCommand(AbstractUriCommand, ABC): 13 | def run(self, _: sublime.Edit, event: EventDict | None = None, browser: str = "") -> None: 14 | for uri in set(map(self.view.substr, self.get_uri_regions(event))): 15 | open_uri_with_browser(uri, browser) 16 | 17 | 18 | class OpenUriFromViewCommand(AbstractOpenUriCommand): 19 | source = UriSource.FILE 20 | 21 | 22 | class OpenUriFromCursorsCommand(AbstractOpenUriCommand): 23 | source = UriSource.CURSORS 24 | -------------------------------------------------------------------------------- /plugin/commands/select_uri.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from abc import ABC 4 | 5 | import sublime 6 | 7 | from ..types import EventDict 8 | from .abstract import AbstractUriCommand, UriSource 9 | 10 | 11 | class AbstractSelectUriCommand(AbstractUriCommand, ABC): 12 | def run(self, _: sublime.Edit, event: EventDict | None = None) -> None: 13 | if uri_regions := self.get_uri_regions(event): 14 | sel = self.view.sel() 15 | sel.clear() 16 | sel.add_all(uri_regions) 17 | 18 | 19 | class SelectUriFromViewCommand(AbstractSelectUriCommand): 20 | source = UriSource.FILE 21 | 22 | 23 | class SelectUriFromCursorsCommand(AbstractSelectUriCommand): 24 | source = UriSource.CURSORS 25 | -------------------------------------------------------------------------------- /plugin/constants.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | assert __package__ 4 | 5 | PLUGIN_NAME = __package__.partition(".")[0] 6 | SETTINGS_FILE_NAME = f"{PLUGIN_NAME}.sublime-settings" 7 | 8 | URI_REGION_KEY = "OUIB_uri_regions" 9 | VIEW_SETTING_TIMESTAMP_KEY = "OUIB_last_update_timestamp" 10 | -------------------------------------------------------------------------------- /plugin/helpers.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import re 4 | import urllib.parse as urllib_parse 5 | import webbrowser 6 | from collections.abc import Iterable 7 | from typing import Any, Pattern 8 | 9 | import sublime 10 | 11 | from .libs import triegex 12 | from .logger import log 13 | from .settings import get_setting 14 | from .shared import global_get 15 | from .types import RegionLike 16 | from .utils import convert_to_st_region, is_regions_intersected, merge_regions, region_expand, region_shift 17 | 18 | 19 | def open_uri_with_browser(uri: str, browser: str | None = "") -> None: 20 | """ 21 | @brief Open the URI with the browser. 22 | 23 | @param uri The uri 24 | @param browser The browser 25 | """ 26 | parsed_uri = urllib_parse.urlparse(uri) 27 | 28 | log("debug", f"Parsed URI: {parsed_uri}") 29 | 30 | # decode URL-encoded "file" scheme such as 31 | # "file:///D:/%E6%B8%AC%E8%A9%A6.html" -> "file:///D:/測試.html" 32 | if parsed_uri.scheme == "file": 33 | uri = urllib_parse.unquote(uri) 34 | 35 | if not browser: 36 | browser = get_setting("browser") 37 | 38 | # modify browser to None to use the system's default 39 | if browser == "": 40 | browser = None 41 | 42 | try: 43 | # https://docs.python.org/3.3/library/webbrowser.html#webbrowser.get 44 | webbrowser.get(browser).open(uri, autoraise=True) 45 | except Exception as e: 46 | log("critical", f'Failed to open browser "{browser}" to "{uri}" because {e}') 47 | 48 | 49 | def compile_uri_regex() -> tuple[Pattern[str] | None, tuple[str, ...]]: 50 | """ 51 | @brief Get the compiled regex object for matching URIs. 52 | 53 | @return (compiled regex object, activated schemes) 54 | """ 55 | detect_schemes: dict[str, dict[str, Any]] = get_setting("detect_schemes") 56 | uri_path_regexes: dict[str, str] = get_setting("uri_path_regexes") 57 | 58 | activated_schemes: list[str] = [] 59 | uri_regexes: list[str] = [] 60 | # longest scheme first 61 | for scheme in sorted(detect_schemes.keys(), key=len, reverse=True): 62 | if not (scheme_settings := detect_schemes[scheme]).get("enabled", False): 63 | continue 64 | 65 | path_regex_name: str = scheme_settings.get("path_regex", "@default") 66 | if path_regex_name not in uri_path_regexes: 67 | log("warning", f'Ignore scheme "{scheme}" due to invalid "path_regex" name: {path_regex_name}') 68 | continue 69 | 70 | activated_schemes.append(scheme) 71 | uri_regexes.append(re.escape(scheme) + rf"(?:(?#{path_regex_name}))") 72 | 73 | regex = r"\b" + (triegex.Triegex(*uri_regexes).to_regex().replace(r"\b", "").replace(r"|~^(?#match nothing)", "")) 74 | 75 | log("debug", f"Optimized URI matching regex (before expanding): {regex}") 76 | 77 | # expand path regexes by their names 78 | for path_regex_name, path_regex in uri_path_regexes.items(): 79 | regex = regex.replace(rf"(?#{path_regex_name})", path_regex) 80 | 81 | log("debug", f"Optimized URI matching regex: {regex}") 82 | 83 | regex_obj = None 84 | try: 85 | regex_obj = re.compile(regex, re.IGNORECASE) 86 | except Exception as e: 87 | log( 88 | "critical", f'Cannot compile regex `{regex}` because {e}. Please check "uri_path_regex" in plugin settings.' 89 | ) 90 | 91 | return regex_obj, tuple(sorted(activated_schemes)) 92 | 93 | 94 | def find_uri_regions_by_region( 95 | view: sublime.View, 96 | region: RegionLike, 97 | search_radius: int | None = None, 98 | ) -> list[sublime.Region]: 99 | """ 100 | @brief Found intersected URI regions from view by the region 101 | 102 | @param view The view 103 | @param region The region 104 | 105 | @return Found URI regions 106 | """ 107 | return find_uri_regions_by_regions(view, (region,), search_radius) 108 | 109 | 110 | def find_uri_regions_by_regions( 111 | view: sublime.View, 112 | regions: Iterable[RegionLike], 113 | search_radius: int | None = None, 114 | ) -> list[sublime.Region]: 115 | """ 116 | @brief Found intersected URI regions from view by regions 117 | 118 | @param view The view 119 | @param regions The regions 120 | 121 | @return Found URI regions 122 | """ 123 | st_regions = sorted(convert_to_st_region(region, sort=True) for region in regions) 124 | search_regions = merge_regions( 125 | ( 126 | # ... 127 | region_expand(st_region, int(search_radius or get_setting("uri_search_radius"))) 128 | for st_region in st_regions 129 | ), 130 | True, 131 | ) 132 | 133 | uri_regions: list[sublime.Region] = [] 134 | for search_region in search_regions: 135 | uri_regions.extend( 136 | # convert "finditer()" coordinate into ST's coordinate 137 | sublime.Region(*region_shift(m.span(), max(0, search_region.a))) 138 | for m in global_get("uri_regex_obj").finditer(view.substr(search_region)) 139 | ) 140 | 141 | # only pick up "uri_region"s that are intersected with "st_regions" 142 | # note that both "st_regions" and "uri_regions" are guaranteed sorted here 143 | regions_idx = 0 144 | uri_regions_intersected: list[sublime.Region] = [] 145 | 146 | for uri_region in uri_regions: 147 | for idx in range(regions_idx, len(st_regions)): 148 | region = st_regions[idx] 149 | 150 | # later "uri_region" is always even larger so this "idx" is useless since now 151 | if uri_region.a > region.b: 152 | regions_idx = idx + 1 153 | 154 | if is_regions_intersected(uri_region, region, True): 155 | uri_regions_intersected.append(uri_region) 156 | break 157 | 158 | return uri_regions_intersected 159 | -------------------------------------------------------------------------------- /plugin/libs/imagesize.py: -------------------------------------------------------------------------------- 1 | import struct 2 | 3 | # @see https://github.com/shibukawa/imagesize_py/blob/master/imagesize.py 4 | # @see https://github.com/capsher/imagesize_py/blob/add-bytes-io/imagesize.py 5 | 6 | _UNIT_KM = -3 7 | _UNIT_100M = -2 8 | _UNIT_10M = -1 9 | _UNIT_1M = 0 10 | _UNIT_10CM = 1 11 | _UNIT_CM = 2 12 | _UNIT_MM = 3 13 | _UNIT_0_1MM = 4 14 | _UNIT_0_01MM = 5 15 | _UNIT_UM = 6 16 | _UNIT_INCH = 6 17 | 18 | _TIFF_TYPE_SIZES = {1: 1, 2: 1, 3: 2, 4: 4, 5: 8, 6: 1, 7: 1, 8: 2, 9: 4, 10: 8, 11: 4, 12: 8} 19 | 20 | 21 | def _convertToDPI(density, unit): 22 | if unit == _UNIT_KM: 23 | return int(density * 0.0000254 + 0.5) 24 | elif unit == _UNIT_100M: 25 | return int(density * 0.000254 + 0.5) 26 | elif unit == _UNIT_10M: 27 | return int(density * 0.00254 + 0.5) 28 | elif unit == _UNIT_1M: 29 | return int(density * 0.0254 + 0.5) 30 | elif unit == _UNIT_10CM: 31 | return int(density * 0.254 + 0.5) 32 | elif unit == _UNIT_CM: 33 | return int(density * 2.54 + 0.5) 34 | elif unit == _UNIT_MM: 35 | return int(density * 25.4 + 0.5) 36 | elif unit == _UNIT_0_1MM: 37 | return density * 254 38 | elif unit == _UNIT_0_01MM: 39 | return density * 2540 40 | elif unit == _UNIT_UM: 41 | return density * 25400 42 | return density 43 | 44 | 45 | def _get(fhandle): 46 | height = -1 47 | width = -1 48 | head = fhandle.read(24) 49 | size = len(head) 50 | # handle GIFs 51 | if size >= 10 and head[:6] in (b"GIF87a", b"GIF89a"): 52 | # Check to see if content_type is correct 53 | try: 54 | width, height = struct.unpack("= 24 and head.startswith(b"\211PNG\r\n\032\n") and head[12:16] == b"IHDR": 59 | try: 60 | width, height = struct.unpack(">LL", head[16:24]) 61 | except struct.error: 62 | raise ValueError("Invalid PNG file") 63 | # Maybe this is for an older PNG version. 64 | elif size >= 16 and head.startswith(b"\211PNG\r\n\032\n"): 65 | # Check to see if we have the right content type 66 | try: 67 | width, height = struct.unpack(">LL", head[8:16]) 68 | except struct.error: 69 | raise ValueError("Invalid PNG file") 70 | # handle JPEGs 71 | elif size >= 2 and head.startswith(b"\377\330"): 72 | try: 73 | fhandle.seek(0) # Read 0xff next 74 | size = 2 75 | ftype = 0 76 | while not 0xC0 <= ftype <= 0xCF or ftype in [0xC4, 0xC8, 0xCC]: 77 | fhandle.seek(size, 1) 78 | byte = fhandle.read(1) 79 | while ord(byte) == 0xFF: 80 | byte = fhandle.read(1) 81 | ftype = ord(byte) 82 | size = struct.unpack(">H", fhandle.read(2))[0] - 2 83 | # We are at a SOFn block 84 | fhandle.seek(1, 1) # Skip `precision' byte. 85 | height, width = struct.unpack(">HH", fhandle.read(4)) 86 | except struct.error: 87 | raise ValueError("Invalid JPEG file") 88 | # handle JPEG2000s 89 | elif size >= 12 and head.startswith(b"\x00\x00\x00\x0cjP \r\n\x87\n"): 90 | fhandle.seek(48) 91 | try: 92 | height, width = struct.unpack(">LL", fhandle.read(8)) 93 | except struct.error: 94 | raise ValueError("Invalid JPEG2000 file") 95 | # handle big endian TIFF 96 | elif size >= 8 and head.startswith(b"\x4d\x4d\x00\x2a"): 97 | offset = struct.unpack(">L", head[4:8])[0] 98 | fhandle.seek(offset) 99 | ifdsize = struct.unpack(">H", fhandle.read(2))[0] 100 | for i in range(ifdsize): 101 | tag, datatype, count, data = struct.unpack(">HHLL", fhandle.read(12)) 102 | if tag == 256: 103 | if datatype == 3: 104 | width = int(data / 65536) 105 | elif datatype == 4: 106 | width = data 107 | else: 108 | raise ValueError( 109 | "Invalid TIFF file: width column data type should be SHORT/LONG." 110 | ) 111 | elif tag == 257: 112 | if datatype == 3: 113 | height = int(data / 65536) 114 | elif datatype == 4: 115 | height = data 116 | else: 117 | raise ValueError( 118 | "Invalid TIFF file: height column data type should be SHORT/LONG." 119 | ) 120 | if width != -1 and height != -1: 121 | break 122 | if width == -1 or height == -1: 123 | raise ValueError("Invalid TIFF file: width and/or height IDS entries are missing.") 124 | elif size >= 8 and head.startswith(b"\x49\x49\x2a\x00"): 125 | offset = struct.unpack("= 10 and head[:6] in (b"GIF87a", b"GIF89a"): 180 | pass 181 | # see png edition spec bytes are below chunk length then and finally the 182 | elif size >= 24 and head.startswith(b"\211PNG\r\n\032\n"): 183 | chunkOffset = 8 184 | chunk = head[8:] 185 | while True: 186 | chunkType = chunk[4:8] 187 | if chunkType == b"pHYs": 188 | try: 189 | xDensity, yDensity, unit = struct.unpack(">LLB", chunk[8:]) 190 | except struct.error: 191 | raise ValueError("Invalid PNG file") 192 | if unit: 193 | xDPI = _convertToDPI(xDensity, _UNIT_1M) 194 | yDPI = _convertToDPI(yDensity, _UNIT_1M) 195 | else: # no unit 196 | xDPI = xDensity 197 | yDPI = yDensity 198 | break 199 | elif chunkType == b"IDAT": 200 | break 201 | else: 202 | try: 203 | dataSize, = struct.unpack(">L", chunk[0:4]) 204 | except struct.error: 205 | raise ValueError("Invalid PNG file") 206 | chunkOffset += dataSize + 12 207 | fhandle.seek(chunkOffset) 208 | chunk = fhandle.read(17) 209 | # handle JPEGs 210 | elif size >= 2 and head.startswith(b"\377\330"): 211 | try: 212 | fhandle.seek(0) # Read 0xff next 213 | size = 2 214 | ftype = 0 215 | while not 0xC0 <= ftype <= 0xCF: 216 | if ftype == 0xE0: # APP0 marker 217 | fhandle.seek(7, 1) 218 | unit, xDensity, yDensity = struct.unpack(">BHH", fhandle.read(5)) 219 | if unit == 1 or unit == 0: 220 | xDPI = xDensity 221 | yDPI = yDensity 222 | elif unit == 2: 223 | xDPI = _convertToDPI(xDensity, _UNIT_CM) 224 | yDPI = _convertToDPI(yDensity, _UNIT_CM) 225 | break 226 | fhandle.seek(size, 1) 227 | byte = fhandle.read(1) 228 | while ord(byte) == 0xFF: 229 | byte = fhandle.read(1) 230 | ftype = ord(byte) 231 | size = struct.unpack(">H", fhandle.read(2))[0] - 2 232 | except struct.error: 233 | raise ValueError("Invalid JPEG file") 234 | # handle JPEG2000s 235 | elif size >= 12 and head.startswith(b"\x00\x00\x00\x0cjP \r\n\x87\n"): 236 | fhandle.seek(32) 237 | # skip JP2 image header box 238 | headerSize = struct.unpack(">L", fhandle.read(4))[0] - 8 239 | fhandle.seek(4, 1) 240 | foundResBox = False 241 | try: 242 | while headerSize > 0: 243 | print("headerSize", headerSize) 244 | boxHeader = fhandle.read(8) 245 | boxType = boxHeader[4:] 246 | print(boxType) 247 | if boxType == "res ": # find resolution super box 248 | foundResBox = True 249 | headerSize -= 8 250 | print("found res super box") 251 | break 252 | print("@1", boxHeader) 253 | boxSize, = struct.unpack(">L", boxHeader[:4]) 254 | print("boxSize", boxSize) 255 | fhandle.seek(boxSize - 8, 1) 256 | headerSize -= boxSize 257 | if foundResBox: 258 | while headerSize > 0: 259 | boxHeader = fhandle.read(8) 260 | boxType = boxHeader[4:] 261 | print(boxType) 262 | if boxType == "resd": # Display resolution box 263 | print("@2") 264 | yDensity, xDensity, yUnit, xUnit = struct.unpack(">HHBB", fhandle.read(10)) 265 | xDPI = _convertToDPI(xDensity, xUnit) 266 | yDPI = _convertToDPI(yDensity, yUnit) 267 | break 268 | boxSize, = struct.unpack(">L", boxHeader[:4]) 269 | print("boxSize", boxSize) 270 | fhandle.seek(boxSize - 8, 1) 271 | headerSize -= boxSize 272 | except struct.error as e: 273 | print(e) 274 | raise ValueError("Invalid JPEG2000 file") 275 | return xDPI, yDPI 276 | 277 | 278 | def getDPI(filepath): 279 | """ 280 | Return (xDPI, yDPI) for a given img file content 281 | no requirements 282 | """ 283 | xDPI = -1 284 | yDPI = -1 285 | with open(filepath, "rb") as fhandle: 286 | xDPI, yDPI = _getDPI(fhandle) 287 | return xDPI, yDPI 288 | 289 | 290 | def getDPI_from_bytes(bytes): 291 | """ 292 | Return (xDPI, yDPI) for a given img content (bytes) 293 | no requirements 294 | """ 295 | import io 296 | 297 | xDPI = -1 298 | yDPI = -1 299 | with io.BytesIO(bytes) as fhandle: 300 | xDPI, yDPI = _getDPI(fhandle) 301 | return xDPI, yDPI 302 | -------------------------------------------------------------------------------- /plugin/libs/triegex/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Alexander Zhukov 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. 22 | -------------------------------------------------------------------------------- /plugin/libs/triegex/__init__.py: -------------------------------------------------------------------------------- 1 | # @see https://github.com/ZhukovAlexander/triegex 2 | import collections 3 | 4 | __all__ = ("Triegex",) 5 | 6 | OR = r"|" 7 | 8 | # regex below matches nothing https://stackoverflow.com/a/940840/2183102. We 9 | # use '~' to ensure it comes last when lexicographically sorted: 10 | # max(string.printable) is '~' 11 | NOTHING = r"~^(?#match nothing)" 12 | GROUP = r"(?:{0})" 13 | WORD_BOUNDARY = r"\b" 14 | 15 | 16 | class TriegexNode: 17 | def __init__(self, char: str, end: bool, *children): 18 | self.char = char if char is not None else "" 19 | self.end = end 20 | self.children = {child.char: child for child in children} 21 | 22 | def __iter__(self): 23 | return iter(sorted(self.children.values(), key=lambda x: x.char)) 24 | 25 | def __len__(self): 26 | return len(self.children) 27 | 28 | def __repr__(self): 29 | return "".format(self) 30 | 31 | def __contains__(self, key): 32 | return key in self.children 33 | 34 | def __getitem__(self, key): 35 | return self.children[key] 36 | 37 | def __delitem__(self, key): 38 | del self.children[key] 39 | 40 | def to_regex(self) -> str: 41 | """ 42 | RECURSIVE IMPLEMENTATION FOR REFERENCE 43 | suffixes = [v.to_regex() for k, v in self.children.items()] 44 | if self.end: 45 | suffixes += [WORD_BOUNDARY] 46 | 47 | if len(suffixes) > 1: 48 | return self.char + GROUP.format(OR.join(suffixes)) 49 | elif len(suffixes) == 1: 50 | return self.char + suffixes[0] 51 | else: 52 | return self.char 53 | """ 54 | 55 | stack = [self] 56 | # marks starting indices of children of a node 57 | lookup = [] 58 | 59 | # Creates an ordered list of nodes starting with root and ending with leaves by using BFS 60 | i = 0 61 | j = 1 62 | while i < len(stack): 63 | stack.extend(sorted(stack[i].children.values(), key=lambda node: node.char)) 64 | lookup.append(j) 65 | j += len(stack[i].children) 66 | i += 1 67 | 68 | i = len(stack) 69 | # temp value array 70 | sub_regexes = [None] * i 71 | while i > 0: 72 | # We start with leaves and end at root thus we decrement 73 | i -= 1 74 | node = stack[i] 75 | # Get regexes of child nodes and make a root regex 76 | suffixes = [ 77 | sub_regexes[child] for child in range(lookup[i], lookup[i] + len(node.children)) 78 | ] 79 | if node.end: 80 | # if the node is an ending node we add a \b character 81 | suffixes += [WORD_BOUNDARY] 82 | # If we arrive at the root node we have to add the NOTHING expression 83 | if i == 0: 84 | suffixes += [NOTHING] 85 | if len(suffixes) > 1: 86 | sub_regexes[i] = node.char + GROUP.format(OR.join(suffixes)) 87 | elif len(suffixes) == 1: 88 | sub_regexes[i] = node.char + suffixes[0] 89 | else: 90 | sub_regexes[i] = node.char 91 | # return the top Regex 92 | return sub_regexes[0] 93 | 94 | 95 | class Triegex(collections.MutableSet): 96 | def __init__(self, *words): 97 | """ 98 | Trigex constructor. 99 | """ 100 | 101 | self._root = TriegexNode(None, False) 102 | 103 | for word in words: 104 | self.add(word) 105 | 106 | def add(self, word: str): 107 | current = self._root 108 | for letter in word[:-1]: 109 | if letter in current.children: 110 | current = current.children[letter] 111 | else: 112 | current = current.children.setdefault(letter, TriegexNode(letter, False)) 113 | # this will ensure that we correctly match the word boundary 114 | if word[-1] in current.children: 115 | current.children[word[-1]].end = True 116 | else: 117 | current.children[word[-1]] = TriegexNode(word[-1], True) 118 | 119 | def to_regex(self) -> str: 120 | r""" 121 | Produce regular expression that will match each word in the 122 | internal trie. 123 | 124 | >>> t = Triegex('foo', 'bar', 'baz') 125 | >>> t.to_regex() 126 | '(?:ba(?:r\\b|z\\b)|foo\\b|~^(?#match nothing))' 127 | """ 128 | return self._root.to_regex() 129 | 130 | def _traverse(self): 131 | stack = [self._root] 132 | current = self._root 133 | while stack: 134 | yield current 135 | current = stack.pop() 136 | stack.extend(current.children.values()) 137 | 138 | def __iter__(self): 139 | paths = {self._root.char: []} 140 | for node in self._traverse(): 141 | for child in node: 142 | paths[child.char] = [node.char] + paths[node.char] 143 | if child.end: 144 | char = child.char 145 | yield "".join(reversed([char] + paths[char])) 146 | 147 | def __len__(self): 148 | return sum(1 for _ in self.__iter__()) 149 | 150 | def __contains__(self, word): 151 | current = self._root 152 | for char in word: 153 | if char not in current: 154 | return False 155 | current = current[char] 156 | return True and current.end # word has to end with the last char 157 | 158 | def discard(self, word): 159 | to_delete = [self._root] 160 | current = self._root 161 | for char in word: 162 | if char not in current: 163 | return 164 | current = current[char] 165 | to_delete.append(current) 166 | if not to_delete[-1].end: 167 | return 168 | while len(to_delete) > 1: 169 | node = to_delete.pop() 170 | if len(node) == 0: 171 | del to_delete[-1][node.char] 172 | return 173 | -------------------------------------------------------------------------------- /plugin/listener.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sublime 4 | import sublime_plugin 5 | 6 | from .helpers import find_uri_regions_by_region 7 | from .settings import get_setting, get_setting_show_open_button 8 | from .ui.phantom_set import delete_phantom_set, init_phantom_set 9 | from .ui.popup import show_popup 10 | from .ui.region_drawing import draw_uri_regions 11 | from .utils import get_timestamp, view_is_dirty_val, view_last_typing_timestamp_val 12 | 13 | 14 | class OpenUriViewEventListener(sublime_plugin.ViewEventListener): 15 | def __init__(self, view: sublime.View) -> None: 16 | super().__init__(view) 17 | 18 | init_phantom_set(self.view) 19 | view_last_typing_timestamp_val(self.view, 0) 20 | 21 | def on_pre_close(self) -> None: 22 | delete_phantom_set(self.view) 23 | 24 | def on_load_async(self) -> None: 25 | view_is_dirty_val(self.view, True) 26 | 27 | def on_modified_async(self) -> None: 28 | view_is_dirty_val(self.view, True) 29 | view_last_typing_timestamp_val(self.view, get_timestamp()) 30 | 31 | def on_hover(self, point: int, hover_zone: int) -> None: 32 | if hover_zone != sublime.HOVER_TEXT: 33 | uri_regions: list[sublime.Region] = [] 34 | else: 35 | uri_regions = find_uri_regions_by_region(self.view, point) 36 | 37 | if uri_regions and get_setting_show_open_button(self.view) == "hover": 38 | show_popup(self.view, uri_regions[0], point) 39 | 40 | if get_setting("draw_uri_regions.enabled") == "hover": 41 | draw_uri_regions(self.view, uri_regions) 42 | -------------------------------------------------------------------------------- /plugin/logger.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | 5 | from .shared import global_get 6 | 7 | LOG_FORMAT = "[%(name)s][%(levelname)s] %(message)s" 8 | LOG_LEVEL_DEFAULT = "INFO" 9 | 10 | 11 | def init_plugin_logger() -> logging.Logger: 12 | """ 13 | @brief Initiate a plugin logger. 14 | 15 | @return The initiated plugin logger. 16 | """ 17 | from .constants import PLUGIN_NAME 18 | 19 | def set_logger_hander(logger: logging.Logger) -> None: 20 | # remove all existing log handlers 21 | for handler in logger.handlers: 22 | logger.removeHandler(handler) 23 | 24 | logging_handler = logging.StreamHandler() 25 | logging_handler.setFormatter(logging.Formatter(LOG_FORMAT)) 26 | logger.addHandler(logging_handler) 27 | 28 | logging.addLevelName(5, "DEBUG_LOW") 29 | logging.addLevelName(1001, "NOTHING") 30 | logger = logging.getLogger(PLUGIN_NAME) 31 | logger.propagate = False # prevent appear multiple same log messages 32 | set_logger_hander(logger) 33 | 34 | return logger 35 | 36 | 37 | def apply_user_log_level(logger: logging.Logger) -> None: 38 | """ 39 | @brief Apply user-set "log_level" to the logger. 40 | 41 | @param logger The logger 42 | """ 43 | from .settings import get_setting 44 | 45 | log_level = get_setting("log_level").upper() 46 | 47 | if not isinstance(logging.getLevelName(log_level), int): 48 | logger.warning(f'Unknown "log_level": {log_level} (assumed "{LOG_LEVEL_DEFAULT}")') 49 | log_level = LOG_LEVEL_DEFAULT 50 | 51 | # temporary set to INFO level for logging log_level changed 52 | logger.setLevel("INFO") 53 | logger.info(f"Set log level: {log_level}") 54 | logger.setLevel(log_level) 55 | 56 | 57 | def log(level: str, msg: str) -> None: 58 | """ 59 | @brief A shorhand for logging message with the global logger. 60 | 61 | @param level The log level 62 | @param msg The message 63 | """ 64 | level_upper = level.upper() 65 | level_int = logging.getLevelName(level_upper) 66 | 67 | if not isinstance(level_int, int): 68 | raise ValueError(f'Unknown log level "{level}" whose message is: {msg}') 69 | 70 | global_get("logger").log(level_int, msg) 71 | 72 | 73 | def msg(msg: str) -> str: 74 | """ 75 | @brief Generate plugin message. 76 | 77 | @param msg The message 78 | 79 | @return The plugin message. 80 | """ 81 | from .constants import PLUGIN_NAME 82 | 83 | return f"[{PLUGIN_NAME}] {msg}" 84 | -------------------------------------------------------------------------------- /plugin/renderer.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import threading 4 | from collections.abc import Callable 5 | from typing import Any 6 | 7 | import sublime 8 | 9 | from .logger import log 10 | from .settings import get_setting, get_setting_show_open_button, is_view_too_large, is_view_typing 11 | from .shared import global_get 12 | from .ui.phantom_set import erase_phantom_set, update_phantom_set 13 | from .ui.region_drawing import draw_uri_regions, erase_uri_regions 14 | from .utils import is_processable_view, is_transient_view, list_foreground_views, view_find_all, view_is_dirty_val 15 | 16 | 17 | class RepeatingTimer: 18 | def __init__(self, interval_ms: int, func: Callable, *args: Any, **kwargs: Any) -> None: 19 | self.interval_s = interval_ms / 1000 20 | self.timer: threading.Timer | None = None 21 | self.is_running = False 22 | self.set_func(func, *args, **kwargs) 23 | 24 | def set_func(self, func: Callable, *args: Any, **kwargs: Any) -> None: 25 | self.func = func 26 | self.args = args 27 | self.kwargs = kwargs 28 | 29 | def set_interval(self, interval_ms: int) -> None: 30 | self.interval_s = interval_ms / 1000 31 | 32 | def start(self) -> None: 33 | self.timer = threading.Timer(self.interval_s, self._callback) 34 | self.timer.start() 35 | self.is_running = True 36 | 37 | def cancel(self) -> None: 38 | if self.timer: 39 | self.timer.cancel() 40 | self.is_running = False 41 | 42 | def _callback(self) -> None: 43 | self.func(*self.args, **self.kwargs) 44 | self.start() 45 | 46 | 47 | class RendererThread(RepeatingTimer): 48 | def __init__(self, interval_ms: int = 1000) -> None: 49 | super().__init__(interval_ms, self._update_foreground_views) 50 | 51 | # to prevent from overlapped processes when using a low interval 52 | self._is_rendering = False 53 | 54 | def _update_foreground_views(self) -> None: 55 | if self._is_rendering: 56 | return 57 | 58 | self._is_rendering = True 59 | for view in list_foreground_views(): 60 | self._update_view(view) 61 | self._is_rendering = False 62 | 63 | def _update_view(self, view: sublime.View) -> None: 64 | if ( 65 | not is_processable_view(view) 66 | or not view_is_dirty_val(view) 67 | or is_view_typing(view) 68 | or (is_transient_view(view) and not get_setting("work_for_transient_view")) 69 | ): 70 | return 71 | 72 | if is_view_too_large(view): 73 | self._clean_up_phantom_set(view) 74 | self._clean_up_uri_regions(view) 75 | view_is_dirty_val(view, False) 76 | return 77 | 78 | self._detect_uris_globally(view) 79 | view_is_dirty_val(view, False) 80 | 81 | def _detect_uris_globally(self, view: sublime.View) -> None: 82 | uri_regions = tuple( 83 | view_find_all( 84 | view, 85 | global_get("uri_regex_obj"), 86 | get_setting("expand_uri_regions_selectors"), 87 | ) 88 | ) 89 | 90 | # handle Phantoms 91 | if get_setting_show_open_button(view) == "always": 92 | update_phantom_set(view, uri_regions) 93 | log("debug_low", "re-render phantoms") 94 | else: 95 | self._clean_up_phantom_set(view) 96 | 97 | # handle draw URI regions 98 | if get_setting("draw_uri_regions.enabled") == "always": 99 | draw_uri_regions(view, uri_regions) 100 | log("debug_low", "draw URI regions") 101 | else: 102 | self._clean_up_uri_regions(view) 103 | 104 | def _clean_up_phantom_set(self, view: sublime.View) -> None: 105 | erase_phantom_set(view) 106 | log("debug_low", "erase phantoms") 107 | 108 | def _clean_up_uri_regions(self, view: sublime.View) -> None: 109 | erase_uri_regions(view) 110 | log("debug_low", "erase URI regions") 111 | -------------------------------------------------------------------------------- /plugin/settings.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import tempfile 5 | from pathlib import Path 6 | from typing import Any 7 | 8 | import sublime 9 | 10 | from .constants import PLUGIN_NAME, SETTINGS_FILE_NAME 11 | from .libs import imagesize 12 | from .logger import log 13 | from .shared import global_get 14 | from .types import ImageDict 15 | from .utils import get_timestamp, view_last_typing_timestamp_val 16 | 17 | 18 | def get_expanding_variables(window: sublime.Window | None) -> dict[str, str]: 19 | variables: dict[str, Any] = { 20 | "home": str(Path.home()), 21 | "package_name": PLUGIN_NAME, 22 | "package_path": f"Packages/{PLUGIN_NAME}", 23 | "temp_dir": tempfile.gettempdir(), 24 | } 25 | 26 | if window: 27 | variables.update(window.extract_variables()) 28 | 29 | return variables 30 | 31 | 32 | def get_settings_object() -> sublime.Settings: 33 | """ 34 | @brief Get the plugin settings object. This function will call `sublime.load_settings()`. 35 | 36 | @return The settings object. 37 | """ 38 | return sublime.load_settings(SETTINGS_FILE_NAME) 39 | 40 | 41 | def get_setting(dotted: str, default: Any | None = None) -> Any: 42 | """ 43 | @brief Get the plugin setting with the dotted key. 44 | 45 | @param dotted The dotted key 46 | @param default The default value if the key doesn't exist 47 | 48 | @return The setting's value. 49 | """ 50 | return global_get(f"settings.{dotted}", default) 51 | 52 | 53 | def get_image_path(img_name: str) -> str: 54 | """ 55 | @brief Get the image resource path from plugin settings. 56 | 57 | @param img_name The image name 58 | 59 | @return The image resource path. 60 | """ 61 | return sublime.expand_variables( 62 | get_setting("image_files")[img_name], 63 | get_expanding_variables(sublime.active_window()), 64 | ) 65 | 66 | 67 | def get_image_info(img_name: str) -> ImageDict: 68 | """ 69 | @brief Get image informations of an image from plugin settings. 70 | 71 | @param img_name The image name 72 | 73 | @return The image information. 74 | """ 75 | img_path = get_image_path(img_name) 76 | img_ext = Path(img_path).suffix 77 | img_mime = "image/png" 78 | 79 | assert img_ext.lower() == ".png" 80 | 81 | try: 82 | img_bytes = sublime.load_binary_resource(img_path) 83 | except OSError: 84 | img_bytes = b"" 85 | log("error", f"Resource not found: {img_path}") 86 | 87 | img_base64 = base64.b64encode(img_bytes).decode() 88 | img_w, img_h = imagesize.get_from_bytes(img_bytes) 89 | 90 | return { 91 | "base64": img_base64, 92 | "bytes": img_bytes, 93 | "ext": img_ext, 94 | "mime": img_mime, 95 | "path": img_path, 96 | "ratio_wh": img_w / img_h, 97 | "size": (img_w, img_h), 98 | } 99 | 100 | 101 | def get_setting_renderer_interval() -> int: 102 | """ 103 | @brief Get the renderer interval. 104 | 105 | @return The renderer interval. 106 | """ 107 | if (interval := get_setting("renderer_interval", 250)) < 0: 108 | interval = float("inf") 109 | 110 | # a minimum for not crashing the system accidentally 111 | return int(max(30, interval)) 112 | 113 | 114 | def get_setting_show_open_button(view: sublime.View) -> str: 115 | return get_setting( 116 | "show_open_button_fallback" if not view.is_loading() and is_view_too_large(view) else "show_open_button" 117 | ) 118 | 119 | 120 | def is_view_too_large(view: sublime.View) -> bool: 121 | """ 122 | @brief Determine if the view is too large. Note that size will be `0` if the view is loading. 123 | 124 | @param view The view 125 | 126 | @return `True` if the view is too large, `False` otherwise. 127 | """ 128 | return view.size() > get_setting("large_file_threshold") 129 | 130 | 131 | def is_view_typing(view: sublime.View) -> bool: 132 | """ 133 | @brief Determine if the view typing. 134 | 135 | @param view The view 136 | 137 | @return `True` if the view is typing, `False` otherwise. 138 | """ 139 | now_s = get_timestamp() 140 | last_typing_s = view_last_typing_timestamp_val(view) or 0 141 | 142 | return (now_s - last_typing_s) * 1000 < get_setting("typing_period") 143 | -------------------------------------------------------------------------------- /plugin/shared.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import logging 4 | import threading 5 | from typing import Any, Pattern 6 | 7 | import sublime 8 | 9 | from .types import ImageDict 10 | from .utils import dotted_get, dotted_set 11 | 12 | 13 | class G: 14 | """This class stores application-level global variables.""" 15 | 16 | settings: sublime.Settings | None = None 17 | """the plugin settings object""" 18 | 19 | logger: logging.Logger | None = None 20 | """the logger to log messages""" 21 | 22 | renderer_thread: threading.Thread | None = None 23 | """the background thread for managing phantoms for views""" 24 | 25 | activated_schemes: tuple[str, ...] = tuple() 26 | uri_regex_obj: Pattern[str] | None = None 27 | 28 | images: dict[str, ImageDict] = { 29 | "phantom": {}, # type: ignore 30 | "popup": {}, # type: ignore 31 | } 32 | 33 | 34 | def is_plugin_ready() -> bool: 35 | return bool(G.settings and G.uri_regex_obj) 36 | 37 | 38 | def global_get(dotted: str, default: Any | None = None) -> Any: 39 | return dotted_get(G, dotted, default) 40 | 41 | 42 | def global_set(dotted: str, value: Any) -> None: 43 | return dotted_set(G, dotted, value) 44 | -------------------------------------------------------------------------------- /plugin/types.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from typing import Any, Callable, List, Tuple, TypedDict, TypeVar, Union 4 | 5 | import sublime 6 | 7 | T_AnyCallable = TypeVar("T_AnyCallable", bound=Callable[..., Any]) 8 | 9 | RegionLike = Union[ 10 | sublime.Region, 11 | int, # point 12 | List[int], # region in list form 13 | Tuple[int, int], # region in tuple form 14 | ] 15 | 16 | 17 | class EventDict(TypedDict): 18 | x: float 19 | y: float 20 | modifier_keys: EventModifierKeysDict 21 | 22 | 23 | class EventModifierKeysDict(TypedDict, total=False): 24 | primary: bool 25 | ctrl: bool 26 | alt: bool 27 | altgr: bool 28 | shift: bool 29 | super: bool 30 | 31 | 32 | class ImageDict(TypedDict): 33 | base64: str 34 | bytes: bytes 35 | ext: str 36 | mime: str 37 | path: str 38 | ratio_wh: float 39 | size: tuple[int, int] 40 | -------------------------------------------------------------------------------- /plugin/ui/image.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import base64 4 | import io 5 | import re 6 | from collections.abc import Sequence 7 | from functools import lru_cache 8 | from itertools import chain 9 | 10 | import sublime 11 | from more_itertools import grouper 12 | 13 | from ..libs import png 14 | from ..settings import get_setting 15 | from ..shared import global_get 16 | from ..utils import simple_decorator 17 | 18 | 19 | def get_image_color(img_name: str, region: sublime.Region) -> str: 20 | """ 21 | @brief Get the image color from plugin settings in the form of #RRGGBBAA. 22 | 23 | @param img_name The image name 24 | @param region The region 25 | 26 | @return The color code in the form of #RRGGBBAA 27 | """ 28 | return color_code_to_rgba(get_setting("image_colors")[img_name], region) 29 | 30 | 31 | @lru_cache 32 | def get_colored_image_base64_by_color(img_name: str, rgba_code: str) -> str: 33 | """ 34 | @brief Get the colored image in base64 string by RGBA color code. 35 | 36 | @param img_name The image name 37 | @param rgba_code The color code in #RRGGBBAA 38 | 39 | @return The image base64 string 40 | """ 41 | if not rgba_code: 42 | return global_get(f"images.{img_name}.base64") 43 | 44 | img_bytes: bytes = global_get(f"images.{img_name}.bytes") 45 | img_bytes = change_png_bytes_color(img_bytes, rgba_code) 46 | 47 | return base64.b64encode(img_bytes).decode() 48 | 49 | 50 | def get_colored_image_base64_by_region(img_name: str, region: sublime.Region) -> str: 51 | """ 52 | @brief Get the colored image in base64 string by region. 53 | 54 | @param img_name The image name 55 | @param region The region 56 | 57 | @return The image base64 string 58 | """ 59 | return get_colored_image_base64_by_color(img_name, get_image_color(img_name, region)) 60 | 61 | 62 | @lru_cache 63 | def change_png_bytes_color(img_bytes: bytes, rgba_code: str) -> bytes: 64 | """ 65 | @brief Change all colors in the PNG bytes to the new color. 66 | 67 | @param img_bytes The PNG image bytes 68 | @param rgba_code The color code in the form of #RRGGBBAA 69 | 70 | @return Color-changed PNG image bytes. 71 | """ 72 | if not rgba_code: 73 | return img_bytes 74 | 75 | if not re.match(r"#[0-9a-fA-F]{8}$", rgba_code): 76 | raise ValueError("Invalid RGBA color code: " + rgba_code) 77 | 78 | def render_pixel( 79 | rgba_src: Sequence[int], # length=4 80 | rgba_dst: Sequence[int], # length=4 81 | invert_gray: bool = False, 82 | ) -> tuple[int, int, int, int]: 83 | gray = calculate_gray(rgba_src) 84 | if invert_gray: 85 | gray = 0xFF - gray 86 | 87 | # ">> 8" is an approximation for "/ 0xFF" in following calculations 88 | return ( 89 | int(rgba_dst[0] * gray) >> 8, 90 | int(rgba_dst[1] * gray) >> 8, 91 | int(rgba_dst[2] * gray) >> 8, 92 | int(rgba_dst[3] * rgba_src[3]) >> 8, 93 | ) 94 | 95 | invert_gray = not is_img_light(img_bytes) # invert for dark image to get a solid looking 96 | rgba_dst = [int(rgba_code[i : i + 2], 16) for i in range(1, 9, 2)] 97 | 98 | rows_dst: list[list[int]] = [] 99 | for row_src in png.Reader(bytes=img_bytes).asRGBA()[2]: 100 | row_dst = list(chain(*(render_pixel(rgba_src, rgba_dst, invert_gray) for rgba_src in grouper(row_src, 4)))) 101 | rows_dst.append(row_dst) 102 | 103 | buf = io.BytesIO() 104 | png.from_array(rows_dst, "RGBA").write(buf) 105 | 106 | return buf.getvalue() 107 | 108 | 109 | def calculate_gray(rgb: Sequence[int]) -> int: 110 | """ 111 | @brief Calculate the gray scale of a color. 112 | @see https://atlaboratary.blogspot.com/2013/08/rgb-g-rey-l-gray-r0.html 113 | 114 | @param rgb The rgb color in list form 115 | 116 | @return The gray scale. 117 | """ 118 | return (rgb[0] * 38 + rgb[1] * 75 + rgb[2] * 15) >> 7 119 | 120 | 121 | def is_img_light(img_bytes: bytes) -> bool: 122 | """ 123 | @brief Determine if image is light colored. 124 | 125 | @param img_bytes The image bytes 126 | 127 | @return True if image is light, False otherwise. 128 | """ 129 | w, h, rows, _ = png.Reader(bytes=img_bytes).asRGBA() 130 | gray_sum = sum(calculate_gray(rgba) for row in rows for rgba in grouper(row, 4)) 131 | return (gray_sum >> 7) > w * h 132 | 133 | 134 | def add_alpha_to_rgb(color_code: str) -> str: 135 | """ 136 | @brief Add the alpha part to a valid RGB color code (#RGB, #RRGGBB, #RRGGBBAA) 137 | 138 | @param color_code The color code 139 | 140 | @return The color code in the form of #RRGGBBAA in lowercase 141 | """ 142 | if not (rgb := color_code.lstrip("#")[:8]): 143 | return "" 144 | 145 | if len(rgb) == 8: 146 | return f"#{rgb}".lower() 147 | 148 | # RGB to RRGGBB 149 | if len(rgb) == 3: 150 | rgb = rgb[0] * 2 + rgb[1] * 2 + rgb[2] * 2 151 | 152 | if len(rgb) == 6: 153 | return f"#{rgb[:6]}ff".lower() 154 | 155 | raise ValueError(f"Invalid RGB/RGBA color code: {color_code}") 156 | 157 | 158 | @simple_decorator(add_alpha_to_rgb) 159 | def color_code_to_rgba(color_code: str, region: sublime.Region) -> str: 160 | """ 161 | @brief Convert user settings color code into #RRGGBBAA form 162 | 163 | @param color_code The color code string from user settings 164 | @param region The scope-related region 165 | 166 | @return The color code in the form of #RRGGBBAA 167 | """ 168 | if not color_code: 169 | return "" 170 | 171 | # "color_code" is a scope? 172 | if not color_code.startswith("#"): 173 | if view := sublime.active_window().active_view(): 174 | # "color" is guaranteed to be #RRGGBB or #RRGGBBAA 175 | color = view.style_for_scope(view.scope_name(region.end() - 1)).get("foreground", "") 176 | 177 | if color_code == "@scope": 178 | return color 179 | 180 | if color_code == "@scope_inverted": 181 | # strip "#" and make color into RRGGBBAA int 182 | rgba_int = int(f"{color}ff"[1:9], 16) 183 | # invert RRGGBB and remain AA, prepend 0s to hex until 8 chars RRGGBBAA 184 | return f"#{(~rgba_int & 0xFFFFFF00) | (rgba_int & 0xFF):08x}" 185 | return "" 186 | 187 | # now color code must starts with "#" 188 | rgb = color_code[1:9] # strip "#" and possible extra chars 189 | 190 | # RGB, RRGGBB, RRGGBBAA are legal 191 | if len(rgb) in {3, 6, 8} and re.match(r"[0-9a-fA-F]+$", rgb): 192 | return f"#{rgb}" 193 | 194 | return "" 195 | -------------------------------------------------------------------------------- /plugin/ui/phantom_set.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Iterable 4 | 5 | import sublime 6 | 7 | from ..constants import PLUGIN_NAME 8 | from ..helpers import open_uri_with_browser 9 | from ..shared import global_get 10 | from ..types import ImageDict 11 | from .image import get_colored_image_base64_by_region 12 | from .phatom_sets_manager import PhatomSetsManager 13 | 14 | PHANTOM_TEMPLATE = """ 15 | 16 | 29 | 30 | 31 | """ 32 | 33 | 34 | def get_phantom_set_id(view: sublime.View) -> str: 35 | return f"v{view.id()}" 36 | 37 | 38 | def init_phantom_set(view: sublime.View) -> None: 39 | PhatomSetsManager.init_phantom_set(view, get_phantom_set_id(view), PLUGIN_NAME) 40 | 41 | 42 | def delete_phantom_set(view: sublime.View) -> None: 43 | PhatomSetsManager.delete_phantom_set(get_phantom_set_id(view)) 44 | 45 | 46 | def erase_phantom_set(view: sublime.View) -> None: 47 | PhatomSetsManager.erase_phantom_set(get_phantom_set_id(view)) 48 | 49 | 50 | def update_phantom_set(view: sublime.View, uri_regions: Iterable[sublime.Region]) -> None: 51 | PhatomSetsManager.update_phantom_set(get_phantom_set_id(view), new_uri_phantoms(view, uri_regions)) 52 | 53 | 54 | def generate_phantom_html(view: sublime.View, uri_region: sublime.Region) -> str: 55 | img: ImageDict = global_get("images.phantom") 56 | 57 | return PHANTOM_TEMPLATE.format( 58 | uri=sublime.html_format_command(view.substr(uri_region)), 59 | mime=img["mime"], 60 | ratio_wh=img["ratio_wh"], 61 | base64=get_colored_image_base64_by_region("phantom", uri_region), 62 | ) 63 | 64 | 65 | def new_uri_phantom(view: sublime.View, uri_region: sublime.Region) -> sublime.Phantom: 66 | return sublime.Phantom( 67 | sublime.Region(uri_region.end()), 68 | generate_phantom_html(view, uri_region), 69 | layout=sublime.LAYOUT_INLINE, 70 | on_navigate=open_uri_with_browser, 71 | ) 72 | 73 | 74 | def new_uri_phantoms(view: sublime.View, uri_regions: Iterable[sublime.Region]) -> tuple[sublime.Phantom, ...]: 75 | return tuple(new_uri_phantom(view, r) for r in uri_regions) 76 | -------------------------------------------------------------------------------- /plugin/ui/phatom_sets_manager.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Iterable 4 | 5 | import sublime 6 | 7 | 8 | class PhatomSetsManager: 9 | # class-level (shared across objects) 10 | _phantom_sets: dict[str, sublime.PhantomSet] = { 11 | # phantom_set_id: PhantomSet object, 12 | } 13 | 14 | @classmethod 15 | def get_phantom_set(cls, phantom_set_id: str) -> sublime.PhantomSet | None: 16 | return cls._phantom_sets.get(phantom_set_id) 17 | 18 | @classmethod 19 | def init_phantom_set(cls, view: sublime.View, phantom_set_id: str, phantom_set_key: str = "") -> None: 20 | cls._phantom_sets[phantom_set_id] = sublime.PhantomSet(view, phantom_set_key) 21 | 22 | @classmethod 23 | def delete_phantom_set(cls, phantom_set_id: str) -> None: 24 | cls._phantom_sets.pop(phantom_set_id, None) 25 | 26 | @classmethod 27 | def erase_phantom_set(cls, phantom_set_id: str) -> None: 28 | if phantom_set_id in cls._phantom_sets: 29 | cls._phantom_sets[phantom_set_id].update(tuple()) 30 | 31 | @classmethod 32 | def update_phantom_set(cls, phantom_set_id: str, phantoms: Iterable[sublime.Phantom]) -> None: 33 | if phantom_set_id in cls._phantom_sets: 34 | cls._phantom_sets[phantom_set_id].update(tuple(phantoms)) 35 | 36 | @classmethod 37 | def clear(cls) -> None: 38 | for phantom_set_id in tuple(cls._phantom_sets.keys()): 39 | cls.delete_phantom_set(phantom_set_id) 40 | cls._phantom_sets = {} 41 | -------------------------------------------------------------------------------- /plugin/ui/popup.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import sublime 4 | 5 | from ..helpers import open_uri_with_browser 6 | from ..settings import get_setting 7 | from ..shared import global_get 8 | from ..types import ImageDict 9 | from .image import get_colored_image_base64_by_region 10 | 11 | POPUP_TEMPLATE = """ 12 | 13 | 19 | 20 | {text_html} 21 | 22 | """ 23 | 24 | 25 | def generate_popup_html(view: sublime.View, uri_region: sublime.Region) -> str: 26 | img: ImageDict = global_get("images.popup") 27 | base_size = 2.5 28 | 29 | return POPUP_TEMPLATE.format( 30 | uri=sublime.html_format_command(view.substr(uri_region)), 31 | mime=img["mime"], 32 | w=base_size * img["ratio_wh"], 33 | h=base_size, 34 | size_unit="em", 35 | base64=get_colored_image_base64_by_region("popup", uri_region), 36 | text_html=get_setting("popup_text_html"), 37 | ) 38 | 39 | 40 | def show_popup(view: sublime.View, uri_region: sublime.Region, point: int) -> None: 41 | view.show_popup( 42 | generate_popup_html(view, uri_region), 43 | flags=sublime.COOPERATE_WITH_AUTO_COMPLETE | sublime.HIDE_ON_MOUSE_MOVE_AWAY, 44 | location=point, 45 | max_width=500, 46 | on_navigate=open_uri_with_browser, 47 | ) 48 | -------------------------------------------------------------------------------- /plugin/ui/region_drawing.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | from collections.abc import Iterable, Sequence 4 | from functools import reduce 5 | from operator import xor 6 | 7 | import sublime 8 | 9 | from ..constants import URI_REGION_KEY 10 | from ..settings import get_setting 11 | 12 | 13 | def erase_uri_regions(view: sublime.View) -> None: 14 | view.erase_regions(URI_REGION_KEY) 15 | 16 | 17 | def draw_uri_regions(view: sublime.View, uri_regions: Iterable[sublime.Region]) -> None: 18 | draw_uri_regions = get_setting("draw_uri_regions") 19 | 20 | view.add_regions( 21 | URI_REGION_KEY, 22 | tuple(uri_regions), 23 | scope=draw_uri_regions["scope"], 24 | icon=draw_uri_regions["icon"], 25 | flags=parse_draw_region_flags(draw_uri_regions["flags"]), 26 | ) 27 | 28 | 29 | def parse_draw_region_flags(flags: int | Sequence[str]) -> int: 30 | if isinstance(flags, int): 31 | return flags 32 | 33 | return reduce(xor, map(lambda flag: getattr(sublime, flag, 0), flags), 0) 34 | -------------------------------------------------------------------------------- /plugin/utils.py: -------------------------------------------------------------------------------- 1 | from __future__ import annotations 2 | 3 | import itertools 4 | import time 5 | from collections.abc import Callable, Generator, Iterable, Sequence 6 | from typing import Any, Pattern, cast, overload 7 | 8 | import sublime 9 | from more_itertools import first_true 10 | 11 | from .constants import VIEW_SETTING_TIMESTAMP_KEY 12 | from .types import RegionLike, T_AnyCallable 13 | 14 | 15 | def get_timestamp() -> float: 16 | return time.time() 17 | 18 | 19 | def list_all_views(*, include_transient: bool = False) -> Generator[sublime.View, None, None]: 20 | for window in sublime.windows(): 21 | yield from window.views(include_transient=include_transient) 22 | 23 | 24 | def list_background_views() -> Generator[sublime.View, None, None]: 25 | foreground_views = set(list_foreground_views()) 26 | for view in list_all_views(include_transient=True): 27 | if view not in foreground_views: 28 | yield view 29 | 30 | 31 | def list_foreground_views() -> Generator[sublime.View, None, None]: 32 | for window in sublime.windows(): 33 | for group_idx in range(window.num_groups()): 34 | if view := window.active_view_in_group(group_idx): 35 | yield view 36 | 37 | 38 | def simple_decorator(decorator: Callable) -> Callable[[T_AnyCallable], T_AnyCallable]: 39 | """A decorator that turns a function into a decorator.""" 40 | 41 | def wrapper(decoratee: T_AnyCallable) -> T_AnyCallable: 42 | def wrapped(*args, **kwargs) -> Any: 43 | return decorator(decoratee(*args, **kwargs)) 44 | 45 | return cast(T_AnyCallable, wrapped) 46 | 47 | return wrapper 48 | 49 | 50 | def dotted_get(var: Any, dotted: str, default: Any | None = None) -> Any: 51 | """ 52 | @brief Get the value from the variable with dotted notation. 53 | 54 | @param var The variable 55 | @param dotted The dotted 56 | @param default The default 57 | 58 | @return The value or the default if dotted not found 59 | """ 60 | keys = dotted.split(".") 61 | 62 | try: 63 | for key in keys: 64 | if isinstance(var, (dict, sublime.Settings)): 65 | var = var.get(key) 66 | elif isinstance(var, (list, tuple, bytes, bytearray)): 67 | var = var[int(key)] 68 | else: 69 | var = getattr(var, key) 70 | 71 | return var 72 | except Exception: 73 | return default 74 | 75 | 76 | def dotted_set(var: Any, dotted: str, value: Any) -> None: 77 | """ 78 | @brief Set the value for the variable with dotted notation. 79 | 80 | @param var The variable 81 | @param dotted The dotted 82 | @param default The default 83 | """ 84 | keys = dotted.split(".") 85 | last_key = keys.pop() 86 | 87 | for key in keys: 88 | if isinstance(var, (dict, sublime.Settings)): 89 | var = var.get(key) 90 | elif isinstance(var, (list, tuple, bytes, bytearray)): 91 | var = var[int(key)] 92 | else: 93 | var = getattr(var, key) 94 | 95 | if isinstance(var, (dict, sublime.Settings)): 96 | var[last_key] = value # type: ignore 97 | elif isinstance(var, (list, tuple, bytes, bytearray)): 98 | var[int(last_key)] = value # type: ignore 99 | else: 100 | setattr(var, last_key, value) 101 | 102 | 103 | def view_find_all( 104 | view: sublime.View, 105 | regex_obj: Pattern[str], 106 | expand_selectors: Iterable[str] = tuple(), 107 | ) -> Generator[sublime.Region, None, None]: 108 | """ 109 | @brief Find all content matching the regex and expand found regions with selectors. 110 | 111 | @param view the View object 112 | @param regex_obj the compiled regex object 113 | @param expand_selector the selectors used to expand found regions 114 | 115 | @return A generator for found regions 116 | """ 117 | 118 | def expand(region: sublime.Region) -> sublime.Region: 119 | return first_true((view.expand_to_scope(region.a, selector) for selector in expand_selectors), region) # type: ignore 120 | 121 | if isinstance(expand_selectors, str): 122 | expand_selectors = (expand_selectors,) 123 | 124 | for m in regex_obj.finditer(view.substr(sublime.Region(0, len(view)))): 125 | yield expand(sublime.Region(*m.span())) 126 | 127 | 128 | @overload 129 | def view_last_typing_timestamp_val(view: sublime.View) -> float: ... 130 | @overload 131 | def view_last_typing_timestamp_val(view: sublime.View, timestamp_s: float) -> None: ... 132 | def view_last_typing_timestamp_val(view: sublime.View, timestamp_s: float | None = None) -> float | None: 133 | """ 134 | @brief Set/Get the last timestamp (in sec) when "OUIB_uri_regions" is updated 135 | 136 | @param view The view 137 | @param timestamp_s The last timestamp (in sec) 138 | 139 | @return None if the set mode, otherwise the value 140 | """ 141 | if timestamp_s is None: 142 | return float(view.settings().get(VIEW_SETTING_TIMESTAMP_KEY, 0)) 143 | 144 | view.settings().set(VIEW_SETTING_TIMESTAMP_KEY, timestamp_s) 145 | return None 146 | 147 | 148 | @overload 149 | def view_is_dirty_val(view: sublime.View) -> bool: ... 150 | 151 | 152 | @overload 153 | def view_is_dirty_val(view: sublime.View, is_dirty: bool) -> None: ... 154 | 155 | 156 | def view_is_dirty_val(view: sublime.View, is_dirty: bool | None = None) -> bool | None: 157 | """ 158 | @brief Set/Get the is_dirty of the current view 159 | 160 | @param view The view 161 | @param is_dirty Indicates if dirty 162 | 163 | @return None if the set mode, otherwise the is_dirty 164 | """ 165 | if is_dirty is None: 166 | return bool(view.settings().get("OUIB_is_dirty", True)) 167 | 168 | view.settings().set("OUIB_is_dirty", is_dirty) 169 | return None 170 | 171 | 172 | @overload 173 | def region_shift(region: sublime.Region, shift: int) -> sublime.Region: ... 174 | @overload 175 | def region_shift(region: int | list[int] | tuple[int, int], shift: int) -> tuple[int, int]: ... 176 | def region_shift(region: RegionLike, shift: int) -> tuple[int, int] | sublime.Region: 177 | """ 178 | @brief Shift the region by given amount. 179 | 180 | @param region The region 181 | @param shift The shift 182 | 183 | @return the shifted region 184 | """ 185 | if isinstance(region, int): 186 | return (region + shift, region + shift) 187 | 188 | if isinstance(region, sublime.Region): 189 | return sublime.Region(region.a + shift, region.b + shift) 190 | 191 | return (region[0] + shift, region[-1] + shift) 192 | 193 | 194 | @overload 195 | def region_expand( 196 | region: sublime.Region, 197 | expansion: int | list[int] | tuple[int, int], 198 | ) -> sublime.Region: ... 199 | @overload 200 | def region_expand( 201 | region: int | list[int] | tuple[int, int], 202 | expansion: int | list[int] | tuple[int, int], 203 | ) -> tuple[int, int]: ... 204 | def region_expand( 205 | region: RegionLike, 206 | expansion: int | list[int] | tuple[int, int], 207 | ) -> tuple[int, int] | sublime.Region: 208 | """ 209 | @brief Expand the region by given amount. 210 | 211 | @param region The region 212 | @param expansion The amount of left/right expansion 213 | 214 | @return the expanded region 215 | """ 216 | if isinstance(expansion, int): 217 | expansion = (expansion, expansion) 218 | 219 | if isinstance(region, int): 220 | return (region - expansion[0], region + expansion[1]) 221 | 222 | if isinstance(region, sublime.Region): 223 | return sublime.Region(region.a - expansion[0], region.b + expansion[1]) 224 | 225 | return (region[0] - expansion[0], region[-1] + expansion[-1]) 226 | 227 | 228 | def convert_to_region_tuple(region: RegionLike, sort: bool = False) -> tuple[int, int]: 229 | """ 230 | @brief Convert the "region" into its tuple form 231 | 232 | @param region The region 233 | @param sort Sort the region 234 | 235 | @return the "region" in tuple form 236 | """ 237 | seq: Sequence[int] 238 | 239 | if isinstance(region, sublime.Region): 240 | seq = region.to_tuple() 241 | elif isinstance(region, int): 242 | seq = (region, region) 243 | elif isinstance(region, Iterable): 244 | seq = tuple(itertools.islice(region, 2)) 245 | 246 | if sort: 247 | seq = sorted(seq) 248 | 249 | return (seq[0], seq[-1]) 250 | 251 | 252 | def convert_to_st_region(region: RegionLike, sort: bool = False) -> sublime.Region: 253 | """ 254 | @brief Convert the "region" into its ST region form 255 | 256 | @param region The region 257 | @param sort Sort the region 258 | 259 | @return the "region" in ST's region form 260 | """ 261 | return sublime.Region(*convert_to_region_tuple(region, sort)) 262 | 263 | 264 | def merge_regions(regions: Iterable[sublime.Region], allow_boundary: bool = False) -> list[sublime.Region]: 265 | """ 266 | @brief Merge intersected regions to reduce numbers of regions. 267 | 268 | @param regions The regions, whose `region.a <= region.b` 269 | @param allow_boundary Treat boundary contact as intersected 270 | 271 | @return Merged regions 272 | """ 273 | merged_regions: list[sublime.Region] = [] 274 | for region in sorted(regions): 275 | if not merged_regions: 276 | merged_regions.append(region) 277 | continue 278 | 279 | if is_regions_intersected(merged_regions[-1], region, allow_boundary): 280 | merged_regions[-1].b = region.b 281 | else: 282 | merged_regions.append(region) 283 | 284 | return merged_regions 285 | 286 | 287 | def is_regions_intersected(region1: sublime.Region, region2: sublime.Region, allow_boundary: bool = False) -> bool: 288 | """ 289 | @brief Determinates whether two regions are intersected. 290 | 291 | @param region1 The 1st region 292 | @param region2 The 2nd region 293 | @param allow_boundary Treat boundary contact as intersected 294 | 295 | @return True if intersected, False otherwise. 296 | """ 297 | return region1.intersects(region2) or bool(allow_boundary and {*region1.to_tuple()} & {*region2.to_tuple()}) 298 | 299 | 300 | def is_processable_view(view: sublime.View) -> bool: 301 | return view.is_valid() and not view.is_loading() and not view.element() 302 | 303 | 304 | def is_transient_view(view: sublime.View) -> bool: 305 | return sheet.is_transient() if view.is_valid() and (sheet := view.sheet()) else False 306 | --------------------------------------------------------------------------------