Refactor the entire storage system and the section editor (#518)
* Squashed commit of the following: commitd84c4dc3fe
Author: eight <eight04@gmail.com> Date: Sun Oct 14 19:13:29 2018 +0800 Fix: remove unused comment commit46027120ec
Author: eight <eight04@gmail.com> Date: Sun Oct 14 19:09:06 2018 +0800 Add: handle styleUpdated message commitf85d4de39b
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:59:29 2018 +0800 Fix: handle styleAdded message in popup commit81f3e69574
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:50:54 2018 +0800 Change: getStylesInfoByUrl -> getStylesByUrl commitf9dc04558f
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:48:20 2018 +0800 Fix: drop getStylesInfo commitfea04d591f
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:39:28 2018 +0800 Fix: remove unused ignoreChromeError commit2aff14e213
Author: eight <eight04@gmail.com> Date: Sun Oct 14 18:09:53 2018 +0800 Fix: don't dup promisify in prefs commitd4ddfcc713
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:56:16 2018 +0800 Change: drop .last and .rotate commit85e70491e4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:36:00 2018 +0800 Fix: unused renderIndex commit7acb131642
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:32:49 2018 +0800 Fix: update title on input commita39405ac4c
Author: eight <eight04@gmail.com> Date: Sun Oct 14 17:17:20 2018 +0800 Fix: remove unused messages commit14c2fdbb58
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:36:12 2018 +0800 Fix: dirty state for new added applies commitfb1b49b8bb
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:27:17 2018 +0800 Fix: minor commit2c2d849fa4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:20:14 2018 +0800 Fix: drop unused getCode commitf133c3e67a
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:18:14 2018 +0800 Fix: drop unused lastActive commit05a6208f5c
Author: eight <eight04@gmail.com> Date: Sun Oct 14 16:17:45 2018 +0800 Fix: minor commit05a87ed00f
Author: eight <eight04@gmail.com> Date: Sun Oct 14 15:58:33 2018 +0800 Fix: minor commit576f73f333
Author: eight <eight04@gmail.com> Date: Sun Oct 14 03:03:35 2018 +0800 Fix: always register listeners commite93819deb4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:58:49 2018 +0800 Fix: unused statement commit39b11685b4
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:54:29 2018 +0800 Fix: minor commit9dd3cd43c1
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:49:22 2018 +0800 Fix: don't reorder options commit90aadfd728
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:43:52 2018 +0800 Fix: drop __ERROR__ commit838c21e3b3
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:36:20 2018 +0800 Fix: use findStyle API commit93a4cdf595
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:34:05 2018 +0800 Add: findStyle API commit8e75871b9b
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:19:01 2018 +0800 Breaking: drop getStylesFallback commitad06551440
Author: eight <eight04@gmail.com> Date: Sun Oct 14 02:16:48 2018 +0800 Fix: use dataurl to inject page script commitcb5cbb4d10
Author: eight <eight04@gmail.com> Date: Sun Oct 14 01:39:50 2018 +0800 Fix: various commit53efd78b89
Author: eight <eight04@gmail.com> Date: Sun Oct 14 01:12:57 2018 +0800 Update doc commit7d005f3eaa
Author: eight <eight04@gmail.com> Date: Sun Oct 14 01:09:22 2018 +0800 Change: kill style.reason commitfc53bed3de
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:56:04 2018 +0800 Fix: doo many indents commit14e321d258
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:40:23 2018 +0800 Fix: don't update icon for popup and options commit01bdd529bc
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:39:17 2018 +0800 Fix: updateCount commitb9968830d3
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:38:49 2018 +0800 Fix: don't send null value commitff3bf6f52d
Author: eight <eight04@gmail.com> Date: Sun Oct 14 00:03:34 2018 +0800 Add: styleViaAPI updateCount commit39d21c3d29
Author: eight <eight04@gmail.com> Date: Sat Oct 13 23:57:45 2018 +0800 Fix: broadcastError -> ignoreError commitecb622c93c
Author: eight <eight04@gmail.com> Date: Sat Oct 13 21:29:06 2018 +0800 Fix: implement styleViaAPI commit7c3d49c005
Author: eight <eight04@gmail.com> Date: Sat Oct 13 17:50:28 2018 +0800 Fix: ROOT may change in XML pages commit3fd8d937f3
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:49:43 2018 +0800 Fix: various commit859afc8ee9
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:39:54 2018 +0800 Enhance: don't cache enabled state commitfbe77a8d15
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:15:07 2018 +0800 Fix: various commita4fc3e9162
Author: eight <eight04@gmail.com> Date: Sat Oct 13 16:11:38 2018 +0800 Fix: various commit7e0eddeb8f
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:58:31 2018 +0800 Fix: various commit8b4ab47d89
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:20:10 2018 +0800 Add: some type hint commit7d340d62dc
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:13:11 2018 +0800 Change: drop storage.js, some functions are moved to sections-util commitd286997d6a
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:12:00 2018 +0800 Fix: minor commitd60db9dbef
Author: eight <eight04@gmail.com> Date: Sat Oct 13 15:03:10 2018 +0800 Fix: minor commit43afa31fa0
Author: eight <eight04@gmail.com> Date: Sat Oct 13 14:50:31 2018 +0800 Fix: update tab icon on forward/backward commitf08faea149
Author: eight <eight04@gmail.com> Date: Sat Oct 13 13:50:03 2018 +0800 Fix: parallel import commit4d06435486
Author: eight <eight04@gmail.com> Date: Fri Oct 12 23:32:03 2018 +0800 Add: importStyle API commitc55675912e
Author: eight <eight04@gmail.com> Date: Fri Oct 12 23:14:46 2018 +0800 Fix: refactor import-export commit86ea846a89
Author: eight <eight04@gmail.com> Date: Fri Oct 12 17:34:36 2018 +0800 Fix: search db is broken commit831ca07c2d
Author: eight <eight04@gmail.com> Date: Fri Oct 12 17:29:35 2018 +0800 fixup! Add: implement sloppy regexp indicator commite67b7f4f36
Author: eight <eight04@gmail.com> Date: Fri Oct 12 17:27:19 2018 +0800 Add: implement sloppy regexp indicator commit36e13f88f0
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:59:23 2018 +0800 Add: return excluded/sloppy state in getStylesInfoByUrl commitf6ce78f55b
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:39:47 2018 +0800 Fix: dead object commit5ae95a1ad9
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:27:54 2018 +0800 Fix: don't reinit all editors on save commit1a5a206fe6
Author: eight <eight04@gmail.com> Date: Fri Oct 12 16:18:40 2018 +0800 Refactor: pull out sections editor section commit8016346035
Author: eight <eight04@gmail.com> Date: Fri Oct 12 15:30:35 2018 +0800 Fix: replaceStyle make style name undefined commitfa080d1913
Author: eight <eight04@gmail.com> Date: Fri Oct 12 15:21:36 2018 +0800 Fix: catch csp error commite0b064115d
Author: eight <eight04@gmail.com> Date: Fri Oct 12 15:03:00 2018 +0800 Fix: use a simple eval to execute page scripts commit405b7f8f06
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:48:13 2018 +0800 Fix: removed unused API commit1b2c88f926
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:46:51 2018 +0800 Fix: no need to access db commita8131fc9c5
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:43:31 2018 +0800 Fix: remove unused methods commit3ae0c4dd13
Author: eight <eight04@gmail.com> Date: Fri Oct 12 03:10:26 2018 +0800 Enhance: allow matcher to return verbose info commit0ea7ada48f
Author: eight <eight04@gmail.com> Date: Fri Oct 12 02:02:14 2018 +0800 Fix: content script may load before the background is ready commit04c2d6bbf6
Author: eight <eight04@gmail.com> Date: Fri Oct 12 01:49:52 2018 +0800 Fix: throw receiving end doesn't exist message commitf0c0bc4d6a
Author: eight <eight04@gmail.com> Date: Fri Oct 12 01:11:17 2018 +0800 Fix: unwrap error commit4d42765d6c
Author: eight <eight04@gmail.com> Date: Thu Oct 11 23:55:16 2018 +0800 fixup! Fix: match subdomain commit99626e4a48
Author: eight <eight04@gmail.com> Date: Thu Oct 11 23:54:58 2018 +0800 Fix: match subdomain commita57b3b2716
Author: eight <eight04@gmail.com> Date: Thu Oct 11 23:39:11 2018 +0800 Fix: firefox commit5cfea3933f
Author: eight <eight04@gmail.com> Date: Thu Oct 11 22:46:34 2018 +0800 Add some comment to db.js commit25fd3a1c2b
Author: eight <eight04@gmail.com> Date: Thu Oct 11 22:14:56 2018 +0800 Fix: remove unused prop commitbdae1c3697
Author: eight <eight04@gmail.com> Date: Thu Oct 11 20:00:25 2018 +0800 Change: simpler styleCodeEmpty commitbd4a453f45
Merge:c1bf9f5
9058c06
Author: eight <eight04@gmail.com> Date: Thu Oct 11 19:49:37 2018 +0800 Merge branch 'dev-usercss-meta' into dev-exclusions commitc1bf9f57e9
Author: eight <eight04@gmail.com> Date: Thu Oct 11 19:29:17 2018 +0800 Fix: minor commitfd5eeb4b81
Author: eight <eight04@gmail.com> Date: Thu Oct 11 19:00:05 2018 +0800 Add: refresh on view commit3e38810a49
Author: eight <eight04@gmail.com> Date: Thu Oct 11 18:13:24 2018 +0800 Fix: make sure icons are refreshed at startup commitc657d7e55c
Author: eight <eight04@gmail.com> Date: Thu Oct 11 17:32:27 2018 +0800 Add: implement bug 461 commit7ed39ab6ef
Author: eight <eight04@gmail.com> Date: Thu Oct 11 15:42:44 2018 +0800 fixup! Add: icon-util commit30e494eda9
Author: eight <eight04@gmail.com> Date: Thu Oct 11 15:42:23 2018 +0800 Add: icon-util commit510a886e14
Author: eight <eight04@gmail.com> Date: Thu Oct 11 03:21:38 2018 +0800 Fix: exposeIframes commitc7f81662c4
Author: eight <eight04@gmail.com> Date: Thu Oct 11 02:19:14 2018 +0800 Fix: autoCloseBrackets is true by default commitf3a103645d
Author: eight <eight04@gmail.com> Date: Thu Oct 11 02:11:14 2018 +0800 Fix: various commitd4436cde20
Author: eight <eight04@gmail.com> Date: Thu Oct 11 01:39:10 2018 +0800 Add: implement exposeIframe commit43db875fd8
Author: eight <eight04@gmail.com> Date: Thu Oct 11 01:26:24 2018 +0800 Kill more globals commitdc491e9be3
Author: eight <eight04@gmail.com> Date: Thu Oct 11 01:22:13 2018 +0800 Kill old storage, storage-dummy commitba64b95575
Author: eight <eight04@gmail.com> Date: Thu Oct 11 00:54:38 2018 +0800 WIP: kill cachedStyles commit7eba890a21
Merge:d2b36a1
81e4823
Author: eight <eight04@gmail.com> Date: Wed Oct 10 23:15:14 2018 +0800 Merge branch 'dev-private-prefs' into dev-exclusions commitd2b36a168e
Author: eight <eight04@gmail.com> Date: Wed Oct 10 23:05:20 2018 +0800 Kill hidden globals commit22d4767511
Author: eight <eight04@gmail.com> Date: Wed Oct 10 19:23:34 2018 +0800 Fix: margin for deleted sections commit00687983f0
Author: eight <eight04@gmail.com> Date: Wed Oct 10 18:21:07 2018 +0800 Fix: default value commitff6fd8cad3
Author: eight <eight04@gmail.com> Date: Wed Oct 10 18:02:51 2018 +0800 Fix: default options commitc23f315c52
Author: eight <eight04@gmail.com> Date: Wed Oct 10 17:40:07 2018 +0800 Refactor: use CodeMirror.defineOption commit4419c5dc1e
Author: eight <eight04@gmail.com> Date: Wed Oct 10 16:32:39 2018 +0800 Change: kill editors, styleId commit6494985b50
Author: eight <eight04@gmail.com> Date: Wed Oct 10 16:14:51 2018 +0800 Fix: various commit37e1f43f75
Author: eight <eight04@gmail.com> Date: Wed Oct 10 15:04:03 2018 +0800 Fix: minor commitd26ce3238e
Author: eight <eight04@gmail.com> Date: Wed Oct 10 14:49:37 2018 +0800 Add: codemirror-factory commit15a1f552f6
Author: eight <eight04@gmail.com> Date: Wed Oct 10 12:08:35 2018 +0800 WIP: kill getSection commitba6159e067
Author: eight <eight04@gmail.com> Date: Wed Oct 10 02:43:09 2018 +0800 WIP: edit page commitfd9ab5d6e5
Author: eight <eight04@gmail.com> Date: Wed Oct 10 00:41:07 2018 +0800 Fix: switch to editor commit06e22d0d18
Author: eight <eight04@gmail.com> Date: Tue Oct 9 23:38:29 2018 +0800 Change: add sections-editor commit30e8662946
Author: eight <eight04@gmail.com> Date: Mon Oct 8 20:12:39 2018 +0800 Add: preview error commit47b2b4fc49
Author: eight <eight04@gmail.com> Date: Mon Oct 8 18:38:01 2018 +0800 Add: livePreview.show commit7b5e7c96d5
Author: eight <eight04@gmail.com> Date: Mon Oct 8 18:16:45 2018 +0800 Hook up live preview commit15efafff3c
Author: eight <eight04@gmail.com> Date: Mon Oct 8 17:49:57 2018 +0800 Add: live preview commita38558ef78
Author: eight <eight04@gmail.com> Date: Mon Oct 8 15:30:39 2018 +0800 WIP: make notifyAllTabs a noop commit582e9078af
Author: eight <eight04@gmail.com> Date: Mon Oct 8 14:39:08 2018 +0800 Fix: inject all scripts commitf4651da8d8
Author: eight <eight04@gmail.com> Date: Sun Oct 7 23:41:46 2018 +0800 Drop deleteStyle commit0489fb3b2f
Author: eight <eight04@gmail.com> Date: Sun Oct 7 23:33:51 2018 +0800 Drop saveStyle commit02f471f077
Author: eight <eight04@gmail.com> Date: Sun Oct 7 23:28:41 2018 +0800 Fix: usercss API commit057111b171
Author: eight <eight04@gmail.com> Date: Sun Oct 7 22:59:31 2018 +0800 Update usercss API commit69cae02381
Author: eight <eight04@gmail.com> Date: Sun Oct 7 21:40:29 2018 +0800 Drop getStyles commitc5d41529d9
Author: eight <eight04@gmail.com> Date: Sun Oct 7 21:28:51 2018 +0800 Minor fixes commit5b3b4e680f
Author: eight <eight04@gmail.com> Date: Sun Oct 7 21:20:39 2018 +0800 Add: navigator-util commitb5107b78a5
Author: eight <eight04@gmail.com> Date: Sun Oct 7 01:42:43 2018 +0800 Add: broadcast messages with reasons commite7ef4948cd
Author: eight <eight04@gmail.com> Date: Sat Oct 6 18:10:47 2018 +0800 Fix: observer is unavailable? commit1c635b5bc1
Author: eight <eight04@gmail.com> Date: Sat Oct 6 17:47:43 2018 +0800 Drop requestStyles commit75f2561154
Author: eight <eight04@gmail.com> Date: Sat Oct 6 16:38:04 2018 +0800 Fix: don't recreate element when style update in popup commit583ca31d97
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:40:07 2018 +0800 fixup! Add: isCodeEmpty commit1cf6008514
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:33:18 2018 +0800 Add: isCodeEmpty commit450cd60aeb
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:22:04 2018 +0800 Fix: ignore comment block commit196b6aac63
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:16:00 2018 +0800 Fix: the return value of getSectionsByUrl is changed commit3122d28c1a
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:14:05 2018 +0800 Fix: always use promise in API call commite594b8ccb1
Author: eight <eight04@gmail.com> Date: Sat Oct 6 15:11:01 2018 +0800 Cache enabled state commit1f18b13a92
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:48:46 2018 +0800 Add: match global sections commitfedf844ddd
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:45:37 2018 +0800 Add: getStylesInfoByUrl commit095998f07c
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:27:58 2018 +0800 Change: switch to msg.js commitfa3127d988
Author: eight <eight04@gmail.com> Date: Sat Oct 6 13:02:45 2018 +0800 Change: switch to msg.js commit05d582c726
Author: eight <eight04@gmail.com> Date: Sat Oct 6 11:43:42 2018 +0800 Add: msg.sendBg commit171339f710
Author: eight <eight04@gmail.com> Date: Sat Oct 6 04:39:48 2018 +0800 WIP: drop api.js commit3a618aca2a
Author: eight <eight04@gmail.com> Date: Sat Oct 6 03:19:51 2018 +0800 WIP: use deepCopy commitbb1cb58024
Author: eight <eight04@gmail.com> Date: Sat Oct 6 03:10:04 2018 +0800 WIP: msg.js commit2472e91f57
Author: eight <eight04@gmail.com> Date: Fri Oct 5 21:28:19 2018 +0800 WIP: emitChangesToTabs commit34497ebe16
Author: eight <eight04@gmail.com> Date: Fri Oct 5 18:47:52 2018 +0800 WIP: switch to API commitf1639cc33e
Author: eight <eight04@gmail.com> Date: Fri Oct 5 01:03:40 2018 +0800 WIP: broadcastMessage commit81e4823f46
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:39:59 2018 +0800 Debounce updateAllTabsIcon commitdc5f3e209f
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:34:36 2018 +0800 Fix: settings could be empty on the first install commit2328cf623a
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:34:22 2018 +0800 Change: start-firefox -> start commit7be6a1cba9
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:24:35 2018 +0800 Add: applications commit630725196f
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:22:44 2018 +0800 fixup! Fix: update all icons when some prefs changed commit0d0e1b4dc0
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:20:36 2018 +0800 Fix: update all icons when some prefs changed commit5c0288e9ba
Author: eight <eight04@gmail.com> Date: Thu Oct 4 19:20:11 2018 +0800 fixup! Remove unused FIREFOX_NO_DOM_STORAGE commit56b737b65a
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:14:57 2018 +0800 Remove unused FIREFOX_NO_DOM_STORAGE commit829a134ed1
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:10:53 2018 +0800 Fix: this -> prefs commitd35f92250e
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:08:19 2018 +0800 Fixme: styleViaAPI commit8a6e8ac03a
Author: eight <eight04@gmail.com> Date: Thu Oct 4 18:05:41 2018 +0800 Change: drop prefChanged, use prefs service commit10f9449144
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:46:45 2018 +0800 Change: move setupLivePrefs to dom.js. Remove prefs.js dependencies commitdd2b8ed091
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:18:38 2018 +0800 Fix: type error commit3af310c341
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:09:26 2018 +0800 Fix: open-manager has no default value commit874a2da33e
Author: eight <eight04@gmail.com> Date: Thu Oct 4 17:04:23 2018 +0800 Enhance: make prefs use storage.sync commitc01f93f62c
Author: eight <eight04@gmail.com> Date: Thu Oct 4 15:57:02 2018 +0800 WIP commit6d32ffb76b
Author: eight <eight04@gmail.com> Date: Thu Oct 4 12:46:19 2018 +0800 WIP commit0f148eac32
Author: eight <eight04@gmail.com> Date: Thu Oct 4 03:35:07 2018 +0800 WIP commit282bdf7706
Author: eight <eight04@gmail.com> Date: Wed Oct 3 20:24:06 2018 +0800 Fix: numbers are not compared correctly commit24b1eea8a4
Merge:8a6011d
5cbe8a8
Author: eight <eight04@gmail.com> Date: Wed Oct 3 15:00:07 2018 +0800 Merge branch 'master' of https://github.com/openstyles/stylus into dev-exclusions commit5cbe8a8d78
Author: eight <eight04@gmail.com> Date: Tue Oct 2 20:22:18 2018 +0800 Add: fetch style object from DB directly in the editor (#507) commit9058c06c54
Author: eight <eight04@gmail.com> Date: Mon Oct 1 23:24:29 2018 +0800 Fix: bad API commit1f2d116aae
Author: eight <eight04@gmail.com> Date: Mon Oct 1 23:14:56 2018 +0800 Fix: use meta parser commit918e47b1ed
Author: eight <eight04@gmail.com> Date: Mon Oct 1 23:01:21 2018 +0800 Fix: emit update event if no fatal errors commit81a7bb9ac9
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:56:25 2018 +0800 Add: editorWorker.metalint commitf47d57aea8
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:49:16 2018 +0800 Change: use editorWorker.metalint commit5778d5c858
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:39:01 2018 +0800 Change: editor-worker-body -> editor-worker commit268e1716b4
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:38:06 2018 +0800 Change: switch to worker-util commitcc2980b647
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:30:16 2018 +0800 Drop: parserlib-loader commit08adcb60f2
Merge:6909c73
2fd531e
Author: eight <eight04@gmail.com> Date: Mon Oct 1 22:29:39 2018 +0800 Merge branch 'master' into dev-usercss-meta commite4135ce35d
Author: eight <eight04@gmail.com> Date: Fri Sep 28 11:57:34 2018 +0800 Fix: remove unused function commit39a6d1909f
Author: eight <eight04@gmail.com> Date: Fri Sep 28 00:26:29 2018 +0800 Fix: prefs doesn't work in FF's private windows. Add web-ext. Drop prefs.readOnlyValues commit6909c73c69
Author: eight <eight04@gmail.com> Date: Wed Sep 26 12:16:33 2018 +0800 Fix: minor commit79833d8bba
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:40:04 2018 +0800 Fix: a better way to draw list? commita849fd6dda
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:39:53 2018 +0800 Fix: missing placeholders commitd5ee31a080
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:37:50 2018 +0800 Fix: a better way to draw character list? commit7b959af3e3
Author: eight <eight04@gmail.com> Date: Wed Sep 26 11:30:10 2018 +0800 Update usercss-meta commitfefa987c4d
Author: eight <eight04@gmail.com> Date: Wed Sep 26 10:37:28 2018 +0800 Change: sections-equal -> sections-util commit2abbf670d8
Author: eight <eight04@gmail.com> Date: Wed Sep 26 10:37:14 2018 +0800 Fix: check err.code commit1fe0586b29
Author: eight <eight04@gmail.com> Date: Wed Sep 26 10:33:02 2018 +0800 Add: i18n error message commitab0ef239cf
Author: eight <eight04@gmail.com> Date: Wed Sep 26 09:34:57 2018 +0800 Change: move styleCodeEmpty to sections-util, load colorConverter in background worker commitd5ade807f0
Author: eight <eight04@gmail.com> Date: Wed Sep 26 09:27:30 2018 +0800 Fix: display error message commit4f5337e51d
Author: eight <eight04@gmail.com> Date: Wed Sep 26 09:26:55 2018 +0800 Fix: remove unused colorconverter commit29b8f51292
Author: eight <eight04@gmail.com> Date: Tue Sep 25 23:21:44 2018 +0800 Fix: vars could be undefined commita7cfeb22e4
Author: eight <eight04@gmail.com> Date: Tue Sep 25 22:54:40 2018 +0800 Fix: window is undefined commit9713c6a3be
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:56:38 2018 +0800 Fix: throw an error for unparsable color commit3c30bc3eb0
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:55:55 2018 +0800 Fix: try to get error message commit3d32b0428b
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:38:40 2018 +0800 Fix: vars might be empty commit7d75dd8754
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:18:39 2018 +0800 Add: meta-parser commita4df641b96
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:18:18 2018 +0800 Enhance: set flag in parserlib so we don't need another loader commit8028a3529f
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:17:40 2018 +0800 Include util, worker-util in background commitba5d6cc31a
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:16:59 2018 +0800 Fix: use spread syntax in loadScript commitb853be13f8
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:14:46 2018 +0800 Enhance: swith to usercss-meta (in worker) commita3e7915199
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:11:54 2018 +0800 Fix: use promise API commit5d07a8cd4e
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:11:09 2018 +0800 Fix: buildMeta now returns a promise commita004bc3c7d
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:10:35 2018 +0800 Move styleCodeEmpty to util commit41ac66a137
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:09:40 2018 +0800 Add: background worker commitffb13bf1db
Author: eight <eight04@gmail.com> Date: Tue Sep 25 21:09:04 2018 +0800 Enhance: move moz-parser/meta-parser/usercss compiler to worker commit42e97ef153
Author: eight <eight04@gmail.com> Date: Tue Sep 25 20:45:07 2018 +0800 Fix: display error on install page commit64aa9fcf53
Author: eight <eight04@gmail.com> Date: Tue Sep 25 17:34:54 2018 +0800 Add: background worker commitb0e407e98f
Author: eight <eight04@gmail.com> Date: Tue Sep 25 14:52:35 2018 +0800 Add: worker util commit7a24547e09
Author: eight <eight04@gmail.com> Date: Tue Sep 25 00:01:18 2018 +0800 Add: usercss-meta commit8a6011de8c
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 22 09:15:09 2018 -0500 Attempt to update icon count commit4fcb1a88d7
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 15 13:44:29 2018 -0500 Fix empty exclusion storage error commitbfe54ab4c4
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 15 12:59:51 2018 -0500 Add tab communication commit983a7bc219
Author: Rob Garrison <wowmotty@gmail.com> Date: Sun Jul 15 10:51:11 2018 -0500 Fix escaped regex example commit3950482f34
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Apr 25 18:11:37 2018 -0500 Fix undefined error commite94c7edb38
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Apr 25 17:09:45 2018 -0500 Attempt to fix popup exclusion issues commit2b4a1a5635
Author: Rob Garrison <wowmotty@gmail.com> Date: Thu Apr 19 13:00:27 2018 -0500 Modify input method commit9f75b69cd8
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Mar 7 11:54:05 2018 -0600 Include iframe urls in exclusion popup commit68dfa0153c
Author: Rob Garrison <wowmotty@gmail.com> Date: Wed Jan 24 19:42:02 2018 -0600 Add style exclusions. Closes #113 * Revert: exclusions * Fix: pass eslint * Fix: the style is injected twice * Fix: don't load script async * Fix: styleCodeEmpty returns true for empty string * Fix: drop array selection * Fix: the config dialog is broken * Fix: popup doesn't use getStyle/getStylesByUrl correctly * Fix: keep disabled state in setStyleContent * Fix: allow live-preview to assign newest vars * Fix: transition fix is broken because setStyleContent becomes async * Fix: typo, TypeError in styleExists * Fix: use new API * Fix: pass linter * Fix: LICENCE -> LICENSE * Fix: remove unused distroy function
This commit is contained in:
parent
79c6506c5c
commit
e3d3604afc
61
.eslintrc
61
.eslintrc
|
@ -8,64 +8,6 @@ env:
|
|||
es6: true
|
||||
webextensions: true
|
||||
|
||||
globals:
|
||||
# messaging.js
|
||||
KEEP_CHANNEL_OPEN: false
|
||||
CHROME: false
|
||||
FIREFOX: false
|
||||
VIVALDI: false
|
||||
OPERA: false
|
||||
URLS: false
|
||||
BG: false
|
||||
API: false
|
||||
notifyAllTabs: false
|
||||
sendMessage: false
|
||||
queryTabs: false
|
||||
getTab: false
|
||||
getOwnTab: false
|
||||
getActiveTab: false
|
||||
getActiveTabRealURL: false
|
||||
getTabRealURL: false
|
||||
openURL: false
|
||||
activateTab: false
|
||||
stringAsRegExp: false
|
||||
ignoreChromeError: false
|
||||
tryCatch: false
|
||||
tryRegExp: false
|
||||
tryJSONparse: false
|
||||
debounce: false
|
||||
deepCopy: false
|
||||
sessionStorageHash: false
|
||||
download: false
|
||||
invokeOrPostpone: false
|
||||
# localization.js
|
||||
template: false
|
||||
t: false
|
||||
o: false
|
||||
tE: false
|
||||
tHTML: false
|
||||
tNodeList: false
|
||||
tDocLoader: false
|
||||
tWordBreak: false
|
||||
formatDate: false
|
||||
# dom.js
|
||||
onDOMready: false
|
||||
onDOMscriptReady: false
|
||||
scrollElementIntoView: false
|
||||
enforceInputRange: false
|
||||
animateElement: false
|
||||
$: false
|
||||
$$: false
|
||||
$create: false
|
||||
$createLink: false
|
||||
# prefs.js
|
||||
prefs: false
|
||||
setupLivePrefs: false
|
||||
# storage-util.js
|
||||
chromeLocal: false
|
||||
chromeSync: false
|
||||
LZString: false
|
||||
|
||||
rules:
|
||||
accessor-pairs: [2]
|
||||
array-bracket-spacing: [2, never]
|
||||
|
@ -214,7 +156,6 @@ rules:
|
|||
no-trailing-spaces: [2]
|
||||
no-undef-init: [2]
|
||||
no-undef: [2]
|
||||
no-undefined: [0]
|
||||
no-underscore-dangle: [0]
|
||||
no-unexpected-multiline: [2]
|
||||
no-unmodified-loop-condition: [0]
|
||||
|
@ -224,7 +165,7 @@ rules:
|
|||
no-unsafe-negation: [2]
|
||||
no-unused-expressions: [1]
|
||||
no-unused-labels: [0]
|
||||
no-unused-vars: [1, {args: after-used, vars: local, argsIgnorePattern: ^_}]
|
||||
no-unused-vars: [2, {args: after-used}]
|
||||
no-use-before-define: [2, nofunc]
|
||||
no-useless-call: [2]
|
||||
no-useless-computed-key: [2]
|
||||
|
|
|
@ -689,6 +689,194 @@
|
|||
"message": "Show active style count",
|
||||
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
|
||||
},
|
||||
"meta_invalidCheckboxDefault": {
|
||||
"message": "Invalid @var checkbox: value must be 0 or 1",
|
||||
"description": "Error displayed when the value of @var checkbox is invalid"
|
||||
},
|
||||
"meta_invalidColor": {
|
||||
"message": "Invalid @var color: $color$ is not a color",
|
||||
"description": "Error displayed when the value of @var color is invalid",
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRange": {
|
||||
"message": "Invalid @var $type$: value must be a number or an array",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRangeMultipleUnits": {
|
||||
"message": "Invalid @var $type$: multiple units are defined",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRangeTooManyValues": {
|
||||
"message": "Invalid @var $type$: the array contains too many items",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRangeValue": {
|
||||
"message": "Invalid @var $type$: items in the array must be number, string, or null",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRangeDefault": {
|
||||
"message": "Invalid @var $type$: default value is null",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRangeMin": {
|
||||
"message": "Invalid @var $type$: default value is lower than the minimum",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRangeMax": {
|
||||
"message": "Invalid @var $type$: default value is larger than the maximum",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidRangeStep": {
|
||||
"message": "Invalid @var $type$: default value is not a mutiple of the step",
|
||||
"description": "Error displayed when the value of @var range or @var number is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidSelectEmptyOptions": {
|
||||
"message": "Invalid @var select: options list is empty",
|
||||
"description": "Error displayed when the value of @var select is invalid"
|
||||
},
|
||||
"meta_invalidSelectMultipleDefaults": {
|
||||
"message": "Invalid @var select: multiple default options are defined",
|
||||
"description": "Error displayed when the value of @var select is invalid"
|
||||
},
|
||||
"meta_invalidSelectValueMismatch": {
|
||||
"message": "Invalid @var select: value doesn't exist in the option list",
|
||||
"description": "Error displayed when the value of @var select is invalid"
|
||||
},
|
||||
"meta_invalidURLProtocol": {
|
||||
"message": "Invalid URL protocol. Only http and https are allowed: $protocol$",
|
||||
"description": "Error displayed when the protocol of the URL is invalid",
|
||||
"placeholders": {
|
||||
"protocol": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidVersion": {
|
||||
"message": "Invalid version number. The value doesn't match SemVer pattern: $version$",
|
||||
"description": "Error displayed when @version is invalid",
|
||||
"placeholders": {
|
||||
"version": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_invalidNumber": {
|
||||
"message": "Expect a number",
|
||||
"description": "Error displayed when the value is expected to be a number"
|
||||
},
|
||||
"meta_invalidString": {
|
||||
"message": "Expect a quoted string",
|
||||
"description": "Error displayed when the value is expected to be a quoted string"
|
||||
},
|
||||
"meta_invalidWord": {
|
||||
"message": "Expect a word",
|
||||
"description": "Error displayed when the value is expected to be a word"
|
||||
},
|
||||
"meta_missingChar": {
|
||||
"message": "Expect characters: $chars$",
|
||||
"description": "Error displayed when the value is expected to be some characters",
|
||||
"placeholders": {
|
||||
"chars": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_missingEOT": {
|
||||
"message": "Expect EOT data",
|
||||
"description": "Error displayed when the value is expected to be an EOT list"
|
||||
},
|
||||
"meta_missingMandatory": {
|
||||
"message": "Missing mandatory metadata: $keys$",
|
||||
"description": "Error displayed when mandatory keys are missing",
|
||||
"placeholders": {
|
||||
"keys": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_unknownJSONLiteral": {
|
||||
"message": "Invalid JSON: $literal$ is not a valid JSON literal",
|
||||
"description": "Error displayed when JSON value is invalid",
|
||||
"placeholders": {
|
||||
"literal": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_unknownMeta": {
|
||||
"message": "Unknown metadata: $key$",
|
||||
"description": "Error displayed when unknown metadata is parsed",
|
||||
"placeholders": {
|
||||
"key": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_unknownVarType": {
|
||||
"message": "Unknown @$varkey$ type: $vartype$",
|
||||
"description": "Error displayed when unknown variable type is parsed",
|
||||
"placeholders": {
|
||||
"varkey": {
|
||||
"content": "$1"
|
||||
},
|
||||
"vartype": {
|
||||
"content": "$2"
|
||||
}
|
||||
}
|
||||
},
|
||||
"meta_unknownPreprocessor": {
|
||||
"message": "Unknown @preprocessor: $preprocessor$",
|
||||
"description": "Error displayed when unknown @preprocessor is parsed",
|
||||
"placeholders": {
|
||||
"preprocessor": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"noStylesForSite": {
|
||||
"message": "No styles installed for this site.",
|
||||
"description": "Text displayed when no styles are installed for the current site"
|
||||
|
@ -922,10 +1110,6 @@
|
|||
"message": "Code",
|
||||
"description": "Label for the code for a section"
|
||||
},
|
||||
"sectionHelp": {
|
||||
"message": "Sections let you define different pieces of code to apply to different sets of URLs in the same style. For example, a single style could change the homepage of a site one way, while changing the rest of a site another way.",
|
||||
"description": "Help text for sections"
|
||||
},
|
||||
"sectionRemove": {
|
||||
"message": "Remove section",
|
||||
"description": "Label for the button to remove a section"
|
||||
|
@ -1038,50 +1222,6 @@
|
|||
},
|
||||
"description": "Confirmation when re-installing a style"
|
||||
},
|
||||
"styleMetaErrorCheckbox": {
|
||||
"message": "Invalid @var checkbox: value must be 0 or 1",
|
||||
"description": "Error displayed when the value of @var checkbox is invalid"
|
||||
},
|
||||
"styleMetaErrorColor": {
|
||||
"message": "$color$ is not a valid color",
|
||||
"placeholders": {
|
||||
"color": {
|
||||
"content": "$1"
|
||||
}
|
||||
},
|
||||
"description": "Error displayed when the value of @var color is invalid"
|
||||
},
|
||||
"styleMetaErrorRangeOrNumber": {
|
||||
"message": "Invalid @var $type$: value must be an array containing at least one number at index zero",
|
||||
"description": "Error displayed when the value of @var number or @var range is invalid",
|
||||
"placeholders": {
|
||||
"type": {
|
||||
"content": "$1"
|
||||
}
|
||||
}
|
||||
},
|
||||
"styleMetaErrorPreprocessor": {
|
||||
"message": "Unsupported @preprocessor: $preprocessor$",
|
||||
"placeholders": {
|
||||
"preprocessor": {
|
||||
"content": "$1"
|
||||
}
|
||||
},
|
||||
"description": "Error displayed when the value of @preprocessor is not supported"
|
||||
},
|
||||
"styleMetaErrorSelectValueMismatch": {
|
||||
"message": "Invalid @select: value doesn't exist in the list",
|
||||
"description": "Error displayed when the value of @select is invalid"
|
||||
},
|
||||
"styleMissingMeta": {
|
||||
"message": "Missing metadata @$key$",
|
||||
"placeholders": {
|
||||
"key": {
|
||||
"content": "$1"
|
||||
}
|
||||
},
|
||||
"description": "Error displayed when a mandatory metadata is missing"
|
||||
},
|
||||
"styleMissingName": {
|
||||
"message": "Enter a name",
|
||||
"description": "Error displayed when user saves without providing a name"
|
||||
|
@ -1136,10 +1276,6 @@
|
|||
"message": "Save",
|
||||
"description": "Label for save button for style editing"
|
||||
},
|
||||
"styleSectionsTitle": {
|
||||
"message": "Sections",
|
||||
"description": "Title for the style sections section"
|
||||
},
|
||||
"styleToMozillaFormatHelp": {
|
||||
"message": "The Mozilla format of the code can be submitted to userstyles.org and used with the classic Stylish for Firefox",
|
||||
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
|
||||
|
|
167
background/background-worker.js
Normal file
167
background/background-worker.js
Normal file
|
@ -0,0 +1,167 @@
|
|||
/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */
|
||||
'use strict';
|
||||
|
||||
importScripts('/js/worker-util.js');
|
||||
const {loadScript, createAPI} = workerUtil;
|
||||
|
||||
createAPI({
|
||||
parseMozFormat(arg) {
|
||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||
return parseMozFormat(arg);
|
||||
},
|
||||
compileUsercss,
|
||||
parseUsercssMeta(text, indexOffset = 0) {
|
||||
loadScript(
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
);
|
||||
return metaParser.parse(text, indexOffset);
|
||||
},
|
||||
nullifyInvalidVars(vars) {
|
||||
loadScript(
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
);
|
||||
return metaParser.nullifyInvalidVars(vars);
|
||||
}
|
||||
});
|
||||
|
||||
function compileUsercss(preprocessor, code, vars) {
|
||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||
const builder = getUsercssCompiler(preprocessor);
|
||||
vars = simpleVars(vars);
|
||||
return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
|
||||
.then(code => parseMozFormat({code}))
|
||||
.then(({sections, errors}) => {
|
||||
if (builder.postprocess) {
|
||||
builder.postprocess(sections, vars);
|
||||
}
|
||||
return {sections, errors};
|
||||
});
|
||||
|
||||
function simpleVars(vars) {
|
||||
if (!vars) {
|
||||
return {};
|
||||
}
|
||||
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
|
||||
// need to test each va's default value.
|
||||
return Object.keys(vars).reduce((output, key) => {
|
||||
const va = vars[key];
|
||||
output[key] = Object.assign({}, va, {
|
||||
value: va.value === null || va.value === undefined ?
|
||||
getVarValue(va, 'default') : getVarValue(va, 'value')
|
||||
});
|
||||
return output;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getVarValue(va, prop) {
|
||||
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
|
||||
// TODO: handle customized image
|
||||
return va.options.find(o => o.name === va[prop]).value;
|
||||
}
|
||||
if ((va.type === 'number' || va.type === 'range') && va.units) {
|
||||
return va[prop] + va.units;
|
||||
}
|
||||
return va[prop];
|
||||
}
|
||||
}
|
||||
|
||||
function getUsercssCompiler(preprocessor) {
|
||||
const BUILDER = {
|
||||
default: {
|
||||
postprocess(sections, vars) {
|
||||
loadScript('/js/sections-util.js');
|
||||
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
|
||||
if (!varDef) return;
|
||||
varDef = ':root {\n' + varDef + '}\n';
|
||||
for (const section of sections) {
|
||||
if (!styleCodeEmpty(section.code)) {
|
||||
section.code = varDef + section.code;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stylus: {
|
||||
preprocess(source, vars) {
|
||||
loadScript('/vendor/stylus-lang-bundle/stylus.min.js');
|
||||
return new Promise((resolve, reject) => {
|
||||
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
|
||||
if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
|
||||
self.stylus(varDef + source).render((err, output) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(output);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
},
|
||||
less: {
|
||||
preprocess(source, vars) {
|
||||
if (!self.less) {
|
||||
self.less = {
|
||||
logLevel: 0,
|
||||
useFileCache: false,
|
||||
};
|
||||
}
|
||||
loadScript('/vendor/less/less.min.js');
|
||||
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
|
||||
return self.less.render(varDefs + source)
|
||||
.then(({css}) => css);
|
||||
}
|
||||
},
|
||||
uso: {
|
||||
preprocess(source, vars) {
|
||||
loadScript('/vendor-overwrites/colorpicker/colorconverter.js');
|
||||
const pool = new Map();
|
||||
return Promise.resolve(doReplace(source));
|
||||
|
||||
function getValue(name, rgb) {
|
||||
if (!vars.hasOwnProperty(name)) {
|
||||
if (name.endsWith('-rgb')) {
|
||||
return getValue(name.slice(0, -4), true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (rgb) {
|
||||
if (vars[name].type === 'color') {
|
||||
const color = colorConverter.parse(vars[name].value);
|
||||
if (!color) return null;
|
||||
const {r, g, b} = color;
|
||||
return `${r}, ${g}, ${b}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
|
||||
// prevent infinite recursion
|
||||
pool.set(name, '');
|
||||
return doReplace(vars[name].value);
|
||||
}
|
||||
return vars[name].value;
|
||||
}
|
||||
|
||||
function doReplace(text) {
|
||||
return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => {
|
||||
if (!pool.has(name)) {
|
||||
const value = getValue(name);
|
||||
pool.set(name, value === null ? match : value);
|
||||
}
|
||||
return pool.get(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (preprocessor) {
|
||||
if (!BUILDER[preprocessor]) {
|
||||
throw new Error('unknwon preprocessor');
|
||||
}
|
||||
return BUILDER[preprocessor];
|
||||
}
|
||||
return BUILDER.default;
|
||||
}
|
|
@ -1,54 +1,54 @@
|
|||
/*
|
||||
global dbExec getStyles saveStyle deleteStyle
|
||||
global handleCssTransitionBug detectSloppyRegexps
|
||||
global openEditor
|
||||
global styleViaAPI
|
||||
global loadScript
|
||||
global usercss
|
||||
*/
|
||||
/* global download prefs openURL FIREFOX CHROME VIVALDI
|
||||
openEditor debounce URLS ignoreChromeError queryTabs getTab
|
||||
styleManager msg navigatorUtil iconUtil workerUtil */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var backgroundWorker = workerUtil.createWorker({
|
||||
url: '/background/background-worker.js'
|
||||
});
|
||||
|
||||
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||
deleteStyle: styleManager.deleteStyle,
|
||||
editSave: styleManager.editSave,
|
||||
findStyle: styleManager.findStyle,
|
||||
getAllStyles: styleManager.getAllStyles, // used by importer
|
||||
getSectionsByUrl: styleManager.getSectionsByUrl,
|
||||
getStyle: styleManager.get,
|
||||
getStylesByUrl: styleManager.getStylesByUrl,
|
||||
importStyle: styleManager.importStyle,
|
||||
installStyle: styleManager.installStyle,
|
||||
styleExists: styleManager.styleExists,
|
||||
toggleStyle: styleManager.toggleStyle,
|
||||
|
||||
getStyles,
|
||||
saveStyle,
|
||||
deleteStyle,
|
||||
|
||||
getStyleFromDB: id =>
|
||||
dbExec('get', id).then(event => event.target.result),
|
||||
getTabUrlPrefix() {
|
||||
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
|
||||
},
|
||||
|
||||
download(msg) {
|
||||
delete msg.method;
|
||||
return download(msg.url, msg);
|
||||
},
|
||||
parseCss({code}) {
|
||||
return usercss.invokeWorker({action: 'parse', code});
|
||||
return backgroundWorker.parseMozFormat({code});
|
||||
},
|
||||
getPrefs: prefs.getAll,
|
||||
healthCheck: () => dbExec().then(() => true),
|
||||
|
||||
detectSloppyRegexps,
|
||||
openEditor,
|
||||
updateIcon,
|
||||
|
||||
updateIconBadge(count) {
|
||||
return updateIconBadge(this.sender.tab.id, count);
|
||||
},
|
||||
|
||||
// exposed for stuff that requires followup sendMessage() like popup::openSettings
|
||||
// that would fail otherwise if another extension forced the tab to open
|
||||
// in the foreground thus auto-closing the popup (in Chrome)
|
||||
openURL,
|
||||
|
||||
closeTab: (msg, sender, respond) => {
|
||||
chrome.tabs.remove(msg.tabId || sender.tab.id, () => {
|
||||
if (chrome.runtime.lastError && msg.tabId !== sender.tab.id) {
|
||||
respond(new Error(chrome.runtime.lastError.message));
|
||||
}
|
||||
});
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
},
|
||||
|
||||
optionsCustomizeHotkeys() {
|
||||
return browser.runtime.openOptionsPage()
|
||||
.then(() => new Promise(resolve => setTimeout(resolve, 100)))
|
||||
.then(() => sendMessage({method: 'optionsCustomizeHotkeys'}));
|
||||
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -57,67 +57,31 @@ var browserCommands, contextMenus;
|
|||
|
||||
// *************************************************************************
|
||||
// register all listeners
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
msg.on(onRuntimeMessage);
|
||||
|
||||
navigatorUtil.onUrlChange(({tabId, frameId}, type) => {
|
||||
if (type === 'committed') {
|
||||
// styles would be updated when content script is injected.
|
||||
return;
|
||||
}
|
||||
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId})
|
||||
.catch(msg.ignoreError);
|
||||
});
|
||||
|
||||
if (FIREFOX) {
|
||||
// see notes in apply.js for getStylesFallback
|
||||
const MSG_GET_STYLES = 'getStyles:';
|
||||
const MSG_GET_STYLES_LEN = MSG_GET_STYLES.length;
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
if (!port.name.startsWith(MSG_GET_STYLES)) return;
|
||||
const tabId = port.sender.tab.id;
|
||||
const frameId = port.sender.frameId;
|
||||
const options = tryJSONparse(port.name.slice(MSG_GET_STYLES_LEN));
|
||||
port.disconnect();
|
||||
getStyles(options).then(styles => {
|
||||
if (!styles.length) return;
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
code: `
|
||||
applyOnMessage({
|
||||
method: 'styleApply',
|
||||
styles: ${JSON.stringify(styles)},
|
||||
})
|
||||
`,
|
||||
runAt: 'document_start',
|
||||
frameId,
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
{
|
||||
const listener =
|
||||
URLS.chromeProtectsNTP
|
||||
? webNavigationListenerChrome
|
||||
: webNavigationListener;
|
||||
|
||||
chrome.webNavigation.onBeforeNavigate.addListener(data =>
|
||||
listener(null, data));
|
||||
|
||||
chrome.webNavigation.onCommitted.addListener(data =>
|
||||
listener('styleApply', data));
|
||||
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
||||
listener('styleReplaceAll', data));
|
||||
|
||||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||
listener('styleReplaceAll', data));
|
||||
|
||||
if (FIREFOX) {
|
||||
// FF applies page CSP even to content scripts, https://bugzil.la/1267027
|
||||
chrome.webNavigation.onCommitted.addListener(webNavUsercssInstallerFF, {
|
||||
navigatorUtil.onCommitted(webNavUsercssInstallerFF, {
|
||||
url: [
|
||||
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.css'},
|
||||
{hostSuffix: '.githubusercontent.com', urlSuffix: '.user.styl'},
|
||||
]
|
||||
});
|
||||
// FF misses some about:blank iframes so we inject our content script explicitly
|
||||
chrome.webNavigation.onDOMContentLoaded.addListener(webNavIframeHelperFF, {
|
||||
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
|
||||
url: [
|
||||
{urlEquals: 'about:blank'},
|
||||
]
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (chrome.contextMenus) {
|
||||
|
@ -130,22 +94,45 @@ if (chrome.commands) {
|
|||
chrome.commands.onCommand.addListener(command => browserCommands[command]());
|
||||
}
|
||||
|
||||
if (!chrome.browserAction ||
|
||||
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
|
||||
window.updateIcon = () => {};
|
||||
}
|
||||
|
||||
const tabIcons = new Map();
|
||||
chrome.tabs.onRemoved.addListener(tabId => tabIcons.delete(tabId));
|
||||
chrome.tabs.onReplaced.addListener((added, removed) => tabIcons.delete(removed));
|
||||
|
||||
// *************************************************************************
|
||||
// set the default icon displayed after a tab is created until webNavigation kicks in
|
||||
prefs.subscribe(['iconset'], () =>
|
||||
updateIcon({
|
||||
tab: {id: undefined},
|
||||
styles: {},
|
||||
}));
|
||||
prefs.subscribe([
|
||||
'disableAll',
|
||||
'badgeDisabled',
|
||||
'badgeNormal',
|
||||
], () => debounce(refreshIconBadgeColor));
|
||||
|
||||
prefs.subscribe([
|
||||
'show-badge'
|
||||
], () => debounce(refreshIconBadgeText));
|
||||
|
||||
prefs.subscribe([
|
||||
'disableAll',
|
||||
'iconset',
|
||||
], () => debounce(refreshAllIcons));
|
||||
|
||||
prefs.initializing.then(() => {
|
||||
refreshIconBadgeColor();
|
||||
refreshAllIconsBadgeText();
|
||||
refreshAllIcons();
|
||||
});
|
||||
|
||||
navigatorUtil.onUrlChange(({tabId, frameId, transitionQualifiers}, type) => {
|
||||
if (type === 'committed' && !frameId) {
|
||||
// it seems that the tab icon would be reset by navigation. We
|
||||
// invalidate the cache here so it would be refreshed by `apply.js`.
|
||||
tabIcons.delete(tabId);
|
||||
|
||||
// however, if the tab was swapped in by forward/backward buttons,
|
||||
// `apply.js` doesn't notify the background to update the icon,
|
||||
// so we have to refresh it manually.
|
||||
if (transitionQualifiers.includes('forward_back')) {
|
||||
msg.sendTab(tabId, {method: 'updateCount'}).catch(msg.ignoreError);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
chrome.runtime.onInstalled.addListener(({reason}) => {
|
||||
|
@ -191,7 +178,7 @@ contextMenus = {
|
|||
contexts: ['editable'],
|
||||
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
|
||||
click: (info, tab) => {
|
||||
sendMessage({tabId: tab.id, method: 'editDeleteText'});
|
||||
msg.sendTab(tab.id, {method: 'editDeleteText'});
|
||||
},
|
||||
}
|
||||
};
|
||||
|
@ -205,11 +192,10 @@ if (chrome.contextMenus) {
|
|||
}
|
||||
item = Object.assign({id}, item);
|
||||
delete item.presentIf;
|
||||
const prefValue = prefs.readOnlyValues[id];
|
||||
item.title = chrome.i18n.getMessage(item.title);
|
||||
if (!item.type && typeof prefValue === 'boolean') {
|
||||
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
|
||||
item.type = 'checkbox';
|
||||
item.checked = prefValue;
|
||||
item.checked = prefs.get(id);
|
||||
}
|
||||
if (!item.contexts) {
|
||||
item.contexts = ['browser_action'];
|
||||
|
@ -233,24 +219,35 @@ if (chrome.contextMenus) {
|
|||
};
|
||||
|
||||
const keys = Object.keys(contextMenus);
|
||||
prefs.subscribe(keys.filter(id => typeof prefs.readOnlyValues[id] === 'boolean'), toggleCheckmark);
|
||||
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark);
|
||||
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence);
|
||||
createContextMenus(keys);
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
// [re]inject content scripts
|
||||
window.addEventListener('storageReady', function _() {
|
||||
window.removeEventListener('storageReady', _);
|
||||
// reinject content scripts when the extension is reloaded/updated. Firefox
|
||||
// would handle this automatically.
|
||||
if (!FIREFOX) {
|
||||
reinjectContentScripts();
|
||||
}
|
||||
|
||||
updateIcon({
|
||||
tab: {id: undefined},
|
||||
styles: {},
|
||||
// register hotkeys
|
||||
if (FIREFOX && browser.commands && browser.commands.update) {
|
||||
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
|
||||
prefs.subscribe(hotkeyPrefs, (name, value) => {
|
||||
try {
|
||||
name = name.split('.')[1];
|
||||
if (value.trim()) {
|
||||
browser.commands.update({name, shortcut: value});
|
||||
} else {
|
||||
browser.commands.reset(name);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
// Firefox injects content script automatically
|
||||
if (FIREFOX) return;
|
||||
msg.broadcastTab({method: 'backgroundReady'});
|
||||
|
||||
function reinjectContentScripts() {
|
||||
const NTP = 'chrome://newtab/';
|
||||
const ALL_URLS = '<all_urls>';
|
||||
const contentScripts = chrome.runtime.getManifest().content_scripts;
|
||||
|
@ -266,20 +263,23 @@ window.addEventListener('storageReady', function _() {
|
|||
|
||||
const injectCS = (cs, tabId) => {
|
||||
ignoreChromeError();
|
||||
for (const file of cs.js) {
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
file: cs.js[0],
|
||||
file,
|
||||
runAt: cs.run_at,
|
||||
allFrames: cs.all_frames,
|
||||
matchAboutBlank: cs.match_about_blank,
|
||||
}, ignoreChromeError);
|
||||
}
|
||||
};
|
||||
|
||||
const pingCS = (cs, {id, url}) => {
|
||||
const maybeInject = pong => !pong && injectCS(cs, id);
|
||||
cs.matches.some(match => {
|
||||
if ((match === ALL_URLS || url.match(match)) &&
|
||||
(!url.startsWith('chrome') || url === NTP)) {
|
||||
sendMessage({method: 'ping', tabId: id}, maybeInject);
|
||||
msg.sendTab(id, {method: 'ping'})
|
||||
.catch(() => false)
|
||||
.then(pong => !pong && injectCS(cs, id));
|
||||
return true;
|
||||
}
|
||||
});
|
||||
|
@ -293,85 +293,19 @@ window.addEventListener('storageReady', function _() {
|
|||
setTimeout(pingCS, 0, cs, tab));
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
// *************************************************************************
|
||||
{
|
||||
const getStylesForFrame = (msg, sender) => {
|
||||
const stylesTask = getStyles(msg);
|
||||
if (!sender || !sender.frameId) return stylesTask;
|
||||
return Promise.all([
|
||||
stylesTask,
|
||||
getTab(sender.tab.id),
|
||||
]).then(([styles, tab]) => {
|
||||
if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1');
|
||||
return styles;
|
||||
});
|
||||
};
|
||||
const updateAPI = (_, enabled) => {
|
||||
window.API_METHODS.getStylesForFrame = enabled ? getStylesForFrame : getStyles;
|
||||
};
|
||||
prefs.subscribe(['exposeIframes'], updateAPI);
|
||||
updateAPI(null, prefs.readOnlyValues.exposeIframes);
|
||||
}
|
||||
|
||||
// *************************************************************************
|
||||
|
||||
function webNavigationListener(method, {url, tabId, frameId}) {
|
||||
Promise.all([
|
||||
getStyles({matchUrl: url, asHash: true}),
|
||||
frameId && prefs.readOnlyValues.exposeIframes && getTab(tabId),
|
||||
]).then(([styles, tab]) => {
|
||||
if (method && URLS.supported(url) && tabId >= 0) {
|
||||
if (method === 'styleApply') {
|
||||
handleCssTransitionBug({tabId, frameId, url, styles});
|
||||
}
|
||||
if (tab) styles.exposeIframes = tab.url.replace(/(\/\/[^/]*).*/, '$1');
|
||||
sendMessage({
|
||||
tabId,
|
||||
frameId,
|
||||
method,
|
||||
// ping own page so it retrieves the styles directly
|
||||
styles: url.startsWith(URLS.ownOrigin) ? 'DIY' : styles,
|
||||
});
|
||||
}
|
||||
// main page frame id is 0
|
||||
if (frameId === 0) {
|
||||
tabIcons.delete(tabId);
|
||||
updateIcon({tab: {id: tabId, url}, styles});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function webNavigationListenerChrome(method, data) {
|
||||
// Chrome 61.0.3161+ doesn't run content scripts on NTP
|
||||
if (
|
||||
!data.url.startsWith('https://www.google.') ||
|
||||
!data.url.includes('/_/chrome/newtab?')
|
||||
) {
|
||||
webNavigationListener(method, data);
|
||||
return;
|
||||
}
|
||||
getTab(data.tabId).then(tab => {
|
||||
if (tab.url === 'chrome://newtab/') {
|
||||
data.url = tab.url;
|
||||
}
|
||||
webNavigationListener(method, data);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function webNavUsercssInstallerFF(data) {
|
||||
const {tabId} = data;
|
||||
Promise.all([
|
||||
sendMessage({tabId, method: 'ping'}),
|
||||
msg.sendTab(tabId, {method: 'ping'})
|
||||
.catch(() => false),
|
||||
// we need tab index to open the installer next to the original one
|
||||
// and also to skip the double-invocation in FF which assigns tab url later
|
||||
getTab(tabId),
|
||||
]).then(([pong, tab]) => {
|
||||
if (pong !== true && tab.url !== 'about:blank') {
|
||||
window.API_METHODS.installUsercss({direct: true}, {tab});
|
||||
window.API_METHODS.openUsercssInstallPage({direct: true}, {tab});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -379,135 +313,107 @@ function webNavUsercssInstallerFF(data) {
|
|||
|
||||
function webNavIframeHelperFF({tabId, frameId}) {
|
||||
if (!frameId) return;
|
||||
sendMessage({method: 'ping', tabId, frameId}, pong => {
|
||||
ignoreChromeError();
|
||||
msg.sendTab(tabId, {method: 'ping'}, {frameId})
|
||||
.catch(() => false)
|
||||
.then(pong => {
|
||||
if (pong) return;
|
||||
// insert apply.js to iframe
|
||||
const files = chrome.runtime.getManifest().content_scripts[0].js;
|
||||
for (const file of files) {
|
||||
chrome.tabs.executeScript(tabId, {
|
||||
frameId,
|
||||
file: '/content/apply.js',
|
||||
file,
|
||||
matchAboutBlank: true,
|
||||
}, ignoreChromeError);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function updateIcon({tab, styles}) {
|
||||
if (tab.id < 0) {
|
||||
function updateIconBadge(tabId, count) {
|
||||
let tabIcon = tabIcons.get(tabId);
|
||||
if (!tabIcon) tabIcons.set(tabId, (tabIcon = {}));
|
||||
if (tabIcon.count === count) {
|
||||
return;
|
||||
}
|
||||
if (URLS.chromeProtectsNTP && tab.url === 'chrome://newtab/') {
|
||||
styles = {};
|
||||
const oldCount = tabIcon.count;
|
||||
tabIcon.count = count;
|
||||
refreshIconBadgeText(tabId, tabIcon);
|
||||
if (Boolean(oldCount) !== Boolean(count)) {
|
||||
refreshIcon(tabId, tabIcon);
|
||||
}
|
||||
if (styles) {
|
||||
stylesReceived(styles);
|
||||
}
|
||||
|
||||
function refreshIconBadgeText(tabId, icon) {
|
||||
iconUtil.setBadgeText({
|
||||
text: prefs.get('show-badge') && icon.count ? String(icon.count) : '',
|
||||
tabId
|
||||
});
|
||||
}
|
||||
|
||||
function refreshIcon(tabId, icon) {
|
||||
const disableAll = prefs.get('disableAll');
|
||||
const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
|
||||
const postfix = disableAll ? 'x' : !icon.count ? 'w' : '';
|
||||
const iconType = iconset + postfix;
|
||||
|
||||
if (icon.iconType === iconType) {
|
||||
return;
|
||||
}
|
||||
getTabRealURL(tab)
|
||||
.then(url => getStyles({matchUrl: url, asHash: true}))
|
||||
.then(stylesReceived);
|
||||
|
||||
function stylesReceived(styles) {
|
||||
const disableAll = 'disableAll' in styles ? styles.disableAll : prefs.get('disableAll');
|
||||
const postfix = disableAll ? 'x' : !styles.length ? 'w' : '';
|
||||
const color = prefs.get(disableAll ? 'badgeDisabled' : 'badgeNormal');
|
||||
const text = prefs.get('show-badge') && styles.length ? String(styles.length) : '';
|
||||
const iconset = ['', 'light/'][prefs.get('iconset')] || '';
|
||||
|
||||
let tabIcon = tabIcons.get(tab.id);
|
||||
if (!tabIcon) tabIcons.set(tab.id, (tabIcon = {}));
|
||||
|
||||
if (tabIcon.iconType !== iconset + postfix) {
|
||||
tabIcon.iconType = iconset + postfix;
|
||||
icon.iconType = iconset + postfix;
|
||||
const sizes = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38];
|
||||
const usePath = tabIcons.get('usePath');
|
||||
Promise.all(sizes.map(size => {
|
||||
const src = `/images/icon/${iconset}${size}${postfix}.png`;
|
||||
return usePath ? src : tabIcons.get(src) || loadIcon(src);
|
||||
})).then(data => {
|
||||
const imageKey = typeof data[0] === 'string' ? 'path' : 'imageData';
|
||||
const imageData = {};
|
||||
sizes.forEach((size, i) => (imageData[size] = data[i]));
|
||||
chrome.browserAction.setIcon({
|
||||
tabId: tab.id,
|
||||
[imageKey]: imageData,
|
||||
}, ignoreChromeError);
|
||||
iconUtil.setIcon({
|
||||
path: sizes.reduce(
|
||||
(obj, size) => {
|
||||
obj[size] = `/images/icon/${iconset}${size}${postfix}.png`;
|
||||
return obj;
|
||||
},
|
||||
{}
|
||||
),
|
||||
tabId
|
||||
});
|
||||
}
|
||||
if (tab.id === undefined) return;
|
||||
}
|
||||
|
||||
let defaultIcon = tabIcons.get(undefined);
|
||||
if (!defaultIcon) tabIcons.set(undefined, (defaultIcon = {}));
|
||||
if (defaultIcon.color !== color) {
|
||||
defaultIcon.color = color;
|
||||
chrome.browserAction.setBadgeBackgroundColor({color});
|
||||
}
|
||||
|
||||
if (tabIcon.text === text) return;
|
||||
tabIcon.text = text;
|
||||
try {
|
||||
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id}, ignoreChromeError);
|
||||
} catch (e) {
|
||||
setTimeout(() => {
|
||||
getTab(tab.id).then(realTab => {
|
||||
// skip pre-rendered tabs
|
||||
if (realTab.index >= 0) {
|
||||
chrome.browserAction.setBadgeText({text, tabId: tab.id});
|
||||
}
|
||||
function refreshIconBadgeColor() {
|
||||
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal');
|
||||
iconUtil.setBadgeBackgroundColor({
|
||||
color
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function loadIcon(src, resolve) {
|
||||
if (!resolve) return new Promise(resolve => loadIcon(src, resolve));
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
const img = new Image();
|
||||
img.src = src;
|
||||
img.onload = () => {
|
||||
const w = canvas.width = img.width;
|
||||
const h = canvas.height = img.height;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
const data = ctx.getImageData(0, 0, w, h);
|
||||
// Firefox breaks Canvas when privacy.resistFingerprinting=true, https://bugzil.la/1412961
|
||||
let usePath = tabIcons.get('usePath');
|
||||
if (usePath === undefined) {
|
||||
usePath = data.data.every(b => b === 255);
|
||||
tabIcons.set('usePath', usePath);
|
||||
function refreshAllIcons() {
|
||||
for (const [tabId, icon] of tabIcons) {
|
||||
refreshIcon(tabId, icon);
|
||||
}
|
||||
if (usePath) {
|
||||
resolve(src);
|
||||
refreshIcon(null, {}); // default icon
|
||||
}
|
||||
|
||||
function refreshAllIconsBadgeText() {
|
||||
for (const [tabId, icon] of tabIcons) {
|
||||
refreshIconBadgeText(tabId, icon);
|
||||
}
|
||||
}
|
||||
|
||||
function onRuntimeMessage(msg, sender) {
|
||||
if (msg.method !== 'invokeAPI') {
|
||||
return;
|
||||
}
|
||||
tabIcons.set(src, data);
|
||||
resolve(data);
|
||||
};
|
||||
const fn = window.API_METHODS[msg.name];
|
||||
if (!fn) {
|
||||
throw new Error(`unknown API: ${msg.name}`);
|
||||
}
|
||||
const context = {msg, sender};
|
||||
return fn.apply(context, msg.args);
|
||||
}
|
||||
|
||||
|
||||
function onRuntimeMessage(msg, sender, sendResponse) {
|
||||
const fn = window.API_METHODS[msg.method];
|
||||
if (!fn) return;
|
||||
|
||||
// wrap 'Error' object instance as {__ERROR__: message},
|
||||
// which will be unwrapped by sendMessage,
|
||||
// and prevent exceptions on sending to a closed tab
|
||||
const respond = data =>
|
||||
tryCatch(sendResponse,
|
||||
data instanceof Error ? {__ERROR__: data.message} : data);
|
||||
|
||||
const result = fn(msg, sender, respond);
|
||||
if (result instanceof Promise) {
|
||||
result
|
||||
.catch(e => ({__ERROR__: e instanceof Error ? e.message : e}))
|
||||
.then(respond);
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
} else if (result === KEEP_CHANNEL_OPEN) {
|
||||
return KEEP_CHANNEL_OPEN;
|
||||
} else if (result !== undefined) {
|
||||
respond(result);
|
||||
// FIXME: popup.js also open editor but it doesn't use this API.
|
||||
function openEditor({id}) {
|
||||
let url = '/edit.html';
|
||||
if (id) {
|
||||
url += `?id=${id}`;
|
||||
}
|
||||
if (chrome.windows && prefs.get('openEditInWindow')) {
|
||||
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
|
||||
} else {
|
||||
openURL({url});
|
||||
}
|
||||
}
|
||||
|
|
154
background/db.js
Normal file
154
background/db.js
Normal file
|
@ -0,0 +1,154 @@
|
|||
/* global tryCatch chromeLocal ignoreChromeError */
|
||||
/* exported db */
|
||||
/*
|
||||
Initialize a database. There are some problems using IndexedDB in Firefox:
|
||||
https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/
|
||||
|
||||
Some of them are fixed in FF59:
|
||||
https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
const db = (() => {
|
||||
let exec;
|
||||
const preparing = prepare();
|
||||
return {
|
||||
exec: (...args) =>
|
||||
preparing.then(() => exec(...args))
|
||||
};
|
||||
|
||||
function prepare() {
|
||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||
// for reliablility and in localStorage for fast synchronous access
|
||||
// (FF may block localStorage depending on its privacy options)
|
||||
|
||||
// test localStorage
|
||||
const fallbackSet = localStorage.dbInChromeStorage;
|
||||
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
||||
useChromeStorage();
|
||||
return Promise.resolve();
|
||||
}
|
||||
if (fallbackSet === 'false') {
|
||||
useIndexedDB();
|
||||
return Promise.resolve();
|
||||
}
|
||||
// test storage.local
|
||||
return chromeLocal.get('dbInChromeStorage')
|
||||
.then(data =>
|
||||
data && data.dbInChromeStorage && Promise.reject())
|
||||
.then(() =>
|
||||
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
||||
Promise.reject())
|
||||
.then(({target}) => (
|
||||
(target.result || [])[0] ?
|
||||
Promise.reject('ok') :
|
||||
dbExecIndexedDB('put', {id: -1})))
|
||||
.then(() =>
|
||||
dbExecIndexedDB('get', -1))
|
||||
.then(({target}) => (
|
||||
(target.result || {}).id === -1 ?
|
||||
dbExecIndexedDB('delete', -1) :
|
||||
Promise.reject()))
|
||||
.then(() =>
|
||||
Promise.reject('ok'))
|
||||
.catch(result => {
|
||||
if (result === 'ok') {
|
||||
useIndexedDB();
|
||||
} else {
|
||||
useChromeStorage();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function useChromeStorage() {
|
||||
exec = dbExecChromeStorage;
|
||||
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
|
||||
localStorage.dbInChromeStorage = 'true';
|
||||
}
|
||||
|
||||
function useIndexedDB() {
|
||||
exec = dbExecIndexedDB;
|
||||
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError);
|
||||
localStorage.dbInChromeStorage = 'false';
|
||||
}
|
||||
|
||||
function dbExecIndexedDB(method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Object.assign(indexedDB.open('stylish', 2), {
|
||||
onsuccess(event) {
|
||||
const database = event.target.result;
|
||||
if (!method) {
|
||||
resolve(database);
|
||||
} else {
|
||||
const transaction = database.transaction(['styles'], 'readwrite');
|
||||
const store = transaction.objectStore('styles');
|
||||
try {
|
||||
Object.assign(store[method](...args), {
|
||||
onsuccess: event => resolve(event, store, transaction, database),
|
||||
onerror: reject,
|
||||
});
|
||||
} catch (err) {
|
||||
reject(err);
|
||||
}
|
||||
}
|
||||
},
|
||||
onerror(event) {
|
||||
console.warn(event.target.error || event.target.errorCode);
|
||||
reject(event);
|
||||
},
|
||||
onupgradeneeded(event) {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore('styles', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function dbExecChromeStorage(method, data) {
|
||||
const STYLE_KEY_PREFIX = 'style-';
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
||||
.then(result => ({target: {result}}));
|
||||
|
||||
case 'put':
|
||||
if (!data.id) {
|
||||
return getAllStyles().then(styles => {
|
||||
data.id = 1;
|
||||
for (const style of styles) {
|
||||
data.id = Math.max(data.id, style.id + 1);
|
||||
}
|
||||
return dbExecChromeStorage('put', data);
|
||||
});
|
||||
}
|
||||
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
||||
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
||||
|
||||
case 'delete':
|
||||
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
||||
|
||||
case 'getAll':
|
||||
return getAllStyles()
|
||||
.then(styles => ({target: {result: styles}}));
|
||||
}
|
||||
return Promise.reject();
|
||||
|
||||
function getAllStyles() {
|
||||
return chromeLocal.get(null).then(storage => {
|
||||
const styles = [];
|
||||
for (const key in storage) {
|
||||
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
||||
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
||||
styles.push(storage[key]);
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
});
|
||||
}
|
||||
}
|
||||
})();
|
91
background/icon-util.js
Normal file
91
background/icon-util.js
Normal file
|
@ -0,0 +1,91 @@
|
|||
/* global ignoreChromeError */
|
||||
/* exported iconUtil */
|
||||
'use strict';
|
||||
|
||||
const iconUtil = (() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d');
|
||||
// https://github.com/openstyles/stylus/issues/335
|
||||
let noCanvas;
|
||||
const imageDataCache = new Map();
|
||||
// test if canvas is usable
|
||||
const canvasReady = loadImage('/images/icon/16.png')
|
||||
.then(imageData => {
|
||||
noCanvas = imageData.data.every(b => b === 255);
|
||||
});
|
||||
|
||||
return extendNative({
|
||||
/*
|
||||
Cache imageData for paths
|
||||
*/
|
||||
setIcon,
|
||||
setBadgeText
|
||||
});
|
||||
|
||||
function loadImage(url) {
|
||||
let result = imageDataCache.get(url);
|
||||
if (!result) {
|
||||
result = new Promise((resolve, reject) => {
|
||||
const img = new Image();
|
||||
img.src = url;
|
||||
img.onload = () => {
|
||||
const w = canvas.width = img.width;
|
||||
const h = canvas.height = img.height;
|
||||
ctx.clearRect(0, 0, w, h);
|
||||
ctx.drawImage(img, 0, 0, w, h);
|
||||
resolve(ctx.getImageData(0, 0, w, h));
|
||||
};
|
||||
img.onerror = reject;
|
||||
});
|
||||
imageDataCache.set(url, result);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setIcon(data) {
|
||||
canvasReady.then(() => {
|
||||
if (noCanvas) {
|
||||
chrome.browserAction.setIcon(data, ignoreChromeError);
|
||||
return;
|
||||
}
|
||||
const pending = [];
|
||||
data.imageData = {};
|
||||
for (const [key, url] of Object.entries(data.path)) {
|
||||
pending.push(loadImage(url)
|
||||
.then(imageData => {
|
||||
data.imageData[key] = imageData;
|
||||
}));
|
||||
}
|
||||
Promise.all(pending).then(() => {
|
||||
delete data.path;
|
||||
chrome.browserAction.setIcon(data, ignoreChromeError);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function setBadgeText(data) {
|
||||
try {
|
||||
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
|
||||
chrome.browserAction.setBadgeText(data, ignoreChromeError);
|
||||
} catch (e) {
|
||||
// FIXME: skip pre-rendered tabs?
|
||||
chrome.browserAction.setBadgeText(data);
|
||||
}
|
||||
}
|
||||
|
||||
function extendNative(target) {
|
||||
return new Proxy(target, {
|
||||
get: (target, prop) => {
|
||||
// FIXME: do we really need this?
|
||||
if (!chrome.browserAction ||
|
||||
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
|
||||
return () => {};
|
||||
}
|
||||
if (target[prop]) {
|
||||
return target[prop];
|
||||
}
|
||||
return chrome.browserAction[prop].bind(chrome.browserAction);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
75
background/navigator-util.js
Normal file
75
background/navigator-util.js
Normal file
|
@ -0,0 +1,75 @@
|
|||
/* global promisify CHROME URLS */
|
||||
/* exported navigatorUtil */
|
||||
'use strict';
|
||||
|
||||
const navigatorUtil = (() => {
|
||||
const handler = {
|
||||
urlChange: null
|
||||
};
|
||||
const tabGet = promisify(chrome.tabs.get.bind(chrome.tabs));
|
||||
return extendNative({onUrlChange});
|
||||
|
||||
function onUrlChange(fn) {
|
||||
initUrlChange();
|
||||
handler.urlChange.push(fn);
|
||||
}
|
||||
|
||||
function initUrlChange() {
|
||||
if (handler.urlChange) {
|
||||
return;
|
||||
}
|
||||
handler.urlChange = [];
|
||||
|
||||
chrome.webNavigation.onCommitted.addListener(data =>
|
||||
fixNTPUrl(data)
|
||||
.then(() => executeCallbacks(handler.urlChange, data, 'committed'))
|
||||
.catch(console.error)
|
||||
);
|
||||
|
||||
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
|
||||
fixNTPUrl(data)
|
||||
.then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated'))
|
||||
.catch(console.error)
|
||||
);
|
||||
|
||||
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
|
||||
fixNTPUrl(data)
|
||||
.then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated'))
|
||||
.catch(console.error)
|
||||
);
|
||||
}
|
||||
|
||||
function fixNTPUrl(data) {
|
||||
if (
|
||||
!CHROME ||
|
||||
!URLS.chromeProtectsNTP ||
|
||||
!data.url.startsWith('https://www.google.') ||
|
||||
!data.url.includes('/_/chrome/newtab?')
|
||||
) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return tabGet(data.tabId)
|
||||
.then(tab => {
|
||||
if (tab.url === 'chrome://newtab/') {
|
||||
data.url = tab.url;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function executeCallbacks(callbacks, data, type) {
|
||||
for (const cb of callbacks) {
|
||||
cb(data, type);
|
||||
}
|
||||
}
|
||||
|
||||
function extendNative(target) {
|
||||
return new Proxy(target, {
|
||||
get: (target, prop) => {
|
||||
if (target[prop]) {
|
||||
return target[prop];
|
||||
}
|
||||
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -1,9 +0,0 @@
|
|||
/* global importScripts parserlib CSSLint parseMozFormat */
|
||||
'use strict';
|
||||
|
||||
importScripts('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
|
||||
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
|
||||
|
||||
self.onmessage = ({data}) => {
|
||||
self.postMessage(parseMozFormat(data));
|
||||
};
|
|
@ -1,226 +0,0 @@
|
|||
/*
|
||||
global API_METHODS cachedStyles
|
||||
global getStyles filterStyles invalidateCache normalizeStyleSections
|
||||
global updateIcon
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
const previewFromTabs = new Map();
|
||||
|
||||
/**
|
||||
* When style id and state is provided, only that style is propagated.
|
||||
* Otherwise all styles are replaced and the toolbar icon is updated.
|
||||
* @param {Object} [msg]
|
||||
* @param {{id:Number, enabled?:Boolean, sections?: (Array|String)}} [msg.style] -
|
||||
* style to propagate
|
||||
* @param {Boolean} [msg.codeIsUpdated]
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
API_METHODS.refreshAllTabs = (msg = {}) =>
|
||||
Promise.all([
|
||||
queryTabs(),
|
||||
maybeParseUsercss(msg),
|
||||
getStyles(),
|
||||
]).then(([tabs, style]) =>
|
||||
new Promise(resolve => {
|
||||
if (style) msg.style.sections = normalizeStyleSections(style);
|
||||
run(tabs, msg, resolve);
|
||||
}));
|
||||
|
||||
|
||||
function run(tabs, msg, resolve) {
|
||||
const {style, codeIsUpdated, refreshOwnTabs} = msg;
|
||||
|
||||
// the style was updated/saved so we need to remove the old copy of the original style
|
||||
if (msg.method === 'styleUpdated' && msg.reason !== 'editPreview') {
|
||||
for (const [tabId, original] of previewFromTabs.entries()) {
|
||||
if (style.id === original.id) {
|
||||
previewFromTabs.delete(tabId);
|
||||
}
|
||||
}
|
||||
if (!previewFromTabs.size) {
|
||||
unregisterTabListeners();
|
||||
}
|
||||
}
|
||||
|
||||
if (!style) {
|
||||
msg = {method: 'styleReplaceAll'};
|
||||
|
||||
// live preview puts the code in cachedStyles, saves the original in previewFromTabs,
|
||||
// and if preview is being disabled, but the style is already deleted, we bail out
|
||||
} else if (msg.reason === 'editPreview' && !updateCache(msg)) {
|
||||
return;
|
||||
|
||||
// simple style update:
|
||||
// * if disabled, apply.js will remove the element
|
||||
// * if toggled and code is unchanged, apply.js will toggle the element
|
||||
} else if (!style.enabled || codeIsUpdated === false) {
|
||||
msg = {
|
||||
method: 'styleUpdated',
|
||||
reason: msg.reason,
|
||||
style: {
|
||||
id: style.id,
|
||||
enabled: style.enabled,
|
||||
},
|
||||
codeIsUpdated,
|
||||
};
|
||||
|
||||
// live preview normal operation, the new code is already in cachedStyles
|
||||
} else {
|
||||
msg.method = 'styleApply';
|
||||
msg.style = {id: msg.style.id};
|
||||
}
|
||||
|
||||
if (!tabs || !tabs.length) {
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
const last = tabs[tabs.length - 1];
|
||||
for (const tab of tabs) {
|
||||
if (FIREFOX && !tab.width) continue;
|
||||
if (refreshOwnTabs === false && tab.url.startsWith(URLS.ownOrigin)) continue;
|
||||
chrome.webNavigation.getAllFrames({tabId: tab.id}, frames =>
|
||||
refreshFrame(tab, frames, msg, tab === last && resolve));
|
||||
}
|
||||
}
|
||||
|
||||
function refreshFrame(tab, frames, msg, resolve) {
|
||||
ignoreChromeError();
|
||||
if (!frames || !frames.length) {
|
||||
frames = [{
|
||||
frameId: 0,
|
||||
url: tab.url,
|
||||
}];
|
||||
}
|
||||
msg.tabId = tab.id;
|
||||
const styleId = msg.style && msg.style.id;
|
||||
|
||||
for (const frame of frames) {
|
||||
|
||||
const styles = filterStyles({
|
||||
matchUrl: getFrameUrl(frame, frames),
|
||||
asHash: true,
|
||||
id: styleId,
|
||||
});
|
||||
|
||||
msg = Object.assign({}, msg);
|
||||
msg.frameId = frame.frameId;
|
||||
|
||||
if (msg.method !== 'styleUpdated') {
|
||||
msg.styles = styles;
|
||||
}
|
||||
|
||||
if (msg.method === 'styleApply' && !styles.length) {
|
||||
// remove the style from a previously matching frame
|
||||
invokeOrPostpone(tab.active, sendMessage, {
|
||||
method: 'styleUpdated',
|
||||
reason: 'editPreview',
|
||||
style: {
|
||||
id: styleId,
|
||||
enabled: false,
|
||||
},
|
||||
tabId: tab.id,
|
||||
frameId: frame.frameId,
|
||||
}, ignoreChromeError);
|
||||
} else {
|
||||
invokeOrPostpone(tab.active, sendMessage, msg, ignoreChromeError);
|
||||
}
|
||||
|
||||
if (!frame.frameId) {
|
||||
setTimeout(updateIcon, 0, {
|
||||
tab,
|
||||
styles: msg.method === 'styleReplaceAll' ? styles : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
if (resolve) resolve();
|
||||
}
|
||||
|
||||
|
||||
function getFrameUrl(frame, frames) {
|
||||
while (frame.url === 'about:blank' && frame.frameId > 0) {
|
||||
const parent = frames.find(f => f.frameId === frame.parentFrameId);
|
||||
if (!parent) break;
|
||||
frame.url = parent.url;
|
||||
frame = parent;
|
||||
}
|
||||
return (frame || frames[0]).url;
|
||||
}
|
||||
|
||||
|
||||
function maybeParseUsercss({style}) {
|
||||
if (style && typeof style.sections === 'string') {
|
||||
return API_METHODS.parseUsercss({sourceCode: style.sections});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function updateCache(msg) {
|
||||
const {style, tabId, restoring} = msg;
|
||||
const spoofed = !restoring && previewFromTabs.get(tabId);
|
||||
const original = cachedStyles.byId.get(style.id);
|
||||
|
||||
if (style.sections && !restoring) {
|
||||
if (!previewFromTabs.size) {
|
||||
registerTabListeners();
|
||||
}
|
||||
if (!spoofed) {
|
||||
previewFromTabs.set(tabId, Object.assign({}, original));
|
||||
}
|
||||
|
||||
} else {
|
||||
previewFromTabs.delete(tabId);
|
||||
if (!previewFromTabs.size) {
|
||||
unregisterTabListeners();
|
||||
}
|
||||
if (!original) {
|
||||
return;
|
||||
}
|
||||
if (!restoring) {
|
||||
msg.style = spoofed || original;
|
||||
}
|
||||
}
|
||||
invalidateCache({updated: msg.style});
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
function registerTabListeners() {
|
||||
chrome.tabs.onRemoved.addListener(onTabRemoved);
|
||||
chrome.tabs.onReplaced.addListener(onTabReplaced);
|
||||
chrome.webNavigation.onCommitted.addListener(onTabNavigated);
|
||||
}
|
||||
|
||||
|
||||
function unregisterTabListeners() {
|
||||
chrome.tabs.onRemoved.removeListener(onTabRemoved);
|
||||
chrome.tabs.onReplaced.removeListener(onTabReplaced);
|
||||
chrome.webNavigation.onCommitted.removeListener(onTabNavigated);
|
||||
}
|
||||
|
||||
|
||||
function onTabRemoved(tabId) {
|
||||
const style = previewFromTabs.get(tabId);
|
||||
if (style) {
|
||||
API_METHODS.refreshAllTabs({
|
||||
style,
|
||||
tabId,
|
||||
reason: 'editPreview',
|
||||
restoring: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function onTabReplaced(addedTabId, removedTabId) {
|
||||
onTabRemoved(removedTabId);
|
||||
}
|
||||
|
||||
|
||||
function onTabNavigated({tabId}) {
|
||||
onTabRemoved(tabId);
|
||||
}
|
||||
})();
|
|
@ -1,4 +1,4 @@
|
|||
/* global API_METHODS filterStyles cachedStyles */
|
||||
/* global API_METHODS styleManager tryRegExp debounce */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
@ -25,7 +25,8 @@
|
|||
if (/^url:/i.test(query)) {
|
||||
matchUrl = query.slice(query.indexOf(':') + 1).trim();
|
||||
if (matchUrl) {
|
||||
return filterStyles({matchUrl}).map(style => style.id);
|
||||
return styleManager.getStylesByUrl(matchUrl)
|
||||
.then(results => results.map(r => r.data.id));
|
||||
}
|
||||
}
|
||||
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
|
||||
|
@ -43,15 +44,18 @@
|
|||
icase = words.some(w => w === lower(w));
|
||||
}
|
||||
|
||||
return styleManager.getAllStyles().then(styles => {
|
||||
if (ids) {
|
||||
const idSet = new Set(ids);
|
||||
styles = styles.filter(s => idSet.has(s.id));
|
||||
}
|
||||
const results = [];
|
||||
for (const item of ids || cachedStyles.list) {
|
||||
const id = isNaN(item) ? item.id : item;
|
||||
for (const style of styles) {
|
||||
const id = style.id;
|
||||
if (!query || words && !words.length) {
|
||||
results.push(id);
|
||||
continue;
|
||||
}
|
||||
const style = isNaN(item) ? item : cachedStyles.byId.get(item);
|
||||
if (!style) continue;
|
||||
for (const part in PARTS) {
|
||||
const text = style[part];
|
||||
if (text && PARTS[part](text, rx, words, icase)) {
|
||||
|
@ -60,9 +64,9 @@
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (cache.size) debounce(clearCache, 60e3);
|
||||
return results;
|
||||
});
|
||||
};
|
||||
|
||||
function searchText(text, rx, words, icase) {
|
||||
|
|
|
@ -1,78 +0,0 @@
|
|||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
(chrome.runtime.id.includes('@temporary') || !('sync' in chrome.storage)) && (() => {
|
||||
|
||||
const listeners = new Set();
|
||||
Object.assign(chrome.storage.onChanged, {
|
||||
addListener: fn => listeners.add(fn),
|
||||
hasListener: fn => listeners.has(fn),
|
||||
removeListener: fn => listeners.delete(fn),
|
||||
});
|
||||
|
||||
for (const name of ['local', 'sync']) {
|
||||
const dummy = tryJSONparse(localStorage['dummyStorage.' + name]) || {};
|
||||
chrome.storage[name] = {
|
||||
get(data, cb) {
|
||||
let result = {};
|
||||
if (data === null) {
|
||||
result = deepCopy(dummy);
|
||||
} else if (Array.isArray(data)) {
|
||||
for (const key of data) {
|
||||
result[key] = dummy[key];
|
||||
}
|
||||
} else if (typeof data === 'object') {
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
for (const key in data) {
|
||||
if (hasOwnProperty.call(data, key)) {
|
||||
const value = dummy[key];
|
||||
result[key] = value === undefined ? data[key] : value;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
result[data] = dummy[data];
|
||||
}
|
||||
if (typeof cb === 'function') cb(result);
|
||||
},
|
||||
set(data, cb) {
|
||||
const hasOwnProperty = Object.prototype.hasOwnProperty;
|
||||
const changes = {};
|
||||
for (const key in data) {
|
||||
if (!hasOwnProperty.call(data, key)) continue;
|
||||
const newValue = data[key];
|
||||
changes[key] = {newValue, oldValue: dummy[key]};
|
||||
dummy[key] = newValue;
|
||||
}
|
||||
localStorage['dummyStorage.' + name] = JSON.stringify(dummy);
|
||||
if (typeof cb === 'function') cb();
|
||||
notify(changes);
|
||||
},
|
||||
remove(keyOrKeys, cb) {
|
||||
const changes = {};
|
||||
for (const key of Array.isArray(keyOrKeys) ? keyOrKeys : [keyOrKeys]) {
|
||||
changes[key] = {oldValue: dummy[key]};
|
||||
delete dummy[key];
|
||||
}
|
||||
localStorage['dummyStorage.' + name] = JSON.stringify(dummy);
|
||||
if (typeof cb === 'function') cb();
|
||||
notify(changes);
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
|
||||
dummyStorageGet: ({data, name}) => new Promise(resolve => chrome.storage[name].get(data, resolve)),
|
||||
dummyStorageSet: ({data, name}) => new Promise(resolve => chrome.storage[name].set(data, resolve)),
|
||||
dummyStorageRemove: ({data, name}) => new Promise(resolve => chrome.storage[name].remove(data, resolve)),
|
||||
});
|
||||
|
||||
function notify(changes, name) {
|
||||
for (const fn of listeners.values()) {
|
||||
fn(changes, name);
|
||||
}
|
||||
sendMessage({
|
||||
dummyStorageChanges: changes,
|
||||
dummyStorageName: name,
|
||||
}, ignoreChromeError);
|
||||
}
|
||||
})();
|
|
@ -1,847 +0,0 @@
|
|||
/* global getStyleWithNoCode styleSectionsEqual */
|
||||
'use strict';
|
||||
|
||||
const RX_NAMESPACE = /\s*(@namespace\s+(?:\S+\s+)?url\(http:\/\/.*?\);)\s*/g;
|
||||
const RX_CHARSET = /\s*@charset\s+(['"]).*?\1\s*;\s*/g;
|
||||
const RX_CSS_COMMENTS = /\/\*[\s\S]*?(?:\*\/|$)/g;
|
||||
// eslint-disable-next-line no-var
|
||||
var SLOPPY_REGEXP_PREFIX = '\0';
|
||||
|
||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||
// the browsers, especially Firefox, may apply all transitions on page load
|
||||
const CSS_TRANSITION_SUPPRESSOR = '* { transition: none !important; }';
|
||||
const RX_CSS_TRANSITION_DETECTOR = /([\s\n;/{]|-webkit-|-moz-)transition[\s\n]*:[\s\n]*(?!none)/;
|
||||
|
||||
// Note, only 'var'-declared variables are visible from another extension page
|
||||
// eslint-disable-next-line no-var
|
||||
var cachedStyles = {
|
||||
list: null, // array of all styles
|
||||
byId: new Map(), // all styles indexed by id
|
||||
filters: new Map(), // filterStyles() parameters mapped to the returned results, 10k max
|
||||
regexps: new Map(), // compiled style regexps
|
||||
urlDomains: new Map(), // getDomain() results for 100 last checked urls
|
||||
needTransitionPatch: new Map(), // FF bug workaround
|
||||
mutex: {
|
||||
inProgress: true, // while getStyles() is reading IndexedDB all subsequent calls
|
||||
// (initially 'true' to prevent rogue getStyles before dbExec.initialized)
|
||||
onDone: [], // to getStyles() are queued and resolved when the first one finishes
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var dbExec = dbExecIndexedDB;
|
||||
dbExec.initialized = false;
|
||||
|
||||
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
|
||||
// which, once detected on the first run, is remembered in chrome.storage.local
|
||||
// for reliablility and in localStorage for fast synchronous access
|
||||
// (FF may block localStorage depending on its privacy options)
|
||||
do {
|
||||
const done = () => {
|
||||
cachedStyles.mutex.inProgress = false;
|
||||
getStyles().then(() => {
|
||||
dbExec.initialized = true;
|
||||
window.dispatchEvent(new Event('storageReady'));
|
||||
});
|
||||
};
|
||||
const fallback = () => {
|
||||
dbExec = dbExecChromeStorage;
|
||||
chromeLocal.set({dbInChromeStorage: true});
|
||||
localStorage.dbInChromeStorage = 'true';
|
||||
ignoreChromeError();
|
||||
done();
|
||||
};
|
||||
const fallbackSet = localStorage.dbInChromeStorage;
|
||||
if (fallbackSet === 'true' || !tryCatch(() => indexedDB)) {
|
||||
fallback();
|
||||
break;
|
||||
} else if (fallbackSet === 'false') {
|
||||
done();
|
||||
break;
|
||||
}
|
||||
chromeLocal.get('dbInChromeStorage')
|
||||
.then(data =>
|
||||
data && data.dbInChromeStorage && Promise.reject())
|
||||
.then(() =>
|
||||
tryCatch(dbExecIndexedDB, 'getAllKeys', IDBKeyRange.lowerBound(1), 1) ||
|
||||
Promise.reject())
|
||||
.then(({target}) => (
|
||||
(target.result || [])[0] ?
|
||||
Promise.reject('ok') :
|
||||
dbExecIndexedDB('put', {id: -1})))
|
||||
.then(() =>
|
||||
dbExecIndexedDB('get', -1))
|
||||
.then(({target}) => (
|
||||
(target.result || {}).id === -1 ?
|
||||
dbExecIndexedDB('delete', -1) :
|
||||
Promise.reject()))
|
||||
.then(() =>
|
||||
Promise.reject('ok'))
|
||||
.catch(result => {
|
||||
if (result === 'ok') {
|
||||
chromeLocal.set({dbInChromeStorage: false});
|
||||
localStorage.dbInChromeStorage = 'false';
|
||||
done();
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
});
|
||||
} while (0);
|
||||
|
||||
|
||||
function dbExecIndexedDB(method, ...args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
Object.assign(indexedDB.open('stylish', 2), {
|
||||
onsuccess(event) {
|
||||
const database = event.target.result;
|
||||
if (!method) {
|
||||
resolve(database);
|
||||
} else {
|
||||
const transaction = database.transaction(['styles'], 'readwrite');
|
||||
const store = transaction.objectStore('styles');
|
||||
Object.assign(store[method](...args), {
|
||||
onsuccess: event => resolve(event, store, transaction, database),
|
||||
onerror: reject,
|
||||
});
|
||||
}
|
||||
},
|
||||
onerror(event) {
|
||||
console.warn(event.target.error || event.target.errorCode);
|
||||
reject(event);
|
||||
},
|
||||
onupgradeneeded(event) {
|
||||
if (event.oldVersion === 0) {
|
||||
event.target.result.createObjectStore('styles', {
|
||||
keyPath: 'id',
|
||||
autoIncrement: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function dbExecChromeStorage(method, data) {
|
||||
const STYLE_KEY_PREFIX = 'style-';
|
||||
switch (method) {
|
||||
case 'get':
|
||||
return chromeLocal.getValue(STYLE_KEY_PREFIX + data)
|
||||
.then(result => ({target: {result}}));
|
||||
|
||||
case 'put':
|
||||
if (!data.id) {
|
||||
return getStyles().then(() => {
|
||||
data.id = 1;
|
||||
for (const style of cachedStyles.list) {
|
||||
data.id = Math.max(data.id, style.id + 1);
|
||||
}
|
||||
return dbExecChromeStorage('put', data);
|
||||
});
|
||||
}
|
||||
return chromeLocal.setValue(STYLE_KEY_PREFIX + data.id, data)
|
||||
.then(() => (chrome.runtime.lastError ? Promise.reject() : data.id));
|
||||
|
||||
case 'delete':
|
||||
return chromeLocal.remove(STYLE_KEY_PREFIX + data);
|
||||
|
||||
case 'getAll':
|
||||
return chromeLocal.get(null).then(storage => {
|
||||
const styles = [];
|
||||
for (const key in storage) {
|
||||
if (key.startsWith(STYLE_KEY_PREFIX) &&
|
||||
Number(key.substr(STYLE_KEY_PREFIX.length))) {
|
||||
styles.push(storage[key]);
|
||||
}
|
||||
}
|
||||
return {target: {result: styles}};
|
||||
});
|
||||
}
|
||||
return Promise.reject();
|
||||
}
|
||||
|
||||
|
||||
function getStyles(options) {
|
||||
if (cachedStyles.list) {
|
||||
return Promise.resolve(filterStyles(options));
|
||||
}
|
||||
if (cachedStyles.mutex.inProgress) {
|
||||
return new Promise(resolve => {
|
||||
cachedStyles.mutex.onDone.push({options, resolve});
|
||||
});
|
||||
}
|
||||
cachedStyles.mutex.inProgress = true;
|
||||
|
||||
return dbExec('getAll').then(event => {
|
||||
cachedStyles.list = event.target.result || [];
|
||||
cachedStyles.list.forEach(fixUsoMd5Issue);
|
||||
cachedStyles.byId.clear();
|
||||
for (const style of cachedStyles.list) {
|
||||
cachedStyles.byId.set(style.id, style);
|
||||
if (!style.name) {
|
||||
style.name = 'ID: ' + style.id;
|
||||
}
|
||||
}
|
||||
|
||||
cachedStyles.mutex.inProgress = false;
|
||||
for (const {options, resolve} of cachedStyles.mutex.onDone) {
|
||||
resolve(filterStyles(options));
|
||||
}
|
||||
cachedStyles.mutex.onDone = [];
|
||||
return filterStyles(options);
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function filterStyles({
|
||||
enabled = null,
|
||||
id = null,
|
||||
matchUrl = null,
|
||||
md5Url = null,
|
||||
asHash = null,
|
||||
omitCode,
|
||||
strictRegexp = true, // used by the popup to detect bad regexps
|
||||
} = {}) {
|
||||
if (id) id = Number(id);
|
||||
if (asHash) enabled = true;
|
||||
|
||||
if (
|
||||
enabled === null &&
|
||||
id === null &&
|
||||
matchUrl === null &&
|
||||
md5Url === null &&
|
||||
asHash !== true
|
||||
) {
|
||||
return cachedStyles.list;
|
||||
}
|
||||
|
||||
if (matchUrl && !URLS.supported(matchUrl)) {
|
||||
return asHash ? {length: 0} : [];
|
||||
}
|
||||
|
||||
const blankHash = asHash && {
|
||||
length: 0,
|
||||
disableAll: prefs.get('disableAll'),
|
||||
exposeIframes: prefs.get('exposeIframes'),
|
||||
};
|
||||
|
||||
// make sure to use the same order in updateFiltersCache()
|
||||
const cacheKey =
|
||||
enabled + '\t' +
|
||||
id + '\t' +
|
||||
matchUrl + '\t' +
|
||||
md5Url + '\t' +
|
||||
asHash + '\t' +
|
||||
strictRegexp;
|
||||
const cached = cachedStyles.filters.get(cacheKey);
|
||||
let styles;
|
||||
if (cached) {
|
||||
cached.hits++;
|
||||
cached.lastHit = Date.now();
|
||||
styles = asHash
|
||||
? Object.assign(blankHash, cached.styles)
|
||||
: cached.styles.slice();
|
||||
} else {
|
||||
styles = filterStylesInternal({
|
||||
enabled,
|
||||
id,
|
||||
matchUrl,
|
||||
md5Url,
|
||||
asHash,
|
||||
strictRegexp,
|
||||
blankHash,
|
||||
cacheKey,
|
||||
});
|
||||
}
|
||||
if (!omitCode) return styles;
|
||||
if (!asHash) return styles.map(getStyleWithNoCode);
|
||||
for (const id in styles) {
|
||||
const sections = styles[id];
|
||||
if (Array.isArray(sections)) {
|
||||
styles[id] = getStyleWithNoCode({sections}).sections;
|
||||
}
|
||||
}
|
||||
return styles;
|
||||
}
|
||||
|
||||
|
||||
// The md5Url provided by USO includes a duplicate "update" subdomain (see #523),
|
||||
// This fixes any already installed styles containing this error
|
||||
function fixUsoMd5Issue(style) {
|
||||
if (style && style.md5Url && style.md5Url.includes('update.update.userstyles')) {
|
||||
style.md5Url = style.md5Url.replace('update.update.userstyles', 'update.userstyles');
|
||||
}
|
||||
}
|
||||
|
||||
function filterStylesInternal({
|
||||
// js engines don't like big functions (V8 often deoptimized the original filterStyles)
|
||||
// it also makes sense to extract the less frequently executed code
|
||||
enabled,
|
||||
id,
|
||||
matchUrl,
|
||||
md5Url,
|
||||
asHash,
|
||||
strictRegexp,
|
||||
blankHash,
|
||||
cacheKey,
|
||||
}) {
|
||||
if (matchUrl && !cachedStyles.urlDomains.has(matchUrl)) {
|
||||
cachedStyles.urlDomains.set(matchUrl, getDomains(matchUrl));
|
||||
for (let i = cachedStyles.urlDomains.size - 100; i > 0; i--) {
|
||||
const firstKey = cachedStyles.urlDomains.keys().next().value;
|
||||
cachedStyles.urlDomains.delete(firstKey);
|
||||
}
|
||||
}
|
||||
|
||||
const styles = id === null
|
||||
? cachedStyles.list
|
||||
: [cachedStyles.byId.get(id)];
|
||||
if (!styles[0]) {
|
||||
// may happen when users [accidentally] reopen an old URL
|
||||
// of edit.html with a non-existent style id parameter
|
||||
return asHash ? blankHash : [];
|
||||
}
|
||||
const filtered = asHash ? {length: 0} : [];
|
||||
const needSections = asHash || matchUrl !== null;
|
||||
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
|
||||
|
||||
let style;
|
||||
for (let i = 0; (style = styles[i]); i++) {
|
||||
if ((enabled === null || style.enabled === enabled)
|
||||
&& (md5Url === null || style.md5Url === md5Url)
|
||||
&& (id === null || style.id === id)) {
|
||||
const sections = needSections &&
|
||||
getApplicableSections({
|
||||
style,
|
||||
matchUrl,
|
||||
strictRegexp,
|
||||
stopOnFirst: !asHash,
|
||||
skipUrlCheck: true,
|
||||
matchUrlBase,
|
||||
});
|
||||
if (asHash) {
|
||||
if (sections.length) {
|
||||
filtered[style.id] = sections;
|
||||
filtered.length++;
|
||||
}
|
||||
} else if (matchUrl === null || sections.length) {
|
||||
filtered.push(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cachedStyles.filters.set(cacheKey, {
|
||||
styles: filtered,
|
||||
lastHit: Date.now(),
|
||||
hits: 1,
|
||||
});
|
||||
if (cachedStyles.filters.size > 10000) {
|
||||
cleanupCachedFilters();
|
||||
}
|
||||
|
||||
// a shallow copy is needed because the cache doesn't store options like disableAll
|
||||
return asHash
|
||||
? Object.assign(blankHash, filtered)
|
||||
: filtered;
|
||||
}
|
||||
|
||||
|
||||
function saveStyle(style) {
|
||||
const id = Number(style.id) || null;
|
||||
const reason = style.reason;
|
||||
const notify = style.notify !== false;
|
||||
delete style.method;
|
||||
delete style.reason;
|
||||
delete style.notify;
|
||||
if (!style.name) {
|
||||
delete style.name;
|
||||
}
|
||||
let existed;
|
||||
let codeIsUpdated;
|
||||
|
||||
fixUsoMd5Issue(style);
|
||||
|
||||
return maybeCalcDigest()
|
||||
.then(maybeImportFix)
|
||||
.then(decide);
|
||||
|
||||
function maybeCalcDigest() {
|
||||
if (['install', 'update', 'update-digest'].includes(reason)) {
|
||||
return calcStyleDigest(style).then(digest => {
|
||||
style.originalDigest = digest;
|
||||
});
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
function maybeImportFix() {
|
||||
if (reason === 'import') {
|
||||
style.originalDigest = style.originalDigest || style.styleDigest; // TODO: remove in the future
|
||||
delete style.styleDigest; // TODO: remove in the future
|
||||
if (typeof style.originalDigest !== 'string' || style.originalDigest.length !== 40) {
|
||||
delete style.originalDigest;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function decide() {
|
||||
if (id !== null) {
|
||||
// Update or create
|
||||
style.id = id;
|
||||
return dbExec('get', id).then((event, store) => {
|
||||
const oldStyle = event.target.result;
|
||||
existed = Boolean(oldStyle);
|
||||
if (reason === 'update-digest' && oldStyle.originalDigest === style.originalDigest) {
|
||||
return style;
|
||||
}
|
||||
codeIsUpdated = !existed || 'sections' in style && !styleSectionsEqual(style, oldStyle);
|
||||
style = Object.assign({installDate: Date.now()}, oldStyle, style);
|
||||
return write(style, store);
|
||||
});
|
||||
} else {
|
||||
// Create
|
||||
delete style.id;
|
||||
style = Object.assign({
|
||||
// Set optional things if they're undefined
|
||||
enabled: true,
|
||||
updateUrl: null,
|
||||
md5Url: null,
|
||||
url: null,
|
||||
originalMd5: null,
|
||||
installDate: Date.now(),
|
||||
}, style);
|
||||
return write(style);
|
||||
}
|
||||
}
|
||||
|
||||
function write(style, store) {
|
||||
style.sections = normalizeStyleSections(style);
|
||||
if (store) {
|
||||
return new Promise(resolve => {
|
||||
store.put(style).onsuccess = event => resolve(done(event));
|
||||
});
|
||||
} else {
|
||||
return dbExec('put', style).then(done);
|
||||
}
|
||||
}
|
||||
|
||||
function done(event) {
|
||||
if (reason === 'update-digest') {
|
||||
return style;
|
||||
}
|
||||
style.id = style.id || event.target.result;
|
||||
invalidateCache(existed ? {updated: style} : {added: style});
|
||||
if (notify) {
|
||||
notifyAllTabs({
|
||||
method: existed ? 'styleUpdated' : 'styleAdded',
|
||||
style, codeIsUpdated, reason,
|
||||
});
|
||||
}
|
||||
return style;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function deleteStyle({id, notify = true}) {
|
||||
id = Number(id);
|
||||
return dbExec('delete', id).then(() => {
|
||||
invalidateCache({deletedId: id});
|
||||
if (notify) {
|
||||
notifyAllTabs({method: 'styleDeleted', id});
|
||||
}
|
||||
return id;
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getApplicableSections({
|
||||
style,
|
||||
matchUrl,
|
||||
strictRegexp = true,
|
||||
// filterStylesInternal() sets the following to avoid recalc on each style:
|
||||
stopOnFirst,
|
||||
skipUrlCheck,
|
||||
matchUrlBase = matchUrl.includes('#') && matchUrl.split('#', 1)[0],
|
||||
// as per spec the fragment portion is ignored in @-moz-document:
|
||||
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
||||
// but the spec is outdated and doesn't account for SPA sites
|
||||
// so we only respect it in case of url("http://exact.url/without/hash")
|
||||
}) {
|
||||
if (!skipUrlCheck && !URLS.supported(matchUrl)) {
|
||||
return [];
|
||||
}
|
||||
const sections = [];
|
||||
for (const section of style.sections) {
|
||||
const {urls, domains, urlPrefixes, regexps, code} = section;
|
||||
const isGlobal = !urls.length && !urlPrefixes.length && !domains.length && !regexps.length;
|
||||
const isMatching = !isGlobal && (
|
||||
urls.length
|
||||
&& (urls.includes(matchUrl) || matchUrlBase && urls.includes(matchUrlBase))
|
||||
|| urlPrefixes.length
|
||||
&& arraySomeIsPrefix(urlPrefixes, matchUrl)
|
||||
|| domains.length
|
||||
&& arraySomeIn(cachedStyles.urlDomains.get(matchUrl) || getDomains(matchUrl), domains)
|
||||
|| regexps.length
|
||||
&& arraySomeMatches(regexps, matchUrl, strictRegexp));
|
||||
if (isGlobal && !styleCodeEmpty(code) || isMatching) {
|
||||
sections.push(section);
|
||||
if (stopOnFirst) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return sections;
|
||||
|
||||
function arraySomeIsPrefix(array, string) {
|
||||
for (const prefix of array) {
|
||||
if (string.startsWith(prefix)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function arraySomeIn(array, haystack) {
|
||||
for (const el of array) {
|
||||
if (haystack.indexOf(el) >= 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function arraySomeMatches(array, matchUrl, strictRegexp) {
|
||||
for (const regexp of array) {
|
||||
for (let pass = 1; pass <= (strictRegexp ? 1 : 2); pass++) {
|
||||
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
||||
let rx = cachedStyles.regexps.get(cacheKey);
|
||||
if (rx === false) {
|
||||
// invalid regexp
|
||||
break;
|
||||
}
|
||||
if (!rx) {
|
||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
rx = tryRegExp(anchored);
|
||||
cachedStyles.regexps.set(cacheKey, rx || false);
|
||||
if (!rx) {
|
||||
// invalid regexp
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (rx.test(matchUrl)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function styleCodeEmpty(code) {
|
||||
// Collect the global section if it's not empty, not comment-only, not namespace-only.
|
||||
const cmtOpen = code && code.indexOf('/*');
|
||||
if (cmtOpen >= 0) {
|
||||
const cmtCloseLast = code.lastIndexOf('*/');
|
||||
if (cmtCloseLast < 0) {
|
||||
code = code.substr(0, cmtOpen);
|
||||
} else {
|
||||
code = code.substr(0, cmtOpen) +
|
||||
code.substring(cmtOpen, cmtCloseLast + 2).replace(RX_CSS_COMMENTS, '') +
|
||||
code.substr(cmtCloseLast + 2);
|
||||
}
|
||||
}
|
||||
if (!code || !code.trim()) return true;
|
||||
if (code.includes('@namespace')) code = code.replace(RX_NAMESPACE, '').trim();
|
||||
if (code.includes('@charset')) code = code.replace(RX_CHARSET, '').trim();
|
||||
return !code;
|
||||
}
|
||||
|
||||
|
||||
function invalidateCache({added, updated, deletedId} = {}) {
|
||||
if (!cachedStyles.list) return;
|
||||
const id = added ? added.id : updated ? updated.id : deletedId;
|
||||
const cached = cachedStyles.byId.get(id);
|
||||
|
||||
if (updated) {
|
||||
if (cached) {
|
||||
const isSectionGlobal = section =>
|
||||
!section.urls.length &&
|
||||
!section.urlPrefixes.length &&
|
||||
!section.domains.length &&
|
||||
!section.regexps.length;
|
||||
const hadOrHasGlobals = cached.sections.some(isSectionGlobal) ||
|
||||
updated.sections.some(isSectionGlobal);
|
||||
const reenabled = !cached.enabled && updated.enabled;
|
||||
const equal = !hadOrHasGlobals &&
|
||||
!reenabled &&
|
||||
styleSectionsEqual(updated, cached, {ignoreCode: true});
|
||||
Object.assign(cached, updated);
|
||||
if (equal) {
|
||||
updateFiltersCache(cached);
|
||||
} else {
|
||||
cachedStyles.filters.clear();
|
||||
}
|
||||
cachedStyles.needTransitionPatch.delete(id);
|
||||
return;
|
||||
} else {
|
||||
added = updated;
|
||||
}
|
||||
}
|
||||
|
||||
if (added) {
|
||||
if (!cached) {
|
||||
cachedStyles.list.push(added);
|
||||
cachedStyles.byId.set(added.id, added);
|
||||
cachedStyles.filters.clear();
|
||||
cachedStyles.needTransitionPatch.delete(id);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (deletedId !== undefined) {
|
||||
if (cached) {
|
||||
const cachedIndex = cachedStyles.list.indexOf(cached);
|
||||
cachedStyles.list.splice(cachedIndex, 1);
|
||||
cachedStyles.byId.delete(deletedId);
|
||||
for (const {styles} of cachedStyles.filters.values()) {
|
||||
if (Array.isArray(styles)) {
|
||||
const index = styles.findIndex(({id}) => id === deletedId);
|
||||
if (index >= 0) styles.splice(index, 1);
|
||||
} else if (deletedId in styles) {
|
||||
delete styles[deletedId];
|
||||
styles.length--;
|
||||
}
|
||||
}
|
||||
cachedStyles.needTransitionPatch.delete(id);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
cachedStyles.list = null;
|
||||
cachedStyles.filters.clear();
|
||||
cachedStyles.needTransitionPatch.clear(id);
|
||||
}
|
||||
|
||||
|
||||
function updateFiltersCache(style) {
|
||||
const {id} = style;
|
||||
for (const [key, {styles}] of cachedStyles.filters.entries()) {
|
||||
if (Array.isArray(styles)) {
|
||||
const index = styles.findIndex(style => style.id === id);
|
||||
if (index >= 0) styles[index] = Object.assign({}, style);
|
||||
continue;
|
||||
}
|
||||
if (id in styles) {
|
||||
const [, , matchUrl, , , strictRegexp] = key.split('\t');
|
||||
if (!style.enabled) {
|
||||
delete styles[id];
|
||||
styles.length--;
|
||||
continue;
|
||||
}
|
||||
const matchUrlBase = matchUrl && matchUrl.includes('#') && matchUrl.split('#', 1)[0];
|
||||
const sections = getApplicableSections({
|
||||
style,
|
||||
matchUrl,
|
||||
matchUrlBase,
|
||||
strictRegexp,
|
||||
skipUrlCheck: true,
|
||||
});
|
||||
if (sections.length) {
|
||||
styles[id] = sections;
|
||||
} else {
|
||||
delete styles[id];
|
||||
styles.length--;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function cleanupCachedFilters({force = false} = {}) {
|
||||
if (!force) {
|
||||
debounce(cleanupCachedFilters, 1000, {force: true});
|
||||
return;
|
||||
}
|
||||
const size = cachedStyles.filters.size;
|
||||
const oldestHit = cachedStyles.filters.values().next().value.lastHit;
|
||||
const now = Date.now();
|
||||
const timeSpan = now - oldestHit;
|
||||
const recencyWeight = 5 / size;
|
||||
const hitWeight = 1 / 4; // we make ~4 hits per URL
|
||||
const lastHitWeight = 10;
|
||||
// delete the oldest 10%
|
||||
[...cachedStyles.filters.entries()]
|
||||
.map(([id, v], index) => ({
|
||||
id,
|
||||
weight:
|
||||
index * recencyWeight +
|
||||
v.hits * hitWeight +
|
||||
(v.lastHit - oldestHit) / timeSpan * lastHitWeight,
|
||||
}))
|
||||
.sort((a, b) => a.weight - b.weight)
|
||||
.slice(0, size / 10 + 1)
|
||||
.forEach(({id}) => cachedStyles.filters.delete(id));
|
||||
}
|
||||
|
||||
|
||||
function getDomains(url) {
|
||||
let d = /.*?:\/*([^/:]+)|$/.exec(url)[1];
|
||||
if (!d || url.startsWith('file:')) {
|
||||
return [];
|
||||
}
|
||||
const domains = [d];
|
||||
while (d.indexOf('.') !== -1) {
|
||||
d = d.substring(d.indexOf('.') + 1);
|
||||
domains.push(d);
|
||||
}
|
||||
return domains;
|
||||
}
|
||||
|
||||
|
||||
function normalizeStyleSections({sections}) {
|
||||
// retain known properties in an arbitrarily predefined order
|
||||
return (sections || []).map(section => ({
|
||||
code: section.code || '',
|
||||
urls: section.urls || [],
|
||||
urlPrefixes: section.urlPrefixes || [],
|
||||
domains: section.domains || [],
|
||||
regexps: section.regexps || [],
|
||||
}));
|
||||
}
|
||||
|
||||
|
||||
function calcStyleDigest(style) {
|
||||
const jsonString = style.usercssData ?
|
||||
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
|
||||
const text = new TextEncoder('utf-8').encode(jsonString);
|
||||
return crypto.subtle.digest('SHA-1', text).then(hex);
|
||||
|
||||
function hex(buffer) {
|
||||
const parts = [];
|
||||
const PAD8 = '00000000';
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < view.byteLength; i += 4) {
|
||||
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function handleCssTransitionBug({tabId, frameId, url, styles}) {
|
||||
for (let id in styles) {
|
||||
id |= 0;
|
||||
if (!id) {
|
||||
continue;
|
||||
}
|
||||
let need = cachedStyles.needTransitionPatch.get(id);
|
||||
if (need === false) {
|
||||
continue;
|
||||
}
|
||||
if (need !== true) {
|
||||
need = styles[id].some(sectionContainsTransitions);
|
||||
cachedStyles.needTransitionPatch.set(id, need);
|
||||
if (!need) {
|
||||
continue;
|
||||
}
|
||||
}
|
||||
if (FIREFOX && !url.startsWith(URLS.ownOrigin)) {
|
||||
patchFirefox();
|
||||
} else {
|
||||
styles.needTransitionPatch = true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
function patchFirefox() {
|
||||
const options = {
|
||||
frameId,
|
||||
code: CSS_TRANSITION_SUPPRESSOR,
|
||||
matchAboutBlank: true,
|
||||
};
|
||||
if (FIREFOX >= 53) {
|
||||
options.cssOrigin = 'user';
|
||||
}
|
||||
browser.tabs.insertCSS(tabId, Object.assign(options, {
|
||||
runAt: 'document_start',
|
||||
})).then(() => setTimeout(() => {
|
||||
browser.tabs.removeCSS(tabId, options).catch(ignoreChromeError);
|
||||
})).catch(ignoreChromeError);
|
||||
}
|
||||
|
||||
function sectionContainsTransitions(section) {
|
||||
let code = section.code;
|
||||
const firstTransition = code.indexOf('transition');
|
||||
if (firstTransition < 0) {
|
||||
return false;
|
||||
}
|
||||
const firstCmt = code.indexOf('/*');
|
||||
// check the part before the first comment
|
||||
if (firstCmt < 0 || firstTransition < firstCmt) {
|
||||
if (quickCheckAround(code, firstTransition)) {
|
||||
return true;
|
||||
} else if (firstCmt < 0) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
// check the rest
|
||||
const lastCmt = code.lastIndexOf('*/');
|
||||
if (lastCmt < firstCmt) {
|
||||
// the comment is unclosed and we already checked the preceding part
|
||||
return false;
|
||||
}
|
||||
let mid = code.slice(firstCmt, lastCmt + 2);
|
||||
mid = mid.indexOf('*/') === mid.length - 2 ? '' : mid.replace(RX_CSS_COMMENTS, '');
|
||||
code = mid + code.slice(lastCmt + 2);
|
||||
return quickCheckAround(code) || RX_CSS_TRANSITION_DETECTOR.test(code);
|
||||
}
|
||||
|
||||
function quickCheckAround(code, pos = code.indexOf('transition')) {
|
||||
return RX_CSS_TRANSITION_DETECTOR.test(code.substr(Math.max(0, pos - 10), 50));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/*
|
||||
According to CSS4 @document specification the entire URL must match.
|
||||
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
||||
We'll detect styles that abuse the bug by finding the sections that
|
||||
would have been applied by Stylish but not by us as we follow the spec.
|
||||
Additionally we'll check for invalid regexps.
|
||||
*/
|
||||
function detectSloppyRegexps({matchUrl, ids}) {
|
||||
const results = [];
|
||||
for (const id of ids) {
|
||||
const style = cachedStyles.byId.get(id);
|
||||
if (!style) continue;
|
||||
// make sure all regexps are compiled
|
||||
const rxCache = cachedStyles.regexps;
|
||||
let hasRegExp = false;
|
||||
for (const section of style.sections) {
|
||||
for (const regexp of section.regexps) {
|
||||
hasRegExp = true;
|
||||
for (let pass = 1; pass <= 2; pass++) {
|
||||
const cacheKey = pass === 1 ? regexp : SLOPPY_REGEXP_PREFIX + regexp;
|
||||
if (!rxCache.has(cacheKey)) {
|
||||
// according to CSS4 @document specification the entire URL must match
|
||||
const anchored = pass === 1 ? '^(?:' + regexp + ')$' : '^' + regexp + '$';
|
||||
// create in the bg context to avoid leaking of "dead objects"
|
||||
const rx = tryRegExp(anchored);
|
||||
rxCache.set(cacheKey, rx || false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!hasRegExp) continue;
|
||||
const applied = getApplicableSections({style, matchUrl});
|
||||
const wannabe = getApplicableSections({style, matchUrl, strictRegexp: false});
|
||||
results.push({
|
||||
id,
|
||||
applied,
|
||||
skipped: wannabe.length - applied.length,
|
||||
hasInvalidRegexps: wannabe.some(({regexps}) => regexps.some(rx => !rxCache.has(rx))),
|
||||
});
|
||||
}
|
||||
return results;
|
||||
}
|
492
background/style-manager.js
Normal file
492
background/style-manager.js
Normal file
|
@ -0,0 +1,492 @@
|
|||
/* eslint no-eq-null: 0, eqeqeq: [2, "smart"] */
|
||||
/* global createCache db calcStyleDigest db tryRegExp styleCodeEmpty
|
||||
getStyleWithNoCode msg */
|
||||
/* exported styleManager */
|
||||
'use strict';
|
||||
|
||||
/*
|
||||
This style manager is a layer between content script and the DB. When a style
|
||||
is added/updated, it broadcast a message to content script and the content
|
||||
script would try to fetch the new code.
|
||||
|
||||
The live preview feature relies on `runtime.connect` and `port.onDisconnect`
|
||||
to cleanup the temporary code. See /edit/live-preview.js.
|
||||
*/
|
||||
const styleManager = (() => {
|
||||
const preparing = prepare();
|
||||
|
||||
/* styleId => {
|
||||
data: styleData,
|
||||
preview: styleData,
|
||||
appliesTo: Set<url>
|
||||
} */
|
||||
const styles = new Map();
|
||||
|
||||
/* url => {
|
||||
maybeMatch: Set<styleId>,
|
||||
sections: Object<styleId => {
|
||||
id: styleId,
|
||||
code: Array<String>
|
||||
}>
|
||||
} */
|
||||
const cachedStyleForUrl = createCache({
|
||||
onDeleted: (url, cache) => {
|
||||
for (const section of Object.values(cache.sections)) {
|
||||
const style = styles.get(section.id);
|
||||
if (style) {
|
||||
style.appliesTo.delete(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const BAD_MATCHER = {test: () => false};
|
||||
const compileRe = createCompiler(text => `^(${text})$`);
|
||||
const compileSloppyRe = createCompiler(text => `^${text}$`);
|
||||
const compileExclusion = createCompiler(buildGlob);
|
||||
|
||||
handleLivePreviewConnections();
|
||||
|
||||
return ensurePrepared({
|
||||
get,
|
||||
getSectionsByUrl,
|
||||
installStyle,
|
||||
deleteStyle,
|
||||
editSave,
|
||||
findStyle,
|
||||
importStyle,
|
||||
toggleStyle,
|
||||
setStyleExclusions,
|
||||
getAllStyles, // used by import-export
|
||||
getStylesByUrl, // used by popup
|
||||
styleExists,
|
||||
});
|
||||
|
||||
function handleLivePreviewConnections() {
|
||||
chrome.runtime.onConnect.addListener(port => {
|
||||
if (port.name !== 'livePreview') {
|
||||
return;
|
||||
}
|
||||
let id;
|
||||
port.onMessage.addListener(data => {
|
||||
if (!id) {
|
||||
id = data.id;
|
||||
}
|
||||
const style = styles.get(id);
|
||||
style.preview = data;
|
||||
broadcastStyleUpdated(style.preview, 'editPreview');
|
||||
});
|
||||
port.onDisconnect.addListener(() => {
|
||||
port = null;
|
||||
if (id) {
|
||||
const style = styles.get(id);
|
||||
if (!style) {
|
||||
// maybe deleted
|
||||
return;
|
||||
}
|
||||
style.preview = null;
|
||||
broadcastStyleUpdated(style.data, 'editPreviewEnd');
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function get(id, noCode = false) {
|
||||
const data = styles.get(id).data;
|
||||
return noCode ? getStyleWithNoCode(data) : data;
|
||||
}
|
||||
|
||||
function getAllStyles(noCode = false) {
|
||||
const datas = [...styles.values()].map(s => s.data);
|
||||
return noCode ? datas.map(getStyleWithNoCode) : datas;
|
||||
}
|
||||
|
||||
function toggleStyle(id, enabled) {
|
||||
const style = styles.get(id);
|
||||
const data = Object.assign({}, style.data, {enabled});
|
||||
return saveStyle(data)
|
||||
.then(newData => handleSave(newData, 'toggle', false))
|
||||
.then(() => id);
|
||||
}
|
||||
|
||||
// used by install-hook-userstyles.js
|
||||
function findStyle(filter, noCode = false) {
|
||||
for (const style of styles.values()) {
|
||||
if (filterMatch(filter, style.data)) {
|
||||
return noCode ? getStyleWithNoCode(style.data) : style.data;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function styleExists(filter) {
|
||||
return [...styles.values()].some(s => filterMatch(filter, s.data));
|
||||
}
|
||||
|
||||
function filterMatch(filter, target) {
|
||||
for (const key of Object.keys(filter)) {
|
||||
if (filter[key] !== target[key]) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function importStyle(data) {
|
||||
// FIXME: is it a good idea to save the data directly?
|
||||
return saveStyle(data)
|
||||
.then(newData => handleSave(newData, 'import'));
|
||||
}
|
||||
|
||||
function installStyle(data, reason = null) {
|
||||
const style = styles.get(data.id);
|
||||
if (!style) {
|
||||
data = Object.assign(createNewStyle(), data);
|
||||
} else {
|
||||
data = Object.assign({}, style.data, data);
|
||||
}
|
||||
if (!reason) {
|
||||
reason = style ? 'update' : 'install';
|
||||
}
|
||||
// FIXME: update updateDate? what about usercss config?
|
||||
return calcStyleDigest(data)
|
||||
.then(digest => {
|
||||
data.originalDigest = digest;
|
||||
return saveStyle(data);
|
||||
})
|
||||
.then(newData => handleSave(newData, reason));
|
||||
}
|
||||
|
||||
function editSave(data) {
|
||||
const style = styles.get(data.id);
|
||||
if (style) {
|
||||
data = Object.assign({}, style.data, data);
|
||||
} else {
|
||||
data = Object.assign(createNewStyle(), data);
|
||||
}
|
||||
return saveStyle(data)
|
||||
.then(newData => handleSave(newData, 'editSave'));
|
||||
}
|
||||
|
||||
function setStyleExclusions(id, exclusions) {
|
||||
const data = Object.assign({}, styles.get(id).data, {exclusions});
|
||||
return saveStyle(data)
|
||||
.then(newData => handleSave(newData, 'exclusions'));
|
||||
}
|
||||
|
||||
function deleteStyle(id) {
|
||||
const style = styles.get(id);
|
||||
return db.exec('delete', id)
|
||||
.then(() => {
|
||||
for (const url of style.appliesTo) {
|
||||
const cache = cachedStyleForUrl.get(url);
|
||||
if (cache) {
|
||||
delete cache.sections[id];
|
||||
}
|
||||
}
|
||||
styles.delete(id);
|
||||
return msg.broadcast({
|
||||
method: 'styleDeleted',
|
||||
style: {id}
|
||||
});
|
||||
})
|
||||
.then(() => id);
|
||||
}
|
||||
|
||||
function ensurePrepared(methods) {
|
||||
const prepared = {};
|
||||
for (const [name, fn] of Object.entries(methods)) {
|
||||
prepared[name] = (...args) =>
|
||||
preparing.then(() => fn(...args));
|
||||
}
|
||||
return prepared;
|
||||
}
|
||||
|
||||
function createNewStyle() {
|
||||
return {
|
||||
enabled: true,
|
||||
updateUrl: null,
|
||||
md5Url: null,
|
||||
url: null,
|
||||
originalMd5: null,
|
||||
installDate: Date.now()
|
||||
};
|
||||
}
|
||||
|
||||
function broadcastStyleUpdated(data, reason, method = 'styleUpdated', codeIsUpdated = true) {
|
||||
const style = styles.get(data.id);
|
||||
const excluded = new Set();
|
||||
const updated = new Set();
|
||||
for (const [url, cache] of cachedStyleForUrl.entries()) {
|
||||
if (!style.appliesTo.has(url)) {
|
||||
cache.maybeMatch.add(data.id);
|
||||
continue;
|
||||
}
|
||||
const code = getAppliedCode(url, data);
|
||||
if (!code) {
|
||||
excluded.add(url);
|
||||
delete cache.sections[data.id];
|
||||
} else {
|
||||
updated.add(url);
|
||||
cache.sections[data.id] = {
|
||||
id: data.id,
|
||||
code
|
||||
};
|
||||
}
|
||||
}
|
||||
style.appliesTo = updated;
|
||||
return msg.broadcast({
|
||||
method,
|
||||
style: {
|
||||
id: data.id,
|
||||
enabled: data.enabled
|
||||
},
|
||||
reason,
|
||||
codeIsUpdated
|
||||
});
|
||||
}
|
||||
|
||||
function saveStyle(style) {
|
||||
if (!style.name) {
|
||||
throw new Error('style name is empty');
|
||||
}
|
||||
if (style.id == null) {
|
||||
delete style.id;
|
||||
}
|
||||
fixUsoMd5Issue(style);
|
||||
return db.exec('put', style)
|
||||
.then(event => {
|
||||
if (style.id == null) {
|
||||
style.id = event.target.result;
|
||||
}
|
||||
return style;
|
||||
});
|
||||
}
|
||||
|
||||
function handleSave(data, reason, codeIsUpdated) {
|
||||
const style = styles.get(data.id);
|
||||
let method;
|
||||
if (!style) {
|
||||
styles.set(data.id, {
|
||||
appliesTo: new Set(),
|
||||
data
|
||||
});
|
||||
method = 'styleAdded';
|
||||
} else {
|
||||
style.data = data;
|
||||
method = 'styleUpdated';
|
||||
}
|
||||
return broadcastStyleUpdated(data, reason, method, codeIsUpdated)
|
||||
.then(() => data);
|
||||
}
|
||||
|
||||
// get styles matching a URL, including sloppy regexps and excluded items.
|
||||
function getStylesByUrl(url, id = null) {
|
||||
// FIXME: do we want to cache this? Who would like to open popup rapidly
|
||||
// or search the DB with the same URL?
|
||||
const result = [];
|
||||
const datas = !id ? [...styles.values()].map(s => s.data) :
|
||||
styles.has(id) ? [styles.get(id).data] : [];
|
||||
for (const data of datas) {
|
||||
let excluded = false;
|
||||
let sloppy = false;
|
||||
let sectionMatched = false;
|
||||
const match = urlMatchStyle(url, data);
|
||||
// TODO: enable this when the function starts returning false
|
||||
// if (match === false) {
|
||||
// continue;
|
||||
// }
|
||||
if (match === 'excluded') {
|
||||
excluded = true;
|
||||
}
|
||||
for (const section of data.sections) {
|
||||
if (styleCodeEmpty(section.code)) {
|
||||
continue;
|
||||
}
|
||||
const match = urlMatchSection(url, section);
|
||||
if (match) {
|
||||
if (match === 'sloppy') {
|
||||
sloppy = true;
|
||||
}
|
||||
sectionMatched = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (sectionMatched) {
|
||||
result.push({
|
||||
data: getStyleWithNoCode(data),
|
||||
excluded,
|
||||
sloppy
|
||||
});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function getSectionsByUrl(url, id) {
|
||||
let cache = cachedStyleForUrl.get(url);
|
||||
if (!cache) {
|
||||
cache = {
|
||||
sections: {},
|
||||
maybeMatch: new Set()
|
||||
};
|
||||
buildCache(styles.values());
|
||||
cachedStyleForUrl.set(url, cache);
|
||||
} else if (cache.maybeMatch.size) {
|
||||
buildCache(
|
||||
[...cache.maybeMatch]
|
||||
.filter(i => styles.has(i))
|
||||
.map(i => styles.get(i))
|
||||
);
|
||||
}
|
||||
if (id) {
|
||||
if (cache.sections[id]) {
|
||||
return {[id]: cache.sections[id]};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
return cache.sections;
|
||||
|
||||
function buildCache(styleList) {
|
||||
for (const {appliesTo, data, preview} of styleList) {
|
||||
const code = getAppliedCode(url, preview || data);
|
||||
if (code) {
|
||||
cache.sections[data.id] = {
|
||||
id: data.id,
|
||||
code
|
||||
};
|
||||
appliesTo.add(url);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getAppliedCode(url, data) {
|
||||
if (urlMatchStyle(url, data) !== true) {
|
||||
return;
|
||||
}
|
||||
const code = [];
|
||||
for (const section of data.sections) {
|
||||
if (urlMatchSection(url, section) === true && !styleCodeEmpty(section.code)) {
|
||||
code.push(section.code);
|
||||
}
|
||||
}
|
||||
return code.length && code;
|
||||
}
|
||||
|
||||
function prepare() {
|
||||
return db.exec('getAll').then(event => {
|
||||
const styleList = event.target.result;
|
||||
if (!styleList) {
|
||||
return;
|
||||
}
|
||||
for (const style of styleList) {
|
||||
fixUsoMd5Issue(style);
|
||||
styles.set(style.id, {
|
||||
appliesTo: new Set(),
|
||||
data: style
|
||||
});
|
||||
if (!style.name) {
|
||||
style.name = 'ID: ' + style.id;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function urlMatchStyle(url, style) {
|
||||
if (style.exclusions && style.exclusions.some(e => compileExclusion(e).test(url))) {
|
||||
return 'excluded';
|
||||
}
|
||||
if (!style.enabled) {
|
||||
return 'disabled';
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function urlMatchSection(url, section) {
|
||||
const domain = getDomain(url);
|
||||
if (section.domains && section.domains.some(d => d === domain || domain.endsWith(`.${d}`))) {
|
||||
return true;
|
||||
}
|
||||
if (section.urlPrefixes && section.urlPrefixes.some(p => url.startsWith(p))) {
|
||||
return true;
|
||||
}
|
||||
// as per spec the fragment portion is ignored in @-moz-document:
|
||||
// https://www.w3.org/TR/2012/WD-css3-conditional-20120911/#url-of-doc
|
||||
// but the spec is outdated and doesn't account for SPA sites
|
||||
// so we only respect it for `url()` function
|
||||
if (section.urls && (
|
||||
section.urls.includes(url) ||
|
||||
section.urls.includes(getUrlNoHash(url))
|
||||
)) {
|
||||
return true;
|
||||
}
|
||||
if (section.regexps && section.regexps.some(r => compileRe(r).test(url))) {
|
||||
return true;
|
||||
}
|
||||
/*
|
||||
According to CSS4 @document specification the entire URL must match.
|
||||
Stylish-for-Chrome implemented it incorrectly since the very beginning.
|
||||
We'll detect styles that abuse the bug by finding the sections that
|
||||
would have been applied by Stylish but not by us as we follow the spec.
|
||||
*/
|
||||
if (section.regexps && section.regexps.some(r => compileSloppyRe(r).test(url))) {
|
||||
return 'sloppy';
|
||||
}
|
||||
// TODO: check for invalid regexps?
|
||||
if (
|
||||
(!section.regexps || !section.regexps.length) &&
|
||||
(!section.urlPrefixes || !section.urlPrefixes.length) &&
|
||||
(!section.urls || !section.urls.length) &&
|
||||
(!section.domains || !section.domains.length)
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function createCompiler(compile) {
|
||||
// FIXME: FIFO cache doesn't work well here, if we want to match many
|
||||
// regexps more than the cache size, we will never hit the cache because
|
||||
// the first cache is deleted. So we use a simple map but it leaks memory.
|
||||
const cache = new Map();
|
||||
return text => {
|
||||
let re = cache.get(text);
|
||||
if (!re) {
|
||||
re = tryRegExp(compile(text));
|
||||
if (!re) {
|
||||
re = BAD_MATCHER;
|
||||
}
|
||||
cache.set(text, re);
|
||||
}
|
||||
return re;
|
||||
};
|
||||
}
|
||||
|
||||
function buildGlob(text) {
|
||||
const prefix = text[0] === '^' ? '' : '\\b';
|
||||
const suffix = text[text.length - 1] === '$' ? '' : '\\b';
|
||||
return `${prefix}${escape(text)}${suffix}`;
|
||||
|
||||
function escape(text) {
|
||||
// FIXME: using .* everywhere is slow
|
||||
return text.replace(/[.*]/g, m => m === '.' ? '\\.' : '.*');
|
||||
}
|
||||
}
|
||||
|
||||
function getDomain(url) {
|
||||
return url.match(/^[\w-]+:\/+(?:[\w:-]+@)?([^:/#]+)/)[1];
|
||||
}
|
||||
|
||||
function getUrlNoHash(url) {
|
||||
return url.split('#')[0];
|
||||
}
|
||||
|
||||
// The md5Url provided by USO includes a duplicate "update" subdomain (see #523),
|
||||
// This fixes any already installed styles containing this error
|
||||
function fixUsoMd5Issue(style) {
|
||||
if (style && style.md5Url && style.md5Url.includes('update.update.userstyles')) {
|
||||
style.md5Url = style.md5Url.replace('update.update.userstyles', 'update.userstyles');
|
||||
}
|
||||
}
|
||||
})();
|
|
@ -1,4 +1,4 @@
|
|||
/* global getStyles API_METHODS */
|
||||
/* global API_METHODS styleManager CHROME prefs updateIconBadge */
|
||||
'use strict';
|
||||
|
||||
API_METHODS.styleViaAPI = !CHROME && (() => {
|
||||
|
@ -9,6 +9,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
|||
styleAdded,
|
||||
styleReplaceAll,
|
||||
prefChanged,
|
||||
updateCount,
|
||||
};
|
||||
const NOP = Promise.resolve(new Error('NOP'));
|
||||
const onError = () => {};
|
||||
|
@ -22,15 +23,23 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
|||
|
||||
let observingTabs = false;
|
||||
|
||||
return (request, sender) => {
|
||||
const action = ACTIONS[request.action];
|
||||
return function (request) {
|
||||
const action = ACTIONS[request.method];
|
||||
return !action ? NOP :
|
||||
action(request, sender)
|
||||
action(request, this.sender)
|
||||
.catch(onError)
|
||||
.then(maybeToggleObserver);
|
||||
};
|
||||
|
||||
function styleApply({id = null, ignoreUrlCheck}, {tab, frameId, url}) {
|
||||
function updateCount(request, {tab, frameId}) {
|
||||
if (frameId) {
|
||||
throw new Error('we do not count styles for frames');
|
||||
}
|
||||
const {frameStyles} = getCachedData(tab.id, frameId);
|
||||
updateIconBadge(tab.id, Object.keys(frameStyles).length);
|
||||
}
|
||||
|
||||
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
|
||||
if (prefs.get('disableAll')) {
|
||||
return NOP;
|
||||
}
|
||||
|
@ -38,24 +47,15 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
|||
if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
|
||||
return NOP;
|
||||
}
|
||||
return getStyles({id, matchUrl: url, asHash: true}).then(styles => {
|
||||
return styleManager.getSectionsByUrl(url, id).then(sections => {
|
||||
const tasks = [];
|
||||
for (const styleId in styles) {
|
||||
if (isNaN(parseInt(styleId))) {
|
||||
continue;
|
||||
}
|
||||
// shallow-extract code from the sections array in order to reuse references
|
||||
// in other places whereas the combined string gets garbage-collected
|
||||
const styleSections = styles[styleId].map(section => section.code);
|
||||
const code = styleSections.join('\n');
|
||||
if (!code) {
|
||||
delete frameStyles[styleId];
|
||||
continue;
|
||||
}
|
||||
for (const section of Object.values(sections)) {
|
||||
const styleId = section.id;
|
||||
const code = section.code.join('\n');
|
||||
if (code === (frameStyles[styleId] || []).join('\n')) {
|
||||
continue;
|
||||
}
|
||||
frameStyles[styleId] = styleSections;
|
||||
frameStyles[styleId] = section.code;
|
||||
tasks.push(
|
||||
browser.tabs.insertCSS(tab.id, {
|
||||
code,
|
||||
|
@ -70,16 +70,18 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
|||
cache.set(tab.id, tabFrames);
|
||||
}
|
||||
return Promise.all(tasks);
|
||||
});
|
||||
})
|
||||
.then(() => updateCount(null, {tab, frameId}));
|
||||
}
|
||||
|
||||
function styleDeleted({id}, {tab, frameId}) {
|
||||
function styleDeleted({style: {id}}, {tab, frameId}) {
|
||||
const {tabFrames, frameStyles, styleSections} = getCachedData(tab.id, frameId, id);
|
||||
const code = styleSections.join('\n');
|
||||
if (code && !duplicateCodeExists({frameStyles, id, code})) {
|
||||
delete frameStyles[id];
|
||||
removeFrameIfEmpty(tab.id, frameId, tabFrames, frameStyles);
|
||||
return removeCSS(tab.id, frameId, code);
|
||||
return removeCSS(tab.id, frameId, code)
|
||||
.then(() => updateCount(null, {tab, frameId}));
|
||||
} else {
|
||||
return NOP;
|
||||
}
|
||||
|
@ -87,7 +89,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
|
|||
|
||||
function styleUpdated({style}, sender) {
|
||||
if (!style.enabled) {
|
||||
return styleDeleted(style, sender);
|
||||
return styleDeleted({style}, sender);
|
||||
}
|
||||
const {tab, frameId} = sender;
|
||||
const {frameStyles, styleSections} = getCachedData(tab.id, frameId, style.id);
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
/*
|
||||
global getStyles saveStyle styleSectionsEqual
|
||||
global calcStyleDigest cachedStyles getStyleWithNoCode
|
||||
global usercss semverCompare
|
||||
global API_METHODS
|
||||
*/
|
||||
/* global styleSectionsEqual prefs download tryJSONparse ignoreChromeError
|
||||
calcStyleDigest getStyleWithNoCode debounce chromeLocal
|
||||
usercss semverCompare
|
||||
API_METHODS styleManager */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
@ -51,7 +49,7 @@ global API_METHODS
|
|||
checkingAll = true;
|
||||
retrying.clear();
|
||||
const port = observe && chrome.runtime.connect({name: 'updater'});
|
||||
return getStyles({}).then(styles => {
|
||||
return styleManager.getAllStyles().then(styles => {
|
||||
styles = styles.filter(style => style.updateUrl);
|
||||
if (port) port.postMessage({count: styles.length});
|
||||
log('');
|
||||
|
@ -70,7 +68,7 @@ global API_METHODS
|
|||
|
||||
function checkStyle({
|
||||
id,
|
||||
style = cachedStyles.byId.get(id),
|
||||
style,
|
||||
port,
|
||||
save = true,
|
||||
ignoreDigest,
|
||||
|
@ -89,14 +87,33 @@ global API_METHODS
|
|||
|
||||
'ignoreDigest' option is set on the second manual individual update check on the manage page.
|
||||
*/
|
||||
return Promise.resolve(style)
|
||||
.then([calcStyleDigest][!ignoreDigest ? 0 : 'skip'])
|
||||
.then([checkIfEdited][!ignoreDigest ? 0 : 'skip'])
|
||||
.then([maybeUpdateUSO, maybeUpdateUsercss][style.usercssData ? 1 : 0])
|
||||
return fetchStyle()
|
||||
.then(() => {
|
||||
if (!ignoreDigest) {
|
||||
return calcStyleDigest(style)
|
||||
.then(checkIfEdited);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
if (style.usercssData) {
|
||||
return maybeUpdateUsercss();
|
||||
}
|
||||
return maybeUpdateUSO();
|
||||
})
|
||||
.then(maybeSave)
|
||||
.then(reportSuccess)
|
||||
.catch(reportFailure);
|
||||
|
||||
function fetchStyle() {
|
||||
if (style) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return styleManager.get(id)
|
||||
.then(style_ => {
|
||||
style = style_;
|
||||
});
|
||||
}
|
||||
|
||||
function reportSuccess(saved) {
|
||||
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
|
||||
const info = {updated: true, style: saved};
|
||||
|
@ -145,8 +162,8 @@ global API_METHODS
|
|||
|
||||
function maybeUpdateUsercss() {
|
||||
// TODO: when sourceCode is > 100kB use http range request(s) for version check
|
||||
return download(style.updateUrl).then(text => {
|
||||
const json = usercss.buildMeta(text);
|
||||
return download(style.updateUrl).then(text =>
|
||||
usercss.buildMeta(text).then(json => {
|
||||
const {usercssData: {version}} = style;
|
||||
const {usercssData: {version: newVersion}} = json;
|
||||
switch (Math.sign(semverCompare(version, newVersion))) {
|
||||
|
@ -162,7 +179,8 @@ global API_METHODS
|
|||
return Promise.reject(STATES.ERROR_VERSION);
|
||||
}
|
||||
return usercss.buildCode(json);
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function maybeSave(json = {}) {
|
||||
|
@ -173,7 +191,6 @@ global API_METHODS
|
|||
|
||||
json.id = style.id;
|
||||
json.updateDate = Date.now();
|
||||
json.reason = 'update';
|
||||
|
||||
// keep current state
|
||||
delete json.enabled;
|
||||
|
@ -185,10 +202,10 @@ global API_METHODS
|
|||
json.originalName = json.name;
|
||||
}
|
||||
|
||||
const newStyle = Object.assign({}, style, json);
|
||||
if (styleSectionsEqual(json, style, {checkSource: true})) {
|
||||
// update digest even if save === false as there might be just a space added etc.
|
||||
json.reason = 'update-digest';
|
||||
return saveStyle(json)
|
||||
return styleManager.installStyle(newStyle)
|
||||
.then(saved => {
|
||||
style.originalDigest = saved.originalDigest;
|
||||
return Promise.reject(STATES.SAME_CODE);
|
||||
|
@ -200,8 +217,8 @@ global API_METHODS
|
|||
}
|
||||
|
||||
return save ?
|
||||
API_METHODS[json.usercssData ? 'saveUsercss' : 'saveStyle'](json) :
|
||||
json;
|
||||
API_METHODS[json.usercssData ? 'installUsercss' : 'installStyle'](newStyle) :
|
||||
newStyle;
|
||||
}
|
||||
|
||||
function styleJSONseemsValid(json) {
|
||||
|
|
|
@ -1,13 +1,15 @@
|
|||
/* global API_METHODS usercss saveStyle getStyles chromeLocal cachedStyles */
|
||||
/* global API_METHODS usercss chromeLocal styleManager FIREFOX deepCopy openURL
|
||||
download */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
API_METHODS.installUsercss = installUsercss;
|
||||
API_METHODS.editSaveUsercss = editSaveUsercss;
|
||||
API_METHODS.configUsercssVars = configUsercssVars;
|
||||
|
||||
API_METHODS.saveUsercss = style => save(style, false);
|
||||
API_METHODS.saveUsercssUnsafe = style => save(style, true);
|
||||
API_METHODS.buildUsercss = build;
|
||||
API_METHODS.installUsercss = install;
|
||||
API_METHODS.parseUsercss = parse;
|
||||
API_METHODS.openUsercssInstallPage = install;
|
||||
|
||||
API_METHODS.findUsercss = find;
|
||||
|
||||
const TEMP_CODE_PREFIX = 'tempUsercssCode';
|
||||
|
@ -40,69 +42,96 @@
|
|||
if (style.usercssData) {
|
||||
return Promise.resolve(style);
|
||||
}
|
||||
try {
|
||||
const {sourceCode} = style;
|
||||
|
||||
// allow sourceCode to be normalized
|
||||
const {sourceCode} = style;
|
||||
delete style.sourceCode;
|
||||
return Promise.resolve(Object.assign(usercss.buildMeta(sourceCode), style));
|
||||
} catch (e) {
|
||||
return Promise.reject(e);
|
||||
}
|
||||
|
||||
return usercss.buildMeta(sourceCode)
|
||||
.then(newStyle => Object.assign(newStyle, style));
|
||||
}
|
||||
|
||||
function assignVars(style) {
|
||||
if (style.reason === 'config' && style.id) {
|
||||
return style;
|
||||
}
|
||||
const dup = find(style);
|
||||
return find(style)
|
||||
.then(dup => {
|
||||
if (dup) {
|
||||
style.id = dup.id;
|
||||
if (style.reason !== 'config') {
|
||||
// preserve style.vars during update
|
||||
usercss.assignVars(style, dup);
|
||||
}
|
||||
return usercss.assignVars(style, dup)
|
||||
.then(() => style);
|
||||
}
|
||||
return style;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse the source and find the duplication
|
||||
* Parse the source, find the duplication, and build sections with variables
|
||||
* @param _
|
||||
* @param {String} _.sourceCode
|
||||
* @param {Boolean=} _.checkDup
|
||||
* @param {Boolean=} _.metaOnly
|
||||
* @param {Object} _.vars
|
||||
* @param {Boolean=} _.assignVars
|
||||
* @returns {Promise<{style, dup:Boolean?}>}
|
||||
*/
|
||||
function build({
|
||||
sourceCode,
|
||||
checkDup,
|
||||
metaOnly,
|
||||
vars,
|
||||
assignVars = false,
|
||||
}) {
|
||||
const task = buildMeta({sourceCode});
|
||||
return (metaOnly ? task : task.then(usercss.buildCode))
|
||||
.then(style => ({
|
||||
style,
|
||||
dup: checkDup && find(style),
|
||||
}));
|
||||
return usercss.buildMeta(sourceCode)
|
||||
.then(style => {
|
||||
const findDup = checkDup || assignVars ? find(style) : null;
|
||||
return Promise.all([
|
||||
metaOnly ? style : doBuild(style, findDup),
|
||||
findDup
|
||||
]);
|
||||
})
|
||||
.then(([style, dup]) => ({style, dup}));
|
||||
|
||||
function doBuild(style, findDup) {
|
||||
if (vars || assignVars) {
|
||||
const getOld = vars ? Promise.resolve({usercssData: {vars}}) : findDup;
|
||||
return getOld
|
||||
.then(oldStyle => usercss.assignVars(style, oldStyle))
|
||||
.then(() => usercss.buildCode(style));
|
||||
}
|
||||
return usercss.buildCode(style);
|
||||
}
|
||||
}
|
||||
|
||||
// Parse the source, apply customizations, report fatal/syntax errors
|
||||
function parse(style, allowErrors = false) {
|
||||
// restore if stripped by getStyleWithNoCode
|
||||
if (typeof style.sourceCode !== 'string') {
|
||||
style.sourceCode = cachedStyles.byId.get(style.id).sourceCode;
|
||||
}
|
||||
// Build the style within aditional properties then inherit variable values
|
||||
// from the old style.
|
||||
function parse(style) {
|
||||
return buildMeta(style)
|
||||
.then(buildMeta)
|
||||
.then(assignVars)
|
||||
.then(style => usercss.buildCode(style, allowErrors));
|
||||
.then(usercss.buildCode);
|
||||
}
|
||||
|
||||
function save(style, allowErrors = false) {
|
||||
return parse(style, allowErrors)
|
||||
.then(result =>
|
||||
allowErrors ?
|
||||
saveStyle(result.style).then(style => ({style, errors: result.errors})) :
|
||||
saveStyle(result));
|
||||
// FIXME: simplify this to `installUsercss(sourceCode)`?
|
||||
function installUsercss(style) {
|
||||
return parse(style)
|
||||
.then(styleManager.installStyle);
|
||||
}
|
||||
|
||||
// FIXME: simplify this to `editSaveUsercss({sourceCode, exclusions})`?
|
||||
function editSaveUsercss(style) {
|
||||
return parse(style)
|
||||
.then(styleManager.editSave);
|
||||
}
|
||||
|
||||
function configUsercssVars(id, vars) {
|
||||
return styleManager.get(id)
|
||||
.then(style => {
|
||||
const newStyle = deepCopy(style);
|
||||
newStyle.usercssData.vars = vars;
|
||||
return usercss.buildCode(newStyle);
|
||||
})
|
||||
.then(style => styleManager.installStyle(style, 'config'))
|
||||
.then(style => style.usercssData.vars);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -110,9 +139,12 @@
|
|||
* @returns {Style}
|
||||
*/
|
||||
function find(styleOrData) {
|
||||
if (styleOrData.id) return cachedStyles.byId.get(styleOrData.id);
|
||||
if (styleOrData.id) {
|
||||
return styleManager.get(styleOrData.id);
|
||||
}
|
||||
const {name, namespace} = styleOrData.usercssData || styleOrData;
|
||||
for (const dup of cachedStyles.list) {
|
||||
return styleManager.getAllStyles().then(styleList => {
|
||||
for (const dup of styleList) {
|
||||
const data = dup.usercssData;
|
||||
if (!data) continue;
|
||||
if (data.name === name &&
|
||||
|
@ -120,9 +152,10 @@
|
|||
return dup;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function install({url, direct, downloaded, tab}, sender) {
|
||||
function install({url, direct, downloaded, tab}, sender = this.sender) {
|
||||
tab = tab !== undefined ? tab : sender.tab;
|
||||
url = url || tab.url;
|
||||
if (direct && !downloaded) {
|
||||
|
|
458
content/apply.js
458
content/apply.js
|
@ -1,44 +1,128 @@
|
|||
/* eslint no-var: 0 */
|
||||
/* global msg API prefs */
|
||||
/* exported APPLY */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
if (typeof window.applyOnMessage === 'function') {
|
||||
// some weird bug in new Chrome: the content script gets injected multiple times
|
||||
return;
|
||||
}
|
||||
// some weird bug in new Chrome: the content script gets injected multiple times
|
||||
// define a constant so it throws when redefined
|
||||
const APPLY = (() => {
|
||||
const CHROME = chrome.app ? parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]) : NaN;
|
||||
const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument;
|
||||
var ID_PREFIX = 'stylus-';
|
||||
var ROOT = document.documentElement;
|
||||
var ROOT;
|
||||
var isOwnPage = location.protocol.endsWith('-extension:');
|
||||
var disableAll = false;
|
||||
var exposeIframes = false;
|
||||
var styleElements = new Map();
|
||||
var disabledElements = new Map();
|
||||
var retiredStyleTimers = new Map();
|
||||
var docRewriteObserver;
|
||||
var docRootObserver;
|
||||
const setStyleContent = createSetStyleContent();
|
||||
const initializing = init();
|
||||
|
||||
msg.onTab(applyOnMessage);
|
||||
|
||||
if (!isOwnPage) {
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id, {
|
||||
detail: pageObject({method: 'orphan'})
|
||||
}));
|
||||
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
}
|
||||
|
||||
let parentDomain;
|
||||
|
||||
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
|
||||
if (window !== parent) {
|
||||
prefs.subscribe(['exposeIframes'], updateExposeIframes);
|
||||
}
|
||||
|
||||
function init() {
|
||||
if (STYLE_VIA_API) {
|
||||
return API.styleViaAPI({method: 'styleApply'});
|
||||
}
|
||||
return API.getSectionsByUrl(getMatchUrl())
|
||||
.then(result => {
|
||||
ROOT = document.documentElement;
|
||||
applyStyles(result, () => {
|
||||
// CSS transition bug workaround: since we insert styles asynchronously,
|
||||
// the browsers, especially Firefox, may apply all transitions on page load
|
||||
if ([...styleElements.values()].some(n => n.textContent.includes('transition'))) {
|
||||
applyTransitionPatch();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function pageObject(target) {
|
||||
// https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Sharing_objects_with_page_scripts
|
||||
const obj = new window.Object();
|
||||
Object.assign(obj, target);
|
||||
return obj;
|
||||
}
|
||||
|
||||
function createSetStyleContent() {
|
||||
// FF59+ bug workaround
|
||||
// See https://github.com/openstyles/stylus/issues/461
|
||||
// Since it's easy to spoof the browser version in pre-Quantum FF we're checking
|
||||
// for getPreventDefault which got removed in FF59 https://bugzil.la/691151
|
||||
const FF_BUG461 = !CHROME && !isOwnPage && !Event.prototype.getPreventDefault;
|
||||
const pageContextQueue = [];
|
||||
|
||||
requestStyles();
|
||||
chrome.runtime.onMessage.addListener(applyOnMessage);
|
||||
window.applyOnMessage = applyOnMessage;
|
||||
|
||||
if (!isOwnPage) {
|
||||
window.dispatchEvent(new CustomEvent(chrome.runtime.id));
|
||||
window.addEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
const EVENT_NAME = chrome.runtime.id;
|
||||
const usePageScript = CHROME || isOwnPage || Event.prototype.getPreventDefault ?
|
||||
Promise.resolve(false) : injectPageScript();
|
||||
return (el, content) =>
|
||||
usePageScript.then(ok => {
|
||||
if (!ok) {
|
||||
const disabled = el.disabled;
|
||||
el.textContent = content;
|
||||
el.disabled = disabled;
|
||||
} else {
|
||||
const detail = pageObject({
|
||||
method: 'setStyleContent',
|
||||
id: el.id,
|
||||
content
|
||||
});
|
||||
window.dispatchEvent(new CustomEvent(EVENT_NAME, {detail}));
|
||||
}
|
||||
});
|
||||
|
||||
function requestStyles(options, callback = applyStyles) {
|
||||
if (!chrome.app && document instanceof XMLDocument) {
|
||||
chrome.runtime.sendMessage({method: 'styleViaAPI', action: 'styleApply'});
|
||||
function injectPageScript() {
|
||||
const scriptContent = EVENT_NAME => {
|
||||
document.currentScript.remove();
|
||||
window.addEventListener(EVENT_NAME, function handler(e) {
|
||||
const {method, id, content} = e.detail;
|
||||
if (method === 'setStyleContent') {
|
||||
const el = document.getElementById(id);
|
||||
if (!el) {
|
||||
return;
|
||||
}
|
||||
const disabled = el.disabled;
|
||||
el.textContent = content;
|
||||
el.disabled = disabled;
|
||||
} else if (method === 'orphan') {
|
||||
window.removeEventListener(EVENT_NAME, handler);
|
||||
}
|
||||
}, true);
|
||||
};
|
||||
const code = `(${scriptContent})(${JSON.stringify(EVENT_NAME)})`;
|
||||
const src = `data:application/javascript;base64,${btoa(code)}`;
|
||||
const script = document.createElement('script');
|
||||
const {resolve, promise} = deferred();
|
||||
script.src = src;
|
||||
script.onload = () => resolve(true);
|
||||
script.onerror = () => resolve(false);
|
||||
document.documentElement.appendChild(script);
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
function deferred() {
|
||||
const o = {};
|
||||
o.promise = new Promise((resolve, reject) => {
|
||||
o.resolve = resolve;
|
||||
o.reject = reject;
|
||||
});
|
||||
return o;
|
||||
}
|
||||
|
||||
function getMatchUrl() {
|
||||
var matchUrl = location.href;
|
||||
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) {
|
||||
// dynamic about: and javascript: iframes don't have an URL yet
|
||||
|
@ -49,78 +133,38 @@
|
|||
}
|
||||
} catch (e) {}
|
||||
}
|
||||
const request = Object.assign({
|
||||
method: 'getStylesForFrame',
|
||||
asHash: true,
|
||||
matchUrl,
|
||||
}, options);
|
||||
// On own pages we request the styles directly to minimize delay and flicker
|
||||
if (typeof API === 'function') {
|
||||
API.getStyles(request).then(callback);
|
||||
} else if (!CHROME && getStylesFallback(request)) {
|
||||
// NOP
|
||||
} else {
|
||||
chrome.runtime.sendMessage(request, callback);
|
||||
}
|
||||
return matchUrl;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO: remove when FF fixes the bug.
|
||||
* Firefox borks sendMessage in same-origin iframes that have 'src' with a real path on the site.
|
||||
* We implement a workaround for the initial styleApply case only.
|
||||
* Everything else (like toggling of styles) is still buggy.
|
||||
* @param {Object} msg
|
||||
* @param {Function} callback
|
||||
* @returns {Boolean|undefined}
|
||||
*/
|
||||
function getStylesFallback(msg) {
|
||||
if (window !== parent &&
|
||||
location.href !== 'about:blank') {
|
||||
try {
|
||||
if (parent.location.origin === location.origin &&
|
||||
parent.location.href !== location.href) {
|
||||
chrome.runtime.connect({name: 'getStyles:' + JSON.stringify(msg)});
|
||||
function applyOnMessage(request) {
|
||||
if (request.method === 'ping') {
|
||||
return true;
|
||||
}
|
||||
} catch (e) {}
|
||||
if (STYLE_VIA_API) {
|
||||
if (request.method === 'urlChanged') {
|
||||
request.method = 'styleReplaceAll';
|
||||
}
|
||||
}
|
||||
|
||||
function applyOnMessage(request, sender, sendResponse) {
|
||||
if (request.styles === 'DIY') {
|
||||
// Do-It-Yourself tells our built-in pages to fetch the styles directly
|
||||
// which is faster because IPC messaging JSON-ifies everything internally
|
||||
requestStyles({}, styles => {
|
||||
request.styles = styles;
|
||||
applyOnMessage(request);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
if (!chrome.app && document instanceof XMLDocument && request.method !== 'ping') {
|
||||
request.action = request.method;
|
||||
request.method = 'styleViaAPI';
|
||||
request.styles = null;
|
||||
if (request.style) {
|
||||
request.style.sections = null;
|
||||
}
|
||||
chrome.runtime.sendMessage(request);
|
||||
API.styleViaAPI(request);
|
||||
return;
|
||||
}
|
||||
|
||||
switch (request.method) {
|
||||
case 'styleDeleted':
|
||||
removeStyle(request);
|
||||
removeStyle(request.style);
|
||||
break;
|
||||
|
||||
case 'styleUpdated':
|
||||
if (request.codeIsUpdated === false) {
|
||||
applyStyleState(request.style);
|
||||
break;
|
||||
} else if (request.style.enabled) {
|
||||
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
||||
.then(sections => {
|
||||
if (!sections[request.style.id]) {
|
||||
removeStyle(request.style);
|
||||
} else {
|
||||
applyStyles(sections);
|
||||
}
|
||||
if (request.style.enabled) {
|
||||
removeStyle({id: request.style.id, retire: true});
|
||||
requestStyles({id: request.style.id});
|
||||
});
|
||||
} else {
|
||||
removeStyle(request.style);
|
||||
}
|
||||
|
@ -128,29 +172,28 @@
|
|||
|
||||
case 'styleAdded':
|
||||
if (request.style.enabled) {
|
||||
requestStyles({id: request.style.id});
|
||||
API.getSectionsByUrl(getMatchUrl(), request.style.id)
|
||||
.then(applyStyles);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'styleApply':
|
||||
applyStyles(request.styles);
|
||||
case 'urlChanged':
|
||||
API.getSectionsByUrl(getMatchUrl())
|
||||
.then(replaceAll);
|
||||
break;
|
||||
|
||||
case 'styleReplaceAll':
|
||||
replaceAll(request.styles);
|
||||
break;
|
||||
|
||||
case 'prefChanged':
|
||||
if ('disableAll' in request.prefs) {
|
||||
doDisableAll(request.prefs.disableAll);
|
||||
}
|
||||
if ('exposeIframes' in request.prefs) {
|
||||
doExposeIframes(request.prefs.exposeIframes);
|
||||
case 'backgroundReady':
|
||||
initializing
|
||||
.catch(err => {
|
||||
if (msg.RX_NO_RECEIVER.test(err.message)) {
|
||||
return init();
|
||||
}
|
||||
})
|
||||
.catch(console.error);
|
||||
break;
|
||||
|
||||
case 'ping':
|
||||
sendResponse(true);
|
||||
case 'updateCount':
|
||||
updateCount();
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -160,6 +203,9 @@
|
|||
return;
|
||||
}
|
||||
disableAll = disable;
|
||||
if (STYLE_VIA_API) {
|
||||
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
|
||||
} else {
|
||||
Array.prototype.forEach.call(document.styleSheets, stylesheet => {
|
||||
if (stylesheet.ownerNode.matches(`style.stylus[id^="${ID_PREFIX}"]`)
|
||||
&& stylesheet.disabled !== disable) {
|
||||
|
@ -167,20 +213,53 @@
|
|||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function doExposeIframes(state = exposeIframes) {
|
||||
if (state === exposeIframes ||
|
||||
state === true && typeof exposeIframes === 'string' ||
|
||||
window === parent) {
|
||||
function fetchParentDomain() {
|
||||
if (parentDomain) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
return API.getTabUrlPrefix()
|
||||
.then(newDomain => {
|
||||
parentDomain = newDomain;
|
||||
});
|
||||
}
|
||||
|
||||
function updateExposeIframes() {
|
||||
if (!prefs.get('exposeIframes') || window === parent || !styleElements.size) {
|
||||
document.documentElement.removeAttribute('stylus-iframe');
|
||||
} else {
|
||||
fetchParentDomain().then(() => {
|
||||
document.documentElement.setAttribute('stylus-iframe', parentDomain);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateCount() {
|
||||
if (window !== parent) {
|
||||
// we don't care about iframes
|
||||
return;
|
||||
}
|
||||
exposeIframes = state;
|
||||
const attr = document.documentElement.getAttribute('stylus-iframe');
|
||||
if (state && state !== attr) {
|
||||
document.documentElement.setAttribute('stylus-iframe', state);
|
||||
} else if (!state && attr !== undefined) {
|
||||
document.documentElement.removeAttribute('stylus-iframe');
|
||||
if (/^\w+?-extension:\/\/.+(popup|options)\.html$/.test(location.href)) {
|
||||
// popup and the option page are not tabs
|
||||
return;
|
||||
}
|
||||
if (STYLE_VIA_API) {
|
||||
API.styleViaAPI({method: 'updateCount'}).catch(msg.ignoreError);
|
||||
return;
|
||||
}
|
||||
let count = 0;
|
||||
for (const id of styleElements.keys()) {
|
||||
if (!disabledElements.has(id)) {
|
||||
count++;
|
||||
}
|
||||
}
|
||||
// we have to send the tabId so we can't use `sendBg` that is used by `API`
|
||||
msg.send({
|
||||
method: 'invokeAPI',
|
||||
name: 'updateIconBadge',
|
||||
args: [count]
|
||||
}).catch(msg.ignoreError);
|
||||
}
|
||||
|
||||
function applyStyleState({id, enabled}) {
|
||||
|
@ -193,7 +272,8 @@
|
|||
addStyleElement(inCache);
|
||||
disabledElements.delete(id);
|
||||
} else {
|
||||
requestStyles({id});
|
||||
return API.getSectionsByUrl(getMatchUrl(), id)
|
||||
.then(applyStyles);
|
||||
}
|
||||
} else {
|
||||
if (inDoc) {
|
||||
|
@ -201,32 +281,25 @@
|
|||
docRootObserver.evade(() => inDoc.remove());
|
||||
}
|
||||
}
|
||||
updateCount();
|
||||
}
|
||||
|
||||
function removeStyle({id, retire = false}) {
|
||||
function removeStyle({id}) {
|
||||
const el = document.getElementById(ID_PREFIX + id);
|
||||
if (el) {
|
||||
if (retire) {
|
||||
// to avoid page flicker when the style is updated
|
||||
// instead of removing it immediately we rename its ID and queue it
|
||||
// to be deleted in applyStyles after a new version is fetched and applied
|
||||
const deadID = id + '-ghost';
|
||||
el.id = ID_PREFIX + deadID;
|
||||
// in case something went wrong and new style was never applied
|
||||
retiredStyleTimers.set(deadID, setTimeout(removeStyle, 1000, {id: deadID}));
|
||||
} else {
|
||||
docRootObserver.evade(() => el.remove());
|
||||
}
|
||||
}
|
||||
styleElements.delete(ID_PREFIX + id);
|
||||
disabledElements.delete(id);
|
||||
retiredStyleTimers.delete(id);
|
||||
if (styleElements.delete(id)) {
|
||||
updateCount();
|
||||
}
|
||||
}
|
||||
|
||||
function applyStyles(styles) {
|
||||
if (!styles) {
|
||||
// Chrome is starting up
|
||||
requestStyles();
|
||||
function applyStyles(sections, done) {
|
||||
if (!Object.keys(sections).length) {
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -234,72 +307,40 @@
|
|||
new MutationObserver((mutations, observer) => {
|
||||
if (document.documentElement) {
|
||||
observer.disconnect();
|
||||
applyStyles(styles);
|
||||
applyStyles(sections, done);
|
||||
}
|
||||
}).observe(document, {childList: true});
|
||||
return;
|
||||
}
|
||||
|
||||
if ('disableAll' in styles) {
|
||||
doDisableAll(styles.disableAll);
|
||||
}
|
||||
if ('exposeIframes' in styles) {
|
||||
doExposeIframes(styles.exposeIframes);
|
||||
}
|
||||
|
||||
const gotNewStyles = styles.length || styles.needTransitionPatch;
|
||||
if (gotNewStyles) {
|
||||
if (docRootObserver) {
|
||||
docRootObserver.stop();
|
||||
} else {
|
||||
initDocRootObserver();
|
||||
}
|
||||
}
|
||||
|
||||
if (styles.needTransitionPatch) {
|
||||
applyTransitionPatch();
|
||||
}
|
||||
|
||||
if (gotNewStyles) {
|
||||
for (const id in styles) {
|
||||
const sections = styles[id];
|
||||
if (!Array.isArray(sections)) continue;
|
||||
applySections(id, sections.map(({code}) => code).join('\n'));
|
||||
const pending = [];
|
||||
for (const section of Object.values(sections)) {
|
||||
pending.push(applySections(section.id, section.code.join('')));
|
||||
}
|
||||
docRootObserver.firstStart();
|
||||
}
|
||||
|
||||
if (FF_BUG461 && (gotNewStyles || styles.needTransitionPatch)) {
|
||||
setContentsInPageContext();
|
||||
}
|
||||
|
||||
if (!isOwnPage && !docRewriteObserver && styleElements.size) {
|
||||
initDocRewriteObserver();
|
||||
}
|
||||
|
||||
if (retiredStyleTimers.size) {
|
||||
setTimeout(() => {
|
||||
for (const [id, timer] of retiredStyleTimers.entries()) {
|
||||
removeStyle({id});
|
||||
clearTimeout(timer);
|
||||
}
|
||||
});
|
||||
updateExposeIframes();
|
||||
updateCount();
|
||||
if (done) {
|
||||
Promise.all(pending).then(done);
|
||||
}
|
||||
}
|
||||
|
||||
function applySections(styleId, code) {
|
||||
const id = ID_PREFIX + styleId;
|
||||
let el = styleElements.get(id) || document.getElementById(id);
|
||||
if (el && el.textContent !== code) {
|
||||
if (CHROME < 3321) {
|
||||
function applySections(id, code) {
|
||||
let el = styleElements.get(id) || document.getElementById(ID_PREFIX + id);
|
||||
if (el && CHROME < 3321) {
|
||||
// workaround for Chrome devtools bug fixed in v65
|
||||
el.remove();
|
||||
el = null;
|
||||
} else if (FF_BUG461) {
|
||||
pageContextQueue.push({id: el.id, el, code});
|
||||
} else {
|
||||
el.textContent = code;
|
||||
}
|
||||
}
|
||||
if (!el) {
|
||||
if (document.documentElement instanceof SVGSVGElement) {
|
||||
|
@ -312,48 +353,19 @@
|
|||
// HTML document style; also works on HTML-embedded SVG
|
||||
el = document.createElement('style');
|
||||
}
|
||||
el.id = id;
|
||||
el.id = ID_PREFIX + id;
|
||||
el.type = 'text/css';
|
||||
// SVG className is not a string, but an instance of SVGAnimatedString
|
||||
el.classList.add('stylus');
|
||||
if (FF_BUG461) {
|
||||
pageContextQueue.push({id: el.id, el, code});
|
||||
} else {
|
||||
el.textContent = code;
|
||||
}
|
||||
addStyleElement(el);
|
||||
}
|
||||
styleElements.set(id, el);
|
||||
disabledElements.delete(Number(styleId));
|
||||
return el;
|
||||
}
|
||||
|
||||
function setContentsInPageContext() {
|
||||
try {
|
||||
(document.head || ROOT).appendChild(document.createElement('script')).text = `(${queue => {
|
||||
document.currentScript.remove();
|
||||
for (const {id, code} of queue) {
|
||||
const el = document.getElementById(id) ||
|
||||
document.querySelector('style.stylus[id="' + id + '"]');
|
||||
if (!el) continue;
|
||||
const {disabled} = el.sheet;
|
||||
el.textContent = code;
|
||||
el.sheet.disabled = disabled;
|
||||
}
|
||||
}})(${JSON.stringify(pageContextQueue)})`;
|
||||
} catch (e) {}
|
||||
let failedSome;
|
||||
for (const {el, code} of pageContextQueue) {
|
||||
let settingStyle;
|
||||
if (el.textContent !== code) {
|
||||
el.textContent = code;
|
||||
failedSome = true;
|
||||
settingStyle = setStyleContent(el, code);
|
||||
}
|
||||
}
|
||||
if (failedSome) {
|
||||
console.debug('Could not set code of some styles in page context, ' +
|
||||
'see https://github.com/openstyles/stylus/issues/461');
|
||||
}
|
||||
pageContextQueue.length = 0;
|
||||
styleElements.set(id, el);
|
||||
disabledElements.delete(id);
|
||||
return Promise.resolve(settingStyle);
|
||||
}
|
||||
|
||||
function addStyleElement(newElement) {
|
||||
|
@ -371,34 +383,32 @@
|
|||
if (next === newElement.nextElementSibling) {
|
||||
return;
|
||||
}
|
||||
docRootObserver.evade(() => {
|
||||
const insert = () => {
|
||||
ROOT.insertBefore(newElement, next || null);
|
||||
if (disableAll) {
|
||||
newElement.disabled = true;
|
||||
}
|
||||
});
|
||||
};
|
||||
if (docRootObserver) {
|
||||
docRootObserver.evade(insert);
|
||||
} else {
|
||||
insert();
|
||||
}
|
||||
}
|
||||
|
||||
function replaceAll(newStyles) {
|
||||
if ('disableAll' in newStyles &&
|
||||
disableAll === newStyles.disableAll &&
|
||||
styleElements.size === countStylesInHash(newStyles) &&
|
||||
[...styleElements.values()].every(el =>
|
||||
el.disabled === disableAll &&
|
||||
el.parentNode === ROOT &&
|
||||
el.textContent === (newStyles[getStyleId(el)] || []).map(({code}) => code).join('\n'))) {
|
||||
return;
|
||||
}
|
||||
const oldStyles = Array.prototype.slice.call(
|
||||
document.querySelectorAll(`style.stylus[id^="${ID_PREFIX}"]`));
|
||||
oldStyles.forEach(el => (el.id += '-ghost'));
|
||||
styleElements.clear();
|
||||
disabledElements.clear();
|
||||
[...retiredStyleTimers.values()].forEach(clearTimeout);
|
||||
retiredStyleTimers.clear();
|
||||
applyStyles(newStyles);
|
||||
docRootObserver.evade(() =>
|
||||
oldStyles.forEach(el => el.remove()));
|
||||
const removeOld = () => oldStyles.forEach(el => el.remove());
|
||||
if (docRewriteObserver) {
|
||||
docRootObserver.evade(removeOld);
|
||||
} else {
|
||||
removeOld();
|
||||
}
|
||||
}
|
||||
|
||||
function applyTransitionPatch() {
|
||||
|
@ -408,11 +418,14 @@
|
|||
const docId = document.documentElement.id ? '#' + document.documentElement.id : '';
|
||||
document.documentElement.classList.add(className);
|
||||
applySections(0, `
|
||||
${docId}.${className}:root * {
|
||||
${docId}.${CSS.escape(className)}:root * {
|
||||
transition: none !important;
|
||||
}
|
||||
`);
|
||||
setTimeout(() => {
|
||||
`)
|
||||
.then(() => {
|
||||
// repaint
|
||||
// eslint-disable-next-line no-unused-expressions
|
||||
document.documentElement.offsetWidth;
|
||||
removeStyle({id: 0});
|
||||
document.documentElement.classList.remove(className);
|
||||
});
|
||||
|
@ -422,15 +435,10 @@
|
|||
return parseInt(el.id.substr(ID_PREFIX.length));
|
||||
}
|
||||
|
||||
function countStylesInHash(styleHash) {
|
||||
let num = 0;
|
||||
for (const k in styleHash) {
|
||||
num += !isNaN(parseInt(k)) ? 1 : 0;
|
||||
function orphanCheck(e) {
|
||||
if (e && e.detail.method !== 'orphan') {
|
||||
return;
|
||||
}
|
||||
return num;
|
||||
}
|
||||
|
||||
function orphanCheck() {
|
||||
if (chrome.i18n && chrome.i18n.getUILanguage()) {
|
||||
return true;
|
||||
}
|
||||
|
@ -439,7 +447,7 @@
|
|||
[docRewriteObserver, docRootObserver].forEach(ob => ob && ob.disconnect());
|
||||
window.removeEventListener(chrome.runtime.id, orphanCheck, true);
|
||||
try {
|
||||
chrome.runtime.onMessage.removeListener(applyOnMessage);
|
||||
msg.off(applyOnMessage);
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* global API */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
@ -33,11 +34,10 @@
|
|||
&& event.data.type === 'ouc-is-installed'
|
||||
&& allowedOrigins.includes(event.origin)
|
||||
) {
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'findUsercss',
|
||||
API.findUsercss({
|
||||
name: event.data.name,
|
||||
namespace: event.data.namespace
|
||||
}, style => {
|
||||
}).then(style => {
|
||||
const data = {event};
|
||||
const callbackObject = {
|
||||
installed: Boolean(style),
|
||||
|
@ -129,12 +129,10 @@
|
|||
&& event.data.type === 'ouc-install-usercss'
|
||||
&& allowedOrigins.includes(event.origin)
|
||||
) {
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'saveUsercss',
|
||||
reason: 'install',
|
||||
API.installUsercss({
|
||||
name: event.data.title,
|
||||
sourceCode: event.data.code,
|
||||
}, style => {
|
||||
}).then(style => {
|
||||
sendInstallCallback({
|
||||
enabled: style.enabled,
|
||||
key: event.data.key
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* global API */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
@ -16,8 +17,8 @@
|
|||
let sourceCode, port, timer;
|
||||
|
||||
chrome.runtime.onConnect.addListener(onConnected);
|
||||
chrome.runtime.sendMessage({method: 'installUsercss', url}, r =>
|
||||
r && r.__ERROR__ && alert(r.__ERROR__));
|
||||
API.openUsercssInstallPage({url})
|
||||
.catch(err => alert(err));
|
||||
|
||||
function onConnected(newPort) {
|
||||
port = newPort;
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global cloneInto */
|
||||
/* global cloneInto msg API */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
@ -8,7 +8,7 @@
|
|||
document.addEventListener('stylishInstallChrome', onClick);
|
||||
document.addEventListener('stylishUpdateChrome', onClick);
|
||||
|
||||
chrome.runtime.onMessage.addListener(onMessage);
|
||||
msg.on(onMessage);
|
||||
|
||||
onDOMready().then(() => {
|
||||
window.postMessage({
|
||||
|
@ -30,10 +30,9 @@
|
|||
gotBody = true;
|
||||
// TODO: remove the following statement when USO pagination title is fixed
|
||||
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: ');
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
API.findStyle({
|
||||
md5Url: getMeta('stylish-md5-url') || location.href
|
||||
}, checkUpdatability);
|
||||
}).then(checkUpdatability);
|
||||
}
|
||||
if (document.getElementById('install_button')) {
|
||||
onDOMready().then(() => {
|
||||
|
@ -44,16 +43,14 @@
|
|||
}
|
||||
}
|
||||
|
||||
function onMessage(msg, sender, sendResponse) {
|
||||
function onMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'ping':
|
||||
// orphaned content script check
|
||||
sendResponse(true);
|
||||
break;
|
||||
return true;
|
||||
case 'openSettings':
|
||||
openSettings();
|
||||
sendResponse(true);
|
||||
break;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -69,7 +66,7 @@
|
|||
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
|
||||
}
|
||||
|
||||
function checkUpdatability([installedStyle]) {
|
||||
function checkUpdatability(installedStyle) {
|
||||
// TODO: remove the following statement when USO is fixed
|
||||
document.dispatchEvent(new CustomEvent('stylusFixBuggyUSOsettings', {
|
||||
detail: installedStyle && installedStyle.updateUrl,
|
||||
|
@ -148,10 +145,9 @@
|
|||
|
||||
function onUpdate() {
|
||||
return new Promise((resolve, reject) => {
|
||||
chrome.runtime.sendMessage({
|
||||
method: 'getStyles',
|
||||
md5Url: getMeta('stylish-md5-url') || location.href,
|
||||
}, ([style]) => {
|
||||
API.findStyle({
|
||||
md5Url: getMeta('stylish-md5-url') || location.href
|
||||
}, true).then(style => {
|
||||
saveStyleCode('styleUpdate', style.name, {id: style.id})
|
||||
.then(resolve, reject);
|
||||
});
|
||||
|
@ -160,35 +156,26 @@
|
|||
|
||||
|
||||
function saveStyleCode(message, name, addProps) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const isNew = message === 'styleInstall';
|
||||
const needsConfirmation = isNew || !saveStyleCode.confirmed;
|
||||
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
|
||||
reject();
|
||||
return;
|
||||
return Promise.reject();
|
||||
}
|
||||
saveStyleCode.confirmed = true;
|
||||
enableUpdateButton(false);
|
||||
getStyleJson().then(json => {
|
||||
return getStyleJson().then(json => {
|
||||
if (!json) {
|
||||
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
|
||||
'https://github.com/openstyles/stylus/issues/195');
|
||||
return;
|
||||
}
|
||||
chrome.runtime.sendMessage(
|
||||
Object.assign(json, addProps, {
|
||||
method: 'saveStyle',
|
||||
reason: isNew ? 'install' : 'update',
|
||||
}),
|
||||
style => {
|
||||
return API.installStyle(Object.assign(json, addProps))
|
||||
.then(style => {
|
||||
if (!isNew && style.updateUrl.includes('?')) {
|
||||
enableUpdateButton(true);
|
||||
} else {
|
||||
sendEvent({type: 'styleInstalledChrome'});
|
||||
}
|
||||
}
|
||||
);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -216,25 +203,18 @@
|
|||
|
||||
|
||||
function getResource(url, options) {
|
||||
return new Promise(resolve => {
|
||||
if (url.startsWith('#')) {
|
||||
resolve(document.getElementById(url.slice(1)).textContent);
|
||||
} else {
|
||||
chrome.runtime.sendMessage(Object.assign({
|
||||
return Promise.resolve(document.getElementById(url.slice(1)).textContent);
|
||||
}
|
||||
return API.download(Object.assign({
|
||||
url,
|
||||
method: 'download',
|
||||
timeout: 60e3,
|
||||
// USO can't handle POST requests for style json
|
||||
body: null,
|
||||
}, options), result => {
|
||||
const error = result && result.__ERROR__;
|
||||
if (error) {
|
||||
}, options))
|
||||
.catch(error => {
|
||||
alert('Error' + (error ? '\n' + error : ''));
|
||||
} else {
|
||||
resolve(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -257,12 +237,12 @@
|
|||
if (codeElement && !codeElement.textContent.trim()) {
|
||||
return style;
|
||||
}
|
||||
return getResource(getMeta('stylish-update-url')).then(code => new Promise(resolve => {
|
||||
chrome.runtime.sendMessage({method: 'parseCss', code}, ({sections}) => {
|
||||
style.sections = sections;
|
||||
resolve(style);
|
||||
return getResource(getMeta('stylish-update-url'))
|
||||
.then(code => API.parseCss({code}))
|
||||
.then(result => {
|
||||
style.sections = result.sections;
|
||||
return style;
|
||||
});
|
||||
}));
|
||||
})
|
||||
.then(tryFixMd5)
|
||||
.catch(() => null);
|
||||
|
@ -349,7 +329,7 @@
|
|||
document.removeEventListener('stylishInstallChrome', onClick);
|
||||
document.removeEventListener('stylishUpdateChrome', onClick);
|
||||
try {
|
||||
chrome.runtime.onMessage.removeListener(onMessage);
|
||||
msg.off(onMessage);
|
||||
} catch (e) {}
|
||||
}
|
||||
})();
|
||||
|
|
77
edit.html
77
edit.html
|
@ -18,26 +18,6 @@
|
|||
}
|
||||
</style>
|
||||
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="edit/util.js"></script>
|
||||
<script src="edit/regexp-tester.js"></script>
|
||||
<script src="edit/applies-to-line-widget.js"></script>
|
||||
<script src="edit/source-editor.js"></script>
|
||||
<script src="edit/colorpicker-helper.js"></script>
|
||||
<script src="edit/beautify.js"></script>
|
||||
<script src="edit/sections.js"></script>
|
||||
<script src="edit/show-keymap-help.js"></script>
|
||||
<script src="edit/codemirror-editing-hooks.js"></script>
|
||||
<script src="edit/edit.js"></script>
|
||||
|
||||
<script src="msgbox/msgbox.js" async></script>
|
||||
|
||||
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/lib/codemirror.js"></script>
|
||||
|
||||
|
@ -46,6 +26,8 @@
|
|||
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/addon/dialog/dialog.js"></script>
|
||||
|
||||
<script src="vendor/codemirror/addon/edit/closebrackets.js"></script>
|
||||
|
||||
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
|
||||
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
|
||||
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
|
||||
|
@ -80,6 +62,18 @@
|
|||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorview.js"></script>
|
||||
|
||||
<script src="js/promisify.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/script-loader.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/worker-util.js"></script>
|
||||
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<link href="edit/global-search.css" rel="stylesheet">
|
||||
<script src="edit/global-search.js"></script>
|
||||
|
||||
|
@ -88,6 +82,25 @@
|
|||
<link href="edit/codemirror-default.css" rel="stylesheet">
|
||||
<script src="edit/codemirror-default.js"></script>
|
||||
|
||||
<script src="edit/util.js"></script>
|
||||
<script src="edit/regexp-tester.js"></script>
|
||||
<script src="edit/live-preview.js"></script>
|
||||
<script src="edit/applies-to-line-widget.js"></script>
|
||||
<script src="edit/reroute-hotkeys.js"></script>
|
||||
<script src="edit/codemirror-factory.js"></script>
|
||||
<script src="edit/colorpicker-helper.js"></script>
|
||||
<script src="edit/beautify.js"></script>
|
||||
<script src="edit/show-keymap-help.js"></script>
|
||||
<script src="edit/refresh-on-view.js"></script>
|
||||
|
||||
<script src="edit/source-editor.js"></script>
|
||||
<script src="edit/sections-editor-section.js"></script>
|
||||
<script src="edit/sections-editor.js"></script>
|
||||
|
||||
<script src="edit/edit.js"></script>
|
||||
|
||||
<script src="msgbox/msgbox.js" async></script>
|
||||
|
||||
<script src="edit/linter.js"></script>
|
||||
<script src="edit/linter-defaults.js"></script>
|
||||
<script src="edit/linter-engines.js"></script>
|
||||
|
@ -96,8 +109,6 @@
|
|||
<script src="edit/linter-report.js"></script>
|
||||
<script src="edit/linter-config-dialog.js"></script>
|
||||
|
||||
<script src="edit/editor-worker.js"></script>
|
||||
|
||||
<link id="cm-theme" rel="stylesheet">
|
||||
|
||||
<template data-id="appliesTo">
|
||||
|
@ -133,8 +144,11 @@
|
|||
|
||||
<template data-id="section">
|
||||
<div class="section">
|
||||
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
|
||||
<p class="deleted-section">
|
||||
<button class="restore-section" i18n-text="sectionRestore"></button>
|
||||
</p>
|
||||
<label i18n-text="sectionCode" class="code-label"></label>
|
||||
<br>
|
||||
<div class="applies-to">
|
||||
<label i18n-text="appliesLabel">
|
||||
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0">
|
||||
|
@ -155,13 +169,6 @@
|
|||
</div>
|
||||
</template>
|
||||
|
||||
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
|
||||
<template data-id="deletedSection">
|
||||
<p class="deleted-section">
|
||||
<button class="restore-section" i18n-text="sectionRestore"></button>
|
||||
</p>
|
||||
</template>
|
||||
|
||||
<template data-id="searchReplaceDialog">
|
||||
<div id="search-replace-dialog">
|
||||
<div data-type="main">
|
||||
|
@ -277,7 +284,7 @@
|
|||
<h1 id="heading"> </h1> <!-- nbsp allocates the actual height which prevents page shift -->
|
||||
<section id="basic-info">
|
||||
<div id="basic-info-name">
|
||||
<input id="name" class="style-contributor" spellcheck="false">
|
||||
<input id="name" class="style-contributor" spellcheck="false" required>
|
||||
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
|
||||
</div>
|
||||
<div id="basic-info-enabled">
|
||||
|
@ -437,11 +444,15 @@
|
|||
</div>
|
||||
</div>
|
||||
<section id="sections">
|
||||
<h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span>
|
||||
<!--
|
||||
It seems that we don't use these anymore
|
||||
https://github.com/openstyles/stylus/blob/5cbe8a8d780a6eb9fce11d5846e92bf244c3a3f3/edit/sections.js#L18
|
||||
-->
|
||||
<!-- <h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span>
|
||||
<a id="sections-help" href="#" class="svg-inline-wrapper" tabindex="0">
|
||||
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
|
||||
</a>
|
||||
</h2>
|
||||
</h2> -->
|
||||
</section>
|
||||
<div id="help-popup">
|
||||
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global regExpTester debounce messageBox CodeMirror template colorMimicry */
|
||||
/* global regExpTester debounce messageBox CodeMirror template colorMimicry msg
|
||||
$ $create t prefs tryCatch */
|
||||
/* exported createAppliesToLineWidget */
|
||||
'use strict';
|
||||
|
||||
function createAppliesToLineWidget(cm) {
|
||||
|
@ -131,7 +133,7 @@ function createAppliesToLineWidget(cm) {
|
|||
cm.on('change', onChange);
|
||||
cm.on('optionChange', onOptionChange);
|
||||
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
requestAnimationFrame(updateWidgetStyle);
|
||||
update();
|
||||
|
@ -144,7 +146,7 @@ function createAppliesToLineWidget(cm) {
|
|||
widgets.length = 0;
|
||||
cm.off('change', onChange);
|
||||
cm.off('optionChange', onOptionChange);
|
||||
chrome.runtime.onMessage.removeListener(onRuntimeMessage);
|
||||
msg.off(onRuntimeMessage);
|
||||
actualStyle.remove();
|
||||
}
|
||||
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
/*
|
||||
global CodeMirror loadScript css_beautify
|
||||
global editors getSectionForChild showHelp
|
||||
*/
|
||||
/* global loadScript css_beautify showHelp prefs t $ $create */
|
||||
/* exported beautify */
|
||||
'use strict';
|
||||
|
||||
function beautify(event) {
|
||||
function beautify(scope) {
|
||||
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js')
|
||||
.then(() => {
|
||||
if (!window.css_beautify && window.exports) {
|
||||
|
@ -22,9 +20,6 @@ function beautify(event) {
|
|||
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
|
||||
options.indent_char = tabs ? '\t' : ' ';
|
||||
|
||||
const section = getSectionForChild(event.target);
|
||||
const scope = section ? [section.CodeMirror] : editors;
|
||||
|
||||
showHelp(t('styleBeautify'),
|
||||
$create([
|
||||
$create('.beautify-options', [
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global CodeMirror prefs loadScript editor editors */
|
||||
/* global CodeMirror prefs loadScript editor $ template */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -9,6 +9,7 @@
|
|||
}
|
||||
|
||||
const defaults = {
|
||||
autoCloseBrackets: prefs.get('editor.autoCloseBrackets'),
|
||||
mode: 'css',
|
||||
lineNumbers: true,
|
||||
lineWrapping: prefs.get('editor.lineWrapping'),
|
||||
|
@ -19,11 +20,10 @@
|
|||
...(prefs.get('editor.linter') ? ['CodeMirror-lint-markers'] : []),
|
||||
],
|
||||
matchBrackets: true,
|
||||
highlightSelectionMatches: {showToken: /[#.\-\w]/, annotateScrollbar: true},
|
||||
hintOptions: {},
|
||||
lintReportDelay: prefs.get('editor.lintReportDelay'),
|
||||
styleActiveLine: true,
|
||||
theme: 'default',
|
||||
theme: prefs.get('editor.theme'),
|
||||
keyMap: prefs.get('editor.keyMap'),
|
||||
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
|
||||
// independent of current keyMap
|
||||
|
@ -228,69 +228,48 @@
|
|||
return isBlank;
|
||||
});
|
||||
|
||||
// doubleclick option
|
||||
if (typeof editors !== 'undefined') {
|
||||
const fn = (cm, repeat) =>
|
||||
repeat === 'double' ?
|
||||
{unit: selectTokenOnDoubleclick} :
|
||||
{};
|
||||
const configure = (_, enabled) => {
|
||||
editors.forEach(cm => cm.setOption('configureMouse', enabled ? fn : null));
|
||||
CodeMirror.defaults.configureMouse = enabled ? fn : null;
|
||||
};
|
||||
configure(null, prefs.get('editor.selectByTokens'));
|
||||
prefs.subscribe(['editor.selectByTokens'], configure);
|
||||
// editor commands
|
||||
for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
|
||||
CodeMirror.commands[name] = () => editor[name]();
|
||||
}
|
||||
|
||||
function selectTokenOnDoubleclick(cm, pos) {
|
||||
let {ch} = pos;
|
||||
const {line, sticky} = pos;
|
||||
const {text, styles} = cm.getLineHandle(line);
|
||||
// CodeMirror convenience commands
|
||||
Object.assign(CodeMirror.commands, {
|
||||
toggleEditorFocus,
|
||||
jumpToLine,
|
||||
commentSelection,
|
||||
});
|
||||
|
||||
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text);
|
||||
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text);
|
||||
const atWord = ch => at(/\w/y, ch);
|
||||
const atSpace = ch => at(/\s/y, ch);
|
||||
|
||||
const atTokenEnd = styles.indexOf(ch, 1);
|
||||
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1;
|
||||
ch = Math.min(text.length, ch);
|
||||
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)});
|
||||
if (atTokenEnd > 0) ch--;
|
||||
|
||||
const isCss = type && !/^(comment|string)/.test(type);
|
||||
const isNumber = type === 'number';
|
||||
const isSpace = atSpace(ch);
|
||||
let wordChars =
|
||||
isNumber ? /[-+\w.%]/y :
|
||||
isCss ? /[-\w@]/y :
|
||||
isSpace ? /\s/y :
|
||||
atWord(ch) ? /\w/y : /[^\w\s]/y;
|
||||
|
||||
let a = ch;
|
||||
while (a && at(wordChars, a)) a--;
|
||||
a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1);
|
||||
|
||||
let b, found;
|
||||
|
||||
if (isNumber) {
|
||||
b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length;
|
||||
found = b >= ch;
|
||||
if (!found) {
|
||||
a = b;
|
||||
ch = a;
|
||||
function jumpToLine(cm) {
|
||||
const cur = cm.getCursor();
|
||||
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
|
||||
if (oldDialog) {
|
||||
// close the currently opened minidialog
|
||||
cm.focus();
|
||||
}
|
||||
// make sure to focus the input in newly opened minidialog
|
||||
// setTimeout(() => {
|
||||
// $('.CodeMirror-dialog', section).focus();
|
||||
// });
|
||||
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
||||
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
||||
if (m) {
|
||||
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
||||
}
|
||||
}, {value: cur.line + 1});
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy');
|
||||
b = ch + execAt(wordChars, ch)[0].length;
|
||||
function commentSelection(cm) {
|
||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||
}
|
||||
|
||||
return {
|
||||
from: {line, ch: a},
|
||||
to: {line, ch: b},
|
||||
};
|
||||
function toggleEditorFocus(cm) {
|
||||
if (!cm) return;
|
||||
if (cm.hasFocus()) {
|
||||
setTimeout(() => cm.display.input.blur());
|
||||
} else {
|
||||
cm.focus();
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
|
@ -371,8 +350,9 @@ CodeMirror.hint && (() => {
|
|||
}
|
||||
|
||||
// USO vars in usercss mode editor
|
||||
const list = Object.keys(editor.getStyle().usercssData.vars)
|
||||
.filter(name => name.startsWith(leftPart));
|
||||
const vars = editor.getStyle().usercssData.vars;
|
||||
const list = vars ?
|
||||
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
|
||||
return {
|
||||
list,
|
||||
from: {line, ch: prev},
|
||||
|
|
|
@ -1,698 +0,0 @@
|
|||
/*
|
||||
global CodeMirror loadScript
|
||||
global editors editor styleId ownTabId
|
||||
global save toggleStyle setupAutocomplete makeSectionVisible getSectionForChild
|
||||
global getSectionsHashes
|
||||
global messageBox
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
onDOMscriptReady('/codemirror.js').then(() => {
|
||||
const COMMANDS = {
|
||||
save,
|
||||
toggleStyle,
|
||||
toggleEditorFocus,
|
||||
jumpToLine,
|
||||
nextEditor, prevEditor,
|
||||
commentSelection,
|
||||
};
|
||||
const ORIGINAL_COMMANDS = {
|
||||
insertTab: CodeMirror.commands.insertTab,
|
||||
};
|
||||
// reroute handling to nearest editor when keypress resolves to one of these commands
|
||||
const REROUTED = new Set([
|
||||
'save',
|
||||
'toggleStyle',
|
||||
'jumpToLine',
|
||||
'nextEditor', 'prevEditor',
|
||||
'toggleEditorFocus',
|
||||
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
||||
'colorpicker',
|
||||
]);
|
||||
Object.assign(CodeMirror, {
|
||||
getOption,
|
||||
setOption,
|
||||
closestVisible,
|
||||
});
|
||||
Object.assign(CodeMirror.prototype, {
|
||||
getSection,
|
||||
rerouteHotkeys,
|
||||
});
|
||||
|
||||
CodeMirror.defineInitHook(cm => {
|
||||
if (!cm.display.wrapper.closest('#sections')) {
|
||||
return;
|
||||
}
|
||||
if (prefs.get('editor.livePreview') && styleId) {
|
||||
cm.on('changes', updatePreview);
|
||||
}
|
||||
if (prefs.get('editor.autocompleteOnTyping')) {
|
||||
setupAutocomplete(cm);
|
||||
}
|
||||
const wrapper = cm.display.wrapper;
|
||||
cm.on('blur', () => {
|
||||
editors.lastActive = cm;
|
||||
cm.rerouteHotkeys(true);
|
||||
setTimeout(() => {
|
||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
||||
});
|
||||
});
|
||||
cm.on('focus', () => {
|
||||
cm.rerouteHotkeys(false);
|
||||
wrapper.classList.add('CodeMirror-active');
|
||||
});
|
||||
});
|
||||
|
||||
new MutationObserver((mutations, observer) => {
|
||||
if (!$('#sections')) {
|
||||
return;
|
||||
}
|
||||
observer.disconnect();
|
||||
|
||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||
showHotkeyInTooltip();
|
||||
|
||||
// N.B. the onchange event listeners should be registered before setupLivePrefs()
|
||||
$('#options').addEventListener('change', onOptionElementChanged);
|
||||
setupLivePreview();
|
||||
buildThemeElement();
|
||||
buildKeymapElement();
|
||||
setupLivePrefs();
|
||||
|
||||
Object.assign(CodeMirror.commands, COMMANDS);
|
||||
rerouteHotkeys(true);
|
||||
}).observe(document, {childList: true, subtree: true});
|
||||
|
||||
return;
|
||||
|
||||
////////////////////////////////////////////////
|
||||
|
||||
function getOption(o) {
|
||||
return CodeMirror.defaults[o];
|
||||
}
|
||||
|
||||
function setOption(o, v) {
|
||||
CodeMirror.defaults[o] = v;
|
||||
if (editors.length > 4 && (o === 'theme' || o === 'lineWrapping')) {
|
||||
throttleSetOption({key: o, value: v, index: 0});
|
||||
return;
|
||||
}
|
||||
editors.forEach(editor => {
|
||||
editor.setOption(o, v);
|
||||
});
|
||||
}
|
||||
|
||||
function throttleSetOption({
|
||||
key,
|
||||
value,
|
||||
index,
|
||||
timeStart = performance.now(),
|
||||
cmStart = editors.lastActive || editors[0],
|
||||
editorsCopy = editors.slice(),
|
||||
progress,
|
||||
}) {
|
||||
if (index === 0) {
|
||||
if (!cmStart) {
|
||||
return;
|
||||
}
|
||||
cmStart.setOption(key, value);
|
||||
}
|
||||
|
||||
const THROTTLE_AFTER_MS = 100;
|
||||
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
|
||||
|
||||
const t0 = performance.now();
|
||||
const total = editorsCopy.length;
|
||||
while (index < total) {
|
||||
const cm = editorsCopy[index++];
|
||||
if (cm === cmStart ||
|
||||
cm !== editors[index] && !editors.includes(cm)) {
|
||||
continue;
|
||||
}
|
||||
cm.setOption(key, value);
|
||||
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= total) {
|
||||
$.remove(progress);
|
||||
return;
|
||||
}
|
||||
if (!progress &&
|
||||
index < total / 2 &&
|
||||
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
||||
let option = $('#editor.' + key);
|
||||
if (option) {
|
||||
if (option.type === 'checkbox') {
|
||||
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
||||
}
|
||||
progress = document.body.appendChild(
|
||||
$create('.set-option-progress', {targetElement: option}));
|
||||
}
|
||||
}
|
||||
if (progress) {
|
||||
const optionBounds = progress.targetElement.getBoundingClientRect();
|
||||
const bounds = {
|
||||
top: optionBounds.top + window.scrollY + 1,
|
||||
left: optionBounds.left + window.scrollX + 1,
|
||||
width: (optionBounds.width - 2) * index / total | 0,
|
||||
height: optionBounds.height - 2,
|
||||
};
|
||||
const style = progress.style;
|
||||
for (const prop in bounds) {
|
||||
if (bounds[prop] !== parseFloat(style[prop])) {
|
||||
style[prop] = bounds[prop] + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(throttleSetOption, 0, {
|
||||
key,
|
||||
value,
|
||||
index,
|
||||
timeStart,
|
||||
cmStart,
|
||||
editorsCopy,
|
||||
progress,
|
||||
});
|
||||
}
|
||||
|
||||
function getSection() {
|
||||
return this.display.wrapper.parentNode;
|
||||
}
|
||||
|
||||
function nextEditor(cm) {
|
||||
return nextPrevEditor(cm, 1);
|
||||
}
|
||||
|
||||
function prevEditor(cm) {
|
||||
return nextPrevEditor(cm, -1);
|
||||
}
|
||||
|
||||
function nextPrevEditor(cm, direction) {
|
||||
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
|
||||
makeSectionVisible(cm);
|
||||
cm.focus();
|
||||
return cm;
|
||||
}
|
||||
|
||||
function jumpToLine(cm) {
|
||||
const cur = cm.getCursor();
|
||||
refocusMinidialog(cm);
|
||||
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
|
||||
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
|
||||
if (m) {
|
||||
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
|
||||
}
|
||||
}, {value: cur.line + 1});
|
||||
}
|
||||
|
||||
function commentSelection(cm) {
|
||||
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
|
||||
}
|
||||
|
||||
function toggleEditorFocus(cm) {
|
||||
if (!cm) return;
|
||||
if (cm.hasFocus()) {
|
||||
setTimeout(() => cm.display.input.blur());
|
||||
} else {
|
||||
cm.focus();
|
||||
}
|
||||
}
|
||||
|
||||
function refocusMinidialog(cm) {
|
||||
const section = cm.getSection();
|
||||
if (!$('.CodeMirror-dialog', section)) {
|
||||
return;
|
||||
}
|
||||
// close the currently opened minidialog
|
||||
cm.focus();
|
||||
// make sure to focus the input in newly opened minidialog
|
||||
setTimeout(() => {
|
||||
$('.CodeMirror-dialog', section).focus();
|
||||
});
|
||||
}
|
||||
|
||||
function onOptionElementChanged(event) {
|
||||
const el = event.target;
|
||||
let option = el.id.replace(/^editor\./, '');
|
||||
if (!option) {
|
||||
console.error('no "cm_option"', el);
|
||||
return;
|
||||
}
|
||||
let value = el.type === 'checkbox' ? el.checked : el.value;
|
||||
switch (option) {
|
||||
case 'tabSize':
|
||||
value = Number(value);
|
||||
CodeMirror.setOption('indentUnit', value);
|
||||
break;
|
||||
|
||||
case 'indentWithTabs':
|
||||
CodeMirror.commands.insertTab = value ?
|
||||
ORIGINAL_COMMANDS.insertTab :
|
||||
CodeMirror.commands.insertSoftTab;
|
||||
break;
|
||||
|
||||
case 'theme': {
|
||||
const themeLink = $('#cm-theme');
|
||||
// use non-localized 'default' internally
|
||||
if (!value || value === 'default' || value === t('defaultTheme')) {
|
||||
value = 'default';
|
||||
if (prefs.get(el.id) !== value) {
|
||||
prefs.set(el.id, value);
|
||||
}
|
||||
themeLink.href = '';
|
||||
el.selectedIndex = 0;
|
||||
break;
|
||||
}
|
||||
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
||||
if (themeLink.href === url) {
|
||||
// preloaded in initCodeMirror()
|
||||
break;
|
||||
}
|
||||
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||
document.head.appendChild($create('link#cm-theme2', {rel: 'stylesheet', href: url}));
|
||||
setTimeout(() => {
|
||||
CodeMirror.setOption(option, value);
|
||||
themeLink.remove();
|
||||
$('#cm-theme2').id = 'cm-theme';
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
case 'autocompleteOnTyping':
|
||||
editors.forEach(cm => setupAutocomplete(cm, el.checked));
|
||||
return;
|
||||
|
||||
case 'autoCloseBrackets':
|
||||
Promise.resolve(value && loadScript('/vendor/codemirror/addon/edit/closebrackets.js')).then(() => {
|
||||
CodeMirror.setOption(option, value);
|
||||
});
|
||||
return;
|
||||
|
||||
case 'matchHighlight':
|
||||
switch (value) {
|
||||
case 'token':
|
||||
case 'selection':
|
||||
document.body.dataset[option] = value;
|
||||
value = {showToken: value === 'token' && /[#.\-\w]/, annotateScrollbar: true};
|
||||
break;
|
||||
default:
|
||||
value = null;
|
||||
}
|
||||
option = 'highlightSelectionMatches';
|
||||
break;
|
||||
|
||||
case 'colorpicker':
|
||||
return;
|
||||
}
|
||||
CodeMirror.setOption(option, value);
|
||||
}
|
||||
|
||||
function buildThemeElement() {
|
||||
const themeElement = $('#editor.theme');
|
||||
const themeList = localStorage.codeMirrorThemes;
|
||||
|
||||
const optionsFromArray = options => {
|
||||
const fragment = document.createDocumentFragment();
|
||||
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
||||
themeElement.appendChild(fragment);
|
||||
};
|
||||
|
||||
if (themeList) {
|
||||
optionsFromArray(themeList.split(/\s+/));
|
||||
} else {
|
||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||
const theme = prefs.get('editor.theme');
|
||||
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
|
||||
getCodeMirrorThemes().then(() => {
|
||||
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
||||
optionsFromArray(themes);
|
||||
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildKeymapElement() {
|
||||
// move 'pc' or 'mac' prefix to the end of the displayed label
|
||||
const maps = Object.keys(CodeMirror.keyMap)
|
||||
.map(name => ({
|
||||
value: name,
|
||||
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
||||
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
||||
}))
|
||||
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let bin = fragment;
|
||||
let groupName;
|
||||
// group suffixed maps in <optgroup>
|
||||
maps.forEach(({value, name}, i) => {
|
||||
groupName = !name.includes('-') ? name : groupName;
|
||||
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
||||
if (groupWithNext) {
|
||||
if (bin === fragment) {
|
||||
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
||||
}
|
||||
}
|
||||
const el = bin.appendChild($create('option', {value}, name));
|
||||
if (value === prefs.defaults['editor.keyMap']) {
|
||||
el.dataset.default = '';
|
||||
el.title = t('defaultTheme');
|
||||
}
|
||||
if (!groupWithNext) bin = fragment;
|
||||
});
|
||||
$('#editor.keyMap').appendChild(fragment);
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
|
||||
function rerouteHotkeys(enable, immediately) {
|
||||
if (!immediately) {
|
||||
debounce(rerouteHotkeys, 0, enable, true);
|
||||
} else if (enable) {
|
||||
document.addEventListener('keydown', rerouteHandler);
|
||||
} else {
|
||||
document.removeEventListener('keydown', rerouteHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function rerouteHandler(event) {
|
||||
const keyName = CodeMirror.keyName(event);
|
||||
if (!keyName) {
|
||||
return;
|
||||
}
|
||||
const rerouteCommand = name => {
|
||||
if (REROUTED.has(name)) {
|
||||
CodeMirror.commands[name](closestVisible(event.target));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
||||
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
|
||||
// priority:
|
||||
// 1. associated CM for applies-to element
|
||||
// 2. last active if visible
|
||||
// 3. first visible
|
||||
function closestVisible(nearbyElement) {
|
||||
const cm =
|
||||
nearbyElement instanceof CodeMirror ? nearbyElement :
|
||||
nearbyElement instanceof Node && (getSectionForChild(nearbyElement) || {}).CodeMirror ||
|
||||
editors.lastActive;
|
||||
if (nearbyElement instanceof Node && cm) {
|
||||
const {left, top} = nearbyElement.getBoundingClientRect();
|
||||
const bounds = cm.display.wrapper.getBoundingClientRect();
|
||||
if (top >= 0 && top >= bounds.top &&
|
||||
left >= 0 && left >= bounds.left) {
|
||||
return cm;
|
||||
}
|
||||
}
|
||||
// closest editor should have at least 2 lines visible
|
||||
const lineHeight = editors[0].defaultTextHeight();
|
||||
const scrollY = window.scrollY;
|
||||
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
||||
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
||||
const distances = [];
|
||||
const alreadyInView = cm && offscreenDistance(null, cm) === 0;
|
||||
return alreadyInView ? cm : findClosest();
|
||||
|
||||
function offscreenDistance(index, cm) {
|
||||
if (index >= 0 && distances[index] !== undefined) {
|
||||
return distances[index];
|
||||
}
|
||||
const section = (cm || editors[index]).getSection();
|
||||
if (!section) {
|
||||
return 1e9;
|
||||
}
|
||||
const top = allSectionsContainerTop + section.offsetTop;
|
||||
if (top < scrollY + lineHeight) {
|
||||
return Math.max(0, scrollY - top - lineHeight);
|
||||
}
|
||||
if (top < windowBottom) {
|
||||
return 0;
|
||||
}
|
||||
const distance = top - windowBottom + section.offsetHeight;
|
||||
if (index >= 0) {
|
||||
distances[index] = distance;
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
|
||||
function findClosest() {
|
||||
const last = editors.length - 1;
|
||||
let a = 0;
|
||||
let b = last;
|
||||
let c;
|
||||
let distance;
|
||||
while (a < b - 1) {
|
||||
c = (a + b) / 2 | 0;
|
||||
distance = offscreenDistance(c);
|
||||
if (!distance || !c) {
|
||||
break;
|
||||
}
|
||||
const distancePrev = offscreenDistance(c - 1);
|
||||
const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
|
||||
if (distancePrev <= distance && distance <= distanceNext) {
|
||||
b = c;
|
||||
} else {
|
||||
a = c;
|
||||
}
|
||||
}
|
||||
while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
|
||||
b--;
|
||||
}
|
||||
const cm = editors[b];
|
||||
if (distances[b] > 0) {
|
||||
makeSectionVisible(cm);
|
||||
}
|
||||
return cm;
|
||||
}
|
||||
}
|
||||
|
||||
////////////////////////////////////////////////
|
||||
|
||||
function getCodeMirrorThemes() {
|
||||
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme'),
|
||||
/* populate-theme-start */
|
||||
'3024-day',
|
||||
'3024-night',
|
||||
'abcdef',
|
||||
'ambiance',
|
||||
'ambiance-mobile',
|
||||
'base16-dark',
|
||||
'base16-light',
|
||||
'bespin',
|
||||
'blackboard',
|
||||
'cobalt',
|
||||
'colorforth',
|
||||
'darcula',
|
||||
'dracula',
|
||||
'duotone-dark',
|
||||
'duotone-light',
|
||||
'eclipse',
|
||||
'elegant',
|
||||
'erlang-dark',
|
||||
'gruvbox-dark',
|
||||
'hopscotch',
|
||||
'icecoder',
|
||||
'idea',
|
||||
'isotope',
|
||||
'lesser-dark',
|
||||
'liquibyte',
|
||||
'lucario',
|
||||
'material',
|
||||
'mbo',
|
||||
'mdn-like',
|
||||
'midnight',
|
||||
'monokai',
|
||||
'neat',
|
||||
'neo',
|
||||
'night',
|
||||
'oceanic-next',
|
||||
'panda-syntax',
|
||||
'paraiso-dark',
|
||||
'paraiso-light',
|
||||
'pastel-on-dark',
|
||||
'railscasts',
|
||||
'rubyblue',
|
||||
'seti',
|
||||
'shadowfox',
|
||||
'solarized',
|
||||
'ssms',
|
||||
'the-matrix',
|
||||
'tomorrow-night-bright',
|
||||
'tomorrow-night-eighties',
|
||||
'ttcn',
|
||||
'twilight',
|
||||
'vibrant-ink',
|
||||
'xq-dark',
|
||||
'xq-light',
|
||||
'yeti',
|
||||
'zenburn',
|
||||
/* populate-theme-end */
|
||||
];
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
return Promise.resolve(themes);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
||||
themeDir.createReader().readEntries(entries => {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme')
|
||||
].concat(
|
||||
entries.filter(entry => entry.isFile)
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||
);
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
resolve(themes);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||
for (const el of $$('[data-hotkey-tooltip]')) {
|
||||
if (el._hotkeyTooltipKeyMap !== mapName) {
|
||||
el._hotkeyTooltipKeyMap = mapName;
|
||||
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
||||
const cmd = el.dataset.hotkeyTooltip;
|
||||
const key = cmd[0] === '=' ? cmd.slice(1) :
|
||||
findKeyForCommand(cmd, mapName) ||
|
||||
extraKeys && findKeyForCommand(cmd, extraKeys);
|
||||
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
||||
if (el.title !== newTitle) el.title = newTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function findKeyForCommand(command, map) {
|
||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
||||
let key = Object.keys(map).find(k => map[k] === command);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
||||
key = ft && findKeyForCommand(command, ft);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function setupLivePreview() {
|
||||
if (!prefs.get('editor.livePreview') && !editors.length) {
|
||||
setTimeout(setupLivePreview);
|
||||
return;
|
||||
}
|
||||
if (styleId) {
|
||||
$('#editor.livePreview').onchange = livePreviewToggled;
|
||||
return;
|
||||
}
|
||||
// wait for #preview-label's class to lose 'hidden' after the first save
|
||||
new MutationObserver((_, observer) => {
|
||||
if (!styleId) return;
|
||||
observer.disconnect();
|
||||
setupLivePreview();
|
||||
livePreviewToggled();
|
||||
}).observe($('#preview-label'), {
|
||||
attributes: true,
|
||||
attributeFilter: ['class'],
|
||||
});
|
||||
}
|
||||
|
||||
function livePreviewToggled() {
|
||||
const me = this instanceof Node ? this : $('#editor.livePreview');
|
||||
const previewing = me.checked;
|
||||
editors.forEach(cm => cm[previewing ? 'on' : 'off']('changes', updatePreview));
|
||||
const addRemove = EventTarget.prototype[previewing ? 'addEventListener' : 'removeEventListener'];
|
||||
addRemove.call($('#enabled'), 'change', updatePreview);
|
||||
if (!editor) {
|
||||
for (const el of $$('#sections .applies-to')) {
|
||||
addRemove.call(el, 'input', updatePreview);
|
||||
}
|
||||
toggleLivePreviewSectionsObserver(previewing);
|
||||
}
|
||||
if (!previewing || document.body.classList.contains('dirty')) {
|
||||
updatePreview(null, previewing);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Observes newly added section elements, and sets these event listeners:
|
||||
* 1. 'changes' on CodeMirror inside
|
||||
* 2. 'input' on .applies-to inside
|
||||
* The goal is to avoid listening to 'input' on the entire #sections tree,
|
||||
* which would trigger updatePreview() twice on any keystroke -
|
||||
* both for the synthetic event from CodeMirror and the original event.
|
||||
* Side effects:
|
||||
* two expando properties on #sections
|
||||
* 1. __livePreviewObserver
|
||||
* 2. __livePreviewObserverEnabled
|
||||
* @param {Boolean} enable
|
||||
*/
|
||||
function toggleLivePreviewSectionsObserver(enable) {
|
||||
const sections = $('#sections');
|
||||
const observing = sections.__livePreviewObserverEnabled;
|
||||
let mo = sections.__livePreviewObserver;
|
||||
if (enable && !mo) {
|
||||
sections.__livePreviewObserver = mo = new MutationObserver(mutations => {
|
||||
for (const {addedNodes} of mutations) {
|
||||
for (const node of addedNodes) {
|
||||
const el = node.children && $('.applies-to', node);
|
||||
if (el) el.addEventListener('input', updatePreview);
|
||||
if (node.CodeMirror) node.CodeMirror.on('changes', updatePreview);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
if (enable && !observing) {
|
||||
mo.observe(sections, {childList: true});
|
||||
sections.__livePreviewObserverEnabled = true;
|
||||
} else if (!enable && observing) {
|
||||
mo.disconnect();
|
||||
sections.__livePreviewObserverEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
function updatePreview(data, previewing) {
|
||||
if (previewing !== true && previewing !== false) {
|
||||
if (data instanceof Event && !data.target.matches('.style-contributor')) return;
|
||||
debounce(updatePreview, data && data.id === 'enabled' ? 0 : 400, null, true);
|
||||
return;
|
||||
}
|
||||
const errors = $('#preview-errors');
|
||||
API.refreshAllTabs({
|
||||
reason: 'editPreview',
|
||||
tabId: ownTabId,
|
||||
style: {
|
||||
id: styleId,
|
||||
enabled: $('#enabled').checked,
|
||||
sections: previewing && (editor ? editors[0].getValue() : getSectionsHashes()),
|
||||
},
|
||||
}).then(() => {
|
||||
errors.classList.add('hidden');
|
||||
}).catch(err => {
|
||||
if (Array.isArray(err)) err = err.join('\n');
|
||||
if (err && editor && !isNaN(err.index)) {
|
||||
const pos = editors[0].posFromIndex(err.index);
|
||||
err = `${pos.line}:${pos.ch} ${err}`;
|
||||
}
|
||||
errors.classList.remove('hidden');
|
||||
errors.onclick = () => messageBox.alert(String(err), 'pre');
|
||||
});
|
||||
}
|
||||
});
|
287
edit/codemirror-factory.js
Normal file
287
edit/codemirror-factory.js
Normal file
|
@ -0,0 +1,287 @@
|
|||
/* global CodeMirror loadScript rerouteHotkeys prefs $ debounce $create */
|
||||
/* exported cmFactory */
|
||||
'use strict';
|
||||
/*
|
||||
All cm instances created by this module are collected so we can broadcast prefs
|
||||
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
|
||||
when the instance is not used anymore.
|
||||
*/
|
||||
const cmFactory = (() => {
|
||||
const editors = new Set();
|
||||
// used by `indentWithTabs` option
|
||||
const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab;
|
||||
const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab;
|
||||
|
||||
CodeMirror.defineOption('tabSize', prefs.get('editor.tabSize'), (cm, value) => {
|
||||
cm.setOption('indentUnit', Number(value));
|
||||
});
|
||||
|
||||
CodeMirror.defineOption('indentWithTabs', prefs.get('editor.indentWithTabs'), (cm, value) => {
|
||||
CodeMirror.commands.insertTab = value ?
|
||||
INSERT_TAB_COMMAND :
|
||||
INSERT_SOFT_TAB_COMMAND;
|
||||
});
|
||||
|
||||
CodeMirror.defineOption('autocompleteOnTyping', prefs.get('editor.autocompleteOnTyping'), (cm, value) => {
|
||||
const onOff = value ? 'on' : 'off';
|
||||
cm[onOff]('changes', autocompleteOnTyping);
|
||||
cm[onOff]('pick', autocompletePicked);
|
||||
});
|
||||
|
||||
CodeMirror.defineOption('matchHighlight', prefs.get('editor.matchHighlight'), (cm, value) => {
|
||||
if (value === 'token') {
|
||||
cm.setOption('highlightSelectionMatches', {
|
||||
showToken: /[#.\-\w]/,
|
||||
annotateScrollbar: true
|
||||
});
|
||||
} else if (value === 'selection') {
|
||||
cm.setOption('highlightSelectionMatches', {
|
||||
showToken: false,
|
||||
annotateScrollbar: true
|
||||
});
|
||||
} else {
|
||||
cm.setOption('highlightSelectionMatches', null);
|
||||
}
|
||||
});
|
||||
|
||||
CodeMirror.defineOption('selectByTokens', prefs.get('editor.selectByTokens'), (cm, value) => {
|
||||
cm.setOption('configureMouse', value ? configureMouseFn : null);
|
||||
});
|
||||
|
||||
prefs.subscribe(null, (key, value) => {
|
||||
const option = key.replace(/^editor\./, '');
|
||||
if (!option) {
|
||||
console.error('no "cm_option"', key);
|
||||
return;
|
||||
}
|
||||
// FIXME: this is implemented in `colorpicker-helper.js`.
|
||||
if (option === 'colorpicker') {
|
||||
return;
|
||||
}
|
||||
if (option === 'theme') {
|
||||
const themeLink = $('#cm-theme');
|
||||
// use non-localized 'default' internally
|
||||
if (value === 'default') {
|
||||
themeLink.href = '';
|
||||
} else {
|
||||
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css');
|
||||
if (themeLink.href !== url) {
|
||||
// avoid flicker: wait for the second stylesheet to load, then apply the theme
|
||||
return loadScript(url, true).then(([newThemeLink]) => {
|
||||
setOption(option, value);
|
||||
themeLink.remove();
|
||||
newThemeLink.id = 'cm-theme';
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// broadcast option
|
||||
setOption(option, value);
|
||||
});
|
||||
return {create, destroy, setOption};
|
||||
|
||||
function configureMouseFn(cm, repeat) {
|
||||
return repeat === 'double' ?
|
||||
{unit: selectTokenOnDoubleclick} :
|
||||
{};
|
||||
}
|
||||
|
||||
function selectTokenOnDoubleclick(cm, pos) {
|
||||
let {ch} = pos;
|
||||
const {line, sticky} = pos;
|
||||
const {text, styles} = cm.getLineHandle(line);
|
||||
|
||||
const execAt = (rx, i) => (rx.lastIndex = i) && null || rx.exec(text);
|
||||
const at = (rx, i) => (rx.lastIndex = i) && null || rx.test(text);
|
||||
const atWord = ch => at(/\w/y, ch);
|
||||
const atSpace = ch => at(/\s/y, ch);
|
||||
|
||||
const atTokenEnd = styles.indexOf(ch, 1);
|
||||
ch += atTokenEnd < 0 ? 0 : sticky === 'before' && atWord(ch - 1) ? 0 : atSpace(ch + 1) ? 0 : 1;
|
||||
ch = Math.min(text.length, ch);
|
||||
const type = cm.getTokenTypeAt({line, ch: ch + (sticky === 'after' ? 1 : 0)});
|
||||
if (atTokenEnd > 0) ch--;
|
||||
|
||||
const isCss = type && !/^(comment|string)/.test(type);
|
||||
const isNumber = type === 'number';
|
||||
const isSpace = atSpace(ch);
|
||||
let wordChars =
|
||||
isNumber ? /[-+\w.%]/y :
|
||||
isCss ? /[-\w@]/y :
|
||||
isSpace ? /\s/y :
|
||||
atWord(ch) ? /\w/y : /[^\w\s]/y;
|
||||
|
||||
let a = ch;
|
||||
while (a && at(wordChars, a)) a--;
|
||||
a += !a && at(wordChars, a) || isCss && at(/[.!#@]/y, a) ? 0 : at(wordChars, a + 1);
|
||||
|
||||
let b, found;
|
||||
|
||||
if (isNumber) {
|
||||
b = a + execAt(/[+-]?[\d.]+(e\d+)?|$/yi, a)[0].length;
|
||||
found = b >= ch;
|
||||
if (!found) {
|
||||
a = b;
|
||||
ch = a;
|
||||
}
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
wordChars = isCss ? /[-\w]*/y : new RegExp(wordChars.source + '*', 'uy');
|
||||
b = ch + execAt(wordChars, ch)[0].length;
|
||||
}
|
||||
|
||||
return {
|
||||
from: {line, ch: a},
|
||||
to: {line, ch: b},
|
||||
};
|
||||
}
|
||||
|
||||
function autocompleteOnTyping(cm, [info], debounced) {
|
||||
const lastLine = info.text[info.text.length - 1];
|
||||
if (
|
||||
cm.state.completionActive ||
|
||||
info.origin && !info.origin.includes('input') ||
|
||||
!lastLine
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (cm.state.autocompletePicked) {
|
||||
cm.state.autocompletePicked = false;
|
||||
return;
|
||||
}
|
||||
if (!debounced) {
|
||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
||||
return;
|
||||
}
|
||||
if (lastLine.match(/[-a-z!]+$/i)) {
|
||||
cm.state.autocompletePicked = false;
|
||||
cm.options.hintOptions.completeSingle = false;
|
||||
cm.execCommand('autocomplete');
|
||||
setTimeout(() => {
|
||||
cm.options.hintOptions.completeSingle = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function autocompletePicked(cm) {
|
||||
cm.state.autocompletePicked = true;
|
||||
}
|
||||
|
||||
function destroy(cm) {
|
||||
editors.delete(cm);
|
||||
}
|
||||
|
||||
function create(init, options) {
|
||||
const cm = CodeMirror(init, options);
|
||||
cm.lastActive = 0;
|
||||
const wrapper = cm.display.wrapper;
|
||||
cm.on('blur', () => {
|
||||
rerouteHotkeys(true);
|
||||
setTimeout(() => {
|
||||
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
|
||||
});
|
||||
});
|
||||
cm.on('focus', () => {
|
||||
rerouteHotkeys(false);
|
||||
wrapper.classList.add('CodeMirror-active');
|
||||
cm.lastActive = Date.now();
|
||||
});
|
||||
editors.add(cm);
|
||||
return cm;
|
||||
}
|
||||
|
||||
function getLastActivated() {
|
||||
let result;
|
||||
for (const cm of editors) {
|
||||
if (!result || result.lastActive < cm.lastActive) {
|
||||
result = cm;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function setOption(key, value) {
|
||||
CodeMirror.defaults[key] = value;
|
||||
if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) {
|
||||
throttleSetOption({key, value, index: 0});
|
||||
return;
|
||||
}
|
||||
for (const cm of editors) {
|
||||
cm.setOption(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
function throttleSetOption({
|
||||
key,
|
||||
value,
|
||||
index,
|
||||
timeStart = performance.now(),
|
||||
editorsCopy = [...editors],
|
||||
cmStart = getLastActivated(),
|
||||
progress,
|
||||
}) {
|
||||
if (index === 0) {
|
||||
if (!cmStart) {
|
||||
return;
|
||||
}
|
||||
cmStart.setOption(key, value);
|
||||
}
|
||||
|
||||
const THROTTLE_AFTER_MS = 100;
|
||||
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
|
||||
|
||||
const t0 = performance.now();
|
||||
const total = editorsCopy.length;
|
||||
while (index < total) {
|
||||
const cm = editorsCopy[index++];
|
||||
if (cm === cmStart || !editors.has(cm)) {
|
||||
continue;
|
||||
}
|
||||
cm.setOption(key, value);
|
||||
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (index >= total) {
|
||||
$.remove(progress);
|
||||
return;
|
||||
}
|
||||
if (!progress &&
|
||||
index < total / 2 &&
|
||||
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
|
||||
let option = $('#editor.' + key);
|
||||
if (option) {
|
||||
if (option.type === 'checkbox') {
|
||||
option = (option.labels || [])[0] || option.nextElementSibling || option;
|
||||
}
|
||||
progress = document.body.appendChild(
|
||||
$create('.set-option-progress', {targetElement: option}));
|
||||
}
|
||||
}
|
||||
if (progress) {
|
||||
const optionBounds = progress.targetElement.getBoundingClientRect();
|
||||
const bounds = {
|
||||
top: optionBounds.top + window.scrollY + 1,
|
||||
left: optionBounds.left + window.scrollX + 1,
|
||||
width: (optionBounds.width - 2) * index / total | 0,
|
||||
height: optionBounds.height - 2,
|
||||
};
|
||||
const style = progress.style;
|
||||
for (const prop in bounds) {
|
||||
if (bounds[prop] !== parseFloat(style[prop])) {
|
||||
style[prop] = bounds[prop] + 'px';
|
||||
}
|
||||
}
|
||||
}
|
||||
setTimeout(throttleSetOption, 0, {
|
||||
key,
|
||||
value,
|
||||
index,
|
||||
timeStart,
|
||||
cmStart,
|
||||
editorsCopy,
|
||||
progress,
|
||||
});
|
||||
}
|
||||
})();
|
|
@ -1,7 +1,7 @@
|
|||
/* global CodeMirror loadScript editors showHelp */
|
||||
/* global CodeMirror showHelp cmFactory onDOMready $ $create prefs t */
|
||||
'use strict';
|
||||
|
||||
onDOMscriptReady('/colorview.js').then(() => {
|
||||
(() => {
|
||||
onDOMready().then(() => {
|
||||
$('#colorpicker-settings').onclick = configureColorpicker;
|
||||
});
|
||||
|
@ -20,7 +20,8 @@ onDOMscriptReady('/colorview.js').then(() => {
|
|||
defaults.extraKeys[keyName] = 'colorpicker';
|
||||
}
|
||||
defaults.colorpicker = {
|
||||
forceUpdate: editors.length > 0,
|
||||
// FIXME: who uses this?
|
||||
// forceUpdate: editor.getEditors().length > 0,
|
||||
tooltip: t('colorpickerTooltip'),
|
||||
popup: {
|
||||
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
|
||||
|
@ -38,8 +39,7 @@ onDOMscriptReady('/colorview.js').then(() => {
|
|||
delete defaults.extraKeys[keyName];
|
||||
}
|
||||
}
|
||||
// on page load runs before CodeMirror.setOption is defined
|
||||
editors.forEach(cm => cm.setOption('colorpicker', defaults.colorpicker));
|
||||
cmFactory.setOption('colorpicker', defaults.colorpicker);
|
||||
}
|
||||
|
||||
function registerHotkey(id, hotkey) {
|
||||
|
@ -112,4 +112,4 @@ onDOMscriptReady('/colorview.js').then(() => {
|
|||
}
|
||||
input.focus();
|
||||
}
|
||||
});
|
||||
})();
|
||||
|
|
|
@ -265,13 +265,10 @@ input:invalid {
|
|||
}
|
||||
/************ content ***********/
|
||||
#sections > * {
|
||||
margin: 0.7rem;
|
||||
padding: 1rem 1rem .3rem;
|
||||
margin: 0 0.7rem;
|
||||
padding: 1rem;
|
||||
}
|
||||
#sections > *:first-child {
|
||||
padding: 0 1rem .3rem;
|
||||
}
|
||||
#sections > *:not(:first-child) {
|
||||
#sections > :not(:first-child) {
|
||||
border-top: 2px solid hsl(0, 0%, 80%);
|
||||
}
|
||||
.add-section:after {
|
||||
|
@ -288,7 +285,7 @@ input:invalid {
|
|||
flex-wrap: wrap;
|
||||
}
|
||||
.edit-actions button {
|
||||
margin: 0 .2rem .5rem 0;
|
||||
margin-right: .2rem;
|
||||
}
|
||||
.dirty > label::before {
|
||||
content: "*";
|
||||
|
@ -312,6 +309,25 @@ input:invalid {
|
|||
.section:only-of-type .move-section-down {
|
||||
display: none;
|
||||
}
|
||||
.section .CodeMirror {
|
||||
margin-bottom: .875rem;
|
||||
}
|
||||
/* deleted section */
|
||||
.deleted-section {
|
||||
margin: 0;
|
||||
}
|
||||
.section .deleted-section {
|
||||
display: none;
|
||||
}
|
||||
.section.removed .deleted-section {
|
||||
display: block;
|
||||
}
|
||||
.section.removed .code-label,
|
||||
.section.removed .applies-to,
|
||||
.section.removed .edit-actions,
|
||||
.section.removed .CodeMirror {
|
||||
display: none;
|
||||
}
|
||||
.move-section-up:after {
|
||||
content: "";
|
||||
display: block;
|
||||
|
|
605
edit/edit.js
605
edit/edit.js
|
@ -1,82 +1,244 @@
|
|||
/*
|
||||
global CodeMirror loadScript
|
||||
global createSourceEditor
|
||||
global closeCurrentTab regExpTester messageBox
|
||||
global setupCodeMirror
|
||||
global beautify
|
||||
global initWithSectionStyle addSections removeSection getSectionsHashes
|
||||
global sectionsToMozFormat
|
||||
global moveFocus editorWorker
|
||||
*/
|
||||
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML
|
||||
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch
|
||||
closeCurrentTab messageBox debounce workerUtil
|
||||
beautify ignoreChromeError
|
||||
moveFocus msg createSectionsEditor rerouteHotkeys */
|
||||
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */
|
||||
'use strict';
|
||||
|
||||
let styleId = null;
|
||||
// only the actually dirty items here
|
||||
let dirty = {};
|
||||
// array of all CodeMirror instances
|
||||
const editors = [];
|
||||
const editorWorker = workerUtil.createWorker({
|
||||
url: '/edit/editor-worker.js'
|
||||
});
|
||||
|
||||
let saveSizeOnClose;
|
||||
let ownTabId;
|
||||
|
||||
// direct & reverse mapping of @-moz-document keywords and internal property names
|
||||
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'};
|
||||
const CssToProperty = {'url': 'urls', 'url-prefix': 'urlPrefixes', 'domain': 'domains', 'regexp': 'regexps'};
|
||||
const CssToProperty = Object.entries(propertyToCss)
|
||||
.reduce((o, v) => {
|
||||
o[v[1]] = v[0];
|
||||
return o;
|
||||
}, {});
|
||||
|
||||
let editor;
|
||||
|
||||
|
||||
document.addEventListener('visibilitychange', beforeUnload);
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
window.addEventListener('beforeunload', beforeUnload);
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
preinit();
|
||||
|
||||
Promise.all([
|
||||
(() => {
|
||||
onDOMready().then(() => {
|
||||
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
|
||||
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
|
||||
showHotkeyInTooltip();
|
||||
|
||||
buildThemeElement();
|
||||
buildKeymapElement();
|
||||
|
||||
setupLivePrefs();
|
||||
});
|
||||
|
||||
initEditor();
|
||||
|
||||
function getCodeMirrorThemes() {
|
||||
if (!chrome.runtime.getPackageDirectoryEntry) {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme'),
|
||||
/* populate-theme-start */
|
||||
'3024-day',
|
||||
'3024-night',
|
||||
'abcdef',
|
||||
'ambiance',
|
||||
'ambiance-mobile',
|
||||
'base16-dark',
|
||||
'base16-light',
|
||||
'bespin',
|
||||
'blackboard',
|
||||
'cobalt',
|
||||
'colorforth',
|
||||
'darcula',
|
||||
'dracula',
|
||||
'duotone-dark',
|
||||
'duotone-light',
|
||||
'eclipse',
|
||||
'elegant',
|
||||
'erlang-dark',
|
||||
'gruvbox-dark',
|
||||
'hopscotch',
|
||||
'icecoder',
|
||||
'idea',
|
||||
'isotope',
|
||||
'lesser-dark',
|
||||
'liquibyte',
|
||||
'lucario',
|
||||
'material',
|
||||
'mbo',
|
||||
'mdn-like',
|
||||
'midnight',
|
||||
'monokai',
|
||||
'neat',
|
||||
'neo',
|
||||
'night',
|
||||
'oceanic-next',
|
||||
'panda-syntax',
|
||||
'paraiso-dark',
|
||||
'paraiso-light',
|
||||
'pastel-on-dark',
|
||||
'railscasts',
|
||||
'rubyblue',
|
||||
'seti',
|
||||
'shadowfox',
|
||||
'solarized',
|
||||
'ssms',
|
||||
'the-matrix',
|
||||
'tomorrow-night-bright',
|
||||
'tomorrow-night-eighties',
|
||||
'ttcn',
|
||||
'twilight',
|
||||
'vibrant-ink',
|
||||
'xq-dark',
|
||||
'xq-light',
|
||||
'yeti',
|
||||
'zenburn',
|
||||
/* populate-theme-end */
|
||||
];
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
return Promise.resolve(themes);
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
chrome.runtime.getPackageDirectoryEntry(rootDir => {
|
||||
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
|
||||
themeDir.createReader().readEntries(entries => {
|
||||
const themes = [
|
||||
chrome.i18n.getMessage('defaultTheme')
|
||||
].concat(
|
||||
entries.filter(entry => entry.isFile)
|
||||
.sort((a, b) => (a.name < b.name ? -1 : 1))
|
||||
.map(entry => entry.name.replace(/\.css$/, ''))
|
||||
);
|
||||
localStorage.codeMirrorThemes = themes.join(' ');
|
||||
resolve(themes);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function findKeyForCommand(command, map) {
|
||||
if (typeof map === 'string') map = CodeMirror.keyMap[map];
|
||||
let key = Object.keys(map).find(k => map[k] === command);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
|
||||
key = ft && findKeyForCommand(command, ft);
|
||||
if (key) {
|
||||
return key;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function buildThemeElement() {
|
||||
const themeElement = $('#editor.theme');
|
||||
const themeList = localStorage.codeMirrorThemes;
|
||||
|
||||
const optionsFromArray = options => {
|
||||
const fragment = document.createDocumentFragment();
|
||||
options.forEach(opt => fragment.appendChild($create('option', opt)));
|
||||
themeElement.appendChild(fragment);
|
||||
};
|
||||
|
||||
if (themeList) {
|
||||
optionsFromArray(themeList.split(/\s+/));
|
||||
} else {
|
||||
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet
|
||||
const theme = prefs.get('editor.theme');
|
||||
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
|
||||
getCodeMirrorThemes().then(() => {
|
||||
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
|
||||
optionsFromArray(themes);
|
||||
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function buildKeymapElement() {
|
||||
// move 'pc' or 'mac' prefix to the end of the displayed label
|
||||
const maps = Object.keys(CodeMirror.keyMap)
|
||||
.map(name => ({
|
||||
value: name,
|
||||
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
|
||||
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
|
||||
}))
|
||||
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
|
||||
|
||||
const fragment = document.createDocumentFragment();
|
||||
let bin = fragment;
|
||||
let groupName;
|
||||
// group suffixed maps in <optgroup>
|
||||
maps.forEach(({value, name}, i) => {
|
||||
groupName = !name.includes('-') ? name : groupName;
|
||||
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
|
||||
if (groupWithNext) {
|
||||
if (bin === fragment) {
|
||||
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
|
||||
}
|
||||
}
|
||||
const el = bin.appendChild($create('option', {value}, name));
|
||||
if (value === prefs.defaults['editor.keyMap']) {
|
||||
el.dataset.default = '';
|
||||
el.title = t('defaultTheme');
|
||||
}
|
||||
if (!groupWithNext) bin = fragment;
|
||||
});
|
||||
$('#editor.keyMap').appendChild(fragment);
|
||||
}
|
||||
|
||||
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
|
||||
const extraKeys = CodeMirror.defaults.extraKeys;
|
||||
for (const el of $$('[data-hotkey-tooltip]')) {
|
||||
if (el._hotkeyTooltipKeyMap !== mapName) {
|
||||
el._hotkeyTooltipKeyMap = mapName;
|
||||
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
|
||||
const cmd = el.dataset.hotkeyTooltip;
|
||||
const key = cmd[0] === '=' ? cmd.slice(1) :
|
||||
findKeyForCommand(cmd, mapName) ||
|
||||
extraKeys && findKeyForCommand(cmd, extraKeys);
|
||||
const newTitle = title + (title && key ? '\n' : '') + (key || '');
|
||||
if (el.title !== newTitle) el.title = newTitle;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initEditor() {
|
||||
return Promise.all([
|
||||
initStyleData(),
|
||||
onDOMready(),
|
||||
])
|
||||
.then(([style]) => {
|
||||
prefs.initializing,
|
||||
])
|
||||
.then(([style]) => {
|
||||
const usercss = isUsercss(style);
|
||||
$('#heading').textContent = t(styleId ? 'editStyleHeading' : 'addStyleTitle');
|
||||
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
|
||||
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
|
||||
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
|
||||
|
||||
$('#preview-label').classList.toggle('hidden', !styleId);
|
||||
$('#preview-label').classList.toggle('hidden', !style.id);
|
||||
|
||||
$('#beautify').onclick = beautify;
|
||||
$('#beautify').onclick = () => beautify(editor.getEditors());
|
||||
$('#lint').addEventListener('scroll', hideLintHeaderOnScroll, {passive: true});
|
||||
window.addEventListener('resize', () => debounce(rememberWindowSize, 100));
|
||||
|
||||
if (usercss) {
|
||||
editor = createSourceEditor(style);
|
||||
} else {
|
||||
initWithSectionStyle(style);
|
||||
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
|
||||
editor = usercss ? createSourceEditor(style) : createSectionsEditor(style);
|
||||
if (editor.ready) {
|
||||
return editor.ready();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
function preinit() {
|
||||
// make querySelectorAll enumeration code readable
|
||||
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
|
||||
NodeList.prototype[method] = Array.prototype[method];
|
||||
});
|
||||
|
||||
// eslint-disable-next-line no-extend-native
|
||||
Object.defineProperties(Array.prototype, {
|
||||
last: {
|
||||
get() {
|
||||
return this[this.length - 1];
|
||||
},
|
||||
},
|
||||
rotate: {
|
||||
value: function (amount) {
|
||||
// negative amount == rotate left
|
||||
const r = this.slice(-amount, this.length);
|
||||
Array.prototype.push.apply(r, this.slice(0, this.length - r.length));
|
||||
return r;
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
|
||||
new MutationObserver((mutations, observer) => {
|
||||
const themeElement = $('#cm-theme');
|
||||
|
@ -114,7 +276,7 @@ function preinit() {
|
|||
}
|
||||
|
||||
getOwnTab().then(tab => {
|
||||
ownTabId = tab.id;
|
||||
const ownTabId = tab.id;
|
||||
|
||||
// use browser history back when 'back to manage' is clicked
|
||||
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
|
||||
|
@ -156,37 +318,28 @@ function preinit() {
|
|||
function onRuntimeMessage(request) {
|
||||
switch (request.method) {
|
||||
case 'styleUpdated':
|
||||
if (styleId && styleId === request.style.id &&
|
||||
request.reason !== 'editPreview' &&
|
||||
request.reason !== 'editSave' &&
|
||||
request.reason !== 'config') {
|
||||
// code-less style from notifyAllTabs
|
||||
const {sections, id} = request.style;
|
||||
((sections[0] || {}).code === null
|
||||
? API.getStyles({id})
|
||||
: Promise.resolve([request.style])
|
||||
).then(([style]) => {
|
||||
if (isUsercss(style)) {
|
||||
editor.replaceStyle(style, request.codeIsUpdated);
|
||||
} else {
|
||||
initWithSectionStyle(style, request.codeIsUpdated);
|
||||
}
|
||||
if (
|
||||
editor.getStyleId() === request.style.id &&
|
||||
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
|
||||
.includes(request.reason)
|
||||
) {
|
||||
Promise.resolve(
|
||||
request.codeIsUpdated === false ?
|
||||
request.style : API.getStyle(request.style.id)
|
||||
)
|
||||
.then(newStyle => {
|
||||
editor.replaceStyle(newStyle, request.codeIsUpdated);
|
||||
});
|
||||
}
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
if (styleId === request.id || editor && editor.getStyle().id === request.id) {
|
||||
if (editor.getStyleId() === request.style.id) {
|
||||
document.removeEventListener('visibilitychange', beforeUnload);
|
||||
window.onbeforeunload = null;
|
||||
document.removeEventListener('beforeunload', beforeUnload);
|
||||
closeCurrentTab();
|
||||
break;
|
||||
}
|
||||
break;
|
||||
case 'prefChanged':
|
||||
if ('editor.smartIndent' in request.prefs) {
|
||||
CodeMirror.setOption('smartIndent', request.prefs['editor.smartIndent']);
|
||||
}
|
||||
break;
|
||||
case 'editDeleteText':
|
||||
document.execCommand('delete');
|
||||
break;
|
||||
|
@ -200,7 +353,7 @@ function onRuntimeMessage(request) {
|
|||
* > Never add a beforeunload listener unconditionally or use it as an end-of-session signal.
|
||||
* > Only add it when a user has unsaved work, and remove it as soon as that work has been saved.
|
||||
*/
|
||||
function beforeUnload() {
|
||||
function beforeUnload(e) {
|
||||
if (saveSizeOnClose) rememberWindowSize();
|
||||
const activeElement = document.activeElement;
|
||||
if (activeElement) {
|
||||
|
@ -209,10 +362,9 @@ function beforeUnload() {
|
|||
// refocus if unloading was canceled
|
||||
setTimeout(() => activeElement.focus());
|
||||
}
|
||||
const isDirty = editor ? editor.isDirty() : !isCleanGlobal();
|
||||
if (isDirty) {
|
||||
if (editor && editor.isDirty()) {
|
||||
// neither confirm() nor custom messages work in modern browsers but just in case
|
||||
return t('styleChangesNotSaved');
|
||||
e.returnValue = t('styleChangesNotSaved');
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -228,7 +380,6 @@ function initStyleData() {
|
|||
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
|
||||
const id = Number(params.get('id'));
|
||||
const createEmptyStyle = () => ({
|
||||
id: null,
|
||||
name: params.get('domain') ||
|
||||
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
|
||||
'',
|
||||
|
@ -244,15 +395,14 @@ function initStyleData() {
|
|||
});
|
||||
return fetchStyle()
|
||||
.then(style => {
|
||||
styleId = style.id;
|
||||
if (styleId) sessionStorage.justEditedStyleId = styleId;
|
||||
if (style.id) sessionStorage.justEditedStyleId = style.id;
|
||||
// we set "usercss" class on <html> when <body> is empty
|
||||
// so there'll be no flickering of the elements that depend on it
|
||||
if (isUsercss(style)) {
|
||||
document.documentElement.classList.add('usercss');
|
||||
}
|
||||
// strip URL parameters when invoked for a non-existent id
|
||||
if (!styleId) {
|
||||
if (!style.id) {
|
||||
history.replaceState({}, document.title, location.pathname);
|
||||
}
|
||||
return style;
|
||||
|
@ -260,268 +410,12 @@ function initStyleData() {
|
|||
|
||||
function fetchStyle() {
|
||||
if (id) {
|
||||
return API.getStyleFromDB(id);
|
||||
return API.getStyle(id);
|
||||
}
|
||||
return Promise.resolve(createEmptyStyle());
|
||||
}
|
||||
}
|
||||
|
||||
function initHooks() {
|
||||
if (initHooks.alreadyDone) {
|
||||
return;
|
||||
}
|
||||
initHooks.alreadyDone = true;
|
||||
$$('#header .style-contributor').forEach(node => {
|
||||
node.addEventListener('change', onChange);
|
||||
node.addEventListener('input', onChange);
|
||||
});
|
||||
$('#to-mozilla').addEventListener('click', showMozillaFormat, false);
|
||||
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp, false);
|
||||
$('#from-mozilla').addEventListener('click', fromMozillaFormat);
|
||||
$('#save-button').addEventListener('click', save, false);
|
||||
$('#sections-help').addEventListener('click', showSectionHelp, false);
|
||||
|
||||
if (!FIREFOX) {
|
||||
$$([
|
||||
'input:not([type])',
|
||||
'input[type="text"]',
|
||||
'input[type="search"]',
|
||||
'input[type="number"]',
|
||||
].join(','))
|
||||
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
|
||||
}
|
||||
}
|
||||
|
||||
function onChange(event) {
|
||||
const node = event.target;
|
||||
if ('savedValue' in node) {
|
||||
const currentValue = node.type === 'checkbox' ? node.checked : node.value;
|
||||
setCleanItem(node, node.savedValue === currentValue);
|
||||
} else {
|
||||
// the manually added section's applies-to is dirty only when the value is non-empty
|
||||
setCleanItem(node, node.localName !== 'input' || !node.value.trim());
|
||||
// only valid when actually saved
|
||||
delete node.savedValue;
|
||||
}
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
// Set .dirty on stylesheet contributors that have changed
|
||||
function setDirtyClass(node, isDirty) {
|
||||
node.classList.toggle('dirty', isDirty);
|
||||
}
|
||||
|
||||
function setCleanItem(node, isClean) {
|
||||
if (!node.id) {
|
||||
node.id = Date.now().toString(32).substr(-6);
|
||||
}
|
||||
|
||||
if (isClean) {
|
||||
delete dirty[node.id];
|
||||
// code sections have .CodeMirror property
|
||||
if (node.CodeMirror) {
|
||||
node.savedValue = node.CodeMirror.changeGeneration();
|
||||
} else {
|
||||
node.savedValue = node.type === 'checkbox' ? node.checked : node.value;
|
||||
}
|
||||
} else {
|
||||
dirty[node.id] = true;
|
||||
}
|
||||
|
||||
setDirtyClass(node, !isClean);
|
||||
}
|
||||
|
||||
function isCleanGlobal() {
|
||||
const clean = Object.keys(dirty).length === 0;
|
||||
setDirtyClass(document.body, !clean);
|
||||
return clean;
|
||||
}
|
||||
|
||||
function setCleanGlobal() {
|
||||
setCleanItem($('#sections'), true);
|
||||
$$('#header, #sections > .section').forEach(setCleanSection);
|
||||
// forget the dirty applies-to ids from a deleted section after the style was saved
|
||||
dirty = {};
|
||||
}
|
||||
|
||||
function setCleanSection(section) {
|
||||
$$('.style-contributor', section).forEach(node => setCleanItem(node, true));
|
||||
setCleanItem(section, true);
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
function toggleStyle() {
|
||||
$('#enabled').dispatchEvent(new MouseEvent('click', {bubbles: true}));
|
||||
}
|
||||
|
||||
function save() {
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
|
||||
API.saveStyle({
|
||||
id: styleId,
|
||||
name: $('#name').value.trim(),
|
||||
enabled: $('#enabled').checked,
|
||||
reason: 'editSave',
|
||||
sections: getSectionsHashes()
|
||||
})
|
||||
.then(style => {
|
||||
styleId = style.id;
|
||||
sessionStorage.justEditedStyleId = styleId;
|
||||
setCleanGlobal();
|
||||
// Go from new style URL to edit style URL
|
||||
if (location.href.indexOf('id=') === -1) {
|
||||
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
|
||||
$('#heading').textContent = t('editStyleHeading');
|
||||
}
|
||||
updateTitle();
|
||||
$('#preview-label').classList.remove('hidden');
|
||||
});
|
||||
}
|
||||
|
||||
function validate() {
|
||||
const name = $('#name').value.trim();
|
||||
if (!name) {
|
||||
$('#name').focus();
|
||||
messageBox.alert(t('styleMissingName'));
|
||||
return false;
|
||||
}
|
||||
|
||||
if ($$('.applies-to-list li:not(.applies-to-everything)')
|
||||
.some(li => {
|
||||
const type = $('[name=applies-type]', li).value;
|
||||
const value = $('[name=applies-value]', li);
|
||||
const rx = value.value.trim();
|
||||
if (type === 'regexp' && rx && !tryRegExp(rx)) {
|
||||
value.focus();
|
||||
value.select();
|
||||
return true;
|
||||
}
|
||||
})) {
|
||||
messageBox.alert(t('styleBadRegexp'));
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
const name = $('#name').savedValue;
|
||||
const clean = isCleanGlobal();
|
||||
const title = styleId === null ? t('addStyleTitle') : name;
|
||||
document.title = (clean ? '' : '* ') + title;
|
||||
window.onbeforeunload = clean ? null : beforeUnload;
|
||||
$('#save-button').disabled = clean;
|
||||
}
|
||||
|
||||
function showMozillaFormat() {
|
||||
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
||||
popup.codebox.setValue(toMozillaFormat());
|
||||
popup.codebox.execCommand('selectAll');
|
||||
}
|
||||
|
||||
function toMozillaFormat() {
|
||||
return sectionsToMozFormat({sections: getSectionsHashes()});
|
||||
}
|
||||
|
||||
function fromMozillaFormat() {
|
||||
const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'),
|
||||
$create('.buttons', [
|
||||
$create('button', {
|
||||
name: 'import-replace',
|
||||
textContent: t('importReplaceLabel'),
|
||||
title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'),
|
||||
onclick: () => doImport({replaceOldStyle: true}),
|
||||
}),
|
||||
$create('button', {
|
||||
name: 'import-append',
|
||||
textContent: t('importAppendLabel'),
|
||||
title: 'Ctrl-Enter:\n' + t('importAppendTooltip'),
|
||||
onclick: doImport,
|
||||
}),
|
||||
]));
|
||||
const contents = $('.contents', popup);
|
||||
contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
|
||||
popup.codebox.focus();
|
||||
popup.codebox.on('changes', cm => {
|
||||
popup.classList.toggle('ready', !cm.isBlank());
|
||||
cm.markClean();
|
||||
});
|
||||
// overwrite default extraKeys as those are inapplicable in popup context
|
||||
popup.codebox.options.extraKeys = {
|
||||
'Ctrl-Enter': doImport,
|
||||
'Shift-Ctrl-Enter': () => doImport({replaceOldStyle: true}),
|
||||
};
|
||||
|
||||
function doImport({replaceOldStyle = false}) {
|
||||
lockPageUI(true);
|
||||
editorWorker.parseMozFormat({code: popup.codebox.getValue().trim()})
|
||||
.then(({sections, errors}) => {
|
||||
// shouldn't happen but just in case
|
||||
if (!sections.length && errors.length) {
|
||||
return Promise.reject(errors);
|
||||
}
|
||||
// show the errors in case linting is disabled or stylelint misses what csslint has found
|
||||
if (errors.length && prefs.get('editor.linter') !== 'csslint') {
|
||||
showError(errors);
|
||||
}
|
||||
removeOldSections(replaceOldStyle);
|
||||
return addSections(sections, div => setCleanItem(div, false));
|
||||
})
|
||||
.then(() => {
|
||||
$('.dismiss').dispatchEvent(new Event('click'));
|
||||
})
|
||||
.catch(showError)
|
||||
.then(() => lockPageUI(false));
|
||||
}
|
||||
|
||||
function removeOldSections(removeAll) {
|
||||
let toRemove;
|
||||
if (removeAll) {
|
||||
toRemove = editors.slice().reverse();
|
||||
} else if (editors.last.isBlank() && $('.applies-to-everything', editors.last.getSection())) {
|
||||
toRemove = [editors.last];
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
toRemove.forEach(cm => removeSection({target: cm.getSection()}));
|
||||
}
|
||||
|
||||
function lockPageUI(locked) {
|
||||
document.documentElement.style.pointerEvents = locked ? 'none' : '';
|
||||
if (popup.codebox) {
|
||||
popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
|
||||
popup.codebox.options.readOnly = locked;
|
||||
popup.codebox.display.wrapper.style.opacity = locked ? '.5' : '';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(errors) {
|
||||
messageBox({
|
||||
className: 'center danger',
|
||||
title: t('styleFromMozillaFormatError'),
|
||||
contents: $create('pre', Array.isArray(errors) ? errors.join('\n') : errors),
|
||||
buttons: [t('confirmClose')],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function showSectionHelp(event) {
|
||||
event.preventDefault();
|
||||
showHelp(t('styleSectionsTitle'), t('sectionHelp'));
|
||||
}
|
||||
|
||||
function showAppliesToHelp(event) {
|
||||
event.preventDefault();
|
||||
showHelp(t('appliesLabel'), t('appliesHelp'));
|
||||
}
|
||||
|
||||
function showToMozillaHelp(event) {
|
||||
event.preventDefault();
|
||||
showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
|
||||
}
|
||||
|
||||
function showHelp(title = '', body) {
|
||||
const div = $('#help-popup');
|
||||
div.className = '';
|
||||
|
@ -594,7 +488,7 @@ function showCodeMirrorPopup(title, html, options) {
|
|||
keyMap: prefs.get('editor.keyMap')
|
||||
}, options));
|
||||
cm.focus();
|
||||
cm.rerouteHotkeys(false);
|
||||
rerouteHotkeys(false);
|
||||
|
||||
document.documentElement.style.pointerEvents = 'none';
|
||||
popup.style.pointerEvents = 'auto';
|
||||
|
@ -613,36 +507,13 @@ function showCodeMirrorPopup(title, html, options) {
|
|||
window.removeEventListener('closeHelp', _);
|
||||
window.removeEventListener('keydown', onKeyDown, true);
|
||||
document.documentElement.style.removeProperty('pointer-events');
|
||||
cm.rerouteHotkeys(true);
|
||||
rerouteHotkeys(true);
|
||||
cm = popup.codebox = null;
|
||||
});
|
||||
|
||||
return popup;
|
||||
}
|
||||
|
||||
function setGlobalProgress(done, total) {
|
||||
const progressElement = $('#global-progress') ||
|
||||
total && document.body.appendChild($create('#global-progress'));
|
||||
if (total) {
|
||||
const progress = (done / Math.max(done, total) * 100).toFixed(1);
|
||||
progressElement.style.borderLeftWidth = progress + 'vw';
|
||||
setTimeout(() => {
|
||||
progressElement.title = progress + '%';
|
||||
});
|
||||
} else {
|
||||
$.remove(progressElement);
|
||||
}
|
||||
}
|
||||
|
||||
function scrollEntirePageOnCtrlShift(event) {
|
||||
// make Shift-Ctrl-Wheel scroll entire page even when mouse is over a code editor
|
||||
if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||
// Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
|
||||
window.scrollBy(0, event.deltaX || event.deltaY);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function hideLintHeaderOnScroll() {
|
||||
// workaround part2 for the <details> not showing its toggle icon: hide <summary> on scroll
|
||||
const newOpacity = this.scrollTop === 0 ? '' : '0';
|
||||
|
|
|
@ -1,118 +0,0 @@
|
|||
/* global importScripts parseMozFormat parserlib CSSLint require */
|
||||
'use strict';
|
||||
|
||||
createAPI({
|
||||
csslint: (code, config) => {
|
||||
loadParserLib();
|
||||
loadScript(['/vendor-overwrites/csslint/csslint.js']);
|
||||
return CSSLint.verify(code, config).messages
|
||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||
},
|
||||
stylelint: (code, config) => {
|
||||
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
|
||||
return require('stylelint').lint({code, config});
|
||||
},
|
||||
parseMozFormat: data => {
|
||||
loadParserLib();
|
||||
loadScript(['/js/moz-parser.js']);
|
||||
return parseMozFormat(data);
|
||||
},
|
||||
getStylelintRules,
|
||||
getCsslintRules
|
||||
});
|
||||
|
||||
function getCsslintRules() {
|
||||
loadScript(['/vendor-overwrites/csslint/csslint.js']);
|
||||
return CSSLint.getRules().map(rule => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (typeof value !== 'function') {
|
||||
output[key] = value;
|
||||
}
|
||||
}
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
function getStylelintRules() {
|
||||
loadScript(['/vendor/stylelint-bundle/stylelint-bundle.min.js']);
|
||||
const stylelint = require('stylelint');
|
||||
const options = {};
|
||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
||||
for (const id of Object.keys(stylelint.rules)) {
|
||||
const ruleCode = String(stylelint.rules[id]);
|
||||
const sets = [];
|
||||
let m, mStr;
|
||||
while ((m = rxPossible.exec(ruleCode))) {
|
||||
const possible = m[1];
|
||||
const set = [];
|
||||
while ((mStr = rxString.exec(possible))) {
|
||||
const s = mStr[1];
|
||||
if (s.includes(' ')) {
|
||||
set.push(...s.split(/\s+/));
|
||||
} else {
|
||||
set.push(s);
|
||||
}
|
||||
}
|
||||
if (possible.includes('ignoreAtRules')) {
|
||||
set.push('ignoreAtRules');
|
||||
}
|
||||
if (possible.includes('ignoreShorthands')) {
|
||||
set.push('ignoreShorthands');
|
||||
}
|
||||
if (set.length) {
|
||||
sets.push(set);
|
||||
}
|
||||
}
|
||||
if (sets.length) {
|
||||
options[id] = sets;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
||||
function loadParserLib() {
|
||||
if (typeof parserlib !== 'undefined') {
|
||||
return;
|
||||
}
|
||||
importScripts('/vendor-overwrites/csslint/parserlib.js');
|
||||
parserlib.css.Tokens[parserlib.css.Tokens.COMMENT].hide = false;
|
||||
}
|
||||
|
||||
const loadedUrls = new Set();
|
||||
function loadScript(urls) {
|
||||
urls = urls.filter(u => !loadedUrls.has(u));
|
||||
importScripts(...urls);
|
||||
urls.forEach(u => loadedUrls.add(u));
|
||||
}
|
||||
|
||||
function createAPI(methods) {
|
||||
self.onmessage = e => {
|
||||
const message = e.data;
|
||||
Promise.resolve()
|
||||
.then(() => methods[message.action](...message.args))
|
||||
.then(result => ({
|
||||
id: message.id,
|
||||
error: false,
|
||||
data: result
|
||||
}))
|
||||
.catch(err => ({
|
||||
id: message.id,
|
||||
error: true,
|
||||
data: cloneError(err)
|
||||
}))
|
||||
.then(data => self.postMessage(data));
|
||||
};
|
||||
}
|
||||
|
||||
function cloneError(err) {
|
||||
return Object.assign({
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
message: err.message,
|
||||
lineNumber: err.lineNumber,
|
||||
columnNumber: err.columnNumber,
|
||||
fileName: err.fileName
|
||||
}, err);
|
||||
}
|
|
@ -1,39 +1,88 @@
|
|||
/* global importScripts workerUtil CSSLint require metaParser */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var editorWorker = (() => {
|
||||
let worker;
|
||||
return new Proxy({}, {
|
||||
get: (target, prop) =>
|
||||
(...args) => {
|
||||
if (!worker) {
|
||||
worker = createWorker();
|
||||
}
|
||||
return worker.invoke(prop, args);
|
||||
}
|
||||
});
|
||||
importScripts('/js/worker-util.js');
|
||||
const {createAPI, loadScript} = workerUtil;
|
||||
|
||||
function createWorker() {
|
||||
let id = 0;
|
||||
const pendingResponse = new Map();
|
||||
const worker = new Worker('/edit/editor-worker-body.js');
|
||||
worker.onmessage = e => {
|
||||
const message = e.data;
|
||||
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
|
||||
pendingResponse.delete(message.id);
|
||||
};
|
||||
return {invoke};
|
||||
createAPI({
|
||||
csslint: (code, config) => {
|
||||
loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
|
||||
return CSSLint.verify(code, config).messages
|
||||
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
|
||||
},
|
||||
stylelint: (code, config) => {
|
||||
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||
return require('stylelint').lint({code, config});
|
||||
},
|
||||
metalint: code => {
|
||||
loadScript(
|
||||
'/vendor/usercss-meta/usercss-meta.min.js',
|
||||
'/vendor-overwrites/colorpicker/colorconverter.js',
|
||||
'/js/meta-parser.js'
|
||||
);
|
||||
const result = metaParser.lint(code);
|
||||
// extract needed info
|
||||
result.errors = result.errors.map(err =>
|
||||
({
|
||||
code: err.code,
|
||||
args: err.args,
|
||||
message: err.message,
|
||||
index: err.index
|
||||
})
|
||||
);
|
||||
return result;
|
||||
},
|
||||
getStylelintRules,
|
||||
getCsslintRules
|
||||
});
|
||||
|
||||
function invoke(action, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingResponse.set(id, {resolve, reject});
|
||||
worker.postMessage({
|
||||
id,
|
||||
action,
|
||||
args
|
||||
});
|
||||
id++;
|
||||
});
|
||||
function getCsslintRules() {
|
||||
loadScript('/vendor-overwrites/csslint/csslint.js');
|
||||
return CSSLint.getRules().map(rule => {
|
||||
const output = {};
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (typeof value !== 'function') {
|
||||
output[key] = value;
|
||||
}
|
||||
}
|
||||
})();
|
||||
return output;
|
||||
});
|
||||
}
|
||||
|
||||
function getStylelintRules() {
|
||||
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js');
|
||||
const stylelint = require('stylelint');
|
||||
const options = {};
|
||||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
|
||||
const rxString = /"([-\w\s]{3,}?)"/g;
|
||||
for (const id of Object.keys(stylelint.rules)) {
|
||||
const ruleCode = String(stylelint.rules[id]);
|
||||
const sets = [];
|
||||
let m, mStr;
|
||||
while ((m = rxPossible.exec(ruleCode))) {
|
||||
const possible = m[1];
|
||||
const set = [];
|
||||
while ((mStr = rxString.exec(possible))) {
|
||||
const s = mStr[1];
|
||||
if (s.includes(' ')) {
|
||||
set.push(...s.split(/\s+/));
|
||||
} else {
|
||||
set.push(s);
|
||||
}
|
||||
}
|
||||
if (possible.includes('ignoreAtRules')) {
|
||||
set.push('ignoreAtRules');
|
||||
}
|
||||
if (possible.includes('ignoreShorthands')) {
|
||||
set.push('ignoreShorthands');
|
||||
}
|
||||
if (set.length) {
|
||||
sets.push(set);
|
||||
}
|
||||
}
|
||||
if (sets.length) {
|
||||
options[id] = sets;
|
||||
}
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
/* global CodeMirror editors makeSectionVisible */
|
||||
/* global focusAccessibility */
|
||||
/* global colorMimicry */
|
||||
/* global CodeMirror focusAccessibility colorMimicry editor
|
||||
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */
|
||||
'use strict';
|
||||
|
||||
onDOMready().then(() => {
|
||||
|
@ -207,12 +206,12 @@ onDOMready().then(() => {
|
|||
}
|
||||
const cmFocused = document.activeElement && document.activeElement.closest('.CodeMirror');
|
||||
state.activeAppliesTo = $(`.${APPLIES_VALUE_CLASS}:focus, .${APPLIES_VALUE_CLASS}.${TARGET_CLASS}`);
|
||||
state.cmStart = CodeMirror.closestVisible(
|
||||
state.cmStart = editor.closestVisible(
|
||||
cmFocused && document.activeElement ||
|
||||
state.activeAppliesTo ||
|
||||
state.cm);
|
||||
const cmExtra = $('body > :not(#sections) .CodeMirror');
|
||||
state.editors = cmExtra ? [cmExtra.CodeMirror] : editors;
|
||||
state.editors = cmExtra ? [cmExtra.CodeMirror] : editor.getEditors();
|
||||
}
|
||||
|
||||
|
||||
|
@ -291,7 +290,7 @@ onDOMready().then(() => {
|
|||
|
||||
function doSearchInApplies(cm, canAdvance) {
|
||||
if (!state.searchInApplies) return;
|
||||
const inputs = [...cm.getSection().getElementsByClassName(APPLIES_VALUE_CLASS)];
|
||||
const inputs = editor.getSearchableInputs(cm);
|
||||
if (state.reverse) inputs.reverse();
|
||||
inputs.splice(0, inputs.indexOf(state.activeAppliesTo));
|
||||
for (const input of inputs) {
|
||||
|
@ -314,7 +313,7 @@ onDOMready().then(() => {
|
|||
});
|
||||
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement);
|
||||
makeTargetVisible(!canFocus && input);
|
||||
makeSectionVisible(cm);
|
||||
editor.scrollToEditor(cm);
|
||||
if (canFocus) input.focus();
|
||||
state.cm = cm;
|
||||
clearMarker();
|
||||
|
@ -778,7 +777,7 @@ onDOMready().then(() => {
|
|||
cm.scrollIntoView(searchCursor.pos, SCROLL_REVEAL_MIN_PX);
|
||||
|
||||
// scroll to the editor itself
|
||||
makeSectionVisible(cm);
|
||||
editor.scrollToEditor(cm);
|
||||
|
||||
// focus or expose as the current search target
|
||||
clearMarker();
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox LINTER_DEFAULTS*/
|
||||
/* global memoize editorWorker showCodeMirrorPopup loadScript messageBox
|
||||
LINTER_DEFAULTS rerouteHotkeys $ $create $createLink tryJSONparse t
|
||||
chromeSync */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
@ -50,10 +52,10 @@
|
|||
});
|
||||
cm.on('changes', updateButtonState);
|
||||
|
||||
cm.rerouteHotkeys(false);
|
||||
rerouteHotkeys(false);
|
||||
window.addEventListener('closeHelp', function _() {
|
||||
window.removeEventListener('closeHelp', _);
|
||||
cm.rerouteHotkeys(true);
|
||||
rerouteHotkeys(true);
|
||||
cm = null;
|
||||
});
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
/* exported LINTER_DEFAULTS */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var LINTER_DEFAULTS = (() => {
|
||||
const LINTER_DEFAULTS = (() => {
|
||||
const SEVERITY = {severity: 'warning'};
|
||||
const STYLELINT = {
|
||||
// 'sugarss' is a indent-based syntax like Sass or Stylus
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
/* global LINTER_DEFAULTS linter editorWorker */
|
||||
/* global LINTER_DEFAULTS linter editorWorker prefs chromeSync */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* global showHelp editorWorker memoize */
|
||||
/* global showHelp editorWorker memoize $ $create $createLink t */
|
||||
/* exported createLinterHelpDialog */
|
||||
'use strict';
|
||||
|
||||
function createLinterHelpDialog(getIssues) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* global linter */
|
||||
/* global linter editorWorker */
|
||||
/* exported createMetaCompiler */
|
||||
'use strict';
|
||||
|
||||
function createMetaCompiler(cm) {
|
||||
|
@ -18,25 +19,23 @@ function createMetaCompiler(cm) {
|
|||
if (match[0] === meta && match.index === metaIndex) {
|
||||
return cache;
|
||||
}
|
||||
return API.parseUsercss({sourceCode: match[0], metaOnly: true})
|
||||
.then(result => result.usercssData)
|
||||
.then(result => {
|
||||
return editorWorker.metalint(match[0])
|
||||
.then(({metadata, errors}) => {
|
||||
if (errors.every(err => err.code === 'unknownMeta')) {
|
||||
for (const cb of updateListeners) {
|
||||
cb(result);
|
||||
cb(metadata);
|
||||
}
|
||||
meta = match[0];
|
||||
metaIndex = match.index;
|
||||
cache = [];
|
||||
return cache;
|
||||
}, err => {
|
||||
meta = match[0];
|
||||
metaIndex = match.index;
|
||||
cache = [{
|
||||
}
|
||||
cache = errors.map(err =>
|
||||
({
|
||||
from: cm.posFromIndex((err.index || 0) + match.index),
|
||||
to: cm.posFromIndex((err.index || 0) + match.index),
|
||||
message: err.message,
|
||||
severity: 'error'
|
||||
}];
|
||||
message: err.code && chrome.i18n.getMessage(`meta_${err.code}`, err.args) || err.message,
|
||||
severity: err.code === 'unknownMeta' ? 'warning' : 'error'
|
||||
})
|
||||
);
|
||||
meta = match[0];
|
||||
metaIndex = match.index;
|
||||
return cache;
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
/* global linter editors clipString createLinterHelpDialog makeSectionVisible */
|
||||
/* global linter editor clipString createLinterHelpDialog $ $create */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
Object.assign(linter, (() => {
|
||||
const tables = new Map();
|
||||
const helpDialog = createLinterHelpDialog(getIssues);
|
||||
|
@ -16,13 +15,9 @@ Object.assign(linter, (() => {
|
|||
table = createTable(cm);
|
||||
tables.set(cm, table);
|
||||
const container = $('.lint-report-container');
|
||||
if (typeof editor === 'object') {
|
||||
container.append(table.element);
|
||||
} else {
|
||||
const nextSibling = findNextSibling(tables, cm);
|
||||
container.insertBefore(table.element, nextSibling && tables.get(nextSibling).element);
|
||||
}
|
||||
}
|
||||
table.updateCaption();
|
||||
table.updateAnnotations(annotations);
|
||||
updateCount();
|
||||
|
@ -57,6 +52,7 @@ Object.assign(linter, (() => {
|
|||
}
|
||||
|
||||
function findNextSibling(tables, cm) {
|
||||
const editors = editor.getEditors();
|
||||
let i = editors.indexOf(cm) + 1;
|
||||
while (i < editors.length) {
|
||||
if (tables.has(editors[i])) {
|
||||
|
@ -85,8 +81,7 @@ Object.assign(linter, (() => {
|
|||
};
|
||||
|
||||
function updateCaption() {
|
||||
caption.textContent = typeof editor === 'object' ?
|
||||
'' : `${t('sectionCode')} ${editors.indexOf(cm) + 1}`;
|
||||
caption.textContent = editor.getEditorTitle(cm);
|
||||
}
|
||||
|
||||
function updateAnnotations(lines) {
|
||||
|
@ -158,7 +153,7 @@ Object.assign(linter, (() => {
|
|||
}
|
||||
|
||||
function gotoLintIssue(cm, anno) {
|
||||
makeSectionVisible(cm);
|
||||
editor.scrollToEditor(cm);
|
||||
cm.focus();
|
||||
cm.setSelection(anno.from);
|
||||
}
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
/* global prefs */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var linter = (() => {
|
||||
/* exported linter */
|
||||
const linter = (() => {
|
||||
const lintingUpdatedListeners = [];
|
||||
const unhookListeners = [];
|
||||
const linters = [];
|
||||
|
|
73
edit/live-preview.js
Normal file
73
edit/live-preview.js
Normal file
|
@ -0,0 +1,73 @@
|
|||
/* global messageBox editor $ prefs */
|
||||
/* exported createLivePreview */
|
||||
'use strict';
|
||||
|
||||
function createLivePreview(preprocess) {
|
||||
let data;
|
||||
let previewer;
|
||||
let enabled = prefs.get('editor.livePreview');
|
||||
const label = $('#preview-label');
|
||||
const errorContainer = $('#preview-errors');
|
||||
|
||||
prefs.subscribe(['editor.livePreview'], (key, value) => {
|
||||
if (value && data && data.id && data.enabled) {
|
||||
previewer = createPreviewer();
|
||||
previewer.update(data);
|
||||
}
|
||||
if (!value && previewer) {
|
||||
previewer.disconnect();
|
||||
previewer = null;
|
||||
}
|
||||
enabled = value;
|
||||
});
|
||||
return {update, show};
|
||||
|
||||
function show(state) {
|
||||
label.classList.toggle('hidden', !state);
|
||||
}
|
||||
|
||||
function update(_data) {
|
||||
data = _data;
|
||||
if (!previewer) {
|
||||
if (!data.id || !data.enabled || !enabled) {
|
||||
return;
|
||||
}
|
||||
previewer = createPreviewer();
|
||||
}
|
||||
previewer.update(data);
|
||||
}
|
||||
|
||||
function createPreviewer() {
|
||||
const port = chrome.runtime.connect({
|
||||
name: 'livePreview'
|
||||
});
|
||||
port.onDisconnect.addListener(err => {
|
||||
throw err;
|
||||
});
|
||||
return {update, disconnect};
|
||||
|
||||
function update(data) {
|
||||
Promise.resolve()
|
||||
.then(() => preprocess ? preprocess(data) : data)
|
||||
.then(data => port.postMessage(data))
|
||||
.then(
|
||||
() => errorContainer.classList.add('hidden'),
|
||||
err => {
|
||||
if (Array.isArray(err)) {
|
||||
err = err.join('\n');
|
||||
} else if (err && err.index !== undefined) {
|
||||
// FIXME: this would fail if editors[0].getValue() !== data.sourceCode
|
||||
const pos = editor.getEditors()[0].posFromIndex(err.index);
|
||||
err.message = `${pos.line}:${pos.ch} ${err.message || String(err)}`;
|
||||
}
|
||||
errorContainer.classList.remove('hidden');
|
||||
errorContainer.onclick = () => messageBox.alert(err.message || String(err), 'pre');
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function disconnect() {
|
||||
port.disconnect();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,4 +1,4 @@
|
|||
/* global CodeMirror */
|
||||
/* global CodeMirror prefs */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
|
27
edit/refresh-on-view.js
Normal file
27
edit/refresh-on-view.js
Normal file
|
@ -0,0 +1,27 @@
|
|||
/* global CodeMirror */
|
||||
/*
|
||||
Initialization of the multi-sections editor is slow if there are many editors
|
||||
e.g. https://github.com/openstyles/stylus/issues/178. So we only refresh the
|
||||
editor when they were scroll into view.
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
CodeMirror.defineExtension('refreshOnView', function () {
|
||||
const cm = this;
|
||||
if (typeof IntersectionObserver === 'undefined') {
|
||||
// uh
|
||||
cm.refresh();
|
||||
return;
|
||||
}
|
||||
const wrapper = cm.display.wrapper;
|
||||
const observer = new IntersectionObserver(entries => {
|
||||
for (const entry of entries) {
|
||||
if (entry.isIntersecting) {
|
||||
// wrapper.style.visibility = 'visible';
|
||||
cm.refresh();
|
||||
observer.disconnect();
|
||||
}
|
||||
}
|
||||
});
|
||||
observer.observe(wrapper);
|
||||
});
|
|
@ -1,8 +1,8 @@
|
|||
/* global showHelp */
|
||||
/* global showHelp $ $create tryRegExp queryTabs URLS t template openURL */
|
||||
/* exported regExpTester */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var regExpTester = (() => {
|
||||
const regExpTester = (() => {
|
||||
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
||||
const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
||||
const cachedRegexps = new Map();
|
||||
|
@ -58,7 +58,7 @@ var regExpTester = (() => {
|
|||
const rxData = Object.assign({text}, cachedRegexps.get(text));
|
||||
if (!rxData.urls) {
|
||||
cachedRegexps.set(text, Object.assign(rxData, {
|
||||
// imitate buggy Stylish-for-chrome, see detectSloppyRegexps()
|
||||
// imitate buggy Stylish-for-chrome
|
||||
rx: tryRegExp('^' + text + '$'),
|
||||
urls: new Map(),
|
||||
}));
|
||||
|
|
48
edit/reroute-hotkeys.js
Normal file
48
edit/reroute-hotkeys.js
Normal file
|
@ -0,0 +1,48 @@
|
|||
/* global CodeMirror editor debounce */
|
||||
/* exported rerouteHotkeys */
|
||||
'use strict';
|
||||
|
||||
const rerouteHotkeys = (() => {
|
||||
// reroute handling to nearest editor when keypress resolves to one of these commands
|
||||
const REROUTED = new Set([
|
||||
'save',
|
||||
'toggleStyle',
|
||||
'jumpToLine',
|
||||
'nextEditor', 'prevEditor',
|
||||
'toggleEditorFocus',
|
||||
'find', 'findNext', 'findPrev', 'replace', 'replaceAll',
|
||||
'colorpicker',
|
||||
]);
|
||||
|
||||
return rerouteHotkeys;
|
||||
|
||||
// note that this function relies on `editor`. Calling this function before
|
||||
// the editor is initialized may throw an error.
|
||||
function rerouteHotkeys(enable, immediately) {
|
||||
if (!immediately) {
|
||||
debounce(rerouteHotkeys, 0, enable, true);
|
||||
} else if (enable) {
|
||||
document.addEventListener('keydown', rerouteHandler);
|
||||
} else {
|
||||
document.removeEventListener('keydown', rerouteHandler);
|
||||
}
|
||||
}
|
||||
|
||||
function rerouteHandler(event) {
|
||||
const keyName = CodeMirror.keyName(event);
|
||||
if (!keyName) {
|
||||
return;
|
||||
}
|
||||
const rerouteCommand = name => {
|
||||
if (REROUTED.has(name)) {
|
||||
CodeMirror.commands[name](editor.closestVisible(event.target));
|
||||
return true;
|
||||
}
|
||||
};
|
||||
if (CodeMirror.lookupKey(keyName, CodeMirror.defaults.keyMap, rerouteCommand) === 'handled' ||
|
||||
CodeMirror.lookupKey(keyName, CodeMirror.defaults.extraKeys, rerouteCommand) === 'handled') {
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
}
|
||||
}
|
||||
})();
|
409
edit/sections-editor-section.js
Normal file
409
edit/sections-editor-section.js
Normal file
|
@ -0,0 +1,409 @@
|
|||
/* global template cmFactory $ propertyToCss CssToProperty linter regExpTester
|
||||
FIREFOX toggleContextMenuDelete beautify showHelp t tryRegExp */
|
||||
/* exported createSection */
|
||||
'use strict';
|
||||
|
||||
function createResizeGrip(cm) {
|
||||
const wrapper = cm.display.wrapper;
|
||||
wrapper.classList.add('resize-grip-enabled');
|
||||
const resizeGrip = template.resizeGrip.cloneNode(true);
|
||||
wrapper.appendChild(resizeGrip);
|
||||
let lastClickTime = 0;
|
||||
resizeGrip.onmousedown = event => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (Date.now() - lastClickTime < 500) {
|
||||
lastClickTime = 0;
|
||||
toggleSectionHeight(cm);
|
||||
return;
|
||||
}
|
||||
lastClickTime = Date.now();
|
||||
const minHeight = cm.defaultTextHeight() +
|
||||
/* .CodeMirror-lines padding */
|
||||
cm.display.lineDiv.offsetParent.offsetTop +
|
||||
/* borders */
|
||||
wrapper.offsetHeight - wrapper.clientHeight;
|
||||
wrapper.style.pointerEvents = 'none';
|
||||
document.body.style.cursor = 's-resize';
|
||||
document.addEventListener('mousemove', resize);
|
||||
document.addEventListener('mouseup', resizeStop);
|
||||
|
||||
function resize(e) {
|
||||
const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
|
||||
const height = Math.max(minHeight, e.pageY - cmPageY);
|
||||
if (height !== wrapper.clientHeight) {
|
||||
cm.setSize(null, height);
|
||||
}
|
||||
}
|
||||
|
||||
function resizeStop() {
|
||||
document.removeEventListener('mouseup', resizeStop);
|
||||
document.removeEventListener('mousemove', resize);
|
||||
wrapper.style.pointerEvents = '';
|
||||
document.body.style.cursor = '';
|
||||
}
|
||||
};
|
||||
|
||||
function toggleSectionHeight(cm) {
|
||||
if (cm.state.toggleHeightSaved) {
|
||||
// restore previous size
|
||||
cm.setSize(null, cm.state.toggleHeightSaved);
|
||||
cm.state.toggleHeightSaved = 0;
|
||||
} else {
|
||||
// maximize
|
||||
const wrapper = cm.display.wrapper;
|
||||
const allBounds = $('#sections').getBoundingClientRect();
|
||||
const pageExtrasHeight = allBounds.top + window.scrollY +
|
||||
parseFloat(getComputedStyle($('#sections')).paddingBottom);
|
||||
const sectionEl = wrapper.parentNode;
|
||||
const sectionExtrasHeight = sectionEl.clientHeight - wrapper.offsetHeight;
|
||||
cm.state.toggleHeightSaved = wrapper.clientHeight;
|
||||
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
|
||||
const bounds = sectionEl.getBoundingClientRect();
|
||||
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
|
||||
window.scrollBy(0, bounds.top);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function createSection({
|
||||
// data model
|
||||
originalSection,
|
||||
dirty,
|
||||
// util
|
||||
nextEditor,
|
||||
prevEditor,
|
||||
genId,
|
||||
// emit events
|
||||
// TODO: better names like `onRemoved`? Or make a real event emitter.
|
||||
showMozillaFormatImport,
|
||||
removeSection,
|
||||
insertSectionAfter,
|
||||
moveSectionUp,
|
||||
moveSectionDown,
|
||||
restoreSection,
|
||||
}) {
|
||||
const sectionId = genId();
|
||||
const el = template.section.cloneNode(true);
|
||||
const cm = cmFactory.create(wrapper => {
|
||||
el.insertBefore(wrapper, $('.code-label', el).nextSibling);
|
||||
}, {value: originalSection.code});
|
||||
|
||||
const changeListeners = new Set();
|
||||
|
||||
const appliesToContainer = $('.applies-to-list', el);
|
||||
const appliesTo = [];
|
||||
for (const [key, fnName] of Object.entries(propertyToCss)) {
|
||||
if (originalSection[key]) {
|
||||
originalSection[key].forEach(value =>
|
||||
insertApplyAfter({type: fnName, value})
|
||||
);
|
||||
}
|
||||
}
|
||||
if (!appliesTo.length) {
|
||||
insertApplyAfter({all: true});
|
||||
}
|
||||
|
||||
let changeGeneration = cm.changeGeneration();
|
||||
let removed = false;
|
||||
|
||||
registerEvents();
|
||||
updateRegexpTester();
|
||||
createResizeGrip(cm);
|
||||
|
||||
linter.enableForEditor(cm);
|
||||
|
||||
const section = {
|
||||
id: sectionId,
|
||||
el,
|
||||
cm,
|
||||
render,
|
||||
getModel,
|
||||
remove,
|
||||
destroy,
|
||||
restore,
|
||||
isRemoved: () => removed,
|
||||
onChange,
|
||||
off,
|
||||
appliesTo
|
||||
};
|
||||
return section;
|
||||
|
||||
function onChange(fn) {
|
||||
changeListeners.add(fn);
|
||||
}
|
||||
|
||||
function off(fn) {
|
||||
changeListeners.delete(fn);
|
||||
}
|
||||
|
||||
function emitSectionChange() {
|
||||
for (const fn of changeListeners) {
|
||||
fn();
|
||||
}
|
||||
}
|
||||
|
||||
function getModel() {
|
||||
const section = {
|
||||
code: cm.getValue()
|
||||
};
|
||||
for (const apply of appliesTo) {
|
||||
if (apply.all) {
|
||||
continue;
|
||||
}
|
||||
const key = CssToProperty[apply.getType()];
|
||||
if (!section[key]) {
|
||||
section[key] = [];
|
||||
}
|
||||
section[key].push(apply.getValue());
|
||||
}
|
||||
return section;
|
||||
}
|
||||
|
||||
function registerEvents() {
|
||||
cm.on('changes', () => {
|
||||
const newGeneration = cm.changeGeneration();
|
||||
dirty.modify(`section.${sectionId}.code`, changeGeneration, newGeneration);
|
||||
changeGeneration = newGeneration;
|
||||
emitSectionChange();
|
||||
});
|
||||
cm.on('paste', (cm, event) => {
|
||||
const text = event.clipboardData.getData('text') || '';
|
||||
if (
|
||||
text.includes('@-moz-document') &&
|
||||
text.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, '')
|
||||
.match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/)
|
||||
) {
|
||||
event.preventDefault();
|
||||
showMozillaFormatImport(text);
|
||||
}
|
||||
});
|
||||
if (!FIREFOX) {
|
||||
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
||||
}
|
||||
cm.display.wrapper.addEventListener('keydown', event =>
|
||||
handleKeydown(cm, event), true);
|
||||
|
||||
$('.applies-to-help', el).addEventListener('click', showAppliesToHelp);
|
||||
$('.remove-section', el).addEventListener('click', () => removeSection(section));
|
||||
$('.add-section', el).addEventListener('click', () => insertSectionAfter(undefined, section));
|
||||
$('.clone-section', el).addEventListener('click', () => insertSectionAfter(getModel(), section));
|
||||
$('.move-section-up', el).addEventListener('click', () => moveSectionUp(section));
|
||||
$('.move-section-down', el).addEventListener('click', () => moveSectionDown(section));
|
||||
$('.beautify-section', el).addEventListener('click', () => beautify([cm]));
|
||||
$('.restore-section', el).addEventListener('click', () => restoreSection(section));
|
||||
$('.test-regexp', el).addEventListener('click', () => {
|
||||
regExpTester.toggle();
|
||||
updateRegexpTester();
|
||||
});
|
||||
}
|
||||
|
||||
function handleKeydown(cm, event) {
|
||||
const key = event.which;
|
||||
if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
const {line, ch} = cm.getCursor();
|
||||
switch (key) {
|
||||
case 37:
|
||||
// arrow Left
|
||||
if (line || ch) {
|
||||
return;
|
||||
}
|
||||
// fallthrough to arrow Up
|
||||
case 38:
|
||||
// arrow Up
|
||||
cm = line === 0 && prevEditor(cm, false);
|
||||
if (!cm) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
|
||||
break;
|
||||
case 39:
|
||||
// arrow Right
|
||||
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
|
||||
return;
|
||||
}
|
||||
// fallthrough to arrow Down
|
||||
case 40:
|
||||
// arrow Down
|
||||
cm = line === cm.doc.size - 1 && nextEditor(cm, false);
|
||||
if (!cm) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cm.setCursor(0, 0);
|
||||
break;
|
||||
}
|
||||
// FIXME: what is this?
|
||||
// const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
|
||||
// if (animation) {
|
||||
// animation.playbackRate = -1;
|
||||
// animation.currentTime = 2000;
|
||||
// animation.play();
|
||||
// }
|
||||
}
|
||||
|
||||
function showAppliesToHelp(event) {
|
||||
event.preventDefault();
|
||||
showHelp(t('appliesLabel'), t('appliesHelp'));
|
||||
}
|
||||
|
||||
function remove() {
|
||||
linter.disableForEditor(cm);
|
||||
el.classList.add('removed');
|
||||
removed = true;
|
||||
appliesTo.forEach(a => a.remove());
|
||||
}
|
||||
|
||||
function destroy() {
|
||||
cmFactory.destroy(cm);
|
||||
}
|
||||
|
||||
function restore() {
|
||||
linter.enableForEditor(cm);
|
||||
el.classList.remove('removed');
|
||||
removed = false;
|
||||
appliesTo.forEach(a => a.restore());
|
||||
render();
|
||||
}
|
||||
|
||||
function render() {
|
||||
cm.refresh();
|
||||
}
|
||||
|
||||
function updateRegexpTester() {
|
||||
const regexps = appliesTo.filter(a => a.getType() === 'regexp')
|
||||
.map(a => a.getValue());
|
||||
if (regexps.length) {
|
||||
el.classList.add('has-regexp');
|
||||
regExpTester.update(regexps);
|
||||
} else {
|
||||
el.classList.remove('has-regexp');
|
||||
regExpTester.toggle(false);
|
||||
}
|
||||
}
|
||||
|
||||
function insertApplyAfter(init, base) {
|
||||
const apply = createApply(init);
|
||||
if (base) {
|
||||
const index = appliesTo.indexOf(base);
|
||||
appliesTo.splice(index + 1, 0, apply);
|
||||
appliesToContainer.insertBefore(apply.el, base.el.nextSibling);
|
||||
} else {
|
||||
appliesTo.push(apply);
|
||||
appliesToContainer.appendChild(apply.el);
|
||||
}
|
||||
dirty.add(apply, apply);
|
||||
if (appliesTo.length > 1 && appliesTo[0].all) {
|
||||
removeApply(appliesTo[0]);
|
||||
}
|
||||
emitSectionChange();
|
||||
}
|
||||
|
||||
function removeApply(apply) {
|
||||
const index = appliesTo.indexOf(apply);
|
||||
appliesTo.splice(index, 1);
|
||||
apply.remove();
|
||||
apply.el.remove();
|
||||
dirty.remove(apply, apply);
|
||||
if (!appliesTo.length) {
|
||||
insertApplyAfter({all: true});
|
||||
}
|
||||
emitSectionChange();
|
||||
}
|
||||
|
||||
function createApply({type = 'url', value, all = false}) {
|
||||
const applyId = genId();
|
||||
const dirtyPrefix = `section.${sectionId}.apply.${applyId}`;
|
||||
const el = all ? template.appliesToEverything.cloneNode(true) :
|
||||
template.appliesTo.cloneNode(true);
|
||||
|
||||
const selectEl = !all && $('.applies-type', el);
|
||||
if (selectEl) {
|
||||
selectEl.value = type;
|
||||
selectEl.addEventListener('change', () => {
|
||||
const oldType = type;
|
||||
dirty.modify(`${dirtyPrefix}.type`, type, selectEl.value);
|
||||
type = selectEl.value;
|
||||
if (oldType === 'regexp' || type === 'regexp') {
|
||||
updateRegexpTester();
|
||||
}
|
||||
emitSectionChange();
|
||||
validate();
|
||||
});
|
||||
}
|
||||
|
||||
const valueEl = !all && $('.applies-value', el);
|
||||
if (valueEl) {
|
||||
valueEl.value = value;
|
||||
valueEl.addEventListener('input', () => {
|
||||
dirty.modify(`${dirtyPrefix}.value`, value, valueEl.value);
|
||||
value = valueEl.value;
|
||||
if (type === 'regexp') {
|
||||
updateRegexpTester();
|
||||
}
|
||||
emitSectionChange();
|
||||
});
|
||||
valueEl.addEventListener('change', validate);
|
||||
}
|
||||
|
||||
restore();
|
||||
|
||||
const apply = {
|
||||
id: applyId,
|
||||
all,
|
||||
remove,
|
||||
restore,
|
||||
el,
|
||||
getType: () => type,
|
||||
getValue: () => value,
|
||||
valueEl // used by validator
|
||||
};
|
||||
|
||||
const removeButton = $('.remove-applies-to', el);
|
||||
if (removeButton) {
|
||||
removeButton.addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
removeApply(apply);
|
||||
});
|
||||
}
|
||||
$('.add-applies-to', el).addEventListener('click', e => {
|
||||
e.preventDefault();
|
||||
insertApplyAfter({type, value: ''}, apply);
|
||||
});
|
||||
|
||||
return apply;
|
||||
|
||||
function validate() {
|
||||
if (type !== 'regexp' || tryRegExp(value)) {
|
||||
valueEl.setCustomValidity('');
|
||||
} else {
|
||||
valueEl.setCustomValidity(t('styleBadRegexp'));
|
||||
setTimeout(() => valueEl.reportValidity());
|
||||
}
|
||||
}
|
||||
|
||||
function remove() {
|
||||
if (all) {
|
||||
return;
|
||||
}
|
||||
dirty.remove(`${dirtyPrefix}.type`, type);
|
||||
dirty.remove(`${dirtyPrefix}.value`, value);
|
||||
}
|
||||
|
||||
function restore() {
|
||||
if (all) {
|
||||
return;
|
||||
}
|
||||
dirty.add(`${dirtyPrefix}.type`, type);
|
||||
dirty.add(`${dirtyPrefix}.value`, value);
|
||||
}
|
||||
}
|
||||
}
|
594
edit/sections-editor.js
Normal file
594
edit/sections-editor.js
Normal file
|
@ -0,0 +1,594 @@
|
|||
/* global dirtyReporter showHelp toggleContextMenuDelete createSection
|
||||
CodeMirror linter createLivePreview showCodeMirrorPopup
|
||||
sectionsToMozFormat messageBox clipString
|
||||
rerouteHotkeys $ $$ $create t FIREFOX API
|
||||
debounce */
|
||||
/* exported createSectionsEditor */
|
||||
'use strict';
|
||||
|
||||
function createSectionsEditor(style) {
|
||||
let INC_ID = 0; // an increment id that is used by various object to track the order
|
||||
const dirty = dirtyReporter();
|
||||
dirty.onChange(updateTitle);
|
||||
|
||||
const container = $('#sections');
|
||||
const sections = [];
|
||||
|
||||
const nameEl = $('#name');
|
||||
nameEl.addEventListener('input', () => {
|
||||
dirty.modify('name', style.name, nameEl.value);
|
||||
style.name = nameEl.value;
|
||||
updateTitle();
|
||||
});
|
||||
|
||||
const enabledEl = $('#enabled');
|
||||
enabledEl.addEventListener('change', () => {
|
||||
dirty.modify('enabled', style.enabled, enabledEl.checked);
|
||||
style.enabled = enabledEl.checked;
|
||||
updateLivePreview();
|
||||
});
|
||||
|
||||
$('#to-mozilla').addEventListener('click', showMozillaFormat);
|
||||
$('#to-mozilla-help').addEventListener('click', showToMozillaHelp);
|
||||
$('#from-mozilla').addEventListener('click', () => showMozillaFormatImport());
|
||||
$('#save-button').addEventListener('click', saveStyle);
|
||||
|
||||
document.addEventListener('wheel', scrollEntirePageOnCtrlShift);
|
||||
|
||||
if (!FIREFOX) {
|
||||
$$([
|
||||
'input:not([type])',
|
||||
'input[type="text"]',
|
||||
'input[type="search"]',
|
||||
'input[type="number"]',
|
||||
].join(','))
|
||||
.forEach(e => e.addEventListener('mousedown', toggleContextMenuDelete));
|
||||
}
|
||||
|
||||
let sectionOrder = '';
|
||||
const initializing = new Promise(resolve => initSection({
|
||||
sections: style.sections.slice(),
|
||||
done:() => {
|
||||
// FIXME: implement this with CSS?
|
||||
// https://github.com/openstyles/stylus/commit/2895ce11e271788df0e4f7314b3b981fde086574
|
||||
dirty.clear();
|
||||
rerouteHotkeys(true);
|
||||
resolve();
|
||||
updateHeader();
|
||||
}
|
||||
}));
|
||||
|
||||
const livePreview = createLivePreview();
|
||||
livePreview.show(Boolean(style.id));
|
||||
|
||||
return {
|
||||
ready: () => initializing,
|
||||
replaceStyle,
|
||||
isDirty: dirty.isDirty,
|
||||
getStyle: () => style,
|
||||
getEditors,
|
||||
scrollToEditor,
|
||||
getStyleId: () => style.id,
|
||||
getEditorTitle: cm => {
|
||||
const index = sections.filter(s => !s.isRemoved()).findIndex(s => s.cm === cm);
|
||||
return `${t('sectionCode')} ${index + 1}`;
|
||||
},
|
||||
save: saveStyle,
|
||||
toggleStyle,
|
||||
nextEditor,
|
||||
prevEditor,
|
||||
closestVisible,
|
||||
getSearchableInputs,
|
||||
};
|
||||
|
||||
function genId() {
|
||||
return INC_ID++;
|
||||
}
|
||||
|
||||
function setGlobalProgress(done, total) {
|
||||
const progressElement = $('#global-progress') ||
|
||||
total && document.body.appendChild($create('#global-progress'));
|
||||
if (total) {
|
||||
const progress = (done / Math.max(done, total) * 100).toFixed(1);
|
||||
progressElement.style.borderLeftWidth = progress + 'vw';
|
||||
setTimeout(() => {
|
||||
progressElement.title = progress + '%';
|
||||
});
|
||||
} else {
|
||||
$.remove(progressElement);
|
||||
}
|
||||
}
|
||||
|
||||
function showToMozillaHelp(event) {
|
||||
event.preventDefault();
|
||||
showHelp(t('styleMozillaFormatHeading'), t('styleToMozillaFormatHelp'));
|
||||
}
|
||||
|
||||
function getSearchableInputs(cm) {
|
||||
return sections.find(s => s.cm === cm).appliesTo.map(a => a.valueEl).filter(Boolean);
|
||||
}
|
||||
|
||||
// priority:
|
||||
// 1. associated CM for applies-to element
|
||||
// 2. last active if visible
|
||||
// 3. first visible
|
||||
function closestVisible(nearbyElement) {
|
||||
const cm =
|
||||
nearbyElement instanceof CodeMirror ? nearbyElement :
|
||||
nearbyElement instanceof Node &&
|
||||
(nearbyElement.closest('#sections > .section') || {}).CodeMirror ||
|
||||
getLastActivatedEditor();
|
||||
if (nearbyElement instanceof Node && cm) {
|
||||
const {left, top} = nearbyElement.getBoundingClientRect();
|
||||
const bounds = cm.display.wrapper.getBoundingClientRect();
|
||||
if (top >= 0 && top >= bounds.top &&
|
||||
left >= 0 && left >= bounds.left) {
|
||||
return cm;
|
||||
}
|
||||
}
|
||||
// closest editor should have at least 2 lines visible
|
||||
const lineHeight = sections[0].cm.defaultTextHeight();
|
||||
const scrollY = window.scrollY;
|
||||
const windowBottom = scrollY + window.innerHeight - 2 * lineHeight;
|
||||
const allSectionsContainerTop = scrollY + $('#sections').getBoundingClientRect().top;
|
||||
const distances = [];
|
||||
const alreadyInView = cm && offscreenDistance(null, cm) === 0;
|
||||
return alreadyInView ? cm : findClosest();
|
||||
|
||||
function offscreenDistance(index, cm) {
|
||||
if (index >= 0 && distances[index] !== undefined) {
|
||||
return distances[index];
|
||||
}
|
||||
const section = cm.display.wrapper.closest('.section');
|
||||
if (!section) {
|
||||
return 1e9;
|
||||
}
|
||||
const top = allSectionsContainerTop + section.offsetTop;
|
||||
if (top < scrollY + lineHeight) {
|
||||
return Math.max(0, scrollY - top - lineHeight);
|
||||
}
|
||||
if (top < windowBottom) {
|
||||
return 0;
|
||||
}
|
||||
const distance = top - windowBottom + section.offsetHeight;
|
||||
if (index >= 0) {
|
||||
distances[index] = distance;
|
||||
}
|
||||
return distance;
|
||||
}
|
||||
|
||||
function findClosest() {
|
||||
const editors = getEditors();
|
||||
const last = editors.length - 1;
|
||||
let a = 0;
|
||||
let b = last;
|
||||
let c;
|
||||
let distance;
|
||||
while (a < b - 1) {
|
||||
c = (a + b) / 2 | 0;
|
||||
distance = offscreenDistance(c);
|
||||
if (!distance || !c) {
|
||||
break;
|
||||
}
|
||||
const distancePrev = offscreenDistance(c - 1);
|
||||
const distanceNext = c < last ? offscreenDistance(c + 1) : 1e20;
|
||||
if (distancePrev <= distance && distance <= distanceNext) {
|
||||
b = c;
|
||||
} else {
|
||||
a = c;
|
||||
}
|
||||
}
|
||||
while (b && offscreenDistance(b - 1) <= offscreenDistance(b)) {
|
||||
b--;
|
||||
}
|
||||
const cm = editors[b];
|
||||
if (distances[b] > 0) {
|
||||
scrollToEditor(cm);
|
||||
}
|
||||
return cm;
|
||||
}
|
||||
}
|
||||
|
||||
function getEditors() {
|
||||
return sections.filter(s => !s.isRemoved()).map(s => s.cm);
|
||||
}
|
||||
|
||||
function toggleStyle() {
|
||||
const newValue = !style.enabled;
|
||||
dirty.modify('enabled', style.enabled, newValue);
|
||||
style.enabled = newValue;
|
||||
enabledEl.checked = newValue;
|
||||
}
|
||||
|
||||
function nextEditor(cm, cycle = true) {
|
||||
if (!cycle) {
|
||||
for (const section of sections) {
|
||||
if (section.isRemoved()) {
|
||||
continue;
|
||||
}
|
||||
if (cm === section.cm) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return nextPrevEditor(cm, 1);
|
||||
}
|
||||
|
||||
function prevEditor(cm, cycle = true) {
|
||||
if (!cycle) {
|
||||
for (let i = sections.length - 1; i >= 0; i--) {
|
||||
if (sections[i].isRemoved()) {
|
||||
continue;
|
||||
}
|
||||
if (cm === sections[i].cm) {
|
||||
return;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
return nextPrevEditor(cm, -1);
|
||||
}
|
||||
|
||||
function nextPrevEditor(cm, direction) {
|
||||
const editors = getEditors();
|
||||
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
|
||||
scrollToEditor(cm);
|
||||
cm.focus();
|
||||
return cm;
|
||||
}
|
||||
|
||||
function scrollToEditor(cm) {
|
||||
const section = sections.find(s => s.cm === cm).el;
|
||||
const bounds = section.getBoundingClientRect();
|
||||
if (
|
||||
(bounds.bottom > window.innerHeight && bounds.top > 0) ||
|
||||
(bounds.top < 0 && bounds.bottom < window.innerHeight)
|
||||
) {
|
||||
if (bounds.top < 0) {
|
||||
window.scrollBy(0, bounds.top - 1);
|
||||
} else {
|
||||
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getLastActivatedEditor() {
|
||||
let result;
|
||||
for (const section of sections) {
|
||||
if (section.isRemoved()) {
|
||||
continue;
|
||||
}
|
||||
// .lastActive is initiated by codemirror-factory
|
||||
if (!result || section.cm.lastActive > result.lastActive) {
|
||||
result = section.cm;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function scrollEntirePageOnCtrlShift(event) {
|
||||
// make Shift-Ctrl-Wheel scroll entire page even when mouse is over a code editor
|
||||
if (event.shiftKey && event.ctrlKey && !event.altKey && !event.metaKey) {
|
||||
// Chrome scrolls horizontally when Shift is pressed but on some PCs this might be different
|
||||
window.scrollBy(0, event.deltaX || event.deltaY);
|
||||
event.preventDefault();
|
||||
}
|
||||
}
|
||||
|
||||
function showMozillaFormat() {
|
||||
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
|
||||
popup.codebox.setValue(sectionsToMozFormat(getModel()));
|
||||
popup.codebox.execCommand('selectAll');
|
||||
}
|
||||
|
||||
function showMozillaFormatImport(text = '') {
|
||||
const popup = showCodeMirrorPopup(t('styleFromMozillaFormatPrompt'),
|
||||
$create('.buttons', [
|
||||
$create('button', {
|
||||
name: 'import-replace',
|
||||
textContent: t('importReplaceLabel'),
|
||||
title: 'Ctrl-Shift-Enter:\n' + t('importReplaceTooltip'),
|
||||
onclick: () => doImport({replaceOldStyle: true}),
|
||||
}),
|
||||
$create('button', {
|
||||
name: 'import-append',
|
||||
textContent: t('importAppendLabel'),
|
||||
title: 'Ctrl-Enter:\n' + t('importAppendTooltip'),
|
||||
onclick: doImport,
|
||||
}),
|
||||
]));
|
||||
const contents = $('.contents', popup);
|
||||
contents.insertBefore(popup.codebox.display.wrapper, contents.firstElementChild);
|
||||
popup.codebox.focus();
|
||||
popup.codebox.on('changes', cm => {
|
||||
popup.classList.toggle('ready', !cm.isBlank());
|
||||
cm.markClean();
|
||||
});
|
||||
if (text) {
|
||||
popup.codebox.setValue(text);
|
||||
popup.codebox.clearHistory();
|
||||
popup.codebox.markClean();
|
||||
}
|
||||
// overwrite default extraKeys as those are inapplicable in popup context
|
||||
popup.codebox.options.extraKeys = {
|
||||
'Ctrl-Enter': doImport,
|
||||
'Shift-Ctrl-Enter': () => doImport({replaceOldStyle: true}),
|
||||
};
|
||||
|
||||
function doImport({replaceOldStyle = false}) {
|
||||
lockPageUI(true);
|
||||
API.parseCss({code: popup.codebox.getValue().trim()})
|
||||
.then(({sections, errors}) => {
|
||||
// shouldn't happen but just in case
|
||||
if (!sections.length || errors.length) {
|
||||
throw errors;
|
||||
}
|
||||
if (replaceOldStyle) {
|
||||
return replaceSections(sections);
|
||||
}
|
||||
return new Promise(resolve => initSection({sections, done: resolve, focusOn: false}));
|
||||
})
|
||||
.then(() => {
|
||||
$('.dismiss').dispatchEvent(new Event('click'));
|
||||
})
|
||||
.catch(showError)
|
||||
.then(() => lockPageUI(false));
|
||||
}
|
||||
|
||||
function lockPageUI(locked) {
|
||||
document.documentElement.style.pointerEvents = locked ? 'none' : '';
|
||||
if (popup.codebox) {
|
||||
popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
|
||||
popup.codebox.options.readOnly = locked;
|
||||
popup.codebox.display.wrapper.style.opacity = locked ? '.5' : '';
|
||||
}
|
||||
}
|
||||
|
||||
function showError(errors) {
|
||||
messageBox({
|
||||
className: 'center danger',
|
||||
title: t('styleFromMozillaFormatError'),
|
||||
contents: $create('pre', Array.isArray(errors) ? errors.join('\n') : errors),
|
||||
buttons: [t('confirmClose')],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function updateSectionOrder() {
|
||||
const oldOrder = sectionOrder;
|
||||
const validSections = sections.filter(s => !s.isRemoved());
|
||||
sectionOrder = validSections.map(s => s.id).join(',');
|
||||
dirty.modify('sectionOrder', oldOrder, sectionOrder);
|
||||
container.dataset.sectionCount = validSections.length;
|
||||
linter.refreshReport();
|
||||
}
|
||||
|
||||
function getModel() {
|
||||
return Object.assign({}, style, {
|
||||
sections: sections.filter(s => !s.isRemoved()).map(s => s.getModel())
|
||||
});
|
||||
}
|
||||
|
||||
function validate() {
|
||||
if (!nameEl.reportValidity()) {
|
||||
messageBox.alert(t('styleMissingName'));
|
||||
return false;
|
||||
}
|
||||
for (const section of sections) {
|
||||
for (const apply of section.appliesTo) {
|
||||
if (apply.getType() !== 'regexp') {
|
||||
continue;
|
||||
}
|
||||
if (!apply.valueEl.reportValidity()) {
|
||||
messageBox.alert(t('styleBadRegexp'));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function saveStyle() {
|
||||
if (!dirty.isDirty()) {
|
||||
return;
|
||||
}
|
||||
const newStyle = getModel();
|
||||
if (!validate(newStyle)) {
|
||||
return;
|
||||
}
|
||||
API.editSave(newStyle)
|
||||
.then(newStyle => {
|
||||
destroyRemovedSections();
|
||||
sessionStorage.justEditedStyleId = newStyle.id;
|
||||
replaceStyle(newStyle, false);
|
||||
});
|
||||
}
|
||||
|
||||
function destroyRemovedSections() {
|
||||
for (let i = 0; i < sections.length;) {
|
||||
if (!sections[i].isRemoved()) {
|
||||
i++;
|
||||
continue;
|
||||
}
|
||||
sections[i].destroy();
|
||||
sections[i].el.remove();
|
||||
sections.splice(i, 1);
|
||||
}
|
||||
}
|
||||
|
||||
function updateHeader() {
|
||||
nameEl.value = style.name || '';
|
||||
enabledEl.checked = style.enabled !== false;
|
||||
$('#url').href = style.url || '';
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
function updateLivePreview() {
|
||||
debounce(_updateLivePreview, 200);
|
||||
}
|
||||
|
||||
function _updateLivePreview() {
|
||||
livePreview.update(getModel());
|
||||
}
|
||||
|
||||
function updateTitle() {
|
||||
const name = style.name;
|
||||
const clean = !dirty.isDirty();
|
||||
const title = !style.id ? t('addStyleTitle') : name;
|
||||
document.title = (clean ? '' : '* ') + title;
|
||||
$('#save-button').disabled = clean;
|
||||
}
|
||||
|
||||
function initSection({
|
||||
sections: originalSections,
|
||||
total = originalSections.length,
|
||||
focusOn = 0,
|
||||
done
|
||||
}) {
|
||||
container.classList.add('hidden');
|
||||
chunk();
|
||||
|
||||
function chunk() {
|
||||
if (!originalSections.length) {
|
||||
setGlobalProgress();
|
||||
if (focusOn !== false) {
|
||||
sections[focusOn].cm.focus();
|
||||
}
|
||||
container.classList.remove('hidden');
|
||||
for (const section of sections) {
|
||||
section.cm.refreshOnView();
|
||||
}
|
||||
if (done) {
|
||||
done();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const t0 = performance.now();
|
||||
while (originalSections.length && performance.now() - t0 < 100) {
|
||||
insertSectionAfter(originalSections.shift());
|
||||
}
|
||||
setGlobalProgress(total - originalSections.length, total);
|
||||
setTimeout(chunk);
|
||||
}
|
||||
}
|
||||
|
||||
function removeSection(section) {
|
||||
if (sections.every(s => s.isRemoved() || s === section)) {
|
||||
// TODO: hide remove button when `#sections[data-section-count=1]`
|
||||
throw new Error('Cannot remove last section');
|
||||
}
|
||||
if (section.cm.isBlank()) {
|
||||
const index = sections.indexOf(section);
|
||||
sections.splice(index, 1);
|
||||
section.el.remove();
|
||||
section.remove();
|
||||
section.destroy();
|
||||
} else {
|
||||
const lines = [];
|
||||
const MAX_LINES = 10;
|
||||
section.cm.doc.iter(0, MAX_LINES + 1, ({text}) => lines.push(text) && false);
|
||||
const title = t('sectionCode') + '\n' +
|
||||
'-'.repeat(20) + '\n' +
|
||||
lines.slice(0, MAX_LINES).map(s => clipString(s, 100)).join('\n') +
|
||||
(lines.length > MAX_LINES ? '\n...' : '');
|
||||
$('.deleted-section', section.el).title = title;
|
||||
section.remove();
|
||||
}
|
||||
dirty.remove(section, section);
|
||||
updateSectionOrder();
|
||||
section.off(updateLivePreview);
|
||||
updateLivePreview();
|
||||
}
|
||||
|
||||
function restoreSection(section) {
|
||||
section.restore();
|
||||
updateSectionOrder();
|
||||
section.onChange(updateLivePreview);
|
||||
updateLivePreview();
|
||||
}
|
||||
|
||||
function insertSectionAfter(init, base) {
|
||||
if (!init) {
|
||||
init = {code: '', urlPrefixes: ['http://example.com']};
|
||||
}
|
||||
const section = createSection({
|
||||
originalSection: init,
|
||||
genId,
|
||||
dirty,
|
||||
showMozillaFormatImport,
|
||||
removeSection,
|
||||
restoreSection,
|
||||
insertSectionAfter,
|
||||
moveSectionUp,
|
||||
moveSectionDown,
|
||||
prevEditor,
|
||||
nextEditor
|
||||
});
|
||||
if (base) {
|
||||
const index = sections.indexOf(base);
|
||||
sections.splice(index + 1, 0, section);
|
||||
container.insertBefore(section.el, base.el.nextSibling);
|
||||
} else {
|
||||
sections.push(section);
|
||||
container.appendChild(section.el);
|
||||
}
|
||||
section.render();
|
||||
updateSectionOrder();
|
||||
section.onChange(updateLivePreview);
|
||||
updateLivePreview();
|
||||
}
|
||||
|
||||
function moveSectionUp(section) {
|
||||
const index = sections.indexOf(section);
|
||||
if (index === 0) {
|
||||
return;
|
||||
}
|
||||
container.insertBefore(section.el, sections[index - 1].el);
|
||||
sections[index] = sections[index - 1];
|
||||
sections[index - 1] = section;
|
||||
updateSectionOrder();
|
||||
}
|
||||
|
||||
function moveSectionDown(section) {
|
||||
const index = sections.indexOf(section);
|
||||
if (index === sections.length - 1) {
|
||||
return;
|
||||
}
|
||||
container.insertBefore(sections[index + 1].el, section.el);
|
||||
sections[index] = sections[index + 1];
|
||||
sections[index + 1] = section;
|
||||
updateSectionOrder();
|
||||
}
|
||||
|
||||
function replaceSections(originalSections) {
|
||||
for (const section of sections) {
|
||||
section.remove(true);
|
||||
}
|
||||
sections.length = 0;
|
||||
container.textContent = '';
|
||||
return new Promise(resolve => initSection({sections: originalSections, done: resolve}));
|
||||
}
|
||||
|
||||
function replaceStyle(newStyle, codeIsUpdated) {
|
||||
// FIXME: avoid recreating all editors?
|
||||
reinit().then(() => {
|
||||
Object.assign(style, newStyle);
|
||||
updateHeader();
|
||||
dirty.clear();
|
||||
// Go from new style URL to edit style URL
|
||||
if (location.href.indexOf('id=') === -1 && style.id) {
|
||||
history.replaceState({}, document.title, 'edit.html?id=' + style.id);
|
||||
$('#heading').textContent = t('editStyleHeading');
|
||||
}
|
||||
livePreview.show(Boolean(style.id));
|
||||
});
|
||||
|
||||
function reinit() {
|
||||
if (codeIsUpdated !== false) {
|
||||
return replaceSections(newStyle.sections.slice());
|
||||
}
|
||||
return Promise.resolve();
|
||||
}
|
||||
}
|
||||
}
|
582
edit/sections.js
582
edit/sections.js
|
@ -1,582 +0,0 @@
|
|||
/*
|
||||
global CodeMirror
|
||||
global editors propertyToCss CssToProperty
|
||||
global onChange initHooks setCleanGlobal
|
||||
global fromMozillaFormat maximizeCodeHeight toggleContextMenuDelete
|
||||
global setCleanItem updateTitle
|
||||
global showAppliesToHelp beautify regExpTester setGlobalProgress setCleanSection
|
||||
global clipString linter
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
function initWithSectionStyle(style, codeIsUpdated) {
|
||||
$('#name').value = style.name || '';
|
||||
$('#enabled').checked = style.enabled !== false;
|
||||
$('#url').href = style.url || '';
|
||||
if (codeIsUpdated !== false) {
|
||||
editors.length = 0;
|
||||
$('#sections').textContent = '';
|
||||
addSections(style.sections.length ? style.sections : [{code: ''}]);
|
||||
initHooks();
|
||||
}
|
||||
setCleanGlobal();
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
function addSections(sections, onAdded = () => {}) {
|
||||
if (addSections.running) {
|
||||
console.error('addSections cannot be re-entered: please report to the developers');
|
||||
// TODO: handle this properly e.g. on update/import
|
||||
return;
|
||||
}
|
||||
addSections.running = true;
|
||||
maximizeCodeHeight.stats = null;
|
||||
// make a shallow copy since we might run asynchronously
|
||||
// and the original array might get modified
|
||||
sections = sections.slice();
|
||||
const t0 = performance.now();
|
||||
const divs = [];
|
||||
let index = 0;
|
||||
|
||||
return new Promise(function run(resolve) {
|
||||
while (index < sections.length) {
|
||||
const div = addSection(null, sections[index]);
|
||||
maximizeCodeHeight(div, index === sections.length - 1);
|
||||
onAdded(div, index);
|
||||
divs.push(div);
|
||||
maybeFocusFirstCM();
|
||||
index++;
|
||||
const elapsed = performance.now() - t0;
|
||||
if (elapsed > 500) {
|
||||
setGlobalProgress(index, sections.length);
|
||||
}
|
||||
if (elapsed > 100) {
|
||||
// after 100ms the sections are added asynchronously
|
||||
setTimeout(run, 0, resolve);
|
||||
return;
|
||||
}
|
||||
}
|
||||
editors.last.state.renderLintReportNow = true;
|
||||
addSections.running = false;
|
||||
setGlobalProgress();
|
||||
resolve(divs);
|
||||
});
|
||||
|
||||
function maybeFocusFirstCM() {
|
||||
const isPageLocked = document.documentElement.style.pointerEvents;
|
||||
if (divs[0] && (isPageLocked ? divs.length === sections.length : index === 0)) {
|
||||
makeSectionVisible(divs[0].CodeMirror);
|
||||
setTimeout(() => {
|
||||
if ((document.activeElement || {}).localName !== 'input') {
|
||||
divs[0].CodeMirror.focus();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addSection(event, section) {
|
||||
const div = template.section.cloneNode(true);
|
||||
$('.applies-to-help', div).addEventListener('click', showAppliesToHelp);
|
||||
$('.remove-section', div).addEventListener('click', removeSection);
|
||||
$('.add-section', div).addEventListener('click', addSection);
|
||||
$('.clone-section', div).addEventListener('click', cloneSection);
|
||||
$('.move-section-up', div).addEventListener('click', moveSection);
|
||||
$('.move-section-down', div).addEventListener('click', moveSection);
|
||||
$('.beautify-section', div).addEventListener('click', beautify);
|
||||
|
||||
const code = (section || {}).code || '';
|
||||
|
||||
const appliesTo = $('.applies-to-list', div);
|
||||
let appliesToAdded = false;
|
||||
|
||||
if (section) {
|
||||
for (const i in propertyToCss) {
|
||||
if (section[i]) {
|
||||
section[i].forEach(url => {
|
||||
addAppliesTo(appliesTo, propertyToCss[i], url);
|
||||
appliesToAdded = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
if (!appliesToAdded) {
|
||||
addAppliesTo(appliesTo, event && 'url-prefix', '');
|
||||
}
|
||||
|
||||
appliesTo.addEventListener('change', onChange);
|
||||
appliesTo.addEventListener('input', onChange);
|
||||
|
||||
toggleTestRegExpVisibility();
|
||||
appliesTo.addEventListener('change', toggleTestRegExpVisibility);
|
||||
$('.test-regexp', div).onclick = () => {
|
||||
regExpTester.toggle();
|
||||
regExpTester.update(getRegExps());
|
||||
};
|
||||
|
||||
function getRegExps() {
|
||||
return [...appliesTo.children]
|
||||
.map(item =>
|
||||
!item.matches('.applies-to-everything') &&
|
||||
$('.applies-type', item).value === 'regexp' &&
|
||||
$('.applies-value', item).value.trim()
|
||||
)
|
||||
.filter(item => item);
|
||||
}
|
||||
|
||||
function toggleTestRegExpVisibility() {
|
||||
const show = getRegExps().length > 0;
|
||||
div.classList.toggle('has-regexp', show);
|
||||
appliesTo.oninput = appliesTo.oninput || show && (event => {
|
||||
if (event.target.matches('.applies-value') &&
|
||||
$('.applies-type', event.target.closest('.applies-to-item')).value === 'regexp') {
|
||||
regExpTester.update(getRegExps());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const sections = $('#sections');
|
||||
let cm;
|
||||
if (event) {
|
||||
let clickedSection = event && getSectionForChild(event.target, {includeDeleted: true});
|
||||
clickedSection.insertAdjacentElement('afterend', div);
|
||||
while (clickedSection && !clickedSection.matches('.section')) {
|
||||
clickedSection = clickedSection.previousElementSibling;
|
||||
}
|
||||
const newIndex = getSections().indexOf(clickedSection) + 1;
|
||||
cm = setupCodeMirror(div, code, newIndex);
|
||||
makeSectionVisible(cm);
|
||||
cm.focus();
|
||||
} else {
|
||||
sections.appendChild(div);
|
||||
cm = setupCodeMirror(div, code);
|
||||
}
|
||||
linter.enableForEditor(cm);
|
||||
linter.refreshReport();
|
||||
div.CodeMirror = cm;
|
||||
setCleanSection(div);
|
||||
return div;
|
||||
}
|
||||
|
||||
// may be invoked as a DOM listener
|
||||
function addAppliesTo(list, type, value) {
|
||||
let clickedItem;
|
||||
if (this instanceof Node) {
|
||||
clickedItem = this.closest('.applies-to-item');
|
||||
list = this.closest('.applies-to-list');
|
||||
// dummy <a> wrapper was clicked
|
||||
if (arguments[0] instanceof Event) arguments[0].preventDefault();
|
||||
}
|
||||
const showingEverything = $('.applies-to-everything', list);
|
||||
// blow away 'Everything' if it's there
|
||||
if (showingEverything) {
|
||||
list.removeChild(list.firstChild);
|
||||
}
|
||||
let item, toFocus;
|
||||
|
||||
// a section is added with known applies-to
|
||||
if (type) {
|
||||
item = template.appliesTo.cloneNode(true);
|
||||
$('[name=applies-type]', item).value = type;
|
||||
$('[name=applies-value]', item).value = value;
|
||||
$('.remove-applies-to', item).addEventListener('click', removeAppliesTo);
|
||||
|
||||
// a "+" button was clicked
|
||||
} else if (showingEverything || clickedItem) {
|
||||
item = template.appliesTo.cloneNode(true);
|
||||
toFocus = $('[name=applies-type]', item);
|
||||
if (clickedItem) {
|
||||
$('[name=applies-type]', item).value = $('[name="applies-type"]', clickedItem).value;
|
||||
}
|
||||
$('.remove-applies-to', item).addEventListener('click', removeAppliesTo);
|
||||
|
||||
// a global section is added
|
||||
} else {
|
||||
item = template.appliesToEverything.cloneNode(true);
|
||||
}
|
||||
|
||||
$('.add-applies-to', item).addEventListener('click', addAppliesTo);
|
||||
list.insertBefore(item, clickedItem && clickedItem.nextElementSibling);
|
||||
if (toFocus) toFocus.focus();
|
||||
}
|
||||
|
||||
function cloneSection(event) {
|
||||
const section = getSectionForChild(event.target);
|
||||
addSection(event, getSectionsHashes([section]).pop());
|
||||
setCleanItem($('#sections'), false);
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
function moveSection(event) {
|
||||
const section = getSectionForChild(event.target);
|
||||
const dir = event.target.closest('.move-section-up') ? -1 : 1;
|
||||
const cm = section.CodeMirror;
|
||||
const index = editors.indexOf(cm);
|
||||
const newIndex = (index + dir + editors.length) % editors.length;
|
||||
const currentNextEl = section.nextElementSibling;
|
||||
const newSection = editors[newIndex].getSection();
|
||||
newSection.insertAdjacentElement('afterend', section);
|
||||
section.parentNode.insertBefore(newSection, currentNextEl || null);
|
||||
cm.focus();
|
||||
editors[index] = editors[newIndex];
|
||||
editors[newIndex] = cm;
|
||||
setCleanItem($('#sections'), false);
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
function setupCodeMirror(sectionDiv, code, index = editors.length) {
|
||||
const cm = CodeMirror(wrapper => {
|
||||
$('.code-label', sectionDiv).insertAdjacentElement('afterend', wrapper);
|
||||
}, {
|
||||
value: code,
|
||||
});
|
||||
const wrapper = cm.display.wrapper;
|
||||
|
||||
let onChangeTimer;
|
||||
cm.on('changes', (cm, changes) => {
|
||||
clearTimeout(onChangeTimer);
|
||||
onChangeTimer = setTimeout(indicateCodeChange, 200, cm, changes);
|
||||
});
|
||||
wrapper.addEventListener('keydown', event => nextPrevEditorOnKeydown(cm, event), true);
|
||||
cm.on('paste', (cm, event) => {
|
||||
const text = event.clipboardData.getData('text') || '';
|
||||
if (
|
||||
text.includes('@-moz-document') &&
|
||||
text.replace(/\/\*[\s\S]*?(?:\*\/|$)/g, '')
|
||||
.match(/@-moz-document[\s\r\n]+(url|url-prefix|domain|regexp)\(/)
|
||||
) {
|
||||
event.preventDefault();
|
||||
fromMozillaFormat();
|
||||
$('#help-popup').codebox.setValue(text);
|
||||
$('#help-popup').codebox.clearHistory();
|
||||
$('#help-popup').codebox.markClean();
|
||||
}
|
||||
if (editors.length === 1) {
|
||||
setTimeout(() => {
|
||||
if (cm.display.sizer.clientHeight > cm.display.wrapper.clientHeight) {
|
||||
maximizeCodeHeight.stats = null;
|
||||
maximizeCodeHeight(cm.getSection(), true);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
if (!FIREFOX) {
|
||||
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
|
||||
}
|
||||
|
||||
wrapper.classList.add('resize-grip-enabled');
|
||||
let lastClickTime = 0;
|
||||
const resizeGrip = wrapper.appendChild(template.resizeGrip.cloneNode(true));
|
||||
resizeGrip.onmousedown = event => {
|
||||
if (event.button !== 0) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
if (Date.now() - lastClickTime < 500) {
|
||||
lastClickTime = 0;
|
||||
toggleSectionHeight(cm);
|
||||
return;
|
||||
}
|
||||
lastClickTime = Date.now();
|
||||
const minHeight = cm.defaultTextHeight() +
|
||||
/* .CodeMirror-lines padding */
|
||||
cm.display.lineDiv.offsetParent.offsetTop +
|
||||
/* borders */
|
||||
wrapper.offsetHeight - wrapper.clientHeight;
|
||||
wrapper.style.pointerEvents = 'none';
|
||||
document.body.style.cursor = 's-resize';
|
||||
function resize(e) {
|
||||
const cmPageY = wrapper.getBoundingClientRect().top + window.scrollY;
|
||||
const height = Math.max(minHeight, e.pageY - cmPageY);
|
||||
if (height !== wrapper.clientHeight) {
|
||||
cm.setSize(null, height);
|
||||
}
|
||||
}
|
||||
document.addEventListener('mousemove', resize);
|
||||
document.addEventListener('mouseup', function resizeStop() {
|
||||
document.removeEventListener('mouseup', resizeStop);
|
||||
document.removeEventListener('mousemove', resize);
|
||||
wrapper.style.pointerEvents = '';
|
||||
document.body.style.cursor = '';
|
||||
});
|
||||
};
|
||||
|
||||
editors.splice(index, 0, cm);
|
||||
return cm;
|
||||
}
|
||||
|
||||
function indicateCodeChange(cm) {
|
||||
const section = cm.getSection();
|
||||
setCleanItem(section, cm.isClean(section.savedValue));
|
||||
updateTitle();
|
||||
}
|
||||
|
||||
function setupAutocomplete(cm, enable = true) {
|
||||
const onOff = enable ? 'on' : 'off';
|
||||
cm[onOff]('changes', autocompleteOnTyping);
|
||||
cm[onOff]('pick', autocompletePicked);
|
||||
}
|
||||
|
||||
function autocompleteOnTyping(cm, [info], debounced) {
|
||||
if (
|
||||
cm.state.completionActive ||
|
||||
info.origin && !info.origin.includes('input') ||
|
||||
!info.text.last
|
||||
) {
|
||||
return;
|
||||
}
|
||||
if (cm.state.autocompletePicked) {
|
||||
cm.state.autocompletePicked = false;
|
||||
return;
|
||||
}
|
||||
if (!debounced) {
|
||||
debounce(autocompleteOnTyping, 100, cm, [info], true);
|
||||
return;
|
||||
}
|
||||
if (info.text.last.match(/[-a-z!]+$/i)) {
|
||||
cm.state.autocompletePicked = false;
|
||||
cm.options.hintOptions.completeSingle = false;
|
||||
cm.execCommand('autocomplete');
|
||||
setTimeout(() => {
|
||||
cm.options.hintOptions.completeSingle = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function autocompletePicked(cm) {
|
||||
cm.state.autocompletePicked = true;
|
||||
}
|
||||
|
||||
function nextPrevEditorOnKeydown(cm, event) {
|
||||
const key = event.which;
|
||||
if (key < 37 || key > 40 || event.shiftKey || event.altKey || event.metaKey) {
|
||||
return;
|
||||
}
|
||||
const {line, ch} = cm.getCursor();
|
||||
switch (key) {
|
||||
case 37:
|
||||
// arrow Left
|
||||
if (line || ch) {
|
||||
return;
|
||||
}
|
||||
// fallthrough to arrow Up
|
||||
case 38:
|
||||
// arrow Up
|
||||
if (line > 0 || cm === editors[0]) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cm = CodeMirror.commands.prevEditor(cm);
|
||||
cm.setCursor(cm.doc.size - 1, key === 37 ? 1e20 : ch);
|
||||
break;
|
||||
case 39:
|
||||
// arrow Right
|
||||
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
|
||||
return;
|
||||
}
|
||||
// fallthrough to arrow Down
|
||||
case 40:
|
||||
// arrow Down
|
||||
if (line < cm.doc.size - 1 || cm === editors.last) {
|
||||
return;
|
||||
}
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
cm = CodeMirror.commands.nextEditor(cm);
|
||||
cm.setCursor(0, 0);
|
||||
break;
|
||||
}
|
||||
const animation = (cm.getSection().firstElementChild.getAnimations() || [])[0];
|
||||
if (animation) {
|
||||
animation.playbackRate = -1;
|
||||
animation.currentTime = 2000;
|
||||
animation.play();
|
||||
}
|
||||
}
|
||||
|
||||
function toggleSectionHeight(cm) {
|
||||
if (cm.state.toggleHeightSaved) {
|
||||
// restore previous size
|
||||
cm.setSize(null, cm.state.toggleHeightSaved);
|
||||
cm.state.toggleHeightSaved = 0;
|
||||
} else {
|
||||
// maximize
|
||||
const wrapper = cm.display.wrapper;
|
||||
const allBounds = $('#sections').getBoundingClientRect();
|
||||
const pageExtrasHeight = allBounds.top + window.scrollY +
|
||||
parseFloat(getComputedStyle($('#sections')).paddingBottom);
|
||||
const sectionExtrasHeight = cm.getSection().clientHeight - wrapper.offsetHeight;
|
||||
cm.state.toggleHeightSaved = wrapper.clientHeight;
|
||||
cm.setSize(null, window.innerHeight - sectionExtrasHeight - pageExtrasHeight);
|
||||
const bounds = cm.getSection().getBoundingClientRect();
|
||||
if (bounds.top < 0 || bounds.bottom > window.innerHeight) {
|
||||
window.scrollBy(0, bounds.top);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function getSectionForChild(el, {includeDeleted} = {}) {
|
||||
return el.closest(`#sections > ${includeDeleted ? '*' : '.section'}`);
|
||||
}
|
||||
|
||||
function getSections() {
|
||||
return $$('#sections > .section');
|
||||
}
|
||||
|
||||
function getSectionsHashes(elements = getSections()) {
|
||||
const sections = [];
|
||||
for (const div of elements) {
|
||||
const meta = {urls: [], urlPrefixes: [], domains: [], regexps: []};
|
||||
for (const li of $('.applies-to-list', div).childNodes) {
|
||||
if (li.className === template.appliesToEverything.className) {
|
||||
break;
|
||||
}
|
||||
const type = $('[name=applies-type]', li).value;
|
||||
const value = $('[name=applies-value]', li).value;
|
||||
if (type && value) {
|
||||
meta[CssToProperty[type]].push(value);
|
||||
}
|
||||
}
|
||||
const code = div.CodeMirror.getValue();
|
||||
if (/^\s*$/.test(code) && Object.keys(meta).length === 0) {
|
||||
continue;
|
||||
}
|
||||
meta.code = code;
|
||||
sections.push(meta);
|
||||
}
|
||||
return sections;
|
||||
}
|
||||
|
||||
function removeAppliesTo(event) {
|
||||
event.preventDefault();
|
||||
const appliesTo = event.target.closest('.applies-to-item');
|
||||
const appliesToList = appliesTo.closest('.applies-to-list');
|
||||
removeAreaAndSetDirty(appliesTo);
|
||||
if (!appliesToList.hasChildNodes()) {
|
||||
addAppliesTo(appliesToList);
|
||||
}
|
||||
}
|
||||
|
||||
function removeSection(event) {
|
||||
const section = getSectionForChild(event.target);
|
||||
const cm = section.CodeMirror;
|
||||
if (event instanceof Event && (!cm.isClean() || !cm.isBlank())) {
|
||||
const stub = template.deletedSection.cloneNode(true);
|
||||
const MAX_LINES = 10;
|
||||
const lines = [];
|
||||
cm.doc.iter(0, MAX_LINES + 1, ({text}) => lines.push(text) && false);
|
||||
stub.title = t('sectionCode') + '\n' +
|
||||
'-'.repeat(20) + '\n' +
|
||||
lines.slice(0, MAX_LINES).map(s => clipString(s, 100)).join('\n') +
|
||||
(lines.length > MAX_LINES ? '\n...' : '');
|
||||
$('.restore-section', stub).onclick = () => {
|
||||
let el = stub;
|
||||
while (el && !el.matches('.section')) {
|
||||
el = el.previousElementSibling;
|
||||
}
|
||||
const index = el ? editors.indexOf(el) + 1 : 0;
|
||||
editors.splice(index, 0, cm);
|
||||
stub.parentNode.replaceChild(section, stub);
|
||||
setCleanItem(section, false);
|
||||
updateTitle();
|
||||
cm.focus();
|
||||
linter.enableForEditor(cm);
|
||||
linter.refreshReport();
|
||||
};
|
||||
section.insertAdjacentElement('afterend', stub);
|
||||
}
|
||||
setCleanItem($('#sections'), false);
|
||||
removeAreaAndSetDirty(section);
|
||||
editors.splice(editors.indexOf(cm), 1);
|
||||
linter.disableForEditor(cm);
|
||||
linter.refreshReport();
|
||||
}
|
||||
|
||||
function removeAreaAndSetDirty(area) {
|
||||
const contributors = $$('.style-contributor', area);
|
||||
if (!contributors.length) {
|
||||
setCleanItem(area, false);
|
||||
}
|
||||
contributors.some(node => {
|
||||
if (node.savedValue) {
|
||||
// it's a saved section, so make it dirty and stop the enumeration
|
||||
setCleanItem(area, false);
|
||||
return true;
|
||||
} else {
|
||||
// it's an empty section, so undirty the applies-to items,
|
||||
// otherwise orphaned ids would keep the style dirty
|
||||
setCleanItem(node, true);
|
||||
}
|
||||
});
|
||||
updateTitle();
|
||||
area.remove();
|
||||
}
|
||||
|
||||
function makeSectionVisible(cm) {
|
||||
if (editors.length === 1) {
|
||||
return;
|
||||
}
|
||||
const section = cm.getSection();
|
||||
const bounds = section.getBoundingClientRect();
|
||||
if (
|
||||
(bounds.bottom > window.innerHeight && bounds.top > 0) ||
|
||||
(bounds.top < 0 && bounds.bottom < window.innerHeight)
|
||||
) {
|
||||
if (bounds.top < 0) {
|
||||
window.scrollBy(0, bounds.top - 1);
|
||||
} else {
|
||||
window.scrollBy(0, bounds.bottom - window.innerHeight + 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function maximizeCodeHeight(sectionDiv, isLast) {
|
||||
const cm = sectionDiv.CodeMirror;
|
||||
const stats = maximizeCodeHeight.stats = maximizeCodeHeight.stats || {totalHeight: 0, deltas: []};
|
||||
if (!stats.cmActualHeight) {
|
||||
stats.cmActualHeight = getComputedHeight(cm.display.wrapper);
|
||||
}
|
||||
if (!stats.sectionMarginTop) {
|
||||
stats.sectionMarginTop = parseFloat(getComputedStyle(sectionDiv).marginTop);
|
||||
}
|
||||
const sectionTop = sectionDiv.getBoundingClientRect().top - stats.sectionMarginTop;
|
||||
if (!stats.firstSectionTop) {
|
||||
stats.firstSectionTop = sectionTop;
|
||||
}
|
||||
const extrasHeight = getComputedHeight(sectionDiv) - stats.cmActualHeight;
|
||||
const cmMaxHeight = window.innerHeight - extrasHeight - sectionTop - stats.sectionMarginTop;
|
||||
const cmDesiredHeight = cm.display.sizer.clientHeight + 2 * cm.defaultTextHeight();
|
||||
const cmGrantableHeight = Math.max(stats.cmActualHeight, Math.min(cmMaxHeight, cmDesiredHeight));
|
||||
stats.deltas.push(cmGrantableHeight - stats.cmActualHeight);
|
||||
stats.totalHeight += cmGrantableHeight + extrasHeight;
|
||||
if (!isLast) {
|
||||
return;
|
||||
}
|
||||
stats.totalHeight += stats.firstSectionTop;
|
||||
if (stats.totalHeight <= window.innerHeight) {
|
||||
editors.forEach((cm, index) => {
|
||||
cm.setSize(null, stats.deltas[index] + stats.cmActualHeight);
|
||||
});
|
||||
return;
|
||||
}
|
||||
// scale heights to fill the gap between last section and bottom edge of the window
|
||||
const sections = $('#sections');
|
||||
const available = window.innerHeight - sections.getBoundingClientRect().bottom -
|
||||
parseFloat(getComputedStyle(sections).marginBottom);
|
||||
if (available <= 0) {
|
||||
return;
|
||||
}
|
||||
const totalDelta = stats.deltas.reduce((sum, d) => sum + d, 0);
|
||||
const q = available / totalDelta;
|
||||
const baseHeight = stats.cmActualHeight - stats.sectionMarginTop;
|
||||
stats.deltas.forEach((delta, index) => {
|
||||
editors[index].setSize(null, baseHeight + Math.floor(q * delta));
|
||||
});
|
||||
|
||||
function getComputedHeight(el) {
|
||||
const compStyle = getComputedStyle(el);
|
||||
return el.getBoundingClientRect().height +
|
||||
parseFloat(compStyle.marginTop) + parseFloat(compStyle.marginBottom);
|
||||
}
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
/* global CodeMirror showHelp */
|
||||
/* global CodeMirror showHelp onDOMready $ $$ $create template t
|
||||
prefs stringAsRegExp */
|
||||
'use strict';
|
||||
|
||||
onDOMready().then(() => {
|
||||
|
|
|
@ -1,11 +1,9 @@
|
|||
/*
|
||||
global editors styleId: true
|
||||
global CodeMirror dirtyReporter
|
||||
global createAppliesToLineWidget messageBox
|
||||
global sectionsToMozFormat
|
||||
global beforeUnload
|
||||
global createMetaCompiler linter
|
||||
*/
|
||||
/* global dirtyReporter
|
||||
createAppliesToLineWidget messageBox
|
||||
sectionsToMozFormat
|
||||
createMetaCompiler linter createLivePreview cmFactory $ $create API prefs t
|
||||
chromeSync */
|
||||
/* exported createSourceEditor */
|
||||
'use strict';
|
||||
|
||||
function createSourceEditor(style) {
|
||||
|
@ -20,7 +18,6 @@ function createSourceEditor(style) {
|
|||
const dirty = dirtyReporter();
|
||||
dirty.onChange(() => {
|
||||
const isDirty = dirty.isDirty();
|
||||
window.onbeforeunload = isDirty ? beforeUnload : null;
|
||||
document.body.classList.toggle('dirty', isDirty);
|
||||
$('#save-button').disabled = !isDirty;
|
||||
updateTitle();
|
||||
|
@ -29,29 +26,26 @@ function createSourceEditor(style) {
|
|||
// normalize style
|
||||
if (!style.id) setupNewStyle(style);
|
||||
|
||||
const cm = CodeMirror($('.single-editor'), {
|
||||
const cm = cmFactory.create($('.single-editor'), {
|
||||
value: style.sourceCode,
|
||||
});
|
||||
let savedGeneration = cm.changeGeneration();
|
||||
|
||||
editors.push(cm);
|
||||
const livePreview = createLivePreview(preprocess);
|
||||
livePreview.show(Boolean(style.id));
|
||||
|
||||
$('#enabled').onchange = function () {
|
||||
const value = this.checked;
|
||||
dirty.modify('enabled', style.enabled, value);
|
||||
style.enabled = value;
|
||||
updateLivePreview();
|
||||
};
|
||||
|
||||
cm.on('changes', () => {
|
||||
dirty.modify('sourceGeneration', savedGeneration, cm.changeGeneration());
|
||||
updateLivePreview();
|
||||
});
|
||||
|
||||
CodeMirror.commands.prevEditor = cm => nextPrevMozDocument(cm, -1);
|
||||
CodeMirror.commands.nextEditor = cm => nextPrevMozDocument(cm, 1);
|
||||
CodeMirror.commands.toggleStyle = toggleStyle;
|
||||
CodeMirror.commands.save = save;
|
||||
CodeMirror.closestVisible = () => cm;
|
||||
|
||||
cm.operation(initAppliesToLineWidget);
|
||||
|
||||
const metaCompiler = createMetaCompiler(cm);
|
||||
|
@ -86,6 +80,24 @@ function createSourceEditor(style) {
|
|||
});
|
||||
});
|
||||
|
||||
function preprocess(style) {
|
||||
return API.buildUsercss({
|
||||
sourceCode: style.sourceCode,
|
||||
assignVars: true
|
||||
})
|
||||
.then(({style: newStyle}) => {
|
||||
delete newStyle.enabled;
|
||||
return Object.assign(style, newStyle);
|
||||
});
|
||||
}
|
||||
|
||||
function updateLivePreview() {
|
||||
if (!style.id) {
|
||||
return;
|
||||
}
|
||||
livePreview.update(Object.assign({}, style, {sourceCode: cm.getValue()}));
|
||||
}
|
||||
|
||||
function initAppliesToLineWidget() {
|
||||
const PREF_NAME = 'editor.appliesToLineWidget';
|
||||
const widget = createAppliesToLineWidget(cm);
|
||||
|
@ -179,6 +191,7 @@ function createSourceEditor(style) {
|
|||
if (codeIsUpdated === false || sameCode) {
|
||||
updateEnvironment();
|
||||
dirty.clear('enabled');
|
||||
updateLivePreview();
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -191,6 +204,10 @@ function createSourceEditor(style) {
|
|||
cm.setCursor(cursor);
|
||||
savedGeneration = cm.changeGeneration();
|
||||
}
|
||||
if (sameCode) {
|
||||
// the code is same but the environment is changed
|
||||
updateLivePreview();
|
||||
}
|
||||
dirty.clear();
|
||||
});
|
||||
|
||||
|
@ -199,10 +216,10 @@ function createSourceEditor(style) {
|
|||
history.replaceState({}, '', `?id=${newStyle.id}`);
|
||||
}
|
||||
sessionStorage.justEditedStyleId = newStyle.id;
|
||||
style = newStyle;
|
||||
styleId = style.id;
|
||||
Object.assign(style, newStyle);
|
||||
$('#preview-label').classList.remove('hidden');
|
||||
updateMeta();
|
||||
livePreview.show(Boolean(style.id));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -218,19 +235,15 @@ function createSourceEditor(style) {
|
|||
if (!dirty.isDirty()) return;
|
||||
const code = cm.getValue();
|
||||
return ensureUniqueStyle(code)
|
||||
.then(() => API.saveUsercssUnsafe({
|
||||
.then(() => API.editSaveUsercss({
|
||||
id: style.id,
|
||||
reason: 'editSave',
|
||||
enabled: style.enabled,
|
||||
sourceCode: code,
|
||||
}))
|
||||
.then(({style, errors}) => {
|
||||
replaceStyle(style);
|
||||
if (errors) return Promise.reject(errors);
|
||||
})
|
||||
.then(replaceStyle)
|
||||
.catch(err => {
|
||||
if (err.handled) return;
|
||||
if (err.message === t('styleMissingMeta', 'name')) {
|
||||
if (err.code === 'missingMandatory' && err.args.includes('name')) {
|
||||
messageBox.confirm(t('usercssReplaceTemplateConfirmation')).then(ok => ok &&
|
||||
chromeSync.setLZValue('usercssTemplate', code)
|
||||
.then(() => chromeSync.getLZValue('usercssTemplate'))
|
||||
|
@ -239,7 +252,7 @@ function createSourceEditor(style) {
|
|||
}
|
||||
const contents = Array.isArray(err) ?
|
||||
$create('pre', err.join('\n')) :
|
||||
[String(err)];
|
||||
[err.message || String(err)];
|
||||
if (Number.isInteger(err.index)) {
|
||||
const pos = cm.posFromIndex(err.index);
|
||||
contents[0] += ` (line ${pos.line + 1} col ${pos.ch + 1})`;
|
||||
|
@ -373,5 +386,15 @@ function createSourceEditor(style) {
|
|||
replaceStyle,
|
||||
isDirty: dirty.isDirty,
|
||||
getStyle: () => style,
|
||||
getEditors: () => [cm],
|
||||
scrollToEditor: () => {},
|
||||
getStyleId: () => style.id,
|
||||
getEditorTitle: () => '',
|
||||
save,
|
||||
toggleStyle,
|
||||
prevEditor: cm => nextPrevMozDocument(cm, -1),
|
||||
nextEditor: cm => nextPrevMozDocument(cm, 1),
|
||||
closestVisible: () => cm,
|
||||
getSearchableInputs: () => []
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* exported dirtyReporter memoize clipString sectionsToMozFormat */
|
||||
'use strict';
|
||||
|
||||
function dirtyReporter() {
|
||||
|
|
|
@ -9,6 +9,8 @@
|
|||
<link href="global.css" rel="stylesheet">
|
||||
<link href="install-usercss/install-usercss.css" rel="stylesheet">
|
||||
|
||||
<script src="js/promisify.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global CodeMirror semverCompare closeCurrentTab */
|
||||
/* global messageBox download chromeLocal */
|
||||
/* global CodeMirror semverCompare closeCurrentTab messageBox download
|
||||
$ $$ $create $createLink t prefs API getTab */
|
||||
'use strict';
|
||||
|
||||
(() => {
|
||||
|
@ -86,13 +86,11 @@
|
|||
cm.setCursor(cursor);
|
||||
cm.scrollTo(scrollInfo.left, scrollInfo.top);
|
||||
|
||||
API.saveUsercssUnsafe({
|
||||
API.installUsercss({
|
||||
id: (installed || installedDup).id,
|
||||
reason: 'update',
|
||||
sourceCode
|
||||
}).then(({style, errors}) => {
|
||||
}).then(style => {
|
||||
updateMeta(style);
|
||||
if (errors) return Promise.reject(errors);
|
||||
}).catch(showError);
|
||||
});
|
||||
}
|
||||
|
@ -242,7 +240,7 @@
|
|||
const contents = Array.isArray(err) ?
|
||||
[$create('pre', err.join('\n'))] :
|
||||
[err && err.message && $create('pre', err.message) || err || 'Unknown error'];
|
||||
if (Number.isInteger(err.index)) {
|
||||
if (Number.isInteger(err.index) && typeof contents[0] === 'string') {
|
||||
const pos = cm.posFromIndex(err.index);
|
||||
contents[0] = `${pos.line + 1}:${pos.ch + 1} ` + contents[0];
|
||||
contents.push($create('pre', drawLinePointer(pos)));
|
||||
|
@ -301,7 +299,7 @@
|
|||
data.version,
|
||||
]))
|
||||
).then(ok => ok &&
|
||||
API.saveUsercss(Object.assign(style, dup && {reason: 'update'}))
|
||||
API.installUsercss(style)
|
||||
.then(install)
|
||||
.catch(err => messageBox.alert(t('styleInstallFailed', err), 'pre'))
|
||||
);
|
||||
|
|
71
js/cache.js
Normal file
71
js/cache.js
Normal file
|
@ -0,0 +1,71 @@
|
|||
/* exported createCache */
|
||||
'use strict';
|
||||
|
||||
// create a FIFO limit-size map.
|
||||
function createCache({size = 1000, onDeleted} = {}) {
|
||||
const map = new Map();
|
||||
const buffer = Array(size);
|
||||
let index = 0;
|
||||
let lastIndex = 0;
|
||||
return {
|
||||
get,
|
||||
set,
|
||||
delete: delete_,
|
||||
clear,
|
||||
has: id => map.has(id),
|
||||
entries: function *() {
|
||||
for (const [id, item] of map) {
|
||||
yield [id, item.data];
|
||||
}
|
||||
},
|
||||
values: function *() {
|
||||
for (const item of map.values()) {
|
||||
yield item.data;
|
||||
}
|
||||
},
|
||||
get size() {
|
||||
return map.size;
|
||||
}
|
||||
};
|
||||
|
||||
function get(id) {
|
||||
const item = map.get(id);
|
||||
return item && item.data;
|
||||
}
|
||||
|
||||
function set(id, data) {
|
||||
if (map.size === size) {
|
||||
// full
|
||||
map.delete(buffer[lastIndex].id);
|
||||
if (onDeleted) {
|
||||
onDeleted(buffer[lastIndex].id, buffer[lastIndex].data);
|
||||
}
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
}
|
||||
const item = {id, data, index};
|
||||
map.set(id, item);
|
||||
buffer[index] = item;
|
||||
index = (index + 1) % size;
|
||||
}
|
||||
|
||||
function delete_(id) {
|
||||
const item = map.get(id);
|
||||
if (!item) {
|
||||
return false;
|
||||
}
|
||||
map.delete(item.id);
|
||||
const lastItem = buffer[lastIndex];
|
||||
lastItem.index = item.index;
|
||||
buffer[item.index] = lastItem;
|
||||
lastIndex = (lastIndex + 1) % size;
|
||||
if (onDeleted) {
|
||||
onDeleted(item.id, item.data);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function clear() {
|
||||
map.clear();
|
||||
index = lastIndex = 0;
|
||||
}
|
||||
}
|
61
js/dom.js
61
js/dom.js
|
@ -1,9 +1,18 @@
|
|||
/* global prefs */
|
||||
/* exported scrollElementIntoView animateElement enforceInputRange $createLink
|
||||
setupLivePrefs moveFocus */
|
||||
'use strict';
|
||||
|
||||
if (!/^Win\d+/.test(navigator.platform)) {
|
||||
document.documentElement.classList.add('non-windows');
|
||||
}
|
||||
|
||||
// make querySelectorAll enumeration code readable
|
||||
// FIXME: avoid extending native?
|
||||
['forEach', 'some', 'indexOf', 'map'].forEach(method => {
|
||||
NodeList.prototype[method] = Array.prototype[method];
|
||||
});
|
||||
|
||||
// polyfill for old browsers to enable [...results] and for-of
|
||||
for (const type of [NodeList, NamedNodeMap, HTMLCollection, HTMLAllCollection]) {
|
||||
if (!type.prototype[Symbol.iterator]) {
|
||||
|
@ -392,3 +401,55 @@ function moveFocus(rootElement, step) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Accepts an array of pref names (values are fetched via prefs.get)
|
||||
// and establishes a two-way connection between the document elements and the actual prefs
|
||||
function setupLivePrefs(
|
||||
IDs = Object.getOwnPropertyNames(prefs.defaults)
|
||||
.filter(id => $('#' + id))
|
||||
) {
|
||||
for (const id of IDs) {
|
||||
const element = $('#' + id);
|
||||
updateElement({id, element, force: true});
|
||||
element.addEventListener('change', onChange);
|
||||
}
|
||||
prefs.subscribe(IDs, (id, value) => updateElement({id, value}));
|
||||
|
||||
function onChange() {
|
||||
const value = getInputValue(this);
|
||||
if (prefs.get(this.id) !== value) {
|
||||
prefs.set(this.id, value);
|
||||
}
|
||||
}
|
||||
function updateElement({
|
||||
id,
|
||||
value = prefs.get(id),
|
||||
element = $('#' + id),
|
||||
force,
|
||||
}) {
|
||||
if (!element) {
|
||||
prefs.unsubscribe(IDs, updateElement);
|
||||
return;
|
||||
}
|
||||
setInputValue(element, value, force);
|
||||
}
|
||||
function getInputValue(input) {
|
||||
if (input.type === 'checkbox') {
|
||||
return input.checked;
|
||||
}
|
||||
if (input.type === 'number') {
|
||||
return Number(input.value);
|
||||
}
|
||||
return input.value;
|
||||
}
|
||||
function setInputValue(input, value, force = false) {
|
||||
if (force || getInputValue(input) !== value) {
|
||||
if (input.type === 'checkbox') {
|
||||
input.checked = value;
|
||||
} else {
|
||||
input.value = value;
|
||||
}
|
||||
input.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/* global tryCatch */
|
||||
/* exported tHTML formatDate */
|
||||
'use strict';
|
||||
|
||||
const template = {};
|
||||
|
|
228
js/messaging.js
228
js/messaging.js
|
@ -1,17 +1,13 @@
|
|||
/*
|
||||
global BG: true
|
||||
global FIREFOX: true
|
||||
global onRuntimeMessage applyOnMessage
|
||||
*/
|
||||
/* exported getActiveTab onTabReady stringAsRegExp getTabRealURL openURL
|
||||
getStyleWithNoCode tryRegExp sessionStorageHash download
|
||||
closeCurrentTab */
|
||||
'use strict';
|
||||
|
||||
// keep message channel open for sendResponse in chrome.runtime.onMessage listener
|
||||
const KEEP_CHANNEL_OPEN = true;
|
||||
|
||||
const CHROME = Boolean(chrome.app) && parseInt(navigator.userAgent.match(/Chrom\w+\/(?:\d+\.){2}(\d+)|$/)[1]);
|
||||
const OPERA = Boolean(chrome.app) && parseFloat(navigator.userAgent.match(/\bOPR\/(\d+\.\d+)|$/)[1]);
|
||||
const VIVALDI = Boolean(chrome.app) && navigator.userAgent.includes('Vivaldi');
|
||||
const ANDROID = !chrome.windows;
|
||||
// FIXME: who use this?
|
||||
// const ANDROID = !chrome.windows;
|
||||
let FIREFOX = !chrome.app && parseFloat(navigator.userAgent.match(/\bFirefox\/(\d+\.\d+)|$/)[1]);
|
||||
|
||||
if (!CHROME && !chrome.browserAction.openPopup) {
|
||||
|
@ -72,14 +68,9 @@ const URLS = {
|
|||
),
|
||||
};
|
||||
|
||||
let BG = chrome.extension.getBackgroundPage();
|
||||
if (BG && !BG.getStyles && BG !== window) {
|
||||
// own page like editor/manage is being loaded on browser startup
|
||||
// before the background page has been fully initialized;
|
||||
// it'll be resolved in onBackgroundReady() instead
|
||||
BG = null;
|
||||
}
|
||||
if (!BG || BG !== window) {
|
||||
const IS_BG = chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window;
|
||||
|
||||
if (!IS_BG) {
|
||||
if (FIREFOX) {
|
||||
document.documentElement.classList.add('firefox');
|
||||
} else if (OPERA) {
|
||||
|
@ -87,169 +78,15 @@ if (!BG || BG !== window) {
|
|||
} else {
|
||||
if (VIVALDI) document.documentElement.classList.add('vivaldi');
|
||||
}
|
||||
// TODO: remove once our manifest's minimum_chrome_version is 50+
|
||||
// Chrome 49 doesn't report own extension pages in webNavigation apparently
|
||||
if (CHROME && CHROME < 2661) {
|
||||
getActiveTab().then(tab =>
|
||||
window.API.updateIcon({tab}));
|
||||
}
|
||||
} else if (!BG.API_METHODS) {
|
||||
BG.API_METHODS = {};
|
||||
}
|
||||
|
||||
const FIREFOX_NO_DOM_STORAGE = FIREFOX && !tryCatch(() => localStorage);
|
||||
if (FIREFOX_NO_DOM_STORAGE) {
|
||||
// may be disabled via dom.storage.enabled
|
||||
Object.defineProperty(window, 'localStorage', {value: {}});
|
||||
Object.defineProperty(window, 'sessionStorage', {value: {}});
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var API = (() => {
|
||||
return new Proxy(() => {}, {
|
||||
get: (target, name) =>
|
||||
name === 'remoteCall' ?
|
||||
remoteCall :
|
||||
arg => invokeBG(name, arg),
|
||||
});
|
||||
|
||||
function remoteCall(name, arg, remoteWindow) {
|
||||
let thing = window[name] || window.API_METHODS[name];
|
||||
if (typeof thing === 'function') {
|
||||
thing = thing(arg);
|
||||
}
|
||||
if (!thing || typeof thing !== 'object') {
|
||||
return thing;
|
||||
} else if (thing instanceof Promise) {
|
||||
return thing.then(product => remoteWindow.deepCopy(product));
|
||||
} else {
|
||||
return remoteWindow.deepCopy(thing);
|
||||
}
|
||||
}
|
||||
|
||||
function invokeBG(name, arg = {}) {
|
||||
if (BG && (name in BG || name in BG.API_METHODS)) {
|
||||
const call = BG !== window ?
|
||||
BG.API.remoteCall(name, BG.deepCopy(arg), window) :
|
||||
remoteCall(name, arg, BG);
|
||||
return Promise.resolve(call);
|
||||
}
|
||||
if (BG && BG.getStyles) {
|
||||
throw new Error('Bad API method', name, arg);
|
||||
}
|
||||
if (FIREFOX) {
|
||||
arg.method = name;
|
||||
return sendMessage(arg);
|
||||
}
|
||||
return onBackgroundReady().then(() => invokeBG(name, arg));
|
||||
}
|
||||
|
||||
function onBackgroundReady() {
|
||||
return BG && BG.getStyles ? Promise.resolve() : new Promise(function ping(resolve) {
|
||||
sendMessage({method: 'healthCheck'}, health => {
|
||||
if (health !== undefined) {
|
||||
BG = chrome.extension.getBackgroundPage();
|
||||
resolve();
|
||||
} else {
|
||||
setTimeout(ping, 0, resolve);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
|
||||
function notifyAllTabs(msg) {
|
||||
const originalMessage = msg;
|
||||
const styleUpdated = msg.method === 'styleUpdated';
|
||||
if (styleUpdated || msg.method === 'styleAdded') {
|
||||
// apply/popup/manage use only meta for these two methods,
|
||||
// editor may need the full code but can fetch it directly,
|
||||
// so we send just the meta to avoid spamming lots of tabs with huge styles
|
||||
msg = Object.assign({}, msg, {
|
||||
style: getStyleWithNoCode(msg.style)
|
||||
});
|
||||
}
|
||||
const affectsAll = !msg.affects || msg.affects.all;
|
||||
const affectsOwnOriginOnly = !affectsAll && (msg.affects.editor || msg.affects.manager);
|
||||
const affectsTabs = affectsAll || affectsOwnOriginOnly;
|
||||
const affectsIcon = affectsAll || msg.affects.icon;
|
||||
const affectsPopup = affectsAll || msg.affects.popup;
|
||||
const affectsSelf = affectsPopup || msg.prefs;
|
||||
// notify all open extension pages and popups
|
||||
if (affectsSelf) {
|
||||
msg.tabId = undefined;
|
||||
sendMessage(msg, ignoreChromeError);
|
||||
}
|
||||
// notify tabs
|
||||
if (affectsTabs || affectsIcon) {
|
||||
const notifyTab = tab => {
|
||||
if (!styleUpdated
|
||||
&& (affectsTabs || URLS.optionsUI.includes(tab.url))
|
||||
// own pages are already notified via sendMessage
|
||||
&& !(affectsSelf && tab.url.startsWith(URLS.ownOrigin))
|
||||
// skip lazy-loaded aka unloaded tabs that seem to start loading on message in FF
|
||||
&& (!FIREFOX || tab.width)) {
|
||||
msg.tabId = tab.id;
|
||||
sendMessage(msg, ignoreChromeError);
|
||||
}
|
||||
if (affectsIcon) {
|
||||
// eslint-disable-next-line no-use-before-define
|
||||
debounce(API.updateIcon, 0, {tab});
|
||||
}
|
||||
};
|
||||
// list all tabs including chrome-extension:// which can be ours
|
||||
Promise.all([
|
||||
queryTabs(affectsOwnOriginOnly ? {url: URLS.ownOrigin + '*'} : {}),
|
||||
getActiveTab(),
|
||||
]).then(([tabs, activeTab]) => {
|
||||
const activeTabId = activeTab && activeTab.id;
|
||||
for (const tab of tabs) {
|
||||
invokeOrPostpone(tab.id === activeTabId, notifyTab, tab);
|
||||
}
|
||||
});
|
||||
}
|
||||
// notify self: the message no longer is sent to the origin in new Chrome
|
||||
if (typeof onRuntimeMessage !== 'undefined') {
|
||||
onRuntimeMessage(originalMessage);
|
||||
}
|
||||
// notify apply.js on own pages
|
||||
if (typeof applyOnMessage !== 'undefined') {
|
||||
applyOnMessage(originalMessage);
|
||||
}
|
||||
// propagate saved style state/code efficiently
|
||||
if (styleUpdated) {
|
||||
msg.refreshOwnTabs = false;
|
||||
API.refreshAllTabs(msg);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function sendMessage(msg, callback) {
|
||||
/*
|
||||
Promise mode [default]:
|
||||
- rejects on receiving {__ERROR__: message} created by background.js::onRuntimeMessage
|
||||
- automatically suppresses chrome.runtime.lastError because it's autogenerated
|
||||
by browserAction.setText which lacks a callback param in chrome API
|
||||
Standard callback mode:
|
||||
- enabled by passing a second param
|
||||
*/
|
||||
const {tabId, frameId} = msg;
|
||||
const fn = tabId >= 0 ? chrome.tabs.sendMessage : chrome.runtime.sendMessage;
|
||||
const args = tabId >= 0 ? [tabId, msg, {frameId}] : [msg];
|
||||
if (callback) {
|
||||
fn(...args, callback);
|
||||
} else {
|
||||
return new Promise((resolve, reject) => {
|
||||
fn(...args, r => {
|
||||
const err = r && r.__ERROR__;
|
||||
(err ? reject : resolve)(err || r);
|
||||
ignoreChromeError();
|
||||
});
|
||||
});
|
||||
}
|
||||
if (IS_BG) {
|
||||
window.API_METHODS = {};
|
||||
}
|
||||
|
||||
// FIXME: `localStorage` and `sessionStorage` may be disabled via dom.storage.enabled
|
||||
// Object.defineProperty(window, 'localStorage', {value: {}});
|
||||
// Object.defineProperty(window, 'sessionStorage', {value: {}});
|
||||
|
||||
function queryTabs(options = {}) {
|
||||
return new Promise(resolve =>
|
||||
|
@ -276,13 +113,6 @@ function getActiveTab() {
|
|||
.then(tabs => tabs[0]);
|
||||
}
|
||||
|
||||
|
||||
function getActiveTabRealURL() {
|
||||
return getActiveTab()
|
||||
.then(getTabRealURL);
|
||||
}
|
||||
|
||||
|
||||
function getTabRealURL(tab) {
|
||||
return new Promise(resolve => {
|
||||
if (tab.url !== 'chrome://newtab/' || URLS.chromeProtectsNTP) {
|
||||
|
@ -385,7 +215,6 @@ function openURL({
|
|||
index,
|
||||
active,
|
||||
currentWindow = true,
|
||||
message,
|
||||
}) {
|
||||
url = url.includes('://') ? url : chrome.runtime.getURL(url);
|
||||
// [some] chromium forks don't handle their fake branded protocols
|
||||
|
@ -401,15 +230,7 @@ function openURL({
|
|||
url.replace(/%2F.*/, '*').replace(/#.*/, '') :
|
||||
url.replace(/#.*/, '');
|
||||
|
||||
const task = queryTabs({url: urlQuery, currentWindow}).then(maybeSwitch);
|
||||
if (!message) {
|
||||
return task;
|
||||
} else {
|
||||
return task.then(onTabReady).then(tab => {
|
||||
message.tabId = tab.id;
|
||||
return sendMessage(message).then(() => tab);
|
||||
});
|
||||
}
|
||||
return queryTabs({url: urlQuery, currentWindow}).then(maybeSwitch);
|
||||
|
||||
function maybeSwitch(tabs = []) {
|
||||
const urlWithSlash = url + '/';
|
||||
|
@ -652,27 +473,6 @@ function download(url, {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function invokeOrPostpone(isInvoke, fn, ...args) {
|
||||
return isInvoke
|
||||
? fn(...args)
|
||||
: setTimeout(invokeOrPostpone, 0, true, fn, ...args);
|
||||
}
|
||||
|
||||
|
||||
function openEditor({id}) {
|
||||
let url = '/edit.html';
|
||||
if (id) {
|
||||
url += `?id=${id}`;
|
||||
}
|
||||
if (chrome.windows && prefs.get('openEditInWindow')) {
|
||||
chrome.windows.create(Object.assign({url}, prefs.get('windowPosition')));
|
||||
} else {
|
||||
openURL({url});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function closeCurrentTab() {
|
||||
// https://bugzilla.mozilla.org/show_bug.cgi?id=1409375
|
||||
getOwnTab().then(tab => {
|
||||
|
|
78
js/meta-parser.js
Normal file
78
js/meta-parser.js
Normal file
|
@ -0,0 +1,78 @@
|
|||
/* global usercssMeta colorConverter */
|
||||
/* exported metaParser */
|
||||
'use strict';
|
||||
|
||||
const metaParser = (() => {
|
||||
const {createParser, ParseError} = usercssMeta;
|
||||
const PREPROCESSORS = new Set(['default', 'uso', 'stylus', 'less']);
|
||||
const options = {
|
||||
validateKey: {
|
||||
preprocessor: state => {
|
||||
if (!PREPROCESSORS.has(state.value)) {
|
||||
throw new ParseError({
|
||||
code: 'unknownPreprocessor',
|
||||
args: [state.value],
|
||||
index: state.valueIndex
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
validateVar: {
|
||||
select: state => {
|
||||
if (state.varResult.options.every(o => o.name !== state.value)) {
|
||||
throw new ParseError({
|
||||
code: 'invalidSelectValueMismatch',
|
||||
index: state.valueIndex
|
||||
});
|
||||
}
|
||||
},
|
||||
color: state => {
|
||||
const color = colorConverter.parse(state.value);
|
||||
if (!color) {
|
||||
throw new ParseError({
|
||||
code: 'invalidColor',
|
||||
args: [state.value],
|
||||
index: state.valueIndex
|
||||
});
|
||||
}
|
||||
state.value = colorConverter.format(color, 'rgb');
|
||||
}
|
||||
}
|
||||
};
|
||||
const parser = createParser(options);
|
||||
const looseParser = createParser(Object.assign({}, options, {allowErrors: true, unknownKey: 'throw'}));
|
||||
return {
|
||||
parse,
|
||||
lint,
|
||||
nullifyInvalidVars
|
||||
};
|
||||
|
||||
function parse(text, indexOffset) {
|
||||
try {
|
||||
return parser.parse(text);
|
||||
} catch (err) {
|
||||
if (typeof err.index === 'number') {
|
||||
err.index += indexOffset;
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
function lint(text) {
|
||||
return looseParser.parse(text);
|
||||
}
|
||||
|
||||
function nullifyInvalidVars(vars) {
|
||||
for (const va of Object.values(vars)) {
|
||||
if (va.value === null) {
|
||||
continue;
|
||||
}
|
||||
try {
|
||||
parser.validateVar(va);
|
||||
} catch (err) {
|
||||
va.value = null;
|
||||
}
|
||||
}
|
||||
return vars;
|
||||
}
|
||||
})();
|
|
@ -1,4 +1,5 @@
|
|||
/* global parserlib */
|
||||
/* exported parseMozFormat */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
|
329
js/msg.js
Normal file
329
js/msg.js
Normal file
|
@ -0,0 +1,329 @@
|
|||
/* global promisify deepCopy */
|
||||
/* exported msg API */
|
||||
// deepCopy is only used if the script is executed in extension pages.
|
||||
'use strict';
|
||||
|
||||
const msg = (() => {
|
||||
let isBg = false;
|
||||
if (chrome.extension.getBackgroundPage && chrome.extension.getBackgroundPage() === window) {
|
||||
isBg = true;
|
||||
window._msg = {
|
||||
id: 1,
|
||||
storage: new Map(),
|
||||
handler: null,
|
||||
clone: deepCopy
|
||||
};
|
||||
}
|
||||
const runtimeSend = promisify(chrome.runtime.sendMessage.bind(chrome.runtime));
|
||||
const tabSend = chrome.tabs && promisify(chrome.tabs.sendMessage.bind(chrome.tabs));
|
||||
const tabQuery = chrome.tabs && promisify(chrome.tabs.query.bind(chrome.tabs));
|
||||
let bg;
|
||||
const preparing = !isBg && chrome.runtime.getBackgroundPage &&
|
||||
promisify(chrome.runtime.getBackgroundPage.bind(chrome.runtime))()
|
||||
.catch(() => null)
|
||||
.then(_bg => {
|
||||
bg = _bg;
|
||||
});
|
||||
bg = isBg ? window : !preparing ? null : undefined;
|
||||
const EXTENSION_URL = chrome.runtime.getURL('');
|
||||
let handler;
|
||||
const from_ = location.href.startsWith(EXTENSION_URL) ? 'extension' : 'content';
|
||||
const RX_NO_RECEIVER = /Receiving end does not exist/;
|
||||
const RX_PORT_CLOSED = /The message port closed before a response was received/;
|
||||
return {
|
||||
send,
|
||||
sendTab,
|
||||
sendBg,
|
||||
broadcast,
|
||||
broadcastTab,
|
||||
broadcastExtension,
|
||||
ignoreError,
|
||||
on,
|
||||
onTab,
|
||||
onExtension,
|
||||
off,
|
||||
RX_NO_RECEIVER,
|
||||
RX_PORT_CLOSED
|
||||
};
|
||||
|
||||
function send(data, target = 'extension') {
|
||||
if (bg === undefined) {
|
||||
return preparing.then(() => send(data, target));
|
||||
}
|
||||
const message = {type: 'direct', data, target, from: from_};
|
||||
if (bg) {
|
||||
exchangeSet(message);
|
||||
}
|
||||
const request = runtimeSend(message).then(unwrapData);
|
||||
if (message.id) {
|
||||
return withCleanup(request, () => bg._msg.storage.delete(message.id));
|
||||
}
|
||||
return request;
|
||||
}
|
||||
|
||||
function sendTab(tabId, data, options, target = 'tab') {
|
||||
return tabSend(tabId, {type: 'direct', data, target, from: from_}, options)
|
||||
.then(unwrapData);
|
||||
}
|
||||
|
||||
function sendBg(data) {
|
||||
if (bg === undefined) {
|
||||
return preparing.then(doSend);
|
||||
}
|
||||
return withPromiseError(doSend);
|
||||
|
||||
function doSend() {
|
||||
if (bg) {
|
||||
if (!bg._msg.handler) {
|
||||
throw new Error('there is no bg handler');
|
||||
}
|
||||
const handlers = bg._msg.handler.extension.concat(bg._msg.handler.both);
|
||||
// in FF, the object would become a dead object when the window
|
||||
// is closed, so we have to clone the object into background.
|
||||
return Promise.resolve(executeCallbacks(handlers, bg._msg.clone(data), {url: location.href}))
|
||||
.then(deepCopy);
|
||||
}
|
||||
return send(data);
|
||||
}
|
||||
}
|
||||
|
||||
function ignoreError(err) {
|
||||
if (err.message && (
|
||||
RX_NO_RECEIVER.test(err.message) ||
|
||||
RX_PORT_CLOSED.test(err.message)
|
||||
)) {
|
||||
return;
|
||||
}
|
||||
console.warn(err);
|
||||
}
|
||||
|
||||
function broadcast(data, filter) {
|
||||
return Promise.all([
|
||||
send(data, 'both').catch(ignoreError),
|
||||
broadcastTab(data, filter, null, true, 'both')
|
||||
]);
|
||||
}
|
||||
|
||||
function broadcastTab(data, filter, options, ignoreExtension = false, target = 'tab') {
|
||||
return tabQuery({})
|
||||
// TODO: send to activated tabs first?
|
||||
.then(tabs => {
|
||||
const requests = [];
|
||||
for (const tab of tabs) {
|
||||
const isExtension = tab.url.startsWith(EXTENSION_URL);
|
||||
if (
|
||||
tab.discarded ||
|
||||
// FIXME: use `URLS.supported`?
|
||||
!/^(http|ftp|file)/.test(tab.url) &&
|
||||
!tab.url.startsWith('chrome://newtab/') &&
|
||||
!isExtension ||
|
||||
isExtension && ignoreExtension ||
|
||||
filter && !filter(tab)
|
||||
) {
|
||||
continue;
|
||||
}
|
||||
const dataObj = typeof data === 'function' ? data(tab) : data;
|
||||
if (!dataObj) {
|
||||
continue;
|
||||
}
|
||||
const message = {type: 'direct', data: dataObj, target, from: from_};
|
||||
if (isExtension) {
|
||||
exchangeSet(message);
|
||||
}
|
||||
let request = tabSend(tab.id, message, options).then(unwrapData);
|
||||
if (message.id) {
|
||||
request = withCleanup(request, () => bg._msg.storage.delete(message.id));
|
||||
}
|
||||
requests.push(request.catch(ignoreError));
|
||||
}
|
||||
return Promise.all(requests);
|
||||
});
|
||||
}
|
||||
|
||||
function broadcastExtension(...args) {
|
||||
return send(...args).catch(ignoreError);
|
||||
}
|
||||
|
||||
function on(fn) {
|
||||
initHandler();
|
||||
handler.both.push(fn);
|
||||
}
|
||||
|
||||
function onTab(fn) {
|
||||
initHandler();
|
||||
handler.tab.push(fn);
|
||||
}
|
||||
|
||||
function onExtension(fn) {
|
||||
initHandler();
|
||||
handler.extension.push(fn);
|
||||
}
|
||||
|
||||
function off(fn) {
|
||||
for (const type of ['both', 'tab', 'extension']) {
|
||||
const index = handler[type].indexOf(fn);
|
||||
if (index >= 0) {
|
||||
handler[type].splice(index, 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function initHandler() {
|
||||
if (handler) {
|
||||
return;
|
||||
}
|
||||
handler = {
|
||||
both: [],
|
||||
tab: [],
|
||||
extension: []
|
||||
};
|
||||
if (isBg) {
|
||||
bg._msg.handler = handler;
|
||||
}
|
||||
chrome.runtime.onMessage.addListener(handleMessage);
|
||||
}
|
||||
|
||||
function executeCallbacks(callbacks, ...args) {
|
||||
let result;
|
||||
for (const fn of callbacks) {
|
||||
const data = withPromiseError(fn, ...args);
|
||||
if (data !== undefined && result === undefined) {
|
||||
result = data;
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function handleMessage(message, sender, sendResponse) {
|
||||
const handlers = message.target === 'tab' ?
|
||||
handler.tab.concat(handler.both) : message.target === 'extension' ?
|
||||
handler.extension.concat(handler.both) :
|
||||
handler.both.concat(handler.extension, handler.tab);
|
||||
if (!handlers.length) {
|
||||
return;
|
||||
}
|
||||
if (message.type === 'exchange') {
|
||||
const pending = exchangeGet(message, true);
|
||||
if (pending) {
|
||||
pending.then(response);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return response();
|
||||
|
||||
function response() {
|
||||
const result = executeCallbacks(handlers, message.data, sender);
|
||||
if (result === undefined) {
|
||||
return;
|
||||
}
|
||||
Promise.resolve(result)
|
||||
.then(
|
||||
data => ({
|
||||
error: false,
|
||||
data
|
||||
}),
|
||||
err => ({
|
||||
error: true,
|
||||
data: Object.assign({
|
||||
message: err.message || String(err),
|
||||
// FIXME: do we want to pass the entire stack?
|
||||
stack: err.stack
|
||||
}, err) // this allows us to pass custom properties e.g. `err.index`
|
||||
})
|
||||
)
|
||||
.then(function doResponse(responseMessage) {
|
||||
if (message.from === 'extension' && bg === undefined) {
|
||||
return preparing.then(() => doResponse(responseMessage));
|
||||
}
|
||||
if (message.from === 'extension' && bg) {
|
||||
exchangeSet(responseMessage);
|
||||
} else {
|
||||
responseMessage.type = 'direct';
|
||||
}
|
||||
return responseMessage;
|
||||
})
|
||||
.then(sendResponse);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function exchangeGet(message, keepStorage = false) {
|
||||
if (bg === undefined) {
|
||||
return preparing.then(() => exchangeGet(message, keepStorage));
|
||||
}
|
||||
message.data = bg._msg.storage.get(message.id);
|
||||
if (keepStorage) {
|
||||
message.data = deepCopy(message.data);
|
||||
} else {
|
||||
bg._msg.storage.delete(message.id);
|
||||
}
|
||||
}
|
||||
|
||||
function exchangeSet(message) {
|
||||
const id = bg._msg.id;
|
||||
bg._msg.storage.set(id, message.data);
|
||||
bg._msg.id++;
|
||||
message.type = 'exchange';
|
||||
message.id = id;
|
||||
delete message.data;
|
||||
}
|
||||
|
||||
function withPromiseError(fn, ...args) {
|
||||
try {
|
||||
return fn(...args);
|
||||
} catch (err) {
|
||||
return Promise.reject(err);
|
||||
}
|
||||
}
|
||||
|
||||
function withCleanup(p, fn) {
|
||||
return p.then(
|
||||
result => {
|
||||
cleanup();
|
||||
return result;
|
||||
},
|
||||
err => {
|
||||
cleanup();
|
||||
throw err;
|
||||
}
|
||||
);
|
||||
|
||||
function cleanup() {
|
||||
try {
|
||||
fn();
|
||||
} catch (err) {
|
||||
// pass
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// {type, error, data, id}
|
||||
function unwrapData(result) {
|
||||
if (result === undefined) {
|
||||
throw new Error('Receiving end does not exist');
|
||||
}
|
||||
if (result.type === 'exchange') {
|
||||
const pending = exchangeGet(result);
|
||||
if (pending) {
|
||||
return pending.then(unwrap);
|
||||
}
|
||||
}
|
||||
return unwrap();
|
||||
|
||||
function unwrap() {
|
||||
if (result.error) {
|
||||
throw Object.assign(new Error(result.data.message), result.data);
|
||||
}
|
||||
return result.data;
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
||||
const API = new Proxy({}, {
|
||||
get: (target, name) =>
|
||||
(...args) => Promise.resolve(msg.sendBg({
|
||||
method: 'invokeAPI',
|
||||
name,
|
||||
args
|
||||
}))
|
||||
});
|
300
js/prefs.js
300
js/prefs.js
|
@ -1,8 +1,8 @@
|
|||
/* global prefs: true, contextMenus, FIREFOX_NO_DOM_STORAGE */
|
||||
/* global promisify */
|
||||
/* exported prefs */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var prefs = new function Prefs() {
|
||||
const prefs = (() => {
|
||||
const defaults = {
|
||||
'openEditInWindow': false, // new editor opens in a own browser window
|
||||
'windowPosition': {}, // detached window position
|
||||
|
@ -98,29 +98,33 @@ var prefs = new function Prefs() {
|
|||
};
|
||||
const values = deepCopy(defaults);
|
||||
|
||||
const affectsIcon = [
|
||||
'show-badge',
|
||||
'disableAll',
|
||||
'badgeDisabled',
|
||||
'badgeNormal',
|
||||
'iconset',
|
||||
];
|
||||
|
||||
const onChange = {
|
||||
any: new Set(),
|
||||
specific: new Map(),
|
||||
};
|
||||
|
||||
// coalesce multiple pref changes in broadcast
|
||||
let broadcastPrefs = {};
|
||||
|
||||
Object.defineProperties(this, {
|
||||
defaults: {value: deepCopy(defaults)},
|
||||
readOnlyValues: {value: {}},
|
||||
const initializing = promisify(chrome.storage.sync.get.bind(chrome.storage.sync))('settings')
|
||||
.then(result => {
|
||||
if (result.settings) {
|
||||
setAll(result.settings, true);
|
||||
}
|
||||
});
|
||||
|
||||
Object.assign(Prefs.prototype, {
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area !== 'sync' || !changes.settings || !changes.settings.newValue) {
|
||||
return;
|
||||
}
|
||||
initializing.then(() => setAll(changes.settings.newValue, true));
|
||||
});
|
||||
|
||||
let timer;
|
||||
|
||||
// coalesce multiple pref changes in broadcast
|
||||
// let changes = {};
|
||||
|
||||
return {
|
||||
initializing,
|
||||
defaults,
|
||||
get(key, defaultValue) {
|
||||
if (key in values) {
|
||||
return values[key];
|
||||
|
@ -133,62 +137,11 @@ var prefs = new function Prefs() {
|
|||
}
|
||||
console.warn("No default preference for '%s'", key);
|
||||
},
|
||||
|
||||
getAll() {
|
||||
return deepCopy(values);
|
||||
},
|
||||
|
||||
set(key, value, {broadcast = true, sync = true, fromBroadcast} = {}) {
|
||||
const oldValue = values[key];
|
||||
switch (typeof defaults[key]) {
|
||||
case typeof value:
|
||||
break;
|
||||
case 'string':
|
||||
value = String(value);
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'boolean':
|
||||
value = value === true || value === 'true';
|
||||
break;
|
||||
}
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
const hasChanged = !equal(value, oldValue);
|
||||
if (!fromBroadcast || FIREFOX_NO_DOM_STORAGE) {
|
||||
localStorage[key] = typeof defaults[key] === 'object'
|
||||
? JSON.stringify(value)
|
||||
: value;
|
||||
}
|
||||
if (!fromBroadcast && broadcast && hasChanged) {
|
||||
this.broadcast(key, value, {sync});
|
||||
}
|
||||
if (hasChanged) {
|
||||
const specific = onChange.specific.get(key);
|
||||
if (typeof specific === 'function') {
|
||||
specific(key, value);
|
||||
} else if (specific instanceof Set) {
|
||||
for (const listener of specific.values()) {
|
||||
listener(key, value);
|
||||
}
|
||||
}
|
||||
for (const listener of onChange.any.values()) {
|
||||
listener(key, value);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
reset: key => this.set(key, deepCopy(defaults[key])),
|
||||
|
||||
broadcast(key, value, {sync = true} = {}) {
|
||||
broadcastPrefs[key] = value;
|
||||
debounce(doBroadcast);
|
||||
if (sync) {
|
||||
debounce(doSyncSet);
|
||||
}
|
||||
},
|
||||
|
||||
set,
|
||||
reset: key => set(key, deepCopy(defaults[key])),
|
||||
subscribe(keys, listener) {
|
||||
// keys: string[] ids
|
||||
// or a falsy value to subscribe to everything
|
||||
|
@ -208,7 +161,6 @@ var prefs = new function Prefs() {
|
|||
onChange.any.add(listener);
|
||||
}
|
||||
},
|
||||
|
||||
unsubscribe(keys, listener) {
|
||||
if (keys) {
|
||||
for (const key of keys) {
|
||||
|
@ -226,147 +178,58 @@ var prefs = new function Prefs() {
|
|||
onChange.all.remove(listener);
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
{
|
||||
const importFromBG = () =>
|
||||
API.getPrefs().then(prefs => {
|
||||
const props = {};
|
||||
for (const id in prefs) {
|
||||
const value = prefs[id];
|
||||
values[id] = value;
|
||||
props[id] = {value: deepCopy(value)};
|
||||
function setAll(settings, synced) {
|
||||
for (const [key, value] of Object.entries(settings)) {
|
||||
set(key, value, synced);
|
||||
}
|
||||
Object.defineProperties(this.readOnlyValues, props);
|
||||
});
|
||||
// Unlike chrome.storage or messaging, HTML5 localStorage is synchronous and always ready,
|
||||
// so we'll mirror the prefs to avoid using the wrong defaults during the startup phase
|
||||
const importFromLocalStorage = () => {
|
||||
forgetOutdatedDefaults(localStorage);
|
||||
for (const key in defaults) {
|
||||
const defaultValue = defaults[key];
|
||||
let value = localStorage[key];
|
||||
if (typeof value === 'string') {
|
||||
switch (typeof defaultValue) {
|
||||
case 'boolean':
|
||||
value = value.toLowerCase() === 'true';
|
||||
}
|
||||
|
||||
function set(key, value, synced = false) {
|
||||
const oldValue = values[key];
|
||||
switch (typeof defaults[key]) {
|
||||
case typeof value:
|
||||
break;
|
||||
case 'string':
|
||||
value = String(value);
|
||||
break;
|
||||
case 'number':
|
||||
value |= 0;
|
||||
break;
|
||||
case 'object':
|
||||
value = tryJSONparse(value) || defaultValue;
|
||||
case 'boolean':
|
||||
value = value === true || value === 'true';
|
||||
break;
|
||||
}
|
||||
} else if (FIREFOX_NO_DOM_STORAGE && BG) {
|
||||
value = BG.localStorage[key];
|
||||
value = value === undefined ? defaultValue : value;
|
||||
localStorage[key] = value;
|
||||
} else {
|
||||
value = defaultValue;
|
||||
if (equal(value, oldValue)) {
|
||||
return;
|
||||
}
|
||||
if (BG === window) {
|
||||
// when in bg page, .set() will write to localStorage
|
||||
this.set(key, value, {broadcast: false, sync: false});
|
||||
} else {
|
||||
values[key] = value;
|
||||
defineReadonlyProperty(this.readOnlyValues, key, value);
|
||||
}
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
(FIREFOX_NO_DOM_STORAGE && !BG ? importFromBG() : importFromLocalStorage()).then(() => {
|
||||
if (BG && BG !== window) return;
|
||||
if (BG === window) {
|
||||
affectsIcon.forEach(key => this.broadcast(key, values[key], {sync: false}));
|
||||
chromeSync.getValue('settings').then(settings => importFromSync.call(this, settings));
|
||||
}
|
||||
chrome.storage.onChanged.addListener((changes, area) => {
|
||||
if (area === 'sync' && 'settings' in changes) {
|
||||
importFromSync.call(this, changes.settings.newValue);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// any access to chrome API takes time due to initialization of bindings
|
||||
window.addEventListener('load', function _() {
|
||||
window.removeEventListener('load', _);
|
||||
chrome.runtime.onMessage.addListener(msg => {
|
||||
if (msg.prefs) {
|
||||
for (const id in msg.prefs) {
|
||||
prefs.set(id, msg.prefs[id], {fromBroadcast: true});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
// register hotkeys
|
||||
if (FIREFOX && (browser.commands || {}).update) {
|
||||
const hotkeyPrefs = Object.keys(values).filter(k => k.startsWith('hotkey.'));
|
||||
this.subscribe(hotkeyPrefs, (name, value) => {
|
||||
try {
|
||||
name = name.split('.')[1];
|
||||
if (value.trim()) {
|
||||
browser.commands.update({name, shortcut: value}).catch(ignoreChromeError);
|
||||
} else {
|
||||
browser.commands.reset(name).catch(ignoreChromeError);
|
||||
}
|
||||
} catch (e) {}
|
||||
});
|
||||
}
|
||||
|
||||
return;
|
||||
|
||||
function doBroadcast() {
|
||||
if (BG && BG === window && !BG.dbExec.initialized) {
|
||||
window.addEventListener('storageReady', function _() {
|
||||
window.removeEventListener('storageReady', _);
|
||||
doBroadcast();
|
||||
});
|
||||
emitChange(key, value);
|
||||
if (synced || timer) {
|
||||
return;
|
||||
}
|
||||
const affects = {
|
||||
all: 'disableAll' in broadcastPrefs
|
||||
|| 'exposeIframes' in broadcastPrefs,
|
||||
};
|
||||
if (!affects.all) {
|
||||
for (const key in broadcastPrefs) {
|
||||
affects.icon = affects.icon || affectsIcon.includes(key);
|
||||
affects.popup = affects.popup || key.startsWith('popup');
|
||||
affects.editor = affects.editor || key.startsWith('editor');
|
||||
affects.manager = affects.manager || key.startsWith('manage');
|
||||
}
|
||||
}
|
||||
notifyAllTabs({method: 'prefChanged', prefs: broadcastPrefs, affects});
|
||||
broadcastPrefs = {};
|
||||
timer = setTimeout(syncPrefs);
|
||||
}
|
||||
|
||||
function doSyncSet() {
|
||||
chromeSync.setValue('settings', values);
|
||||
function emitChange(key, value) {
|
||||
const specific = onChange.specific.get(key);
|
||||
if (typeof specific === 'function') {
|
||||
specific(key, value);
|
||||
} else if (specific instanceof Set) {
|
||||
for (const listener of specific.values()) {
|
||||
listener(key, value);
|
||||
}
|
||||
|
||||
function importFromSync(synced = {}) {
|
||||
forgetOutdatedDefaults(synced);
|
||||
for (const key in defaults) {
|
||||
if (key in synced) {
|
||||
this.set(key, synced[key], {sync: false});
|
||||
}
|
||||
for (const listener of onChange.any.values()) {
|
||||
listener(key, value);
|
||||
}
|
||||
}
|
||||
|
||||
function forgetOutdatedDefaults(storage) {
|
||||
// our linter runs as a worker so we can reduce the delay and forget the old default values
|
||||
if (Number(storage['editor.lintDelay']) === 500) delete storage['editor.lintDelay'];
|
||||
if (Number(storage['editor.lintReportDelay']) === 4500) delete storage['editor.lintReportDelay'];
|
||||
}
|
||||
|
||||
function defineReadonlyProperty(obj, key, value) {
|
||||
const copy = deepCopy(value);
|
||||
if (typeof copy === 'object') {
|
||||
Object.freeze(copy);
|
||||
}
|
||||
Object.defineProperty(obj, key, {value: copy, configurable: true});
|
||||
function syncPrefs() {
|
||||
// FIXME: we always set the entire object? Ideally, this should only use `changes`.
|
||||
chrome.storage.sync.set({settings: values});
|
||||
timer = null;
|
||||
}
|
||||
|
||||
function equal(a, b) {
|
||||
|
@ -389,7 +252,7 @@ var prefs = new function Prefs() {
|
|||
}
|
||||
|
||||
function contextDeleteMissing() {
|
||||
return CHROME && (
|
||||
return /Chrome\/\d+/.test(navigator.userAgent) && (
|
||||
// detect browsers without Delete by looking at the end of UA string
|
||||
/Vivaldi\/[\d.]+$/.test(navigator.userAgent) ||
|
||||
// Chrome and co.
|
||||
|
@ -398,44 +261,17 @@ var prefs = new function Prefs() {
|
|||
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')
|
||||
);
|
||||
}
|
||||
}();
|
||||
|
||||
|
||||
// Accepts an array of pref names (values are fetched via prefs.get)
|
||||
// and establishes a two-way connection between the document elements and the actual prefs
|
||||
function setupLivePrefs(
|
||||
IDs = Object.getOwnPropertyNames(prefs.readOnlyValues)
|
||||
.filter(id => $('#' + id))
|
||||
) {
|
||||
const checkedProps = {};
|
||||
for (const id of IDs) {
|
||||
const element = $('#' + id);
|
||||
checkedProps[id] = element.type === 'checkbox' ? 'checked' : 'value';
|
||||
updateElement({id, element, force: true});
|
||||
element.addEventListener('change', onChange);
|
||||
function deepCopy(obj) {
|
||||
if (!obj || typeof obj !== 'object') {
|
||||
return obj;
|
||||
}
|
||||
prefs.subscribe(IDs, (id, value) => updateElement({id, value}));
|
||||
|
||||
function onChange() {
|
||||
const value = this[checkedProps[this.id]];
|
||||
if (prefs.get(this.id) !== value) {
|
||||
prefs.set(this.id, value);
|
||||
if (Array.isArray(obj)) {
|
||||
return obj.map(deepCopy);
|
||||
}
|
||||
return Object.keys(obj).reduce((output, key) => {
|
||||
output[key] = deepCopy(obj[key]);
|
||||
return output;
|
||||
}, {});
|
||||
}
|
||||
function updateElement({
|
||||
id,
|
||||
value = prefs.get(id),
|
||||
element = $('#' + id),
|
||||
force,
|
||||
}) {
|
||||
if (!element) {
|
||||
prefs.unsubscribe(IDs, updateElement);
|
||||
return;
|
||||
}
|
||||
const prop = checkedProps[id];
|
||||
if (force || element[prop] !== value) {
|
||||
element[prop] = value;
|
||||
element.dispatchEvent(new Event('change', {bubbles: true, cancelable: true}));
|
||||
}
|
||||
}
|
||||
}
|
||||
})();
|
||||
|
|
24
js/promisify.js
Normal file
24
js/promisify.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
/* exported promisify */
|
||||
'use strict';
|
||||
/*
|
||||
Convert chrome APIs into promises. Example:
|
||||
|
||||
const storageSyncGet = promisify(chrome.storage.sync.get.bind(chrome.storage.sync));
|
||||
storageSyncGet(['key']).then(result => {...});
|
||||
|
||||
*/
|
||||
function promisify(fn) {
|
||||
return (...args) =>
|
||||
new Promise((resolve, reject) => {
|
||||
fn(...args, (...result) => {
|
||||
if (chrome.runtime.lastError) {
|
||||
reject(chrome.runtime.lastError);
|
||||
return;
|
||||
}
|
||||
resolve(
|
||||
result.length === 0 ? undefined :
|
||||
result.length === 1 ? result[0] : result
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,8 +1,8 @@
|
|||
/* exported loadScript */
|
||||
'use strict';
|
||||
|
||||
// loadScript(script: Array<Promise|string>|string): Promise
|
||||
// eslint-disable-next-line no-var
|
||||
var loadScript = (() => {
|
||||
const loadScript = (() => {
|
||||
const cache = new Map();
|
||||
|
||||
function inject(file) {
|
||||
|
@ -26,7 +26,7 @@ var loadScript = (() => {
|
|||
el.onload = () => {
|
||||
el.onload = null;
|
||||
el.onerror = null;
|
||||
resolve();
|
||||
resolve(el);
|
||||
};
|
||||
el.onerror = () => {
|
||||
el.onload = null;
|
||||
|
@ -37,11 +37,15 @@ var loadScript = (() => {
|
|||
});
|
||||
}
|
||||
|
||||
return files => {
|
||||
return (files, noCache = false) => {
|
||||
if (!Array.isArray(files)) {
|
||||
files = [files];
|
||||
}
|
||||
return Promise.all(files.map(f => (typeof f === 'string' ? inject(f) : f)));
|
||||
return Promise.all(files.map(f =>
|
||||
typeof f !== 'string' ? f :
|
||||
noCache ? doInject(f) :
|
||||
inject(f)
|
||||
));
|
||||
};
|
||||
})();
|
||||
|
||||
|
@ -65,7 +69,10 @@ var loadScript = (() => {
|
|||
subscribers.set(srcSuffix, [resolve]);
|
||||
}
|
||||
// a resolved Promise won't reject anymore
|
||||
setTimeout(() => emptyAfterCleanup(srcSuffix) + reject(), timeout);
|
||||
setTimeout(() => {
|
||||
emptyAfterCleanup(srcSuffix);
|
||||
reject(new Error('Timeout'));
|
||||
}, timeout);
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,5 +1,19 @@
|
|||
/* exported styleSectionsEqual styleCodeEmpty calcStyleDigest */
|
||||
'use strict';
|
||||
|
||||
function styleCodeEmpty(code) {
|
||||
if (!code) {
|
||||
return true;
|
||||
}
|
||||
const rx = /\s+|\/\*[\s\S]*?\*\/|@namespace[^;]+;|@charset[^;]+;/giy;
|
||||
while (rx.exec(code)) {
|
||||
if (rx.lastIndex === code.length) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Style} a - first style object
|
||||
* @param {Style} b - second style object
|
||||
|
@ -54,3 +68,31 @@ function styleSectionsEqual(a, b, {ignoreCode, checkSource} = {}) {
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeStyleSections({sections}) {
|
||||
// retain known properties in an arbitrarily predefined order
|
||||
return (sections || []).map(section => ({
|
||||
code: section.code || '',
|
||||
urls: section.urls || [],
|
||||
urlPrefixes: section.urlPrefixes || [],
|
||||
domains: section.domains || [],
|
||||
regexps: section.regexps || [],
|
||||
}));
|
||||
}
|
||||
|
||||
function calcStyleDigest(style) {
|
||||
const jsonString = style.usercssData ?
|
||||
style.sourceCode : JSON.stringify(normalizeStyleSections(style));
|
||||
const text = new TextEncoder('utf-8').encode(jsonString);
|
||||
return crypto.subtle.digest('SHA-1', text).then(hex);
|
||||
|
||||
function hex(buffer) {
|
||||
const parts = [];
|
||||
const PAD8 = '00000000';
|
||||
const view = new DataView(buffer);
|
||||
for (let i = 0; i < view.byteLength; i += 4) {
|
||||
parts.push((PAD8 + view.getUint32(i).toString(16)).slice(-8));
|
||||
}
|
||||
return parts.join('');
|
||||
}
|
||||
}
|
|
@ -1,20 +1,14 @@
|
|||
/* global loadScript */
|
||||
/* global loadScript tryJSONparse */
|
||||
/* exported chromeLocal chromeSync */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var [chromeLocal, chromeSync] = (() => {
|
||||
const native = 'sync' in chrome.storage &&
|
||||
!chrome.runtime.id.includes('@temporary');
|
||||
if (!native && BG !== window) {
|
||||
setupOnChangeRelay();
|
||||
}
|
||||
const [chromeLocal, chromeSync] = (() => {
|
||||
return [
|
||||
createWrapper('local'),
|
||||
createWrapper('sync'),
|
||||
];
|
||||
|
||||
function createWrapper(name) {
|
||||
if (!native) createDummyStorage(name);
|
||||
const storage = chrome.storage[name];
|
||||
const wrapper = {
|
||||
get: data => new Promise(resolve => storage.get(data, resolve)),
|
||||
|
@ -58,39 +52,10 @@ var [chromeLocal, chromeSync] = (() => {
|
|||
return wrapper;
|
||||
}
|
||||
|
||||
function createDummyStorage(name) {
|
||||
chrome.storage[name] = {
|
||||
get: (data, cb) => API.dummyStorageGet({data, name}).then(cb),
|
||||
set: (data, cb) => API.dummyStorageSet({data, name}).then(cb),
|
||||
remove: (data, cb) => API.dummyStorageRemove({data, name}).then(cb),
|
||||
};
|
||||
}
|
||||
|
||||
function loadLZStringScript() {
|
||||
return window.LZString ?
|
||||
Promise.resolve(window.LZString) :
|
||||
loadScript('/vendor/lz-string-unsafe/lz-string-unsafe.min.js').then(() =>
|
||||
(window.LZString = window.LZString || window.LZStringUnsafe));
|
||||
}
|
||||
|
||||
function setupOnChangeRelay() {
|
||||
const listeners = new Set();
|
||||
const onMessage = msg => {
|
||||
if (!msg.dummyStorageChanges) return;
|
||||
for (const fn of listeners.values()) {
|
||||
fn(msg.dummyStorageChanges, msg.dummyStorageName);
|
||||
}
|
||||
};
|
||||
Object.assign(chrome.storage.onChanged, {
|
||||
addListener(fn) {
|
||||
if (!listeners.size) chrome.runtime.onMessage.addListener(onMessage);
|
||||
listeners.add(fn);
|
||||
},
|
||||
hasListener: fn => listeners.has(fn),
|
||||
removeListener(fn) {
|
||||
listeners.delete(fn);
|
||||
if (!listeners.size) chrome.runtime.onMessage.removeListener(onMessage);
|
||||
}
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
|
650
js/usercss.js
650
js/usercss.js
|
@ -1,514 +1,55 @@
|
|||
/* global loadScript semverCompare colorConverter styleCodeEmpty */
|
||||
/* global backgroundWorker */
|
||||
/* exported usercss */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var usercss = (() => {
|
||||
// true = global
|
||||
// false or 0 = private
|
||||
// <string> = global key name
|
||||
// <function> = (style, newValue)
|
||||
const KNOWN_META = new Map([
|
||||
['author', true],
|
||||
['advanced', 0],
|
||||
['description', true],
|
||||
['homepageURL', 'url'],
|
||||
['icon', 0],
|
||||
['license', 0],
|
||||
['name', true],
|
||||
['namespace', 0],
|
||||
//['noframes', 0],
|
||||
['preprocessor', 0],
|
||||
['supportURL', 0],
|
||||
['updateURL', (style, newValue) => {
|
||||
// always preserve locally installed style's updateUrl
|
||||
if (!/^file:/.test(style.updateUrl)) {
|
||||
style.updateUrl = newValue;
|
||||
}
|
||||
}],
|
||||
['var', 0],
|
||||
['version', 0],
|
||||
]);
|
||||
const MANDATORY_META = ['name', 'namespace', 'version'];
|
||||
const META_VARS = ['text', 'color', 'checkbox', 'select', 'dropdown', 'image', 'number', 'range'];
|
||||
const META_URLS = [...KNOWN_META.keys()].filter(k => k.endsWith('URL'));
|
||||
|
||||
const BUILDER = {
|
||||
default: {
|
||||
postprocess(sections, vars) {
|
||||
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
|
||||
if (!varDef) return;
|
||||
varDef = ':root {\n' + varDef + '}\n';
|
||||
for (const section of sections) {
|
||||
if (!styleCodeEmpty(section.code)) {
|
||||
section.code = varDef + section.code;
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
stylus: {
|
||||
preprocess(source, vars) {
|
||||
return loadScript('/vendor/stylus-lang-bundle/stylus.min.js').then(() => (
|
||||
new Promise((resolve, reject) => {
|
||||
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
|
||||
if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
|
||||
window.stylus(varDef + source).render((err, output) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
} else {
|
||||
resolve(output);
|
||||
}
|
||||
});
|
||||
})
|
||||
));
|
||||
}
|
||||
},
|
||||
less: {
|
||||
preprocess(source, vars) {
|
||||
window.less = window.less || {
|
||||
logLevel: 0,
|
||||
useFileCache: false,
|
||||
const usercss = (() => {
|
||||
const GLOBAL_METAS = {
|
||||
author: undefined,
|
||||
description: undefined,
|
||||
homepageURL: 'url',
|
||||
// updateURL: 'updateUrl',
|
||||
name: undefined,
|
||||
};
|
||||
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
|
||||
return loadScript('/vendor/less/less.min.js')
|
||||
.then(() => window.less.render(varDefs + source))
|
||||
.then(({css}) => css);
|
||||
}
|
||||
},
|
||||
uso: {
|
||||
preprocess(source, vars) {
|
||||
const pool = new Map();
|
||||
return Promise.resolve(doReplace(source));
|
||||
|
||||
function getValue(name, rgb) {
|
||||
if (!vars.hasOwnProperty(name)) {
|
||||
if (name.endsWith('-rgb')) {
|
||||
return getValue(name.slice(0, -4), true);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (rgb) {
|
||||
if (vars[name].type === 'color') {
|
||||
const color = colorConverter.parse(vars[name].value);
|
||||
if (!color) return null;
|
||||
const {r, g, b} = color;
|
||||
return `${r}, ${g}, ${b}`;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
|
||||
// prevent infinite recursion
|
||||
pool.set(name, '');
|
||||
return doReplace(vars[name].value);
|
||||
}
|
||||
return vars[name].value;
|
||||
}
|
||||
|
||||
function doReplace(text) {
|
||||
return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => {
|
||||
if (!pool.has(name)) {
|
||||
const value = getValue(name);
|
||||
pool.set(name, value === null ? match : value);
|
||||
}
|
||||
return pool.get(name);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const RX_NUMBER = /-?\d+(\.\d+)?\s*/y;
|
||||
const RX_WHITESPACE = /\s*/y;
|
||||
const RX_WORD = /([\w-]+)\s*/y;
|
||||
const RX_STRING_BACKTICK = /(`(?:\\`|[\s\S])*?`)\s*/y;
|
||||
const RX_STRING_QUOTED = /((['"])(?:\\\2|[^\n])*?\2|\w+)\s*/y;
|
||||
|
||||
const worker = {};
|
||||
|
||||
function getMetaSource(source) {
|
||||
const commentRe = /\/\*[\s\S]*?\*\//g;
|
||||
const metaRe = /==userstyle==[\s\S]*?==\/userstyle==/i;
|
||||
|
||||
let m;
|
||||
// iterate through each comment
|
||||
while ((m = commentRe.exec(source))) {
|
||||
const commentSource = source.slice(m.index, m.index + m[0].length);
|
||||
const n = commentSource.match(metaRe);
|
||||
if (n) {
|
||||
return {
|
||||
index: m.index + n.index,
|
||||
text: n[0]
|
||||
};
|
||||
}
|
||||
}
|
||||
return {text: '', index: 0};
|
||||
}
|
||||
|
||||
function parseWord(state, error = 'invalid word') {
|
||||
RX_WORD.lastIndex = state.re.lastIndex;
|
||||
const match = RX_WORD.exec(state.text);
|
||||
if (!match) {
|
||||
throw new Error((state.errorPrefix || '') + error);
|
||||
}
|
||||
state.value = match[1];
|
||||
state.re.lastIndex += match[0].length;
|
||||
}
|
||||
|
||||
function parseVar(state) {
|
||||
const result = {
|
||||
type: null,
|
||||
label: null,
|
||||
name: null,
|
||||
value: null,
|
||||
default: null,
|
||||
options: null
|
||||
};
|
||||
|
||||
parseWord(state, 'missing type');
|
||||
result.type = state.type = state.value;
|
||||
|
||||
if (!META_VARS.includes(state.type)) {
|
||||
throw new Error(`unknown type: ${state.type}`);
|
||||
}
|
||||
|
||||
parseWord(state, 'missing name');
|
||||
result.name = state.value;
|
||||
|
||||
parseString(state);
|
||||
result.label = state.value;
|
||||
|
||||
const {re, type, text} = state;
|
||||
|
||||
switch (type === 'image' && state.key === 'var' ? '@image@var' : type) {
|
||||
case 'checkbox': {
|
||||
const match = text.slice(re.lastIndex).match(/([01])\s+/);
|
||||
if (!match) {
|
||||
throw new Error('value must be 0 or 1');
|
||||
}
|
||||
re.lastIndex += match[0].length;
|
||||
result.default = match[1];
|
||||
break;
|
||||
}
|
||||
|
||||
case 'select':
|
||||
case '@image@var': {
|
||||
state.errorPrefix = 'Invalid JSON: ';
|
||||
parseJSONValue(state);
|
||||
state.errorPrefix = '';
|
||||
const extractDefaultOption = (key, value) => {
|
||||
if (key.endsWith('*')) {
|
||||
const option = createOption(key.slice(0, -1), value);
|
||||
result.default = option.name;
|
||||
return option;
|
||||
}
|
||||
return createOption(key, value);
|
||||
};
|
||||
if (Array.isArray(state.value)) {
|
||||
result.options = state.value.map(k => extractDefaultOption(k));
|
||||
} else {
|
||||
result.options = Object.keys(state.value).map(k => extractDefaultOption(k, state.value[k]));
|
||||
}
|
||||
if (result.default === null) {
|
||||
result.default = (result.options[0] || {}).name || '';
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'number':
|
||||
case 'range': {
|
||||
state.errorPrefix = 'Invalid JSON: ';
|
||||
parseJSONValue(state);
|
||||
state.errorPrefix = '';
|
||||
// [default, start, end, step, units] (start, end, step & units are optional)
|
||||
if (Array.isArray(state.value) && state.value.length) {
|
||||
// label may be placed anywhere
|
||||
result.units = (state.value.find(i => typeof i === 'string') || '').replace(/[\d.+-]/g, '');
|
||||
const range = state.value.filter(i => typeof i === 'number' || i === null);
|
||||
result.default = range[0];
|
||||
result.min = range[1];
|
||||
result.max = range[2];
|
||||
result.step = range[3] === 0 ? 1 : range[3];
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case 'dropdown':
|
||||
case 'image': {
|
||||
if (text[re.lastIndex] !== '{') {
|
||||
throw new Error('no open {');
|
||||
}
|
||||
result.options = [];
|
||||
re.lastIndex++;
|
||||
while (text[re.lastIndex] !== '}') {
|
||||
const option = {};
|
||||
|
||||
parseStringUnquoted(state);
|
||||
option.name = state.value;
|
||||
|
||||
parseString(state);
|
||||
option.label = state.value;
|
||||
|
||||
if (type === 'dropdown') {
|
||||
parseEOT(state);
|
||||
} else {
|
||||
parseString(state);
|
||||
}
|
||||
option.value = state.value;
|
||||
|
||||
result.options.push(option);
|
||||
}
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
result.default = result.options[0].name;
|
||||
break;
|
||||
}
|
||||
|
||||
default: {
|
||||
// text, color
|
||||
parseStringToEnd(state);
|
||||
result.default = state.value;
|
||||
}
|
||||
}
|
||||
state.usercssData.vars[result.name] = result;
|
||||
validateVar(result);
|
||||
}
|
||||
|
||||
function createOption(label, value) {
|
||||
let name;
|
||||
const match = label.match(/^(\w+):(.*)/);
|
||||
if (match) {
|
||||
([, name, label] = match);
|
||||
}
|
||||
if (!name) {
|
||||
name = label;
|
||||
}
|
||||
if (!value) {
|
||||
value = name;
|
||||
}
|
||||
return {name, label, value};
|
||||
}
|
||||
|
||||
function parseEOT(state) {
|
||||
const re = /<<<EOT([\s\S]+?)EOT;/y;
|
||||
re.lastIndex = state.re.lastIndex;
|
||||
const match = state.text.match(re);
|
||||
if (!match) {
|
||||
throw new Error('missing EOT');
|
||||
}
|
||||
state.re.lastIndex += match[0].length;
|
||||
state.value = match[1].trim().replace(/\*\\\//g, '*/');
|
||||
eatWhitespace(state);
|
||||
}
|
||||
|
||||
function parseStringUnquoted(state) {
|
||||
const pos = state.re.lastIndex;
|
||||
const nextQuoteOrEOL = posOrEnd(state.text, '"', pos);
|
||||
state.re.lastIndex = nextQuoteOrEOL;
|
||||
state.value = state.text.slice(pos, nextQuoteOrEOL).trim().replace(/\s+/g, '-');
|
||||
}
|
||||
|
||||
function parseString(state) {
|
||||
const pos = state.re.lastIndex;
|
||||
const rx = state.text[pos] === '`' ? RX_STRING_BACKTICK : RX_STRING_QUOTED;
|
||||
rx.lastIndex = pos;
|
||||
const match = rx.exec(state.text);
|
||||
if (!match) {
|
||||
throw new Error((state.errorPrefix || '') + 'Quoted string expected');
|
||||
}
|
||||
state.re.lastIndex += match[0].length;
|
||||
state.value = unquote(match[1]);
|
||||
}
|
||||
|
||||
function parseJSONValue(state) {
|
||||
const JSON_PRIME = {
|
||||
__proto__: null,
|
||||
'null': null,
|
||||
'true': true,
|
||||
'false': false
|
||||
};
|
||||
const {text, re, errorPrefix} = state;
|
||||
if (text[re.lastIndex] === '{') {
|
||||
// object
|
||||
const obj = {};
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
while (text[re.lastIndex] !== '}') {
|
||||
parseString(state);
|
||||
const key = state.value;
|
||||
if (text[re.lastIndex] !== ':') {
|
||||
throw new Error(`${errorPrefix}missing ':'`);
|
||||
}
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
parseJSONValue(state);
|
||||
obj[key] = state.value;
|
||||
if (text[re.lastIndex] === ',') {
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
} else if (text[re.lastIndex] !== '}') {
|
||||
throw new Error(`${errorPrefix}missing ',' or '}'`);
|
||||
}
|
||||
}
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
state.value = obj;
|
||||
} else if (text[re.lastIndex] === '[') {
|
||||
// array
|
||||
const arr = [];
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
while (text[re.lastIndex] !== ']') {
|
||||
parseJSONValue(state);
|
||||
arr.push(state.value);
|
||||
if (text[re.lastIndex] === ',') {
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
} else if (text[re.lastIndex] !== ']') {
|
||||
throw new Error(`${errorPrefix}missing ',' or ']'`);
|
||||
}
|
||||
}
|
||||
re.lastIndex++;
|
||||
eatWhitespace(state);
|
||||
state.value = arr;
|
||||
} else if (text[re.lastIndex] === '"' || text[re.lastIndex] === '`') {
|
||||
// string
|
||||
parseString(state);
|
||||
} else if (/\d/.test(text[re.lastIndex])) {
|
||||
// number
|
||||
parseNumber(state);
|
||||
} else {
|
||||
parseWord(state);
|
||||
if (!(state.value in JSON_PRIME)) {
|
||||
throw new Error(`${errorPrefix}unknown literal '${state.value}'`);
|
||||
}
|
||||
state.value = JSON_PRIME[state.value];
|
||||
}
|
||||
}
|
||||
|
||||
function parseNumber(state) {
|
||||
RX_NUMBER.lastIndex = state.re.lastIndex;
|
||||
const match = RX_NUMBER.exec(state.text);
|
||||
if (!match) {
|
||||
throw new Error((state.errorPrefix || '') + 'invalid number');
|
||||
}
|
||||
state.value = Number(match[0].trim());
|
||||
state.re.lastIndex += match[0].length;
|
||||
}
|
||||
|
||||
function eatWhitespace(state) {
|
||||
RX_WHITESPACE.lastIndex = state.re.lastIndex;
|
||||
state.re.lastIndex += RX_WHITESPACE.exec(state.text)[0].length;
|
||||
}
|
||||
|
||||
function parseStringToEnd(state) {
|
||||
const EOL = posOrEnd(state.text, '\n', state.re.lastIndex);
|
||||
const match = state.text.slice(state.re.lastIndex, EOL);
|
||||
state.value = unquote(match.trim());
|
||||
state.re.lastIndex += match.length;
|
||||
}
|
||||
|
||||
function unquote(s) {
|
||||
const q = s[0];
|
||||
if (q === s[s.length - 1] && (q === '"' || q === "'" || q === '`')) {
|
||||
// http://www.json.org/
|
||||
return s.slice(1, -1).replace(
|
||||
new RegExp(`\\\\([${q}\\\\/bfnrt]|u[0-9a-fA-F]{4})`, 'g'),
|
||||
s => {
|
||||
if (s[1] === q) {
|
||||
return q;
|
||||
}
|
||||
return JSON.parse(`"${s}"`);
|
||||
}
|
||||
);
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
function posOrEnd(haystack, needle, start) {
|
||||
const pos = haystack.indexOf(needle, start);
|
||||
return pos < 0 ? haystack.length : pos;
|
||||
}
|
||||
const RX_META = /\/\*\s*==userstyle==[\s\S]*?==\/userstyle==\s*\*\//i;
|
||||
const ERR_ARGS_IS_LIST = new Set(['missingMandatory', 'missingChar']);
|
||||
return {buildMeta, buildCode, assignVars};
|
||||
|
||||
function buildMeta(sourceCode) {
|
||||
sourceCode = sourceCode.replace(/\r\n?/g, '\n');
|
||||
|
||||
const usercssData = {
|
||||
vars: {}
|
||||
};
|
||||
|
||||
const style = {
|
||||
reason: 'install',
|
||||
enabled: true,
|
||||
sourceCode,
|
||||
sections: [],
|
||||
usercssData
|
||||
sections: []
|
||||
};
|
||||
|
||||
const {text, index: metaIndex} = getMetaSource(sourceCode);
|
||||
const re = /@(\w+)[ \t\xA0]*/mg;
|
||||
const state = {style, re, text, usercssData};
|
||||
|
||||
function doParse() {
|
||||
let match;
|
||||
while ((match = re.exec(text))) {
|
||||
const key = state.key = match[1];
|
||||
const route = KNOWN_META.get(key);
|
||||
if (route === undefined) {
|
||||
continue;
|
||||
}
|
||||
if (key === 'var' || key === 'advanced') {
|
||||
if (key === 'advanced') {
|
||||
state.maybeUSO = true;
|
||||
}
|
||||
parseVar(state);
|
||||
} else {
|
||||
parseStringToEnd(state);
|
||||
usercssData[key] = state.value;
|
||||
}
|
||||
let value = state.value;
|
||||
if (key === 'version') {
|
||||
value = usercssData[key] = normalizeVersion(value);
|
||||
validateVersion(value);
|
||||
}
|
||||
if (META_URLS.includes(key)) {
|
||||
validateUrl(key, value);
|
||||
}
|
||||
switch (typeof route) {
|
||||
case 'function':
|
||||
route(style, value);
|
||||
break;
|
||||
case 'string':
|
||||
style[route] = value;
|
||||
break;
|
||||
default:
|
||||
if (route) {
|
||||
style[key] = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
const match = sourceCode.match(RX_META);
|
||||
if (!match) {
|
||||
throw new Error('can not find metadata');
|
||||
}
|
||||
|
||||
try {
|
||||
doParse();
|
||||
} catch (e) {
|
||||
// the source code string offset
|
||||
e.index = metaIndex + state.re.lastIndex;
|
||||
throw e;
|
||||
return backgroundWorker.parseUsercssMeta(match[0], match.index)
|
||||
.catch(err => {
|
||||
if (err.code) {
|
||||
const args = ERR_ARGS_IS_LIST.has(err.code) ? drawList(err.args) : err.args;
|
||||
const message = chrome.i18n.getMessage(`meta_${err.code}`, args);
|
||||
if (message) {
|
||||
err.message = message;
|
||||
}
|
||||
|
||||
if (state.maybeUSO && !usercssData.preprocessor) {
|
||||
usercssData.preprocessor = 'uso';
|
||||
}
|
||||
|
||||
validateStyle(style);
|
||||
throw err;
|
||||
})
|
||||
.then(({metadata}) => {
|
||||
style.usercssData = metadata;
|
||||
for (const [key, value = key] of Object.entries(GLOBAL_METAS)) {
|
||||
style[value] = metadata[key];
|
||||
}
|
||||
return style;
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeVersion(version) {
|
||||
// https://docs.npmjs.com/misc/semver#versions
|
||||
if (version[0] === 'v' || version[0] === '=') {
|
||||
return version.slice(1);
|
||||
}
|
||||
return version;
|
||||
function drawList(items) {
|
||||
return items.map(i => i.length === 1 ? JSON.stringify(i) : i).join(', ');
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -518,136 +59,37 @@ var usercss = (() => {
|
|||
* when allowErrors is falsy or {style, errors} object when allowErrors is truthy
|
||||
*/
|
||||
function buildCode(style, allowErrors) {
|
||||
const {usercssData: {preprocessor, vars}, sourceCode} = style;
|
||||
let builder;
|
||||
if (preprocessor) {
|
||||
if (!BUILDER[preprocessor]) {
|
||||
return Promise.reject(chrome.i18n.getMessage('styleMetaErrorPreprocessor', preprocessor));
|
||||
}
|
||||
builder = BUILDER[preprocessor];
|
||||
} else {
|
||||
builder = BUILDER.default;
|
||||
}
|
||||
|
||||
const sVars = simpleVars(vars);
|
||||
|
||||
return (
|
||||
Promise.resolve(
|
||||
builder.preprocess && builder.preprocess(sourceCode, sVars) ||
|
||||
sourceCode)
|
||||
.then(mozStyle => invokeWorker({
|
||||
action: 'parse',
|
||||
styleId: style.id,
|
||||
code: mozStyle,
|
||||
}))
|
||||
const match = style.sourceCode.match(RX_META);
|
||||
return backgroundWorker.compileUsercss(
|
||||
style.usercssData.preprocessor,
|
||||
style.sourceCode.slice(0, match.index) + style.sourceCode.slice(match.index + match[0].length),
|
||||
style.usercssData.vars
|
||||
)
|
||||
.then(({sections, errors}) => {
|
||||
if (!errors.length) errors = false;
|
||||
if (!sections.length || errors && !allowErrors) {
|
||||
return Promise.reject(errors);
|
||||
throw errors;
|
||||
}
|
||||
style.sections = sections;
|
||||
if (builder.postprocess) builder.postprocess(style.sections, sVars);
|
||||
return allowErrors ? {style, errors} : style;
|
||||
}));
|
||||
}
|
||||
|
||||
function simpleVars(vars) {
|
||||
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
|
||||
// need to test each va's default value.
|
||||
return Object.keys(vars).reduce((output, key) => {
|
||||
const va = vars[key];
|
||||
output[key] = Object.assign({}, va, {
|
||||
value: va.value === null || va.value === undefined ?
|
||||
getVarValue(va, 'default') : getVarValue(va, 'value')
|
||||
});
|
||||
return output;
|
||||
}, {});
|
||||
}
|
||||
|
||||
function getVarValue(va, prop) {
|
||||
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
|
||||
// TODO: handle customized image
|
||||
return va.options.find(o => o.name === va[prop]).value;
|
||||
}
|
||||
if ((va.type === 'number' || va.type === 'range') && va.units) {
|
||||
return va[prop] + va.units;
|
||||
}
|
||||
return va[prop];
|
||||
}
|
||||
|
||||
function validateStyle({usercssData: data}) {
|
||||
for (const prop of MANDATORY_META) {
|
||||
if (!data[prop]) {
|
||||
throw new Error(chrome.i18n.getMessage('styleMissingMeta', prop));
|
||||
}
|
||||
}
|
||||
validateVersion(data.version);
|
||||
META_URLS.forEach(k => validateUrl(k, data[k]));
|
||||
Object.keys(data.vars).forEach(k => validateVar(data.vars[k]));
|
||||
}
|
||||
|
||||
function validateVersion(version) {
|
||||
semverCompare(version, '0.0.0');
|
||||
}
|
||||
|
||||
function validateUrl(key, url) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
url = new URL(url);
|
||||
if (!/^https?:/.test(url.protocol)) {
|
||||
throw new Error(`${url.protocol} is not a valid protocol in ${key}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateVar(va, value = 'default') {
|
||||
if (va.type === 'select' || va.type === 'dropdown') {
|
||||
if (va.options.every(o => o.name !== va[value])) {
|
||||
throw new Error(chrome.i18n.getMessage('styleMetaErrorSelectValueMismatch'));
|
||||
}
|
||||
} else if (va.type === 'checkbox' && !/^[01]$/.test(va[value])) {
|
||||
throw new Error(chrome.i18n.getMessage('styleMetaErrorCheckbox'));
|
||||
} else if (va.type === 'color') {
|
||||
va[value] = colorConverter.format(colorConverter.parse(va[value]), 'rgb');
|
||||
} else if ((va.type === 'number' || va.type === 'range') && typeof va[value] !== 'number') {
|
||||
throw new Error(chrome.i18n.getMessage('styleMetaErrorRangeOrNumber', va.type));
|
||||
}
|
||||
}
|
||||
|
||||
function assignVars(style, oldStyle) {
|
||||
const {usercssData: {vars}} = style;
|
||||
const {usercssData: {vars: oldVars}} = oldStyle;
|
||||
if (!vars || !oldVars) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
// The type of var might be changed during the update. Set value to null if the value is invalid.
|
||||
for (const key of Object.keys(vars)) {
|
||||
if (oldVars[key] && oldVars[key].value) {
|
||||
vars[key].value = oldVars[key].value;
|
||||
try {
|
||||
validateVar(vars[key], 'value');
|
||||
} catch (e) {
|
||||
vars[key].value = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function invokeWorker(message) {
|
||||
if (!worker.queue) {
|
||||
worker.instance = new Worker('/background/parserlib-loader.js');
|
||||
worker.queue = [];
|
||||
worker.instance.onmessage = ({data}) => {
|
||||
worker.queue.shift().resolve(data.__ERROR__ ? Promise.reject(data.__ERROR__) : data);
|
||||
if (worker.queue.length) {
|
||||
worker.instance.postMessage(worker.queue[0].message);
|
||||
}
|
||||
};
|
||||
}
|
||||
return new Promise(resolve => {
|
||||
worker.queue.push({message, resolve});
|
||||
if (worker.queue.length === 1) {
|
||||
worker.instance.postMessage(message);
|
||||
}
|
||||
return backgroundWorker.nullifyInvalidVars(vars)
|
||||
.then(vars => {
|
||||
style.usercssData.vars = vars;
|
||||
});
|
||||
}
|
||||
|
||||
return {buildMeta, buildCode, assignVars, invokeWorker};
|
||||
})();
|
||||
|
|
98
js/worker-util.js
Normal file
98
js/worker-util.js
Normal file
|
@ -0,0 +1,98 @@
|
|||
/* global importScripts */
|
||||
/* exported workerUtil */
|
||||
'use strict';
|
||||
|
||||
const workerUtil = (() => {
|
||||
const loadedScripts = new Set();
|
||||
return {createWorker, createAPI, loadScript, cloneError};
|
||||
|
||||
function createWorker({url, lifeTime = 30}) {
|
||||
let worker;
|
||||
let id;
|
||||
let timer;
|
||||
const pendingResponse = new Map();
|
||||
|
||||
return new Proxy({}, {
|
||||
get: (target, prop) =>
|
||||
(...args) => {
|
||||
if (!worker) {
|
||||
init();
|
||||
}
|
||||
return invoke(prop, args);
|
||||
}
|
||||
});
|
||||
|
||||
function init() {
|
||||
id = 0;
|
||||
worker = new Worker(url);
|
||||
worker.onmessage = onMessage;
|
||||
}
|
||||
|
||||
function uninit() {
|
||||
worker.onmessage = null;
|
||||
worker.terminate();
|
||||
worker = null;
|
||||
}
|
||||
|
||||
function onMessage(e) {
|
||||
const message = e.data;
|
||||
pendingResponse.get(message.id)[message.error ? 'reject' : 'resolve'](message.data);
|
||||
pendingResponse.delete(message.id);
|
||||
if (!pendingResponse.size && lifeTime >= 0) {
|
||||
timer = setTimeout(uninit, lifeTime * 1000);
|
||||
}
|
||||
}
|
||||
|
||||
function invoke(action, args) {
|
||||
return new Promise((resolve, reject) => {
|
||||
pendingResponse.set(id, {resolve, reject});
|
||||
clearTimeout(timer);
|
||||
worker.postMessage({
|
||||
id,
|
||||
action,
|
||||
args
|
||||
});
|
||||
id++;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function createAPI(methods) {
|
||||
self.onmessage = e => {
|
||||
const message = e.data;
|
||||
Promise.resolve()
|
||||
.then(() => methods[message.action](...message.args))
|
||||
.then(result => ({
|
||||
id: message.id,
|
||||
error: false,
|
||||
data: result
|
||||
}))
|
||||
.catch(err => ({
|
||||
id: message.id,
|
||||
error: true,
|
||||
data: cloneError(err)
|
||||
}))
|
||||
.then(data => self.postMessage(data));
|
||||
};
|
||||
}
|
||||
|
||||
function cloneError(err) {
|
||||
return Object.assign({
|
||||
name: err.name,
|
||||
stack: err.stack,
|
||||
message: err.message,
|
||||
lineNumber: err.lineNumber,
|
||||
columnNumber: err.columnNumber,
|
||||
fileName: err.fileName
|
||||
}, err);
|
||||
}
|
||||
|
||||
function loadScript(...scripts) {
|
||||
const urls = scripts.filter(u => !loadedScripts.has(u));
|
||||
if (!urls.length) {
|
||||
return;
|
||||
}
|
||||
importScripts(...urls);
|
||||
urls.forEach(u => loadedScripts.add(u));
|
||||
}
|
||||
})();
|
23
manage.html
23
manage.html
|
@ -146,26 +146,27 @@
|
|||
</details>
|
||||
</template>
|
||||
|
||||
<script src="js/promisify.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="manage/filters.js"></script>
|
||||
<script src="manage/sort.js"></script>
|
||||
<script src="manage/manage.js"></script>
|
||||
|
||||
<script src="vendor-overwrites/colorpicker/colorconverter.js" async></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorpicker.js" async></script>
|
||||
<script src="manage/config-dialog.js" async></script>
|
||||
<script src="manage/updater-ui.js" async></script>
|
||||
<script src="manage/object-diff.js" async></script>
|
||||
<script src="manage/import-export.js" async></script>
|
||||
|
||||
<script src="manage/incremental-search.js" async></script>
|
||||
<script src="msgbox/msgbox.js" async></script>
|
||||
<script src="js/sections-equal.js" async></script>
|
||||
<script src="js/storage-util.js" async></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorconverter.js"></script>
|
||||
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
|
||||
<script src="manage/config-dialog.js"></script>
|
||||
<script src="manage/updater-ui.js"></script>
|
||||
<script src="manage/object-diff.js"></script>
|
||||
<script src="manage/import-export.js"></script>
|
||||
<script src="manage/incremental-search.js"></script>
|
||||
<script src="msgbox/msgbox.js"></script>
|
||||
<script src="js/sections-util.js"></script>
|
||||
<script src="js/storage-util.js"></script>
|
||||
|
||||
<script src="sync/vendor/dropbox/dropbox-sdk.js" async></script>
|
||||
<script src="sync/vendor/zipjs/zip.js" defer></script>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global messageBox */
|
||||
/* global messageBox deepCopy $create $createLink $ t tWordBreak
|
||||
prefs setupLivePrefs debounce API */
|
||||
/* exported configDialog */
|
||||
'use strict';
|
||||
|
||||
function configDialog(style) {
|
||||
|
@ -117,13 +119,13 @@ function configDialog(style) {
|
|||
return;
|
||||
}
|
||||
if (!bgStyle) {
|
||||
API.getStyles({id: style.id, omitCode: !BG})
|
||||
.then(([bgStyle]) => save({anyChangeIsDirty}, bgStyle || {}));
|
||||
API.getStyle(style.id, true)
|
||||
.catch(() => ({}))
|
||||
.then(bgStyle => save({anyChangeIsDirty}, bgStyle));
|
||||
return;
|
||||
}
|
||||
style = style.sections ? Object.assign({}, style) : style;
|
||||
style.enabled = true;
|
||||
style.reason = 'config';
|
||||
style.sourceCode = null;
|
||||
style.sections = null;
|
||||
const styleVars = style.usercssData.vars;
|
||||
|
@ -171,9 +173,9 @@ function configDialog(style) {
|
|||
return;
|
||||
}
|
||||
saving = true;
|
||||
return API.saveUsercss(style)
|
||||
.then(saved => {
|
||||
varsInitial = getInitialValues(saved.usercssData.vars);
|
||||
return API.configUsercssVars(style.id, style.usercssData.vars)
|
||||
.then(newVars => {
|
||||
varsInitial = getInitialValues(newVars);
|
||||
vars.forEach(va => onchange({target: va.input, justSaved: true}));
|
||||
renderValues();
|
||||
updateButtons();
|
||||
|
@ -182,7 +184,7 @@ function configDialog(style) {
|
|||
.catch(errors => {
|
||||
const el = $('.config-error', messageBox.element) ||
|
||||
$('#message-box-buttons').insertAdjacentElement('afterbegin', $create('.config-error'));
|
||||
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors;
|
||||
el.textContent = el.title = Array.isArray(errors) ? errors.join('\n') : errors.message || String(errors);
|
||||
})
|
||||
.then(() => {
|
||||
saving = false;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global installed messageBox */
|
||||
/* global sorter */
|
||||
/* global installed messageBox sorter $ $$ $create t debounce prefs API onDOMready */
|
||||
/* exported filterAndAppend */
|
||||
'use strict';
|
||||
|
||||
const filtersSelector = {
|
||||
|
@ -114,7 +114,7 @@ onDOMready().then(() => {
|
|||
}
|
||||
if (value !== undefined) {
|
||||
el.lastValue = value;
|
||||
if (el.id in prefs.readOnlyValues) {
|
||||
if (el.id in prefs.defaults) {
|
||||
prefs.set(el.id, false);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,54 @@
|
|||
/* global messageBox handleUpdate handleDelete applyOnMessage styleSectionsEqual */
|
||||
/* global messageBox styleSectionsEqual getOwnTab API onDOMready
|
||||
tryJSONparse scrollElementIntoView $ $$ API $create t animateElement */
|
||||
'use strict';
|
||||
|
||||
const STYLISH_DUMP_FILE_EXT = '.txt';
|
||||
const STYLUS_BACKUP_FILE_EXT = '.json';
|
||||
|
||||
onDOMready().then(() => {
|
||||
$('#file-all-styles').onclick = exportToFile;
|
||||
$('#unfile-all-styles').onclick = () => {
|
||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
};
|
||||
|
||||
Object.assign(document.body, {
|
||||
ondragover(event) {
|
||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||
event.dataTransfer.dropEffect = hasFiles || event.target.type === 'search' ? 'copy' : 'none';
|
||||
this.classList.toggle('dropzone', hasFiles);
|
||||
if (hasFiles) {
|
||||
event.preventDefault();
|
||||
clearTimeout(this.fadeoutTimer);
|
||||
this.classList.remove('fadeout');
|
||||
}
|
||||
},
|
||||
ondragend() {
|
||||
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
||||
this.style.animationDuration = '';
|
||||
});
|
||||
},
|
||||
ondragleave(event) {
|
||||
try {
|
||||
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
||||
if (event.target === this) {
|
||||
this.ondragend();
|
||||
}
|
||||
} catch (e) {
|
||||
this.ondragend();
|
||||
}
|
||||
},
|
||||
ondrop(event) {
|
||||
this.ondragend();
|
||||
if (event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
if ($('#only-updates input').checked) {
|
||||
$('#only-updates input').click();
|
||||
}
|
||||
importFromFile({file: event.dataTransfer.files[0]});
|
||||
}
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
function importFromFile({fileTypeFilter, file} = {}) {
|
||||
return new Promise(resolve => {
|
||||
|
@ -41,7 +86,7 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
importFromString(text) :
|
||||
getOwnTab().then(tab => {
|
||||
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
||||
return API.installUsercss({direct: true, tab})
|
||||
return API.openUsercssInstallPage({direct: true, tab})
|
||||
.then(() => URL.revokeObjectURL(tab.url));
|
||||
})
|
||||
).then(numStyles => {
|
||||
|
@ -56,19 +101,14 @@ function importFromFile({fileTypeFilter, file} = {}) {
|
|||
}
|
||||
|
||||
|
||||
function importFromString(jsonString, oldStyles) {
|
||||
if (!oldStyles) {
|
||||
return API.getStyles().then(styles => importFromString(jsonString, styles));
|
||||
function importFromString(jsonString) {
|
||||
const json = tryJSONparse(jsonString);
|
||||
if (!Array.isArray(json)) {
|
||||
return Promise.reject(new Error('the backup is not a valid JSON file'));
|
||||
}
|
||||
const json = tryJSONparse(jsonString) || [];
|
||||
if (typeof json.slice !== 'function') {
|
||||
json.length = 0;
|
||||
}
|
||||
const oldStylesById = new Map(
|
||||
oldStyles.map(style => [style.id, style]));
|
||||
const oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
|
||||
let oldStyles;
|
||||
let oldStylesById;
|
||||
let oldStylesByName;
|
||||
const stats = {
|
||||
added: {names: [], ids: [], legend: 'importReportLegendAdded'},
|
||||
unchanged: {names: [], ids: [], legend: 'importReportLegendIdentical'},
|
||||
|
@ -78,31 +118,25 @@ function importFromString(jsonString, oldStyles) {
|
|||
invalid: {names: [], legend: 'importReportLegendInvalid'},
|
||||
};
|
||||
|
||||
let index = 0;
|
||||
let lastRenderTime = performance.now();
|
||||
const renderQueue = [];
|
||||
const RENDER_NAP_TIME_MAX = 1000; // ms
|
||||
const RENDER_QUEUE_MAX = 50; // number of styles
|
||||
const SAVE_OPTIONS = {reason: 'import', notify: false};
|
||||
|
||||
return new Promise(proceed);
|
||||
|
||||
function proceed(resolve) {
|
||||
while (index < json.length) {
|
||||
const item = json[index++];
|
||||
const info = analyze(item);
|
||||
return API.getAllStyles().then(styles => {
|
||||
// make a copy of the current database, that may be used when we want to
|
||||
// undo
|
||||
oldStyles = styles;
|
||||
oldStylesById = new Map(
|
||||
oldStyles.map(style => [style.id, style]));
|
||||
oldStylesByName = json.length && new Map(
|
||||
oldStyles.map(style => [style.name.trim(), style]));
|
||||
return Promise.all(json.map((item, i) => {
|
||||
const info = analyze(item, i);
|
||||
if (info) {
|
||||
// using saveStyle directly since json was parsed in background page context
|
||||
return API.saveStyle(Object.assign(item, SAVE_OPTIONS))
|
||||
.then(style => account({style, info, resolve}));
|
||||
}
|
||||
}
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
renderQueue.length = 0;
|
||||
done(resolve);
|
||||
return API.importStyle(item)
|
||||
.then(style => updateStats(style, info));
|
||||
}
|
||||
}));
|
||||
})
|
||||
.then(done);
|
||||
|
||||
function analyze(item) {
|
||||
function analyze(item, index) {
|
||||
if (typeof item !== 'object' ||
|
||||
!item ||
|
||||
!item.name ||
|
||||
|
@ -146,17 +180,7 @@ function importFromString(jsonString, oldStyles) {
|
|||
.some(field => oldStyle[field] && oldStyle[field] === newStyle[field]);
|
||||
}
|
||||
|
||||
function account({style, info, resolve}) {
|
||||
renderQueue.push(style);
|
||||
if (performance.now() - lastRenderTime > RENDER_NAP_TIME_MAX
|
||||
|| renderQueue.length > RENDER_QUEUE_MAX) {
|
||||
renderQueue.forEach(style => handleUpdate(style, {reason: 'import'}));
|
||||
setTimeout(scrollElementIntoView, 0, $('#style-' + renderQueue.pop().id));
|
||||
renderQueue.length = 0;
|
||||
lastRenderTime = performance.now();
|
||||
}
|
||||
setTimeout(proceed, 0, resolve);
|
||||
const {oldStyle, metaEqual, codeEqual} = info;
|
||||
function updateStats(style, {oldStyle, metaEqual, codeEqual}) {
|
||||
if (!oldStyle) {
|
||||
stats.added.names.push(style.name);
|
||||
stats.added.ids.push(style.id);
|
||||
|
@ -176,12 +200,11 @@ function importFromString(jsonString, oldStyles) {
|
|||
stats.metaOnly.ids.push(style.id);
|
||||
}
|
||||
|
||||
function done(resolve) {
|
||||
function done() {
|
||||
const numChanged = stats.metaAndCode.names.length +
|
||||
stats.metaOnly.names.length +
|
||||
stats.codeOnly.names.length +
|
||||
stats.added.names.length;
|
||||
Promise.resolve(numChanged && API.refreshAllTabs()).then(() => {
|
||||
const report = Object.keys(stats)
|
||||
.filter(kind => stats[kind].names.length)
|
||||
.map(kind => {
|
||||
|
@ -205,13 +228,13 @@ function importFromString(jsonString, oldStyles) {
|
|||
contents: report.length ? report : t('importReportUnchanged'),
|
||||
buttons: [t('confirmClose'), numChanged && t('undo')],
|
||||
onshow: bindClick,
|
||||
}).then(({button}) => {
|
||||
})
|
||||
.then(({button}) => {
|
||||
if (button === 1) {
|
||||
undo();
|
||||
}
|
||||
});
|
||||
resolve(numChanged);
|
||||
});
|
||||
return Promise.resolve(numChanged);
|
||||
}
|
||||
|
||||
function undo() {
|
||||
|
@ -222,23 +245,16 @@ function importFromString(jsonString, oldStyles) {
|
|||
...stats.added.ids,
|
||||
];
|
||||
let tasks = Promise.resolve();
|
||||
let tasksUI = Promise.resolve();
|
||||
for (const id of newIds) {
|
||||
tasks = tasks.then(() => API.deleteStyle({id, notify: false}));
|
||||
tasksUI = tasksUI.then(() => handleDelete(id));
|
||||
tasks = tasks.then(() => API.deleteStyle(id));
|
||||
const oldStyle = oldStylesById.get(id);
|
||||
if (oldStyle) {
|
||||
Object.assign(oldStyle, SAVE_OPTIONS);
|
||||
tasks = tasks.then(() => API.saveStyle(oldStyle));
|
||||
tasksUI = tasksUI.then(() => handleUpdate(oldStyle, {reason: 'import'}));
|
||||
tasks = tasks.then(() => API.importStyle(oldStyle));
|
||||
}
|
||||
}
|
||||
// taskUI is superfast and updates style list only in this page,
|
||||
// which should account for 99.99999999% of cases, supposedly
|
||||
return tasks
|
||||
.then(tasksUI)
|
||||
.then(API.refreshAllTabs)
|
||||
.then(() => messageBox({
|
||||
return tasks.then(() => messageBox({
|
||||
title: t('importReportUndoneTitle'),
|
||||
contents: newIds.length + ' ' + t('importReportUndone'),
|
||||
buttons: [t('confirmClose')],
|
||||
|
@ -273,8 +289,8 @@ function importFromString(jsonString, oldStyles) {
|
|||
}
|
||||
|
||||
|
||||
$('#file-all-styles').onclick = () => {
|
||||
API.getStyles().then(styles => {
|
||||
function exportToFile() {
|
||||
API.getAllStyles().then(styles => {
|
||||
// https://crbug.com/714373
|
||||
document.documentElement.appendChild(
|
||||
$create('iframe', {
|
||||
|
@ -313,47 +329,4 @@ $('#file-all-styles').onclick = () => {
|
|||
const yyyy = today.getFullYear();
|
||||
return `stylus-${yyyy}-${mm}-${dd}${STYLUS_BACKUP_FILE_EXT}`;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
$('#unfile-all-styles').onclick = () => {
|
||||
importFromFile({fileTypeFilter: STYLUS_BACKUP_FILE_EXT});
|
||||
};
|
||||
|
||||
Object.assign(document.body, {
|
||||
ondragover(event) {
|
||||
const hasFiles = event.dataTransfer.types.includes('Files');
|
||||
event.dataTransfer.dropEffect = hasFiles || event.target.type === 'search' ? 'copy' : 'none';
|
||||
this.classList.toggle('dropzone', hasFiles);
|
||||
if (hasFiles) {
|
||||
event.preventDefault();
|
||||
clearTimeout(this.fadeoutTimer);
|
||||
this.classList.remove('fadeout');
|
||||
}
|
||||
},
|
||||
ondragend() {
|
||||
animateElement(this, {className: 'fadeout', removeExtraClasses: ['dropzone']}).then(() => {
|
||||
this.style.animationDuration = '';
|
||||
});
|
||||
},
|
||||
ondragleave(event) {
|
||||
try {
|
||||
// in Firefox event.target could be XUL browser and hence there is no permission to access it
|
||||
if (event.target === this) {
|
||||
this.ondragend();
|
||||
}
|
||||
} catch (e) {
|
||||
this.ondragend();
|
||||
}
|
||||
},
|
||||
ondrop(event) {
|
||||
this.ondragend();
|
||||
if (event.dataTransfer.files.length) {
|
||||
event.preventDefault();
|
||||
if ($('#only-updates input').checked) {
|
||||
$('#only-updates input').click();
|
||||
}
|
||||
importFromFile({file: event.dataTransfer.files[0]});
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* global installed */
|
||||
/* global installed onDOMready $create debounce $ scrollElementIntoView
|
||||
animateElement */
|
||||
'use strict';
|
||||
|
||||
onDOMready().then(() => {
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
/*
|
||||
global messageBox getStyleWithNoCode retranslateCSS
|
||||
global filtersSelector filterAndAppend urlFilterParam showFiltersStats
|
||||
global checkUpdate handleUpdateInstalled
|
||||
global objectDiff
|
||||
global configDialog
|
||||
global sorter
|
||||
global messageBox getStyleWithNoCode
|
||||
filterAndAppend urlFilterParam showFiltersStats
|
||||
checkUpdate handleUpdateInstalled
|
||||
objectDiff
|
||||
configDialog
|
||||
sorter msg prefs API onDOMready $ $$ $create template setupLivePrefs
|
||||
URLS enforceInputRange t tWordBreak formatDate
|
||||
getOwnTab getActiveTab openURL animateElement sessionStorageHash debounce
|
||||
scrollElementIntoView CHROME VIVALDI FIREFOX
|
||||
*/
|
||||
'use strict';
|
||||
|
||||
|
@ -23,7 +26,6 @@ const newUI = {
|
|||
},
|
||||
};
|
||||
newUI.renderClass();
|
||||
requestAnimationFrame(usePrefsDuringPageLoad);
|
||||
|
||||
const TARGET_TYPES = ['domains', 'urls', 'urlPrefixes', 'regexps'];
|
||||
const GET_FAVICON_URL = 'https://www.google.com/s2/favicons?domain=';
|
||||
|
@ -32,23 +34,38 @@ const OWN_ICON = chrome.runtime.getManifest().icons['16'];
|
|||
const handleEvent = {};
|
||||
|
||||
Promise.all([
|
||||
API.getStyles({omitCode: !BG}),
|
||||
API.getAllStyles(true),
|
||||
urlFilterParam && API.searchDB({query: 'url:' + urlFilterParam}),
|
||||
onDOMready().then(initGlobalEvents),
|
||||
Promise.all([
|
||||
onDOMready(),
|
||||
prefs.initializing,
|
||||
])
|
||||
.then(() => {
|
||||
initGlobalEvents();
|
||||
if (!VIVALDI) {
|
||||
$$('#header select').forEach(el => el.adjustWidth());
|
||||
}
|
||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||
const btn = $('#manage-shortcuts-button');
|
||||
btn.classList.remove('chromium-only');
|
||||
btn.onclick = API.optionsCustomizeHotkeys;
|
||||
}
|
||||
}),
|
||||
]).then(args => {
|
||||
showStyles(...args);
|
||||
});
|
||||
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'styleUpdated':
|
||||
case 'styleAdded':
|
||||
handleUpdate(msg.style, msg);
|
||||
API.getStyle(msg.style.id, true)
|
||||
.then(style => handleUpdate(style, msg));
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
handleDelete(msg.id);
|
||||
handleDelete(msg.style.id);
|
||||
break;
|
||||
case 'styleApply':
|
||||
case 'styleReplaceAll':
|
||||
|
@ -96,8 +113,7 @@ function initGlobalEvents() {
|
|||
setupLivePrefs();
|
||||
sorter.init();
|
||||
|
||||
$$('[id^="manage.newUI"]')
|
||||
.forEach(el => (el.oninput = (el.onchange = switchUI)));
|
||||
prefs.subscribe(['manage.newUI'], () => switchUI());
|
||||
|
||||
switchUI({styleOnly: true});
|
||||
|
||||
|
@ -119,7 +135,7 @@ function showStyles(styles = [], matchUrlIds) {
|
|||
const sorted = sorter.sort({
|
||||
styles: styles.map(style => ({
|
||||
style,
|
||||
name: style.name.toLocaleLowerCase() + '\n' + style.name,
|
||||
name: (style.name || '').toLocaleLowerCase() + '\n' + style.name,
|
||||
})),
|
||||
});
|
||||
let index = 0;
|
||||
|
@ -195,7 +211,7 @@ function createStyleElement({style, name}) {
|
|||
};
|
||||
}
|
||||
const parts = createStyleElement.parts;
|
||||
const configurable = style.usercssData && Object.keys(style.usercssData.vars).length > 0;
|
||||
const configurable = style.usercssData && style.usercssData.vars && Object.keys(style.usercssData.vars).length > 0;
|
||||
parts.checker.checked = style.enabled;
|
||||
parts.nameLink.textContent = tWordBreak(style.name);
|
||||
parts.nameLink.href = parts.editLink.href = parts.editHrefBase + style.id;
|
||||
|
@ -395,10 +411,7 @@ Object.assign(handleEvent, {
|
|||
},
|
||||
|
||||
toggle(event, entry) {
|
||||
API.saveStyle({
|
||||
id: entry.styleId,
|
||||
enabled: this.matches('.enable') || this.checked,
|
||||
});
|
||||
API.toggleStyle(entry.styleId, this.matches('.enable') || this.checked);
|
||||
},
|
||||
|
||||
check(event, entry) {
|
||||
|
@ -410,8 +423,7 @@ Object.assign(handleEvent, {
|
|||
event.preventDefault();
|
||||
const json = entry.updatedCode;
|
||||
json.id = entry.styleId;
|
||||
json.reason = 'update';
|
||||
API[json.usercssData ? 'saveUsercss' : 'saveStyle'](json);
|
||||
API[json.usercssData ? 'installUsercss' : 'installStyle'](json);
|
||||
},
|
||||
|
||||
delete(event, entry) {
|
||||
|
@ -426,7 +438,7 @@ Object.assign(handleEvent, {
|
|||
})
|
||||
.then(({button}) => {
|
||||
if (button === 0) {
|
||||
API.deleteStyle({id});
|
||||
API.deleteStyle(id);
|
||||
}
|
||||
});
|
||||
},
|
||||
|
@ -510,10 +522,7 @@ Object.assign(handleEvent, {
|
|||
|
||||
|
||||
function handleUpdate(style, {reason, method} = {}) {
|
||||
if (reason === 'editPreview') return;
|
||||
// the style was toggled and refreshAllTabs() sent a mini-notification,
|
||||
// but we've already processed 'styleUpdated' sent directly from notifyAllTabs()
|
||||
if (!style.sections) return;
|
||||
if (reason === 'editPreview' || reason === 'editPreviewEnd') return;
|
||||
let entry;
|
||||
let oldEntry = $(ENTRY_ID_PREFIX + style.id);
|
||||
if (oldEntry && method === 'styleUpdated') {
|
||||
|
@ -638,7 +647,7 @@ function switchUI({styleOnly} = {}) {
|
|||
const missingFavicons = newUI.enabled && newUI.favicons && !$('.applies-to img');
|
||||
if (changed.enabled || (missingFavicons && !createStyleElement.parts)) {
|
||||
installed.textContent = '';
|
||||
API.getStyles().then(showStyles);
|
||||
API.getAllStyles(true).then(showStyles);
|
||||
return;
|
||||
}
|
||||
if (changed.targets) {
|
||||
|
@ -662,7 +671,8 @@ function onVisibilityChange() {
|
|||
// assuming other changes aren't important enough to justify making a complicated DOM sync
|
||||
case 'visible':
|
||||
if (sessionStorage.justEditedStyleId) {
|
||||
API.getStyles({id: sessionStorage.justEditedStyleId}).then(([style]) => {
|
||||
API.getStyle(Number(sessionStorage.justEditedStyleId), true)
|
||||
.then(style => {
|
||||
handleUpdate(style, {method: 'styleUpdated'});
|
||||
});
|
||||
delete sessionStorage.justEditedStyleId;
|
||||
|
@ -685,30 +695,3 @@ function highlightEditedStyle() {
|
|||
requestAnimationFrame(() => scrollElementIntoView(entry));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
function usePrefsDuringPageLoad() {
|
||||
for (const id of Object.getOwnPropertyNames(prefs.readOnlyValues)) {
|
||||
const value = prefs.readOnlyValues[id];
|
||||
if (value !== true) continue;
|
||||
const el = document.getElementById(id) ||
|
||||
id.includes('expanded') && $(`details[data-pref="${id}"]`);
|
||||
if (!el) continue;
|
||||
if (el.type === 'checkbox') {
|
||||
el.checked = value;
|
||||
} else if (el.localName === 'details') {
|
||||
el.open = value;
|
||||
} else {
|
||||
el.value = value;
|
||||
}
|
||||
}
|
||||
if (!VIVALDI) {
|
||||
$$('#header select').forEach(el => el.adjustWidth());
|
||||
}
|
||||
|
||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||
const btn = $('#manage-shortcuts-button');
|
||||
btn.classList.remove('chromium-only');
|
||||
btn.onclick = API.optionsCustomizeHotkeys;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* exported objectDiff */
|
||||
'use strict';
|
||||
|
||||
function objectDiff(first, second, path = '') {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global installed */
|
||||
/* global messageBox */
|
||||
/* global installed messageBox t $ $create prefs */
|
||||
/* exported sorter */
|
||||
'use strict';
|
||||
|
||||
const sorter = (() => {
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
/* global messageBox */
|
||||
/* global ENTRY_ID_PREFIX, newUI */
|
||||
/* global filtersSelector, filterAndAppend, sorter */
|
||||
/* global messageBox ENTRY_ID_PREFIX newUI filtersSelector filterAndAppend
|
||||
sorter $ $$ $create API onDOMready scrollElementIntoView t chromeLocal */
|
||||
/* exported handleUpdateInstalled */
|
||||
'use strict';
|
||||
|
||||
onDOMready().then(() => {
|
||||
|
|
|
@ -24,23 +24,27 @@
|
|||
],
|
||||
"background": {
|
||||
"scripts": [
|
||||
"js/promisify.js",
|
||||
"js/messaging.js",
|
||||
"js/msg.js",
|
||||
"js/storage-util.js",
|
||||
"js/sections-equal.js",
|
||||
"background/storage-dummy.js",
|
||||
"background/storage.js",
|
||||
"js/sections-util.js",
|
||||
"js/worker-util.js",
|
||||
"js/prefs.js",
|
||||
"js/script-loader.js",
|
||||
"js/usercss.js",
|
||||
"js/cache.js",
|
||||
"background/db.js",
|
||||
"background/style-manager.js",
|
||||
"background/navigator-util.js",
|
||||
"background/icon-util.js",
|
||||
"background/background.js",
|
||||
"background/usercss-helper.js",
|
||||
"background/style-via-api.js",
|
||||
"background/search-db.js",
|
||||
"background/update.js",
|
||||
"background/refresh-all-tabs.js",
|
||||
"background/openusercss-api.js",
|
||||
"vendor/semver-bundle/semver.js",
|
||||
"vendor-overwrites/colorpicker/colorconverter.js"
|
||||
"vendor/semver-bundle/semver.js"
|
||||
]
|
||||
},
|
||||
"commands": {
|
||||
|
@ -58,7 +62,7 @@
|
|||
"run_at": "document_start",
|
||||
"all_frames": true,
|
||||
"match_about_blank": true,
|
||||
"js": ["content/apply.js"]
|
||||
"js": ["js/promisify.js", "js/msg.js", "js/prefs.js", "content/apply.js"]
|
||||
},
|
||||
{
|
||||
"matches": ["http://userstyles.org/*", "https://userstyles.org/*"],
|
||||
|
@ -111,5 +115,11 @@
|
|||
"options_ui": {
|
||||
"page": "options.html",
|
||||
"chrome_style": false
|
||||
},
|
||||
"applications": {
|
||||
"gecko": {
|
||||
"id": "{7a7a4a92-a2a0-41d1-9fd7-1e92480d612d}",
|
||||
"strict_min_version": "53"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
/* global focusAccessibility */
|
||||
/* global moveFocus */
|
||||
/* global focusAccessibility moveFocus $ $create t tHTML animateElement */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
|
|
@ -21,6 +21,8 @@
|
|||
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/promisify.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/storage-util.js" async></script>
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global messageBox */
|
||||
/* global messageBox msg setupLivePrefs enforceInputRange
|
||||
$ $$ $create $createLink
|
||||
FIREFOX OPERA CHROME URLS openURL prefs t API ignoreChromeError */
|
||||
'use strict';
|
||||
|
||||
setupLivePrefs();
|
||||
|
@ -21,7 +23,7 @@ if (!FIREFOX && !OPERA && CHROME < 3343) {
|
|||
|
||||
if (FIREFOX && 'update' in (chrome.commands || {})) {
|
||||
$('[data-cmd="open-keyboard"]').classList.remove('chromium-only');
|
||||
chrome.runtime.onMessage.addListener(msg => {
|
||||
msg.onExtension(msg => {
|
||||
if (msg.method === 'optionsCustomizeHotkeys') {
|
||||
customizeHotkeys();
|
||||
}
|
||||
|
@ -57,7 +59,7 @@ document.onclick = e => {
|
|||
|
||||
case 'reset':
|
||||
$$('input')
|
||||
.filter(input => input.id in prefs.readOnlyValues)
|
||||
.filter(input => input.id in prefs.defaults)
|
||||
.forEach(input => prefs.reset(input.id));
|
||||
break;
|
||||
|
||||
|
|
|
@ -18,6 +18,8 @@
|
|||
"stylelint-bundle": "^8.0.0",
|
||||
"stylus-lang-bundle": "^0.54.5",
|
||||
"updates": "^5.1.2",
|
||||
"web-ext": "^2.9.1",
|
||||
"usercss-meta": "^0.8.1",
|
||||
"webext-tx-fix": "^0.3.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
@ -30,6 +32,7 @@
|
|||
"update-transifex": "tx push -s",
|
||||
"update-vendor": "node tools/update-libraries.js && node tools/update-codemirror-themes.js",
|
||||
"update-versions": "node tools/update-versions",
|
||||
"zip": "npm run update-versions && node tools/zip.js"
|
||||
"zip": "npm run update-versions && node tools/zip.js",
|
||||
"start": "web-ext run"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -151,10 +151,12 @@
|
|||
<link rel="stylesheet" href="manage/config-dialog.css">
|
||||
<script src="manage/config-dialog.js"></script>
|
||||
|
||||
<script src="js/promisify.js"></script>
|
||||
<script src="js/dom.js"></script>
|
||||
<script src="js/messaging.js"></script>
|
||||
<script src="js/localization.js"></script>
|
||||
<script src="js/prefs.js"></script>
|
||||
<script src="js/msg.js"></script>
|
||||
<script src="content/apply.js"></script>
|
||||
|
||||
<link rel="stylesheet" href="popup/popup.css">
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
/* global applyOnMessage installed */
|
||||
/* global $ $$ API debounce $create t */
|
||||
'use strict';
|
||||
|
||||
// eslint-disable-next-line no-var
|
||||
var hotkeys = (() => {
|
||||
/* exported hotkeys */
|
||||
const hotkeys = (() => {
|
||||
const entries = document.getElementsByClassName('entry');
|
||||
let togglablesShown;
|
||||
let togglables;
|
||||
|
@ -101,11 +101,9 @@ var hotkeys = (() => {
|
|||
entry = typeof entry === 'string' ? $('#' + entry) : entry;
|
||||
if (!match && $('.checker', entry).checked !== enable || entry.classList.contains(match)) {
|
||||
results.push(entry.id);
|
||||
task = task.then(() => API.saveStyle({
|
||||
id: entry.styleId,
|
||||
enabled: enable,
|
||||
notify: false,
|
||||
})).then(() => {
|
||||
task = task
|
||||
.then(() => API.toggleStyle(entry.styleId, enable))
|
||||
.then(() => {
|
||||
entry.classList.toggle('enabled', enable);
|
||||
entry.classList.toggle('disabled', !enable);
|
||||
$('.checker', entry).checked = enable;
|
||||
|
|
|
@ -301,6 +301,10 @@ html[style*="border"] .entry:nth-child(11):before {
|
|||
box-sizing: border-box;
|
||||
cursor: pointer;
|
||||
font-size: 90%;
|
||||
display: none;
|
||||
}
|
||||
.regexp-partial .regexp-problem-indicator {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.regexp-partial .actions,
|
||||
|
@ -311,6 +315,8 @@ html[style*="border"] .entry:nth-child(11):before {
|
|||
#regexp-explanation {
|
||||
position: fixed;
|
||||
background-color: white;
|
||||
top: 50%;
|
||||
transform: translateY(-50%);
|
||||
left: 0;
|
||||
right: 0;
|
||||
padding: .5rem;
|
||||
|
|
225
popup/popup.js
225
popup/popup.js
|
@ -1,4 +1,7 @@
|
|||
/* global configDialog hotkeys */
|
||||
/* global configDialog hotkeys onTabReady msg
|
||||
getActiveTab FIREFOX getTabRealURL URLS API onDOMready $ $$ prefs CHROME
|
||||
setupLivePrefs template t $create tWordBreak animateElement
|
||||
tryJSONparse debounce */
|
||||
|
||||
'use strict';
|
||||
|
||||
|
@ -11,44 +14,45 @@ const ENTRY_ID_PREFIX = '#' + ENTRY_ID_PREFIX_RAW;
|
|||
|
||||
toggleSideBorders();
|
||||
|
||||
getActiveTab().then(tab =>
|
||||
getActiveTab()
|
||||
.then(tab =>
|
||||
FIREFOX && tab.url === 'about:blank' && tab.status === 'loading'
|
||||
? getTabRealURLFirefox(tab)
|
||||
: getTabRealURL(tab)
|
||||
).then(url => Promise.all([
|
||||
)
|
||||
.then(url => Promise.all([
|
||||
(tabURL = URLS.supported(url) ? url : '') &&
|
||||
API.getStyles({
|
||||
matchUrl: tabURL,
|
||||
omitCode: !BG,
|
||||
}),
|
||||
API.getStylesByUrl(tabURL),
|
||||
onDOMready().then(initPopup),
|
||||
])).then(([styles]) => {
|
||||
showStyles(styles);
|
||||
});
|
||||
]))
|
||||
.then(([results]) => {
|
||||
if (!results) {
|
||||
// unsupported URL;
|
||||
return;
|
||||
}
|
||||
showStyles(results.map(r => Object.assign(r.data, r)));
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
chrome.runtime.onMessage.addListener(onRuntimeMessage);
|
||||
msg.onExtension(onRuntimeMessage);
|
||||
|
||||
prefs.subscribe(['popup.stylesFirst'], (key, stylesFirst) => {
|
||||
const actions = $('body > .actions');
|
||||
const before = stylesFirst ? actions : actions.nextSibling;
|
||||
document.body.insertBefore(installed, before);
|
||||
});
|
||||
prefs.subscribe(['popupWidth'], (key, value) => setPopupWidth(value));
|
||||
prefs.subscribe(['popup.borders'], (key, value) => toggleSideBorders(value));
|
||||
|
||||
function onRuntimeMessage(msg) {
|
||||
switch (msg.method) {
|
||||
case 'styleAdded':
|
||||
case 'styleUpdated':
|
||||
if (msg.reason === 'editPreview') return;
|
||||
handleUpdate(msg.style);
|
||||
if (msg.reason === 'editPreview' || msg.reason === 'editPreviewEnd') return;
|
||||
handleUpdate(msg);
|
||||
break;
|
||||
case 'styleDeleted':
|
||||
handleDelete(msg.id);
|
||||
break;
|
||||
case 'prefChanged':
|
||||
if ('popup.stylesFirst' in msg.prefs) {
|
||||
const stylesFirst = msg.prefs['popup.stylesFirst'];
|
||||
const actions = $('body > .actions');
|
||||
const before = stylesFirst ? actions : actions.nextSibling;
|
||||
document.body.insertBefore(installed, before);
|
||||
} else if ('popupWidth' in msg.prefs) {
|
||||
setPopupWidth(msg.prefs.popupWidth);
|
||||
} else if ('popup.borders' in msg.prefs) {
|
||||
toggleSideBorders(msg.prefs['popup.borders']);
|
||||
}
|
||||
handleDelete(msg.style.id);
|
||||
break;
|
||||
}
|
||||
dispatchEvent(new CustomEvent(msg.method, {detail: msg}));
|
||||
|
@ -111,11 +115,12 @@ function initPopup() {
|
|||
}
|
||||
|
||||
getActiveTab().then(function ping(tab, retryCountdown = 10) {
|
||||
sendMessage({tabId: tab.id, method: 'ping', frameId: 0}, pong => {
|
||||
msg.sendTab(tab.id, {method: 'ping'}, {frameId: 0})
|
||||
.catch(() => false)
|
||||
.then(pong => {
|
||||
if (pong) {
|
||||
return;
|
||||
}
|
||||
ignoreChromeError();
|
||||
// FF and some Chrome forks (e.g. CentBrowser) implement tab-on-demand
|
||||
// so we'll wait a bit to handle popup being invoked right after switching
|
||||
if (retryCountdown > 0 && (
|
||||
|
@ -227,93 +232,92 @@ function showStyles(styles) {
|
|||
const container = document.createDocumentFragment();
|
||||
styles.forEach(style => createStyleElement({style, container}));
|
||||
installed.appendChild(container);
|
||||
setTimeout(detectSloppyRegexps, 100, styles);
|
||||
|
||||
API.getStyles({
|
||||
matchUrl: tabURL,
|
||||
strictRegexp: false,
|
||||
omitCode: true,
|
||||
}).then(unscreenedStyles => {
|
||||
for (const style of unscreenedStyles) {
|
||||
if (!styles.find(({id}) => id === style.id)) {
|
||||
createStyleElement({style, check: true});
|
||||
}
|
||||
}
|
||||
window.dispatchEvent(new Event('showStyles:done'));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function createStyleElement({
|
||||
style,
|
||||
check = false,
|
||||
container = installed,
|
||||
}) {
|
||||
const entry = template.style.cloneNode(true);
|
||||
let entry = $(ENTRY_ID_PREFIX + style.id);
|
||||
if (!entry) {
|
||||
entry = template.style.cloneNode(true);
|
||||
entry.setAttribute('style-id', style.id);
|
||||
Object.assign(entry, {
|
||||
id: ENTRY_ID_PREFIX_RAW + style.id,
|
||||
styleId: style.id,
|
||||
styleIsUsercss: Boolean(style.usercssData),
|
||||
className: entry.className + ' ' + (style.enabled ? 'enabled' : 'disabled'),
|
||||
onmousedown: handleEvent.maybeEdit,
|
||||
styleMeta: style
|
||||
});
|
||||
|
||||
const checkbox = $('.checker', entry);
|
||||
Object.assign(checkbox, {
|
||||
id: ENTRY_ID_PREFIX_RAW + style.id,
|
||||
checked: style.enabled,
|
||||
// title: t('exclusionsPopupTip'),
|
||||
onclick: handleEvent.toggle,
|
||||
// oncontextmenu: handleEvent.openExcludeMenu
|
||||
});
|
||||
|
||||
const editLink = $('.style-edit-link', entry);
|
||||
Object.assign(editLink, {
|
||||
href: editLink.getAttribute('href') + style.id,
|
||||
onclick: handleEvent.openLink,
|
||||
});
|
||||
|
||||
const styleName = $('.style-name', entry);
|
||||
Object.assign(styleName, {
|
||||
htmlFor: ENTRY_ID_PREFIX_RAW + style.id,
|
||||
onclick: handleEvent.name,
|
||||
});
|
||||
styleName.checkbox = checkbox;
|
||||
styleName.appendChild(document.createTextNode(style.name));
|
||||
setTimeout((el = styleName) => {
|
||||
if (el.scrollWidth > el.clientWidth + 1) {
|
||||
el.title = el.textContent;
|
||||
}
|
||||
});
|
||||
|
||||
styleName.appendChild(document.createTextNode(' '));
|
||||
const config = $('.configure', entry);
|
||||
if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) {
|
||||
config.href = style.url;
|
||||
config.target = '_blank';
|
||||
config.title = t('configureStyleOnHomepage');
|
||||
config.dataset.sendMessage = JSON.stringify({method: 'openSettings'});
|
||||
$('use', config).attributes['xlink:href'].nodeValue = '#svg-icon-config-uso';
|
||||
} else if (!style.usercssData || !Object.keys(style.usercssData.vars || {}).length) {
|
||||
config.style.display = 'none';
|
||||
}
|
||||
|
||||
$('.enable', entry).onclick = handleEvent.toggle;
|
||||
$('.disable', entry).onclick = handleEvent.toggle;
|
||||
$('.delete', entry).onclick = handleEvent.delete;
|
||||
$('.configure', entry).onclick = handleEvent.configure;
|
||||
|
||||
if (check) detectSloppyRegexps([style]);
|
||||
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
||||
indicator.appendChild(document.createTextNode('!'));
|
||||
indicator.onclick = handleEvent.indicator;
|
||||
$('.main-controls', entry).appendChild(indicator);
|
||||
}
|
||||
|
||||
const oldElement = $(ENTRY_ID_PREFIX + style.id);
|
||||
if (oldElement && oldElement.contains(document.activeElement)) {
|
||||
// preserve the focused element inside
|
||||
const {className} = document.activeElement;
|
||||
oldElement.parentNode.replaceChild(entry, oldElement);
|
||||
// we're not using $() since className may contain multiple tokens
|
||||
const el = entry.getElementsByClassName(className)[0];
|
||||
if (el) el.focus();
|
||||
} else if (oldElement) {
|
||||
oldElement.parentNode.replaceChild(entry, oldElement);
|
||||
style = Object.assign(entry.styleMeta, style);
|
||||
|
||||
entry.classList.toggle('disabled', !style.enabled);
|
||||
entry.classList.toggle('enabled', style.enabled);
|
||||
$('.checker', entry).checked = style.enabled;
|
||||
|
||||
const styleName = $('.style-name', entry);
|
||||
styleName.lastChild.textContent = style.name;
|
||||
setTimeout(() => {
|
||||
styleName.title = entry.styleMeta.sloppy ?
|
||||
t('styleNotAppliedRegexpProblemTooltip') :
|
||||
styleName.scrollWidth > styleName.clientWidth + 1 ?
|
||||
styleName.textContent : '';
|
||||
});
|
||||
|
||||
const config = $('.configure', entry);
|
||||
if (!style.usercssData && style.updateUrl && style.updateUrl.includes('?') && style.url) {
|
||||
config.href = style.url;
|
||||
} else {
|
||||
config.removeAttribute('href');
|
||||
}
|
||||
config.style.display =
|
||||
!style.usercssData && config.href ||
|
||||
style.usercssData && Object.keys(style.usercssData.vars || {}).length ?
|
||||
'' : 'none';
|
||||
|
||||
entry.classList.toggle('not-applied', style.excluded || style.sloppy);
|
||||
entry.classList.toggle('regexp-partial', style.sloppy);
|
||||
|
||||
if (entry.parentNode !== container) {
|
||||
container.appendChild(entry);
|
||||
}
|
||||
}
|
||||
|
@ -337,10 +341,10 @@ Object.assign(handleEvent, {
|
|||
toggle(event) {
|
||||
// when fired on checkbox, prevent the parent label from seeing the event, see #501
|
||||
event.stopPropagation();
|
||||
API.saveStyle({
|
||||
id: handleEvent.getClickedStyleId(event),
|
||||
enabled: this.matches('.enable') || this.checked,
|
||||
});
|
||||
API.toggleStyle(
|
||||
handleEvent.getClickedStyleId(event),
|
||||
this.matches('.enable') || this.checked
|
||||
);
|
||||
},
|
||||
|
||||
delete(event) {
|
||||
|
@ -367,14 +371,14 @@ Object.assign(handleEvent, {
|
|||
className: 'lights-on',
|
||||
onComplete: () => (box.dataset.display = false),
|
||||
});
|
||||
if (ok) API.deleteStyle({id});
|
||||
if (ok) API.deleteStyle(id);
|
||||
}
|
||||
},
|
||||
|
||||
configure(event) {
|
||||
const {styleId, styleIsUsercss} = handleEvent.getClickedStyleElement(event);
|
||||
if (styleIsUsercss) {
|
||||
API.getStyles({id: styleId}).then(([style]) => {
|
||||
API.getStyle(styleId, true).then(style => {
|
||||
hotkeys.setState(false);
|
||||
configDialog(style).then(() => {
|
||||
hotkeys.setState(true);
|
||||
|
@ -436,12 +440,18 @@ Object.assign(handleEvent, {
|
|||
|
||||
openURLandHide(event) {
|
||||
event.preventDefault();
|
||||
const message = tryJSONparse(this.dataset.sendMessage);
|
||||
getActiveTab()
|
||||
.then(activeTab => API.openURL({
|
||||
url: this.href || this.dataset.href,
|
||||
index: activeTab.index + 1,
|
||||
message: tryJSONparse(this.dataset.sendMessage),
|
||||
index: activeTab.index + 1
|
||||
}))
|
||||
.then(tab => {
|
||||
if (message) {
|
||||
return onTabReady(tab)
|
||||
.then(() => msg.sendTab(tab.id, message));
|
||||
}
|
||||
})
|
||||
.then(window.close);
|
||||
},
|
||||
|
||||
|
@ -457,24 +467,31 @@ Object.assign(handleEvent, {
|
|||
});
|
||||
|
||||
|
||||
function handleUpdate(style) {
|
||||
if ($(ENTRY_ID_PREFIX + style.id)) {
|
||||
createStyleElement({style, check: true});
|
||||
function handleUpdate({style, reason}) {
|
||||
if (!tabURL) return;
|
||||
|
||||
fetchStyle()
|
||||
.then(style => {
|
||||
if (!style) {
|
||||
return;
|
||||
}
|
||||
if ($(ENTRY_ID_PREFIX + style.id)) {
|
||||
createStyleElement({style});
|
||||
return;
|
||||
}
|
||||
if (!tabURL) return;
|
||||
// Add an entry when a new style for the current url is installed
|
||||
API.getStyles({
|
||||
matchUrl: tabURL,
|
||||
stopOnFirst: true,
|
||||
omitCode: true,
|
||||
}).then(([style]) => {
|
||||
if (style) {
|
||||
document.body.classList.remove('blocked');
|
||||
$$.remove('.blocked-info, #no-styles');
|
||||
createStyleElement({style, check: true});
|
||||
createStyleElement({style});
|
||||
})
|
||||
.catch(console.error);
|
||||
|
||||
function fetchStyle() {
|
||||
if (reason === 'toggle' && $(ENTRY_ID_PREFIX + style.id)) {
|
||||
return Promise.resolve(style);
|
||||
}
|
||||
return API.getStylesByUrl(tabURL, style.id)
|
||||
.then(([result]) => result && Object.assign(result.data, result));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
@ -485,32 +502,6 @@ function handleDelete(id) {
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
function detectSloppyRegexps(styles) {
|
||||
API.detectSloppyRegexps({
|
||||
matchUrl: tabURL,
|
||||
ids: styles.map(({id}) => id),
|
||||
}).then(results => {
|
||||
for (const {id, applied, skipped, hasInvalidRegexps} of results) {
|
||||
const entry = $(ENTRY_ID_PREFIX + id);
|
||||
if (!entry) continue;
|
||||
if (!applied) {
|
||||
entry.classList.add('not-applied');
|
||||
$('.style-name', entry).title = t('styleNotAppliedRegexpProblemTooltip');
|
||||
}
|
||||
if (skipped || hasInvalidRegexps) {
|
||||
entry.classList.toggle('regexp-partial', Boolean(skipped));
|
||||
entry.classList.toggle('regexp-invalid', Boolean(hasInvalidRegexps));
|
||||
const indicator = template.regexpProblemIndicator.cloneNode(true);
|
||||
indicator.appendChild(document.createTextNode(entry.skipped || '!'));
|
||||
indicator.onclick = handleEvent.indicator;
|
||||
$('.main-controls', entry).appendChild(indicator);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
function getTabRealURLFirefox(tab) {
|
||||
// wait for FF tab-on-demand to get a real URL (initially about:blank), 5 sec max
|
||||
return new Promise(resolve => {
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global tabURL handleEvent */
|
||||
/* global tabURL handleEvent $ $$ prefs template FIREFOX chromeLocal debounce
|
||||
$create t API tWordBreak formatDate tryCatch tryJSONparse LZString
|
||||
ignoreChromeError */
|
||||
'use strict';
|
||||
|
||||
window.addEventListener('showStyles:done', function _() {
|
||||
|
@ -135,7 +137,7 @@ window.addEventListener('showStyles:done', function _() {
|
|||
if (result) {
|
||||
result.installed = false;
|
||||
result.installedStyleId = -1;
|
||||
(BG || window).clearTimeout(result.pingbackTimer);
|
||||
window.clearTimeout(result.pingbackTimer);
|
||||
renderActionButtons($('#' + RESULT_ID_PREFIX + result.id));
|
||||
}
|
||||
});
|
||||
|
@ -287,14 +289,14 @@ window.addEventListener('showStyles:done', function _() {
|
|||
return;
|
||||
}
|
||||
const md5Url = UPDATE_URL.replace('%', result.id);
|
||||
API.getStyles({md5Url}).then(([installedStyle]) => {
|
||||
if (installedStyle) {
|
||||
API.styleExists({md5Url}).then(exist => {
|
||||
if (exist) {
|
||||
totalResults = Math.max(0, totalResults - 1);
|
||||
} else {
|
||||
processedResults.push(result);
|
||||
render();
|
||||
}
|
||||
setTimeout(processNextResult, !installedStyle && DELAY_AFTER_FETCHING_STYLES);
|
||||
setTimeout(processNextResult, !exist && DELAY_AFTER_FETCHING_STYLES);
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -529,7 +531,7 @@ window.addEventListener('showStyles:done', function _() {
|
|||
event.stopPropagation();
|
||||
const entry = this.closest('.search-result');
|
||||
saveScrollPosition(entry);
|
||||
API.deleteStyle({id: entry._result.installedStyleId})
|
||||
API.deleteStyle(entry._result.installedStyleId)
|
||||
.then(restoreScrollPosition);
|
||||
}
|
||||
|
||||
|
@ -555,9 +557,7 @@ window.addEventListener('showStyles:done', function _() {
|
|||
pingback(result);
|
||||
// show a 'config-on-homepage' icon in the popup
|
||||
style.updateUrl += settings.length ? '?' : '';
|
||||
// show a 'style installed' tooltip in the manager
|
||||
style.reason = 'install';
|
||||
return API.saveStyle(style);
|
||||
return API.installStyle(style);
|
||||
})
|
||||
.catch(reason => {
|
||||
const usoId = result.id;
|
||||
|
@ -581,7 +581,8 @@ window.addEventListener('showStyles:done', function _() {
|
|||
}
|
||||
|
||||
function pingback(result) {
|
||||
const wnd = BG || window;
|
||||
const wnd = window;
|
||||
// FIXME: move this to background page and create an API like installUSOStyle
|
||||
result.pingbackTimer = wnd.setTimeout(wnd.download, PINGBACK_DELAY,
|
||||
BASE_URL + '/styles/install/' + result.id + '?source=stylish-ch');
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/* global messageBox */
|
||||
/* global zip */
|
||||
/* global zip onDOMready */
|
||||
/* exported createZipFileFromText readZipFileFromBlob */
|
||||
'use strict';
|
||||
|
||||
onDOMready().then(() => {
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
/* exported getRedirectUrlAuthFlow launchWebAuthFlow */
|
||||
'use strict';
|
||||
|
||||
/**
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
/* global messageBox Dropbox createZipFileFromText readZipFileFromBlob launchWebAuthFlow getRedirectUrlAuthFlow importFromString resolve */
|
||||
/* global messageBox Dropbox createZipFileFromText readZipFileFromBlob
|
||||
launchWebAuthFlow getRedirectUrlAuthFlow importFromString resolve
|
||||
$ $create t chromeLocal API getOwnTab */
|
||||
'use strict';
|
||||
|
||||
const DROPBOX_API_KEY = 'zg52vphuapvpng9';
|
||||
|
@ -66,7 +68,7 @@ $('#sync-dropbox-export').onclick = () => {
|
|||
// file deleted with success, get styles and create a file
|
||||
.then(() => {
|
||||
messageProgressBar({title: title, text: t('gettingStyles')});
|
||||
return API.getStyles().then(styles => JSON.stringify(styles, null, '\t'));
|
||||
return API.getAllStyles().then(styles => JSON.stringify(styles, null, '\t'));
|
||||
})
|
||||
// create zip file
|
||||
.then(stylesText => {
|
||||
|
@ -85,7 +87,7 @@ $('#sync-dropbox-export').onclick = () => {
|
|||
console.log(error);
|
||||
// saving file first time
|
||||
if (error.status === API_ERROR_STATUS_FILE_NOT_FOUND) {
|
||||
API.getStyles()
|
||||
API.getAllStyles()
|
||||
.then(styles => {
|
||||
messageProgressBar({title: title, text: t('gettingStyles')});
|
||||
return JSON.stringify(styles, null, '\t');
|
||||
|
@ -141,7 +143,7 @@ $('#sync-dropbox-import').onclick = () => {
|
|||
importFromString(text) :
|
||||
getOwnTab().then(tab => {
|
||||
tab.url = URL.createObjectURL(new Blob([text], {type: 'text/css'}));
|
||||
return API.installUsercss({direct: true, tab})
|
||||
return API.openUsercssInstallPage({direct: true, tab})
|
||||
.then(() => URL.revokeObjectURL(tab.url));
|
||||
})
|
||||
).then(numStyles => {
|
||||
|
|
|
@ -28,6 +28,9 @@ const files = {
|
|||
],
|
||||
'stylus-lang-bundle': [
|
||||
'stylus.min.js'
|
||||
],
|
||||
'usercss-meta': [
|
||||
'dist/usercss-meta.min.js → usercss-meta.min.js'
|
||||
]
|
||||
};
|
||||
|
||||
|
@ -35,7 +38,7 @@ async function updateReadme(lib) {
|
|||
const pkg = await fs.readJson(`${root}/node_modules/${lib}/package.json`);
|
||||
const file = `${root}/vendor/${lib}/README.md`;
|
||||
const txt = await fs.readFile(file, 'utf8');
|
||||
return fs.writeFile(file, txt.replace(/\bv[\d.]+[-\w]*\b/g, `v${pkg.version}`));
|
||||
return fs.writeFile(file, txt.replace(/\b([v@])[\d.]+[-\w]*\b/g, `$1${pkg.version}`));
|
||||
}
|
||||
|
||||
function isFolder(fileOrFolder) {
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
/* global CodeMirror colorConverter */
|
||||
/* global colorConverter $create debounce */
|
||||
/* exported colorMimicry */
|
||||
'use strict';
|
||||
|
||||
(window.CodeMirror ? window.CodeMirror.prototype : window).colorpicker = function () {
|
||||
|
|
|
@ -5505,3 +5505,5 @@ self.parserlib = (() => {
|
|||
|
||||
//endregion
|
||||
})();
|
||||
|
||||
self.parserlib.css.Tokens[self.parserlib.css.Tokens.COMMENT].hide = false;
|
||||
|
|
4
vendor/README.md
vendored
4
vendor/README.md
vendored
|
@ -9,7 +9,8 @@ Using this repo, run `npm install`... the latest versions of:
|
|||
* `less` (https://github.com/less/less.js) is installed.
|
||||
* `lz-string-unsafe` (https://github.com/openstyles/lz-string-unsafe) is installed.
|
||||
* `semver-bundle` (https://github.com/openstyles/semver-bundle) is installed.
|
||||
* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.<br><br>
|
||||
* `stylus-lang` (https://github.com/openstyles/stylus-lang-bundle) is installed.
|
||||
* `usercss-meta` (https://github.com/StylishThemes/parse-usercss) is installed.
|
||||
* The necessary build tools are installed; see `devDependencies` in the `package.json`.
|
||||
|
||||
## Running the build script
|
||||
|
@ -24,6 +25,7 @@ The following changes are made:
|
|||
* `lz-string-unsafe`: The compressed `lz-string-unsafe.min.js` file is copied directly into `vendor/lz-string-unsafe`.
|
||||
* `semver-bundle`: The `dist/semver.js` file is copied directly into `vendor/semver`.
|
||||
* `stylus-lang-bundle`: The `stylus.min.js` file is copied directly into `vendor/stylus-lang-bundle`.
|
||||
* `usercss-meta`: The `dist/usercss-meta.min.js` file is copied directly into `vendor/usercss-meta`.
|
||||
|
||||
## Creating the ZIP
|
||||
|
||||
|
|
22
vendor/usercss-meta/LICENSE
vendored
Normal file
22
vendor/usercss-meta/LICENSE
vendored
Normal file
|
@ -0,0 +1,22 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Original code: Copyright (c) Stylus team (github.com/openstyles/stylus)
|
||||
Modified code: Copyright (c) StylishThemes (github.com/StylishThemes/parse-usercss)
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in
|
||||
all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
||||
THE SOFTWARE.
|
5
vendor/usercss-meta/README.md
vendored
Normal file
5
vendor/usercss-meta/README.md
vendored
Normal file
|
@ -0,0 +1,5 @@
|
|||
## usercss-meta v0.8.1
|
||||
|
||||
usercss-meta installed via npm - source repo:
|
||||
|
||||
https://unpkg.com/usercss-meta@0.8.1/dist/usercss-meta.min.js
|
2
vendor/usercss-meta/usercss-meta.min.js
vendored
Normal file
2
vendor/usercss-meta/usercss-meta.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
Loading…
Reference in New Issue
Block a user