├── images ├── promo.png ├── logo128.png ├── logo16.png ├── logo32.png ├── logo48.png ├── logo512.png ├── logo64.png ├── marquee.png ├── screenshot.png ├── instructions.png ├── large-promo.png └── submission-screenshot.png ├── .env.example ├── .gitignore ├── .github ├── FUNDING.yml └── scripts │ ├── run-firefox.sh │ ├── sign-firefox.sh │ └── build.sh ├── Makefile ├── CONTRIBUTING.md ├── LICENSE ├── manifest-v2.json ├── manifest.json ├── README.md ├── vendor └── showdown.min.js └── script.js /images/promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/promo.png -------------------------------------------------------------------------------- /images/logo128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/logo128.png -------------------------------------------------------------------------------- /images/logo16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/logo16.png -------------------------------------------------------------------------------- /images/logo32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/logo32.png -------------------------------------------------------------------------------- /images/logo48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/logo48.png -------------------------------------------------------------------------------- /images/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/logo512.png -------------------------------------------------------------------------------- /images/logo64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/logo64.png -------------------------------------------------------------------------------- /images/marquee.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/marquee.png -------------------------------------------------------------------------------- /images/screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/screenshot.png -------------------------------------------------------------------------------- /images/instructions.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/instructions.png -------------------------------------------------------------------------------- /images/large-promo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/large-promo.png -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Mozilla API Key 2 | JWT_USER=user:YOUR_USER_ID 3 | 4 | # Mozilla API Secret 5 | JWT_SECRET=YOUR_SECRET 6 | -------------------------------------------------------------------------------- /images/submission-screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/HEAD/images/submission-screenshot.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Generated files 2 | web-ext-artifacts/ 3 | .web-extension-id 4 | dist/ 5 | *.zip 6 | 7 | # Environment 8 | .env 9 | 10 | # IDE 11 | .vscode 12 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [DenverCoder1] 2 | patreon: 3 | open_collective: 4 | ko_fi: 5 | tidelift: 6 | community_bridge: 7 | liberapay: 8 | issuehunt: 9 | otechie: 10 | custom: 11 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | include .env 2 | 3 | build: 4 | bash ./.github/scripts/build.sh 5 | 6 | run-firefox: 7 | bash ./.github/scripts/run-firefox.sh 8 | 9 | sign-firefox: 10 | export JWT_USER=$(JWT_USER) && \ 11 | export JWT_SECRET=$(JWT_SECRET) && \ 12 | bash ./.github/scripts/sign-firefox.sh 13 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Contributions are welcome! Feel free to open an issue or submit a pull request if you have a way to improve this project. 4 | 5 | If you are making a significant change, please open an issue before creating a pull request. This will allow us to discuss the design and implementation. 6 | 7 | Make sure your request is meaningful and you have tested the app locally before submitting a pull request. 8 | 9 | ## Installation 10 | 11 | See the [Installation](https://github.com/DenverCoder1/Unedit-for-Reddit/blob/master/README.md#installation) section of the README for multiple ways to install the script. 12 | 13 | Using a userscript editor or by installing the script as an unpacked extension in development mode, you should be able to test your changes locally. 14 | -------------------------------------------------------------------------------- /.github/scripts/run-firefox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | setup() { 4 | # Ensure files to move exist 5 | if [ ! -f manifest-v2.json ] || [ ! -f manifest.json ]; then 6 | echo "manifest-v2.json or manifest.json was not found. Please run this script from the root of the extension." 7 | exit 1 8 | fi 9 | # Copy the v2 manifest to use instead of the v3 manifest 10 | mv manifest.json manifest-v3.json && mv manifest-v2.json manifest.json 11 | # Run cleanup in case of Ctrl+C 12 | trap cleanup SIGINT 13 | } 14 | 15 | cleanup() { 16 | # Ensure files to move exist 17 | if [ ! -f manifest-v3.json ] || [ ! -f manifest.json ]; then 18 | echo "manifest-v3.json or manifest.json was not found. Please run this script from the root of the extension." 19 | exit 1 20 | fi 21 | # Copy the v3 manifest back to the v2 manifest 22 | mv manifest.json manifest-v2.json && mv manifest-v3.json manifest.json 23 | } 24 | 25 | run() { 26 | # Run extension in Firefox development mode 27 | web-ext run 28 | } 29 | 30 | setup 31 | run 32 | cleanup 33 | -------------------------------------------------------------------------------- /.github/scripts/sign-firefox.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | setup() { 4 | # Ensure files to move exist 5 | if [ ! -f manifest-v2.json ] || [ ! -f manifest.json ]; then 6 | echo "manifest-v2.json or manifest.json was not found. Please run this script from the root of the extension." 7 | exit 1 8 | fi 9 | # Copy the v2 manifest to use instead of the v3 manifest 10 | mv manifest.json manifest-v3.json && mv manifest-v2.json manifest.json 11 | # Run cleanup in case of Ctrl+C 12 | trap cleanup SIGINT 13 | } 14 | 15 | cleanup() { 16 | # Ensure files to move exist 17 | if [ ! -f manifest-v3.json ] || [ ! -f manifest.json ]; then 18 | echo "manifest-v3.json or manifest.json was not found. Please run this script from the root of the extension." 19 | exit 1 20 | fi 21 | # Copy the v3 manifest back to the v2 manifest 22 | mv manifest.json manifest-v2.json && mv manifest-v3.json manifest.json 23 | } 24 | 25 | sign() { 26 | # Sign the Firefox extension 27 | web-ext sign --api-key=$JWT_USER --api-secret=$JWT_SECRET 28 | } 29 | 30 | setup 31 | sign 32 | cleanup 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Jonah Lawrence 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 | -------------------------------------------------------------------------------- /.github/scripts/build.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Declare list of files to zip 4 | FILES='images/* vendor/* LICENSE manifest.json README.md script.js' 5 | 6 | # Create directory for zip files 7 | mkdir -p dist 8 | 9 | ############################ 10 | # Chrome Extension Archive # 11 | ############################ 12 | 13 | zip -r -X "dist/chrome-extension.zip" $FILES 14 | 15 | ############################# 16 | # Firefox Extension Archive # 17 | ############################# 18 | 19 | setup() { 20 | # Ensure files to move exist 21 | if [ ! -f manifest-v2.json ] || [ ! -f manifest.json ]; then 22 | echo "manifest-v2.json or manifest.json was not found. Please run this script from the root of the extension." 23 | exit 1 24 | fi 25 | # Copy the v2 manifest to use instead of the v3 manifest 26 | mv manifest.json manifest-v3.json && mv manifest-v2.json manifest.json 27 | # Run cleanup in case of Ctrl+C 28 | trap cleanup SIGINT 29 | } 30 | 31 | cleanup() { 32 | # Ensure files to move exist 33 | if [ ! -f manifest-v3.json ] || [ ! -f manifest.json ]; then 34 | echo "manifest-v3.json or manifest.json was not found. Please run this script from the root of the extension." 35 | exit 1 36 | fi 37 | # Copy the v3 manifest back to the v2 manifest 38 | mv manifest.json manifest-v2.json && mv manifest-v3.json manifest.json 39 | } 40 | 41 | setup 42 | zip -r -X "dist/firefox-extension.zip" $FILES 43 | cleanup 44 | -------------------------------------------------------------------------------- /manifest-v2.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 2, 3 | "name": "Unedit and Undelete for Reddit", 4 | "description": "Show original comments and posts from before they were edited or removed", 5 | "version": "3.17.4", 6 | "content_scripts": [ 7 | { 8 | "run_at": "document_idle", 9 | "matches": [ 10 | "*://*.reddit.com/", 11 | "*://*.reddit.com/me/f/*", 12 | "*://*.reddit.com/message/*", 13 | "*://*.reddit.com/r/*", 14 | "*://*.reddit.com/user/*" 15 | ], 16 | "exclude_matches": [ 17 | "*://*.reddit.com/*/about/banned*", 18 | "*://*.reddit.com/*/about/contributors*", 19 | "*://*.reddit.com/*/about/edit*", 20 | "*://*.reddit.com/*/about/flair*", 21 | "*://*.reddit.com/*/about/log*", 22 | "*://*.reddit.com/*/about/moderators*", 23 | "*://*.reddit.com/*/about/muted*", 24 | "*://*.reddit.com/*/about/rules*", 25 | "*://*.reddit.com/*/about/stylesheet*", 26 | "*://*.reddit.com/*/about/traffic*", 27 | "*://*.reddit.com/*/wiki/*", 28 | "*://mod.reddit.com/*" 29 | ], 30 | "js": ["vendor/showdown.min.js", "script.js"] 31 | } 32 | ], 33 | "web_accessible_resources": ["vendor/showdown.min.js.map"], 34 | "page_action": { 35 | "default_icon": "images/logo64.png", 36 | "default_title": "Unedit and Undelete for Reddit" 37 | }, 38 | "icons": { 39 | "16": "images/logo16.png", 40 | "32": "images/logo32.png", 41 | "48": "images/logo48.png", 42 | "128": "images/logo128.png" 43 | }, 44 | "permissions": [ 45 | "*://*.pushshift.io/*", 46 | "storage" 47 | ] 48 | } 49 | -------------------------------------------------------------------------------- /manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Unedit and Undelete for Reddit", 4 | "description": "Show original comments and posts from before they were edited or removed", 5 | "version": "3.17.4", 6 | "content_scripts": [ 7 | { 8 | "run_at": "document_idle", 9 | "matches": [ 10 | "*://*.reddit.com/", 11 | "*://*.reddit.com/me/f/*", 12 | "*://*.reddit.com/message/*", 13 | "*://*.reddit.com/r/*", 14 | "*://*.reddit.com/user/*" 15 | ], 16 | "exclude_matches": [ 17 | "*://*.reddit.com/*/about/banned*", 18 | "*://*.reddit.com/*/about/contributors*", 19 | "*://*.reddit.com/*/about/edit*", 20 | "*://*.reddit.com/*/about/flair*", 21 | "*://*.reddit.com/*/about/log*", 22 | "*://*.reddit.com/*/about/moderators*", 23 | "*://*.reddit.com/*/about/muted*", 24 | "*://*.reddit.com/*/about/rules*", 25 | "*://*.reddit.com/*/about/stylesheet*", 26 | "*://*.reddit.com/*/about/traffic*", 27 | "*://*.reddit.com/*/wiki/*", 28 | "*://mod.reddit.com/*" 29 | ], 30 | "js": ["vendor/showdown.min.js", "script.js"] 31 | } 32 | ], 33 | "web_accessible_resources": [ 34 | { 35 | "matches": ["*://*.reddit.com/*"], 36 | "resources": ["vendor/showdown.min.js.map"] 37 | } 38 | ], 39 | "action": { 40 | "default_icon": "images/logo64.png", 41 | "default_title": "Unedit and Undelete for Reddit" 42 | }, 43 | "icons": { 44 | "16": "images/logo16.png", 45 | "32": "images/logo32.png", 46 | "48": "images/logo48.png", 47 | "128": "images/logo128.png" 48 | }, 49 | "host_permissions": ["https://api.pushshift.io/reddit/search/*"], 50 | "permissions": ["storage"] 51 | } 52 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

3 | 4 |

Unedit and Undelete for Reddit

5 | 6 |

7 | 8 | Dev Pro Tips Discussion & Support Server 9 | 10 | Issues 11 | 12 | Greasy Fork users 13 | 14 | Chrome Web Store users 15 | 16 | Firefox users 17 |

