├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── prettier.config.js ├── rollup.config.js ├── src ├── background │ ├── background.js │ ├── css │ │ ├── chrome_shared.css │ │ └── widgets.css │ ├── icon.png │ ├── icon_128.png │ ├── icon_gray.png │ ├── manifest.json │ ├── options.html │ ├── options.js │ ├── twitter_button.js │ └── twitter_widgets.js ├── content │ ├── index.ts │ ├── lib │ │ ├── extract.ts │ │ ├── iframe.ts │ │ ├── readable.ts │ │ └── scroll.ts │ ├── style │ │ ├── toast.css │ │ └── toc.css │ ├── toc.ts │ ├── types.ts │ ├── ui │ │ ├── handle.ts │ │ ├── index.ts │ │ └── toc_content.ts │ └── util │ │ ├── assert.ts │ │ ├── debug.ts │ │ ├── decorator.ts │ │ ├── dom │ │ ├── css.ts │ │ ├── depth.ts │ │ ├── px.ts │ │ └── to_array.ts │ │ ├── env.ts │ │ ├── event.ts │ │ ├── logger.ts │ │ ├── math │ │ └── between.ts │ │ ├── stream.ts │ │ └── toast.ts └── types │ └── modules.d.ts ├── test └── list.md ├── tsconfig.json └── yarn.lock /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | dist 4 | *.log 5 | *.zip 6 | .rpt2_cache 7 | /chrome 8 | /firefox -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "{}" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright {yyyy} {name of copyright owner} 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Simple Outliner / 智能网页大纲 (for Chrome), 💥 Support Inoreader and Feedly 2 | 3 | > Simple Outliner / 自动生成网页大纲、目录,Support Inoreader and Feedly。 4 | 5 | ## Download 6 | 7 | - Chrome extension: [Simple Outliner / 智能网页大纲](https://chrome.google.com/webstore/detail/smart-toc-%E6%99%BA%E8%83%BD%E7%BD%91%E9%A1%B5%E5%A4%A7%E7%BA%B2/ppdjhggfcaenclmimmdigbcglfoklgaf) 8 | 9 | ## Build 10 | 11 | Requires `node.js` and `yarn` 12 | 13 | ```bash 14 | # install dependencies 15 | yarn 16 | 17 | # build and bundle the extension as `.zip` for release 18 | yarn run build 19 | 20 | # build/watch the extension for local development (please use Chrome's `Load unpacked extension` to load `/dist` folder) 21 | yarn run start 22 | ``` 23 | 24 | Thank you for using the extension. Any suggestions/issues/problems are welcomed! 25 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "smart-toc", 3 | "version": "0.0.1", 4 | "description": "an chrome extension that generates a table of content for webpage", 5 | "main": "src/js/index.js", 6 | "scripts": { 7 | "clean": "rm -rf dist/* firefox/* chrome/*", 8 | "start": "export ENV=development && yarn run build:background && rollup --config rollup.config.js --watch", 9 | "lint:watch": "tsc --watch", 10 | "build": "export ENV=production && yarn run clean; yarn run build:content && yarn run build:background && yarn run build:chrome && yarn run build:firefox", 11 | "build:content": "rollup --config rollup.config.js", 12 | "build:background": "mkdir -p dist && cp -R src/background/* dist/", 13 | "build:chrome": "mkdir -p chrome && zip -r chrome/smart-toc.zip dist", 14 | "build:firefox": "yarn run build:firefox:package && yarn run build:firefox:source", 15 | "build:firefox:package": "web-ext build --overwrite-dest --artifacts-dir ./firefox --source-dir ./dist/ ", 16 | "build:firefox:source": "zip -r ./firefox/smart-toc_source.zip src package.json README.md tsconfig.json rollup.config.js yarn.lock" 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/FallenMax/smart-toc.git" 21 | }, 22 | "keywords": [ 23 | "chrome", 24 | "extension", 25 | "table-of-content", 26 | "toc", 27 | "outline", 28 | "outliner" 29 | ], 30 | "author": "FallenMax@gmail.com", 31 | "license": "ISC", 32 | "bugs": { 33 | "url": "https://github.com/FallenMax/smart-toc/issues" 34 | }, 35 | "homepage": "https://github.com/FallenMax/smart-toc#readme", 36 | "devDependencies": { 37 | "@types/chrome": "^0.0.154", 38 | "@types/mithril": "^2.0.8", 39 | "rollup": "2.56.0", 40 | "rollup-plugin-commonjs": "^10.1.0", 41 | "rollup-plugin-node-resolve": "^5.2.0", 42 | "rollup-plugin-replace": "2.2.0", 43 | "rollup-plugin-string": "^3.0.0", 44 | "rollup-plugin-typescript": "^1.0.1", 45 | "rollup-plugin-typescript2": "^0.30.0", 46 | "typescript": "^4.3.5", 47 | "web-ext": "^6.2.0" 48 | }, 49 | "dependencies": { 50 | "mithril": "^2.0.4", 51 | "pcf-start": "^1.6.5", 52 | "tslib": "^2.3.0" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /prettier.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | semi: false, 3 | singleQuote: true, 4 | trailingComma: 'all', 5 | arrowParens: 'always', 6 | } 7 | -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import replace from 'rollup-plugin-replace' 2 | import { string } from 'rollup-plugin-string' 3 | import nodeResolve from 'rollup-plugin-node-resolve' 4 | import commonjs from 'rollup-plugin-commonjs' 5 | import ts from 'rollup-plugin-typescript2' 6 | 7 | export default { 8 | input: 'src/content/index.ts', 9 | output: { 10 | format: 'iife', 11 | file: 'dist/toc.js', 12 | name: 'smarttoc', 13 | }, 14 | plugins: [ 15 | replace({ 16 | 'process.env': JSON.stringify({ 17 | ENV: process.env.ENV, 18 | }), 19 | }), 20 | ts(), 21 | nodeResolve({ main: true, browser: true }), 22 | commonjs(), 23 | string({ include: '**/*.css' }), 24 | ], 25 | } 26 | -------------------------------------------------------------------------------- /src/background/background.js: -------------------------------------------------------------------------------- 1 | const getCurrentTab = (cb) => { 2 | chrome.tabs.query({ active: true, currentWindow: true }, ([activeTab]) => { 3 | cb(activeTab) 4 | }) 5 | } 6 | 7 | const execOnCurrentTab = (command) => { 8 | getCurrentTab((tab) => { 9 | if (tab && tab.url.indexOf("chrome") !== 0) { 10 | chrome.tabs.sendMessage(tab.id, command, {}, (response) => { 11 | if (!chrome.runtime.lastError) { 12 | // console.log({response}) 13 | // content_script 正常加载 14 | } else { 15 | if (command === 'toggle' && response === undefined) { 16 | chrome.scripting.executeScript({ 17 | target: {tabId: tab.id, allFrames: true}, 18 | files: ['toc.js'] 19 | },()=>{ 20 | chrome.tabs.sendMessage(tab.id, command, {}, (response) => { }) // load then send again 21 | }) 22 | } 23 | } 24 | }) 25 | } 26 | }) 27 | } 28 | 29 | chrome.action.onClicked.addListener(() => execOnCurrentTab('toggle')) 30 | chrome.commands.onCommand.addListener((command) => execOnCurrentTab(command)) 31 | 32 | chrome.runtime.onInstalled.addListener(async () => { 33 | chrome.contextMenus.create({ 34 | id: "position_menu", 35 | title: "Reset TOC Position", 36 | type: 'normal', 37 | contexts: ["all"], 38 | }); 39 | let url = chrome.runtime.getURL("options.html"); 40 | await chrome.tabs.create({ url }); 41 | }); 42 | 43 | chrome.contextMenus.onClicked.addListener(function (item, tab) { 44 | if (item.menuItemId === 'position_menu') { 45 | if (chrome.storage) { 46 | chrome.storage.local.set({ "smarttoc_offset": { x: 0, y: 0 } }); 47 | execOnCurrentTab('refresh') 48 | } 49 | } 50 | }); 51 | 52 | chrome.action.setIcon({ 53 | path: "icon_gray.png" 54 | }); 55 | 56 | chrome.runtime.onMessage.addListener(function (request, sender, sendResponse) { 57 | getCurrentTab(tab=>{ 58 | if (tab) { 59 | if(request == 'unload'){ 60 | chrome.action.setIcon({ 61 | tabId : tab.id, 62 | path: "icon_gray.png" 63 | }); 64 | } 65 | else if(request === 'load'){ 66 | chrome.action.setIcon({ 67 | tabId : tab.id, 68 | path: "icon.png" 69 | }); 70 | } 71 | } 72 | }); 73 | sendResponse(true) 74 | }); -------------------------------------------------------------------------------- /src/background/css/chrome_shared.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 | * Use of this source code is governed by a BSD-style license that can be 3 | * found in the LICENSE file. */ 4 | 5 | /* CSS has been written by The Chromium Authors but modified and 6 | * enhanced by Ram Swaroop. */ 7 | 8 | /* This file holds CSS that should be shared, in theory, by all user-visible 9 | * chrome:// pages. */ 10 | 11 | @import url("widgets.css"); 12 | 13 | 14 | /* Prevent CSS from overriding the hidden property. */ 15 | [hidden] { 16 | display: none !important; 17 | } 18 | 19 | html.loading * { 20 | -webkit-transition-delay: 0 !important; 21 | -webkit-transition-duration: 0 !important; 22 | } 23 | 24 | body { 25 | cursor: default; 26 | margin: 0; 27 | font-family:'Segoe UI', Tahoma, sans-serif; 28 | font-size: 75%; 29 | color:rgb(48, 57, 66) 30 | } 31 | 32 | p { 33 | line-height: 1.8em; 34 | } 35 | 36 | h1, 37 | h2, 38 | h3 { 39 | -webkit-user-select: none; 40 | font-weight: normal; 41 | /* Makes the vertical size of the text the same for all fonts. */ 42 | line-height: 1; 43 | } 44 | 45 | h1 { 46 | font-size: 1.5em; 47 | } 48 | 49 | h2 { 50 | font-size: 1.3em; 51 | margin-bottom: 0.4em; 52 | } 53 | 54 | h3 { 55 | font-size: 1.2em; 56 | margin-bottom: 0.8em; 57 | } 58 | 59 | a { 60 | color: rgb(17, 85, 204); 61 | text-decoration: underline; 62 | } 63 | 64 | a:active { 65 | color: rgb(5, 37, 119); 66 | } 67 | 68 | /* Elements that need to be LTR even in an RTL context, but should align 69 | * right. (Namely, URLs, search engine names, etc.) 70 | */ 71 | html[dir='rtl'] .weakrtl { 72 | direction: ltr; 73 | text-align: right; 74 | } 75 | 76 | /* Input fields in search engine table need to be weak-rtl. Since those input 77 | * fields are generated for all cr.ListItem elements (and we only want weakrtl 78 | * on some), the class needs to be on the enclosing div. 79 | */ 80 | html[dir='rtl'] div.weakrtl input { 81 | direction: ltr; 82 | text-align: right; 83 | } 84 | 85 | html[dir='rtl'] .favicon-cell.weakrtl { 86 | -webkit-padding-end: 22px; 87 | -webkit-padding-start: 0; 88 | } 89 | 90 | /* weakrtl for selection drop downs needs to account for the fact that 91 | * Webkit does not honor the text-align attribute for the select element. 92 | * (See Webkit bug #40216) 93 | */ 94 | html[dir='rtl'] select.weakrtl { 95 | direction: rtl; 96 | } 97 | 98 | html[dir='rtl'] select.weakrtl option { 99 | direction: ltr; 100 | } 101 | 102 | /* WebKit does not honor alignment for text specified via placeholder attribute. 103 | * This CSS is a workaround. Please remove once WebKit bug is fixed. 104 | * https://bugs.webkit.org/show_bug.cgi?id=63367 105 | */ 106 | html[dir='rtl'] input.weakrtl::-webkit-input-placeholder, 107 | html[dir='rtl'] .weakrtl input::-webkit-input-placeholder { 108 | direction: rtl; 109 | } 110 | 111 | /* Horizontal line */ 112 | hr{ 113 | border:0; 114 | height:0; 115 | border-top:1px solid rgba(0, 0, 0, 0.1); 116 | border-bottom:1px solid rgba(255, 255, 255, 0.1) 117 | } 118 | /* Different sizes */ 119 | .small{ 120 | font-size:11px 121 | } 122 | .large{ 123 | font-size:18px 124 | } 125 | .x-large{ 126 | font-size:24px 127 | } 128 | .xx-large{ 129 | font-size:30px 130 | } -------------------------------------------------------------------------------- /src/background/css/widgets.css: -------------------------------------------------------------------------------- 1 | /* Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 | * Use of this source code is governed by a BSD-style license that can be 3 | * found in the LICENSE file. */ 4 | 5 | /* CSS has been written by The Chromium Authors but modified and 6 | * enhanced by Ram Swaroop. */ 7 | 8 | /* This file defines styles for form controls. The order of rule blocks is 9 | * important as there are some rules with equal specificity that rely on order 10 | * as a tiebreaker. These are marked with OVERRIDE. */ 11 | 12 | /* Default state **************************************************************/ 13 | 14 | :-webkit-any(button, 15 | input[type='button'], 16 | input[type='reset'], 17 | input[type='submit']):not(.custom-appearance):not(.link-button), 18 | select, 19 | input[type='checkbox'], 20 | input[type='radio'] { 21 | -webkit-appearance: none; 22 | -webkit-user-select: none; 23 | background-image: -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); 24 | border: 1px solid rgba(0, 0, 0, 0.25); 25 | border-radius: 2px; 26 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.08), 27 | inset 0 1px 2px rgba(255, 255, 255, 0.75); 28 | color: #444; 29 | font: inherit; 30 | margin: 0 1px 0 0; 31 | text-shadow: 0 1px 0 rgb(240, 240, 240); 32 | } 33 | 34 | :-webkit-any(button, 35 | input[type='button'], 36 | input[type='reset'], 37 | input[type='submit']):not(.custom-appearance):not(.link-button), 38 | select { 39 | min-height: 2em; 40 | min-width: 4em; 41 | /* The following platform-specific rule is necessary to get adjacent 42 | * buttons, text inputs, and so forth to align on their borders while also 43 | * aligning on the text's baselines. */ 44 | padding-bottom: 1px; 45 | } 46 | 47 | :-webkit-any(button, 48 | input[type='button'], 49 | input[type='reset'], 50 | input[type='submit']):not(.custom-appearance):not(.link-button) { 51 | -webkit-padding-end: 10px; 52 | -webkit-padding-start: 10px; 53 | } 54 | 55 | select { 56 | -webkit-appearance: none; 57 | -webkit-padding-end: 20px; 58 | -webkit-padding-start: 6px; 59 | /* OVERRIDE */ 60 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAaUlEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEImJmZvwE1/UfHIHGQPNGGAbHCggULFrKxsf1ENgjEB4mD5EnxJoaByAZB5Yk3DNlAPj6+L8gGkWUYzMC3b982IRtEtmFQjaxYxDAwAGi4TwMYKNLfAAAAAElFTkSuQmCC'), 61 | -webkit-linear-gradient(#ededed, #ededed 38%, #dedede); 62 | background-position: right center; 63 | background-repeat: no-repeat; 64 | } 65 | 66 | html[dir='rtl'] select { 67 | background-position: center left; 68 | } 69 | 70 | input[type='checkbox'] { 71 | bottom: 2px; 72 | height: 13px; 73 | position: relative; 74 | vertical-align: middle; 75 | width: 13px; 76 | } 77 | 78 | input[type='radio'] { 79 | /* OVERRIDE */ 80 | border-radius: 100%; 81 | bottom: 3px; 82 | height: 15px; 83 | position: relative; 84 | vertical-align: middle; 85 | width: 15px; 86 | } 87 | 88 | /* TODO(estade): add more types here? */ 89 | input[type='number'], 90 | input[type='password'], 91 | input[type='search'], 92 | input[type='text'], 93 | input[type='url'], 94 | input:not([type]), 95 | textarea { 96 | border: 1px solid #bfbfbf; 97 | border-radius: 2px; 98 | box-sizing: border-box; 99 | color: #444; 100 | font: inherit; 101 | margin: 0; 102 | /* Use min-height to accommodate addditional padding for touch as needed. */ 103 | min-height: 2em; 104 | padding: 3px; 105 | /* For better alignment between adjacent buttons and inputs. */ 106 | padding-bottom: 4px; 107 | } 108 | 109 | input[type='search'] { 110 | -webkit-appearance: textfield; 111 | /* NOTE: Keep a relatively high min-width for this so we don't obscure the end 112 | * of the default text in relatively spacious languages (i.e. German). */ 113 | min-width: 160px; 114 | } 115 | 116 | /* Remove when https://bugs.webkit.org/show_bug.cgi?id=51499 is fixed. 117 | * TODO(dbeam): are there more types that would benefit from this? */ 118 | input[type='search']::-webkit-textfield-decoration-container { 119 | direction: inherit; 120 | } 121 | 122 | /* Checked ********************************************************************/ 123 | 124 | input[type='checkbox']:checked::before { 125 | -webkit-user-select: none; 126 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAsAAAALCAYAAACprHcmAAAAcklEQVQY02NgwA/YoJgoEA/Es4DYgJBCJSBeD8SboRinBiYg7kZS2IosyQ/Eakh8LySFq4FYHFlxGRBvBOJYqMRqJMU+yApNkSRAeC0Sux3dfSCTetE0wKyXxOWhMKhTYIr9CAUXyJMzgLgBagBBgDPGAI2LGdNt0T1AAAAAAElFTkSuQmCC'); 127 | background-size: 100% 100%; 128 | content: ''; 129 | display: block; 130 | height: 100%; 131 | width: 100%; 132 | } 133 | 134 | input[type='radio']:checked::before { 135 | background-color: #666; 136 | border-radius: 100%; 137 | bottom: 3px; 138 | content: ''; 139 | display: block; 140 | left: 3px; 141 | position: absolute; 142 | right: 3px; 143 | top: 3px; 144 | } 145 | 146 | /* Hover **********************************************************************/ 147 | 148 | :enabled:hover:-webkit-any( 149 | select, 150 | input[type='checkbox'], 151 | input[type='radio'], 152 | :-webkit-any( 153 | button, 154 | input[type='button'], 155 | input[type='reset'], 156 | input[type='submit']):not(.custom-appearance):not(.link-button)) { 157 | background-image: -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); 158 | border-color: rgba(0, 0, 0, 0.3); 159 | box-shadow: 0 1px 0 rgba(0, 0, 0, 0.12), 160 | inset 0 1px 2px rgba(255, 255, 255, 0.95); 161 | color: black; 162 | } 163 | 164 | :enabled:hover:-webkit-any(select) { 165 | /* OVERRIDE */ 166 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAaUlEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEImJmZvwE1/UfHIHGQPNGGAbHCggULFrKxsf1ENgjEB4mD5EnxJoaByAZB5Yk3DNlAPj6+L8gGkWUYzMC3b982IRtEtmFQjaxYxDAwAGi4TwMYKNLfAAAAAElFTkSuQmCC'), 167 | -webkit-linear-gradient(#f0f0f0, #f0f0f0 38%, #e0e0e0); 168 | } 169 | 170 | /* Active *********************************************************************/ 171 | 172 | :enabled:active:-webkit-any( 173 | select, 174 | input[type='checkbox'], 175 | input[type='radio'], 176 | :-webkit-any( 177 | button, 178 | input[type='button'], 179 | input[type='reset'], 180 | input[type='submit']):not(.custom-appearance):not(.link-button)) { 181 | background-image: -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); 182 | box-shadow: none; 183 | text-shadow: none; 184 | } 185 | 186 | :enabled:active:-webkit-any(select) { 187 | /* OVERRIDE */ 188 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAaUlEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEImJmZvwE1/UfHIHGQPNGGAbHCggULFrKxsf1ENgjEB4mD5EnxJoaByAZB5Yk3DNlAPj6+L8gGkWUYzMC3b982IRtEtmFQjaxYxDAwAGi4TwMYKNLfAAAAAElFTkSuQmCC'), 189 | -webkit-linear-gradient(#e7e7e7, #e7e7e7 38%, #d7d7d7); 190 | } 191 | 192 | /* Disabled *******************************************************************/ 193 | 194 | :disabled:-webkit-any( 195 | button, 196 | input[type='button'], 197 | input[type='reset'], 198 | input[type='submit']):not(.custom-appearance):not(.link-button), 199 | select:disabled { 200 | background-image: -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); 201 | border-color: rgba(80, 80, 80, 0.2); 202 | box-shadow: 0 1px 0 rgba(80, 80, 80, 0.08), 203 | inset 0 1px 2px rgba(255, 255, 255, 0.75); 204 | color: #aaa; 205 | } 206 | 207 | select:disabled { 208 | /* OVERRIDE */ 209 | background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABMAAAAICAYAAAAbQcSUAAAAWklEQVQoz2P4//8/A7UwdkEGhiggTsODo4g2LBEIGhoa/uPCIHmiDQNihQULFizEZhBIHCRPijexGggzCCpPvGHoBiIbRJZhMAPfvn3bhGwQ2YZBNbJiEcPAAIgGZrTRc1ZLAAAAAElFTkSuQmCC'), 210 | -webkit-linear-gradient(#f1f1f1, #f1f1f1 38%, #e6e6e6); 211 | } 212 | 213 | input:disabled:-webkit-any([type='checkbox'], 214 | [type='radio']) { 215 | opacity: .75; 216 | } 217 | 218 | input:disabled:-webkit-any([type='password'], 219 | [type='search'], 220 | [type='text'], 221 | [type='url'], 222 | :not([type])) { 223 | color: #999; 224 | } 225 | 226 | /* Focus **********************************************************************/ 227 | 228 | :enabled:focus:-webkit-any( 229 | select, 230 | input[type='checkbox'], 231 | input[type='number'], 232 | input[type='password'], 233 | input[type='radio'], 234 | input[type='search'], 235 | input[type='text'], 236 | input[type='url'], 237 | input:not([type]), 238 | :-webkit-any( 239 | button, 240 | input[type='button'], 241 | input[type='reset'], 242 | input[type='submit']):not(.custom-appearance):not(.link-button)) { 243 | /* OVERRIDE */ 244 | -webkit-transition: border-color 200ms; 245 | /* We use border color because it follows the border radius (unlike outline). 246 | * This is particularly noticeable on mac. */ 247 | border-color: rgb(77, 144, 254); 248 | outline: none; 249 | } 250 | 251 | /* Link buttons ***************************************************************/ 252 | 253 | .link-button { 254 | -webkit-box-shadow: none; 255 | background: transparent none; 256 | border: none; 257 | color: rgb(17, 85, 204); 258 | cursor: pointer; 259 | /* Input elements have -webkit-small-control which can override the body font. 260 | * Resolve this by using 'inherit'. */ 261 | font: inherit; 262 | margin: 0; 263 | padding: 0 4px; 264 | } 265 | 266 | .link-button:hover { 267 | text-decoration: underline; 268 | } 269 | 270 | .link-button:active { 271 | color: rgb(5, 37, 119); 272 | text-decoration: underline; 273 | } 274 | 275 | .link-button[disabled] { 276 | color: #999; 277 | cursor: default; 278 | text-decoration: none; 279 | } 280 | 281 | /* Checkbox/radio helpers ****************************************************** 282 | * 283 | * .checkbox and .radio classes wrap labels. Checkboxes and radios should use 284 | * these classes with the markup structure: 285 | * 286 | *
287 | * 291 | *
292 | */ 293 | 294 | :-webkit-any(.checkbox, .radio) label { 295 | /* Don't expand horizontally: . */ 296 | display: -webkit-inline-box; 297 | padding-bottom: 7px; 298 | padding-top: 7px; 299 | } 300 | 301 | :-webkit-any(.checkbox, .radio) label input ~ span { 302 | -webkit-margin-start: 0.6em; 303 | /* Make sure long spans wrap at the same horizontal position they start. */ 304 | display: block; 305 | } 306 | 307 | :-webkit-any(.checkbox, .radio) label:hover { 308 | color: black; 309 | } 310 | 311 | label > input:disabled:-webkit-any([type='checkbox'], [type='radio']) ~ span { 312 | color: #999; 313 | } 314 | -------------------------------------------------------------------------------- /src/background/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/smart-toc/4082315a58005389ab0ba4f0daada9ee2bc6f87a/src/background/icon.png -------------------------------------------------------------------------------- /src/background/icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/smart-toc/4082315a58005389ab0ba4f0daada9ee2bc6f87a/src/background/icon_128.png -------------------------------------------------------------------------------- /src/background/icon_gray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lcomplete/smart-toc/4082315a58005389ab0ba4f0daada9ee2bc6f87a/src/background/icon_gray.png -------------------------------------------------------------------------------- /src/background/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "manifest_version": 3, 3 | "name": "Simple Outliner / 智能网页大纲", 4 | "version": "0.7.3", 5 | "description": "Simple Outliner / 自动生成网页大纲、目录,Support Inoreader and Feedly。", 6 | "options_page": "options.html", 7 | "action": { 8 | "default_icon": "icon.png", 9 | "default_title": "Simple Outliner / 智能网页大纲" 10 | }, 11 | "icons": { 12 | "16": "icon.png", 13 | "128": "icon.png" 14 | }, 15 | "background": { 16 | "service_worker": "background.js" 17 | }, 18 | "content_scripts": [ 19 | { 20 | "matches": [ 21 | "http://*/*", 22 | "https://*/*" 23 | ], 24 | "js": [ 25 | "toc.js" 26 | ], 27 | "run_at": "document_end" 28 | } 29 | ], 30 | "content_security_policy":{"extension_pages": "script-src 'self';object-src 'self';script-src-elem 'self' 'unsafe-inline' https://platform.twitter.com https://syndication.twitter.com;"}, 31 | "commands": { 32 | "toggle": { 33 | "suggested_key": { 34 | "default": "Ctrl+Shift+E", 35 | "mac": "Command+Shift+E", 36 | "chromeos": "Ctrl+Shift+E", 37 | "linux": "Ctrl+Shift+E" 38 | }, 39 | "description": "Load/unload TOC" 40 | }, 41 | "prev": { 42 | "description": "Jump to previous heading" 43 | }, 44 | "next": { 45 | "description": "Jump to next heading" 46 | } 47 | }, 48 | "permissions": [ 49 | "activeTab", 50 | "storage", 51 | "contextMenus", 52 | "scripting" 53 | ], 54 | "host_permissions": [ 55 | "http://*/*", 56 | "https://*/*" 57 | ], 58 | "author": "louchenabc@gmail.com" 59 | } -------------------------------------------------------------------------------- /src/background/options.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Simple Outliner / 智能网页大纲 - Support Inoreader and Feedly 6 | 7 | 8 | 9 | 14 | 15 | 16 | 17 | 18 |
19 |
20 | Simple Outliner Options / 智能网页大纲-选项 21 |
22 | 23 |

Auto Load / 自动加载

24 |
25 |
26 |
27 | 28 | 29 |
30 |
31 |
32 |
33 | 34 | 35 |
36 |
37 |
38 |
39 | 40 | 41 |
42 |
43 | 44 |

UI

45 |
46 | 47 |
48 | 51 |
52 |
53 | 56 |
57 | 58 |

Inoreader and Feedly Support

59 |
60 |
Don't change this unless the web apps change its dom. / 除非它们的网页结构发生了变化,否则不要进行修改。
61 |

Inoreader article querySelector / Inoreader 文章选择器

62 | 63 |

Feedly article querySelector / Feedly 文章选择器

64 | 65 | 66 |
67 |
68 |
69 |
70 | 71 | 72 |
73 |
74 |
75 |
If you like this extension, no need to buy me a coffee, just share to people who need this.
76 |
如果你喜欢这个插件的话,不需要给我买咖啡,分享给有需要的人吧。
77 |
78 | 84 |
85 |
86 | Source Code: https://github.com/lcomplete/smart-toc 87 |
88 |

89 | Original ♥ by FallenMax 90 |
91 | Modified ♥ by lcomplete 92 |

93 |
94 |
95 |
96 |
97 | 98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /src/background/options.js: -------------------------------------------------------------------------------- 1 | // Saves options to chrome.storage 2 | function save_options() { 3 | var autoType = document.querySelector('input[name="auto"]:checked').value; 4 | var showTip = document.getElementById('show-tip').checked; 5 | var rememberPos = document.getElementById('remember-pos').checked; 6 | var selectorInoreader = document.getElementById('selector-inoreader').value; 7 | var selectorFeedly = document.getElementById('selector-feedly').value; 8 | chrome.storage.local.set({ 9 | isShowTip: showTip, 10 | isRememberPos: rememberPos, 11 | autoType: autoType, 12 | selectorInoreader: selectorInoreader, 13 | selectorFeedly, selectorFeedly 14 | }, function() { 15 | var status = document.getElementById('status'); 16 | status.textContent = 'Options saved. / 已保存。'; 17 | setTimeout(function() { 18 | status.textContent = ''; 19 | }, 5000); 20 | }); 21 | } 22 | 23 | // Restores select box and checkbox state using the preferences 24 | // stored in chrome.storage. 25 | function restore_options() { 26 | chrome.storage.local.get({ 27 | isShowTip: true, 28 | isRememberPos: true, 29 | autoType: '0', 30 | selectorInoreader: '.article_content', 31 | selectorFeedly: '.entryBody' 32 | }, function(items) { 33 | document.getElementById('show-tip').checked = items.isShowTip; 34 | document.getElementById('remember-pos').checked = items.isRememberPos; 35 | document.getElementById('auto-'+items.autoType).checked = true; 36 | document.getElementById('selector-inoreader').value = items.selectorInoreader; 37 | document.getElementById('selector-feedly').value = items.selectorFeedly; 38 | }); 39 | } 40 | 41 | function reset_options(){ 42 | chrome.storage.local.clear(); 43 | restore_options(); 44 | } 45 | 46 | document.addEventListener('DOMContentLoaded', restore_options); 47 | var inputs = document.getElementsByTagName('input'); 48 | for (let index = 0; index < inputs.length; index++) { 49 | const input = inputs[index]; 50 | input.addEventListener('change', save_options); 51 | } 52 | 53 | document.getElementById("btnReset").addEventListener('click', reset_options); -------------------------------------------------------------------------------- /src/background/twitter_button.js: -------------------------------------------------------------------------------- 1 | (window.__twttrll=window.__twttrll||[]).push([[3],{169:function(t,e,a){var r=a(39),n=a(171),s=a(7);(r=Object.create(r)).build=s(r.build,null,n),t.exports=r},170:function(t,e,a){var r=a(42),n=a(37),s=a(38),i=a(0),o=a(7),u=a(33),c=a(5),l=a(175);t.exports=function(t){t.params({partner:{fallback:o(u.val,u,"partner")}}),t.define("scribeItems",function(){return{}}),t.define("scribeNamespace",function(){return{client:"tfw"}}),t.define("scribeData",function(){return{widget_origin:s.rootDocumentLocation(),widget_frame:s.isFramed()&&s.currentDocumentLocation(),widget_partner:this.params.partner,widget_site_screen_name:l(u.val("site")),widget_site_user_id:c.asNumber(u.val("site:id")),widget_creator_screen_name:l(u.val("creator")),widget_creator_user_id:c.asNumber(u.val("creator:id"))}}),t.define("scribe",function(t,e,a){t=i.aug(this.scribeNamespace(),t||{}),e=i.aug(this.scribeData(),e||{}),r.scribe(t,e,!1,a)}),t.define("scribeInteraction",function(t,e,a){var r=n.extractTermsFromDOM(t.target);r.action=t.type,"url"===r.element&&(r.element=n.clickEventElement(t.target)),this.scribe(r,e,a)})}},171:function(t,e,a){var r=a(40),n=a(0),s=a(172);function i(){r.apply(this,arguments),this.Widget=this.Component}i.prototype=Object.create(r.prototype),n.aug(i.prototype,{factory:s,build:function(){return r.prototype.build.apply(this,arguments)},selectors:function(t){var e=this.Widget.prototype.selectors;t=t||{},this.Widget.prototype.selectors=n.aug({},t,e)}}),t.exports=i},172:function(t,e,a){var r=a(6),n=a(35),s=a(41),i=a(0),o=a(7),u=a(173),c="twitter-widget-";t.exports=function(){var t=s();function e(e,a){t.apply(this,arguments),this.id=c+u(),this.sandbox=a}return e.prototype=Object.create(t.prototype),i.aug(e.prototype,{selectors:{},hydrate:function(){return r.resolve()},prepForInsertion:function(){},render:function(){return r.resolve()},show:function(){return r.resolve()},resize:function(){return r.resolve()},select:function(t,e){return 1===arguments.length&&(e=t,t=this.el),t?(e=this.selectors[e]||e,i.toRealArray(t.querySelectorAll(e))):[]},selectOne:function(){return this.select.apply(this,arguments)[0]},selectLast:function(){return this.select.apply(this,arguments).pop()},on:function(t,e,a){var r,s=this.el;this.el&&(t=(t||"").split(/\s+/),2===arguments.length?a=e:r=e,r=this.selectors[r]||r,a=o(a,this),t.forEach(r?function(t){n.delegate(s,t,r,a)}:function(t){s.addEventListener(t,a,!1)}))}}),e}},173:function(t,e){var a=0;t.exports=function(){return String(a++)}},174:function(t,e,a){var r=a(5),n=a(0);t.exports=function(t){t.define("widgetDataAttributes",function(){return{}}),t.define("setDataAttributes",function(){var t=this.sandbox.sandboxEl;n.forIn(this.widgetDataAttributes(),function(e,a){r.hasValue(a)&&t.setAttribute("data-"+e,a)})}),t.after("render",function(){this.setDataAttributes()})}},175:function(t,e){t.exports=function(t){return t&&"@"===t[0]?t.substr(1):t}},192:function(t,e){var a=/\{\{([\w_]+)\}\}/g;t.exports=function(t,e){return t.replace(a,function(t,a){return void 0!==e[a]?e[a]:t})}},210:function(t,e,a){var r=a(6),n=a(169),s=a(33),i=a(19),o=a(192),u=a(0),c=a(11),l=a(7),p=a(72),h=a(71),m=p.followButtonHtmlPath,f="Twitter Follow Button",d="twitter-follow-button";function g(t){return"large"===t?"l":"m"}t.exports=n.couple(a(170),a(174),function(t){t.params({screenName:{required:!0},lang:{required:!0,transform:h.matchLanguage,fallback:"en"},size:{fallback:"medium",transform:g},showScreenName:{fallback:!0},showCount:{fallback:!0},partner:{fallback:l(s.val,s,"partner")},count:{},preview:{}}),t.define("getUrlParams",function(){return u.compact({id:this.id,lang:this.params.lang,size:this.params.size,screen_name:this.params.screenName,show_count:"none"!==this.params.count&&this.params.showCount,show_screen_name:this.params.showScreenName,preview:this.params.preview,partner:this.params.partner,dnt:i.enabled(),time:+new Date})}),t.around("widgetDataAttributes",function(t){return u.aug({"screen-name":this.params.screenName},t())}),t.around("scribeNamespace",function(t){return u.aug(t(),{page:"button",section:"follow"})}),t.define("scribeImpression",function(){this.scribe({action:"impression"},{language:this.params.lang,message:[this.params.size,"none"===this.params.count?"nocount":"withcount"].join(":")+":"})}),t.override("render",function(){var t=o(m,{lang:this.params.lang}),e=c.encode(this.getUrlParams()),a=p.resourceBaseUrl+t+"#"+e;return this.scribeImpression(),r.all([this.sandbox.setTitle(f),this.sandbox.addClass(d),this.sandbox.loadDocument(a)])})})},243:function(t,e,a){var r=a(6),n=a(4),s=a(9),i=a(33),o=a(19),u=a(192),c=a(75),l=a(0),p=a(11),h=a(3),m=a(169),f=a(7),d=a(72),g=a(71),b=d.tweetButtonHtmlPath,w="Twitter Tweet Button",v="twitter-tweet-button",y="twitter-share-button",_="twitter-hashtag-button",x="twitter-mention-button",N=["share","hashtag","mention"];function D(t){return"large"===t?"l":"m"}function k(t){return l.contains(N,t)}function z(t){return h.hashTag(t,!1)}function A(t){return/\+/.test(t)&&!/ /.test(t)?t.replace(/\+/g," "):t}t.exports=m.couple(a(170),a(174),function(t){t.params({lang:{required:!0,transform:g.matchLanguage,fallback:"en"},size:{fallback:"medium",transform:D},type:{fallback:"share",validate:k},text:{transform:A},screenName:{transform:h.screenName},buttonHashtag:{transform:z},partner:{fallback:f(i.val,i,"partner")},via:{},related:{},hashtags:{},url:{}}),t.define("getUrlParams",function(){var t=this.params.text,e=this.params.url,a=this.params.via,r=this.params.related,i=c.getScreenNameFromPage();return"share"===this.params.type?(t=t||n.title,e=e||c.getCanonicalURL()||s.href,a=a||i):i&&(r=r?i+","+r:i),l.compact({id:this.id,lang:this.params.lang,size:this.params.size,type:this.params.type,text:t,url:e,via:a,related:r,button_hashtag:this.params.buttonHashtag,screen_name:this.params.screenName,hashtags:this.params.hashtags,partner:this.params.partner,original_referer:s.href,dnt:o.enabled(),time:+new Date})}),t.around("widgetDataAttributes",function(t){return"mention"==this.params.type?l.aug({"screen-name":this.params.screenName},t()):"hashtag"==this.params.type?l.aug({hashtag:this.params.buttonHashtag},t()):l.aug({url:this.params.url},t())}),t.around("scribeNamespace",function(t){return l.aug(t(),{page:"button",section:this.params.type})}),t.define("scribeImpression",function(){this.scribe({action:"impression"},{language:this.params.lang,message:[this.params.size,"nocount"].join(":")+":"})}),t.override("render",function(){var t,e=u(b,{lang:this.params.lang}),a=p.encode(this.getUrlParams()),n=d.resourceBaseUrl+e+"#"+a;switch(this.params.type){case"hashtag":t=_;break;case"mention":t=x;break;default:t=y}return this.scribeImpression(),r.all([this.sandbox.setTitle(w),this.sandbox.addClass(v),this.sandbox.addClass(t),this.sandbox.loadDocument(n)])})})},84:function(t,e,a){var r=a(169);t.exports=r.build([a(210)])},90:function(t,e,a){var r=a(169);t.exports=r.build([a(243)])}}]); -------------------------------------------------------------------------------- /src/background/twitter_widgets.js: -------------------------------------------------------------------------------- 1 | Function&&Function.prototype&&Function.prototype.bind&&(/(MSIE ([6789]|10|11))|Trident/.test(navigator.userAgent)||(window.__twttr&&window.__twttr.widgets&&window.__twttr.widgets.loaded&&window.twttr.widgets.load&&window.twttr.widgets.load(),window.__twttr&&window.__twttr.widgets&&window.__twttr.widgets.init||function(t){function e(e){for(var n,i,o=e[0],s=e[1],a=0,c=[];a-1},forIn:i,isObject:s,isEmptyObject:a,toType:o,isType:function(t,e){return t==o(e)},toRealArray:u}},function(t,e){t.exports=window},function(t,e,n){var r=n(6);t.exports=function(){var t=this;this.promise=new r(function(e,n){t.resolve=e,t.reject=n})}},function(t,e,n){var r=n(11),i=/(?:^|(?:https?:)?\/\/(?:www\.)?twitter\.com(?::\d+)?(?:\/intent\/(?:follow|user)\/?\?screen_name=|(?:\/#!)?\/))@?([\w]+)(?:\?|&|$)/i,o=/(?:^|(?:https?:)?\/\/(?:www\.)?twitter\.com(?::\d+)?\/(?:#!\/)?[\w_]+\/status(?:es)?\/)(\d+)/i,s=/^http(s?):\/\/(\w+\.)*twitter\.com([:/]|$)/i,a=/^http(s?):\/\/(ton|pbs)\.twimg\.com/,u=/^#?([^.,<>!\s/#\-()'"]+)$/,c=/twitter\.com(?::\d{2,4})?\/intent\/(\w+)/,d=/^https?:\/\/(?:www\.)?twitter\.com\/\w+\/timelines\/(\d+)/i,l=/^https?:\/\/(?:www\.)?twitter\.com\/i\/moments\/(\d+)/i,f=/^https?:\/\/(?:www\.)?twitter\.com\/(\w+)\/(?:likes|favorites)/i,h=/^https?:\/\/(?:www\.)?twitter\.com\/(\w+)\/lists\/([\w-%]+)/i,p=/^https?:\/\/(?:www\.)?twitter\.com\/i\/live\/(\d+)/i,m=/^https?:\/\/syndication\.twitter\.com\/settings/i,v=/^https?:\/\/(localhost|platform)\.twitter\.com(?::\d+)?\/widgets\/widget_iframe\.(.+)/i,g=/^https?:\/\/(?:www\.)?twitter\.com\/search\?q=(\w+)/i;function w(t){return"string"==typeof t&&i.test(t)&&RegExp.$1.length<=20}function y(t){if(w(t))return RegExp.$1}function b(t,e){var n=r.decodeURL(t);if(e=e||!1,n.screen_name=y(t),n.screen_name)return r.url("https://twitter.com/intent/"+(e?"follow":"user"),n)}function _(t){return"string"==typeof t&&u.test(t)}function E(t){return"string"==typeof t&&o.test(t)}t.exports={isHashTag:_,hashTag:function(t,e){if(e=void 0===e||e,_(t))return(e?"#":"")+RegExp.$1},isScreenName:w,screenName:y,isStatus:E,status:function(t){return E(t)&&RegExp.$1},intentForProfileURL:b,intentForFollowURL:function(t){return b(t,!0)},isTwitterURL:function(t){return s.test(t)},isTwimgURL:function(t){return a.test(t)},isIntentURL:function(t){return c.test(t)},isSettingsURL:function(t){return m.test(t)},isWidgetIframeURL:function(t){return v.test(t)},isSearchUrl:function(t){return g.test(t)},regexen:{profile:i},momentId:function(t){return l.test(t)&&RegExp.$1},collectionId:function(t){return d.test(t)&&RegExp.$1},intentType:function(t){return c.test(t)&&RegExp.$1},likesScreenName:function(t){return f.test(t)&&RegExp.$1},listScreenNameAndSlug:function(t){var e,n,r;if(h.test(t)){e=RegExp.$1,n=RegExp.$2;try{r=decodeURIComponent(n)}catch(t){}return{ownerScreenName:e,slug:r||n}}return!1},eventId:function(t){return p.test(t)&&RegExp.$1}}},function(t,e){t.exports=document},function(t,e,n){var r=n(0),i=[!0,1,"1","on","ON","true","TRUE","yes","YES"],o=[!1,0,"0","off","OFF","false","FALSE","no","NO"];function s(t){return void 0!==t&&null!==t&&""!==t}function a(t){return c(t)&&t%1==0}function u(t){return c(t)&&!a(t)}function c(t){return s(t)&&!isNaN(t)}function d(t){return r.contains(o,t)}function l(t){return r.contains(i,t)}t.exports={hasValue:s,isInt:a,isFloat:u,isNumber:c,isString:function(t){return"string"===r.toType(t)},isArray:function(t){return s(t)&&"array"==r.toType(t)},isTruthValue:l,isFalseValue:d,asInt:function(t){if(a(t))return parseInt(t,10)},asFloat:function(t){if(u(t))return t},asNumber:function(t){if(c(t))return t},asBoolean:function(t){return!(!s(t)||!l(t)&&(d(t)||!t))}}},function(t,e,n){var r=n(1),i=n(21),o=n(49);i.hasPromiseSupport()||(r.Promise=o),t.exports=r.Promise},function(t,e,n){var r=n(0);t.exports=function(t,e){var n=Array.prototype.slice.call(arguments,2);return function(){var i=r.toRealArray(arguments);return t.apply(e,n.concat(i))}}},function(t,e,n){var r=n(51);t.exports=new r("__twttr")},function(t,e){t.exports=location},function(t,e,n){var r=n(0),i=/\b([\w-_]+)\b/g;function o(t){return new RegExp("\\b"+t+"\\b","g")}function s(t,e){t.classList?t.classList.add(e):o(e).test(t.className)||(t.className+=" "+e)}function a(t,e){t.classList?t.classList.remove(e):t.className=t.className.replace(o(e)," ")}function u(t,e){return t.classList?t.classList.contains(e):r.contains(c(t),e)}function c(t){return r.toRealArray(t.classList?t.classList:t.className.match(i))}t.exports={add:s,remove:a,replace:function(t,e,n){if(t.classList&&u(t,e))return a(t,e),void s(t,n);t.className=t.className.replace(o(e),n)},toggle:function(t,e,n){return void 0===n&&t.classList&&t.classList.toggle?t.classList.toggle(e,n):(n?s(t,e):a(t,e),n)},present:u,list:c}},function(t,e,n){var r=n(5),i=n(0);function o(t){return encodeURIComponent(t).replace(/\+/g,"%2B").replace(/'/g,"%27")}function s(t){return decodeURIComponent(t)}function a(t){var e=[];return i.forIn(t,function(t,n){var s=o(t);i.isType("array",n)||(n=[n]),n.forEach(function(t){r.hasValue(t)&&e.push(s+"="+o(t))})}),e.sort().join("&")}function u(t){var e={};return t?(t.split("&").forEach(function(t){var n=t.split("="),r=s(n[0]),o=s(n[1]);if(2==n.length){if(!i.isType("array",e[r]))return r in e?(e[r]=[e[r]],void e[r].push(o)):void(e[r]=o);e[r].push(o)}}),e):{}}t.exports={url:function(t,e){return a(e).length>0?i.contains(t,"?")?t+"&"+a(e):t+"?"+a(e):t},decodeURL:function(t){var e=t&&t.split("?");return 2==e.length?u(e[1]):{}},decode:u,encode:a,encodePart:o,decodePart:s}},function(t,e,n){var r=n(9),i=n(1),o=n(0),s={},a=o.contains(r.href,"tw_debug=true");function u(){}function c(){}function d(){return i.performance&&+i.performance.now()||+new Date}function l(t,e){if(i.console&&i.console[t])switch(e.length){case 1:i.console[t](e[0]);break;case 2:i.console[t](e[0],e[1]);break;case 3:i.console[t](e[0],e[1],e[2]);break;case 4:i.console[t](e[0],e[1],e[2],e[3]);break;case 5:i.console[t](e[0],e[1],e[2],e[3],e[4]);break;default:0!==e.length&&i.console.warn&&i.console.warn("too many params passed to logger."+t)}}t.exports={devError:u,devInfo:c,devObject:function(t,e){},publicError:function(){l("error",o.toRealArray(arguments))},publicLog:function(){l("info",o.toRealArray(arguments))},publicWarn:function(){l("warn",o.toRealArray(arguments))},time:function(t){a&&(s[t]=d())},timeEnd:function(t){a&&s[t]&&(d(),s[t])}}},function(t,e,n){var r=n(19),i=n(5),o=n(11),s=n(0),a=n(115);t.exports=function(t){var e=t.href&&t.href.split("?")[1],n=e?o.decode(e):{},u={lang:a(t),width:t.getAttribute("data-width")||t.getAttribute("width"),height:t.getAttribute("data-height")||t.getAttribute("height"),related:t.getAttribute("data-related"),partner:t.getAttribute("data-partner")};return i.asBoolean(t.getAttribute("data-dnt"))&&r.setOn(),s.forIn(u,function(t,e){var r=n[t];n[t]=i.hasValue(r)?r:e}),s.compact(n)}},function(t,e,n){var r=n(77),i=n(22);t.exports=function(){var t="data-twitter-extracted-"+i.generate();return function(e,n){return r(e,n).filter(function(e){return!e.hasAttribute(t)}).map(function(e){return e.setAttribute(t,"true"),e})}}},function(t,e){function n(t,e,n,r,i,o,s){this.factory=t,this.Sandbox=e,this.srcEl=o,this.targetEl=i,this.parameters=r,this.className=n,this.options=s}n.prototype.destroy=function(){this.srcEl=this.targetEl=null},t.exports=n},function(t,e){t.exports={DM_BUTTON:"twitter-dm-button",FOLLOW_BUTTON:"twitter-follow-button",HASHTAG_BUTTON:"twitter-hashtag-button",MENTION_BUTTON:"twitter-mention-button",MOMENT:"twitter-moment",PERISCOPE:"periscope-on-air",SHARE_BUTTON:"twitter-share-button",TIMELINE:"twitter-timeline",TWEET:"twitter-tweet"}},function(t,e,n){var r=n(6),i=n(19),o=n(53),s=n(36),a=n(5),u=n(0);t.exports=function(t,e,n){var c;return t=t||[],e=e||{},c="ƒ("+t.join(", ")+", target, [options]);",function(){var d,l,f,h,p=Array.prototype.slice.apply(arguments,[0,t.length]),m=Array.prototype.slice.apply(arguments,[t.length]);return m.forEach(function(t){t&&(t.nodeType!==Node.ELEMENT_NODE?u.isType("function",t)?d=t:u.isType("object",t)&&(l=t):f=t)}),p.length!==t.length||0===m.length?(d&&u.async(function(){d(!1)}),r.reject(new Error("Not enough parameters. Expected: "+c))):f?(l=u.aug({},l||{},e),t.forEach(function(t){l[t]=p.shift()}),a.asBoolean(l.dnt)&&i.setOn(),h=s.getExperiments().then(function(t){return o.addWidget(n(l,f,void 0,t))}),d&&h.then(d,function(){d(!1)}),h):(d&&u.async(function(){d(!1)}),r.reject(new Error("No target element specified. Expected: "+c)))}}},function(t,e,n){var r=n(98),i=n(2),o=n(0);function s(t,e){return function(){try{e.resolve(t.call(this))}catch(t){e.reject(t)}}}t.exports={sync:function(t,e){t.call(e)},read:function(t,e){var n=new i;return r.read(s(t,n),e),n.promise},write:function(t,e){var n=new i;return r.write(s(t,n),e),n.promise},defer:function(t,e,n){var a=new i;return o.isType("function",t)&&(n=e,e=t,t=1),r.defer(t,s(e,a),n),a.promise}}},function(t,e,n){var r=n(4),i=n(9),o=n(38),s=n(102),a=n(5),u=n(33),c=!1,d=/https?:\/\/([^/]+).*/i;t.exports={setOn:function(){c=!0},enabled:function(t,e){return!!(c||a.asBoolean(u.val("dnt"))||s.isUrlSensitive(e||i.host)||o.isFramed()&&s.isUrlSensitive(o.rootDocumentLocation())||(t=d.test(t||r.referrer)&&RegExp.$1)&&s.isUrlSensitive(t))}}},function(t,e,n){var r=n(8),i=n(59),o="https://syndication.twitter.com",s="https://platform.twitter.com",a=["https://syndication.twitter.com","https://cdn.syndication.twimg.com","https://localhost.twitter.com:8444"],u=["https://syndication.twitter.com","https://localhost.twitter.com:8445"],c=["https://platform.twitter.com","https://localhost.twitter.com",/^https:\/\/ton\.smf1\.twitter\.com\/syndication-internal\/embed-iframe\/[0-9A-Za-z_-]+\/app/],d=function(t,e){return t.some(function(t){return t instanceof RegExp?t.test(e):t===e})},l=function(){var t=r.get("backendHost");return t&&d(a,t)?t:"https://cdn.syndication.twimg.com"},f=function(){var t=r.get("settingsSvcHost");return t&&d(u,t)?t:o};function h(t,e){var n=[t];return e.forEach(function(t){n.push(function(t){var e=(t||"").toString(),n="/"===e.slice(0,1)?1:0,r=function(t){return"/"===t.slice(-1)}(e)?-1:void 0;return e.slice(n,r)}(t))}),n.join("/")}t.exports={cookieConsent:function(t){var e=t||[];return e.unshift("cookie/consent"),h(f(),e)},embedIframe:function(t,e){var n=t||[],o=s,a=r.get("embedIframeURL");return a&&d(c,a)?h(a,n)+".html":(n.unshift(i.getBaseURLPath(e)),h(o,n)+".html")},embedService:function(t){var e=t||[],n=o;return e.unshift("srv"),h(n,e)},eventVideo:function(t){var e=t||[];return e.unshift("video/event"),h(l(),e)},grid:function(t){var e=t||[];return e.unshift("grid/collection"),h(l(),e)},moment:function(t){var e=t||[];return e.unshift("moments"),h(l(),e)},settings:function(t){var e=t||[];return e.unshift("settings"),h(f(),e)},timeline:function(t){var e=t||[];return e.unshift("timeline"),h(l(),e)},tweetBatch:function(t){var e=t||[];return e.unshift("tweets.json"),h(l(),e)},video:function(t){var e=t||[];return e.unshift("widgets/video"),h(l(),e)}}},function(t,e,n){var r=n(4),i=n(92),o=n(1),s=n(0),a=i.userAgent;function u(t){return/(Trident|MSIE|Edge[/ ]?\d)/.test(t=t||a)}t.exports={retina:function(t){return(t=t||o).devicePixelRatio?t.devicePixelRatio>=1.5:!!t.matchMedia&&t.matchMedia("only screen and (min-resolution: 144dpi)").matches},anyIE:u,ie9:function(t){return/MSIE 9/.test(t=t||a)},ie10:function(t){return/MSIE 10/.test(t=t||a)},ios:function(t){return/(iPad|iPhone|iPod)/.test(t=t||a)},android:function(t){return/^Mozilla\/5\.0 \(Linux; (U; )?Android/.test(t=t||a)},canPostMessage:function(t,e){return t=t||o,e=e||a,t.postMessage&&!(u(e)&&t.opener)},touch:function(t,e,n){return t=t||o,e=e||i,n=n||a,"ontouchstart"in t||/Opera Mini/.test(n)||e.msMaxTouchPoints>0},cssTransitions:function(){var t=r.body.style;return void 0!==t.transition||void 0!==t.webkitTransition||void 0!==t.mozTransition||void 0!==t.oTransition||void 0!==t.msTransition},hasPromiseSupport:function(){return!!(o.Promise&&o.Promise.resolve&&o.Promise.reject&&o.Promise.all&&o.Promise.race&&(new o.Promise(function(e){t=e}),s.isType("function",t)));var t},hasIntersectionObserverSupport:function(){return!!o.IntersectionObserver},hasPerformanceInformation:function(){return o.performance&&o.performance.getEntriesByType}}},function(t,e){var n="i",r=0,i=0;t.exports={generate:function(){return n+String(+new Date)+Math.floor(1e5*Math.random())+r++},deterministic:function(){return n+String(i++)}}},function(t,e,n){var r=n(50),i=n(52),o=n(0);t.exports=o.aug(r.get("events")||{},i.Emitter)},function(t,e,n){var r=n(6),i=n(2);function o(t,e){return t.then(e,e)}function s(t){return t instanceof r}t.exports={always:o,allResolved:function(t){var e;return void 0===t?r.reject(new Error("undefined is not an object")):Array.isArray(t)?(e=t.length)?new r(function(n,r){var i=0,o=[];function a(){(i+=1)===e&&(0===o.length?r():n(o))}function u(t){o.push(t),a()}t.forEach(function(t){s(t)?t.then(u,a):u(t)})}):r.resolve([]):r.reject(new Error("Type error"))},some:function(t){var e;return e=(t=t||[]).length,t=t.filter(s),e?e!==t.length?r.reject("non-Promise passed to .some"):new r(function(e,n){var r=0;function i(){(r+=1)===t.length&&n()}t.forEach(function(t){t.then(e,i)})}):r.reject("no promises passed to .some")},isPromise:s,allSettled:function(t){function e(){}return r.all((t||[]).map(function(t){return o(t,e)}))},timeout:function(t,e){var n=new i;return setTimeout(function(){n.reject(new Error("Promise timed out"))},e),t.then(function(t){n.resolve(t)},function(t){n.reject(t)}),n.promise}}},function(t,e,n){var r=n(1).JSON;t.exports={stringify:r.stringify||r.encode,parse:r.parse||r.decode}},function(t,e,n){var r=n(27),i=n(108);t.exports=r.build([i])},function(t,e,n){var r=n(39),i=n(105),o=n(7);(r=Object.create(r)).build=o(r.build,null,i),t.exports=r},function(t,e,n){var r=n(39),i=n(40),o=n(7);(r=Object.create(r)).build=o(r.build,null,i),t.exports=r},function(t,e,n){var r=n(79),i=n(80),o=n(81),s=n(9),a=n(71),u=n(82),c=n(19),d=n(5),l=n(22),f=n(0),h=600;function p(t){if(!t||!t.headers)throw new Error("unexpected response schema");return{html:t.body,config:t.config,pollInterval:1e3*parseInt(t.headers.xPolling,10)||null,maxCursorPosition:t.headers.maxPosition,minCursorPosition:t.headers.minPosition}}function m(t){if(t&&t.headers)throw new Error(t.headers.status);throw t instanceof Error?t:new Error(t)}t.exports=function(t){t.params({chrome:{},height:{transform:d.asInt},instanceId:{required:!0,fallback:l.deterministic},isPreconfigured:{},lang:{required:!0,transform:a.matchLanguage,fallback:"en"},theme:{},tweetLimit:{transform:d.asInt}}),t.defineProperty("endpoint",{get:function(){throw new Error("endpoint not specified")}}),t.defineProperty("pollEndpoint",{get:function(){return this.endpoint}}),t.define("cbId",function(t){var e=t?"_new":"_old";return"tl_"+this.params.instanceId+"_"+this.id+e}),t.define("queryParams",function(){return{lang:this.params.lang,tz:u.getTimezoneOffset(),t:r(),domain:s.host,tweet_limit:this.params.tweetLimit,dnt:c.enabled()}}),t.define("horizonQueryParams",function(){var t=this.params.height,e=-1===(this.params.chrome||"").indexOf("noheader");return this.params.isPreconfigured&&!this.params.height&&(t=h),f.compact({dnt:c.enabled(),limit:this.params.tweetLimit,lang:this.params.lang,theme:this.params.theme,maxHeight:t,showHeader:e})}),t.define("fetch",function(){return i.fetch(this.endpoint,this.queryParams(),o,this.cbId()).then(p,m)}),t.define("poll",function(t,e){var n,r;return n={since_id:(t=t||{}).sinceId,max_id:t.maxId,min_position:t.minPosition,max_position:t.maxPosition},r=f.aug(this.queryParams(),n),i.fetch(this.pollEndpoint,r,o,this.cbId(e)).then(p,m)})}},function(t,e,n){var r=n(52).makeEmitter();t.exports={emitter:r,START:"start",ALL_WIDGETS_RENDER_START:"all_widgets_render_start",ALL_WIDGETS_RENDER_END:"all_widgets_render_end",ALL_WIDGETS_AND_IMAGES_LOADED:"all_widgets_and_images_loaded"}},function(t,e,n){var r=n(4),i=n(0);t.exports=function(t,e,n){var o;if(n=n||r,t=t||{},e=e||{},t.name){try{o=n.createElement('')}catch(e){(o=n.createElement("iframe")).name=t.name}delete t.name}else o=n.createElement("iframe");return t.id&&(o.id=t.id,delete t.id),o.allowtransparency="true",o.scrolling="no",o.setAttribute("frameBorder",0),o.setAttribute("allowTransparency",!0),i.forIn(t,function(t,e){o.setAttribute(t,e)}),i.forIn(e,function(t,e){o.style[t]=e}),o}},function(t,e,n){var r=n(27),i=n(122);t.exports=r.build([i])},function(t,e,n){var r,i=n(4);function o(t){var e,n,o,s=0;for(r={},e=(t=t||i).getElementsByTagName("meta");e[s];s++){if(n=e[s],/^twitter:/.test(n.getAttribute("name")))o=n.getAttribute("name").replace(/^twitter:/,"");else{if(!/^twitter:/.test(n.getAttribute("property")))continue;o=n.getAttribute("property").replace(/^twitter:/,"")}r[o]=n.getAttribute("content")||n.getAttribute("value")}}o(),t.exports={init:o,val:function(t){return r[t]}}},function(t,e,n){var r=n(0),i=n(45);t.exports={closest:function t(e,n,o){var s;if(n)return o=o||n&&n.ownerDocument,s=r.isType("function",e)?e:function(t){return function(e){return!!e.tagName&&i(e,t)}}(e),n===o?s(n)?n:void 0:s(n)?n:t(s,n.parentNode,o)}}},function(t,e,n){var r=n(10),i={},o=-1,s={};function a(t){var e=t.getAttribute("data-twitter-event-id");return e||(t.setAttribute("data-twitter-event-id",++o),o)}function u(t,e,n){var r=0,i=t&&t.length||0;for(r=0;r1?(e=Math.floor(t.item_ids.length/2),n=t.item_ids.slice(0,e),r={},i=t.item_ids.slice(e),o={},n.forEach(function(e){r[e]=t.item_details[e]}),i.forEach(function(e){o[e]=t.item_details[e]}),[l.aug({},t,{item_ids:n,item_details:r}),l.aug({},t,{item_ids:i,item_details:o})]):[t]},stringify:function(t){var e,n=Array.prototype.toJSON;return delete Array.prototype.toJSON,e=u.stringify(t),n&&(Array.prototype.toJSON=n),e},CLIENT_EVENT_ENDPOINT:p,RUFOUS_REDIRECT:"https://platform.twitter.com/jot.html"}},function(t,e,n){var r=n(9),i=n(75),o=n(0),s=i.getCanonicalURL()||r.href,a=s;t.exports={isFramed:function(){return s!==a},rootDocumentLocation:function(t){return t&&o.isType("string",t)&&(s=t),s},currentDocumentLocation:function(){return a}}},function(t,e,n){var r=n(103),i=n(104),o=n(0);t.exports={couple:function(){return o.toRealArray(arguments)},build:function(t,e,n){var o=new t;return(e=i(r(e||[]))).forEach(function(t){t.call(null,o)}),o.build(n)}}},function(t,e,n){var r=n(106),i=n(0),o=n(41);function s(){this.Component=this.factory(),this._adviceArgs=[],this._lastArgs=[]}i.aug(s.prototype,{factory:o,build:function(t){var e=this;return this.Component,i.aug(this.Component.prototype.boundParams,t),this._adviceArgs.concat(this._lastArgs).forEach(function(t){(function(t,e,n){var r=this[e];if(!r)throw new Error(e+" does not exist");this[e]=t(r,n)}).apply(e.Component.prototype,t)}),delete this._lastArgs,delete this._adviceArgs,this.Component},params:function(t){var e=this.Component.prototype.paramConfigs;t=t||{},this.Component.prototype.paramConfigs=i.aug({},t,e)},define:function(t,e){if(t in this.Component.prototype)throw new Error(t+" has previously been defined");this.override(t,e)},defineStatic:function(t,e){this.Component[t]=e},override:function(t,e){this.Component.prototype[t]=e},defineProperty:function(t,e){if(t in this.Component.prototype)throw new Error(t+" has previously been defined");this.overrideProperty(t,e)},overrideProperty:function(t,e){var n=i.aug({configurable:!0},e);Object.defineProperty(this.Component.prototype,t,n)},before:function(t,e){this._adviceArgs.push([r.before,t,e])},after:function(t,e){this._adviceArgs.push([r.after,t,e])},around:function(t,e){this._adviceArgs.push([r.around,t,e])},last:function(t,e){this._lastArgs.push([r.after,t,e])}}),t.exports=s},function(t,e,n){var r=n(0);function i(){return!0}function o(t){return t}t.exports=function(){function t(t){var e=this;t=t||{},this.params=Object.keys(this.paramConfigs).reduce(function(n,s){var a=[],u=e.boundParams,c=e.paramConfigs[s],d=c.validate||i,l=c.transform||o;if(s in u&&a.push(u[s]),s in t&&a.push(t[s]),a="fallback"in c?a.concat(c.fallback):a,n[s]=function(t,e,n){var i=null;return t.some(function(t){if(t=r.isType("function",t)?t():t,e(t))return i=n(t),!0}),i}(a,d,l),c.required&&null==n[s])throw new Error(s+" is a required parameter");return n},{}),this.initialize()}return r.aug(t.prototype,{paramConfigs:{},boundParams:{},initialize:function(){}}),t}},function(t,e,n){var r=n(101),i=n(76),o=new(n(110))(function(t){(!function(t){return 1===t.length&&i.canFlushOneItem(t[0])}(t)?function(t){r.init(),t.forEach(function(t){var e=t.input.namespace,n=t.input.data,i=t.input.offsite,o=t.input.version;r.clientEvent(e,n,i,o)}),r.flush().then(function(){t.forEach(function(t){t.taskDoneDeferred.resolve()})},function(){t.forEach(function(t){t.taskDoneDeferred.reject()})})}:function(t){t.forEach(function(t){var e=t.input.namespace,n=t.input.data,r=t.input.offsite,o=t.input.version;i.clientEvent(e,n,r,o),t.taskDoneDeferred.resolve()})})(t)});t.exports={scribe:function(t,e,n,r){return o.add({namespace:t,data:e,offsite:n,version:r})},pause:function(){o.pause()},resume:function(){o.resume()}}},function(t,e,n){var r,i=n(10),o=n(4),s=n(1),a=n(33),u=n(54),c=n(5),d=n(22),l="csptest";t.exports={inlineStyle:function(){var t=l+d.generate(),e=o.createElement("div"),n=o.createElement("style"),f="."+t+" { visibility: hidden; }";return!!o.body&&(c.asBoolean(a.val("widgets:csp"))&&(r=!1),void 0!==r?r:(e.style.display="none",i.add(e,t),n.type="text/css",n.appendChild(o.createTextNode(f)),o.body.appendChild(n),o.body.appendChild(e),r="hidden"===s.getComputedStyle(e).visibility,u(e),u(n),r))}}},function(t,e,n){var r=n(1);t.exports=function(t,e,n){var i,o=0;return n=n||null,function s(){var a=n||this,u=arguments,c=+new Date;if(r.clearTimeout(i),c-o>e)return o=c,void t.apply(a,u);i=r.setTimeout(function(){s.apply(a,u)},e)}}},function(t,e,n){var r=n(1).HTMLElement,i=r.prototype.matches||r.prototype.matchesSelector||r.prototype.webkitMatchesSelector||r.prototype.mozMatchesSelector||r.prototype.msMatchesSelector||r.prototype.oMatchesSelector;t.exports=function(t,e){if(i)return i.call(t,e)}},function(t){t.exports={version:"2582c61:1645036219416"}},function(t,e){t.exports=function(t){var e=t.getBoundingClientRect();return{width:e.width,height:e.height}}},function(t,e,n){var r=n(12).publicWarn;t.exports=function(){r("Warning: This Timeline type belongs to a group that will not be supported in the future (Likes, Collections, & Moments). It is not recommended for use. \n\t","* Twitter will continue to support Profile and List Timelines \n\t","* You can learn more about this change in our announcement: \n\t","https://twittercommunity.com/t/removing-support-for-embedded-like-collection-and-moment-timelines/150313 \n\t","* In order to create a new Embedded Timeline, visit: https://publish.twitter.com")}},function(t,e,n){ 2 | /*! 3 | * @overview es6-promise - a tiny implementation of Promises/A+. 4 | * @copyright Copyright (c) 2014 Yehuda Katz, Tom Dale, Stefan Penner and contributors (Conversion to ES6 API by Jake Archibald) 5 | * @license Licensed under MIT license 6 | * See https://raw.githubusercontent.com/stefanpenner/es6-promise/master/LICENSE 7 | * @version v4.2.5+7f2b526d 8 | */var r;r=function(){"use strict";function t(t){return"function"==typeof t}var e=Array.isArray?Array.isArray:function(t){return"[object Array]"===Object.prototype.toString.call(t)},n=0,r=void 0,i=void 0,o=function(t,e){f[n]=t,f[n+1]=e,2===(n+=2)&&(i?i(h):w())},s="undefined"!=typeof window?window:void 0,a=s||{},u=a.MutationObserver||a.WebKitMutationObserver,c="undefined"==typeof self&&"undefined"!=typeof process&&"[object process]"==={}.toString.call(process),d="undefined"!=typeof Uint8ClampedArray&&"undefined"!=typeof importScripts&&"undefined"!=typeof MessageChannel;function l(){var t=setTimeout;return function(){return t(h,1)}}var f=new Array(1e3);function h(){for(var t=0;t=0&&this._handlers[t].splice(n,1):this._handlers[t]=[])},trigger:function(t,e){var n=this._handlers&&this._handlers[t];(e=e||{}).type=t,n&&n.forEach(function(t){r.async(i(t,this,e))})}};t.exports={Emitter:o,makeEmitter:function(){return r.aug(function(){},o)}}},function(t,e,n){var r=n(97),i=n(99),o=n(6),s=n(24),a=n(7),u=n(0),c=new i(function(t){var e=function(t){return t.reduce(function(t,e){return t[e._className]=t[e._className]||[],t[e._className].push(e),t},{})}(t.map(r.fromRawTask));u.forIn(e,function(t,e){s.allSettled(e.map(function(t){return t.initialize()})).then(function(){e.forEach(function(t){o.all([t.hydrate(),t.insertIntoDom()]).then(a(t.render,t)).then(a(t.success,t),a(t.fail,t))})})})});t.exports={addWidget:function(t){return c.add(t)}}},function(t,e,n){var r=n(18);t.exports=function(t){return r.write(function(){t&&t.parentNode&&t.parentNode.removeChild(t)})}},function(t,e,n){n(12),t.exports={log:function(t,e){}}},function(t,e,n){var r=n(1);function i(t){return(t=t||r).getSelection&&t.getSelection()}t.exports={getSelection:i,getSelectedText:function(t){var e=i(t);return e?e.toString():""}}},function(t,e,n){var r=n(4),i=n(1),o=n(2),s=2e4;t.exports=function(t){var e=new o,n=r.createElement("img");return n.onload=n.onerror=function(){i.setTimeout(e.resolve,50)},n.src=t,i.setTimeout(e.reject,s),e.promise}},function(t,e,n){var r=n(109);t.exports=function(t){t.define("createElement",r),t.define("createFragment",r),t.define("htmlToElement",r),t.define("hasSelectedText",r),t.define("addRootClass",r),t.define("removeRootClass",r),t.define("hasRootClass",r),t.define("prependStyleSheet",r),t.define("appendStyleSheet",r),t.define("prependCss",r),t.define("appendCss",r),t.define("makeVisible",r),t.define("injectWidgetEl",r),t.define("matchHeightToContent",r),t.define("matchWidthToContent",r)}},function(t,e){var n="tfw_horizon_tweet_embed_9555",r="tfw_horizon_timeline_12034";t.exports={getBaseURLPath:function(t){switch(t&&t.tfw_team_holdback_11929&&t.tfw_team_holdback_11929.bucket){case"control":return"embed-holdback";case"holdback_prod":return"embed-holdback-prod";default:return"embed"}},isHorizonTweetEnabled:function(t){return!(t&&t[n]&&"control"===t[n].bucket)},isHorizonTimelineEnabled:function(t,e){return t&&t[r]&&"treatment"===t[r].bucket&&("profile"===e||"list"===e)}}},function(t,e){t.exports=function(t){var e,n=!1;return function(){return n?e:(n=!0,e=t.apply(this,arguments))}}},function(t,e,n){var r=n(15),i=n(116),o=n(117),s=n(16);t.exports=function(t,e,n){return new r(i,o,s.DM_BUTTON,t,e,n)}},function(t,e,n){var r=n(27),i=n(118);t.exports=r.build([i])},function(t,e,n){var r=n(15),i=n(121),o=n(32),s=n(16);t.exports=function(t,e,n){return new r(i,o,s.FOLLOW_BUTTON,t,e,n)}},function(t,e,n){var r=n(15),i=n(129),o=n(26),s=n(16);t.exports=function(t,e,n){return new r(i,o,s.MOMENT,t,e,n)}},function(t,e,n){var r=n(15),i=n(131),o=n(26),s=n(16);t.exports=function(t,e,n){return new r(i,o,s.PERISCOPE,t,e,n)}},function(t,e,n){var r=n(78),i=n(133),o=n(137),s=n(139),a=n(141),u=n(143),c={collection:i,event:o,likes:s,list:a,profile:u,url:l},d=[u,s,i,a,o];function l(t){return r(d,function(e){try{return new e(t)}catch(t){}})}t.exports=function(t){return t?function(t){var e,n;return e=(t.sourceType+"").toLowerCase(),(n=c[e])?new n(t):null}(t)||l(t):null}},function(t,e,n){var r=n(4),i=n(59),o=n(15),s=n(145),a=n(32),u=n(146),c=n(26),d=n(147),l=n(16);t.exports=function(t,e,n,f){var h,p=s.get(t.id);return i.isHorizonTimelineEnabled(f,p)?(h=r.createElement("div"),new o(u,a,l.TIMELINE,t,e,n,{sandboxWrapperEl:h})):new o(d,c,l.TIMELINE,t,e,n)}},function(t,e,n){var r=n(4),i=n(15),o=n(32),s=n(149),a=n(16);t.exports=function(t,e,n){var u=r.createElement("div");return new i(s,o,a.TWEET,t,e,n,{sandboxWrapperEl:u})}},function(t,e,n){var r=n(15),i=n(151),o=n(32),s=n(16);t.exports=function(t,e,n){var a=t&&t.type||"share",u="hashtag"==a?s.HASHTAG_BUTTON:"mention"==a?s.MENTION_BUTTON:s.SHARE_BUTTON;return new r(i,o,u,t,e,n)}},function(t,e,n){var r=n(42),i=n(38),o=n(0);t.exports=function(t){var e={widget_origin:i.rootDocumentLocation(),widget_frame:i.isFramed()?i.currentDocumentLocation():null,duration_ms:t.duration,item_ids:t.widgetIds||[]},n=o.aug(t.namespace,{page:"page",component:"performance"});r.scribe(n,e)}},function(t,e,n){var r=n(0),i=n(134),o=["ar","fa","he","ur"];t.exports={isRtlLang:function(t){return t=String(t).toLowerCase(),r.contains(o,t)},matchLanguage:function(t){return t=(t=(t||"").toLowerCase()).replace("_","-"),i(t)?t:(t=t.replace(/-.*/,""),i(t)?t:"en")}}},function(t){t.exports={tweetButtonHtmlPath:"/widgets/tweet_button.a58e82e150afc25eb5372dd55a98b778.{{lang}}.html",followButtonHtmlPath:"/widgets/follow_button.a58e82e150afc25eb5372dd55a98b778.{{lang}}.html",hubHtmlPath:"/widgets/hub.html",widgetIframeHtmlPath:"/widgets/widget_iframe.a58e82e150afc25eb5372dd55a98b778.html",resourceBaseUrl:"https://platform.twitter.com"}},function(t){t.exports={TWEET:0,RETWEET:10,CUSTOM_TIMELINE:17,LIVE_VIDEO_EVENT:28,QUOTE_TWEET:23}},function(t,e,n){var r=n(3),i=n(95),o=n(23),s=n(11),a={favorite:["favorite","like"],follow:["follow"],like:["favorite","like"],retweet:["retweet"],tweet:["tweet"]};function u(t){this.srcEl=[],this.element=t}u.open=function(t,e,n){var u=(r.intentType(t)||"").toLowerCase();r.isTwitterURL(t)&&(function(t,e){i.open(t,e)}(t,n),e&&o.trigger("click",{target:e,region:"intent",type:"click",data:{}}),e&&a[u]&&a[u].forEach(function(n){o.trigger(n,{target:e,region:"intent",type:n,data:function(t,e){var n=s.decodeURL(e);switch(t){case"favorite":case"like":return{tweet_id:n.tweet_id};case"follow":return{screen_name:n.screen_name,user_id:n.user_id};case"retweet":return{source_tweet_id:n.tweet_id};default:return{}}}(u,t)})}))},t.exports=u},function(t,e,n){var r=n(4),i=n(9),o=n(3);function s(t,e){var n,r;return e=e||i,/^https?:\/\//.test(t)?t:/^\/\//.test(t)?e.protocol+t:(n=e.host+(e.port.length?":"+e.port:""),0!==t.indexOf("/")&&((r=e.pathname.split("/")).pop(),r.push(t),t="/"+r.join("/")),[e.protocol,"//",n,t].join(""))}t.exports={absolutize:s,getCanonicalURL:function(){for(var t,e=r.getElementsByTagName("link"),n=0;e[n];n++)if("canonical"==(t=e[n]).rel)return s(t.href)},getScreenNameFromPage:function(){for(var t,e,n,i=[r.getElementsByTagName("a"),r.getElementsByTagName("link")],s=0,a=0,u=/\bme\b/;t=i[s];s++)for(a=0;e=t[a];a++)if(u.test(e.rel)&&(n=o.screenName(e.href)))return n}}},function(t,e,n){var r=n(19),i=n(55),o=n(11),s=n(37),a=n(0),u=n(8).get("scribeCallback"),c=2083,d=[],l=o.url(s.CLIENT_EVENT_ENDPOINT,{dnt:0,l:""}),f=encodeURIComponent(l).length;function h(t,e,n,r,i){var o=!a.isObject(t),c=!!e&&!a.isObject(e);o||c||(u&&u(arguments),p(s.formatClientEventNamespace(t),s.formatClientEventData(e,n,r),s.CLIENT_EVENT_ENDPOINT,i))}function p(t,e,n,r){var u;n&&a.isObject(t)&&a.isObject(e)&&(i.log(t,e),u=s.flattenClientEventPayload(t,e),r=a.aug({},r,{l:s.stringify(u)}),u.dnt&&(r.dnt=1),w(o.url(n,r)))}function m(t,e,n,r){var i=!a.isObject(t),o=!!e&&!a.isObject(e);if(!i&&!o)return v(s.flattenClientEventPayload(s.formatClientEventNamespace(t),s.formatClientEventData(e,n,r)))}function v(t){return d.push(t),d}function g(t){return encodeURIComponent(t).length+3}function w(t){return(new Image).src=t}t.exports={canFlushOneItem:function(t){var e=g(s.stringify(t));return f+e1&&m({page:"widgets_js",component:"scribe_pixel",action:"batch_log"},{}),t=d,d=[],t.reduce(function(e,n,r){var i=e.length,o=i&&e[i-1];return r+1==t.length&&n.event_namespace&&"batch_log"==n.event_namespace.action&&(n.message=["entries:"+r,"requests:"+i].join("/")),function t(e){return Array.isArray(e)||(e=[e]),e.reduce(function(e,n){var r,i=s.stringify(n),o=g(i);return f+o1&&(e=e.concat(t(r))),e},[])}(n).forEach(function(t){var n=g(t);(!o||o.urlLength+n>c)&&(o={urlLength:f,items:[]},e.push(o)),o.urlLength+=n,o.items.push(t)}),e},[]).map(function(t){var e={l:t.items};return r.enabled()&&(e.dnt=1),w(o.url(s.CLIENT_EVENT_ENDPOINT,e))})},interaction:function(t,e,n,r){var i=s.extractTermsFromDOM(t.target||t.srcElement);i.action=r||"click",h(i,e,n)}}},function(t,e,n){var r=n(0),i=n(45);t.exports=function(t,e){return i(t,e)?[t]:r.toRealArray(t.querySelectorAll(e))}},function(t,e){t.exports=function(t,e,n){for(var r,i=0;i")}).then(function(){t.close(),a.resolve(c)})}),c.src=["javascript:",'document.write("");',"try { window.parent.document; }",'catch (e) { document.domain="'+r.domain+'"; }',"window.parent."+g.fullPath(["sandbox",u])+"();"].join(""),c.addEventListener("error",a.reject,!1),o.write(function(){i.parentNode.replaceChild(c,i)}),a.promise}t.exports=a.couple(n(58),function(t){t.overrideProperty("id",{get:function(){return this.sandboxEl&&this.sandboxEl.id}}),t.overrideProperty("initialized",{get:function(){return!!this.win}}),t.overrideProperty("width",{get:function(){return this._width}}),t.overrideProperty("height",{get:function(){return this._height}}),t.overrideProperty("sandboxEl",{get:function(){return this.iframeEl}}),t.defineProperty("iframeEl",{get:function(){return this._iframe}}),t.defineProperty("rootEl",{get:function(){return this.doc&&this.doc.documentElement}}),t.defineProperty("widgetEl",{get:function(){return this.doc&&this.doc.body.firstElementChild}}),t.defineProperty("win",{get:function(){return this.iframeEl&&this.iframeEl.contentWindow}}),t.defineProperty("doc",{get:function(){return this.win&&this.win.document}}),t.define("_updateCachedDimensions",function(){var t=this;return o.read(function(){var e,n=h(t.sandboxEl);"visible"==t.sandboxEl.style.visibility?t._width=n.width:(e=h(t.sandboxEl.parentElement).width,t._width=Math.min(n.width,e)),t._height=n.height})}),t.define("_setTargetToBlank",function(){var t=this.createElement("base");t.target="_blank",this.doc.head.appendChild(t)}),t.define("_didResize",function(){var t=this,e=this._resizeHandlers.slice(0);return this._updateCachedDimensions().then(function(){e.forEach(function(e){e(t)})})}),t.define("setTitle",function(t){this.iframeEl.title=t}),t.override("createElement",function(t){return this.doc.createElement(t)}),t.override("createFragment",function(){return this.doc.createDocumentFragment()}),t.override("htmlToElement",function(t){var e;return(e=this.createElement("div")).innerHTML=t,e.firstElementChild}),t.override("hasSelectedText",function(){return!!s.getSelectedText(this.win)}),t.override("addRootClass",function(t){var e=this.rootEl;return t=Array.isArray(t)?t:[t],this.initialized?o.write(function(){t.forEach(function(t){i.add(e,t)})}):m.reject(new Error("sandbox not initialized"))}),t.override("removeRootClass",function(t){var e=this.rootEl;return t=Array.isArray(t)?t:[t],this.initialized?o.write(function(){t.forEach(function(t){i.remove(e,t)})}):m.reject(new Error("sandbox not initialized"))}),t.override("hasRootClass",function(t){return i.present(this.rootEl,t)}),t.define("addStyleSheet",function(t,e){var n,r=new p;return this.initialized?((n=this.createElement("link")).type="text/css",n.rel="stylesheet",n.href=t,n.addEventListener("load",r.resolve,!1),n.addEventListener("error",r.reject,!1),o.write(y(e,null,n)).then(function(){return u(t).then(r.resolve,r.reject),r.promise})):m.reject(new Error("sandbox not initialized"))}),t.override("prependStyleSheet",function(t){var e=this.doc;return this.addStyleSheet(t,function(t){var n=e.head.firstElementChild;return n?e.head.insertBefore(t,n):e.head.appendChild(t)})}),t.override("appendStyleSheet",function(t){var e=this.doc;return this.addStyleSheet(t,function(t){return e.head.appendChild(t)})}),t.define("addCss",function(t,e){var n;return c.inlineStyle()?((n=this.createElement("style")).type="text/css",n.appendChild(this.doc.createTextNode(t)),o.write(y(e,null,n))):(f.devError("CSP enabled; cannot embed inline styles"),m.resolve())}),t.override("prependCss",function(t){var e=this.doc;return this.addCss(t,function(t){var n=e.head.firstElementChild;return n?e.head.insertBefore(t,n):e.head.appendChild(t)})}),t.override("appendCss",function(t){var e=this.doc;return this.addCss(t,function(t){return e.head.appendChild(t)})}),t.override("makeVisible",function(){var t=this;return this.styleSelf(E).then(function(){t._updateCachedDimensions()})}),t.override("injectWidgetEl",function(t){var e=this;return this.initialized?this.widgetEl?m.reject(new Error("widget already injected")):o.write(function(){e.doc.body.appendChild(t)}):m.reject(new Error("sandbox not initialized"))}),t.override("matchHeightToContent",function(){var t,e=this;return o.read(function(){t=e.widgetEl?h(e.widgetEl).height:0}),o.write(function(){e.sandboxEl.style.height=t+"px"}).then(function(){return e._updateCachedDimensions()})}),t.override("matchWidthToContent",function(){var t,e=this;return o.read(function(){t=e.widgetEl?h(e.widgetEl).width:0}),o.write(function(){e.sandboxEl.style.width=t+"px"}).then(function(){return e._updateCachedDimensions()})}),t.after("initialize",function(){this._iframe=null,this._width=this._height=0,this._resizeHandlers=[]}),t.override("insert",function(t,e,n,r){var i=this,s=new p,a=this.targetGlobal.document,u=S(t,e,n,a);return o.write(y(r,null,u)),u.addEventListener("load",function(){(function(t){try{t.contentWindow.document}catch(t){return m.reject(t)}return m.resolve(t)})(u).then(null,y(R,null,t,e,n,u,a)).then(s.resolve,s.reject)},!1),u.addEventListener("error",s.reject,!1),s.promise.then(function(t){var e=d(i._didResize,A,i);return i._iframe=t,i.win.addEventListener("resize",e,!1),m.all([i._setTargetToBlank(),i.addRootClass(x),i.prependCss(T)])})}),t.override("onResize",function(t){this._resizeHandlers.push(t)}),t.after("styleSelf",function(){return this._updateCachedDimensions()})})},function(t,e){t.exports=function(){throw new Error("unimplemented method")}},function(t,e,n){var r=n(2),i=n(7),o=100,s=3e3;function a(t,e){this._inputsQueue=[],this._task=t,this._isPaused=!1,this._flushDelay=e&&e.flushDelay||o,this._pauseLength=e&&e.pauseLength||s,this._flushTimeout=void 0}a.prototype.add=function(t){var e=new r;return this._inputsQueue.push({input:t,taskDoneDeferred:e}),this._scheduleFlush(),e.promise},a.prototype._scheduleFlush=function(){this._isPaused||(clearTimeout(this._flushTimeout),this._flushTimeout=setTimeout(i(this._flush,this),this._flushDelay))},a.prototype._flush=function(){try{this._task.call(null,this._inputsQueue)}catch(t){this._inputsQueue.forEach(function(e){e.taskDoneDeferred.reject(t)})}this._inputsQueue=[],this._flushTimeout=void 0},a.prototype.pause=function(t){clearTimeout(this._flushTimeout),this._isPaused=!0,!t&&this._pauseLength&&setTimeout(i(this.resume,this),this._pauseLength)},a.prototype.resume=function(){this._isPaused=!1,this._scheduleFlush()},t.exports=a},function(t,e,n){var r=n(72),i=n(31),o=n(2),s=n(4),a=n(20),u=n(21),c=n(25),d=n(9),l=n(12),f=n(112),h=n(60),p=n(8),m=n(11),v=n(3),g=n(0),w=n(1),y=h(function(){return new o}),b={shouldObtainCookieConsent:!1,features:{}};t.exports={load:function(){var t,e,n,o;if(u.ie9()||u.ie10()||"http:"!==d.protocol&&"https:"!==d.protocol)return l.devError("Using default settings due to unsupported browser or protocol."),void y().resolve();t={origin:d.origin},a.settings().indexOf("localhost")>-1&&(t.localSettings=!0),e=m.url(r.resourceBaseUrl+r.widgetIframeHtmlPath,t),n=function(t){var n,r,i,o;if(r=v.isTwitterURL(t.origin),i=e.substr(0,t.origin.length)===t.origin,o=v.isTwimgURL(t.origin),i&&r||o)try{(n="string"==typeof t.data?c.parse(t.data):t.data).namespace===f.settings&&(b=g.aug(b,{features:n.settings.features,sessionId:n.sessionId}),y().resolve())}catch(t){l.devError(t)}},w.addEventListener("message",n),o=i({src:e,title:"Twitter settings iframe"},{display:"none"}),s.body.appendChild(o)},settingsLoaded:function(){var t,e;return t=p.get("experimentOverride"),y().promise.then(function(){return t&&t.name&&t.assignment&&((e={})[t.name]={bucket:t.assignment},b.features=g.aug(b.features,e)),b})}}},function(t,e){t.exports={settings:"twttr.settings"}},function(t,e,n){t.exports=[n(114),n(120),n(128),n(130),n(132),n(148),n(150)]},function(t,e,n){var r=n(11),i=n(5),o=n(0),s=n(13),a=n(14)(),u=n(61),c="a.twitter-dm-button";t.exports=function(t){return a(t,c).map(function(t){return u(function(t){var e=t.getAttribute("data-show-screen-name"),n=s(t),a=t.getAttribute("href"),u=t.getAttribute("data-screen-name"),c=e?i.asBoolean(e):null,d=t.getAttribute("data-size"),l=r.decodeURL(a),f=l.recipient_id,h=t.getAttribute("data-text")||l.text,p=t.getAttribute("data-welcome-message-id")||l.welcomeMessageId;return o.aug(n,{screenName:u,showScreenName:c,size:d,text:h,userId:f,welcomeMessageId:p})}(t),t.parentNode,t)})}},function(t,e,n){var r=n(0);t.exports=function t(e){var n;if(e)return n=e.lang||e.getAttribute("data-lang"),r.isType("string",n)?n:t(e.parentElement)}},function(t,e,n){var r=n(2);t.exports=function(t,e){var i=new r;return n.e(2).then(function(r){var o;try{o=n(83),i.resolve(new o(t,e))}catch(t){i.reject(t)}}.bind(null,n)).catch(function(t){i.reject(t)}),i.promise}},function(t,e,n){var r=n(62),i=n(26);t.exports=r.isSupported()?r:i},function(t,e,n){var r=n(119),i=n(1),o=n(10),s=n(35),a=n(18),u=n(56),c=n(27),d=n(57),l=n(43),f=n(47),h=n(7),p=n(44),m=n(6),v=n(0),g=50,w={position:"absolute",visibility:"hidden",display:"block",transform:"rotate(0deg)"},y={position:"static",visibility:"visible"},b="twitter-widget",_="open",E="SandboxRoot",x=".SandboxRoot { display: none; max-height: 10000px; }";t.exports=c.couple(n(58),function(t){t.defineStatic("isSupported",function(){return!!i.HTMLElement.prototype.attachShadow&&l.inlineStyle()}),t.overrideProperty("id",{get:function(){return this.sandboxEl&&this.sandboxEl.id}}),t.overrideProperty("initialized",{get:function(){return!!this._shadowHost}}),t.overrideProperty("width",{get:function(){return this._width}}),t.overrideProperty("height",{get:function(){return this._height}}),t.overrideProperty("sandboxEl",{get:function(){return this._shadowHost}}),t.define("_updateCachedDimensions",function(){var t=this;return a.read(function(){var e,n=f(t.sandboxEl);"visible"==t.sandboxEl.style.visibility?t._width=n.width:(e=f(t.sandboxEl.parentElement).width,t._width=Math.min(n.width,e)),t._height=n.height})}),t.define("_didResize",function(){var t=this,e=this._resizeHandlers.slice(0);return this._updateCachedDimensions().then(function(){e.forEach(function(e){e(t)})})}),t.override("createElement",function(t){return this.targetGlobal.document.createElement(t)}),t.override("createFragment",function(){return this.targetGlobal.document.createDocumentFragment()}),t.override("htmlToElement",function(t){var e;return(e=this.createElement("div")).innerHTML=t,e.firstElementChild}),t.override("hasSelectedText",function(){return!!u.getSelectedText(this.targetGlobal)}),t.override("addRootClass",function(t){var e=this._shadowRootBody;return t=Array.isArray(t)?t:[t],this.initialized?a.write(function(){t.forEach(function(t){o.add(e,t)})}):m.reject(new Error("sandbox not initialized"))}),t.override("removeRootClass",function(t){var e=this._shadowRootBody;return t=Array.isArray(t)?t:[t],this.initialized?a.write(function(){t.forEach(function(t){o.remove(e,t)})}):m.reject(new Error("sandbox not initialized"))}),t.override("hasRootClass",function(t){return o.present(this._shadowRootBody,t)}),t.override("addStyleSheet",function(t,e){return this.addCss('@import url("'+t+'");',e).then(function(){return d(t)})}),t.override("prependStyleSheet",function(t){var e=this._shadowRoot;return this.addStyleSheet(t,function(t){var n=e.firstElementChild;return n?e.insertBefore(t,n):e.appendChild(t)})}),t.override("appendStyleSheet",function(t){var e=this._shadowRoot;return this.addStyleSheet(t,function(t){return e.appendChild(t)})}),t.override("addCss",function(t,e){var n;return this.initialized?l.inlineStyle()?((n=this.createElement("style")).type="text/css",n.appendChild(this.targetGlobal.document.createTextNode(t)),a.write(h(e,null,n))):m.resolve():m.reject(new Error("sandbox not initialized"))}),t.override("prependCss",function(t){var e=this._shadowRoot;return this.addCss(t,function(t){var n=e.firstElementChild;return n?e.insertBefore(t,n):e.appendChild(t)})}),t.override("appendCss",function(t){var e=this._shadowRoot;return this.addCss(t,function(t){return e.appendChild(t)})}),t.override("makeVisible",function(){return this.styleSelf(y)}),t.override("injectWidgetEl",function(t){var e=this;return this.initialized?this._shadowRootBody.firstElementChild?m.reject(new Error("widget already injected")):a.write(function(){e._shadowRootBody.appendChild(t)}).then(function(){return e._updateCachedDimensions()}).then(function(){var t=p(e._didResize,g,e);new r(e._shadowRootBody,t)}):m.reject(new Error("sandbox not initialized"))}),t.override("matchHeightToContent",function(){return m.resolve()}),t.override("matchWidthToContent",function(){return m.resolve()}),t.override("insert",function(t,e,n,r){var i=this.targetGlobal.document,o=this._shadowHost=i.createElement(b),u=this._shadowRoot=o.attachShadow({mode:_}),c=this._shadowRootBody=i.createElement("div");return v.forIn(e||{},function(t,e){o.setAttribute(t,e)}),o.id=t,u.appendChild(c),s.delegate(c,"click","A",function(t,e){e.hasAttribute("target")||e.setAttribute("target","_blank")}),m.all([this.styleSelf(w),this.addRootClass(E),this.prependCss(x),a.write(r.bind(null,o))])}),t.override("onResize",function(t){this._resizeHandlers.push(t)}),t.after("initialize",function(){this._shadowHost=this._shadowRoot=this._shadowRootBody=null,this._width=this._height=0,this._resizeHandlers=[]}),t.after("styleSelf",function(){return this._updateCachedDimensions()})})},function(t,e){var n;(n=function(t,e){function r(t,e){if(t.resizedAttached){if(t.resizedAttached)return void t.resizedAttached.add(e)}else t.resizedAttached=new function(){var t,e;this.q=[],this.add=function(t){this.q.push(t)},this.call=function(){for(t=0,e=this.q.length;t
',t.appendChild(t.resizeSensor),{fixed:1,absolute:1}[function(t,e){return t.currentStyle?t.currentStyle[e]:window.getComputedStyle?window.getComputedStyle(t,null).getPropertyValue(e):t.style[e]}(t,"position")]||(t.style.position="relative");var i,o,s=t.resizeSensor.childNodes[0],a=s.childNodes[0],u=t.resizeSensor.childNodes[1],c=(u.childNodes[0],function(){a.style.width=s.offsetWidth+10+"px",a.style.height=s.offsetHeight+10+"px",s.scrollLeft=s.scrollWidth,s.scrollTop=s.scrollHeight,u.scrollLeft=u.scrollWidth,u.scrollTop=u.scrollHeight,i=t.offsetWidth,o=t.offsetHeight});c();var d=function(t,e,n){t.attachEvent?t.attachEvent("on"+e,n):t.addEventListener(e,n)},l=function(){t.offsetWidth==i&&t.offsetHeight==o||t.resizedAttached&&t.resizedAttached.call(),c()};d(s,"scroll",l),d(u,"scroll",l)}var i=Object.prototype.toString.call(t),o="[object Array]"===i||"[object NodeList]"===i||"[object HTMLCollection]"===i||"undefined"!=typeof jQuery&&t instanceof jQuery||"undefined"!=typeof Elements&&t instanceof Elements;if(o)for(var s=0,a=t.length;s0;return this.updateCachedDimensions().then(function(){e&&t._resizeHandlers.forEach(function(e){e(t)})})}),t.define("loadDocument",function(t){var e=new a;return this.initialized?this.iframeEl.src?u.reject(new Error("widget already loaded")):(this.iframeEl.addEventListener("load",e.resolve,!1),this.iframeEl.addEventListener("error",e.reject,!1),this.iframeEl.src=t,e.promise):u.reject(new Error("sandbox not initialized"))}),t.after("initialize",function(){var t=new a,e=new a;this._iframe=null,this._iframeVersion=null,this._width=this._height=0,this._resizeHandlers=[],this._rendered=t,this._results=e,this._waitToSwapUntilRendered=!1}),t.override("insert",function(t,e,n,i){var a=this;return e=d.aug({id:t},l,e),n=d.aug({},f,n),this._iframe=s(e,n),p[t]=this,a._waitToSwapUntilRendered||this.onResize(o(function(){a.makeVisible()})),r.write(c(i,null,this._iframe))}),t.override("onResize",function(t){this._resizeHandlers.push(t)}),t.after("styleSelf",function(){return this.updateCachedDimensions()})}},function(t,e,n){var r=n(1),i=n(124),o=n(126),s=n(23),a=n(5),u=n(127);t.exports=function(t,e,n,c,d){function l(t){var e=u(this);s.trigger(t.type,{target:e,region:t.region,type:t.type,data:t.data||{}})}function f(e){var n=u(this),r=n&&n.id,i=a.asInt(e.width),o=a.asInt(e.height);r&&void 0!==i&&void 0!==o&&t(r,i,o)}(new i).attachReceiver(new o.Receiver(r,"twttr.button")).bind("twttr.private.trigger",l).bind("twttr.private.resizeButton",f),(new i).attachReceiver(new o.Receiver(r,"twttr.embed")).bind("twttr.private.initialized",function(t){var e=u(this),n=e&&e.id,r=t.iframe_version;n&&r&&c&&c(n,r)}).bind("twttr.private.trigger",l).bind("twttr.private.results",function(){var t=u(this),n=t&&t.id;n&&e&&e(n)}).bind("twttr.private.rendered",function(){var t=u(this),e=t&&t.id;e&&n&&n(e)}).bind("twttr.private.no_results",function(){var t=u(this),e=t&&t.id;e&&d&&d(e)}).bind("twttr.private.resize",f)}},function(t,e,n){var r=n(25),i=n(125),o=n(0),s=n(6),a=n(24),u="2.0";function c(t){this.registry=t||{}}function d(t){var e,n;return e=o.isType("string",t),n=o.isType("number",t),e||n||null===t}function l(t,e){return{jsonrpc:u,id:d(t)?t:null,error:e}}c.prototype._invoke=function(t,e){var n,r,i;n=this.registry[t.method],r=t.params||[],r=o.isType("array",r)?r:[r];try{i=n.apply(e.source||null,r)}catch(t){i=s.reject(t.message)}return a.isPromise(i)?i:s.resolve(i)},c.prototype._processRequest=function(t,e){var n,r;return function(t){var e,n,r;return!!o.isObject(t)&&(e=t.jsonrpc===u,n=o.isType("string",t.method),r=!("id"in t)||d(t.id),e&&n&&r)}(t)?(n="params"in t&&(r=t.params,!o.isObject(r)||o.isType("function",r))?s.resolve(l(t.id,i.INVALID_PARAMS)):this.registry[t.method]?this._invoke(t,{source:e}).then(function(e){return n=t.id,{jsonrpc:u,id:n,result:e};var n},function(){return l(t.id,i.INTERNAL_ERROR)}):s.resolve(l(t.id,i.METHOD_NOT_FOUND)),null!=t.id?n:s.resolve()):s.resolve(l(t.id,i.INVALID_REQUEST))},c.prototype.attachReceiver=function(t){return t.attachTo(this),this},c.prototype.bind=function(t,e){return this.registry[t]=e,this},c.prototype.receive=function(t,e){var n,a,u,c=this;try{u=t,t=o.isType("string",u)?r.parse(u):u}catch(t){return s.resolve(l(null,i.PARSE_ERROR))}return e=e||null,a=((n=o.isType("array",t))?t:[t]).map(function(t){return c._processRequest(t,e)}),n?function(t){return s.all(t).then(function(t){return(t=t.filter(function(t){return void 0!==t})).length?t:void 0})}(a):a[0]},t.exports=c},function(t){t.exports={PARSE_ERROR:{code:-32700,message:"Parse error"},INVALID_REQUEST:{code:-32600,message:"Invalid Request"},INVALID_PARAMS:{code:-32602,message:"Invalid params"},METHOD_NOT_FOUND:{code:-32601,message:"Method not found"},INTERNAL_ERROR:{code:-32603,message:"Internal error"}}},function(t,e,n){var r=n(9),i=n(1),o=n(25),s=n(2),a=n(21),u=n(0),c=n(3),d=n(7),l=a.ie9();function f(t,e,n){var r;t&&t.postMessage&&(l?r=(n||"")+o.stringify(e):n?(r={})[n]=e:r=e,t.postMessage(r,"*"))}function h(t){return u.isType("string",t)?t:"JSONRPC"}function p(t,e){return e?u.isType("string",t)&&0===t.indexOf(e)?t.substring(e.length):t&&t[e]?t[e]:void 0:t}function m(t,e){var n=t.document;this.filter=h(e),this.server=null,this.isTwitterFrame=c.isTwitterURL(n.location.href),t.addEventListener("message",d(this._onMessage,this),!1)}function v(t,e){this.pending={},this.target=t,this.isTwitterHost=c.isTwitterURL(r.href),this.filter=h(e),i.addEventListener("message",d(this._onMessage,this),!1)}u.aug(m.prototype,{_onMessage:function(t){var e,n=this;this.server&&(this.isTwitterFrame&&!c.isTwitterURL(t.origin)||(e=p(t.data,this.filter))&&this.server.receive(e,t.source).then(function(e){e&&f(t.source,e,n.filter)}))},attachTo:function(t){this.server=t},detach:function(){this.server=null}}),u.aug(v.prototype,{_processResponse:function(t){var e=this.pending[t.id];e&&(e.resolve(t),delete this.pending[t.id])},_onMessage:function(t){var e;if((!this.isTwitterHost||c.isTwitterURL(t.origin))&&(e=p(t.data,this.filter))){if(u.isType("string",e))try{e=o.parse(e)}catch(t){return}(e=u.isType("array",e)?e:[e]).forEach(d(this._processResponse,this))}},send:function(t){var e=new s;return t.id?this.pending[t.id]=e:e.resolve(),f(this.target,t,this.filter),e.promise}}),t.exports={Receiver:m,Dispatcher:v,_stringifyPayload:function(t){return arguments.length>0&&(l=!!t),l}}},function(t,e,n){var r=n(4);t.exports=function(t){for(var e,n=r.getElementsByTagName("iframe"),i=0;n[i];i++)if((e=n[i]).contentWindow===t)return e}},function(t,e,n){var r=n(5),i=n(0),o=n(3),s=n(13),a=n(14)(),u=n(64),c="a.twitter-moment";t.exports=function(t){return a(t,c).map(function(t){return u(function(t){var e=s(t),n={momentId:o.momentId(t.href),chrome:t.getAttribute("data-chrome"),limit:t.getAttribute("data-limit")};return i.forIn(n,function(t,n){var i=e[t];e[t]=r.hasValue(i)?i:n}),e}(t),t.parentNode,t)})}},function(t,e,n){var r=n(2);t.exports=function(t,e){var i=new r;return Promise.all([n.e(0),n.e(4)]).then(function(r){var o;try{o=n(85),i.resolve(new o(t,e))}catch(t){i.reject(t)}}.bind(null,n)).catch(function(t){i.reject(t)}),i.promise}},function(t,e,n){var r=n(0),i=n(13),o=n(14)(),s=n(65),a="a.periscope-on-air",u=/^https?:\/\/(?:www\.)?(?:periscope|pscp)\.tv\/@?([a-zA-Z0-9_]+)\/?$/i;t.exports=function(t){return o(t,a).map(function(t){return s(function(t){var e=i(t),n=t.getAttribute("href"),o=t.getAttribute("data-size"),s=u.exec(n)[1];return r.aug(e,{username:s,size:o})}(t),t.parentNode,t)})}},function(t,e,n){var r=n(2);t.exports=function(t,e){var i=new r;return n.e(5).then(function(r){var o;try{o=n(86),i.resolve(new o(t,e))}catch(t){i.reject(t)}}.bind(null,n)).catch(function(t){i.reject(t)}),i.promise}},function(t,e,n){var r=n(5),i=n(0),o=n(66),s=n(13),a=n(14)(),u=n(67),c=n(3),d=n(12),l="a.twitter-timeline,div.twitter-timeline,a.twitter-grid",f="Embedded Search timelines have been deprecated. See https://twittercommunity.com/t/deprecating-widget-settings/102295.",h="You may have been affected by an update to settings in embedded timelines. See https://twittercommunity.com/t/deprecating-widget-settings/102295.",p="Embedded grids have been deprecated and will now render as timelines. Please update your embed code to use the twitter-timeline class. More info: https://twittercommunity.com/t/update-on-the-embedded-grid-display-type/119564.";t.exports=function(t,e){return a(t,l).map(function(t){return u(function(t){var e=s(t),n=t.getAttribute("data-show-replies"),a={isPreconfigured:!!t.getAttribute("data-widget-id"),chrome:t.getAttribute("data-chrome"),tweetLimit:t.getAttribute("data-tweet-limit")||t.getAttribute("data-limit"),ariaLive:t.getAttribute("data-aria-polite"),theme:t.getAttribute("data-theme"),borderColor:t.getAttribute("data-border-color"),showReplies:n?r.asBoolean(n):null,profileScreenName:t.getAttribute("data-screen-name"),profileUserId:t.getAttribute("data-user-id"),favoritesScreenName:t.getAttribute("data-favorites-screen-name"),favoritesUserId:t.getAttribute("data-favorites-user-id"),likesScreenName:t.getAttribute("data-likes-screen-name"),likesUserId:t.getAttribute("data-likes-user-id"),listOwnerScreenName:t.getAttribute("data-list-owner-screen-name"),listOwnerUserId:t.getAttribute("data-list-owner-id"),listId:t.getAttribute("data-list-id"),listSlug:t.getAttribute("data-list-slug"),customTimelineId:t.getAttribute("data-custom-timeline-id"),staticContent:t.getAttribute("data-static-content"),url:t.href};return a.isPreconfigured&&(c.isSearchUrl(a.url)?d.publicError(f,t):d.publicLog(h,t)),"twitter-grid"===t.className&&d.publicLog(p,t),(a=i.aug(a,e)).dataSource=o(a),a.id=a.dataSource&&a.dataSource.id,a}(t),t.parentNode,t,e)})}},function(t,e,n){var r=n(28);t.exports=r.build([n(29),n(136)])},function(t,e,n){var r=n(0),i=n(135);t.exports=function(t){return"en"===t||r.contains(i,t)}},function(t,e){t.exports=["hi","zh-cn","fr","zh-tw","msa","fil","fi","sv","pl","ja","ko","de","it","pt","es","ru","id","tr","da","no","nl","hu","fa","ar","ur","he","th","cs","uk","vi","ro","bn","el","en-gb","gu","kn","mr","ta","bg","ca","hr","sr","sk"]},function(t,e,n){var r=n(3),i=n(0),o=n(20),s=n(48),a="collection:";function u(t,e){return r.collectionId(t)||e}t.exports=function(t){t.params({id:{},url:{}}),t.overrideProperty("id",{get:function(){var t=u(this.params.url,this.params.id);return a+t}}),t.overrideProperty("endpoint",{get:function(){return o.timeline(["collection"])}}),t.around("queryParams",function(t){return i.aug(t(),{collection_id:u(this.params.url,this.params.id)})}),t.before("initialize",function(){if(!u(this.params.url,this.params.id))throw new Error("one of url or id is required");s()})}},function(t,e,n){var r=n(28);t.exports=r.build([n(29),n(138)])},function(t,e,n){var r=n(3),i=n(0),o=n(20),s="event:";function a(t,e){return r.eventId(t)||e}t.exports=function(t){t.params({id:{},url:{}}),t.overrideProperty("id",{get:function(){var t=a(this.params.url,this.params.id);return s+t}}),t.overrideProperty("endpoint",{get:function(){return o.timeline(["event"])}}),t.around("queryParams",function(t){return i.aug(t(),{event_id:a(this.params.url,this.params.id)})}),t.before("initialize",function(){if(!a(this.params.url,this.params.id))throw new Error("one of url or id is required")})}},function(t,e,n){var r=n(28);t.exports=r.build([n(29),n(140)])},function(t,e,n){var r=n(3),i=n(0),o=n(20),s=n(48),a="likes:";function u(t){return r.likesScreenName(t.url)||t.screenName}t.exports=function(t){t.params({screenName:{},userId:{},url:{}}),t.overrideProperty("id",{get:function(){var t=u(this.params)||this.params.userId;return a+t}}),t.overrideProperty("endpoint",{get:function(){return o.timeline(["likes"])}}),t.define("_getLikesQueryParam",function(){var t=u(this.params);return t?{screen_name:t}:{user_id:this.params.userId}}),t.around("queryParams",function(t){return i.aug(t(),this._getLikesQueryParam())}),t.before("initialize",function(){if(!u(this.params)&&!this.params.userId)throw new Error("screen name or user id is required");s()})}},function(t,e,n){var r=n(28);t.exports=r.build([n(29),n(142)])},function(t,e,n){var r=n(3),i=n(0),o=n(20),s="list:";function a(t){var e=r.listScreenNameAndSlug(t.url)||t;return i.compact({screen_name:e.ownerScreenName,user_id:e.ownerUserId,list_slug:e.slug})}t.exports=function(t){t.params({id:{},ownerScreenName:{},ownerUserId:{},slug:{},url:{}}),t.overrideProperty("id",{get:function(){var t,e,n;return this.params.id?s+this.params.id:(e=(t=a(this.params))&&t.list_slug.replace(/-/g,"_"),n=t&&(t.screen_name||t.user_id),s+(n+":")+e)}}),t.overrideProperty("endpoint",{get:function(){return o.timeline(["list"])}}),t.define("_getListQueryParam",function(){return this.params.id?{list_id:this.params.id}:a(this.params)}),t.around("queryParams",function(t){return i.aug(t(),this._getListQueryParam())}),t.defineProperty("horizonEndpoint",{get:function(){var t,e=["timeline-list"];return this.params.id?e.push("list-id",this.params.id):(t=a(this.params),e.push("screen-name",t.screen_name,"slug",t.list_slug)),o.embedService(e)}}),t.before("initialize",function(){var t=a(this.params);if(i.isEmptyObject(t)&&!this.params.id)throw new Error("qualified slug or list id required")})}},function(t,e,n){var r=n(28);t.exports=r.build([n(29),n(144)])},function(t,e,n){var r=n(3),i=n(5),o=n(0),s=n(20),a="profile:";function u(t,e){return r.screenName(t)||e}t.exports=function(t){t.params({showReplies:{fallback:!1,transform:i.asBoolean},screenName:{},userId:{},url:{}}),t.overrideProperty("id",{get:function(){var t=u(this.params.url,this.params.screenName);return a+(t||this.params.userId)}}),t.overrideProperty("endpoint",{get:function(){return s.timeline(["profile"])}}),t.define("_getProfileQueryParam",function(){var t=u(this.params.url,this.params.screenName),e=t?{screen_name:t}:{user_id:this.params.userId};return o.aug(e,{with_replies:this.params.showReplies?"true":"false"})}),t.around("queryParams",function(t){return o.aug(t(),this._getProfileQueryParam())}),t.defineProperty("horizonEndpoint",{get:function(){var t=["timeline-profile"],e=u(this.params.url,this.params.screenName);return e?t.push("screen-name",e):t.push("user-id",this.params.userId),s.embedService(t)}}),t.around("horizonQueryParams",function(t){return o.aug(t(),{showReplies:this.params.showReplies?"true":"false"})}),t.before("initialize",function(){if(!u(this.params.url,this.params.screenName)&&!this.params.userId)throw new Error("screen name or user id is required")})}},function(t,e){var n={collection:"collection",moment:"moment",event:"event",likes:"likes",list:"list",profile:"profile"};t.exports={get:function(t){var e;return!!t&&(e=t.slice(0,t.indexOf(":")),-1!==Object.keys(n).indexOf(e)&&e)},DATASOURCE_MAP:n}},function(t,e,n){var r=n(2);t.exports=function(t,e){var i=new r;return n.e(6).then(function(r){var o;try{o=n(87),i.resolve(new o(t,e))}catch(t){i.reject(t)}}.bind(null,n)).catch(function(t){i.reject(t)}),i.promise}},function(t,e,n){var r=n(2);t.exports=function(t,e){var i=new r;return Promise.all([n.e(0),n.e(7)]).then(function(r){var o;try{o=n(88),i.resolve(new o(t,e))}catch(t){i.reject(t)}}.bind(null,n)).catch(function(t){i.reject(t)}),i.promise}},function(t,e,n){var r=n(10),i=n(3),o=n(0),s=n(13),a=n(14)(),u=n(68),c="blockquote.twitter-tweet, blockquote.twitter-video",d=/\btw-align-(left|right|center)\b/;t.exports=function(t,e){return a(t,c).map(function(t){return u(function(t){var e=s(t),n=t.getElementsByTagName("A"),a=n&&n[n.length-1],u=a&&i.status(a.href),c=t.getAttribute("data-conversation"),l="none"==c||"hidden"==c||r.present(t,"tw-hide-thread"),f=t.getAttribute("data-cards"),h="none"==f||"hidden"==f||r.present(t,"tw-hide-media"),p=t.getAttribute("data-align")||t.getAttribute("align"),m=t.getAttribute("data-theme");return!p&&d.test(t.className)&&(p=RegExp.$1),o.aug(e,{tweetId:u,hideThread:l,hideCard:h,align:p,theme:m,id:u})}(t),t.parentNode,t,e)})}},function(t,e,n){var r=n(2);t.exports=function(t,e){var i=new r;return n.e(8).then(function(r){var o;try{o=n(89),i.resolve(new o(t,e))}catch(t){i.reject(t)}}.bind(null,n)).catch(function(t){i.reject(t)}),i.promise}},function(t,e,n){var r=n(10),i=n(0),o=n(13),s=n(14)(),a=n(69),u=n(5),c="a.twitter-share-button, a.twitter-mention-button, a.twitter-hashtag-button",d="twitter-hashtag-button",l="twitter-mention-button";t.exports=function(t){return s(t,c).map(function(t){return a(function(t){var e=o(t),n={screenName:t.getAttribute("data-button-screen-name"),text:t.getAttribute("data-text"),type:t.getAttribute("data-type"),size:t.getAttribute("data-size"),url:t.getAttribute("data-url"),hashtags:t.getAttribute("data-hashtags"),via:t.getAttribute("data-via"),buttonHashtag:t.getAttribute("data-button-hashtag")};return i.forIn(n,function(t,n){var r=e[t];e[t]=u.hasValue(r)?r:n}),e.screenName=e.screenName||e.screen_name,e.buttonHashtag=e.buttonHashtag||e.button_hashtag||e.hashtag,r.present(t,d)&&(e.type="hashtag"),r.present(t,l)&&(e.type="mention"),e}(t),t.parentNode,t)})}},function(t,e,n){var r=n(2);t.exports=function(t,e){var i=new r;return n.e(3).then(function(r){var o;try{o=n(90),i.resolve(new o(t,e))}catch(t){i.reject(t)}}.bind(null,n)).catch(function(t){i.reject(t)}),i.promise}},function(t,e,n){var r=n(0);t.exports=r.aug({},n(153),n(154),n(155),n(156),n(157),n(158),n(159))},function(t,e,n){var r=n(61),i=n(17)(["userId"],{},r);t.exports={createDMButton:i}},function(t,e,n){var r=n(63),i=n(17)(["screenName"],{},r);t.exports={createFollowButton:i}},function(t,e,n){var r=n(64),i=n(17)(["momentId"],{},r);t.exports={createMoment:i}},function(t,e,n){var r=n(65),i=n(17)(["username"],{},r);t.exports={createPeriscopeOnAirButton:i}},function(t,e,n){var r=n(9),i=n(12),o=n(3),s=n(0),a=n(5),u=n(66),c=n(67),d=n(17)([],{},c),l=n(6),f="Embedded grids have been deprecated. Please use twttr.widgets.createTimeline instead. More info: https://twittercommunity.com/t/update-on-the-embedded-grid-display-type/119564.",h={createTimeline:p,createGridFromCollection:function(t){var e=s.toRealArray(arguments).slice(1),n={sourceType:"collection",id:t};return e.unshift(n),i.publicLog(f),p.apply(this,e)}};function p(t){var e,n=s.toRealArray(arguments).slice(1);return a.isString(t)||a.isNumber(t)?l.reject("Embedded timelines with widget settings have been deprecated. See https://twittercommunity.com/t/deprecating-widget-settings/102295."):s.isObject(t)?(t=t||{},n.forEach(function(t){s.isType("object",t)&&function(t){t.ariaLive=t.ariaPolite}(e=t)}),e||(e={},n.push(e)),t.lang=e.lang,t.tweetLimit=e.tweetLimit,t.showReplies=e.showReplies,e.dataSource=u(t),d.apply(this,n)):l.reject("data source must be an object.")}o.isTwitterURL(r.href)&&(h.createTimelinePreview=function(t,e,n){var r={previewParams:t,useLegacyDefaults:!0,isPreviewTimeline:!0};return r.dataSource=u(r),d(e,r,n)}),t.exports=h},function(t,e,n){var r,i=n(0),o=n(68),s=n(17),a=(r=s(["tweetId"],{},o),function(){return i.toRealArray(arguments).slice(1).forEach(function(t){i.isType("object",t)&&(t.hideCard="none"==t.cards||"hidden"==t.cards,t.hideThread="none"==t.conversation||"hidden"==t.conversation)}),r.apply(this,arguments)});t.exports={createTweet:a,createTweetEmbed:a,createVideo:a}},function(t,e,n){var r=n(0),i=n(69),o=n(17),s=o(["url"],{type:"share"},i),a=o(["buttonHashtag"],{type:"hashtag"},i),u=o(["screenName"],{type:"mention"},i);function c(t){return function(){return r.toRealArray(arguments).slice(1).forEach(function(t){r.isType("object",t)&&(t.screenName=t.screenName||t.screen_name,t.buttonHashtag=t.buttonHashtag||t.button_hashtag||t.hashtag)}),t.apply(this,arguments)}}t.exports={createShareButton:c(s),createHashtagButton:c(a),createMentionButton:c(u)}},function(t,e,n){var r,i,o,s=n(4),a=n(1),u=0,c=[],d=s.createElement("a");function l(){var t,e;for(u=1,t=0,e=c.length;t { 43 | const article = extractArticle() 44 | const headings = article && extractHeadings(article) 45 | renderToc(article,headings) 46 | } 47 | 48 | const renderToc = (article, headings): void => { 49 | if (toc) { 50 | toc.dispose() 51 | } 52 | 53 | if (!(article && headings && headings.length)) { 54 | chrome.storage.local.get({ 55 | isShowTip: true 56 | }, function (items) { 57 | if(items.isShowTip){ 58 | showToast('No article/headings are detected.') 59 | } 60 | }); 61 | return 62 | } 63 | 64 | isNewArticleDetected = true 65 | 66 | toc = createToc({ 67 | article, 68 | preference, 69 | }) 70 | toc.on('error', (error) => { 71 | if (toc) { 72 | toc.dispose() 73 | toc = undefined 74 | } 75 | // re-extract && restart 76 | // start() 77 | }) 78 | toc.show() 79 | } 80 | 81 | chrome.runtime.onMessage.addListener( 82 | (request: 'toggle' | 'prev' | 'next' | 'refresh' | 'load' | 'unload', sender, sendResponse) => { 83 | if(request === 'load' || request ==='unload'){ 84 | sendResponse(true) 85 | return 86 | } 87 | try { 88 | if (!isLoad || request === 'refresh') { 89 | load(); 90 | } else { 91 | if(toc){ 92 | toc[request]() 93 | } 94 | if(isLoad && request === 'toggle'){ 95 | unload() 96 | } 97 | } 98 | sendResponse(true) 99 | } catch (e) { 100 | console.error(e) 101 | sendResponse(false) 102 | } 103 | }, 104 | ) 105 | 106 | let observer:any = null; 107 | let timeoutTrack: any = null; 108 | 109 | function domListener() { 110 | var MutationObserver = 111 | window.MutationObserver || window.WebKitMutationObserver 112 | if (typeof MutationObserver !== 'function') { 113 | console.error( 114 | 'DOM Listener Extension: MutationObserver is not available in your browser.', 115 | ) 116 | return 117 | } 118 | 119 | let domChangeCount = 0; 120 | const callback = function (mutationsList, observer) { 121 | clearInterval(timeoutTrack); 122 | domChangeCount++; 123 | let intervalCount=0; 124 | timeoutTrack = setInterval(() => { 125 | intervalCount++; 126 | if(intervalCount === 4){ // 最多检测次数 127 | clearInterval(timeoutTrack) 128 | } 129 | if(isDebugging){ 130 | console.log({domChangeCount}); 131 | } 132 | domChangeCount = 0; 133 | if(intervalCount == 1){ 134 | setPreference(preference, trackArticle) 135 | } 136 | else if(intervalCount > 1 && !isNewArticleDetected){ 137 | detectToc() 138 | } 139 | }, 300); 140 | } 141 | 142 | if(observer === null){ 143 | observer = new MutationObserver(callback) 144 | } 145 | else { 146 | observer.disconnect() 147 | } 148 | 149 | const config = { 150 | attributes: true, attributeOldValue: true, subtree: true, 151 | childList: true 152 | } 153 | observer.observe(document, config) 154 | } 155 | 156 | let articleId = '' 157 | let articleContentClass = '' 158 | let selectorInoreader = '.article_content' 159 | let selectorFeedly = '.entryBody' 160 | 161 | chrome.storage.local.get({ 162 | selectorInoreader: '.article_content', 163 | selectorFeedly: '.entryBody' 164 | }, function(items) { 165 | selectorInoreader = items.selectorInoreader 166 | selectorFeedly = items.selectorFeedly 167 | }); 168 | 169 | function trackArticle() { 170 | const articleClass = isFeedly ? selectorFeedly : selectorInoreader; 171 | const el: HTMLElement = document.querySelector(articleClass) as HTMLElement; 172 | let isArticleChanged = (el && (el.id !== articleId || el.className !== articleContentClass)) || (!el && articleId !== '') 173 | if (isArticleChanged) { 174 | isNewArticleDetected = false 175 | if (isDebugging) { 176 | console.log('refresh') 177 | console.log(el) 178 | } 179 | articleId = el ? el.id : '' 180 | articleContentClass = el ? el.className : '' 181 | start() 182 | } 183 | } 184 | 185 | function detectToc(){ 186 | const article = extractArticle() 187 | const headings = article && extractHeadings(article) 188 | if (article && headings && headings.length>0){ 189 | setPreference(preference, ()=>{ 190 | renderToc(article, headings) 191 | }); 192 | clearInterval(timeoutTrack) 193 | } 194 | } 195 | 196 | const dm = document.domain 197 | const isInoReader = 198 | dm.indexOf('inoreader.com') >= 0 || dm.indexOf('innoreader.com') > 0 199 | const isFeedly = dm.indexOf('feedly.com') >= 0 200 | 201 | // auto load 202 | chrome.storage.local.get({ 203 | autoType: '0' 204 | }, function(items) { 205 | if(items.autoType!=='0'){ // not disabled 206 | let isAutoLoad = items.autoType === '1'; // all page 207 | if (items.autoType === '2') { // rss web app 208 | const dm = document.domain 209 | const isInoReader = 210 | dm.indexOf('inoreader.com') >= 0 || dm.indexOf('innoreader.com') > 0 211 | const isFeedly = dm.indexOf('feedly.com') >= 0 212 | isAutoLoad = isInoReader || isFeedly; 213 | } 214 | 215 | if(isAutoLoad){ 216 | load(); 217 | } 218 | } 219 | }); 220 | 221 | function load(){ 222 | chrome.runtime.sendMessage("load") 223 | isLoad = true 224 | setPreference(preference, start); 225 | if (isInoReader || isFeedly) { 226 | domListener() 227 | } 228 | } 229 | 230 | function unload(){ 231 | chrome.runtime.sendMessage("unload") 232 | isLoad = false 233 | if (toc) { 234 | toc.dispose() 235 | } 236 | if(observer !== null){ 237 | observer.disconnect() 238 | observer = null 239 | } 240 | } 241 | 242 | } -------------------------------------------------------------------------------- /src/content/lib/extract.ts: -------------------------------------------------------------------------------- 1 | import { isDebugging } from '../util/env' 2 | import { draw } from '../util/debug' 3 | import { Heading } from '../types' 4 | import { toArray } from '../util/dom/to_array' 5 | import { canScroll } from './scroll' 6 | 7 | const getAncestors = function(elem: HTMLElement, maxDepth = -1): HTMLElement[] { 8 | const ancestors: HTMLElement[] = [] 9 | let cur: HTMLElement | null = elem 10 | while (cur && maxDepth--) { 11 | ancestors.push(cur) 12 | cur = cur.parentElement 13 | } 14 | return ancestors 15 | } 16 | 17 | const ARTICLE_TAG_WEIGHTS: { [Selector: string]: number[] } = { 18 | h1: [0, 100, 60, 40, 30, 25, 22, 18].map((s) => s * 0.4), 19 | h2: [0, 100, 60, 40, 30, 25, 22, 18], 20 | h3: [0, 100, 60, 40, 30, 25, 22, 18].map((s) => s * 0.5), 21 | h4: [0, 100, 60, 40, 30, 25, 22, 18].map((s) => s * 0.5 * 0.5), 22 | h5: [0, 100, 60, 40, 30, 25, 22, 18].map((s) => s * 0.5 * 0.5 * 0.5), 23 | h6: [0, 100, 60, 40, 30, 25, 22, 18].map((s) => s * 0.5 * 0.5 * 0.5 * 0.5), 24 | strong: [0, 100, 60, 40, 30, 25, 22, 18].map((s) => s * 0.5 * 0.5 * 0.5), 25 | article: [500], 26 | '.article': [500], 27 | '#article': [500], 28 | '.content': [101], 29 | sidebar: [-500, -100, -50], 30 | '.sidebar': [-500, -100, -50], 31 | '#sidebar': [-500, -100, -50], 32 | aside: [-500, -100, -50], 33 | '.aside': [-500, -100, -50], 34 | '#aside': [-500, -100, -50], 35 | nav: [-500, -100, -50], 36 | '.nav': [-500, -100, -50], 37 | '.navigation': [-500, -100, -50], 38 | '.toc': [-500, -100, -50], 39 | '.table-of-contents': [-500, -100, -50], 40 | '.comment': [-500, -100, -50], 41 | } 42 | 43 | const getElemsCommonLeft = (elems: HTMLElement[]): number | undefined => { 44 | if (!elems.length) { 45 | return undefined 46 | } 47 | const lefts: { [Left: number]: number } = {} 48 | elems.forEach((el) => { 49 | const left = el.getBoundingClientRect().left 50 | if (!lefts[left]) { 51 | lefts[left] = 0 52 | } 53 | lefts[left]++ 54 | }) 55 | const count = elems.length 56 | 57 | const isAligned = Object.keys(lefts).length <= Math.ceil(0.3 * count) 58 | if (!isAligned) { 59 | return undefined 60 | } 61 | const sortedByCount = Object.keys(lefts).sort((a, b) => lefts[b] - lefts[a]) 62 | const most = Number(sortedByCount[0]) 63 | return most 64 | } 65 | 66 | export const extractArticle = function(): HTMLElement | undefined { 67 | const elemScores = new Map() 68 | 69 | // weigh nodes by factor: "selector" "distance from this node" 70 | Object.keys(ARTICLE_TAG_WEIGHTS).forEach((selector) => { 71 | let elems = toArray(document.querySelectorAll(selector)) as HTMLElement[] 72 | if (selector.toLowerCase() === 'strong') { 73 | // for elements, only take them as heading when they align at left 74 | const commonLeft = getElemsCommonLeft(elems) 75 | if (commonLeft === undefined || commonLeft > window.innerWidth / 2) { 76 | elems = [] 77 | } else { 78 | elems = elems.filter( 79 | (elem) => elem.getBoundingClientRect().left === commonLeft, 80 | ) 81 | } 82 | } 83 | elems.forEach((elem) => { 84 | const weights = ARTICLE_TAG_WEIGHTS[selector] 85 | const ancestors = getAncestors(elem as HTMLElement, weights.length) 86 | ancestors.forEach((elem, distance) => { 87 | elemScores.set( 88 | elem, 89 | (elemScores.get(elem) || 0) + weights[distance] || 0, 90 | ) 91 | }) 92 | }) 93 | }) 94 | const sortedByScore = [...elemScores].sort((a, b) => b[1] - a[1]) 95 | 96 | // pick top 5 node to re-weigh 97 | const candicates = sortedByScore 98 | .slice(0, 5) 99 | .filter(Boolean) 100 | .map(([elem, score]) => { 101 | return { elem, score } 102 | }) 103 | 104 | // re-weigh by factor: "take-lots-vertical-space", "contain-less-links", "not-too-narrow", "cannot-scroll" 105 | const isTooNarrow = (e: HTMLElement) => e.scrollWidth < 400 // rule out sidebars 106 | candicates.forEach((candicate) => { 107 | if (isTooNarrow(candicate.elem)) { 108 | candicate.score = 0 109 | candicates.forEach((parent) => { 110 | if (parent.elem.contains(candicate.elem)) { 111 | parent.score *= 0.7 112 | } 113 | }) 114 | } 115 | if (canScroll(candicate.elem) && candicate.elem !== document.body) { 116 | candicate.score *= 0.5 117 | } 118 | }) 119 | 120 | const reweighted = candicates 121 | .map(({ elem, score }) => { 122 | return { 123 | elem, 124 | score: 125 | score * 126 | Math.log( 127 | (elem.scrollHeight * elem.scrollHeight) / 128 | (elem.querySelectorAll('a').length || 1), 129 | ), 130 | } 131 | }) 132 | .sort((a, b) => b.score - a.score) 133 | 134 | if (isDebugging) { 135 | console.log('[extract]', { 136 | elemScores, 137 | sortedByScore, 138 | candicates, 139 | reweighted, 140 | }) 141 | } 142 | let article: HTMLElement | undefined = reweighted.length ? reweighted[0].elem : undefined 143 | 144 | const dm=document.domain; 145 | const isInoReader = dm.indexOf('inoreader.com')>=0 || dm.indexOf('innoreader.com')>0; 146 | const isFeedly = dm.indexOf('feedly.com')>=0; 147 | 148 | let selectorInoreader = '.article_content' 149 | let selectorFeedly = '.entryBody' 150 | 151 | chrome.storage.local.get({ 152 | selectorInoreader: '.article_content', 153 | selectorFeedly: '.entryBody' 154 | }, function(items) { 155 | selectorInoreader = items.selectorInoreader 156 | selectorFeedly = items.selectorFeedly 157 | }); 158 | 159 | if(isInoReader || isFeedly){ 160 | const articleClass= isFeedly ? selectorFeedly : selectorInoreader 161 | const content = document.querySelector(articleClass) 162 | if(content!=null){ 163 | article = content as HTMLElement 164 | } 165 | else{ 166 | article = undefined 167 | } 168 | } 169 | 170 | if (isDebugging) { 171 | draw(article, 'red') 172 | } 173 | return article 174 | } 175 | 176 | const HEADING_TAG_WEIGHTS = { 177 | H1: 4, 178 | H2: 9, 179 | H3: 9, 180 | H4: 10, 181 | H5: 10, 182 | H6: 10, 183 | STRONG: 5, 184 | } 185 | export const extractHeadings = (articleDom: HTMLElement): Heading[] => { 186 | const isVisible = (elem: HTMLElement) => elem.offsetHeight !== 0 187 | type HeadingGroup = { 188 | tag: string 189 | elems: HTMLElement[] 190 | score: number 191 | } 192 | 193 | const isHeadingGroupVisible = (group: HeadingGroup) => { 194 | return group.elems.filter(isVisible).length >= group.elems.length * 0.5 195 | } 196 | 197 | const headingTagGroups: HeadingGroup[] = Object.keys(HEADING_TAG_WEIGHTS) 198 | .map( 199 | (tag): HeadingGroup => { 200 | let elems = toArray( 201 | articleDom.getElementsByTagName(tag), 202 | ) as HTMLElement[] 203 | if (tag.toLowerCase() === 'strong') { 204 | // for elements, only take them as heading when they align at left 205 | const commonLeft = getElemsCommonLeft(elems) 206 | if (commonLeft === undefined || commonLeft > articleDom.getBoundingClientRect().left + 100) { 207 | elems = [] 208 | } else { 209 | elems = elems.filter( 210 | (elem) => elem.getBoundingClientRect().left === commonLeft, 211 | ) 212 | } 213 | } 214 | return { 215 | tag, 216 | elems, 217 | score: elems.length * HEADING_TAG_WEIGHTS[tag], 218 | } 219 | }, 220 | ) 221 | .filter((group) => group.score >= 10 && group.elems.length > 0) 222 | .filter((group) => isHeadingGroupVisible(group)) 223 | .slice(0, 3) 224 | 225 | // use document sequence 226 | const headingTags = headingTagGroups.map((headings) => headings.tag) 227 | const acceptNode = (node: HTMLElement) => { 228 | const group = headingTagGroups.find((g) => g.tag === node.tagName) 229 | if (!group) { 230 | return NodeFilter.FILTER_SKIP 231 | } 232 | return group.elems.includes(node) && isVisible(node) 233 | ? NodeFilter.FILTER_ACCEPT 234 | : NodeFilter.FILTER_SKIP 235 | } 236 | const treeWalker = document.createTreeWalker( 237 | articleDom, 238 | NodeFilter.SHOW_ELEMENT, 239 | { acceptNode }, 240 | ) 241 | const headings: Heading[] = [] 242 | let id = 0 243 | while (treeWalker.nextNode()) { 244 | const dom = treeWalker.currentNode as HTMLElement 245 | const anchor = 246 | dom.id || 247 | toArray(dom.querySelectorAll('a')) 248 | .map((a) => { 249 | let href = a.getAttribute('href') || '' 250 | return href.startsWith('#') ? href.substr(1) : a.id 251 | }) 252 | .filter(Boolean)[0] 253 | headings.push({ 254 | dom, 255 | text: dom.textContent || '', 256 | level: headingTags.indexOf(dom.tagName) + 1, 257 | id, 258 | anchor, 259 | }) 260 | id++ 261 | } 262 | if (isDebugging) { 263 | if (headingTagGroups.length > 0) draw(headingTagGroups[0].elems, 'blue') 264 | if (headingTagGroups.length > 1) draw(headingTagGroups[1].elems, 'green') 265 | if (headingTagGroups.length > 2) draw(headingTagGroups[2].elems, 'yellow') 266 | } 267 | return headings 268 | } 269 | -------------------------------------------------------------------------------- /src/content/lib/iframe.ts: -------------------------------------------------------------------------------- 1 | const getIframes = (wnd: Window): HTMLIFrameElement[] => { 2 | let iframes: HTMLIFrameElement[] = [] 3 | try { 4 | iframes = [].slice.apply(wnd.document.getElementsByTagName('iframe')) 5 | } catch (error) { 6 | // ignore 7 | } 8 | return iframes.reduce((prev, curIframe) => { 9 | const curWindow = curIframe.contentWindow 10 | return prev.concat(curIframe, curWindow ? getIframes(curWindow) : []) 11 | }, [] as HTMLIFrameElement[]) 12 | } 13 | 14 | export const getContentWindow = (): Window => { 15 | const rootWindow = window.top 16 | 17 | const allIframes = getIframes(rootWindow) 18 | 19 | const allIframesWithArea = allIframes 20 | .map((iframe) => { 21 | return { 22 | iframe: iframe, 23 | area: iframe.offsetWidth * iframe.offsetHeight, 24 | } 25 | }) 26 | .sort((a, b) => b.area - a.area) 27 | 28 | if (allIframesWithArea.length === 0) { 29 | return rootWindow 30 | } 31 | const largest = allIframesWithArea[0] 32 | 33 | const rootDocument = rootWindow.document.documentElement 34 | const rootArea = rootDocument.offsetWidth * rootDocument.offsetHeight 35 | 36 | return largest.area > rootArea * 0.5 37 | ? largest.iframe.contentWindow || rootWindow 38 | : rootWindow 39 | } 40 | -------------------------------------------------------------------------------- /src/content/lib/readable.ts: -------------------------------------------------------------------------------- 1 | import { num, px } from '../util/dom/px' 2 | import { between } from '../util/math/between' 3 | import { applyStyle } from '../util/dom/css' 4 | import { Content, Heading } from '../types' 5 | import { isDebugging } from '../util/env' 6 | import { toArray } from '../util/dom/to_array' 7 | 8 | //-------------- container extender -------------- 9 | 10 | const EXTENDER_ID = 'smarttoc-extender' 11 | const appendExtender = (content: Content, topbarHeight: number): void => { 12 | const { article, scroller, headings } = content 13 | let extender: HTMLElement | null = scroller.dom.querySelector( 14 | '#' + EXTENDER_ID, 15 | ) 16 | const extenderHeight = extender ? (extender as HTMLElement).offsetHeight : 0 17 | if (!extender) { 18 | extender = document.createElement('div') 19 | extender.id = EXTENDER_ID 20 | scroller.dom.appendChild(extender) 21 | } 22 | 23 | const visibleAreaBottom = Math.max(topbarHeight, scroller.rect.top) 24 | 25 | const getBottomHeading = (headings: Heading[]): Heading | undefined => { 26 | const [first, ...rest] = headings 27 | return ( 28 | first && 29 | rest.reduce( 30 | (lowest, cur) => 31 | cur.fromArticleTop! > lowest.fromArticleTop! ? cur : lowest, 32 | first, 33 | ) 34 | ) 35 | } 36 | const bottomHeading = getBottomHeading(headings) 37 | if (!bottomHeading) { 38 | return 39 | } 40 | 41 | // the top of last heading, when scrolled to bottom of scroller 42 | const bottomHeadingTop = 43 | scroller.rect.bottom - 44 | (scroller.dom.scrollHeight - extenderHeight) + 45 | article.fromScrollerTop + 46 | bottomHeading.fromArticleTop! 47 | 48 | const additionalVerticalSpaceNeeded = Math.max( 49 | 0, 50 | bottomHeadingTop - visibleAreaBottom, 51 | ) 52 | 53 | extender.style.height = additionalVerticalSpaceNeeded + 'px' 54 | if (isDebugging) { 55 | console.log('[extender] height: ', additionalVerticalSpaceNeeded) 56 | } 57 | } 58 | const removeExtender = (): void => { 59 | const extender = document.getElementById(EXTENDER_ID) 60 | if (extender) { 61 | extender.remove() 62 | } 63 | } 64 | 65 | //-------------- apply readable style -------------- 66 | 67 | const DATASET_ARTICLE = 'smarttocArticle' 68 | const DATASET_ARTICLE__CAMELCASE = 'smarttoc-article' 69 | const DATASET_ORIGIN_STYLE = 'smarttocOriginStyle' 70 | 71 | const applyReadableStyle = (article: HTMLElement): void => { 72 | article.dataset[DATASET_ARTICLE] = '1' 73 | article.dataset[DATASET_ORIGIN_STYLE] = article.style.cssText 74 | 75 | const computed = window.getComputedStyle(article) 76 | if (!computed) throw new Error('article should be element') 77 | const rect = article.getBoundingClientRect() 78 | 79 | let bestWidth = between(12, num(computed.fontSize!), 16) * 66 80 | if (computed['box-sizing'] === 'border-box') { 81 | bestWidth += num(computed['padding-left']) + num(computed['padding-right']) 82 | } 83 | 84 | let readableStyle = {} as CSSStyleDeclaration 85 | if (bestWidth < rect.width) { 86 | readableStyle.maxWidth = px(bestWidth) 87 | if (!(num(computed.marginLeft!) || num(computed.marginRight!))) { 88 | readableStyle.marginLeft = 'auto' 89 | readableStyle.marginRight = 'auto' 90 | } 91 | } 92 | applyStyle(article, readableStyle) 93 | } 94 | 95 | const removeReadableStyle = (): void => { 96 | const articles = toArray( 97 | document.querySelectorAll(`[data-${DATASET_ARTICLE__CAMELCASE}]`), 98 | ) as HTMLElement[] 99 | articles.forEach((article) => { 100 | applyStyle(article, article.dataset[DATASET_ORIGIN_STYLE]) 101 | delete article.dataset[DATASET_ARTICLE] 102 | delete article.dataset[DATASET_ORIGIN_STYLE] 103 | }) 104 | } 105 | 106 | export const enterReadableMode = ( 107 | content: Content, 108 | { topbarHeight }: { topbarHeight: number }, 109 | ): void => { 110 | leaveReadableMode() 111 | 112 | if (isDebugging) { 113 | console.log('[readable mode] enter') 114 | } 115 | applyReadableStyle(content.article.dom) 116 | appendExtender(content, topbarHeight) 117 | } 118 | 119 | export const leaveReadableMode = (): void => { 120 | if (isDebugging) { 121 | console.log('[readable mode] leave') 122 | } 123 | removeReadableStyle() 124 | removeExtender() 125 | } 126 | -------------------------------------------------------------------------------- /src/content/lib/scroll.ts: -------------------------------------------------------------------------------- 1 | import { isDebugging } from '../util/env' 2 | import { draw } from '../util/debug' 3 | 4 | export const getScrollTop = (elem: HTMLElement): number => { 5 | if (elem === document.body) { 6 | return document.documentElement.scrollTop || document.body.scrollTop 7 | } else { 8 | return elem.scrollTop 9 | } 10 | } 11 | 12 | export const setScrollTop = (elem: HTMLElement, val: number): void => { 13 | if (elem === document.body) { 14 | document.documentElement.scrollTop = val 15 | window.scrollTo(window.scrollX, val) 16 | } else { 17 | elem.scrollTop = val 18 | } 19 | } 20 | 21 | export const canScroll = (el: HTMLElement) => { 22 | return ( 23 | ['auto', 'scroll'].includes(window.getComputedStyle(el)!.overflowY!) && 24 | el.clientHeight + 1 < el.scrollHeight 25 | ) 26 | } 27 | 28 | export const getScrollElement = (elem: HTMLElement): HTMLElement => { 29 | while (elem && elem !== document.body && !canScroll(elem)) { 30 | elem = elem.parentElement! 31 | } 32 | if (isDebugging) { 33 | draw(elem, 'purple') 34 | } 35 | return elem 36 | } 37 | 38 | const easeOutQuad = ( 39 | progress: number, 40 | start: number, 41 | distance: number, 42 | ): number => { 43 | return distance * progress * (2 - progress) + start 44 | } 45 | 46 | export const smoothScroll = ({ 47 | target, 48 | scroller, 49 | topMargin = 0, 50 | maxDuration = 300, 51 | callback, 52 | }: { 53 | target: HTMLElement 54 | scroller: HTMLElement 55 | maxDuration?: number 56 | topMargin?: number 57 | callback?(): void 58 | }) => { 59 | const ease = easeOutQuad 60 | const targetTop = target.getBoundingClientRect().top 61 | const containerTop = 62 | scroller === document.body ? 0 : scroller.getBoundingClientRect().top 63 | 64 | const scrollStart = getScrollTop(scroller) 65 | const scrollEnd = targetTop - (containerTop - scrollStart) - topMargin 66 | 67 | const distance = scrollEnd - scrollStart 68 | const distanceRatio = Math.min(1, Math.abs(distance) / 10000) 69 | const duration = Math.max( 70 | 10, 71 | maxDuration * distanceRatio * (2 - distanceRatio), 72 | ) 73 | 74 | if (maxDuration === 0) { 75 | setScrollTop(scroller, scrollEnd) 76 | if (callback) { 77 | callback() 78 | } 79 | return 80 | } 81 | 82 | let startTime: number 83 | function update(timestamp: number) { 84 | if (!startTime) { 85 | startTime = timestamp 86 | } 87 | const progress = (timestamp - startTime) / duration 88 | if (progress < 1) { 89 | const scrollPos = ease(progress, scrollStart, distance) 90 | setScrollTop(scroller, scrollPos) 91 | window.requestAnimationFrame(update) 92 | } else { 93 | setScrollTop(scroller, scrollEnd) 94 | if (callback) { 95 | callback() 96 | } 97 | } 98 | } 99 | window.requestAnimationFrame(update) 100 | } 101 | -------------------------------------------------------------------------------- /src/content/style/toast.css: -------------------------------------------------------------------------------- 1 | #smarttoc-toast { 2 | all: initial; 3 | } 4 | 5 | #smarttoc-toast * { 6 | all: unset; 7 | } 8 | 9 | #smarttoc-toast { 10 | display: none; 11 | position: fixed; 12 | right: 0; 13 | top: 0; 14 | margin: 1em 2em; 15 | padding: 1em; 16 | z-index: 10000; 17 | box-sizing: border-box; 18 | background-color: #fff; 19 | border: 1px solid rgba(158, 158, 158, 0.22); 20 | color: gray; 21 | font-size: calc(12px + 0.15vw); 22 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 23 | font-weight: normal; 24 | -webkit-font-smoothing: subpixel-antialiased; 25 | font-smoothing: subpixel-antialiased; 26 | transition: opacity 200ms ease-out, transform 200ms ease-out; 27 | } 28 | 29 | #smarttoc-toast.enter { 30 | display: block; 31 | opacity: 0.01; 32 | transform: translate3d(0, -2em, 0); 33 | } 34 | 35 | #smarttoc-toast.enter.enter-active { 36 | display: block; 37 | opacity: 1; 38 | transform: translate3d(0, 0, 0); 39 | } 40 | 41 | #smarttoc-toast.leave { 42 | display: block; 43 | opacity: 1; 44 | transform: translate3d(0, 0, 0); 45 | } 46 | 47 | #smarttoc-toast.leave.leave-active { 48 | display: block; 49 | opacity: 0.01; 50 | transform: translate3d(0, -2em, 0); 51 | } 52 | -------------------------------------------------------------------------------- /src/content/style/toc.css: -------------------------------------------------------------------------------- 1 | #smarttoc { 2 | all: initial; 3 | } 4 | 5 | #smarttoc *, 6 | #smarttoc *::before, 7 | #smarttoc *::after { 8 | all: unset; 9 | } 10 | 11 | #smarttoc { 12 | display: flex; 13 | flex-direction: column; 14 | align-items: stretch; 15 | position: fixed; 16 | /* left: 0px; */ 17 | /* top: 0px; */ 18 | min-width: 14em; 19 | width: 260px; 20 | max-height: 80vh; 21 | z-index: 10000; 22 | box-sizing: border-box; 23 | background-color: #fff; 24 | color: gray; 25 | font-size: calc(12px + 0.1vw); 26 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 27 | line-height: 1.5; 28 | font-weight: normal; 29 | border: 1px solid rgba(158, 158, 158, 0.22); 30 | -webkit-font-smoothing: subpixel-antialiased; 31 | font-smoothing: subpixel-antialiased; 32 | overflow: hidden; 33 | will-change: transform, max-width; 34 | transition: max-width 0.3s; 35 | contain: content; 36 | box-shadow: rgba(17, 17, 26, 0.1) 0px 4px 16px, rgba(17, 17, 26, 0.1) 0px 8px 24px, rgba(17, 17, 26, 0.1) 0px 16px 56px; 37 | } 38 | 39 | #smarttoc.hidden { 40 | display: none; 41 | } 42 | 43 | #smarttoc .handle { 44 | -webkit-user-select: none; 45 | user-select: none; 46 | 47 | border-bottom: 1px solid rgba(158, 158, 158, 0.22); 48 | padding: 0.1em 0.7em; 49 | font-variant-caps: inherit; 50 | font-variant: small-caps; 51 | font-size: 0.9em; 52 | color: #bbb; 53 | cursor: pointer; 54 | text-align: center; 55 | opacity: 0; 56 | will-change: opacity; 57 | transition: opacity 0.3s; 58 | } 59 | 60 | #smarttoc:hover .handle { 61 | max-width: 100vw; 62 | opacity: 1; 63 | } 64 | 65 | #smarttoc .handle:hover { 66 | cursor: grab; 67 | } 68 | #smarttoc .handle:active { 69 | cursor: grabbing; 70 | } 71 | 72 | #smarttoc .handle:active { 73 | background: #f9f9f9; 74 | } 75 | 76 | #smarttoc > ul { 77 | flex-grow: 1; 78 | padding: 0 1.3em 1.3em 1em; 79 | overflow-y: auto; 80 | } 81 | 82 | /* all headings */ 83 | 84 | #smarttoc ul, 85 | #smarttoc li { 86 | list-style: none; 87 | display: block; 88 | } 89 | 90 | #smarttoc a { 91 | text-decoration: none; 92 | color: gray; 93 | display: block; 94 | line-height: 1.3; 95 | padding-top: 0.2em; 96 | padding-bottom: 0.2em; 97 | text-overflow: ellipsis; 98 | overflow-x: hidden; 99 | /* white-space: nowrap; */ 100 | word-break: break-all; 101 | cursor: pointer; 102 | } 103 | 104 | #smarttoc a:hover, 105 | #smarttoc a:active { 106 | border-left-color: rgba(86, 61, 124, 0.5); 107 | color: #563d7c; 108 | } 109 | 110 | #smarttoc li.active > a { 111 | border-left-color: #563d7c; 112 | color: #563d7c; 113 | } 114 | 115 | /* heading level: 1 */ 116 | 117 | #smarttoc ul { 118 | line-height: 2; 119 | } 120 | 121 | #smarttoc ul a { 122 | font-size: 1em; 123 | padding-left: 1.3em; 124 | } 125 | 126 | #smarttoc ul a:hover, 127 | #smarttoc ul a:active, 128 | #smarttoc ul li.active > a { 129 | border-left-width: 3px; 130 | border-left-style: solid; 131 | padding-left: calc(1.3em - 3px); 132 | } 133 | 134 | #smarttoc ul li.active > a { 135 | font-weight: 700; 136 | } 137 | 138 | /* heading level: 2 (hidden only when there are too many headings) */ 139 | 140 | #smarttoc ul ul { 141 | line-height: 1.8; 142 | } 143 | 144 | #smarttoc.lengthy ul ul { 145 | display: none; 146 | } 147 | 148 | #smarttoc.lengthy ul li.active > ul { 149 | display: block; 150 | } 151 | 152 | #smarttoc ul ul a { 153 | font-size: 1em; 154 | padding-left: 2.7em; 155 | } 156 | 157 | #smarttoc ul ul a:hover, 158 | #smarttoc ul ul a:active, 159 | #smarttoc ul ul li.active > a { 160 | border-left-width: 2px; 161 | border-left-style: solid; 162 | padding-left: calc(2.7em - 2px); 163 | font-weight: normal; 164 | } 165 | 166 | /* heading level: 3 (hidden unless parent is active) */ 167 | 168 | #smarttoc ul ul ul { 169 | line-height: 1.7; 170 | display: none; 171 | } 172 | 173 | #smarttoc ul ul li.active > ul { 174 | display: block; 175 | } 176 | 177 | #smarttoc ul ul ul a { 178 | font-size: 1em; 179 | padding-left: 4em; 180 | } 181 | 182 | #smarttoc ul ul ul a:hover, 183 | #smarttoc ul ul ul a:active, 184 | #smarttoc ul ul ul li.active > a { 185 | border-left-width: 1px; 186 | border-left-style: solid; 187 | padding-left: calc(4em - 1px); 188 | font-weight: normal; 189 | } 190 | -------------------------------------------------------------------------------- /src/content/toc.ts: -------------------------------------------------------------------------------- 1 | import { extractHeadings } from './lib/extract' 2 | import { enterReadableMode, leaveReadableMode } from './lib/readable' 3 | import { getScrollElement, getScrollTop, smoothScroll } from './lib/scroll' 4 | import { Article, Content, Heading, Scroller } from './types' 5 | import { ui } from './ui/index' 6 | import { createEventEmitter } from './util/event' 7 | import { Stream } from './util/stream' 8 | import { isDebugging, offsetKey } from './util/env' 9 | 10 | export interface TocPreference { 11 | offset: { 12 | x: number 13 | y: number 14 | } 15 | } 16 | 17 | export interface TocEvents { 18 | error: { 19 | reason: string 20 | } 21 | } 22 | 23 | function activeHeadingStream({ 24 | scroller, 25 | $isShown, 26 | $content, 27 | $topbarHeight, 28 | addDisposer, 29 | }: { 30 | scroller: HTMLElement 31 | $isShown: Stream 32 | $content: Stream 33 | $topbarHeight: Stream 34 | addDisposer: (unsub: () => void) => number 35 | }) { 36 | const calcActiveHeading = ({ 37 | article, 38 | scroller, 39 | headings, 40 | topbarHeight, 41 | }: { 42 | article: Article 43 | scroller: Scroller 44 | headings: Heading[] 45 | topbarHeight: number 46 | }): number => { 47 | const visibleAreaHeight = Math.max(topbarHeight, scroller.rect.top) 48 | const scrollY = getScrollTop(scroller.dom) 49 | 50 | let i = 0 51 | for (; i < headings.length; i++) { 52 | const heading = headings[i] 53 | const headingRectTop = 54 | scroller.rect.top - 55 | scrollY + 56 | article.fromScrollerTop + 57 | heading.fromArticleTop! 58 | const isCompletelyVisible = headingRectTop >= visibleAreaHeight + 15 59 | if (isCompletelyVisible) { 60 | break 61 | } 62 | } 63 | // the 'nearly-visible' heading (headings[i-1]) is the current heading 64 | const curIndex = Math.max(0, i - 1) 65 | return curIndex 66 | } 67 | 68 | const $scroll = Stream.fromEvent( 69 | scroller === document.body ? window : scroller, 70 | 'scroll', 71 | addDisposer, 72 | ) 73 | .map(() => null) 74 | .startsWith(null) 75 | .log('scroll') 76 | 77 | const $activeHeading: Stream = Stream.combine( 78 | $content, 79 | $topbarHeight, 80 | $scroll, 81 | $isShown, 82 | ) 83 | .filter(() => $isShown()) 84 | .map(([content, topbarHeight, _]) => { 85 | const { article, scroller, headings } = content 86 | if (!(headings && headings.length)) { 87 | return 0 88 | } else { 89 | return calcActiveHeading({ 90 | article, 91 | scroller, 92 | headings, 93 | topbarHeight: topbarHeight || 0, 94 | }) 95 | } 96 | }) 97 | 98 | return $activeHeading 99 | } 100 | 101 | function contentStream({ 102 | $isShown, 103 | $periodicCheck, 104 | $triggerContentChange, 105 | article, 106 | scroller, 107 | addDisposer, 108 | }: { 109 | article: HTMLElement 110 | scroller: HTMLElement 111 | $triggerContentChange: Stream 112 | $isShown: Stream 113 | $periodicCheck: Stream 114 | addDisposer: (unsub: () => void) => number 115 | }) { 116 | const $resize = Stream.fromEvent(window, 'resize', addDisposer) 117 | .throttle(100) 118 | .log('resize') 119 | 120 | const $content: Stream = Stream.merge( 121 | $triggerContentChange, 122 | $isShown, 123 | $resize, 124 | $periodicCheck, 125 | ) 126 | .filter(() => $isShown()) 127 | .map((): Content => { 128 | const articleRect = article.getBoundingClientRect() 129 | const scrollerRect = 130 | scroller === document.body || scroller === document.documentElement 131 | ? { 132 | left: 0, 133 | right: window.innerWidth, 134 | top: 0, 135 | bottom: window.innerHeight, 136 | height: window.innerHeight, 137 | width: window.innerWidth, 138 | } 139 | : scroller.getBoundingClientRect() 140 | const headings = extractHeadings(article) 141 | const scrollY = getScrollTop(scroller) 142 | const headingsMeasured = headings.map((h) => { 143 | const headingRect = h.dom.getBoundingClientRect() 144 | return { 145 | ...h, 146 | fromArticleTop: 147 | headingRect.top - (articleRect.top - article.scrollTop), 148 | } 149 | }) 150 | return { 151 | article: { 152 | dom: article, 153 | fromScrollerTop: 154 | article === scroller 155 | ? 0 156 | : articleRect.top - scrollerRect.top + scrollY, 157 | left: articleRect.left, 158 | right: articleRect.right, 159 | height: articleRect.height, 160 | }, 161 | scroller: { 162 | dom: scroller, 163 | rect: scrollerRect, 164 | }, 165 | headings: headingsMeasured, 166 | } 167 | }) 168 | 169 | return $content 170 | } 171 | 172 | function topbarStream($triggerTopbarMeasure: Stream, scroller: HTMLElement) { 173 | const getTopbarHeight = (targetElem: HTMLElement): number => { 174 | const findFixedParent = (elem: HTMLElement | null) => { 175 | const isFixed = (elem: HTMLElement) => { 176 | let { position, zIndex } = window.getComputedStyle(elem) 177 | return position === 'fixed' && zIndex 178 | } 179 | while (elem && elem !== document.body && !isFixed(elem) && elem !== scroller) { 180 | elem = elem.parentElement 181 | } 182 | return elem === document.body || elem === scroller ? null : elem 183 | } 184 | 185 | const { top, left, right, bottom } = targetElem.getBoundingClientRect() 186 | const leftTopmost = document.elementFromPoint(left + 1, top + 1) 187 | const rightTopmost = document.elementFromPoint(right - 1, top + 1) 188 | const leftTopFixed = 189 | leftTopmost && findFixedParent(leftTopmost as HTMLElement) 190 | const rightTopFixed = 191 | rightTopmost && findFixedParent(rightTopmost as HTMLElement) 192 | 193 | if (leftTopFixed && rightTopFixed && leftTopFixed === rightTopFixed) { 194 | return leftTopFixed.offsetHeight 195 | } else { 196 | return 0 197 | } 198 | } 199 | 200 | const $topbarHeightMeasured: Stream = $triggerTopbarMeasure 201 | .throttle(50) 202 | .map((elem) => getTopbarHeight(elem)) 203 | .unique() 204 | .log('topbarHeightMeasured') 205 | 206 | const $topbarHeight: Stream = $topbarHeightMeasured.scan( 207 | (height, measured) => Math.max(height, measured), 208 | 0 as number, 209 | ) 210 | return $topbarHeight 211 | } 212 | 213 | export function createToc(options: { 214 | article: HTMLElement 215 | preference: TocPreference 216 | }) { 217 | const article = options.article 218 | const scroller = getScrollElement(article) 219 | 220 | //-------------- Helpers -------------- 221 | const disposers: (() => void)[] = [] 222 | const addDisposer = (dispose: () => void) => disposers.push(dispose) 223 | const emitter = createEventEmitter() 224 | 225 | //-------------- Triggers -------------- 226 | const $triggerTopbarMeasure = Stream().log( 227 | 'triggerTopbarMeasure', 228 | ) 229 | const $triggerContentChange = Stream(null).log('triggerContentChange') 230 | const $triggerIsShown = Stream().log('triggerIsShown') 231 | const $periodicCheck = Stream.fromInterval(1000 * 60, addDisposer).log( 232 | 'check', 233 | ) 234 | 235 | //-------------- Observables -------------- 236 | const $isShown = $triggerIsShown.unique().log('isShown') 237 | 238 | const $topbarHeight = topbarStream($triggerTopbarMeasure,scroller).log('topbarHeight') 239 | 240 | const $content = contentStream({ 241 | $triggerContentChange, 242 | $periodicCheck, 243 | $isShown, 244 | article, 245 | scroller, 246 | addDisposer, 247 | }).log('content') 248 | 249 | const $activeHeading: Stream = activeHeadingStream({ 250 | scroller, 251 | $isShown, 252 | $content, 253 | $topbarHeight, 254 | addDisposer, 255 | }).log('activeHeading') 256 | 257 | const $offset = Stream( 258 | options.preference.offset, 259 | ).log('offset') 260 | 261 | const $readableMode = Stream.combine( 262 | $isShown.unique(), 263 | $content.map((c) => c.article.height).unique(), 264 | $content.map((c) => c.scroller.rect.height).unique(), 265 | $content.map((c) => c.headings.length).unique(), 266 | ) 267 | .map(([isShown]) => isShown) 268 | .log('readable') 269 | 270 | //-------------- Effects -------------- 271 | const scrollToHeading = (headingIndex: number): Promise => { 272 | return new Promise((resolve, reject) => { 273 | const { headings, scroller } = $content() 274 | const topbarHeight = $topbarHeight() 275 | const heading = headings[headingIndex] 276 | if (heading) { 277 | const dm=document.domain; 278 | const isInoReader = dm.indexOf('inoreader.com')>=0 || dm.indexOf('innoreader.com')>0; 279 | smoothScroll({ 280 | target: heading.dom, 281 | scroller: scroller.dom, 282 | topMargin: (topbarHeight || 0) + ( isInoReader ? 50 : 10), 283 | callback() { 284 | $triggerTopbarMeasure(heading.dom) 285 | resolve() 286 | }, 287 | }) 288 | } else { 289 | resolve() 290 | } 291 | }) 292 | } 293 | 294 | //remove this mode to fix style issue 295 | // $readableMode.subscribe((enableReadableMode) => { 296 | // if (enableReadableMode) { 297 | // enterReadableMode($content(), { topbarHeight: $topbarHeight() }) 298 | // } else { 299 | // leaveReadableMode() 300 | // } 301 | // $triggerContentChange(null) 302 | // }) 303 | 304 | const validate = (content: Content): void => { 305 | const { article, headings, scroller } = content 306 | const isScrollerValid = 307 | document.documentElement === scroller.dom || 308 | document.documentElement.contains(scroller.dom) 309 | const isArticleValid = 310 | scroller.dom === article.dom || scroller.dom.contains(article.dom) 311 | const isHeadingsValid = 312 | headings.length && 313 | article.dom.contains(headings[0].dom) && 314 | article.dom.contains(headings[headings.length - 1].dom) 315 | const isValid = isScrollerValid && isArticleValid && isHeadingsValid 316 | if (!isValid) { 317 | emitter.emit('error', { reason: 'Article Changed' }) 318 | } 319 | } 320 | $content.subscribe(validate) 321 | 322 | let isRememberPos = true; 323 | chrome.storage.local.get({ 324 | isRememberPos: true 325 | }, function (items) { 326 | isRememberPos = items.isRememberPos; 327 | }); 328 | ui.render({ 329 | $isShown, 330 | $article: $content.map((c) => c.article), 331 | $scroller: $content.map((c) => c.scroller), 332 | $headings: $content.map((c) => c.headings), 333 | $offset, 334 | $activeHeading, 335 | $topbarHeight, 336 | onDrag(offset) { 337 | $offset(offset) 338 | if (isRememberPos) { 339 | const data = {}; 340 | data[offsetKey] = offset; 341 | chrome.storage.local.set(data, function () { 342 | // no callback 343 | }); 344 | } 345 | }, 346 | onScrollToHeading: scrollToHeading, 347 | }) 348 | 349 | //-------------- Exposed Commands -------------- 350 | 351 | const next = () => { 352 | const { headings } = $content() 353 | const activeHeading = $activeHeading() 354 | const nextIndex = Math.min(headings.length - 1, activeHeading + 1) 355 | scrollToHeading(nextIndex) 356 | } 357 | const prev = () => { 358 | const activeHeading = $activeHeading() 359 | const prevIndex = Math.max(0, activeHeading - 1) 360 | scrollToHeading(prevIndex) 361 | } 362 | const show = () => { 363 | $triggerIsShown(true) 364 | } 365 | const hide = () => { 366 | $triggerIsShown(false) 367 | } 368 | const toggle = () => { 369 | if ($isShown()) { 370 | hide() 371 | } else { 372 | show() 373 | } 374 | } 375 | const dispose = () => { 376 | hide() 377 | disposers.forEach((dispose) => dispose()) 378 | emitter.removeAllListeners() 379 | } 380 | const getPreference = (): TocPreference => { 381 | return { 382 | offset: $offset(), 383 | } 384 | } 385 | 386 | return { 387 | ...emitter, 388 | show, 389 | hide, 390 | toggle, 391 | prev, 392 | next, 393 | getPreference, 394 | dispose, 395 | } 396 | } 397 | 398 | export type Toc = ReturnType 399 | -------------------------------------------------------------------------------- /src/content/types.ts: -------------------------------------------------------------------------------- 1 | export type Rect = { 2 | top: number 3 | left: number 4 | right: number 5 | bottom: number 6 | height: number 7 | width: number 8 | } 9 | 10 | export interface Article { 11 | dom: HTMLElement 12 | fromScrollerTop: number 13 | left: number 14 | right: number 15 | height: number 16 | } 17 | 18 | export interface Scroller { 19 | dom: HTMLElement 20 | rect: Rect 21 | } 22 | 23 | export interface Heading { 24 | dom: HTMLElement 25 | level: number 26 | text: string 27 | id: number 28 | anchor?: string 29 | fromArticleTop?: number 30 | } 31 | 32 | export interface Content { 33 | article: Article 34 | scroller: Scroller 35 | headings: Heading[] 36 | } 37 | 38 | export enum Theme { 39 | Light = 'light', 40 | Dark = 'dark', 41 | } 42 | 43 | export interface Offset { 44 | x: number 45 | y: number 46 | } 47 | 48 | declare global { 49 | interface Window { 50 | MutationObserver:any; 51 | WebKitMutationObserver:any; 52 | } 53 | } -------------------------------------------------------------------------------- /src/content/ui/handle.ts: -------------------------------------------------------------------------------- 1 | import m from 'mithril' 2 | import { Offset } from '../types' 3 | 4 | type MithrilEvent = { 5 | redraw?: boolean 6 | } 7 | 8 | const stop = (e: Event) => { 9 | e.stopPropagation() 10 | e.preventDefault() 11 | } 12 | 13 | export interface HandleAttrs { 14 | userOffset: Offset 15 | onDrag(userOffset: Offset): void 16 | } 17 | export const Handle: m.FactoryComponent = (vnode) => { 18 | let dragStart: Offset = { x: 0, y: 0 } 19 | let offset = { x: 0, y: 0 } 20 | 21 | const onDrag = (e: MouseEvent & MithrilEvent) => { 22 | stop(e) 23 | const [dX, dY] = [e.clientX - dragStart.x, e.clientY - dragStart.y] 24 | vnode.attrs.onDrag({ x: offset.x + dX, y: offset.y + dY }) 25 | } 26 | 27 | const onDragEnd = (e: MouseEvent & MithrilEvent) => { 28 | window.removeEventListener('mousemove', onDrag) 29 | window.removeEventListener('mouseup', onDragEnd) 30 | } 31 | 32 | const onDragStart = ( 33 | e: MouseEvent & MithrilEvent, 34 | vnode: m.Vnode, 35 | ) => { 36 | if (e.button === 0) { 37 | stop(e) 38 | dragStart = { 39 | x: e.clientX, 40 | y: e.clientY, 41 | } 42 | offset = { 43 | ...vnode.attrs.userOffset, 44 | } 45 | window.addEventListener('mousemove', onDrag) 46 | window.addEventListener('mouseup', onDragEnd) 47 | m.redraw() 48 | } 49 | } 50 | 51 | return { 52 | view(vnode) { 53 | return m( 54 | '.handle', 55 | { 56 | onmousedown(e) { 57 | onDragStart(e, vnode) 58 | }, 59 | }, 60 | 'table of contents', 61 | ) 62 | }, 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/content/ui/index.ts: -------------------------------------------------------------------------------- 1 | import m from 'mithril' 2 | import tocCSS from '../style/toc.css' 3 | import { Article, Heading, Offset, Scroller } from '../types' 4 | import { addCSS } from '../util/dom/css' 5 | import { Stream } from '../util/stream' 6 | import { Handle } from './handle' 7 | import { TocContent } from './toc_content' 8 | 9 | const ROOT_ID = 'smarttoc-wrapper' 10 | const CSS_ID = 'smarttoc__css' 11 | 12 | const calcPlacement = function (article: Article): 'left' | 'right' { 13 | const { left, right } = article 14 | const winWidth = window.innerWidth 15 | const panelMinWidth = 250 16 | const spaceRight = winWidth - right 17 | const spaceLeft = left 18 | const gap = 80 19 | return spaceRight < panelMinWidth + gap && spaceLeft > panelMinWidth + gap 20 | ? 'left' 21 | : 'right' 22 | } 23 | 24 | const calcStyle = function (options: { 25 | article: Article 26 | scroller: Scroller 27 | offset: Offset 28 | topMargin: number 29 | placement: 'left' | 'right' 30 | }): Partial { 31 | const { article, scroller, offset, topMargin } = options 32 | 33 | //-------------- x -------------- 34 | const { left, right } = article 35 | const winWidth = window.innerWidth 36 | const winHeight = window.innerHeight 37 | const panelMinWidth = 250 38 | const gap = 30 39 | 40 | // just make it in right bottom corner 41 | const x = winWidth - gap - panelMinWidth 42 | // const x = 43 | // options.placement === 'left' 44 | // ? Math.max(0, left - gap - panelMinWidth) // place at left 45 | // : Math.min(right + gap, winWidth - panelMinWidth) // place at right 46 | 47 | //-------------- y -------------- 48 | const scrollableTop = scroller.dom === document.body ? 0 : scroller.rect.top 49 | 50 | // just make it in right bottom corner 51 | const y = winHeight - gap - scrollableTop 52 | // const y = Math.max(scrollableTop, topMargin) + 50 53 | 54 | // const style = { 55 | // left: `${x + offset.x}px`, 56 | // top: `${y + offset.y}px`, 57 | // maxHeight: `calc(100vh - ${Math.max(scrollableTop, topMargin)}px - 50px)`, 58 | // } 59 | 60 | // just make it in right bottom corner 61 | const style = { 62 | right: `${gap - offset.x}px`, 63 | bottom: `${gap - offset.y}px`, 64 | // maxHeight: `250px`, 65 | } 66 | 67 | return style 68 | } 69 | 70 | export const ui = { 71 | render: (options: { 72 | $isShown: Stream 73 | $offset: Stream<{ x: number; y: number }> 74 | $article: Stream
75 | $scroller: Stream 76 | $headings: Stream 77 | $activeHeading: Stream 78 | $topbarHeight: Stream 79 | onDrag(offset: Offset): void 80 | onScrollToHeading(index: number): Promise 81 | }) => { 82 | const { 83 | $isShown, 84 | $offset, 85 | $article, 86 | $scroller, 87 | $headings, 88 | $activeHeading, 89 | $topbarHeight, 90 | onDrag, 91 | onScrollToHeading, 92 | } = options 93 | 94 | const $redraw = Stream.merge( 95 | $isShown, 96 | $offset, 97 | $article, 98 | $scroller, 99 | $headings, 100 | $activeHeading, 101 | $topbarHeight, 102 | ).log('redraw') 103 | 104 | let root = document.getElementById(ROOT_ID) 105 | if (!root) { 106 | root = document.body.appendChild(document.createElement('DIV')) 107 | root.id = ROOT_ID 108 | } 109 | addCSS(tocCSS, CSS_ID) 110 | 111 | const isTooManyHeadings = () => 112 | $headings().filter((h) => h.level <= 2).length > 50 113 | 114 | let initialPlacement: 'left' | 'right' 115 | 116 | m.mount(root, { 117 | view() { 118 | if ( 119 | !$isShown() || 120 | !$article() || 121 | !$scroller() || 122 | !($headings() && $headings().length) 123 | ) { 124 | return null 125 | } 126 | if (!initialPlacement) { 127 | // initialPlacement = calcPlacement($article()) 128 | // set default placement to right 129 | initialPlacement = 'right' 130 | } 131 | 132 | return m( 133 | 'nav#smarttoc', 134 | { 135 | class: isTooManyHeadings() ? 'lengthy' : '', 136 | style: calcStyle({ 137 | article: $article(), 138 | scroller: $scroller(), 139 | offset: $offset(), 140 | topMargin: $topbarHeight() || 0, 141 | placement: initialPlacement, 142 | }), 143 | }, 144 | [ 145 | m(Handle, { 146 | userOffset: $offset(), 147 | onDrag, 148 | }), 149 | m(TocContent, { 150 | article: $article(), 151 | headings: $headings(), 152 | activeHeading: $activeHeading(), 153 | onScrollToHeading, 154 | }), 155 | ], 156 | ) 157 | }, 158 | }) 159 | 160 | $redraw.subscribe(() => m.redraw()) 161 | }, 162 | 163 | dispose: () => { 164 | const root = document.getElementById(ROOT_ID) 165 | if (root) { 166 | m.mount(root, null) 167 | root.remove() 168 | } 169 | }, 170 | } 171 | -------------------------------------------------------------------------------- /src/content/ui/toc_content.ts: -------------------------------------------------------------------------------- 1 | import m from 'mithril' 2 | import { Heading, Article } from '../types' 3 | import { smoothScroll } from '../lib/scroll' 4 | import { toArray } from '../util/dom/to_array' 5 | 6 | type MithrilEvent = { 7 | redraw: boolean 8 | } 9 | 10 | type HeadingNode = { 11 | level?: number 12 | heading?: Heading | undefined 13 | isActive?: boolean 14 | children: HeadingNode[] 15 | } 16 | 17 | const toTree = (headings: Heading[], activeHeading: number): HeadingNode => { 18 | const tree: HeadingNode = { level: 0, children: [] } 19 | const stack = [tree] 20 | const stackTop = () => stack.slice(-1)[0] 21 | 22 | let i = 0 23 | while (i < headings.length) { 24 | const { level } = headings[i] 25 | if (level === stack.length) { 26 | // is direct children 27 | const node: HeadingNode = { 28 | heading: { ...headings[i] }, 29 | children: [], 30 | } 31 | stackTop().children.push(node) 32 | stack.push(node) 33 | if (i === activeHeading) { 34 | stack.forEach((node) => { 35 | node.isActive = true 36 | }) 37 | } 38 | i++ 39 | } else if (level < stack.length) { 40 | // is sibiling/parent 41 | stack.pop() 42 | } else if (level > stack.length) { 43 | // is grand child 44 | const node: HeadingNode = { 45 | heading: undefined, 46 | children: [], 47 | } 48 | stackTop().children.push(node) 49 | stack.push(node) 50 | } 51 | } 52 | return tree 53 | } 54 | 55 | const restrictScroll = function(e: WheelEvent & MithrilEvent) { 56 | const toc = e.currentTarget as HTMLElement 57 | const maxScroll = toc.scrollHeight - toc.offsetHeight 58 | if (toc.scrollTop + e.deltaY < 0) { 59 | toc.scrollTop = 0 60 | e.preventDefault() 61 | } else if (toc.scrollTop + e.deltaY > maxScroll) { 62 | toc.scrollTop = maxScroll 63 | e.preventDefault() 64 | } 65 | e.redraw = false 66 | } 67 | 68 | export const TocContent: m.FactoryComponent<{ 69 | article: Article 70 | headings: Heading[] 71 | activeHeading: number 72 | onScrollToHeading(index: number): Promise 73 | }> = () => { 74 | let isScrollingToHeading = false 75 | 76 | const revealActiveHeading = ( 77 | tocPanelDom: HTMLElement, 78 | headingDom: HTMLElement, 79 | ) => { 80 | const panelRect = tocPanelDom.getBoundingClientRect() 81 | const headingRect = headingDom.getBoundingClientRect() 82 | const isOutOfView = 83 | headingRect.top > panelRect.bottom || headingRect.bottom < panelRect.top 84 | if (isOutOfView) { 85 | const halfPanelHeight = 86 | tocPanelDom.offsetHeight / 2 - headingDom.offsetHeight / 2 87 | smoothScroll({ 88 | target: headingDom, 89 | scroller: tocPanelDom as HTMLElement, 90 | topMargin: halfPanelHeight, 91 | maxDuration: 0, 92 | }) 93 | } 94 | } 95 | 96 | return { 97 | onupdate(vnode) { 98 | if (isScrollingToHeading) { 99 | return 100 | } 101 | const activeHeadings = toArray(vnode.dom.querySelectorAll('.active')) 102 | const activeHeading = activeHeadings[activeHeadings.length - 1] // could have several '.active' headings (for multiple levels) 103 | if (activeHeading) { 104 | revealActiveHeading( 105 | vnode.dom as HTMLElement, 106 | activeHeading as HTMLElement, 107 | ) 108 | } 109 | }, 110 | view(vnode) { 111 | const { headings, activeHeading, onScrollToHeading } = vnode.attrs 112 | const tree = toTree(headings, activeHeading) 113 | 114 | const HeadingList = (nodes: HeadingNode[], { isRoot = false } = {}) => 115 | m( 116 | 'ul', 117 | { 118 | onwheel: isRoot && restrictScroll, 119 | onclick: 120 | isRoot && 121 | (async (e: MouseEvent) => { 122 | e.preventDefault() 123 | e.stopPropagation() 124 | const index = Number((e.target as HTMLElement).dataset.index) 125 | if (!Number.isNaN(index)) { 126 | isScrollingToHeading = true 127 | await onScrollToHeading(index) 128 | isScrollingToHeading = false 129 | } 130 | }), 131 | }, 132 | nodes.map(HeadingItem), 133 | ) 134 | 135 | const HeadingItem = ( 136 | { heading, children, isActive }: HeadingNode, 137 | index: number, 138 | ) => 139 | m( 140 | 'li', 141 | { class: isActive ? 'active' : '', key: index }, 142 | [ 143 | heading && 144 | m( 145 | 'a', 146 | { 147 | ...(heading.anchor ? { href: `#${heading.anchor}` } : {}), 148 | [`data-index`]: heading.id, 149 | }, 150 | heading.text, 151 | ), 152 | children && children.length && HeadingList(children), 153 | ].filter(Boolean), 154 | ) 155 | 156 | return HeadingList(tree.children, { isRoot: true }) 157 | }, 158 | } 159 | } 160 | -------------------------------------------------------------------------------- /src/content/util/assert.ts: -------------------------------------------------------------------------------- 1 | export function assert(condition: any, message: string): asserts condition { 2 | if (!condition) { 3 | throw new Error(message) 4 | } 5 | } 6 | 7 | export const assertNever = (o: never): never => { 8 | throw new TypeError('Unexpected type:' + JSON.stringify(o)) 9 | } 10 | -------------------------------------------------------------------------------- /src/content/util/debug.ts: -------------------------------------------------------------------------------- 1 | export function draw( 2 | elem: HTMLElement | HTMLElement[] | null | undefined, 3 | color = 'red', 4 | ): void { 5 | if (elem) { 6 | if (Array.isArray(elem)) { 7 | elem.forEach((el) => { 8 | el.style.outline = '2px solid ' + color 9 | }) 10 | } else { 11 | elem.style.outline = '2px solid ' + color 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/content/util/decorator.ts: -------------------------------------------------------------------------------- 1 | const isPromise = (o: any): o is Promise => { 2 | return typeof (o && o.then) === 'function' 3 | } 4 | 5 | export type AnyFunction = (...args: any[]) => any 6 | export type Decorator = (fn: T) => T 7 | 8 | export interface DecoratorOptions { 9 | onCalled?(params: Parameters, fnName: string): void 10 | onReturned?( 11 | result: ReturnType, 12 | params: Parameters, 13 | fnName: string, 14 | ): void 15 | onError?(error: any, params: Parameters, fnName: string): void 16 | fnName?: string 17 | self?: any 18 | } 19 | 20 | /** 创建一个函数装饰器 */ 21 | export function createDecorator({ 22 | onCalled, 23 | onReturned, 24 | onError, 25 | fnName, 26 | self = null, 27 | }: DecoratorOptions): Decorator { 28 | const decorator = (fn) => { 29 | const decoratedFunction = ((...params: Parameters): ReturnType => { 30 | if (onCalled) { 31 | onCalled(params, fnName || fn.name) 32 | } 33 | 34 | try { 35 | const result = fn.apply(self, params) as ReturnType | ReturnType 36 | if (isPromise(result)) { 37 | return result.then( 38 | (result: ReturnType) => { 39 | if (onReturned) { 40 | onReturned(result, params, fnName || fn.name) 41 | } 42 | return result 43 | }, 44 | (error: any) => { 45 | if (onError) { 46 | onError(error, params, fnName || fn.name) 47 | } 48 | throw error 49 | }, 50 | ) 51 | } else { 52 | if (onReturned) { 53 | onReturned(result, params, fnName || fn.name) 54 | } 55 | return result 56 | } 57 | } catch (error) { 58 | if (onError) { 59 | onError(error, params, fnName || fn.name) 60 | } 61 | throw error 62 | } 63 | }) as T 64 | 65 | return decoratedFunction 66 | } 67 | return decorator 68 | } 69 | 70 | /** 装饰一个对象,对象的所有方法都会被装饰 */ 71 | export function decorateObject( 72 | object: T, 73 | options: DecoratorOptions, 74 | ): T { 75 | for (const key in object) { 76 | const fn = object[key] 77 | if (key !== 'constructor' && typeof fn == 'function') { 78 | const decorator = createDecorator({ 79 | ...options, 80 | self: object, 81 | fnName: key, 82 | }) 83 | object[key] = decorator(fn as any) as any 84 | } 85 | } 86 | return object 87 | } 88 | -------------------------------------------------------------------------------- /src/content/util/dom/css.ts: -------------------------------------------------------------------------------- 1 | import { px } from './px' 2 | 3 | export function applyStyle(elem: HTMLElement, style = {}, reset = false): void { 4 | function toDash(str: string): string { 5 | return str.replace(/([A-Z])/g, (match, p1) => '-' + p1.toLowerCase()) 6 | } 7 | if (reset) { 8 | // @ts-ignore 9 | elem.style = '' 10 | } 11 | if (typeof style === 'string') { 12 | // @ts-ignore 13 | elem.style = style 14 | } else { 15 | for (let prop in style) { 16 | if (typeof style[prop] === 'number') { 17 | elem.style.setProperty(toDash(prop), px(style[prop]), 'important') 18 | } else { 19 | elem.style.setProperty(toDash(prop), style[prop], 'important') 20 | } 21 | } 22 | } 23 | } 24 | 25 | export const addCSS = function(css: string, id: string): () => void { 26 | let style: HTMLStyleElement 27 | if (!document.getElementById(id)) { 28 | style = document.createElement('style') 29 | style.type = 'text/css' 30 | style.id = id 31 | style.textContent = css 32 | document.head.appendChild(style) 33 | } 34 | const removeCSS = () => { 35 | style.remove() 36 | } 37 | return removeCSS 38 | } 39 | -------------------------------------------------------------------------------- /src/content/util/dom/depth.ts: -------------------------------------------------------------------------------- 1 | export type Point = [number, number] 2 | 3 | export const depthOf = function(elem: HTMLElement | null): number | undefined { 4 | if (!elem) return undefined 5 | 6 | let depth = 0 7 | while (elem) { 8 | elem = elem.parentElement 9 | depth++ 10 | } 11 | return depth 12 | } 13 | 14 | export const depthOfPoint = function([x, y]: Point): number | undefined { 15 | const elem = document.elementFromPoint(x, y) 16 | return elem ? depthOf(elem as HTMLElement) : undefined 17 | } 18 | -------------------------------------------------------------------------------- /src/content/util/dom/px.ts: -------------------------------------------------------------------------------- 1 | // '12px' => 12 2 | export const num = (size: string | number | null = '0'): number => { 3 | if (!size) return NaN 4 | if (typeof size === 'number') return size 5 | if (/px$/.test(size)) { 6 | return +size.replace(/px/, '') 7 | } 8 | return NaN 9 | } 10 | 11 | // 12 => '12px' 12 | export const px = (size: string | number = 0): string => { 13 | return num(size) + 'px' 14 | } 15 | -------------------------------------------------------------------------------- /src/content/util/dom/to_array.ts: -------------------------------------------------------------------------------- 1 | export const toArray = ( 2 | arr: NodeListOf | HTMLCollectionOf, 3 | ): T[] => { 4 | return [].slice.apply(arr) 5 | } 6 | -------------------------------------------------------------------------------- /src/content/util/env.ts: -------------------------------------------------------------------------------- 1 | export const isDebugging = /dev/.test(process.env.ENV || '') 2 | export const offsetKey = "smarttoc_offset" -------------------------------------------------------------------------------- /src/content/util/event.ts: -------------------------------------------------------------------------------- 1 | export const createEventEmitter = < 2 | EventMap extends { [K: string]: any } = { [K: string]: any } 3 | >() => { 4 | type Keys = keyof EventMap 5 | type KeysPayloadRequired = { 6 | [K in Keys]: EventMap[K] extends undefined ? never : K 7 | }[Keys] 8 | 9 | type KeysPayloadOptional = Exclude 10 | 11 | type Handler = (payload: EventMap[K]) => void 12 | 13 | let listeners = {} as { [K in Keys]: Handler[] } 14 | 15 | const on = (event: K, handler: Handler) => { 16 | if (!listeners[event]) { 17 | listeners[event] = [] 18 | } 19 | if (!listeners[event].includes(handler)) { 20 | listeners[event].push(handler) 21 | } 22 | } 23 | 24 | const off = (event: K, handler: Handler) => { 25 | if (listeners[event]) { 26 | listeners[event] = listeners[event].filter((h) => h !== handler) 27 | } 28 | } 29 | 30 | function emit( 31 | event: K, 32 | payload?: EventMap[K], 33 | ): void 34 | function emit( 35 | event: K, 36 | payload: EventMap[K], 37 | ): void 38 | function emit(event: K, payload?: EventMap[K]): void { 39 | if (listeners[event]) { 40 | listeners[event].forEach((handler) => { 41 | handler(payload!) 42 | }) 43 | } 44 | } 45 | 46 | const removeAllListeners = () => { 47 | listeners = {} as { [K in Keys]: Handler[] } 48 | } 49 | 50 | return { on, off, emit, removeAllListeners } 51 | } 52 | -------------------------------------------------------------------------------- /src/content/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { 2 | createDecorator, 3 | AnyFunction, 4 | decorateObject, 5 | Decorator, 6 | } from './decorator' 7 | 8 | export const createLogger = (name: string) => (...args: any[]) => { 9 | console.info(`[${name}]`, ...args) 10 | } 11 | 12 | /** 13 | * 记录函数调用和返回 14 | * 15 | * 创建一个装饰器,装饰后的 fn 会记录调用参数和返回结果 16 | */ 17 | export const createLoggingDecorator = ( 18 | name: string, 19 | self: any = null, 20 | ): Decorator => { 21 | const logger = createLogger(name) 22 | const decorator = createDecorator({ 23 | onCalled(params) { 24 | // @ts-ignore 25 | logger(...params) 26 | }, 27 | onReturned(result) { 28 | logger(result) 29 | }, 30 | onError(error) { 31 | logger(` `) 32 | console.error(error) 33 | }, 34 | }) 35 | return decorator 36 | } 37 | 38 | /** 39 | * 记录对象各个方法的调用情况 40 | * 并添加到 window.logged.*上 41 | */ 42 | export const logObject = (object: T, namespace: string): T => { 43 | const loggers = {} as any 44 | const getLogger = (fnName: string) => { 45 | if (!loggers[fnName]) { 46 | loggers[fnName] = createLogger(`${namespace}:${fnName}`) 47 | } 48 | return loggers[fnName] 49 | } 50 | const loggedObject = decorateObject(object, { 51 | onCalled(params, fnName) { 52 | getLogger(fnName)(...params) 53 | }, 54 | onReturned(result, params, fnName) { 55 | getLogger(fnName)(result) 56 | }, 57 | onError(error, params, fnName) { 58 | getLogger(fnName)(` `) 59 | console.error(error) 60 | }, 61 | }) 62 | 63 | // @ts-ignore 64 | if (typeof window !== 'undefined') { 65 | // @ts-ignore 66 | const w = window as any 67 | if (!w.logged) { 68 | w.logged = {} 69 | } 70 | w.logged[namespace] = object 71 | } 72 | return loggedObject 73 | } 74 | -------------------------------------------------------------------------------- /src/content/util/math/between.ts: -------------------------------------------------------------------------------- 1 | export const between = (min: number, value: number, max: number): number => { 2 | return Math.max(min, Math.min(max, value)) 3 | } 4 | -------------------------------------------------------------------------------- /src/content/util/stream.ts: -------------------------------------------------------------------------------- 1 | import { assert } from './assert' 2 | import { isDebugging } from './env' 3 | import { createLogger } from './logger' 4 | 5 | /** valid values */ 6 | type VV = number | boolean | string | null | object 7 | 8 | interface StreamListener { 9 | (value: T): void 10 | } 11 | 12 | interface StreamDependent { 13 | updateDependent(val: T): void 14 | flushDependent(): void 15 | } 16 | 17 | // dirty workaround as typescript does not support callable class for now 18 | interface StreamCallable { 19 | (val: T | undefined): void 20 | (): T 21 | } 22 | 23 | type StreamTuple = { 24 | // here tsc complains T[K] does not extends VV, but we know it should 25 | // - changing to `T[K] extends VV ? Stream : never` fixes this error, but introduces further problems 26 | // @ts-ignore 27 | [K in keyof T]: Stream 28 | } 29 | 30 | let nextId = 0 31 | 32 | class StreamClass { 33 | public displayName?: string 34 | private listeners: StreamListener[] = [] 35 | private dependents: StreamDependent[] = [] 36 | private value: T | undefined = undefined 37 | private changed: boolean = false 38 | private constructor(value: T | undefined, name?: string) { 39 | this.displayName = name || `s_${nextId++}` 40 | this.value = value 41 | } 42 | 43 | static isStream(o: any): o is Stream { 44 | return o && typeof o.subscribe === 'function' 45 | } 46 | 47 | static create(init?: T | undefined, name?: string): Stream { 48 | const instance = new StreamClass(init, name) 49 | 50 | const callable = function (val) { 51 | if (arguments.length === 0) { 52 | return stream$.value 53 | } else { 54 | assert( 55 | typeof val !== 'undefined', 56 | 'sending `undefined` to a stream is not allowed', 57 | ) 58 | stream$.update(val) 59 | stream$.flush() 60 | } 61 | } as StreamCallable 62 | 63 | const stream$ = Object.assign(callable, instance) 64 | 65 | Object.setPrototypeOf(stream$, StreamClass.prototype) 66 | 67 | return stream$ 68 | } 69 | 70 | static combine(...streams: [...StreamTuple]): Stream { 71 | const cached = streams.map((stream$) => stream$()) as T // could be undefined thought 72 | const allHasValue = () => 73 | cached.every((elem) => typeof elem !== 'undefined') 74 | 75 | const combined$ = Stream( 76 | allHasValue() ? cached : undefined, 77 | `combine(${streams.map((s) => s.displayName).join(',')})`, 78 | ) 79 | 80 | streams.forEach((stream, i) => { 81 | stream.dependents.push({ 82 | updateDependent(val: any) { 83 | cached[i] = val 84 | if (allHasValue()) { 85 | combined$.update(cached) 86 | } 87 | }, 88 | flushDependent() { 89 | combined$.flush() 90 | }, 91 | }) 92 | }) 93 | return combined$ 94 | } 95 | 96 | static merge( 97 | ...streams: [...StreamTuple] 98 | ): Stream { 99 | const merged$ = Stream( 100 | undefined, 101 | `merge(${streams.map((s) => s.displayName).join(',')})`, 102 | ) 103 | streams.forEach((stream$) => { 104 | stream$.subscribe((val) => merged$(val)) 105 | }) 106 | return merged$ 107 | } 108 | 109 | static flatten( 110 | highOrderStream: Stream>, 111 | ): Stream { 112 | const $flattened = Stream( 113 | undefined, 114 | `flatten(${highOrderStream.displayName})`, 115 | ) 116 | 117 | highOrderStream.unique().subscribe((stream) => { 118 | stream.subscribe((value) => { 119 | $flattened(value) 120 | }) 121 | }) 122 | 123 | return $flattened 124 | } 125 | 126 | static fromInterval( 127 | interval: number, 128 | callback?: (unsubscribe: () => void) => void, 129 | ) { 130 | const interval$ = Stream(undefined, `interval(${interval})`) 131 | const timer = setInterval(() => interval$(null), interval) 132 | if (callback) { 133 | callback(() => clearInterval(timer)) 134 | } 135 | return interval$ 136 | } 137 | 138 | static fromEvent( 139 | elem: { 140 | addEventListener( 141 | type: string, 142 | listener: (payload: T | undefined) => void, 143 | ): void 144 | removeEventListener( 145 | type: string, 146 | listener: (payload: T | undefined) => void, 147 | ): void 148 | }, 149 | type: string, 150 | callback?: (unsubscribe: () => void) => void, 151 | ): Stream { 152 | const event$ = Stream(undefined, `event(${type})`) 153 | 154 | const listener = (payload: T | undefined) => { 155 | event$(payload == null ? (null as T) : payload) 156 | } 157 | elem.addEventListener(type, listener) 158 | 159 | if (callback) { 160 | callback(() => elem.removeEventListener(type, listener)) 161 | } 162 | 163 | return event$ 164 | } 165 | 166 | startsWith(value: S): Stream { 167 | const start$ = Stream(value) 168 | return Stream.merge(start$, this.asStream()) 169 | } 170 | 171 | subscribe(listener: StreamListener, emitCurrent = true): () => void { 172 | if (emitCurrent && this.value !== undefined) { 173 | listener(this.value as T) 174 | } 175 | this.listeners.push(listener) 176 | 177 | return () => { 178 | this.unsubscribe(listener) 179 | } 180 | } 181 | 182 | unsubscribe(listener: StreamListener): void { 183 | this.listeners = this.listeners.filter((l) => l !== listener) 184 | } 185 | 186 | map(mapper: (val: T) => V): Stream { 187 | const $mapped = Stream( 188 | typeof this.value === 'undefined' ? undefined : mapper(this.value), 189 | `map(${this.displayName})`, 190 | ) 191 | this.subscribe((val) => { 192 | $mapped(mapper(val)) 193 | }) 194 | return $mapped 195 | } 196 | 197 | unique(): Stream { 198 | const unique$ = Stream(this.value, `unique(${this.displayName})`) 199 | this.subscribe((val) => { 200 | if (val !== unique$()) { 201 | unique$(val) 202 | } 203 | }) 204 | return unique$ 205 | } 206 | 207 | flatten(this: Stream>): Stream { 208 | const flattened$ = Stream() 209 | this.subscribe((childStream) => { 210 | childStream.subscribe((innerValue) => { 211 | flattened$(innerValue) 212 | }) 213 | }) 214 | 215 | return flattened$ 216 | } 217 | 218 | scan( 219 | this: Stream, 220 | reducer: (last: V, current: T) => V, 221 | initValue: V, 222 | ): Stream { 223 | const scanned$ = Stream(initValue) 224 | this.subscribe((current) => { 225 | scanned$(reducer(scanned$(), current)) 226 | }) 227 | return scanned$ 228 | } 229 | 230 | switchLatest(this: Stream>): Stream { 231 | const latest$ = Stream() 232 | let unsubscribeLast = () => {} 233 | 234 | this.subscribe((childStream) => { 235 | unsubscribeLast() 236 | unsubscribeLast = childStream.subscribe((innerValue) => { 237 | latest$(innerValue) 238 | }) 239 | }) 240 | return latest$ 241 | } 242 | 243 | filter(predict: (val: T) => boolean): Stream { 244 | const filtered$ = Stream(undefined, `filter(${this.displayName})`) 245 | this.subscribe((val) => { 246 | if (predict(val)) { 247 | filtered$(val as V) 248 | } 249 | }) 250 | return filtered$ 251 | } 252 | 253 | delay(delayInMs: number): Stream { 254 | const delayed$ = Stream(undefined, `delay(${this.displayName})`) 255 | this.subscribe((value) => { 256 | setTimeout(() => { 257 | delayed$(value) 258 | }, delayInMs) 259 | }) 260 | return delayed$ 261 | } 262 | 263 | debounce(delay: number): Stream { 264 | const debounced$ = Stream(undefined, `debounce(${this.displayName})`) 265 | 266 | let timer: number 267 | this.subscribe((val) => { 268 | clearTimeout(timer) 269 | timer = window.setTimeout(function () { 270 | debounced$(val) 271 | }, delay) 272 | }) 273 | 274 | return debounced$ 275 | } 276 | 277 | throttle(delay: number): Stream { 278 | const throttled$ = Stream(undefined, `throttle(${this.displayName})`) 279 | 280 | let lastEmit: number = -Infinity 281 | let timer: number 282 | const emit = (val: T) => { 283 | throttled$(val) 284 | lastEmit = performance.now() 285 | } 286 | 287 | this.subscribe((val) => { 288 | window.clearTimeout(timer) 289 | const timePassed = performance.now() - lastEmit 290 | if (timePassed < delay) { 291 | timer = window.setTimeout(() => { 292 | emit(val) 293 | }, delay - timePassed) 294 | } else { 295 | emit(val) 296 | } 297 | }) 298 | 299 | return throttled$ 300 | } 301 | 302 | log(name: string): this { 303 | this.displayName = name 304 | if (isDebugging) { 305 | const logger = createLogger('$' + name) 306 | this.subscribe(logger) 307 | } 308 | return this 309 | } 310 | 311 | setName(name: string): this { 312 | this.displayName = name 313 | return this 314 | } 315 | 316 | private update(val: T) { 317 | if (val === undefined) { 318 | if (isDebugging) { 319 | debugger 320 | } 321 | throw new TypeError(`update() received undefined`) 322 | } 323 | this.value = val 324 | this.changed = true 325 | this.dependents.forEach((dep) => dep.updateDependent(val)) 326 | } 327 | 328 | private flush() { 329 | if (this.changed) { 330 | this.changed = false 331 | this.listeners.forEach((l) => l(this.value!)) 332 | this.dependents.forEach((dep) => dep.flushDependent()) 333 | } 334 | } 335 | 336 | private asStream(): Stream { 337 | assert(typeof this === 'function', 'not callable Stream') 338 | return this as unknown as Stream 339 | } 340 | } 341 | 342 | export type Stream = StreamClass & StreamCallable 343 | 344 | export const Stream = Object.assign( 345 | StreamClass.create, 346 | 347 | // provides type 348 | StreamClass, 349 | 350 | // actually provides static methods 351 | Object.fromEntries( 352 | Object.getOwnPropertyNames(StreamClass) 353 | .map((key) => [key, StreamClass[key]]) 354 | .filter((t) => typeof t[1] === 'function'), 355 | ) as {}, 356 | ) 357 | -------------------------------------------------------------------------------- /src/content/util/toast.ts: -------------------------------------------------------------------------------- 1 | import toastCSS from '../style/toast.css' 2 | import { addCSS } from './dom/css' 3 | 4 | function setClass( 5 | elem: HTMLElement, 6 | names: string, 7 | delay?: number, 8 | ): number | undefined { 9 | if (delay == null) { 10 | elem.className = names 11 | } else { 12 | return window.setTimeout(() => { 13 | elem.className = names 14 | }, delay) 15 | } 16 | } 17 | 18 | let timers: (number | undefined)[] = [] 19 | export function showToast(msg: string) { 20 | addCSS(toastCSS, 'smarttoc-toast__css') 21 | const set = (classNames: string, delay?: number) => { 22 | return setClass(toast!, classNames, delay) 23 | } 24 | let toast = document.getElementById('smarttoc-toast') as HTMLElement | null 25 | if (!toast) { 26 | toast = document.createElement('DIV') 27 | toast.id = 'smarttoc-toast' 28 | document.body.appendChild(toast) 29 | } 30 | toast.textContent = msg 31 | 32 | timers.forEach(window.clearTimeout) 33 | 34 | set('') 35 | set('enter') 36 | timers = [ 37 | set('enter enter-active', 0), 38 | set('leave', 3000), 39 | set('leave leave-active', 3000), 40 | set('', 3000 + 200), 41 | ] 42 | } 43 | -------------------------------------------------------------------------------- /src/types/modules.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.css' 2 | -------------------------------------------------------------------------------- /test/list.md: -------------------------------------------------------------------------------- 1 | https://ponyfoo.com/articles/understanding-javascript-async-await 2 | section.md-markdown.at-body 3 | 4 | http://zh.wikipedia.org/wiki/%E8%A3%9D%E7%94%B2%E6%83%A1%E9%AC%BC%E6%9D%91%E6%AD%A3 5 | div#mw-content-text 6 | 7 | https://www.npmjs.com/package/request 8 | div#readme.markdown 9 | 10 | https://diversity.github.com/ 11 | #demographics 12 | 13 | http://mobxjs.github.io/mobx/#introduction 14 | section.normal 15 | 16 | http://paperjs.org/reference/raster/#Remove-On-Event 17 | 18 | http://threejs.org/docs/index.html#Cameras 19 | 20 | http://www.cnblogs.com/nankezhishi/archive/2012/06/09/getandpost.html 21 | #cnblogs_post_body 22 | 23 | http://www.zenspider.com/Languages/Ruby/QuickRef.html -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Basic Options */ 4 | "target": "es6" /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017','ES2018' or 'ESNEXT'. */, 5 | "module": "esnext" /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', or 'ESNext'. */, 6 | "lib": [ 7 | "esnext", 8 | "dom", 9 | "scripthost" 10 | ] /* Specify library files to be included in the compilation. */, 11 | "allowJs": true /* Allow javascript files to be compiled. */, 12 | // "checkJs": true, /* Report errors in .js files. */ 13 | // "jsx": "preserve" /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */, 14 | // "declaration": true /* Generates corresponding '.d.ts' file. */, 15 | // "declarationMap": true /* Generates a sourcemap for each corresponding '.d.ts' file. */, 16 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 17 | // "outFile": "./", /* Concatenate and emit output to single file. */ 18 | "outDir": "./dist" /* Redirect output structure to the directory. */, 19 | // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 20 | // "composite": true, /* Enable project compilation */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | // "strict": true /* Enable all strict type-checking options. */, 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | "strictNullChecks": true /* Enable strict null checks. */, 31 | "strictFunctionTypes": true /* Enable strict checking of function types. */, 32 | "strictPropertyInitialization": true /* Enable strict checking of property initialization in classes. */, 33 | "noImplicitThis": true /* Raise error on 'this' expressions with an implied 'any' type. */, 34 | "alwaysStrict": true /* Parse in strict mode and emit "use strict" for each source file. */, 35 | 36 | /* Additional Checks */ 37 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 38 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 39 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 40 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 41 | 42 | /* Module Resolution Options */ 43 | // "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 44 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 45 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 46 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 47 | // "typeRoots": [], /* List of folders to include type definitions from. */ 48 | // "types": [], /* Type declaration files to be included in compilation. */ 49 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 50 | "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, 51 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 52 | 53 | /* Source Map Options */ 54 | // "sourceRoot": "./", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 55 | // "mapRoot": "./", /* Specify the location where debugger should locate map files instead of generated locations. */ 56 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 57 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 58 | 59 | /* Experimental Options */ 60 | "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */ 61 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 62 | } 63 | } 64 | --------------------------------------------------------------------------------