├── .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 | *
288 | *
289 | *
290 | *
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 | Disable / 禁用
29 |
30 |
31 |
32 |
33 |
34 | All Page / 所有页面
35 |
36 |
37 |
38 |
39 |
40 | Inoreader and Feedly Web App / 网页版 Inoreader 和 Feedly
41 |
42 |
43 |
44 |
UI
45 |
46 |
47 |
48 |
49 | Show Detect Toast / 显示检测提示
50 |
51 |
52 |
53 |
54 | Remember TOC Position / 记住目录位置
55 |
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 |
Reset / 重置设置
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 |
79 |
80 |
81 |
82 |
83 |
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 |
--------------------------------------------------------------------------------