18 |
19 | 20 | > **Warning** 21 | > 22 | > Unedit and Undelete for Reddit relies on Pushshift to work. 23 | > 24 | > At this time, Pushshift is only available to approved moderators as announced [here](https://redd.it/14ei799). 25 | > 26 | > Checking [r/pushshift](https://www.reddit.com/r/pushshift) for updates is recommended. 27 | > 28 | > June 22, 2023 29 | 30 | Creates a link next to edited and deleted Reddit comments and submissions to show the original post from before it was edited/removed. 31 | 32 | The unedited comment will be displayed inline, right below the current comment or submission's text. 33 | 34 | This script is compatible with both Reddit's Redesign and Old Reddit. 35 | 36 | The [Pushshift Reddit API](https://github.com/pushshift/api) is used for fetching the comments as they will be archived soon after they have been posted. 37 | 38 | ## How to use 39 | 40 | ![instructions](https://user-images.githubusercontent.com/20955511/172483035-90eff88d-4b7d-416a-951d-001c96299476.png) 41 | 42 | ## Installation 43 | 44 | [![Install with Greasy Fork](https://user-images.githubusercontent.com/20955511/201193167-934b9e7b-66b2-4b6b-84e3-d8a677c66da4.png)](https://greasyfork.org/en/scripts/407466-unedit-and-undelete-for-reddit) 45 | [![Available in the Chrome Web Store](https://user-images.githubusercontent.com/20955511/201192698-df2474d7-83e8-429f-a4a5-d590ff1bfb5b.png)](https://chrome.google.com/webstore/detail/unedit-and-undelete-for-r/cnpmnmpafbfojcoofaobmhmafiflgmka) 46 | [![Firefox Get the Add-on](https://user-images.githubusercontent.com/20955511/172904059-eb121557-ef91-43a6-a5f6-f4be5e20a5dc.png)](https://addons.mozilla.org/en-US/firefox/addon/unedit-for-reddit/) 47 | 48 | ### As a Userscript 49 | 50 | This script can be installed on most browsers using userscript browser extensions such as [Violentmonkey](https://violentmonkey.github.io/), [Tampermonkey](https://www.tampermonkey.net/), among others using the green button on [Greasy Fork](https://greasyfork.org/en/scripts/407466-unedit-and-undelete-for-reddit). 51 | 52 | Alternatively, you may copy the contents of [`script.js`](https://github.com/DenverCoder1/Unedit-for-Reddit/blob/master/script.js) into a new script using any userscript browser extension. 53 | 54 | > **Note**: Some userscript engines such as Bromite and base Chromium do not support `@require` (see [here](https://www.chromium.org/developers/design-documents/user-scripts/)). In order to get this userscript to work without a dedicated extension, you will need to copy-paste the contents of [`showdown.min.js`](https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js) at the top of the script, just below the `// ==/UserScript==` line. 55 | 56 | ### As a Chrome Extension 57 | 58 | Install from the [Chrome Web Store](https://chrome.google.com/webstore/detail/unedit-and-undelete-for-r/cnpmnmpafbfojcoofaobmhmafiflgmka), or alternatively, download or clone this repository, enable "Developer mode" at , and load the folder unpacked. 59 | 60 | ### As a Firefox Addon 61 | 62 | Install from [Mozilla Add-ons](https://addons.mozilla.org/en-US/firefox/addon/unedit-for-reddit/), or alternatively, follow these steps to build and install from the source: 63 | 64 | To sign the extension for use in Firefox, you will need credentials from https://addons.mozilla.org/en-US/developers/addon/api/key/. Create a copy of `.env.example` named `.env` and replace the placeholders with your API key and secret. Install `web-ext` with `npm install -g web-ext` and sign the extension with `make sign-firefox`. The generated extension will appear as a `.xpi` file in `./web-ext-artifacts`. This file can be opened in Firefox to install the add-on. 65 | 66 | ## Known issues 67 | 68 | The following are known limitations that cannot be fixed: 69 | 70 | - The fetched comment may occasionally be the edited version instead of the original. This is because the Pushshift archive may take more time to archive the comment than it took the user to edit the comment, therefore causing Pushshift to archive the edited version and not the original. Additionally, comments that are several years old may also show the edited version since the original versions of comments edited before the first archival will not appear in Pushshift. 71 | - Comments that were posted within the past few minutes may occasionally show "not found" since Pushshift can take some time to archive all comments. 72 | - Comments in private subreddits will show "not found" as they are not able to be archived by Pushshift. 73 | - Comments deleted by Reddit's spam filter will show "not found" as the text is never available for Pushshift to archive. 74 | - Comments and posts by users who have requested the deletion of the data from Pushshift will show as "not found". 75 | - If the Pushshift API is temporarily down or partially down, the message "fetch failed" may appear. 76 | 77 | ## Changelog 78 | 79 | ### Changes in 3.17.4 80 | 81 | - Refactored storage interface to use `browser.storage.local` or `chrome.storage.local` if available 82 | 83 | ### Changes in 3.17.3 84 | 85 | - Make inline fetch fix only apply to Firefox 86 | - Store hidden state of token container so it is hidden if previously closed (will reopen if token is used and is invalid) 87 | 88 | ### Changes in 3.17.2 89 | 90 | - ~~Fixed an unsafe inline issue in Chrome from previous version (not released on Chrome)~~ 91 | 92 | ### Changes in 3.17.1 93 | 94 | - Fixed issue with headers not being passed when using Firefox 95 | - Added explicit variable declaration that was previously implicit 96 | - Minor refactoring 97 | 98 | ### Changes in 3.17.0 99 | 100 | - Added support for Pushshift API token authentication 101 | 102 | ### Changes in 3.16.4 103 | 104 | - Added link to r/pushshift issue post in fetch failed message 105 | - Added rounded corners for the original comment box 106 | 107 | ### Changes in 3.16.3 108 | 109 | - Added support for `[ Removed by Reddit ]` comments in moderator view 110 | - Added icon to Userscript metadata 111 | - Added more error handling for Pushshift API issues 112 | - Prevent class names from being added to user flairs on old Reddit 113 | 114 | ### Changes in 3.16.2 115 | 116 | - Updated manifest v2 permissions list for Firefox 109 support 117 | - Added version number to startup logs 118 | - Remove title text from appearing in parentheses for RES users 119 | 120 | ### Changes in 3.16.1 121 | 122 | - Removed sort order parameters due to recent breaking changes in the Pushshift API 123 | - Added title text to show original links and error links to show additional information when hovering over them 124 | 125 | ### Changes in 3.16.0 126 | 127 | - Support for .compact Reddit layout 128 | - Show original links will not be shown if the post ID could not be determined 129 | 130 | ### Changes in 3.15.1 131 | 132 | - Added some URL patterns to be excluded from running the script 133 | 134 | ### Changes in 3.15.0 135 | 136 | - Added support for comments not shown in the comment tree if the link is visited directly. 137 | - Added missing `showdown.min.js.map` file to `/vendor` to avoid Source Map error (Browser Extension only). 138 | - Avoids sending queries with the author being undefined, and checks the parent post if the author is not known. 139 | - Ensure initialization runs even if the window finishes loading before the extension is loaded. 140 | 141 | ### Changes in 3.14.0 142 | 143 | - Added support for `[unavailable]` comments and submissions (occurs when the user has been blocked by the author). 144 | 145 | ### Changes in 3.13.0 146 | 147 | - Added support for Classic view and Compact view on Reddit Redesign. 148 | - Adjusted the font size of links on the search results page to be smaller (Reddit Redesign). 149 | 150 | ### Changes in 3.12.1 151 | 152 | - Hide link on edited submissions in Classic View since it does not display well (Reddit Redesign) 153 | 154 | ### Changes in 3.12.0 155 | 156 | - Added support to show the author of posts that were not edited but the author no longer exists 157 | - Added support for edited submissions on comment context pages 158 | - Ensure the original post fills the available space on Reddit Redesign 159 | 160 | ### Changes in 3.11.0 161 | 162 | - Fix markdown conversion to include strikethrough, Github-style code blocks, indented sublists without 4 spaces, and underscores in the middle of a word 163 | - Added links wherever username and subreddit mentions appear in the original post 164 | - Added support for hiding spoiler text in the original post 165 | - Added support for superscripts using `^` in the original post 166 | - Fixed the search results page on Reddit Redesign to only show the link once 167 | - Fixed fetching edited submissions sometimes failing 168 | 169 | ### Changes in 3.10.0 170 | 171 | - Added color overrides for compatibility with RES Night Mode and more custom CSS themes on Old Reddit 172 | - Added extra spacing around paragraphs and headings in the original comment 173 | - Improved styling of code blocks in the original comment on Reddit Redesign 174 | - Added support for displaying tables in the original comment 175 | 176 | ### Changes in 3.9.5 177 | 178 | - Updated @​match for better compatibility 179 | 180 | ### Changes in 3.9.4 181 | 182 | - Added the author and created time at the bottom of the original comment. This helps to find out who posted a deleted comment. 183 | - Fix showing of empty self-text of a post when it was empty and then edited to have text 184 | - Updates to the color of links in the original post on Redesign to be easier to read 185 | - Prevents the original post from being displayed more than once if "show original" links are clicked multiple times 186 | 187 | ### Changes in 3.9.3 188 | 189 | - Fix bug where the submission edited time appears more than once 190 | 191 | ### Changes in 3.9.2 192 | 193 | - Fixed edited submissions when not using Reddit Enhancement Suite (RES is not a requirement) 194 | 195 | ### Changes in 3.9.1 196 | 197 | - Support edited submissions in Firefox by using regex instead of `URLPattern` 198 | 199 | ### Changes in 3.9.0 200 | 201 | - Support for edited submissions on Reddit Redesign on submission pages, list view, and popup view 202 | - Displays how long ago submissions were edited on Redesign since Reddit doesn't display this information 203 | - Minor code refactoring and added comments 204 | 205 | ### Changes in 3.8.0 206 | 207 | - Added support for viewing deleted and moderator-removed submissions on Redesign and Old Reddit 208 | - Added support for viewing edited submissions in list views (Old Reddit only) 209 | - Better error handling and logging 210 | - Changed background and foreground color of original posts to be consistent across all posts and themes 211 | - Made "Original Post" heading bold on Redesign for consistency 212 | 213 | ### Changes in 3.7.3 214 | 215 | - Fix duplicate "Show Original" links appearing inside deleted comments 216 | 217 | ### Changes in 3.7.2 218 | 219 | - Prevent "show original" links from appearing twice on comments that are both edited and deleted 220 | - Expand comment when the "show original" link is clicked on a collapsed comment (Old Reddit only) 221 | - Some minor code refactoring and formatting 222 | 223 | ### Changes in 3.7.1 224 | 225 | - Fixed positioning of original comment on Old Reddit to be inline with text rather than below replies 226 | 227 | ### Changes in 3.7.0 228 | 229 | - Added support for viewing comments removed by moderators 230 | 231 | ### Changes in 3.6.1 232 | 233 | - Better error handling 234 | - More reliable when viewing edited comments on profile pages 235 | 236 | ### Changes in 3.6.0 237 | 238 | - Check a second source for comments so more recent comments are less likely to be "not found" 239 | - Fixes to getting comment ids in Reddit redesign 240 | 241 | ### Changes in 3.5 242 | 243 | - Added compatibility with Old Reddit links without "old.reddit" in the URL 244 | - Added additional @​includes for more compatibility 245 | 246 | ### Changes in 3.4 247 | 248 | - Fixed @​match for more compatibility 249 | 250 | ### Changes in 3.3 251 | 252 | - Added support for profile pages (Redesign) 253 | 254 | ### Changes in 3.2 255 | 256 | - Works more accurately in post previews (Redesign feature where the post is shown in a popup when clicked from post list) 257 | 258 | ### Changes in 3.1 259 | 260 | - Fixed missing styling on comments shown in the Redesign 261 | - Fixed placement of inline comment to work on comments that do not end with a paragraph (`

`) element 262 | 263 | ### Changes in 3.0 264 | 265 | - Added support for deleted comments 266 | 267 | ### Changes in 2.0 268 | 269 | - The original comment is converted from markdown to HTML to show custom formatting 270 | - Support for self-text submissions (old Reddit only) 271 | -------------------------------------------------------------------------------- /vendor/showdown.min.js: -------------------------------------------------------------------------------- 1 | /*! showdown v 2.1.0 - 21-04-2022 */ 2 | !function(){function a(e){"use strict";var r={omitExtraWLInCodeBlocks:{defaultValue:!1,describe:"Omit the default extra whiteline added to code blocks",type:"boolean"},noHeaderId:{defaultValue:!1,describe:"Turn on/off generated header id",type:"boolean"},prefixHeaderId:{defaultValue:!1,describe:"Add a prefix to the generated header ids. Passing a string will prefix that string to the header id. Setting to true will add a generic 'section-' prefix",type:"string"},rawPrefixHeaderId:{defaultValue:!1,describe:'Setting this option to true will prevent showdown from modifying the prefix. This might result in malformed IDs (if, for instance, the " char is used in the prefix)',type:"boolean"},ghCompatibleHeaderId:{defaultValue:!1,describe:"Generate header ids compatible with github style (spaces are replaced with dashes, a bunch of non alphanumeric chars are removed)",type:"boolean"},rawHeaderId:{defaultValue:!1,describe:"Remove only spaces, ' and \" from generated header ids (including prefixes), replacing them with dashes (-). WARNING: This might result in malformed ids",type:"boolean"},headerLevelStart:{defaultValue:!1,describe:"The header blocks level start",type:"integer"},parseImgDimensions:{defaultValue:!1,describe:"Turn on/off image dimension parsing",type:"boolean"},simplifiedAutoLink:{defaultValue:!1,describe:"Turn on/off GFM autolink style",type:"boolean"},excludeTrailingPunctuationFromURLs:{defaultValue:!1,describe:"Excludes trailing punctuation from links generated with autoLinking",type:"boolean"},literalMidWordUnderscores:{defaultValue:!1,describe:"Parse midword underscores as literal underscores",type:"boolean"},literalMidWordAsterisks:{defaultValue:!1,describe:"Parse midword asterisks as literal asterisks",type:"boolean"},strikethrough:{defaultValue:!1,describe:"Turn on/off strikethrough support",type:"boolean"},tables:{defaultValue:!1,describe:"Turn on/off tables support",type:"boolean"},tablesHeaderId:{defaultValue:!1,describe:"Add an id to table headers",type:"boolean"},ghCodeBlocks:{defaultValue:!0,describe:"Turn on/off GFM fenced code blocks support",type:"boolean"},tasklists:{defaultValue:!1,describe:"Turn on/off GFM tasklist support",type:"boolean"},smoothLivePreview:{defaultValue:!1,describe:"Prevents weird effects in live previews due to incomplete input",type:"boolean"},smartIndentationFix:{defaultValue:!1,describe:"Tries to smartly fix indentation in es6 strings",type:"boolean"},disableForced4SpacesIndentedSublists:{defaultValue:!1,describe:"Disables the requirement of indenting nested sublists by 4 spaces",type:"boolean"},simpleLineBreaks:{defaultValue:!1,describe:"Parses simple line breaks as
(GFM Style)",type:"boolean"},requireSpaceBeforeHeadingText:{defaultValue:!1,describe:"Makes adding a space between `#` and the header text mandatory (GFM Style)",type:"boolean"},ghMentions:{defaultValue:!1,describe:"Enables github @mentions",type:"boolean"},ghMentionsLink:{defaultValue:"https://github.com/{u}",describe:"Changes the link generated by @mentions. Only applies if ghMentions option is enabled.",type:"string"},encodeEmails:{defaultValue:!0,describe:"Encode e-mail addresses through the use of Character Entities, transforming ASCII e-mail addresses into its equivalent decimal entities",type:"boolean"},openLinksInNewWindow:{defaultValue:!1,describe:"Open all links in new windows",type:"boolean"},backslashEscapesHTMLTags:{defaultValue:!1,describe:"Support for HTML Tag escaping. ex:

foo
",type:"boolean"},emoji:{defaultValue:!1,describe:"Enable emoji support. Ex: `this is a :smile: emoji`",type:"boolean"},underline:{defaultValue:!1,describe:"Enable support for underline. Syntax is double or triple underscores: `__underline word__`. With this option enabled, underscores no longer parses into `` and ``",type:"boolean"},ellipsis:{defaultValue:!0,describe:"Replaces three dots with the ellipsis unicode character",type:"boolean"},completeHTMLDocument:{defaultValue:!1,describe:"Outputs a complete html document, including ``, `` and `` tags",type:"boolean"},metadata:{defaultValue:!1,describe:"Enable support for document metadata (defined at the top of the document between `«««` and `»»»` or between `---` and `---`).",type:"boolean"},splitAdjacentBlockquotes:{defaultValue:!1,describe:"Split adjacent blockquote blocks",type:"boolean"}};if(!1===e)return JSON.parse(JSON.stringify(r));var t,a={};for(t in r)r.hasOwnProperty(t)&&(a[t]=r[t].defaultValue);return a}var x={},t={},d={},p=a(!0),h="vanilla",_={github:{omitExtraWLInCodeBlocks:!0,simplifiedAutoLink:!0,excludeTrailingPunctuationFromURLs:!0,literalMidWordUnderscores:!0,strikethrough:!0,tables:!0,tablesHeaderId:!0,ghCodeBlocks:!0,tasklists:!0,disableForced4SpacesIndentedSublists:!0,simpleLineBreaks:!0,requireSpaceBeforeHeadingText:!0,ghCompatibleHeaderId:!0,ghMentions:!0,backslashEscapesHTMLTags:!0,emoji:!0,splitAdjacentBlockquotes:!0},original:{noHeaderId:!0,ghCodeBlocks:!1},ghost:{omitExtraWLInCodeBlocks:!0,parseImgDimensions:!0,simplifiedAutoLink:!0,excludeTrailingPunctuationFromURLs:!0,literalMidWordUnderscores:!0,strikethrough:!0,tables:!0,tablesHeaderId:!0,ghCodeBlocks:!0,tasklists:!0,smoothLivePreview:!0,simpleLineBreaks:!0,requireSpaceBeforeHeadingText:!0,ghMentions:!1,encodeEmails:!0},vanilla:a(!0),allOn:function(){"use strict";var e,r=a(!0),t={};for(e in r)r.hasOwnProperty(e)&&(t[e]=!0);return t}()};function g(e,r){"use strict";var t=r?"Error in "+r+" extension->":"Error in unnamed extension",a={valid:!0,error:""};x.helper.isArray(e)||(e=[e]);for(var n=0;n").replace(/&/g,"&")};function u(e,r,t,a){"use strict";var n,s,o,i=-1<(a=a||"").indexOf("g"),l=new RegExp(r+"|"+t,"g"+a.replace(/g/g,"")),c=new RegExp(r,a.replace(/g/g,"")),u=[];do{for(n=0;p=l.exec(e);)if(c.test(p[0]))n++||(o=(s=l.lastIndex)-p[0].length);else if(n&&!--n){var d=p.index+p[0].length,p={left:{start:o,end:s},match:{start:s,end:p.index},right:{start:p.index,end:d},wholeMatch:{start:o,end:d}};if(u.push(p),!i)return u}}while(n&&(l.lastIndex=s));return u}function s(u){"use strict";return function(e,r,t,a,n,s,o){var i=t=t.replace(x.helper.regexes.asteriskDashAndColon,x.helper.escapeCharactersCallback),l="",c="",r=r||"",o=o||"";return/^www\./i.test(t)&&(t=t.replace(/^www\./i,"http://www.")),u.excludeTrailingPunctuationFromURLs&&s&&(l=s),r+'"+i+""+l+o}}function o(n,s){"use strict";return function(e,r,t){var a="mailto:";return r=r||"",t=x.subParser("unescapeSpecialChars")(t,n,s),n.encodeEmails?(a=x.helper.encodeEmailAddress(a+t),t=x.helper.encodeEmailAddress(t)):a+=t,r+''+t+""}}x.helper.matchRecursiveRegExp=function(e,r,t,a){"use strict";for(var n=u(e,r,t,a),s=[],o=0;o>=0,t=String(t||" "),e.length>r?String(e):((r-=e.length)>t.length&&(t+=t.repeat(r/t.length)),String(e)+t.slice(0,r))},"undefined"==typeof console&&(console={warn:function(e){"use strict";alert(e)},log:function(e){"use strict";alert(e)},error:function(e){"use strict";throw e}}),x.helper.regexes={asteriskDashAndColon:/([*_:~])/g},x.helper.emojis={"+1":"👍","-1":"👎",100:"💯",1234:"🔢","1st_place_medal":"🥇","2nd_place_medal":"🥈","3rd_place_medal":"🥉","8ball":"🎱",a:"🅰️",ab:"🆎",abc:"🔤",abcd:"🔡",accept:"🉑",aerial_tramway:"🚡",airplane:"✈️",alarm_clock:"⏰",alembic:"⚗️",alien:"👽",ambulance:"🚑",amphora:"🏺",anchor:"⚓️",angel:"👼",anger:"💢",angry:"😠",anguished:"😧",ant:"🐜",apple:"🍎",aquarius:"♒️",aries:"♈️",arrow_backward:"◀️",arrow_double_down:"⏬",arrow_double_up:"⏫",arrow_down:"⬇️",arrow_down_small:"🔽",arrow_forward:"▶️",arrow_heading_down:"⤵️",arrow_heading_up:"⤴️",arrow_left:"⬅️",arrow_lower_left:"↙️",arrow_lower_right:"↘️",arrow_right:"➡️",arrow_right_hook:"↪️",arrow_up:"⬆️",arrow_up_down:"↕️",arrow_up_small:"🔼",arrow_upper_left:"↖️",arrow_upper_right:"↗️",arrows_clockwise:"🔃",arrows_counterclockwise:"🔄",art:"🎨",articulated_lorry:"🚛",artificial_satellite:"🛰",astonished:"😲",athletic_shoe:"👟",atm:"🏧",atom_symbol:"⚛️",avocado:"🥑",b:"🅱️",baby:"👶",baby_bottle:"🍼",baby_chick:"🐤",baby_symbol:"🚼",back:"🔙",bacon:"🥓",badminton:"🏸",baggage_claim:"🛄",baguette_bread:"🥖",balance_scale:"⚖️",balloon:"🎈",ballot_box:"🗳",ballot_box_with_check:"☑️",bamboo:"🎍",banana:"🍌",bangbang:"‼️",bank:"🏦",bar_chart:"📊",barber:"💈",baseball:"⚾️",basketball:"🏀",basketball_man:"⛹️",basketball_woman:"⛹️‍♀️",bat:"🦇",bath:"🛀",bathtub:"🛁",battery:"🔋",beach_umbrella:"🏖",bear:"🐻",bed:"🛏",bee:"🐝",beer:"🍺",beers:"🍻",beetle:"🐞",beginner:"🔰",bell:"🔔",bellhop_bell:"🛎",bento:"🍱",biking_man:"🚴",bike:"🚲",biking_woman:"🚴‍♀️",bikini:"👙",biohazard:"☣️",bird:"🐦",birthday:"🎂",black_circle:"⚫️",black_flag:"🏴",black_heart:"🖤",black_joker:"🃏",black_large_square:"⬛️",black_medium_small_square:"◾️",black_medium_square:"◼️",black_nib:"✒️",black_small_square:"▪️",black_square_button:"🔲",blonde_man:"👱",blonde_woman:"👱‍♀️",blossom:"🌼",blowfish:"🐡",blue_book:"📘",blue_car:"🚙",blue_heart:"💙",blush:"😊",boar:"🐗",boat:"⛵️",bomb:"💣",book:"📖",bookmark:"🔖",bookmark_tabs:"📑",books:"📚",boom:"💥",boot:"👢",bouquet:"💐",bowing_man:"🙇",bow_and_arrow:"🏹",bowing_woman:"🙇‍♀️",bowling:"🎳",boxing_glove:"🥊",boy:"👦",bread:"🍞",bride_with_veil:"👰",bridge_at_night:"🌉",briefcase:"💼",broken_heart:"💔",bug:"🐛",building_construction:"🏗",bulb:"💡",bullettrain_front:"🚅",bullettrain_side:"🚄",burrito:"🌯",bus:"🚌",business_suit_levitating:"🕴",busstop:"🚏",bust_in_silhouette:"👤",busts_in_silhouette:"👥",butterfly:"🦋",cactus:"🌵",cake:"🍰",calendar:"📆",call_me_hand:"🤙",calling:"📲",camel:"🐫",camera:"📷",camera_flash:"📸",camping:"🏕",cancer:"♋️",candle:"🕯",candy:"🍬",canoe:"🛶",capital_abcd:"🔠",capricorn:"♑️",car:"🚗",card_file_box:"🗃",card_index:"📇",card_index_dividers:"🗂",carousel_horse:"🎠",carrot:"🥕",cat:"🐱",cat2:"🐈",cd:"💿",chains:"⛓",champagne:"🍾",chart:"💹",chart_with_downwards_trend:"📉",chart_with_upwards_trend:"📈",checkered_flag:"🏁",cheese:"🧀",cherries:"🍒",cherry_blossom:"🌸",chestnut:"🌰",chicken:"🐔",children_crossing:"🚸",chipmunk:"🐿",chocolate_bar:"🍫",christmas_tree:"🎄",church:"⛪️",cinema:"🎦",circus_tent:"🎪",city_sunrise:"🌇",city_sunset:"🌆",cityscape:"🏙",cl:"🆑",clamp:"🗜",clap:"👏",clapper:"🎬",classical_building:"🏛",clinking_glasses:"🥂",clipboard:"📋",clock1:"🕐",clock10:"🕙",clock1030:"🕥",clock11:"🕚",clock1130:"🕦",clock12:"🕛",clock1230:"🕧",clock130:"🕜",clock2:"🕑",clock230:"🕝",clock3:"🕒",clock330:"🕞",clock4:"🕓",clock430:"🕟",clock5:"🕔",clock530:"🕠",clock6:"🕕",clock630:"🕡",clock7:"🕖",clock730:"🕢",clock8:"🕗",clock830:"🕣",clock9:"🕘",clock930:"🕤",closed_book:"📕",closed_lock_with_key:"🔐",closed_umbrella:"🌂",cloud:"☁️",cloud_with_lightning:"🌩",cloud_with_lightning_and_rain:"⛈",cloud_with_rain:"🌧",cloud_with_snow:"🌨",clown_face:"🤡",clubs:"♣️",cocktail:"🍸",coffee:"☕️",coffin:"⚰️",cold_sweat:"😰",comet:"☄️",computer:"💻",computer_mouse:"🖱",confetti_ball:"🎊",confounded:"😖",confused:"😕",congratulations:"㊗️",construction:"🚧",construction_worker_man:"👷",construction_worker_woman:"👷‍♀️",control_knobs:"🎛",convenience_store:"🏪",cookie:"🍪",cool:"🆒",policeman:"👮",copyright:"©️",corn:"🌽",couch_and_lamp:"🛋",couple:"👫",couple_with_heart_woman_man:"💑",couple_with_heart_man_man:"👨‍❤️‍👨",couple_with_heart_woman_woman:"👩‍❤️‍👩",couplekiss_man_man:"👨‍❤️‍💋‍👨",couplekiss_man_woman:"💏",couplekiss_woman_woman:"👩‍❤️‍💋‍👩",cow:"🐮",cow2:"🐄",cowboy_hat_face:"🤠",crab:"🦀",crayon:"🖍",credit_card:"💳",crescent_moon:"🌙",cricket:"🏏",crocodile:"🐊",croissant:"🥐",crossed_fingers:"🤞",crossed_flags:"🎌",crossed_swords:"⚔️",crown:"👑",cry:"😢",crying_cat_face:"😿",crystal_ball:"🔮",cucumber:"🥒",cupid:"💘",curly_loop:"➰",currency_exchange:"💱",curry:"🍛",custard:"🍮",customs:"🛃",cyclone:"🌀",dagger:"🗡",dancer:"💃",dancing_women:"👯",dancing_men:"👯‍♂️",dango:"🍡",dark_sunglasses:"🕶",dart:"🎯",dash:"💨",date:"📅",deciduous_tree:"🌳",deer:"🦌",department_store:"🏬",derelict_house:"🏚",desert:"🏜",desert_island:"🏝",desktop_computer:"🖥",male_detective:"🕵️",diamond_shape_with_a_dot_inside:"💠",diamonds:"♦️",disappointed:"😞",disappointed_relieved:"😥",dizzy:"💫",dizzy_face:"😵",do_not_litter:"🚯",dog:"🐶",dog2:"🐕",dollar:"💵",dolls:"🎎",dolphin:"🐬",door:"🚪",doughnut:"🍩",dove:"🕊",dragon:"🐉",dragon_face:"🐲",dress:"👗",dromedary_camel:"🐪",drooling_face:"🤤",droplet:"💧",drum:"🥁",duck:"🦆",dvd:"📀","e-mail":"📧",eagle:"🦅",ear:"👂",ear_of_rice:"🌾",earth_africa:"🌍",earth_americas:"🌎",earth_asia:"🌏",egg:"🥚",eggplant:"🍆",eight_pointed_black_star:"✴️",eight_spoked_asterisk:"✳️",electric_plug:"🔌",elephant:"🐘",email:"✉️",end:"🔚",envelope_with_arrow:"📩",euro:"💶",european_castle:"🏰",european_post_office:"🏤",evergreen_tree:"🌲",exclamation:"❗️",expressionless:"😑",eye:"👁",eye_speech_bubble:"👁‍🗨",eyeglasses:"👓",eyes:"👀",face_with_head_bandage:"🤕",face_with_thermometer:"🤒",fist_oncoming:"👊",factory:"🏭",fallen_leaf:"🍂",family_man_woman_boy:"👪",family_man_boy:"👨‍👦",family_man_boy_boy:"👨‍👦‍👦",family_man_girl:"👨‍👧",family_man_girl_boy:"👨‍👧‍👦",family_man_girl_girl:"👨‍👧‍👧",family_man_man_boy:"👨‍👨‍👦",family_man_man_boy_boy:"👨‍👨‍👦‍👦",family_man_man_girl:"👨‍👨‍👧",family_man_man_girl_boy:"👨‍👨‍👧‍👦",family_man_man_girl_girl:"👨‍👨‍👧‍👧",family_man_woman_boy_boy:"👨‍👩‍👦‍👦",family_man_woman_girl:"👨‍👩‍👧",family_man_woman_girl_boy:"👨‍👩‍👧‍👦",family_man_woman_girl_girl:"👨‍👩‍👧‍👧",family_woman_boy:"👩‍👦",family_woman_boy_boy:"👩‍👦‍👦",family_woman_girl:"👩‍👧",family_woman_girl_boy:"👩‍👧‍👦",family_woman_girl_girl:"👩‍👧‍👧",family_woman_woman_boy:"👩‍👩‍👦",family_woman_woman_boy_boy:"👩‍👩‍👦‍👦",family_woman_woman_girl:"👩‍👩‍👧",family_woman_woman_girl_boy:"👩‍👩‍👧‍👦",family_woman_woman_girl_girl:"👩‍👩‍👧‍👧",fast_forward:"⏩",fax:"📠",fearful:"😨",feet:"🐾",female_detective:"🕵️‍♀️",ferris_wheel:"🎡",ferry:"⛴",field_hockey:"🏑",file_cabinet:"🗄",file_folder:"📁",film_projector:"📽",film_strip:"🎞",fire:"🔥",fire_engine:"🚒",fireworks:"🎆",first_quarter_moon:"🌓",first_quarter_moon_with_face:"🌛",fish:"🐟",fish_cake:"🍥",fishing_pole_and_fish:"🎣",fist_raised:"✊",fist_left:"🤛",fist_right:"🤜",flags:"🎏",flashlight:"🔦",fleur_de_lis:"⚜️",flight_arrival:"🛬",flight_departure:"🛫",floppy_disk:"💾",flower_playing_cards:"🎴",flushed:"😳",fog:"🌫",foggy:"🌁",football:"🏈",footprints:"👣",fork_and_knife:"🍴",fountain:"⛲️",fountain_pen:"🖋",four_leaf_clover:"🍀",fox_face:"🦊",framed_picture:"🖼",free:"🆓",fried_egg:"🍳",fried_shrimp:"🍤",fries:"🍟",frog:"🐸",frowning:"😦",frowning_face:"☹️",frowning_man:"🙍‍♂️",frowning_woman:"🙍",middle_finger:"🖕",fuelpump:"⛽️",full_moon:"🌕",full_moon_with_face:"🌝",funeral_urn:"⚱️",game_die:"🎲",gear:"⚙️",gem:"💎",gemini:"♊️",ghost:"👻",gift:"🎁",gift_heart:"💝",girl:"👧",globe_with_meridians:"🌐",goal_net:"🥅",goat:"🐐",golf:"⛳️",golfing_man:"🏌️",golfing_woman:"🏌️‍♀️",gorilla:"🦍",grapes:"🍇",green_apple:"🍏",green_book:"📗",green_heart:"💚",green_salad:"🥗",grey_exclamation:"❕",grey_question:"❔",grimacing:"😬",grin:"😁",grinning:"😀",guardsman:"💂",guardswoman:"💂‍♀️",guitar:"🎸",gun:"🔫",haircut_woman:"💇",haircut_man:"💇‍♂️",hamburger:"🍔",hammer:"🔨",hammer_and_pick:"⚒",hammer_and_wrench:"🛠",hamster:"🐹",hand:"✋",handbag:"👜",handshake:"🤝",hankey:"💩",hatched_chick:"🐥",hatching_chick:"🐣",headphones:"🎧",hear_no_evil:"🙉",heart:"❤️",heart_decoration:"💟",heart_eyes:"😍",heart_eyes_cat:"😻",heartbeat:"💓",heartpulse:"💗",hearts:"♥️",heavy_check_mark:"✔️",heavy_division_sign:"➗",heavy_dollar_sign:"💲",heavy_heart_exclamation:"❣️",heavy_minus_sign:"➖",heavy_multiplication_x:"✖️",heavy_plus_sign:"➕",helicopter:"🚁",herb:"🌿",hibiscus:"🌺",high_brightness:"🔆",high_heel:"👠",hocho:"🔪",hole:"🕳",honey_pot:"🍯",horse:"🐴",horse_racing:"🏇",hospital:"🏥",hot_pepper:"🌶",hotdog:"🌭",hotel:"🏨",hotsprings:"♨️",hourglass:"⌛️",hourglass_flowing_sand:"⏳",house:"🏠",house_with_garden:"🏡",houses:"🏘",hugs:"🤗",hushed:"😯",ice_cream:"🍨",ice_hockey:"🏒",ice_skate:"⛸",icecream:"🍦",id:"🆔",ideograph_advantage:"🉐",imp:"👿",inbox_tray:"📥",incoming_envelope:"📨",tipping_hand_woman:"💁",information_source:"ℹ️",innocent:"😇",interrobang:"⁉️",iphone:"📱",izakaya_lantern:"🏮",jack_o_lantern:"🎃",japan:"🗾",japanese_castle:"🏯",japanese_goblin:"👺",japanese_ogre:"👹",jeans:"👖",joy:"😂",joy_cat:"😹",joystick:"🕹",kaaba:"🕋",key:"🔑",keyboard:"⌨️",keycap_ten:"🔟",kick_scooter:"🛴",kimono:"👘",kiss:"💋",kissing:"😗",kissing_cat:"😽",kissing_closed_eyes:"😚",kissing_heart:"😘",kissing_smiling_eyes:"😙",kiwi_fruit:"🥝",koala:"🐨",koko:"🈁",label:"🏷",large_blue_circle:"🔵",large_blue_diamond:"🔷",large_orange_diamond:"🔶",last_quarter_moon:"🌗",last_quarter_moon_with_face:"🌜",latin_cross:"✝️",laughing:"😆",leaves:"🍃",ledger:"📒",left_luggage:"🛅",left_right_arrow:"↔️",leftwards_arrow_with_hook:"↩️",lemon:"🍋",leo:"♌️",leopard:"🐆",level_slider:"🎚",libra:"♎️",light_rail:"🚈",link:"🔗",lion:"🦁",lips:"👄",lipstick:"💄",lizard:"🦎",lock:"🔒",lock_with_ink_pen:"🔏",lollipop:"🍭",loop:"➿",loud_sound:"🔊",loudspeaker:"📢",love_hotel:"🏩",love_letter:"💌",low_brightness:"🔅",lying_face:"🤥",m:"Ⓜ️",mag:"🔍",mag_right:"🔎",mahjong:"🀄️",mailbox:"📫",mailbox_closed:"📪",mailbox_with_mail:"📬",mailbox_with_no_mail:"📭",man:"👨",man_artist:"👨‍🎨",man_astronaut:"👨‍🚀",man_cartwheeling:"🤸‍♂️",man_cook:"👨‍🍳",man_dancing:"🕺",man_facepalming:"🤦‍♂️",man_factory_worker:"👨‍🏭",man_farmer:"👨‍🌾",man_firefighter:"👨‍🚒",man_health_worker:"👨‍⚕️",man_in_tuxedo:"🤵",man_judge:"👨‍⚖️",man_juggling:"🤹‍♂️",man_mechanic:"👨‍🔧",man_office_worker:"👨‍💼",man_pilot:"👨‍✈️",man_playing_handball:"🤾‍♂️",man_playing_water_polo:"🤽‍♂️",man_scientist:"👨‍🔬",man_shrugging:"🤷‍♂️",man_singer:"👨‍🎤",man_student:"👨‍🎓",man_teacher:"👨‍🏫",man_technologist:"👨‍💻",man_with_gua_pi_mao:"👲",man_with_turban:"👳",tangerine:"🍊",mans_shoe:"👞",mantelpiece_clock:"🕰",maple_leaf:"🍁",martial_arts_uniform:"🥋",mask:"😷",massage_woman:"💆",massage_man:"💆‍♂️",meat_on_bone:"🍖",medal_military:"🎖",medal_sports:"🏅",mega:"📣",melon:"🍈",memo:"📝",men_wrestling:"🤼‍♂️",menorah:"🕎",mens:"🚹",metal:"🤘",metro:"🚇",microphone:"🎤",microscope:"🔬",milk_glass:"🥛",milky_way:"🌌",minibus:"🚐",minidisc:"💽",mobile_phone_off:"📴",money_mouth_face:"🤑",money_with_wings:"💸",moneybag:"💰",monkey:"🐒",monkey_face:"🐵",monorail:"🚝",moon:"🌔",mortar_board:"🎓",mosque:"🕌",motor_boat:"🛥",motor_scooter:"🛵",motorcycle:"🏍",motorway:"🛣",mount_fuji:"🗻",mountain:"⛰",mountain_biking_man:"🚵",mountain_biking_woman:"🚵‍♀️",mountain_cableway:"🚠",mountain_railway:"🚞",mountain_snow:"🏔",mouse:"🐭",mouse2:"🐁",movie_camera:"🎥",moyai:"🗿",mrs_claus:"🤶",muscle:"💪",mushroom:"🍄",musical_keyboard:"🎹",musical_note:"🎵",musical_score:"🎼",mute:"🔇",nail_care:"💅",name_badge:"📛",national_park:"🏞",nauseated_face:"🤢",necktie:"👔",negative_squared_cross_mark:"❎",nerd_face:"🤓",neutral_face:"😐",new:"🆕",new_moon:"🌑",new_moon_with_face:"🌚",newspaper:"📰",newspaper_roll:"🗞",next_track_button:"⏭",ng:"🆖",no_good_man:"🙅‍♂️",no_good_woman:"🙅",night_with_stars:"🌃",no_bell:"🔕",no_bicycles:"🚳",no_entry:"⛔️",no_entry_sign:"🚫",no_mobile_phones:"📵",no_mouth:"😶",no_pedestrians:"🚷",no_smoking:"🚭","non-potable_water":"🚱",nose:"👃",notebook:"📓",notebook_with_decorative_cover:"📔",notes:"🎶",nut_and_bolt:"🔩",o:"⭕️",o2:"🅾️",ocean:"🌊",octopus:"🐙",oden:"🍢",office:"🏢",oil_drum:"🛢",ok:"🆗",ok_hand:"👌",ok_man:"🙆‍♂️",ok_woman:"🙆",old_key:"🗝",older_man:"👴",older_woman:"👵",om:"🕉",on:"🔛",oncoming_automobile:"🚘",oncoming_bus:"🚍",oncoming_police_car:"🚔",oncoming_taxi:"🚖",open_file_folder:"📂",open_hands:"👐",open_mouth:"😮",open_umbrella:"☂️",ophiuchus:"⛎",orange_book:"📙",orthodox_cross:"☦️",outbox_tray:"📤",owl:"🦉",ox:"🐂",package:"📦",page_facing_up:"📄",page_with_curl:"📃",pager:"📟",paintbrush:"🖌",palm_tree:"🌴",pancakes:"🥞",panda_face:"🐼",paperclip:"📎",paperclips:"🖇",parasol_on_ground:"⛱",parking:"🅿️",part_alternation_mark:"〽️",partly_sunny:"⛅️",passenger_ship:"🛳",passport_control:"🛂",pause_button:"⏸",peace_symbol:"☮️",peach:"🍑",peanuts:"🥜",pear:"🍐",pen:"🖊",pencil2:"✏️",penguin:"🐧",pensive:"😔",performing_arts:"🎭",persevere:"😣",person_fencing:"🤺",pouting_woman:"🙎",phone:"☎️",pick:"⛏",pig:"🐷",pig2:"🐖",pig_nose:"🐽",pill:"💊",pineapple:"🍍",ping_pong:"🏓",pisces:"♓️",pizza:"🍕",place_of_worship:"🛐",plate_with_cutlery:"🍽",play_or_pause_button:"⏯",point_down:"👇",point_left:"👈",point_right:"👉",point_up:"☝️",point_up_2:"👆",police_car:"🚓",policewoman:"👮‍♀️",poodle:"🐩",popcorn:"🍿",post_office:"🏣",postal_horn:"📯",postbox:"📮",potable_water:"🚰",potato:"🥔",pouch:"👝",poultry_leg:"🍗",pound:"💷",rage:"😡",pouting_cat:"😾",pouting_man:"🙎‍♂️",pray:"🙏",prayer_beads:"📿",pregnant_woman:"🤰",previous_track_button:"⏮",prince:"🤴",princess:"👸",printer:"🖨",purple_heart:"💜",purse:"👛",pushpin:"📌",put_litter_in_its_place:"🚮",question:"❓",rabbit:"🐰",rabbit2:"🐇",racehorse:"🐎",racing_car:"🏎",radio:"📻",radio_button:"🔘",radioactive:"☢️",railway_car:"🚃",railway_track:"🛤",rainbow:"🌈",rainbow_flag:"🏳️‍🌈",raised_back_of_hand:"🤚",raised_hand_with_fingers_splayed:"🖐",raised_hands:"🙌",raising_hand_woman:"🙋",raising_hand_man:"🙋‍♂️",ram:"🐏",ramen:"🍜",rat:"🐀",record_button:"⏺",recycle:"♻️",red_circle:"🔴",registered:"®️",relaxed:"☺️",relieved:"😌",reminder_ribbon:"🎗",repeat:"🔁",repeat_one:"🔂",rescue_worker_helmet:"⛑",restroom:"🚻",revolving_hearts:"💞",rewind:"⏪",rhinoceros:"🦏",ribbon:"🎀",rice:"🍚",rice_ball:"🍙",rice_cracker:"🍘",rice_scene:"🎑",right_anger_bubble:"🗯",ring:"💍",robot:"🤖",rocket:"🚀",rofl:"🤣",roll_eyes:"🙄",roller_coaster:"🎢",rooster:"🐓",rose:"🌹",rosette:"🏵",rotating_light:"🚨",round_pushpin:"📍",rowing_man:"🚣",rowing_woman:"🚣‍♀️",rugby_football:"🏉",running_man:"🏃",running_shirt_with_sash:"🎽",running_woman:"🏃‍♀️",sa:"🈂️",sagittarius:"♐️",sake:"🍶",sandal:"👡",santa:"🎅",satellite:"📡",saxophone:"🎷",school:"🏫",school_satchel:"🎒",scissors:"✂️",scorpion:"🦂",scorpius:"♏️",scream:"😱",scream_cat:"🙀",scroll:"📜",seat:"💺",secret:"㊙️",see_no_evil:"🙈",seedling:"🌱",selfie:"🤳",shallow_pan_of_food:"🥘",shamrock:"☘️",shark:"🦈",shaved_ice:"🍧",sheep:"🐑",shell:"🐚",shield:"🛡",shinto_shrine:"⛩",ship:"🚢",shirt:"👕",shopping:"🛍",shopping_cart:"🛒",shower:"🚿",shrimp:"🦐",signal_strength:"📶",six_pointed_star:"🔯",ski:"🎿",skier:"⛷",skull:"💀",skull_and_crossbones:"☠️",sleeping:"😴",sleeping_bed:"🛌",sleepy:"😪",slightly_frowning_face:"🙁",slightly_smiling_face:"🙂",slot_machine:"🎰",small_airplane:"🛩",small_blue_diamond:"🔹",small_orange_diamond:"🔸",small_red_triangle:"🔺",small_red_triangle_down:"🔻",smile:"😄",smile_cat:"😸",smiley:"😃",smiley_cat:"😺",smiling_imp:"😈",smirk:"😏",smirk_cat:"😼",smoking:"🚬",snail:"🐌",snake:"🐍",sneezing_face:"🤧",snowboarder:"🏂",snowflake:"❄️",snowman:"⛄️",snowman_with_snow:"☃️",sob:"😭",soccer:"⚽️",soon:"🔜",sos:"🆘",sound:"🔉",space_invader:"👾",spades:"♠️",spaghetti:"🍝",sparkle:"❇️",sparkler:"🎇",sparkles:"✨",sparkling_heart:"💖",speak_no_evil:"🙊",speaker:"🔈",speaking_head:"🗣",speech_balloon:"💬",speedboat:"🚤",spider:"🕷",spider_web:"🕸",spiral_calendar:"🗓",spiral_notepad:"🗒",spoon:"🥄",squid:"🦑",stadium:"🏟",star:"⭐️",star2:"🌟",star_and_crescent:"☪️",star_of_david:"✡️",stars:"🌠",station:"🚉",statue_of_liberty:"🗽",steam_locomotive:"🚂",stew:"🍲",stop_button:"⏹",stop_sign:"🛑",stopwatch:"⏱",straight_ruler:"📏",strawberry:"🍓",stuck_out_tongue:"😛",stuck_out_tongue_closed_eyes:"😝",stuck_out_tongue_winking_eye:"😜",studio_microphone:"🎙",stuffed_flatbread:"🥙",sun_behind_large_cloud:"🌥",sun_behind_rain_cloud:"🌦",sun_behind_small_cloud:"🌤",sun_with_face:"🌞",sunflower:"🌻",sunglasses:"😎",sunny:"☀️",sunrise:"🌅",sunrise_over_mountains:"🌄",surfing_man:"🏄",surfing_woman:"🏄‍♀️",sushi:"🍣",suspension_railway:"🚟",sweat:"😓",sweat_drops:"💦",sweat_smile:"😅",sweet_potato:"🍠",swimming_man:"🏊",swimming_woman:"🏊‍♀️",symbols:"🔣",synagogue:"🕍",syringe:"💉",taco:"🌮",tada:"🎉",tanabata_tree:"🎋",taurus:"♉️",taxi:"🚕",tea:"🍵",telephone_receiver:"📞",telescope:"🔭",tennis:"🎾",tent:"⛺️",thermometer:"🌡",thinking:"🤔",thought_balloon:"💭",ticket:"🎫",tickets:"🎟",tiger:"🐯",tiger2:"🐅",timer_clock:"⏲",tipping_hand_man:"💁‍♂️",tired_face:"😫",tm:"™️",toilet:"🚽",tokyo_tower:"🗼",tomato:"🍅",tongue:"👅",top:"🔝",tophat:"🎩",tornado:"🌪",trackball:"🖲",tractor:"🚜",traffic_light:"🚥",train:"🚋",train2:"🚆",tram:"🚊",triangular_flag_on_post:"🚩",triangular_ruler:"📐",trident:"🔱",triumph:"😤",trolleybus:"🚎",trophy:"🏆",tropical_drink:"🍹",tropical_fish:"🐠",truck:"🚚",trumpet:"🎺",tulip:"🌷",tumbler_glass:"🥃",turkey:"🦃",turtle:"🐢",tv:"📺",twisted_rightwards_arrows:"🔀",two_hearts:"💕",two_men_holding_hands:"👬",two_women_holding_hands:"👭",u5272:"🈹",u5408:"🈴",u55b6:"🈺",u6307:"🈯️",u6708:"🈷️",u6709:"🈶",u6e80:"🈵",u7121:"🈚️",u7533:"🈸",u7981:"🈲",u7a7a:"🈳",umbrella:"☔️",unamused:"😒",underage:"🔞",unicorn:"🦄",unlock:"🔓",up:"🆙",upside_down_face:"🙃",v:"✌️",vertical_traffic_light:"🚦",vhs:"📼",vibration_mode:"📳",video_camera:"📹",video_game:"🎮",violin:"🎻",virgo:"♍️",volcano:"🌋",volleyball:"🏐",vs:"🆚",vulcan_salute:"🖖",walking_man:"🚶",walking_woman:"🚶‍♀️",waning_crescent_moon:"🌘",waning_gibbous_moon:"🌖",warning:"⚠️",wastebasket:"🗑",watch:"⌚️",water_buffalo:"🐃",watermelon:"🍉",wave:"👋",wavy_dash:"〰️",waxing_crescent_moon:"🌒",wc:"🚾",weary:"😩",wedding:"💒",weight_lifting_man:"🏋️",weight_lifting_woman:"🏋️‍♀️",whale:"🐳",whale2:"🐋",wheel_of_dharma:"☸️",wheelchair:"♿️",white_check_mark:"✅",white_circle:"⚪️",white_flag:"🏳️",white_flower:"💮",white_large_square:"⬜️",white_medium_small_square:"◽️",white_medium_square:"◻️",white_small_square:"▫️",white_square_button:"🔳",wilted_flower:"🥀",wind_chime:"🎐",wind_face:"🌬",wine_glass:"🍷",wink:"😉",wolf:"🐺",woman:"👩",woman_artist:"👩‍🎨",woman_astronaut:"👩‍🚀",woman_cartwheeling:"🤸‍♀️",woman_cook:"👩‍🍳",woman_facepalming:"🤦‍♀️",woman_factory_worker:"👩‍🏭",woman_farmer:"👩‍🌾",woman_firefighter:"👩‍🚒",woman_health_worker:"👩‍⚕️",woman_judge:"👩‍⚖️",woman_juggling:"🤹‍♀️",woman_mechanic:"👩‍🔧",woman_office_worker:"👩‍💼",woman_pilot:"👩‍✈️",woman_playing_handball:"🤾‍♀️",woman_playing_water_polo:"🤽‍♀️",woman_scientist:"👩‍🔬",woman_shrugging:"🤷‍♀️",woman_singer:"👩‍🎤",woman_student:"👩‍🎓",woman_teacher:"👩‍🏫",woman_technologist:"👩‍💻",woman_with_turban:"👳‍♀️",womans_clothes:"👚",womans_hat:"👒",women_wrestling:"🤼‍♀️",womens:"🚺",world_map:"🗺",worried:"😟",wrench:"🔧",writing_hand:"✍️",x:"❌",yellow_heart:"💛",yen:"💴",yin_yang:"☯️",yum:"😋",zap:"⚡️",zipper_mouth_face:"🤐",zzz:"💤",octocat:':octocat:',showdown:"S"},x.Converter=function(e){"use strict";var r,t,n={},i=[],l=[],o={},a=h,s={parsed:{},raw:"",format:""};for(r in e=e||{},p)p.hasOwnProperty(r)&&(n[r]=p[r]);if("object"!=typeof e)throw Error("Converter expects the passed parameter to be an object, but "+typeof e+" was passed instead.");for(t in e)e.hasOwnProperty(t)&&(n[t]=e[t]);function c(e,r){if(r=r||null,x.helper.isString(e)){if(r=e=x.helper.stdExtName(e),x.extensions[e]){console.warn("DEPRECATION WARNING: "+e+" is an old extension that uses a deprecated loading method.Please inform the developer that the extension should be updated!");var t=x.extensions[e],a=e;if("function"==typeof t&&(t=t(new x.Converter)),x.helper.isArray(t)||(t=[t]),!(a=g(t,a)).valid)throw Error(a.error);for(var n=0;n[ \t]+¨NBSP;<"),!r){if(!window||!window.document)throw new Error("HTMLParser is undefined. If in a webworker or nodejs environment, you need to provide a WHATWG DOM and HTML such as JSDOM");r=window.document}for(var r=r.createElement("div"),t=(r.innerHTML=e,{preList:function(e){for(var r=e.querySelectorAll("pre"),t=[],a=0;a'}else t.push(r[a].innerHTML),r[a].innerHTML="",r[a].setAttribute("prenum",a.toString());return t}(r)}),a=(!function e(r){for(var t=0;t? ?(['"].*['"])?\)$/m))a="";else if(!a){if(a="#"+(t=t||r.toLowerCase().replace(/ ?\n/g," ")),x.helper.isUndefined(l.gUrls[t]))return e;a=l.gUrls[t],x.helper.isUndefined(l.gTitles[t])||(o=l.gTitles[t])}return e='"}return e=(e=(e=(e=(e=l.converter._dispatch("anchors.before",e,i,l)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)] ?(?:\n *)?\[(.*?)]()()()()/g,r)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]?<([^>]*)>(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,r)).replace(/\[((?:\[[^\]]*]|[^\[\]])*)]()[ \t]*\([ \t]??(?:[ \t]*((["'])([^"]*?)\5))?[ \t]?\)/g,r)).replace(/\[([^\[\]]+)]()()()()()/g,r),i.ghMentions&&(e=e.replace(/(^|\s)(\\)?(@([a-z\d]+(?:[a-z\d.-]+?[a-z\d]+)*))/gim,function(e,r,t,a,n){if("\\"===t)return r+a;if(!x.helper.isString(i.ghMentionsLink))throw new Error("ghMentionsLink option must be a string");t="";return r+'"+a+""})),e=l.converter._dispatch("anchors.after",e,i,l)});var i=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+?\.[^'">\s]+?)()(\1)?(?=\s|$)(?!["<>])/gi,l=/([*~_]+|\b)(((https?|ftp|dict):\/\/|www\.)[^'">\s]+\.[^'">\s]+?)([.!?,()\[\]])?(\1)?(?=\s|$)(?!["<>])/gi,c=/()<(((https?|ftp|dict):\/\/|www\.)[^'">\s]+)()>()/gi,m=/(^|\s)(?:mailto:)?([A-Za-z0-9!#$%&'*+-/=?^_`{|}~.]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)(?=$|\s)/gim,f=/<()(?:mailto:)?([-.\w]+@[-a-z0-9]+(\.[-a-z0-9]+)*\.[a-z]+)>/gi;x.subParser("autoLinks",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("autoLinks.before",e,r,t)).replace(c,s(r))).replace(f,o(r,t)),e=t.converter._dispatch("autoLinks.after",e,r,t)}),x.subParser("simplifiedAutoLinks",function(e,r,t){"use strict";return r.simplifiedAutoLink?(e=t.converter._dispatch("simplifiedAutoLinks.before",e,r,t),e=(e=r.excludeTrailingPunctuationFromURLs?e.replace(l,s(r)):e.replace(i,s(r))).replace(m,o(r,t)),t.converter._dispatch("simplifiedAutoLinks.after",e,r,t)):e}),x.subParser("blockGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("blockGamut.before",e,r,t),e=x.subParser("blockQuotes")(e,r,t),e=x.subParser("headers")(e,r,t),e=x.subParser("horizontalRule")(e,r,t),e=x.subParser("lists")(e,r,t),e=x.subParser("codeBlocks")(e,r,t),e=x.subParser("tables")(e,r,t),e=x.subParser("hashHTMLBlocks")(e,r,t),e=x.subParser("paragraphs")(e,r,t),e=t.converter._dispatch("blockGamut.after",e,r,t)}),x.subParser("blockQuotes",function(e,r,t){"use strict";e=t.converter._dispatch("blockQuotes.before",e,r,t);var a=/(^ {0,3}>[ \t]?.+\n(.+\n)*\n*)+/gm;return r.splitAdjacentBlockquotes&&(a=/^ {0,3}>[\s\S]*?(?:\n\n)/gm),e=(e+="\n\n").replace(a,function(e){return e=(e=(e=e.replace(/^[ \t]*>[ \t]?/gm,"")).replace(/¨0/g,"")).replace(/^[ \t]+$/gm,""),e=x.subParser("githubCodeBlocks")(e,r,t),e=(e=(e=x.subParser("blockGamut")(e,r,t)).replace(/(^|\n)/g,"$1 ")).replace(/(\s*
[^\r]+?<\/pre>)/gm,function(e,r){return r.replace(/^  /gm,"¨0").replace(/¨0/g,"")}),x.subParser("hashBlock")("
\n"+e+"\n
",r,t)}),e=t.converter._dispatch("blockQuotes.after",e,r,t)}),x.subParser("codeBlocks",function(e,n,s){"use strict";e=s.converter._dispatch("codeBlocks.before",e,n,s);return e=(e=(e+="¨0").replace(/(?:\n\n|^)((?:(?:[ ]{4}|\t).*\n+)+)(\n*[ ]{0,3}[^ \t\n]|(?=¨0))/g,function(e,r,t){var a="\n",r=x.subParser("outdent")(r,n,s);return r=x.subParser("encodeCode")(r,n,s),r="
"+(r=(r=(r=x.subParser("detab")(r,n,s)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+(a=n.omitExtraWLInCodeBlocks?"":a)+"
",x.subParser("hashBlock")(r,n,s)+t})).replace(/¨0/,""),e=s.converter._dispatch("codeBlocks.after",e,n,s)}),x.subParser("codeSpans",function(e,n,s){"use strict";return e=(e=void 0===(e=s.converter._dispatch("codeSpans.before",e,n,s))?"":e).replace(/(^|[^\\])(`+)([^\r]*?[^`])\2(?!`)/gm,function(e,r,t,a){return a=(a=a.replace(/^([ \t]*)/g,"")).replace(/[ \t]*$/g,""),a=r+""+(a=x.subParser("encodeCode")(a,n,s))+"",a=x.subParser("hashHTMLSpans")(a,n,s)}),e=s.converter._dispatch("codeSpans.after",e,n,s)}),x.subParser("completeHTMLDocument",function(e,r,t){"use strict";if(!r.completeHTMLDocument)return e;e=t.converter._dispatch("completeHTMLDocument.before",e,r,t);var a,n="html",s="\n",o="",i='\n',l="",c="";for(a in void 0!==t.metadata.parsed.doctype&&(s="\n","html"!==(n=t.metadata.parsed.doctype.toString().toLowerCase())&&"html5"!==n||(i='')),t.metadata.parsed)if(t.metadata.parsed.hasOwnProperty(a))switch(a.toLowerCase()){case"doctype":break;case"title":o=""+t.metadata.parsed.title+"\n";break;case"charset":i="html"===n||"html5"===n?'\n':'\n';break;case"language":case"lang":l=' lang="'+t.metadata.parsed[a]+'"',c+='\n';break;default:c+='\n'}return e=s+"\n\n"+o+i+c+"\n\n"+e.trim()+"\n\n",e=t.converter._dispatch("completeHTMLDocument.after",e,r,t)}),x.subParser("detab",function(e,r,t){"use strict";return e=(e=(e=(e=(e=(e=t.converter._dispatch("detab.before",e,r,t)).replace(/\t(?=\t)/g," ")).replace(/\t/g,"¨A¨B")).replace(/¨B(.+?)¨A/g,function(e,r){for(var t=r,a=4-t.length%4,n=0;n/g,">"),e=t.converter._dispatch("encodeAmpsAndAngles.after",e,r,t)}),x.subParser("encodeBackslashEscapes",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("encodeBackslashEscapes.before",e,r,t)).replace(/\\(\\)/g,x.helper.escapeCharactersCallback)).replace(/\\([`*_{}\[\]()>#+.!~=|:-])/g,x.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeBackslashEscapes.after",e,r,t)}),x.subParser("encodeCode",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("encodeCode.before",e,r,t)).replace(/&/g,"&").replace(//g,">").replace(/([*_{}\[\]\\=~-])/g,x.helper.escapeCharactersCallback),e=t.converter._dispatch("encodeCode.after",e,r,t)}),x.subParser("escapeSpecialCharsWithinTagAttributes",function(e,r,t){"use strict";return e=(e=(e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.before",e,r,t)).replace(/<\/?[a-z\d_:-]+(?:[\s]+[\s\S]+?)?>/gi,function(e){return e.replace(/(.)<\/?code>(?=.)/g,"$1`").replace(/([\\`*_~=|])/g,x.helper.escapeCharactersCallback)})).replace(/-]|-[^>])(?:[^-]|-[^-])*)--)>/gi,function(e){return e.replace(/([\\`*_~=|])/g,x.helper.escapeCharactersCallback)}),e=t.converter._dispatch("escapeSpecialCharsWithinTagAttributes.after",e,r,t)}),x.subParser("githubCodeBlocks",function(e,s,o){"use strict";return s.ghCodeBlocks?(e=o.converter._dispatch("githubCodeBlocks.before",e,s,o),e=(e=(e+="¨0").replace(/(?:^|\n)(?: {0,3})(```+|~~~+)(?: *)([^\s`~]*)\n([\s\S]*?)\n(?: {0,3})\1/g,function(e,r,t,a){var n=s.omitExtraWLInCodeBlocks?"":"\n";return a=x.subParser("encodeCode")(a,s,o),a="
"+(a=(a=(a=x.subParser("detab")(a,s,o)).replace(/^\n+/g,"")).replace(/\n+$/g,""))+n+"
",a=x.subParser("hashBlock")(a,s,o),"\n\n¨G"+(o.ghCodeBlocks.push({text:e,codeblock:a})-1)+"G\n\n"})).replace(/¨0/,""),o.converter._dispatch("githubCodeBlocks.after",e,s,o)):e}),x.subParser("hashBlock",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("hashBlock.before",e,r,t)).replace(/(^\n+|\n+$)/g,""),e="\n\n¨K"+(t.gHtmlBlocks.push(e)-1)+"K\n\n",e=t.converter._dispatch("hashBlock.after",e,r,t)}),x.subParser("hashCodeTags",function(e,n,s){"use strict";e=s.converter._dispatch("hashCodeTags.before",e,n,s);return e=x.helper.replaceRecursiveRegExp(e,function(e,r,t,a){t=t+x.subParser("encodeCode")(r,n,s)+a;return"¨C"+(s.gHtmlSpans.push(t)-1)+"C"},"]*>","","gim"),e=s.converter._dispatch("hashCodeTags.after",e,n,s)}),x.subParser("hashElement",function(e,r,t){"use strict";return function(e,r){return r=(r=(r=r.replace(/\n\n/g,"\n")).replace(/^\n/,"")).replace(/\n+$/g,""),r="\n\n¨K"+(t.gHtmlBlocks.push(r)-1)+"K\n\n"}}),x.subParser("hashHTMLBlocks",function(e,r,n){"use strict";e=n.converter._dispatch("hashHTMLBlocks.before",e,r,n);function t(e,r,t,a){return-1!==t.search(/\bmarkdown\b/)&&(e=t+n.converter.makeHtml(r)+a),"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"}var a=["pre","div","h1","h2","h3","h4","h5","h6","blockquote","table","dl","ol","ul","script","noscript","form","fieldset","iframe","math","style","section","header","footer","nav","article","aside","address","audio","canvas","figure","hgroup","output","video","p"];r.backslashEscapesHTMLTags&&(e=e.replace(/\\<(\/?[^>]+?)>/g,function(e,r){return"<"+r+">"}));for(var s=0;s]*>)","im"),i="<"+a[s]+"\\b[^>]*>",l="";-1!==(c=x.helper.regexIndexOf(e,o));){var c=x.helper.splitAtIndex(e,c),u=x.helper.replaceRecursiveRegExp(c[1],t,i,l,"im");if(u===c[1])break;e=c[0].concat(u)}return e=e.replace(/(\n {0,3}(<(hr)\b([^<>])*?\/?>)[ \t]*(?=\n{2,}))/g,x.subParser("hashElement")(e,r,n)),e=(e=x.helper.replaceRecursiveRegExp(e,function(e){return"\n\n¨K"+(n.gHtmlBlocks.push(e)-1)+"K\n\n"},"^ {0,3}\x3c!--","--\x3e","gm")).replace(/(?:\n\n)( {0,3}(?:<([?%])[^\r]*?\2>)[ \t]*(?=\n{2,}))/g,x.subParser("hashElement")(e,r,n)),e=n.converter._dispatch("hashHTMLBlocks.after",e,r,n)}),x.subParser("hashHTMLSpans",function(e,r,t){"use strict";function a(e){return"¨C"+(t.gHtmlSpans.push(e)-1)+"C"}return e=(e=(e=(e=(e=t.converter._dispatch("hashHTMLSpans.before",e,r,t)).replace(/<[^>]+?\/>/gi,a)).replace(/<([^>]+?)>[\s\S]*?<\/\1>/g,a)).replace(/<([^>]+?)\s[^>]+?>[\s\S]*?<\/\1>/g,a)).replace(/<[^>]+?>/gi,a),e=t.converter._dispatch("hashHTMLSpans.after",e,r,t)}),x.subParser("unhashHTMLSpans",function(e,r,t){"use strict";e=t.converter._dispatch("unhashHTMLSpans.before",e,r,t);for(var a=0;a]*>\\s*]*>","^ {0,3}\\s*
","gim"),e=s.converter._dispatch("hashPreCodeTags.after",e,n,s)}),x.subParser("headers",function(e,n,s){"use strict";e=s.converter._dispatch("headers.before",e,n,s);var o=isNaN(parseInt(n.headerLevelStart))?1:parseInt(n.headerLevelStart),r=n.smoothLivePreview?/^(.+)[ \t]*\n={2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n=+[ \t]*\n+/gm,t=n.smoothLivePreview?/^(.+)[ \t]*\n-{2,}[ \t]*\n+/gm:/^(.+)[ \t]*\n-+[ \t]*\n+/gm,r=(e=(e=e.replace(r,function(e,r){var t=x.subParser("spanGamut")(r,n,s),r=n.noHeaderId?"":' id="'+i(r)+'"',r=""+t+"";return x.subParser("hashBlock")(r,n,s)})).replace(t,function(e,r){var t=x.subParser("spanGamut")(r,n,s),r=n.noHeaderId?"":' id="'+i(r)+'"',a=o+1,r=""+t+"";return x.subParser("hashBlock")(r,n,s)}),n.requireSpaceBeforeHeadingText?/^(#{1,6})[ \t]+(.+?)[ \t]*#*\n+/gm:/^(#{1,6})[ \t]*(.+?)[ \t]*#*\n+/gm);function i(e){var r=e=n.customizedHeaderId&&(r=e.match(/\{([^{]+?)}\s*$/))&&r[1]?r[1]:e,e=x.helper.isString(n.prefixHeaderId)?n.prefixHeaderId:!0===n.prefixHeaderId?"section-":"";return n.rawPrefixHeaderId||(r=e+r),r=(n.ghCompatibleHeaderId?r.replace(/ /g,"-").replace(/&/g,"").replace(/¨T/g,"").replace(/¨D/g,"").replace(/[&+$,\/:;=?@"#{}|^¨~\[\]`\\*)(%.!'<>]/g,""):n.rawHeaderId?r.replace(/ /g,"-").replace(/&/g,"&").replace(/¨T/g,"¨").replace(/¨D/g,"$").replace(/["']/g,"-"):r.replace(/[^\w]/g,"")).toLowerCase(),n.rawPrefixHeaderId&&(r=e+r),s.hashLinkCounts[r]?r=r+"-"+s.hashLinkCounts[r]++:s.hashLinkCounts[r]=1,r}return e=e.replace(r,function(e,r,t){var a=t,a=(n.customizedHeaderId&&(a=t.replace(/\s?\{([^{]+?)}\s*$/,"")),x.subParser("spanGamut")(a,n,s)),t=n.noHeaderId?"":' id="'+i(t)+'"',r=o-1+r.length,t=""+a+"";return x.subParser("hashBlock")(t,n,s)}),e=s.converter._dispatch("headers.after",e,n,s)}),x.subParser("horizontalRule",function(e,r,t){"use strict";e=t.converter._dispatch("horizontalRule.before",e,r,t);var a=x.subParser("hashBlock")("
",r,t);return e=(e=(e=e.replace(/^ {0,2}( ?-){3,}[ \t]*$/gm,a)).replace(/^ {0,2}( ?\*){3,}[ \t]*$/gm,a)).replace(/^ {0,2}( ?_){3,}[ \t]*$/gm,a),e=t.converter._dispatch("horizontalRule.after",e,r,t)}),x.subParser("images",function(e,r,d){"use strict";function l(e,r,t,a,n,s,o,i){var l=d.gUrls,c=d.gTitles,u=d.gDimensions;if(t=t.toLowerCase(),i=i||"",-1? ?(['"].*['"])?\)$/m))a="";else if(""===a||null===a){if(a="#"+(t=""!==t&&null!==t?t:r.toLowerCase().replace(/ ?\n/g," ")),x.helper.isUndefined(l[t]))return e;a=l[t],x.helper.isUndefined(c[t])||(i=c[t]),x.helper.isUndefined(u[t])||(n=u[t].width,s=u[t].height)}r=r.replace(/"/g,""").replace(x.helper.regexes.asteriskDashAndColon,x.helper.escapeCharactersCallback);e=''+r+'"}return e=(e=(e=(e=(e=(e=d.converter._dispatch("images.before",e,r,d)).replace(/!\[([^\]]*?)] ?(?:\n *)?\[([\s\S]*?)]()()()()()/g,l)).replace(/!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,function(e,r,t,a,n,s,o,i){return l(e,r,t,a=a.replace(/\s/g,""),n,s,0,i)})).replace(/!\[([^\]]*?)][ \t]*()\([ \t]?<([^>]*)>(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(?:(["'])([^"]*?)\6))?[ \t]?\)/g,l)).replace(/!\[([^\]]*?)][ \t]*()\([ \t]??(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*(?:(["'])([^"]*?)\6)?[ \t]?\)/g,l)).replace(/!\[([^\[\]]+)]()()()()()/g,l),e=d.converter._dispatch("images.after",e,r,d)}),x.subParser("italicsAndBold",function(e,r,t){"use strict";return e=t.converter._dispatch("italicsAndBold.before",e,r,t),e=r.literalMidWordUnderscores?(e=(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return""+r+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return""+r+""})).replace(/\b_(\S[\s\S]*?)_\b/g,function(e,r){return""+r+""}):(e=(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/_([^\s_][\s\S]*?)_/g,function(e,r){return/\S$/.test(r)?""+r+"":e}),e=r.literalMidWordAsterisks?(e=(e=e.replace(/([^*]|^)\B\*\*\*(\S[\s\S]*?)\*\*\*\B(?!\*)/g,function(e,r,t){return r+""+t+""})).replace(/([^*]|^)\B\*\*(\S[\s\S]*?)\*\*\B(?!\*)/g,function(e,r,t){return r+""+t+""})).replace(/([^*]|^)\B\*(\S[\s\S]*?)\*\B(?!\*)/g,function(e,r,t){return r+""+t+""}):(e=(e=e.replace(/\*\*\*(\S[\s\S]*?)\*\*\*/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/\*\*(\S[\s\S]*?)\*\*/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/\*([^\s*][\s\S]*?)\*/g,function(e,r){return/\S$/.test(r)?""+r+"":e}),e=t.converter._dispatch("italicsAndBold.after",e,r,t)}),x.subParser("lists",function(e,d,c){"use strict";function p(e,r){c.gListLevel++,e=e.replace(/\n{2,}$/,"\n");var t=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0| {0,3}([*+-]|\d+[.])[ \t]+))/gm,l=/\n[ \t]*\n(?!¨0)/.test(e+="¨0");return d.disableForced4SpacesIndentedSublists&&(t=/(\n)?(^ {0,3})([*+-]|\d+[.])[ \t]+((\[(x|X| )?])?[ \t]*[^\r]+?(\n{1,2}))(?=\n*(¨0|\2([*+-]|\d+[.])[ \t]+))/gm),e=(e=e.replace(t,function(e,r,t,a,n,s,o){o=o&&""!==o.trim();var n=x.subParser("outdent")(n,d,c),i="";return s&&d.tasklists&&(i=' class="task-list-item" style="list-style-type: none;"',n=n.replace(/^[ \t]*\[(x|X| )?]/m,function(){var e='"+(n=(n=r||-1\n"})).replace(/¨0/g,""),c.gListLevel--,e=r?e.replace(/\s+$/,""):e}function h(e,r){if("ol"===r){r=e.match(/^ *(\d+)\./);if(r&&"1"!==r[1])return' start="'+r[1]+'"'}return""}function n(n,s,o){var e,i=d.disableForced4SpacesIndentedSublists?/^ ?\d+\.[ \t]/gm:/^ {0,3}\d+\.[ \t]/gm,l=d.disableForced4SpacesIndentedSublists?/^ ?[*+-][ \t]/gm:/^ {0,3}[*+-][ \t]/gm,c="ul"===s?i:l,u="";return-1!==n.search(c)?function e(r){var t=r.search(c),a=h(n,s);-1!==t?(u+="\n\n<"+s+a+">\n"+p(r.slice(0,t),!!o)+"\n",c="ul"===(s="ul"===s?"ol":"ul")?i:l,e(r.slice(t))):u+="\n\n<"+s+a+">\n"+p(r,!!o)+"\n"}(n):(e=h(n,s),u="\n\n<"+s+e+">\n"+p(n,!!o)+"\n"),u}return e=c.converter._dispatch("lists.before",e,d,c),e+="¨0",e=(e=c.gListLevel?e.replace(/^(( {0,3}([*+-]|\d+[.])[ \t]+)[^\r]+?(¨0|\n{2,}(?=\S)(?![ \t]*(?:[*+-]|\d+[.])[ \t]+)))/gm,function(e,r,t){return n(r,-1"),i+="

",n.push(i))}for(s=n.length,o=0;o]*>\s*]*>/.test(c)&&(u=!0)}n[o]=c}return e=(e=(e=n.join("\n")).replace(/^\n+/g,"")).replace(/\n+$/g,""),t.converter._dispatch("paragraphs.after",e,r,t)}),x.subParser("runExtension",function(e,r,t,a){"use strict";return e.filter?r=e.filter(r,a.converter,t):e.regex&&((a=e.regex)instanceof RegExp||(a=new RegExp(a,"g")),r=r.replace(a,e.replace)),r}),x.subParser("spanGamut",function(e,r,t){"use strict";return e=t.converter._dispatch("spanGamut.before",e,r,t),e=x.subParser("codeSpans")(e,r,t),e=x.subParser("escapeSpecialCharsWithinTagAttributes")(e,r,t),e=x.subParser("encodeBackslashEscapes")(e,r,t),e=x.subParser("images")(e,r,t),e=x.subParser("anchors")(e,r,t),e=x.subParser("autoLinks")(e,r,t),e=x.subParser("simplifiedAutoLinks")(e,r,t),e=x.subParser("emoji")(e,r,t),e=x.subParser("underline")(e,r,t),e=x.subParser("italicsAndBold")(e,r,t),e=x.subParser("strikethrough")(e,r,t),e=x.subParser("ellipsis")(e,r,t),e=x.subParser("hashHTMLSpans")(e,r,t),e=x.subParser("encodeAmpsAndAngles")(e,r,t),r.simpleLineBreaks?/\n\n¨K/.test(e)||(e=e.replace(/\n+/g,"
\n")):e=e.replace(/ +\n/g,"
\n"),e=t.converter._dispatch("spanGamut.after",e,r,t)}),x.subParser("strikethrough",function(e,t,a){"use strict";return t.strikethrough&&(e=(e=a.converter._dispatch("strikethrough.before",e,t,a)).replace(/(?:~){2}([\s\S]+?)(?:~){2}/g,function(e,r){return r=r,""+(r=t.simplifiedAutoLink?x.subParser("simplifiedAutoLinks")(r,t,a):r)+""}),e=a.converter._dispatch("strikethrough.after",e,t,a)),e}),x.subParser("stripLinkDefinitions",function(i,l,c){"use strict";function e(e,r,t,a,n,s,o){return r=r.toLowerCase(),i.toLowerCase().split(r).length-1<2?e:(t.match(/^data:.+?\/.+?;base64,/)?c.gUrls[r]=t.replace(/\s/g,""):c.gUrls[r]=x.subParser("encodeAmpsAndAngles")(t,l,c),s?s+o:(o&&(c.gTitles[r]=o.replace(/"|'/g,""")),l.parseImgDimensions&&a&&n&&(c.gDimensions[r]={width:a,height:n}),""))}return i=(i=(i=(i+="¨0").replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n\n|(?=¨0)|(?=\n\[))/gm,e)).replace(/^ {0,3}\[([^\]]+)]:[ \t]*\n?[ \t]*\s]+)>?(?: =([*\d]+[A-Za-z%]{0,4})x([*\d]+[A-Za-z%]{0,4}))?[ \t]*\n?[ \t]*(?:(\n*)["|'(](.+?)["|')][ \t]*)?(?:\n+|(?=¨0))/gm,e)).replace(/¨0/,"")}),x.subParser("tables",function(e,y,P){"use strict";if(!y.tables)return e;function r(e){for(var r=e.split("\n"),t=0;t"+(n=x.subParser("spanGamut")(n,y,P))+"\n"));for(t=0;t"+x.subParser("spanGamut")(i,y,P)+"\n"));h.push(_)}for(var m=d,f=h,b="\n\n\n",w=m.length,k=0;k\n\n\n",k=0;k\n";for(var v=0;v\n"}return b+="\n
\n"}return e=(e=(e=(e=P.converter._dispatch("tables.before",e,y,P)).replace(/\\(\|)/g,x.helper.escapeCharactersCallback)).replace(/^ {0,3}\|?.+\|.+\n {0,3}\|?[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*:?[ \t]*(?:[-=]){2,}[\s\S]+?(?:\n\n|¨0)/gm,r)).replace(/^ {0,3}\|.+\|[ \t]*\n {0,3}\|[ \t]*:?[ \t]*(?:[-=]){2,}[ \t]*:?[ \t]*\|[ \t]*\n( {0,3}\|.+\|[ \t]*\n)*(?:\n|¨0)/gm,r),e=P.converter._dispatch("tables.after",e,y,P)}),x.subParser("underline",function(e,r,t){"use strict";return r.underline?(e=t.converter._dispatch("underline.before",e,r,t),e=(e=r.literalMidWordUnderscores?(e=e.replace(/\b___(\S[\s\S]*?)___\b/g,function(e,r){return""+r+""})).replace(/\b__(\S[\s\S]*?)__\b/g,function(e,r){return""+r+""}):(e=e.replace(/___(\S[\s\S]*?)___/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/__(\S[\s\S]*?)__/g,function(e,r){return/\S$/.test(r)?""+r+"":e})).replace(/(_)/g,x.helper.escapeCharactersCallback),t.converter._dispatch("underline.after",e,r,t)):e}),x.subParser("unescapeSpecialChars",function(e,r,t){"use strict";return e=(e=t.converter._dispatch("unescapeSpecialChars.before",e,r,t)).replace(/¨E(\d+)E/g,function(e,r){r=parseInt(r);return String.fromCharCode(r)}),e=t.converter._dispatch("unescapeSpecialChars.after",e,r,t)}),x.subParser("makeMarkdown.blockquote",function(e,r){"use strict";var t="";if(e.hasChildNodes())for(var a=e.childNodes,n=a.length,s=0;s ")}),x.subParser("makeMarkdown.codeBlock",function(e,r){"use strict";var t=e.getAttribute("language"),e=e.getAttribute("precodenum");return"```"+t+"\n"+r.preList[e]+"\n```"}),x.subParser("makeMarkdown.codeSpan",function(e){"use strict";return"`"+e.innerHTML+"`"}),x.subParser("makeMarkdown.emphasis",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="*";for(var a=e.childNodes,n=a.length,s=0;s",e.hasAttribute("width")&&e.hasAttribute("height")&&(r+=" ="+e.getAttribute("width")+"x"+e.getAttribute("height")),e.hasAttribute("title")&&(r+=' "'+e.getAttribute("title")+'"'),r+=")"),r}),x.subParser("makeMarkdown.links",function(e,r){"use strict";var t="";if(e.hasChildNodes()&&e.hasAttribute("href")){for(var a=e.childNodes,n=a.length,t="[",s=0;s"),e.hasAttribute("title")&&(t+=' "'+e.getAttribute("title")+'"'),t+=")"}return t}),x.subParser("makeMarkdown.list",function(e,r,t){"use strict";var a="";if(!e.hasChildNodes())return"";for(var n=e.childNodes,s=n.length,o=e.getAttribute("start")||1,i=0;i"+r.preList[e]+""}),x.subParser("makeMarkdown.strikethrough",function(e,r){"use strict";var t="";if(e.hasChildNodes()){t+="~~";for(var a=e.childNodes,n=a.length,s=0;str>th"),s=e.querySelectorAll("tbody>tr"),o=0;o/g,"\\$1>")).replace(/^#/gm,"\\#")).replace(/^(\s*)([-=]{3,})(\s*)$/,"$1\\$2$3")).replace(/^( {0,3}\d+)\./gm,"$1\\.")).replace(/^( {0,3})([+-])/gm,"$1\\$2")).replace(/]([\s]*)\(/g,"\\]$1\\(")).replace(/^ {0,3}\[([\S \t]*?)]:/gm,"\\[$1]:")});"function"==typeof define&&define.amd?define(function(){"use strict";return x}):"undefined"!=typeof module&&module.exports?module.exports=x:this.showdown=x}.call(this); 3 | //# sourceMappingURL=showdown.min.js.map 4 | -------------------------------------------------------------------------------- /script.js: -------------------------------------------------------------------------------- 1 | // ==UserScript== 2 | // @name Unedit and Undelete for Reddit 3 | // @namespace http://tampermonkey.net/ 4 | // @version 3.17.4 5 | // @description Creates the option next to edited and deleted Reddit comments/posts to show the original comment from before it was edited 6 | // @author Jonah Lawrence (DenverCoder1) 7 | // @grant none 8 | // @require https://cdn.jsdelivr.net/npm/showdown@2.1.0/dist/showdown.min.js 9 | // @license MIT 10 | // @icon https://raw.githubusercontent.com/DenverCoder1/unedit-for-reddit/master/images/logo512.png 11 | // @match https://*.reddit.com/ 12 | // @match https://*.reddit.com/me/f/* 13 | // @match https://*.reddit.com/message/* 14 | // @match https://*.reddit.com/r/* 15 | // @match https://*.reddit.com/user/* 16 | // @exclude https://*.reddit.com/*/about/banned* 17 | // @exclude https://*.reddit.com/*/about/contributors* 18 | // @exclude https://*.reddit.com/*/about/edit* 19 | // @exclude https://*.reddit.com/*/about/flair* 20 | // @exclude https://*.reddit.com/*/about/log* 21 | // @exclude https://*.reddit.com/*/about/moderators* 22 | // @exclude https://*.reddit.com/*/about/muted* 23 | // @exclude https://*.reddit.com/*/about/rules* 24 | // @exclude https://*.reddit.com/*/about/stylesheet* 25 | // @exclude https://*.reddit.com/*/about/traffic* 26 | // @exclude https://*.reddit.com/*/wiki/* 27 | // @exclude https://mod.reddit.com/* 28 | // ==/UserScript== 29 | 30 | /* jshint esversion: 8 */ 31 | 32 | (function () { 33 | "use strict"; 34 | 35 | /** 36 | * The current version of the script 37 | * @type {string} 38 | */ 39 | const VERSION = "3.17.4"; 40 | 41 | /** 42 | * Whether or not we are on old reddit and not redesign. 43 | * This will be set in the "load" event listener. 44 | * @type {boolean} 45 | */ 46 | let isOldReddit = false; 47 | 48 | /** 49 | * Whether or not we are on compact mode. 50 | * This will be set in the "load" event listener. 51 | * @type {boolean} 52 | */ 53 | let isCompact = false; 54 | 55 | /** 56 | * Timeout to check for new edited comments on page. 57 | * This will be updated when scrolling. 58 | * @type {number?} 59 | */ 60 | let scriptTimeout = null; 61 | 62 | /** 63 | * The element that is currently requesting content 64 | * @type {Element?} 65 | */ 66 | let currentLoading = null; 67 | 68 | /** 69 | * List of submission ids of edited posts. 70 | * Used on Reddit redesign since the submissions are not marked as such. 71 | * This is set in the "load" event listener from the Reddit JSON API. 72 | * @type {Array<{id: string, edited: float}>} 73 | */ 74 | let editedSubmissions = []; 75 | 76 | /** 77 | * The current URL that is being viewed. 78 | * On Redesign, this can change without the user leaving page, 79 | * so we want to look for new edited submissions if it changes. 80 | * @type {string} 81 | */ 82 | let currentURL = window.location.href; 83 | 84 | /** 85 | * Showdown markdown converter 86 | * @type {showdown.Converter} 87 | */ 88 | const mdConverter = new showdown.Converter({ 89 | tables: true, 90 | simplifiedAutoLink: true, 91 | literalMidWordUnderscores: true, 92 | strikethrough: true, 93 | ghCodeBlocks: true, 94 | disableForced4SpacesIndentedSublists: true, 95 | }); 96 | 97 | /** 98 | * Logging methods for displaying formatted logs in the console. 99 | * 100 | * logging.info("This is an info message"); 101 | * logging.warn("This is a warning message"); 102 | * logging.error("This is an error message"); 103 | * logging.table({a: 1, b: 2, c: 3}); 104 | */ 105 | const logging = { 106 | INFO: "info", 107 | WARN: "warn", 108 | ERROR: "error", 109 | TABLE: "table", 110 | 111 | /** 112 | * Log a message to the console 113 | * @param {string} level The console method to use e.g. "log", "info", "warn", "error", "table" 114 | * @param {...string} messages - Any number of messages to log 115 | */ 116 | _format_log(level, ...messages) { 117 | const logger = level in console ? console[level] : console.log; 118 | logger(`%c[unedit-for-reddit] %c[${level.toUpperCase()}]`, "color: #00b6b6", "color: #888800", ...messages); 119 | }, 120 | 121 | /** 122 | * Log an info message to the console 123 | * @param {...string} messages - Any number of messages to log 124 | */ 125 | info(...messages) { 126 | logging._format_log(this.INFO, ...messages); 127 | }, 128 | 129 | /** 130 | * Log a warning message to the console 131 | * @param {...string} messages - Any number of messages to log 132 | */ 133 | warn(...messages) { 134 | logging._format_log(this.WARN, ...messages); 135 | }, 136 | 137 | /** 138 | * Log an error message to the console 139 | * @param {...string} messages - Any number of messages to log 140 | */ 141 | error(...messages) { 142 | logging._format_log(this.ERROR, ...messages); 143 | }, 144 | 145 | /** 146 | * Log a table to the console 147 | * @param {Object} data - The table to log 148 | */ 149 | table(data) { 150 | logging._format_log(this.TABLE, data); 151 | }, 152 | }; 153 | 154 | /** 155 | * Storage methods for saving and retrieving data from local storage. 156 | * 157 | * Use the storage API or chrome.storage API if available, otherwise use localStorage. 158 | * 159 | * storage.get("key").then((value) => { ... }); 160 | * storage.get("key", "default value").then((value) => { ... }); 161 | * storage.set("key", "value").then(() => { ... }); 162 | */ 163 | const storage = { 164 | /** 165 | * Get a value from storage 166 | * @param {string} key - The key to retrieve 167 | * @param {string?} defaultValue - The default value to return if the key does not exist 168 | * @returns {Promise} A promise that resolves with the value 169 | */ 170 | get(key, defaultValue = null) { 171 | // retrieve from storage API 172 | if (storage._isBrowserStorageAvailable()) { 173 | logging.info(`Retrieving '${key}' from browser.storage.local`); 174 | return browser.storage.local.get(key).then((result) => { 175 | return result[key] || localStorage.getItem(key) || defaultValue; 176 | }); 177 | } else if (storage._isChromeStorageAvailable()) { 178 | logging.info(`Retrieving '${key}' from chrome.storage.local`); 179 | return new Promise((resolve) => { 180 | chrome.storage.local.get(key, (result) => { 181 | resolve(result[key] || localStorage.getItem(key) || defaultValue); 182 | }); 183 | }); 184 | } else { 185 | logging.info(`Retrieving '${key}' from localStorage`); 186 | return Promise.resolve(localStorage.getItem(key) || defaultValue); 187 | } 188 | }, 189 | 190 | /** 191 | * Set a value in storage 192 | * @param {string} key - The key to set 193 | * @param {string} value - The value to set 194 | * @returns {Promise} A promise that resolves when the value is set 195 | */ 196 | set(key, value) { 197 | if (storage._isBrowserStorageAvailable()) { 198 | logging.info(`Storing '${key}' in browser.storage.local`); 199 | return browser.storage.local.set({ [key]: value }); 200 | } else if (storage._isChromeStorageAvailable()) { 201 | logging.info(`Storing '${key}' in chrome.storage.local`); 202 | return new Promise((resolve) => { 203 | chrome.storage.local.set({ [key]: value }, resolve); 204 | }); 205 | } else { 206 | logging.info(`Storing '${key}' in localStorage`); 207 | return Promise.resolve(localStorage.setItem(key, value)); 208 | } 209 | }, 210 | 211 | /** 212 | * Return whether browser.storage is available 213 | * @returns {boolean} Whether browser.storage is available 214 | */ 215 | _isBrowserStorageAvailable() { 216 | return typeof browser !== "undefined" && browser.storage; 217 | }, 218 | 219 | /** 220 | * Return whether chrome.storage is available 221 | * @returns {boolean} Whether chrome.storage is available 222 | */ 223 | _isChromeStorageAvailable() { 224 | return typeof chrome !== "undefined" && chrome.storage; 225 | }, 226 | }; 227 | 228 | /** 229 | * Parse the URL for the submission ID and comment ID if it exists. 230 | * @returns {{submissionId: string|null, commentId: string|null}} 231 | */ 232 | function parseURL() { 233 | const match = window.location.href.match(/\/comments\/([A-Za-z0-9]+)\/(?:.*?\/([A-Za-z0-9]+))?/); 234 | return { 235 | submissionId: (match && match[1]) || null, 236 | commentId: (match && match[2]) || null, 237 | }; 238 | } 239 | 240 | /** 241 | * Find the ID of a comment or submission. 242 | * @param {Element} innerEl An element inside the comment. 243 | * @returns {string} The Reddit ID of the comment. 244 | */ 245 | function getPostId(innerEl) { 246 | let postId = ""; 247 | // redesign 248 | if (!isOldReddit) { 249 | const post = innerEl?.closest("[class*='t1_'], [class*='t3_']"); 250 | if (post) { 251 | postId = Array.from(post.classList).filter(function (el) { 252 | return el.indexOf("t1_") > -1 || el.indexOf("t3_") > -1; 253 | })[0]; 254 | } else { 255 | // if post not found, try to find the post id in the URL 256 | const parsedURL = parseURL(); 257 | postId = parsedURL.commentId || parsedURL.submissionId || postId; 258 | } 259 | } 260 | // old reddit 261 | else if (!isCompact) { 262 | // old reddit comment 263 | postId = innerEl?.closest(".thing")?.id.replace("thing_", ""); 264 | // old reddit submission 265 | if (!postId && isInSubmission(innerEl)) { 266 | const match = window.location.href.match(/comments\/([A-Za-z0-9]{5,8})\//); 267 | postId = match ? match[1] : null; 268 | // submission in list view 269 | if (!postId) { 270 | const thing = innerEl.closest(".thing"); 271 | postId = thing?.id.replace("thing_", ""); 272 | } 273 | } 274 | // if still not found, check for the .reportform element 275 | if (!postId) { 276 | postId = innerEl?.closest(".entry")?.querySelector(".reportform")?.className.replace(/.*t1/, "t1"); 277 | } 278 | // if still not found check the url 279 | if (!postId) { 280 | const parsedURL = parseURL(); 281 | postId = parsedURL.commentId || parsedURL.submissionId || postId; 282 | } 283 | // otherwise log an error 284 | if (!postId) { 285 | logging.error("Could not find post id", innerEl); 286 | postId = ""; 287 | } 288 | } 289 | // compact 290 | else { 291 | const thing = innerEl?.closest(".thing"); 292 | if (thing) { 293 | const idClass = [...thing.classList].find((c) => c.startsWith("id-")); 294 | postId = idClass ? idClass.replace("id-", "") : ""; 295 | } 296 | // if not found, check the url 297 | if (!postId) { 298 | const parsedURL = parseURL(); 299 | postId = parsedURL.commentId || parsedURL.submissionId || postId; 300 | } 301 | } 302 | // if the post appears on the page after the last 3 characters are removed, remove them 303 | const reMatch = postId.match(/(t1_\w+)\w{3}/) || postId.match(/(t3_\w+)\w{3}/); 304 | if (reMatch && document.querySelector(`.${reMatch[1]}, #thing_${reMatch[1]}`)) { 305 | postId = reMatch[1]; 306 | } 307 | return postId; 308 | } 309 | 310 | /** 311 | * Get the container of the comment or submission body for appending the original comment to. 312 | * @param {string} postId The ID of the comment or submission 313 | * @returns {Element} The container element of the comment or submission body. 314 | */ 315 | function getPostBodyElement(postId) { 316 | let bodyEl = null, 317 | baseEl = null; 318 | // redesign 319 | if (!isOldReddit) { 320 | baseEl = document.querySelector(`#${postId}, .Comment.${postId}`); 321 | // in post preview popups, the id will appear again but in #overlayScrollContainer 322 | const popupEl = document.querySelector(`#overlayScrollContainer .Post.${postId}`); 323 | baseEl = popupEl ? popupEl : baseEl; 324 | if (baseEl) { 325 | if (baseEl.getElementsByClassName("RichTextJSON-root").length > 0) { 326 | bodyEl = baseEl.getElementsByClassName("RichTextJSON-root")[0]; 327 | } else if (isInSubmission(baseEl) && baseEl?.firstElementChild?.lastElementChild) { 328 | const classicBodyEl = baseEl.querySelector(`div[data-adclicklocation="background"]`); 329 | if (classicBodyEl) { 330 | bodyEl = classicBodyEl; 331 | } else { 332 | bodyEl = baseEl.firstElementChild.lastElementChild; 333 | if (bodyEl.childNodes.length === 1) { 334 | bodyEl = bodyEl.firstElementChild; 335 | } 336 | } 337 | } else { 338 | bodyEl = baseEl; 339 | } 340 | } else { 341 | // check for a paragraph with the text "That Comment Is Missing" 342 | const missingCommentEl = document.querySelectorAll(`div > div > svg:first-child + p`); 343 | [...missingCommentEl].some(function (el) { 344 | if (el.innerText === "That Comment Is Missing") { 345 | bodyEl = el.parentElement; 346 | return true; 347 | } 348 | }); 349 | } 350 | } 351 | // old reddit 352 | else if (!isCompact) { 353 | // old reddit comments 354 | baseEl = document.querySelector(`form[id*='${postId}'] .md`); 355 | if (baseEl?.closest(".entry")) { 356 | bodyEl = baseEl; 357 | } else { 358 | baseEl = document.querySelector(".report-" + postId); 359 | bodyEl = baseEl 360 | ? baseEl.closest(".entry").querySelector(".usertext") 361 | : document.querySelector("p#noresults"); 362 | } 363 | // old reddit submissions 364 | if (!bodyEl) { 365 | bodyEl = 366 | document.querySelector("div[data-url] .entry form .md") || 367 | document.querySelector("div[data-url] .entry form .usertext-body") || 368 | document.querySelector("div[data-url] .entry .top-matter"); 369 | } 370 | // link view 371 | if (!bodyEl) { 372 | bodyEl = document.querySelector(`.id-${postId}`); 373 | } 374 | } 375 | // compact view 376 | else { 377 | bodyEl = document.querySelector(`.id-${postId} .md, .id-${postId} form.usertext`); 378 | // if not found, check for the .usertext element containing it as part of its id 379 | if (!bodyEl) { 380 | bodyEl = document.querySelector(".showOriginal")?.parentElement; 381 | } 382 | } 383 | return bodyEl; 384 | } 385 | 386 | /** 387 | * Check if surrounding elements imply element is in a selftext submission. 388 | * @param {Element} innerEl An element inside the post to check. 389 | * @returns {boolean} Whether or not the element is in a selftext submission 390 | */ 391 | function isInSubmission(innerEl) { 392 | const selectors = [ 393 | "a.thumbnail", // old reddit on profile page or list view 394 | "div[data-url]", // old reddit on submission page 395 | ".Post", // redesign 396 | ]; 397 | // class list of .thing contains id-t3_... 398 | const thing = innerEl?.closest(".thing"); 399 | if (thing) { 400 | const idClass = [...thing.classList].find((c) => c.startsWith("id-")); 401 | if (idClass) { 402 | return idClass.startsWith("id-t3_"); 403 | } 404 | } 405 | return Boolean(innerEl.closest(selectors.join(", "))); 406 | } 407 | 408 | /** 409 | * Check if the element bounds are within the window bounds. 410 | * @param {Element} element The element to check 411 | * @returns {boolean} Whether or not the element is within the window 412 | */ 413 | function isInViewport(element) { 414 | const rect = element.getBoundingClientRect(); 415 | return ( 416 | rect.top >= 0 && 417 | rect.left >= 0 && 418 | rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) && 419 | rect.right <= (window.innerWidth || document.documentElement.clientWidth) 420 | ); 421 | } 422 | 423 | /** 424 | * Generate HTML from markdown for a comment or submission. 425 | * @param {string} postType The type of post - "comment" or "post" (submission) 426 | * @param {string} original The markdown to convert 427 | * @returns {string} The HTML of the markdown 428 | */ 429 | function redditPostToHTML(postType, original) { 430 | // fix Reddit tables to have at least two dashes per cell in the alignment row 431 | let body = original.replace(/(?<=^\s*|\|\s*)(:?)-(:?)(?=\s*\|[-|\s:]*$)/gm, "$1--$2"); 432 | // convert superscripts in the form "^(some text)" or "^text" to text 433 | const multiwordSuperscriptRegex = /\^\((.+?)\)/gm; 434 | while (multiwordSuperscriptRegex.test(body)) { 435 | body = body.replace(multiwordSuperscriptRegex, "$1"); 436 | } 437 | const superscriptRegex = /\^(\S+)/gm; 438 | while (superscriptRegex.test(body)) { 439 | body = body.replace(superscriptRegex, "$1"); 440 | } 441 | // convert user and subreddit mentions to links (can be /u/, /r/, u/, or r/) 442 | body = body.replace(/(?<=^|[^\w\/])(\/?)([ur]\/\w+)/gm, "[$1$2](/$2)"); 443 | // add spaces after '>' to keep blockquotes (if it has '>!' ignore since that is spoilertext) 444 | body = body.replace(/^((?:>|>)+)(?=[^!\s])/gm, function (match, p1) { 445 | return p1.replace(/>/g, ">") + " "; 446 | }); 447 | // convert markdown to HTML 448 | let html = mdConverter.makeHtml("\n\n### Original " + postType + ":\n\n" + body); 449 | // convert Reddit spoilertext 450 | html = html.replace( 451 | /(?<=^|\s|>)>!(.+?)!<(?=$|\s|<)/gm, 452 | "$1" 453 | ); 454 | // replace ​ with a zero-width space 455 | return html.replace(/&#x200B;/g, "\u200B"); 456 | } 457 | 458 | /** 459 | * Create a new paragraph containing the body of the original comment/post. 460 | * @param {Element} commentBodyElement The container element of the comment/post body. 461 | * @param {string} postType The type of post - "comment" or "post" (submission) 462 | * @param {object} postData The archived data of the original comment/post. 463 | * @param {Boolean} includeBody Whether or not to include the body of the original comment/post. 464 | */ 465 | function showOriginalComment(commentBodyElement, postType, postData, includeBody) { 466 | const originalBody = typeof postData?.body === "string" ? postData.body : postData?.selftext; 467 | // create paragraph element 468 | const origBodyEl = document.createElement("p"); 469 | origBodyEl.className = "og"; 470 | // set text 471 | origBodyEl.innerHTML = includeBody ? redditPostToHTML(postType, originalBody) : ""; 472 | // author and date details 473 | const detailsEl = document.createElement("div"); 474 | detailsEl.style.fontSize = "12px"; 475 | detailsEl.appendChild(document.createTextNode("Posted by ")); 476 | const authorEl = document.createElement("a"); 477 | authorEl.href = `/user/${postData.author}`; 478 | authorEl.innerText = postData.author; 479 | detailsEl.appendChild(authorEl); 480 | detailsEl.appendChild(document.createTextNode(" · ")); 481 | const dateEl = document.createElement("a"); 482 | dateEl.href = postData.permalink; 483 | dateEl.title = new Date(postData.created_utc * 1000).toString(); 484 | dateEl.innerText = getRelativeTime(postData.created_utc); 485 | detailsEl.appendChild(dateEl); 486 | // append horizontal rule if the original body is shown 487 | if (includeBody) { 488 | origBodyEl.appendChild(document.createElement("hr")); 489 | } 490 | // append to original comment 491 | origBodyEl.appendChild(detailsEl); 492 | const existingOg = commentBodyElement.querySelector(".og"); 493 | if (existingOg && includeBody) { 494 | // if there is an existing paragraph and this element contains the body, replace it 495 | existingOg.replaceWith(origBodyEl); 496 | } else if (!existingOg) { 497 | // if there is no existing paragraph, append it 498 | commentBodyElement.appendChild(origBodyEl); 499 | } 500 | // scroll into view 501 | setTimeout(function () { 502 | if (!isInViewport(origBodyEl)) { 503 | origBodyEl.scrollIntoView({ behavior: "smooth" }); 504 | } 505 | }, 500); 506 | // Redesign 507 | if (!isOldReddit) { 508 | // Make sure collapsed submission previews are expanded to not hide the original comment. 509 | commentBodyElement.parentElement.style.maxHeight = "unset"; 510 | } 511 | // Old reddit 512 | else { 513 | // If the comment is collapsed, expand it so the original comment is visible 514 | expandComment(commentBodyElement); 515 | } 516 | } 517 | 518 | /** 519 | * Expand comment if it is collapsed (on old reddit only). 520 | * @param {Element} innerEl An element inside the comment. 521 | */ 522 | function expandComment(innerEl) { 523 | const collapsedComment = innerEl.closest(".collapsed"); 524 | if (collapsedComment) { 525 | collapsedComment.classList.remove("collapsed"); 526 | collapsedComment.classList.add("noncollapsed"); 527 | } 528 | } 529 | 530 | /** 531 | * Handle show original event given the post to show content for. 532 | * @param {Element} linkEl The link element for showing the status. 533 | * @param {object} out The response from the API. 534 | * @param {object} post The archived data of the original comment/post. 535 | * @param {string} postId The ID of the original comment/post. 536 | * @param {Boolean} includeBody Whether or not to include the body of the original comment/post. 537 | */ 538 | function handleShowOriginalEvent(linkEl, out, post, postId, includeBody) { 539 | // locate comment body 540 | const commentBodyElement = getPostBodyElement(postId); 541 | // check that comment was fetched and body element exists 542 | if (!commentBodyElement) { 543 | // the comment body element was not found 544 | linkEl.innerText = "body element not found"; 545 | linkEl.title = "Please report this issue to the developer on GitHub."; 546 | logging.error("Body element not found:", out); 547 | } else if (typeof post?.body === "string") { 548 | // create new paragraph containing the body of the original comment 549 | showOriginalComment(commentBodyElement, "comment", post, includeBody); 550 | // remove loading status from comment 551 | linkEl.innerText = ""; 552 | linkEl.removeAttribute("title"); 553 | logging.info("Successfully loaded comment."); 554 | } else if (typeof post?.selftext === "string") { 555 | // check if result has selftext instead of body (it is a submission post) 556 | // create new paragraph containing the selftext of the original submission 557 | showOriginalComment(commentBodyElement, "post", post, includeBody); 558 | // remove loading status from post 559 | linkEl.innerText = ""; 560 | linkEl.removeAttribute("title"); 561 | logging.info("Successfully loaded post."); 562 | } else if (out?.data?.length === 0) { 563 | // data was returned empty 564 | linkEl.innerText = "not found"; 565 | linkEl.title = "No matching results were found in the Pushshift archive."; 566 | logging.warn("No results:", out); 567 | } else if (out?.data?.length > 0) { 568 | // no matching comment/post was found in the data 569 | linkEl.innerText = "not found"; 570 | linkEl.title = "The comment/post was not found in the Pushshift archive."; 571 | logging.warn("No matching post:", out); 572 | } else { 573 | // other issue occurred with displaying comment 574 | if (linkEl.innerText === "fetch failed") { 575 | const errorLink = linkEl.parentElement.querySelector(".error-link"); 576 | const linkToPushshift = errorLink || document.createElement("a"); 577 | linkToPushshift.target = "_blank"; 578 | linkToPushshift.style = `text-decoration: underline; 579 | cursor: pointer; 580 | margin-left: 6px; 581 | font-style: normal; 582 | font-weight: bold; 583 | color: #e5766e;`; 584 | linkToPushshift.className = linkEl.className; 585 | linkToPushshift.classList.add("error-link"); 586 | linkToPushshift.href = out?.detail 587 | ? "https://api.pushshift.io/signup" 588 | : "https://www.reddit.com/r/pushshift/"; 589 | linkToPushshift.innerText = out?.detail || "CHECK r/PUSHSHIFT FOR MORE INFO"; 590 | if (errorLink === null) { 591 | linkEl.parentElement.appendChild(linkToPushshift); 592 | } 593 | // unhide token container if token is missing or invalid 594 | if (out?.detail) { 595 | const tokenContainer = document.querySelector("#tokenContainer"); 596 | tokenContainer.style.display = "block"; 597 | storage.set("hideTokenContainer", "false"); 598 | } 599 | } 600 | linkEl.innerText = "fetch failed"; 601 | linkEl.title = "A Pushshift error occurred. Please check r/pushshift for updates."; 602 | logging.error("Fetch failed:", out); 603 | } 604 | } 605 | 606 | /** 607 | * Fetch alternative that runs fetch from the window context using a helper element. 608 | * 609 | * This is necessary because in Firefox the headers are not sent when running fetch from the addon context. 610 | * 611 | * @param {string} url The URL to fetch. 612 | * @param {object} options The options to pass to fetch. 613 | * @returns {Promise} The fetch promise. 614 | */ 615 | function inlineFetch(url, options) { 616 | const outputContainer = document.createElement("div"); 617 | outputContainer.id = "outputContainer" + Math.floor(Math.random() * Math.pow(10, 10)); 618 | outputContainer.style.display = "none"; 619 | document.body.appendChild(outputContainer); 620 | const responseContainer = document.createElement("div"); 621 | responseContainer.id = "responseContainer" + Math.floor(Math.random() * Math.pow(10, 10)); 622 | responseContainer.style.display = "none"; 623 | document.body.appendChild(responseContainer); 624 | const temp = document.createElement("button"); 625 | temp.setAttribute("type", "button"); 626 | temp.setAttribute( 627 | "onclick", 628 | `fetch("${url}", ${JSON.stringify(options)}) 629 | .then(r => { 630 | document.querySelector("#${responseContainer.id}").innerText = JSON.stringify({ 631 | ok: r.ok, 632 | status: r.status, 633 | statusText: r.statusText, 634 | headers: Object.fromEntries(r.headers.entries()), 635 | }); 636 | return r.text(); 637 | }) 638 | .then(t => document.querySelector("#${outputContainer.id}").innerText = t)` 639 | ); 640 | temp.style.display = "none"; 641 | document.body.appendChild(temp); 642 | temp.click(); 643 | // wait for fetch to complete and return a promise 644 | return new Promise((resolve) => { 645 | const interval = setInterval(() => { 646 | if (outputContainer.innerText && responseContainer.innerText) { 647 | clearInterval(interval); 648 | const responseData = JSON.parse(responseContainer.innerText); 649 | const mockResponse = { 650 | text: () => outputContainer.innerText, 651 | json: () => JSON.parse(outputContainer.innerText), 652 | ok: responseData.ok, 653 | status: responseData.status, 654 | statusText: responseData.statusText, 655 | headers: { 656 | get: (header) => responseData.headers[header], 657 | }, 658 | }; 659 | resolve(mockResponse); 660 | outputContainer.remove(); 661 | responseContainer.remove(); 662 | temp.remove(); 663 | } 664 | }, 100); 665 | }); 666 | } 667 | 668 | /** 669 | * Create a link to view the original comment/post. 670 | * @param {Element} innerEl An element inside the comment or post to create a link for. 671 | */ 672 | function createLink(innerEl) { 673 | // if there is already a link, don't create another unless the other was a show author link 674 | if (innerEl.parentElement.querySelector("a.showOriginal:not(.showAuthorOnly)")) { 675 | return; 676 | } 677 | // create link to "Show orginal" or "Show author" 678 | const showAuthor = innerEl.classList.contains("showAuthorOnly"); 679 | const showLinkEl = document.createElement("a"); 680 | showLinkEl.innerText = showAuthor ? "Show author" : "Show original"; 681 | showLinkEl.className = innerEl.className + " showOriginal"; 682 | showLinkEl.classList.remove("error"); 683 | showLinkEl.style.textDecoration = "underline"; 684 | showLinkEl.style.cursor = "pointer"; 685 | showLinkEl.style.marginLeft = "6px"; 686 | showLinkEl.title = "Click to show data from the original post or comment"; 687 | innerEl.parentElement.appendChild(showLinkEl); 688 | innerEl.classList.add("match"); 689 | // find id of selected comment or submission 690 | const postId = getPostId(showLinkEl); 691 | showLinkEl.alt = `View original post for ID ${postId}`; 692 | if (!postId) { 693 | showLinkEl.parentElement.removeChild(showLinkEl); 694 | } 695 | // click event 696 | showLinkEl.addEventListener( 697 | "click", 698 | async function () { 699 | // allow only 1 request at a time 700 | if (typeof currentLoading != "undefined" && currentLoading !== null) { 701 | return; 702 | } 703 | // create url for getting comment/post from pushshift api 704 | const URLs = []; 705 | const idURL = isInSubmission(this) 706 | ? `https://api.pushshift.io/reddit/search/submission/?ids=${postId}&fields=selftext,author,id,created_utc,permalink` 707 | : `https://api.pushshift.io/reddit/search/comment/?ids=${postId}&fields=body,author,id,link_id,created_utc,permalink`; 708 | URLs.push(idURL); 709 | // create url for getting author comments/posts from pushshift api 710 | const author = this.parentElement.querySelector("a[href*=user]")?.innerText; 711 | if (author) { 712 | const authorURL = isInSubmission(this) 713 | ? `https://api.pushshift.io/reddit/search/submission/?author=${author}&size=200&fields=selftext,author,id,created_utc,permalink` 714 | : `https://api.pushshift.io/reddit/search/comment/?author=${author}&size=200&fields=body,author,id,link_id,created_utc,permalink`; 715 | URLs.push(authorURL); 716 | } 717 | // if the author is unknown, check the parent post as an alternative instead 718 | else if (!isInSubmission(this)) { 719 | const parsedURL = parseURL(); 720 | if (parsedURL.submissionId) { 721 | const parentURL = `https://api.pushshift.io/reddit/comment/search?q=*&link_id=${parsedURL.submissionId}&size=200&fields=body,author,id,link_id,created_utc,permalink`; 722 | URLs.push(parentURL); 723 | } 724 | } 725 | 726 | // set loading status 727 | currentLoading = this; 728 | this.innerText = "loading..."; 729 | this.title = "Loading data from the original post or comment"; 730 | 731 | logging.info(`Fetching from ${URLs.join(" and ")}`); 732 | 733 | const token = document.querySelector("#apiToken").value; 734 | 735 | // request from pushshift api 736 | await Promise.all( 737 | URLs.map((url) => 738 | fetch(url, { 739 | method: "GET", 740 | headers: { 741 | "Content-Type": "application/json", 742 | "User-Agent": "Unedit and Undelete for Reddit", 743 | accept: "application/json", 744 | Authorization: `Bearer ${token}`, 745 | }, 746 | }) 747 | .then((response) => { 748 | if (!response.ok) { 749 | logging.error("Response not ok:", response); 750 | } 751 | try { 752 | return response.json(); 753 | } catch (e) { 754 | throw Error(`Invalid JSON Response: ${response}`); 755 | } 756 | }) 757 | .catch((error) => { 758 | logging.error("Error:", error); 759 | }) 760 | ) 761 | ) 762 | .then((responses) => { 763 | responses.forEach((out) => { 764 | // locate the comment that was being loaded 765 | const loading = currentLoading; 766 | // exit if already found 767 | if (loading.innerText === "") { 768 | return; 769 | } 770 | const post = out?.data?.find((p) => p?.id === postId?.split("_").pop()); 771 | logging.info("Response:", { author, id: postId, post, data: out?.data }); 772 | const includeBody = !loading.classList.contains("showAuthorOnly"); 773 | handleShowOriginalEvent(loading, out, post, postId, includeBody); 774 | }); 775 | }) 776 | .catch(function (err) { 777 | throw err; 778 | }); 779 | 780 | // reset status 781 | currentLoading = null; 782 | }, 783 | false 784 | ); 785 | } 786 | 787 | /** 788 | * Convert unix timestamp in seconds to a relative time string (e.g. "2 hours ago"). 789 | * @param {number} timestamp A unix timestamp in seconds. 790 | * @returns {string} A relative time string. 791 | */ 792 | function getRelativeTime(timestamp) { 793 | const time = new Date(timestamp * 1000); 794 | const now = new Date(); 795 | const seconds = Math.round((now.getTime() - time.getTime()) / 1000); 796 | const minutes = Math.round(seconds / 60); 797 | const hours = Math.round(minutes / 60); 798 | const days = Math.round(hours / 24); 799 | const months = Math.round(days / 30.5); 800 | const years = Math.round(days / 365); 801 | if (years > 0 && months >= 12) { 802 | return `${years} ${years === 1 ? "year" : "years"} ago`; 803 | } 804 | if (months > 0 && days >= 30) { 805 | return `${months} ${months === 1 ? "month" : "months"} ago`; 806 | } 807 | if (days > 0 && hours >= 24) { 808 | return `${days} ${days === 1 ? "day" : "days"} ago`; 809 | } 810 | if (hours > 0 && minutes >= 60) { 811 | return `${hours} ${hours === 1 ? "hour" : "hours"} ago`; 812 | } 813 | if (minutes > 0 && seconds >= 60) { 814 | return `${minutes} ${minutes === 1 ? "minute" : "minutes"} ago`; 815 | } 816 | return "just now"; 817 | } 818 | 819 | /** 820 | * Locate comments and add links to each. 821 | */ 822 | function findEditedComments() { 823 | // when function runs, cancel timeout 824 | if (scriptTimeout) { 825 | scriptTimeout = null; 826 | } 827 | // list elements to check for edited or deleted status 828 | let selectors = [], 829 | elementsToCheck = [], 830 | editedComments = []; 831 | // redesign 832 | if (!isOldReddit) { 833 | // check for edited/deleted comments and deleted submissions 834 | selectors = [ 835 | ".Comment div:first-of-type span:not([data-text]):not(.found)", // Comments "edited..." or "Comment deleted/removed..." 836 | ".Post div div div:last-of-type div ~ div:last-of-type:not([data-text]):not(.found)", // Submissions "It doesn't appear in any feeds..." message 837 | ".Post > div:only-child > div:nth-of-type(5) > div:last-of-type > div:not([data-text]):only-child:not(.found)", // Submissions "Sorry, this post is no longer available." message 838 | ".Comment div.RichTextJSON-root > p:only-child:not([data-text]):not(.found)", // Comments "[unavailable]" message 839 | "div > div > svg:first-child + p:not(.found)", // "That Comment Is Missing" page 840 | ]; 841 | elementsToCheck = Array.from(document.querySelectorAll(selectors.join(", "))); 842 | editedComments = elementsToCheck.filter(function (el) { 843 | el.classList.add("found"); 844 | // we only care about the element if it has no children 845 | if (el.children.length) { 846 | return false; 847 | } 848 | // there are only specific phrases we care about in a P element 849 | if ( 850 | el.tagName === "P" && 851 | el.innerText !== "[unavailable]" && 852 | el.innerText !== "[ Removed by Reddit ]" && 853 | el.innerText !== "That Comment Is Missing" 854 | ) { 855 | return false; 856 | } 857 | // include "[unavailable]" comments (blocked by user) if from a deleted user 858 | const isUnavailable = 859 | el.innerText === "[unavailable]" && 860 | el?.parentElement?.parentElement?.parentElement 861 | ?.querySelector("div") 862 | ?.innerText?.includes("[deleted]"); 863 | const isEditedOrRemoved = 864 | el.innerText.substring(0, 6) === "edited" || // include edited comments 865 | el.innerText.substring(0, 15) === "Comment deleted" || // include comments deleted by user 866 | el.innerText.substring(0, 15) === "Comment removed" || // include comments removed by moderator 867 | el.innerText.substring(0, 30) === "It doesn't appear in any feeds" || // include deleted submissions 868 | el.innerText.substring(0, 23) === "Moderators remove posts" || // include submissions removed by moderators 869 | isUnavailable || // include unavailable comments (blocked by user) 870 | el.innerText === "[ Removed by Reddit ]" || // include comments removed by Reddit 871 | el.innerText === "That Comment Is Missing" || // include comments not found in comment tree 872 | el.innerText.substring(0, 29) === "Sorry, this post is no longer"; // include unavailable submissions (blocked by user) 873 | const isDeletedAuthor = el.innerText === "[deleted]"; // include comments from deleted users 874 | // if the element has a deleted author, make a link to only show the deleted author 875 | if (isDeletedAuthor) { 876 | el.classList.add("showAuthorOnly"); 877 | } 878 | // keep element if it is edited or removed or if it has a deleted author 879 | return isEditedOrRemoved || isDeletedAuthor; 880 | }); 881 | // Edited submissions found using the Reddit API 882 | editedSubmissions.forEach((submission) => { 883 | let found = false; 884 | const postId = submission.id; 885 | const editedAt = submission.edited; 886 | const deletedAuthor = submission.deletedAuthor; 887 | const deletedPost = submission.deletedPost; 888 | selectors = [ 889 | `#t3_${postId} > div:first-of-type > div:nth-of-type(2) > div:first-of-type > div:first-of-type > span:first-of-type:not(.found)`, // Submission page 890 | `#t3_${postId} > div:first-of-type > div:nth-of-type(2) > div:first-of-type > div:first-of-type > div:first-of-type > div:first-of-type > span:first-of-type:not(.found)`, // Comment context page 891 | `#t3_${postId} > div:last-of-type[data-click-id] > div:first-of-type > div:first-of-type > div:first-of-type:not(.found)`, // Subreddit listing view 892 | `.Post.t3_${postId} > div:last-of-type[data-click-id] > div:first-of-type > div:nth-of-type(2) > div:not([data-adclicklocation]):first-of-type:not(.found)`, // Profile/home/classic listing view 893 | `.Post.t3_${postId} > div:first-of-type > div[data-click-id="background"] > div:first-of-type > div[data-click-id="body"] > div[data-adclicklocation="top_bar"]:not(.found)`, // Compact listing view 894 | `.Post.t3_${postId} > div:last-of-type[data-click-id] > div:first-of-type > div:nth-of-type(2) div[data-adclicklocation="top_bar"]:not(.found)`, // Profile/home listing view 895 | `.Post.t3_${postId}:not(.scrollerItem) > div:first-of-type > div:nth-of-type(2) > div:nth-of-type(2) > div:first-of-type > div:first-of-type:not(.found)`, // Preview popup 896 | ]; 897 | Array.from(document.querySelectorAll(selectors.join(", "))).forEach((el) => { 898 | // add found class so that it won't be checked again in the future 899 | el.classList.add("found"); 900 | // if this is the first time we've found this post, add it to the list of posts to add the link to 901 | if (!found) { 902 | found = true; 903 | editedComments.push(el); 904 | if (editedAt) { 905 | if (!el.parentElement.querySelector(".edited-date")) { 906 | // display when the post was edited 907 | const editedDateElement = document.createElement("span"); 908 | editedDateElement.classList.add("edited-date"); 909 | editedDateElement.style.fontStyle = "italic"; 910 | editedDateElement.innerText = ` \u00b7 edited ${getRelativeTime(editedAt)}`; // middle-dot = \u00b7 911 | el.parentElement.appendChild(editedDateElement); 912 | } 913 | } else if (deletedAuthor && !deletedPost) { 914 | // if the post was not edited, make a link to only show the deleted author 915 | el.classList.add("showAuthorOnly"); 916 | } 917 | } 918 | }); 919 | }); 920 | // If the url has changed, check for edited submissions again 921 | // This is an async fetch that will check for edited submissions again when it is done 922 | if (currentURL !== window.location.href) { 923 | logging.info(`URL changed from ${currentURL} to ${window.location.href}`); 924 | currentURL = window.location.href; 925 | checkForEditedSubmissions(); 926 | } 927 | } 928 | // old Reddit and compact Reddit 929 | else { 930 | selectors = [ 931 | ".entry p.tagline time:not(.found)", // Comment or Submission "last edited" timestamp 932 | ".entry p.tagline em:not(.found), .entry .tagline span:first-of-type:not(.flair):not(.found)", // Comment "[deleted]" author 933 | "div[data-url] p.tagline span:first-of-type:not(.flair):not(.found)", // Submission "[deleted]" author 934 | "div[data-url] .usertext-body em:not(.found), form.usertext em:not(.found)", // Submission "[removed]" body 935 | ".entry .usertext .usertext-body > div.md > p:only-child:not(.found)", // Comment "[unavailable]" body 936 | "p#noresults", // "there doesn't seem to be anything here" page 937 | ]; 938 | elementsToCheck = Array.from(document.querySelectorAll(selectors.join(", "))); 939 | editedComments = elementsToCheck.filter(function (el) { 940 | el.classList.add("found"); 941 | // The only messages we care about in a P element right now is "[unavailable]" or #noresults 942 | if ( 943 | el.tagName === "P" && 944 | el.innerText !== "[unavailable]" && 945 | el.innerText !== "[ Removed by Reddit ]" && 946 | el.id !== "noresults" 947 | ) { 948 | return false; 949 | } 950 | // include "[unavailable]" comments (blocked by user) if from a deleted user 951 | const isUnavailable = 952 | el.innerText === "[unavailable]" && 953 | el?.closest(".entry").querySelector(".tagline").innerText.includes("[deleted]"); 954 | const isEditedRemovedOrDeletedAuthor = 955 | el.title.substring(0, 11) === "last edited" || // include edited comments or submissions 956 | el.innerText === "[deleted]" || // include comments or submissions deleted by user 957 | el.innerText === "[removed]" || // include comments or submissions removed by moderator 958 | el.innerText === "[ Removed by Reddit ]" || // include comments or submissions removed by Reddit 959 | el.id === "noresults" || // include "there doesn't seem to be anything here" page 960 | isUnavailable; // include unavailable submissions (blocked by user) 961 | // if the element is a deleted author and not edited or removed, only show the deleted author 962 | if ( 963 | el.innerText === "[deleted]" && 964 | el.tagName.toUpperCase() === "SPAN" && // tag name is span (not em as it appears for deleted comments) 965 | ["[deleted]", "[removed]"].indexOf(el.closest(".entry")?.querySelector(".md")?.innerText) === -1 // content of post is not deleted or removed 966 | ) { 967 | el.classList.add("showAuthorOnly"); 968 | } 969 | // keep element if it is edited or removed or if it has a deleted author 970 | return isEditedRemovedOrDeletedAuthor; 971 | }); 972 | } 973 | // create links 974 | editedComments.forEach(function (el) { 975 | // for removed submissions, add the link to an element in the tagline instead of the body 976 | if (el.closest(".usertext-body") && el.innerText === "[removed]") { 977 | el = el.closest(".entry")?.querySelector("p.tagline span:first-of-type") || el; 978 | } 979 | createLink(el); 980 | }); 981 | } 982 | 983 | /** 984 | * If the script timeout is not already set, set it and 985 | * run the findEditedComments in a second, otherwise do nothing. 986 | */ 987 | function waitAndFindEditedComments() { 988 | if (!scriptTimeout) { 989 | scriptTimeout = setTimeout(findEditedComments, 1000); 990 | } 991 | } 992 | 993 | /** 994 | * Check for edited submissions using the Reddit JSON API. 995 | * 996 | * Since the Reddit Redesign website does not show if a submission was edited, 997 | * we will check the data in the Reddit JSON API for the information. 998 | */ 999 | function checkForEditedSubmissions() { 1000 | // don't need to check if we're not on a submission page or list view 1001 | if (!document.querySelector(".Post, .ListingLayout-backgroundContainer")) { 1002 | return; 1003 | } 1004 | // append .json to the page URL but before the ? 1005 | const [url, query] = window.location.href.split("?"); 1006 | const jsonUrl = `${url}.json` + (query ? `?${query}` : ""); 1007 | logging.info(`Fetching additional info from ${jsonUrl}`); 1008 | fetch(jsonUrl, { 1009 | method: "GET", 1010 | headers: { 1011 | "Content-Type": "application/json", 1012 | "User-Agent": "Unedit and Undelete for Reddit", 1013 | }, 1014 | }) 1015 | .then(function (response) { 1016 | if (!response.ok) { 1017 | throw new Error(`${response.status} ${response.statusText}`); 1018 | } 1019 | return response.json(); 1020 | }) 1021 | .then(function (data) { 1022 | logging.info("Response:", data); 1023 | const out = data?.length ? data[0] : data; 1024 | const children = out?.data?.children; 1025 | if (children) { 1026 | editedSubmissions = children 1027 | .filter(function (post) { 1028 | return post.kind === "t3" && (post.data.edited || post.data.author === "[deleted]"); 1029 | }) 1030 | .map(function (post) { 1031 | return { 1032 | id: post.data.id, 1033 | edited: post.data.edited, 1034 | deletedAuthor: post.data.author === "[deleted]", 1035 | deletedPost: post.data.selftext === "[deleted]" || post.data.selftext === "[removed]", 1036 | }; 1037 | }); 1038 | logging.info("Edited submissions:", editedSubmissions); 1039 | setTimeout(findEditedComments, 1000); 1040 | } 1041 | }) 1042 | .catch(function (error) { 1043 | logging.error(`Error fetching additional info from ${jsonUrl}`, error); 1044 | }); 1045 | } 1046 | 1047 | // check for new comments when you scroll 1048 | window.addEventListener("scroll", waitAndFindEditedComments, true); 1049 | 1050 | // check for new comments when you click 1051 | document.body.addEventListener("click", waitAndFindEditedComments, true); 1052 | 1053 | // add additional styling, find edited comments, and set old reddit status on page load 1054 | function init() { 1055 | // output the version number to the console 1056 | logging.info(`Unedit and Undelete for Reddit v${VERSION}`); 1057 | // determine if reddit is old or redesign 1058 | isOldReddit = /old\.reddit/.test(window.location.href) || !!document.querySelector("#header-img"); 1059 | isCompact = document.querySelector("#header-img-a")?.href?.endsWith(".compact") || false; 1060 | // upgrade insecure requests 1061 | document.head.insertAdjacentHTML( 1062 | "beforeend", 1063 | `` 1064 | ); 1065 | // Reddit redesign 1066 | if (!isOldReddit) { 1067 | // fix styling of created paragraphs in new reddit 1068 | document.head.insertAdjacentHTML( 1069 | "beforeend", 1070 | `` 1188 | ); 1189 | // listen for spoilertext in original body to be revealed 1190 | window.addEventListener( 1191 | "click", 1192 | function (e) { 1193 | /** 1194 | * @type {HTMLSpanElement} 1195 | */ 1196 | const spoiler = e.target.closest("span.md-spoiler-text"); 1197 | if (spoiler) { 1198 | spoiler.classList.add("revealed"); 1199 | spoiler.removeAttribute("title"); 1200 | spoiler.style.cursor = "auto"; 1201 | } 1202 | }, 1203 | false 1204 | ); 1205 | // check for edited submissions 1206 | checkForEditedSubmissions(); 1207 | } 1208 | // Old Reddit 1209 | else { 1210 | // fix styling of created paragraphs in old reddit 1211 | document.head.insertAdjacentHTML( 1212 | "beforeend", 1213 | `` 1263 | ); 1264 | } 1265 | // find edited comments 1266 | findEditedComments(); 1267 | 1268 | // create an input field in the bottom right corner of the screen for the api token 1269 | document.head.insertAdjacentHTML( 1270 | "beforeend", 1271 | `` 1315 | ); 1316 | const tokenInput = document.createElement("input"); 1317 | tokenInput.type = "text"; 1318 | tokenInput.id = "apiToken"; 1319 | tokenInput.placeholder = "Pushshift API Token"; 1320 | // if there is a token saved in local storage, use it 1321 | storage.get("apiToken").then((token) => { 1322 | if (token) { 1323 | tokenInput.value = token; 1324 | } 1325 | }); 1326 | const requestTokenLink = document.createElement("a"); 1327 | requestTokenLink.href = "https://api.pushshift.io/signup"; 1328 | requestTokenLink.target = "_blank"; 1329 | requestTokenLink.rel = "noopener noreferrer"; 1330 | requestTokenLink.textContent = "Request Token"; 1331 | requestTokenLink.id = "requestTokenLink"; 1332 | const saveButton = document.createElement("button"); 1333 | saveButton.textContent = "Save"; 1334 | saveButton.id = "saveButton"; 1335 | saveButton.addEventListener("click", function () { 1336 | // save in local storage 1337 | storage.set("apiToken", tokenInput.value); 1338 | }); 1339 | tokenInput.addEventListener("keydown", function (e) { 1340 | if (e.key === "Enter") { 1341 | saveButton.click(); 1342 | } 1343 | }); 1344 | const closeButton = document.createElement("button"); 1345 | closeButton.textContent = "\u00D7"; // times symbol 1346 | closeButton.id = "closeButton"; 1347 | const tokenContainer = document.createElement("div"); 1348 | tokenContainer.id = "tokenContainer"; 1349 | tokenContainer.appendChild(tokenInput); 1350 | tokenContainer.appendChild(saveButton); 1351 | tokenContainer.appendChild(requestTokenLink); 1352 | tokenContainer.appendChild(closeButton); 1353 | closeButton.addEventListener("click", function () { 1354 | // set the token container to display none 1355 | tokenContainer.style.display = "none"; 1356 | // save preference in local storage 1357 | storage.set("hideTokenContainer", "true"); 1358 | }); 1359 | // if the user has hidden the token container before, hide it again 1360 | storage.get("hideTokenContainer").then((hideTokenContainer) => { 1361 | if (hideTokenContainer === "true") { 1362 | tokenContainer.style.display = "none"; 1363 | } 1364 | }); 1365 | document.body.appendChild(tokenContainer); 1366 | 1367 | // switch from fetch to inlineFetch if browser is Firefox 1368 | if (navigator.userAgent.includes("Firefox")) { 1369 | fetch = inlineFetch; 1370 | } 1371 | } 1372 | 1373 | // if the window is loaded, run init(), otherwise wait for it to load 1374 | if (document.readyState === "complete") { 1375 | init(); 1376 | } else { 1377 | window.addEventListener("load", init, false); 1378 | } 1379 | })(); 1380 | --------------------------------------------------------------------------------