Compare commits

...

395 Commits

Author SHA1 Message Date
tophf
fc39f0d5a6 parserlib: dot-separated layer names 2022-10-19 22:47:19 +03:00
tophf
1725c0ecb9 show installed styles in popup finder
fixes #1488
2022-10-12 20:04:45 +03:00
tophf
79dff2775b add upDownKeyJumps option, remove left/right 2022-10-08 13:07:57 +03:00
tophf
b22bbaaec0 make <iframe> more evident 2022-10-08 13:05:40 +03:00
tophf
fff59ee12f use a self-explanatory "..." in #write-for-frames 2022-09-17 21:42:39 +03:00
tophf
540e2af62c micro-optimize colorConverter.parse
#hex, named colors: 15x faster
rgb() and other functions: 1.6x faster
2022-09-17 20:10:18 +03:00
tophf
0128489bbb unbork rgb colors with % 2022-09-17 01:29:11 +03:00
dependabot[bot]
a5b11ac687
Bump node-forge and web-ext (#1474) 2022-09-16 16:24:27 +03:00
Rob Garrison
5d3a9dccf3 1.5.28 2022-09-16 08:11:25 -05:00
tophf
09691a6362 add a comment for USO devs 2022-09-16 15:33:11 +03:00
tophf
62987fe5f8 update locales 2022-09-16 12:47:55 +03:00
tophf
405a93f9e5 no need for fse dep here 2022-09-16 12:47:37 +03:00
tophf
da4bdc6821 API should return something 2022-09-16 12:40:14 +03:00
tophf
d1f5468a81 fix USO install button 2022-09-16 12:39:46 +03:00
tophf
efc6d09d49 properly show "no styles found" 2022-09-15 17:14:13 +03:00
tophf
0bb0d32c29 simplify fitNameColumn
as it wasn't using average and an average is too small anyway
2022-09-15 13:05:00 +03:00
tophf
f5397b8aec simplify hover highlight to an underline 2022-09-15 12:57:01 +03:00
tophf
1594b4dcd8 help auto-hyphenation at word boundaries 2022-09-15 12:54:26 +03:00
tophf
527d7c0fbc add fake header before linking to USW 2022-09-11 20:44:19 +03:00
tophf
cda606e7cc tall applies-to toggle + simplify CSS 2022-09-08 00:20:25 +03:00
tophf
5cb30c8b69 simplify oldUI CSS 2022-09-07 23:57:19 +03:00
tophf
1b914a397e simplify hover animation: 50% less CPU 2022-09-06 21:25:13 +03:00
tophf
7b64deeb37 create templates on demand 2022-09-06 18:41:22 +03:00
tophf
9398857d93 clear searchMode along with search 2022-09-06 01:07:14 +03:00
tophf
b45825c015 preserve current URL params in openManage 2022-09-06 00:54:52 +03:00
tophf
3aae3f181a linkify numbers in option labels 2022-09-05 23:08:03 +03:00
tophf
48d90544f6 reuse setInputValue 2022-09-05 22:41:17 +03:00
tophf
ef998e423e add multi-column mode option 2022-09-04 22:50:27 +03:00
tophf
6979958908 use localization cache 2022-09-04 22:50:26 +03:00
tophf
4236eb4e29 shorten highlight animation
...because it was invisible 90% of time anyway
2022-09-04 22:50:26 +03:00
tophf
c351413c3f show Size column + simplify sorter 2022-09-03 20:08:24 +03:00
tophf
d91cf11366 parserlib: update forced-color-adjust 2022-09-02 09:12:32 +03:00
tophf
e49644f1c8 CodeMirror 5.65.8 2022-09-01 11:35:37 +03:00
tophf
15b59ae207 update locales 2022-09-01 11:35:37 +03:00
tophf
ad43560016 fix and simplify applyScrollInfo 2022-09-01 11:35:24 +03:00
tophf
c423025c5d postpone lazyScripts more 2022-09-01 10:37:29 +03:00
tophf
030621462c error.stack has no message in FF 2022-09-01 10:30:46 +03:00
tophf
bf3dd0318d don't apply global section to Stylus pages
unless it was intentionally targeted via url(),  url-prefix(), or regexp(). The regexp must contain the word "extension" without quotes.
2022-08-31 16:27:31 +03:00
tophf
3489b513c9 [autocomplete] find LESS vars 2022-08-30 10:31:21 +03:00
Rob Garrison
984fa6e425 1.5.27 2022-08-29 17:07:09 -05:00
tophf
6110cbee68 use /* */ for line comments in less/stylus 2022-08-28 22:48:49 +03:00
tophf
40ec2c000f [autocomplete] use parserlib's list of CSS props 2022-08-27 21:58:29 +03:00
tophf
9022f6b318 [autocomplete] add ":" to LESS props 2022-08-27 21:58:28 +03:00
tophf
c5667b0352 hide lint errors for LESS vars 2022-08-27 21:58:27 +03:00
tophf
5379f62c90
add year selector in popup search (#1411) 2022-08-27 21:57:14 +03:00
Rob Garrison
9d3fa1c5f9 1.5.26 2022-08-06 17:48:47 -05:00
tophf
906a5ca960 use license from stylelint-bundle 2022-08-06 23:35:32 +03:00
tophf
e1e351d956 report network failures in build-vendor 2022-08-06 23:35:01 +03:00
tophf
7a5569e9d5 stylelint 14.9.1 2022-08-06 23:20:29 +03:00
tophf
2d21bb1dd6 generate separate zip for FF with options_ui 2022-08-05 18:35:16 +03:00
tophf
8302c50d54 remove the unused identity permission 2022-08-05 18:34:38 +03:00
dependabot[bot]
27a878aed8
Bump moment from 2.29.2 to 2.29.4 (#1463) 2022-08-05 17:56:52 +03:00
tophf
f9e6df116f update deps 2022-08-05 17:50:31 +03:00
tophf
a832b51d9c update locales 2022-08-05 17:41:08 +03:00
tophf
b88a978843 improve orphan check + cosmetics 2022-08-03 23:58:55 +03:00
tophf
685bf1fa3e
fix USO site installation (#1461) 2022-08-03 22:37:04 +03:00
tophf
6995483ec0 parserlib: cosmetics/simplifications 2022-08-03 22:35:01 +03:00
tophf
6ff5f17140 assume bg is ready if messaging succeeded 2022-08-03 22:20:41 +03:00
tophf
7a1045b45a keep valid url path chars intact 2022-07-30 20:48:43 +03:00
tophf
079e7e50f1 retry API on browser startup automatically 2022-07-25 19:28:25 +03:00
tophf
00b732177f speed up regex for block comments 2022-07-23 23:54:24 +03:00
tophf
484ff24950 parserlib: container query 2022-07-17 12:38:07 +03:00
tophf
31177f1017 parserlib: fix custom-ident and use it more 2022-07-17 12:38:07 +03:00
tophf
e406e2b5dc fetch all indexes before showing "not found" 2022-07-07 17:17:33 +03:00
tophf
bd2b435781 parserlib: simplify/flatten _keyframe 2022-07-01 10:29:43 +03:00
tophf
b742ed2c65 parserlib: forbid default in custom-ident 2022-07-01 10:26:46 +03:00
tophf
d68433c867 fix usage of chrome.windows in android 2022-07-01 07:55:11 +03:00
tophf
a10003ee80 parserlib: has() selector 2022-06-30 13:40:39 +03:00
tophf
3fcd4f8027 code cosmetics 2022-06-27 12:42:27 +03:00
tophf
9364ef585f
fix nextcloud WebDAV csrf error (#1448) 2022-06-27 12:41:39 +03:00
tophf
6e0cfb7d0f fix infinite loop in readUnknownSym 2022-06-26 18:13:13 +03:00
tophf
91501cce19 parserlib: simplified media query L4 2022-06-24 21:09:14 +03:00
tophf
95a4514f15 parserlib: object-overflow, object-view-box 2022-06-24 08:25:33 +03:00
tophf
3d6b55e53e error may be an object 2022-06-23 21:29:52 +03:00
tophf
88a0a3858c update locales 2022-06-23 21:21:56 +03:00
tophf
685ee625ee update codemirror 5.65.6 2022-06-23 21:21:56 +03:00
tophf
944a7f23ef update stylus-lang-bundle 0.58.1 2022-06-23 21:16:02 +03:00
tophf
d78329d295
add transifex languages info 2022-06-07 21:08:39 +03:00
tophf
b5fd8b63dc new Vivaldi uses vivExtData 2022-06-04 19:20:20 +03:00
tophf
a77996df60 window type may be 'app' 2022-06-04 18:02:41 +03:00
tophf
2e1961bdf7 messageBox.element is nulled in close() 2022-06-03 18:41:25 +03:00
tophf
cd88aff733 hide undefined label in colorpicker in FF 2022-06-02 21:52:49 +03:00
Rob Garrison
0f3ba30066 1.5.25 2022-05-31 12:11:33 -05:00
tophf
cda6e4fb9e recreate idb with missing stores 2022-05-31 16:08:03 +03:00
tophf
97520be9b2 create missing idb store 2022-05-31 15:32:01 +03:00
Rob Garrison
30af48a69a 1.5.24 2022-05-30 09:24:45 -05:00
Rob Garrison
b33b5ba624 Update locales 2022-05-30 09:11:47 -05:00
tophf
ad5c90e240 actually set INJECTED 2022-04-24 00:21:25 +03:00
tophf
3522a53ad4 limit section's height to viewport 2022-04-11 14:05:02 +03:00
tophf
79ada00a01 don't show reset-name on local classic styles 2022-04-11 09:57:38 +03:00
tophf
a34fe5f400 parserlib: add webkit-optimize-contrast 2022-04-09 09:00:08 +03:00
dependabot[bot]
ffb72cc656
Bump moment from 2.29.1 to 2.29.2 (#1424) 2022-04-09 08:59:30 +03:00
tophf
a74ae5ac4f extract popup search + show error + CtrlF 2022-04-05 13:49:48 +03:00
tophf
3907116328 getEventKeyName: uppercase for single letters 2022-04-05 13:46:37 +03:00
tophf
fd42f2d365 update icon on bfcache navigation 2022-04-04 10:11:41 +03:00
tophf
537372dffa colorpicker: add hwb colors 2022-04-04 09:52:01 +03:00
tophf
f54d145bf5 parserlib: add font-palette, hwb 2022-04-04 09:52:00 +03:00
tophf
2efb6c5cd1 strip source mapping from vendor js 2022-03-31 14:00:12 +03:00
tophf
ea388ea9a7 add [x] as usercss to write-style in popup 2022-03-31 12:27:11 +03:00
tophf
24aaf2fdb8
improve power-off switch (#1412)
* show it in manager/editor when active
* reflect the state in the label when active
2022-03-31 12:25:37 +03:00
tophf
963e58f237 unbork 57a7939b 2022-03-27 22:42:35 +03:00
tophf
57a7939b5e show livepreview error in compact mode 2022-03-27 18:47:52 +03:00
tophf
ec5a685ee2 limit details width in compact mode 2022-03-27 18:47:52 +03:00
dependabot[bot]
b21497c1b2
Bump minimist from 1.2.5 to 1.2.6 (#1420) 2022-03-26 09:12:05 +03:00
tophf
ba1f10b865 fix/simplify editor loader
fixes #1419
2022-03-24 17:52:26 +03:00
Rob Garrison
7ab95cce33 1.5.23 2022-03-19 17:15:43 -05:00
dependabot[bot]
0a84d802d3
Bump node-fetch from 2.6.6 to 2.6.7 (#1415) 2022-03-17 09:21:29 +03:00
dependabot[bot]
2c42f40acb
Bump nanoid from 3.1.30 to 3.3.1 (#1416) 2022-03-17 09:21:17 +03:00
tophf
ee0826e538 update CodeMirror 5.65.2 2022-03-17 09:18:59 +03:00
tophf
411d7f7b95 update locales 2022-03-17 09:18:04 +03:00
tophf
614dce0e8c transition patch is redundant in Chrome 93+ 2022-03-08 23:38:51 +03:00
tophf
619f771b4a strip www for greasyfork search 2022-03-07 01:49:14 +03:00
tophf
9ed550c882 fix popup find error 2022-03-06 03:02:41 +03:00
tophf
6d721fe7a5 actually hide invisible "?" favicons 2022-03-05 22:37:31 +03:00
tophf
d9ad0fec3c parserlib: new values for mix-blend-mode 2022-03-04 19:07:52 +03:00
tophf
d623aa679f properly update hash when closing UI, part 2
for the history log
2022-03-04 08:10:46 +03:00
tophf
9e009726c5 properly update hash when closing UI 2022-03-03 05:23:34 +03:00
tophf
bcc98d913b fix/simplify dom::collapsible
now it observes the `open` attribute as the `click` was too fragile due to dependency on timing
2022-03-02 02:54:25 +03:00
tophf
2e31cae71e instant match hl same as other editors
also enable it when the find dialog is open because selection may be different
2022-02-28 10:18:15 +03:00
tophf
b80c3e2f73 enable activeline highlight with selection 2022-02-28 10:15:11 +03:00
tophf
37baf8a2c6 reuse deepCopy 2022-02-26 10:32:58 +03:00
tophf
b4f10cb296 fix popup buttons in styles-last mode 2022-02-25 03:12:43 +03:00
tophf
8fc6c8bcde cosmetics 2022-02-25 01:26:50 +03:00
tophf
29cafd619c extract initBadFavs 2022-02-25 00:43:21 +03:00
tophf
bd9d51308a simplify context menu + shorten titles 2022-02-24 07:53:14 +03:00
tophf
d2a99b5be1 simplify getProxy 2022-02-24 07:29:11 +03:00
tophf
e567b6d56d use the public defaults 2022-02-24 07:29:11 +03:00
tophf
329d0caac1 avoid flicker/delay when opening manager 2022-02-23 09:07:30 +03:00
tophf
51e9b09f52 load favicons earlier + show bads as ? 2022-02-23 06:11:28 +03:00
tophf
f5b2fe2fc6 properly show another split-btn + tiny popup 2022-02-23 00:17:14 +03:00
tophf
38ac974d8a remember and skip bad favicons 2022-02-22 01:39:03 +03:00
tophf
5ff664f9b9 simplify/generalize close-on-esc 2022-02-21 23:45:42 +03:00
tophf
b75e2cb56b add @-moz-document indent to beautifier 2022-02-21 23:35:49 +03:00
tophf
d162928fb9 right-click to expand all applies-to targets 2022-02-21 07:12:11 +03:00
tophf
ce7137c54d preload nearby favicons 2022-02-21 07:11:09 +03:00
tophf
62974751ff limit disable-all area to its text 2022-02-21 03:18:12 +03:00
tophf
e0c9c13856 explicitly hyphenate domains in popup 2022-02-21 02:36:03 +03:00
tophf
6ff68aaa74 simplify conditional css in popup 2022-02-21 02:24:34 +03:00
tophf
bd0e8273c1 add USA/USW/US/GF splitter to Find styles 2022-02-21 02:01:50 +03:00
tophf
268c7b758b give history log a navigatable #hash 2022-02-20 19:57:11 +03:00
tophf
38ddf5d79d simplify and speed up init in manage/edit 2022-02-20 19:26:26 +03:00
tophf
90a71c0afc deflicker name element in new usercss 2022-02-20 18:34:51 +03:00
tophf
dd1f30e6aa fix popup regression on blocked page 2022-02-19 17:20:32 +03:00
tophf
b804e39de6 workaround for crbug.com/1288447 2022-02-19 17:20:26 +03:00
tophf
16f7e19915 attenuate dark scrollbar colors 2022-02-19 17:08:05 +03:00
tophf
ad969fca6a
simplify css/html in popup (#1408)
+ reuse global checkbox-wrapper more
2022-02-19 11:18:14 +03:00
tophf
9d64e9ba54 wait for real prefs on bg startup
+ convert msg.sendXXX to async
2022-02-19 10:05:47 +03:00
tophf
cc3c85be58 fix update when using embedded meta 2022-02-19 06:48:55 +03:00
tophf
f7bbfd6ead fix mozdoc widget when duplicating section 2022-02-18 22:29:02 +03:00
tophf
dff0c133b4 hide 'manage site styles' if blocked 2022-02-18 08:02:47 +03:00
tophf
1364d3a8ba add colorScheme.isDark() 2022-02-18 06:03:52 +03:00
tophf
bef70f6db3 fix light mode in dark system, properly 2022-02-18 04:40:23 +03:00
tophf
77562ecd8d fix light mode in dark system 2022-02-18 04:35:36 +03:00
tophf
9fbf641571 fix some colors in dark theme 2022-02-18 03:59:00 +03:00
tophf
a58af7f816 speed up and simplify i18n 2022-02-18 03:49:16 +03:00
tophf
8d3e01e05a
shuffle and tidy up options (#1406)
* move updates/sync to the top, theme to the bottom
* remove font override
* replace 'Back to manage' with 'Close'
* add a note for the built-in shortcuts UI in FF
- update button
+ confirm reset
* one button to connect/disconnect
* shorten ids
* simplify/extract sync js
* reuse :invalid style
2022-02-18 03:47:22 +03:00
tophf
c9b8593830 fix setupLivePrefs regression 2022-02-17 19:51:58 +03:00
tophf
6fb2727f6b restore button's svg color in light mode 2022-02-17 03:35:51 +03:00
tophf
1721bad7d8 reuse radio-wrapper for visual uniformity 2022-02-17 03:26:20 +03:00
tophf
944c44ffb8 use default sorter pref for resiliency 2022-02-17 03:12:11 +03:00
tophf
adef93c3ed speed up setupLivePrefs 2022-02-17 03:12:11 +03:00
tophf
b7cfbe6e66
use color palette and enable a simple dark theme (#1405)
* add 'auto' iconset and use it by default
* expose `data-ui-theme` on html

Co-authored-by: narcolepticinsomniac <therealdoctorgonzo@gmail.com>
2022-02-17 03:10:59 +03:00
tophf
dd38856eda
scrollable details + sticky header (#1400)
* shorten section labels in lint report
* `sectioned` class on html for sectioned editor
* fix scrollElementIntoView
2022-02-14 22:19:20 +03:00
tophf
225a2cec31 unbork installer, regressed in 81a5acfd 2022-02-13 16:19:58 +03:00
tophf
2e021f6ac9 preserve style element name during livePreview
also strip _ from methods as we don't use such convention
2022-02-13 04:36:33 +03:00
tophf
fcad0ee523 avoid double handling of keyboard event in rerouter
...and thus fix the bug with restoring keyboard focus to codemirror when closing color picker
2022-02-12 21:31:35 +03:00
tophf
f9a2b9de35 fix/simplify i18n in templates
fix: double-translation, regressed in cc7eba9
2022-02-12 01:44:52 +03:00
tophf
648b295ef2 parserlib: handle layer statements anywhere 2022-02-12 01:18:03 +03:00
tophf
81a5acfda3 .user.css in virtual style urls
benefits:
* styles are listed directly under `Stylus` in devtools source file tree
* easy to differentiate from site's css
2022-02-11 00:08:20 +03:00
tophf
10de02f04d
expose style name (#1403) 2022-02-10 21:28:47 +03:00
tophf
b86cb6a36c reuse standard style of applies-to in usercss
+ shorten the Test button label
2022-02-10 21:25:09 +03:00
tophf
290a0f99d2 keep internal tooltips in js 2022-02-10 21:09:49 +03:00
tophf
ae6f7024ce identify by UUID when importing 2022-02-10 09:53:02 +03:00
tophf
de5eb32d2d update locales 2022-02-09 05:03:47 +03:00
tophf
c29048c366 parserlib: units L4, scrollbar-gutter 2022-02-08 15:13:35 +03:00
tophf
6b87f7840f parserlib: text-emphasis-color 2022-02-04 13:26:47 +03:00
tophf
421526d60b parserlib: cascade layers
+ simplify Parser.ACTIONS
2022-02-04 02:28:29 +03:00
tophf
9b0db09b6c
use hardcoded redirect_uri fallback (#1399) 2022-02-03 13:06:29 +03:00
tophf
bab1ba17a9 use standard web cursor for controls in options
+ increase clickable area of (i) notes
2022-02-02 13:46:55 +03:00
tophf
50da5f1966 intercept click on a note properly
regressed in cc7eba97
2022-02-02 13:38:05 +03:00
tophf
2e2c6765b5
keep router buffer on tab reload (#1393) 2022-01-31 17:27:58 +03:00
tophf
b2bc18c3d5 restore usercss section numbering and applies-to gaps
regressed in f9e11f58
2022-01-31 01:27:30 +03:00
tophf
84f4262176 fix rmDir deprecation warning 2022-01-30 14:23:02 +03:00
tophf
799423629a use standard cursor for svg inside buttons 2022-01-30 05:51:09 +03:00
tophf
f282623ef7
fix split-btn with popup.stylesFirst (#1394) 2022-01-30 05:50:13 +03:00
tophf
513ced6d57 lazy-load favicons 2022-01-29 18:53:49 +03:00
tophf
d048c480c3 require FF >= 55 2022-01-29 18:19:21 +03:00
tophf
f966b2ef96 allow non-object values in db 2022-01-29 16:39:30 +03:00
tophf
c597303692 skip deleted styles in order 2022-01-29 03:41:47 +03:00
tophf
26b75e77b3
separate storage for order + important styles (#1390)
* use Proxy for `db`
* don't merge arrays in deepMerge by default
* extract sync and cache from styleMan
2022-01-29 02:54:56 +03:00
tophf
46785b0550 use pageshow/pagehide events
...to avoid DOM update/scroll on switching tabs
2022-01-29 02:46:12 +03:00
tophf
554d121c45 remove duplicate toggleDataset call 2022-01-29 00:32:11 +03:00
tophf
f9e11f5806
align 'Applies-to' and actions (#1392) 2022-01-28 15:52:55 +03:00
tophf
5529cbec2b fix and simplify editDeleteText context menu
* enable it on inputs added by the user later
* enable it in all of our pages
2022-01-28 03:11:25 +03:00
tophf
5253c863b9 remove the unused collapsible from options 2022-01-27 15:27:10 +03:00
tophf
60834f7bd6 check userAgentData when available 2022-01-27 15:21:02 +03:00
tophf
8afab0eaeb fix first click on regexp tester 2022-01-27 06:03:37 +03:00
tophf
4c4a319b33 use duckduckgo favicons 2022-01-27 05:10:17 +03:00
tophf
ea7c26ce71 toLoad is always an array 2022-01-26 14:53:17 +03:00
tophf
f740686cb5 fix icon when opening an unstyled Options frame
...and the containing page is styled
2022-01-24 22:49:27 +03:00
tophf
e54178a43c
draft recovery in editor (#1388)
+ use toolbox::clamp() more
2022-01-23 12:44:25 +03:00
tophf
60f59e9f06 fix split button [re]activation 2022-01-22 22:21:14 +03:00
tophf
0e31557748 show a button to reset the template 2022-01-22 12:29:35 +03:00
tophf
45eeedbe97 strip only dummy sections from the template 2022-01-22 12:06:57 +03:00
tophf
b4b135826c use css borders for split button triangle 2022-01-22 12:06:29 +03:00
tophf
ce9e74e2a0 add per-style setting for autoupdate checks 2022-01-21 19:45:56 +03:00
tophf
f041f2265a fix $ for detached nodes 2022-01-21 19:45:56 +03:00
tophf
98da86f816 update CodeMirror, stylelint, deps 2022-01-20 17:43:31 +03:00
tophf
4c696fefbc show search results for the faster index first
+ refresh after the slower index is downloaded
+ use an existing `f` instead of `isUsw`
2022-01-20 00:45:51 +03:00
tophf
6a07ad0f56 split button for manage::export, popup::manage 2022-01-19 16:13:53 +03:00
tophf
b692cf9608 show source code in build error when updating
fixes #891
2022-01-19 14:46:09 +03:00
tophf
be43bf3f23 force no-cache in update checker 2022-01-19 14:46:08 +03:00
tophf
cc7eba979e
save-as-template button in editor (#1385)
+ keep i18n attributes to use them as CSS selectors
+ reduce flicker when creating a new style
+ split button
2022-01-19 14:45:45 +03:00
tophf
594ca3520c
actually use the global font everywhere (#1384) 2022-01-19 00:25:11 +03:00
tophf
42d6e2f2af parserlib: reimplement d9a80623 properly 2022-01-18 20:24:41 +03:00
tophf
f7729eac15 remove unused files 2022-01-18 17:23:51 +03:00
tophf
0705392fb2
fix/deduplicate/simplify installer html/css/js (#1383) 2022-01-18 16:39:33 +03:00
tophf
936f5b40d2 pixel-align (i) icon 2022-01-18 01:10:45 +03:00
tophf
9136631bc6 manager: trivial alignments
* removed `Filters` left shift
* removed inadvertent extra padding in compact mode
* fixed `Backup` label in verbose languages
* flexible width of header blocks in compact mode
2022-01-18 01:10:45 +03:00
tophf
14e0a418bf create URL fallback only when necessary 2022-01-18 01:10:45 +03:00
tophf
12eb243610 replace installer if another file is drag'n'dropped 2022-01-16 20:22:42 +03:00
tophf
956b60e1ef fix header width when maximizing initially small window 2022-01-16 15:45:55 +03:00
tophf
0c20ef5d17 refactor a complex ternary 2022-01-14 18:47:09 +03:00
eight
ddc09f3511
Add: a draggable list to customize injection order (#1364)
+ implement messageBox.close()
+ fix require() with root urls in /dir/page.html
+ limit messageBox focus shift to config-dialog
+ flatten vendor dirs and simplify build-vendor:
  + replace the unicode symbol with ASCII `->`
  + flatten dirs by default to simplify writing the rules and improve their readability
  + rename and sort functions in the order they run
  + use `node-fetch` instead of the gargantuan `make-fetch-happen`
  + use `glob` which is already installed by other deps

Co-authored-by: tophf <tophf@gmx.com>
2022-01-14 15:44:48 +03:00
tophf
8ddeef221b
resizable header panel (#1378) 2022-01-13 12:47:37 +03:00
tophf
cce483dd22 update locales 2022-01-12 01:30:46 +03:00
tophf
d9a80623e1 parserlib: fix endless loop on incomplete tokens
regressed in 048c2672
fixes #1381
2022-01-11 21:18:37 +03:00
tophf
ca5402136d removed an unused string 2022-01-11 12:21:44 +03:00
tophf
5791ae7b05 removed an unused duplicate string 2022-01-11 11:46:20 +03:00
tophf
68dc584749 deduplicate editor.useSavedStyle
...and call it when saving in sectioned editor (regressed in ba9d904c)
2022-01-10 21:31:15 +03:00
tophf
6e4fc3236e confirm reload in sectioned editor 2022-01-10 19:12:29 +03:00
tophf
ba9d904ccc don't rebuild editor DOM on save, fixes #1380 2022-01-10 17:57:46 +03:00
tophf
51e56110e8 parserlib: color-scheme, scrollbar-gutter, transforms 2022-01-10 07:32:02 +03:00
tophf
906508832b
show style settings in a dialog (#1374)
+ simplify css/html
+ save button and autosave checkbox just like in config-dialog
+ generalize can-close-on-esc
+ add `props` parameter to helpPopup.show
+ deduplicate usage of #help-popup id
+ uniform padding in popups
+ disambiguate style settings from editor options
2021-12-29 22:57:22 +03:00
tophf
8128100cef ensure editor window is visible
fixes #1375
2021-12-26 19:35:13 +03:00
tophf
6afb4dc634 fix toggling of newline in beautifier dialog 2021-12-25 22:17:35 +03:00
tophf
440395db9f export in compat mode on shift-click/right-click 2021-12-25 18:05:29 +03:00
tophf
3cdf526fa3
don't export redundant values (#1373)
+ implement proper check for same code in usercss so unchanged styles won't be unnecessarily imported
2021-12-25 13:08:38 +03:00
tophf
6b9cdf2bc2 extend drop zone to viewport in Chrome too 2021-12-22 20:05:42 +03:00
tophf
249196d414 don't add _usw to all styles 2021-12-22 19:49:21 +03:00
tophf
0ac01d2e22
add a callstack to errors in browser and msg (#1369) 2021-12-16 20:21:02 +03:00
eight
9d2854c272
Fix: compatibility with kiwi (#1368) 2021-12-16 16:04:22 +03:00
eight
f6e6a138db
Add: webdav sync (#1363) 2021-12-12 03:05:58 +03:00
eight
3ea7e45624
Change: stop revoking google token, change syncErrorRelogin message, recognize token manager errors as grant error (#1362)
* WIP: don't revoke google token, add TokenError

* Fix: stop suggesting disconnecting

* Add: recognize token error as grant error

* Change: sync immediately after re-login
2021-12-09 12:00:38 +08:00
eight
7e3c6f16e9
Fix: bump db-to-cloud, show detailed LockError (#1361)
* Bump db-to-cloud

* Fix: bump db-to-cloud, show detailed LockError

* Fix: used -> use

* Change: drop our retry code
2021-12-09 00:00:30 +08:00
eight
e23077a7ea
Add: support inclusions (#1359)
* Add: support inclusions

* Fix: refresh settings page after configuring in popup
2021-12-08 18:30:16 +08:00
tophf
9ab5369393
autosize textareas in style settings (#1360) 2021-12-07 14:43:21 +03:00
eight
9d1243073b
Add: style settings (#1358)
* Add: style settings

* Change: use radio instead of select for dark/light mode

* Change: x -> Delete

* Change: (in|ex)clusion messages

* Fix: avoid extra space when there is no rule

* Fix: UI in mobile

* Change: delete priority

* Change: use textarea for include/exclude, remove isCodeUpdated

* Fix: separate toggle

* Fix: minor

* Fix: remove codeIsUpdated in styleman
2021-12-07 12:44:49 +08:00
tophf
a59aab73fc enforce plain-text for styleInstallOverwrite
fixes #1357
2021-12-03 15:37:47 +03:00
eight
6c13db1468
Add: toggle dark/night mode styles automatically (#736)
* Add: color-scheme.js

* Add: handle color scheme

* Add: styleManager.setMeta

* Add: make setupLivePrefs work with radio

* Change: drop setupRadioButtons

* Add: UI for schemeSwitcher

* Add: prefer-scheme select in installation page

* Fix: add alarm listener

* Add: display excluded reason in popup

* Fix: rely on data-value-type instead of input name

* Fix: oldValue and newValue should have the same type

* Change: detect media change in content script

* Fix: duplicate capitalize

* Fix: minor

* Update web-ext

* Fix: valueAsNumber doesn't work for all inputs

* Fix: disable colorscheme selection after install

* Fix: API error
2021-12-03 00:49:03 +08:00
tophf
19ebeedf6a
add vars after @import in compiled code (#1348)
fixes #1347
2021-11-14 11:00:12 +03:00
tophf
b17eef4053 allow live-reload on localhost and updates on file://
...if file access is allowed
2021-11-02 18:15:26 +03:00
tophf
7ad3f94697 unbork and simplify applies-to css 2021-10-24 00:11:09 +03:00
tophf
8b67eb885d fix compact usercss height + vcenter fixed header 2021-10-23 23:56:38 +03:00
tophf
707d8bb1a7 parserlib: update transforms 2021-10-21 23:25:32 +03:00
tophf
0034dcb941 preserve installationUrl 2021-10-12 20:26:57 +03:00
tophf
992b89f0eb more stuff in fixKnownProblems
fixes #1342
2021-10-12 20:20:54 +03:00
tophf
37a174b092 CodeMirror 5.63.3 2021-10-12 16:05:21 +03:00
tophf
4243815349 update external urls in manager 2021-10-04 09:46:33 +03:00
tophf
11028bd635 update locales 2021-10-01 17:49:38 +03:00
tophf
dc18320b60 codemirror 5.63.1 2021-10-01 17:48:04 +03:00
tophf
eaef854bcf show iframe "+" only if there's a unique url 2021-09-24 19:18:57 +03:00
tophf
e5807a7823 fix tooltip for iframe "+" 2021-09-24 19:14:34 +03:00
tophf
818031a86b preserve iframe's sender.url 2021-09-24 11:05:55 +03:00
tophf
fce049a911
fix iframe visibility detection (#1336) 2021-09-24 09:43:10 +03:00
tophf
cf0ecbfd4a support EyeDropper API in color picker
Chrome 95+
https://wicg.github.io/eyedropper-api/
2021-09-24 09:40:49 +03:00
tophf
997f1fe8de avoid FOUC for dark themes in applies-to widget 2021-09-21 22:51:18 +03:00
tophf
94f727dc09 avoid startup flicker in applies-to widget 2021-09-21 14:24:17 +03:00
tophf
05bc4301ad 349a8c38 follow-up: stretch editor in compact mode 2021-09-21 10:50:50 +03:00
tophf
58ad6f64d6 focus editor when clicking section name 2021-09-21 10:12:58 +03:00
tophf
349a8c3878 graduate from Quirks mode 2021-09-21 09:43:30 +03:00
tophf
4a0f74764a update regexp report when tab is removed 2021-09-11 15:58:59 +03:00
tophf
dfd8f7a1b1
fix regexp report overflow (#1332) 2021-09-11 15:53:10 +03:00
tophf
59cac7a469 fix USO-archive site search link 2021-09-08 19:13:27 +03:00
tophf
e7d5fff736 use normal warning if typo candidate is empty 2021-09-07 12:42:06 +03:00
tophf
048c267266 parserlib: check @font-face + shorten grammar tokens 2021-09-07 12:36:36 +03:00
tophf
a56676122f
csslint: add "known-pseudos" rule enabled by default (#1328) 2021-09-02 00:12:29 +03:00
tophf
701c30a6c8 fix links in linter help dialog 2021-08-31 17:22:10 +03:00
tophf
a08dd2800d fix objectDiff 2021-08-30 21:15:44 +03:00
Rob Garrison
31dd972c7a 1.5.22 2021-08-30 08:38:44 -05:00
tophf
d84e1bddab update locales 2021-08-30 14:07:47 +03:00
Gusted
d181298e56
Improve issue template (#1319)
* Improve issue template

- Add a CSS section(so I don't have to see those comments about posting CSS etc).
- Make it into more sections and separate them.
- Add notes to each section.

* Apply feedback

* Add restart/profile note

* tweaks

Co-authored-by: narcolepticinsomniac <therealdoctorgonzo@gmail.com>
2021-08-28 15:44:29 -04:00
tophf
2c31dc2af8
installer: show action buttons for installed styles (#1322) 2021-08-26 22:10:08 +03:00
tophf
abced603b4 parserlib: fix ieFunction
fixes #1325
2021-08-26 22:03:30 +03:00
tophf
a5a8d97767 csslint: fix bugs in 'box-model' and 'ids' 2021-08-26 21:01:53 +03:00
tophf
12764baacb parserlib: accept uso-var as ident/string 2021-08-24 14:38:19 +03:00
tophf
b0ed85c5ea use second metablock's @updateURL in USO-archive styles
fixes #1323
2021-08-23 16:27:31 +03:00
tophf
2eeab2c1be openusercss is on an indefinite hiatus 2021-08-22 21:09:47 +03:00
tophf
bfd0d03871 fix #1321 2021-08-22 20:30:09 +03:00
tophf
8ac43fca31
keep the original color format in usercss @var (#1320) 2021-08-22 11:57:35 +03:00
Rob Garrison
32c5e17d08 1.5.21 2021-08-20 08:01:40 -05:00
tophf
95205c70e5 parserlib: check if prop with vars inside is known 2021-08-20 13:03:23 +03:00
tophf
8e36e0277f parserlib: reuse global keywords 2021-08-20 13:01:56 +03:00
tophf
9d6542a39d CodeMirror 5.62.3 2021-08-20 12:16:24 +03:00
tophf
c08747202a fix CSS linting in new CodeMirror
fixes #1317
2021-08-17 22:03:10 +03:00
tophf
6927fa5d70 update translations 2021-08-16 16:59:20 +03:00
Rob Garrison
46b0c9005d 1.5.20 2021-08-15 12:48:52 -05:00
tophf
9722554b3b
show action hints on preview-less search results (#1313)
+ use CSS to control visibility of action buttons
+ avoid mutating DOM unnecessarily in toggleDataset()
2021-08-13 23:03:01 +03:00
Gusted
07291f9486
Add comment to await (#1311) 2021-08-13 13:40:24 +03:00
Gusted
598735fc7b
Fix Firefox's android icon (#1312) 2021-08-13 08:28:50 +03:00
tophf
1e5f118d2d
update USO-archive urls (#1308) 2021-08-12 20:35:56 +03:00
tophf
49af723078 don't let wrapped text flow below the usw icon 2021-08-12 16:56:35 +03:00
tophf
304dcb1489
show installation error inline, allow retrying (#1309) 2021-08-12 16:44:02 +03:00
tophf
434e7ff6c6
rework/simplify external links (#1244)
* rework/simplify external links
* remove title from svg (i) icon as it randomly shows instead of the parent's title
* ensure original element isn't focused when showing modal
* center more help modals
2021-08-12 16:40:27 +03:00
Gusted
2dbccf71db
Prefer webp when available (#1306) 2021-08-12 16:02:48 +03:00
Gusted
ce2250a1f3
clarify purpose of unused rule (#1307) 2021-08-12 15:30:22 +03:00
tophf
50717465b9
show config for usercss vars in installer (#1302)
* simplify messageBox code
* also bind events correctly in case messageBox is called when a messageBox was already shown
2021-08-12 14:40:03 +03:00
tophf
91324a4a48 use center-dialog class for import&history
regressed in a2c8953e which centered the contents too
2021-08-06 19:50:36 +03:00
tophf
6798114196 add buttons to hotkey input, reset on Del/BackSpace 2021-08-06 16:01:55 +03:00
tophf
ba7b55c23d parserlib: add accent-color 2021-08-03 19:45:44 +03:00
tophf
295cb4541c reduce margins for closed details
to provide more space for sections/issues
2021-08-01 21:42:23 +03:00
tophf
b6cc6a09b9 detect typo in metadata when linting
makes use of https://github.com/openstyles/usercss-meta/pull/78
2021-08-01 19:41:52 +03:00
tophf
404efcecf9 unbork max-width for details in compact mode
regressed in 6650a371
2021-08-01 19:28:08 +03:00
tophf
55046ef68c custom version validator is redundant now
...thanks to 4913ba1d2c
2021-08-01 18:38:36 +03:00
tophf
f35bf6a2a5 typo 2021-07-30 17:28:35 +03:00
tophf
c34b054642
refactor usw content script (#1300)
* extract handlers to async functions
* limit event source to the current window
2021-07-30 17:04:15 +03:00
Gusted
e1b65ae21f
Send requested info (#1294) 2021-07-30 15:52:27 +03:00
tophf
6e591b0d52
patchCSP option: allow @import from any URL (#1297) 2021-07-30 15:45:27 +03:00
tophf
6650a37194
tidy up USW-related UI and code (#1285)
* shortened the title to "Publish" and fixed the compact mode
* made collapsed <details> share the same line in compact mode
* made hard-coded strings localizable
* IIFE modules instead of generically named globals
* unified and sorted the names of localized messages
* adjusted spacing of header items
* center auth popup to current window

Co-authored-by: Gusted <williamzijl7@hotmail.com>
2021-07-30 15:44:06 +03:00
tophf
23d86c53a7 properly account for scrollbar in applies-to widget 2021-07-30 15:38:38 +03:00
tophf
ebb5fcafbc show sections on compact->normal layout switch 2021-07-30 07:58:08 +03:00
tophf
39c51435ca avoid recursion when closing regexp tester 2021-07-30 07:37:33 +03:00
tophf
a46bb103c5 update usercss-meta 0.11.0
fixes #1220
2021-07-29 18:54:37 +03:00
tophf
02db1aab6f CM apparently fixed their formula for max-height 2021-07-26 09:48:16 +03:00
tophf
7a0ac57b06 CodeMirror 5.62.2 2021-07-21 15:32:01 +03:00
tophf
33ff2c8373 CodeMirror 5.62.0
skipping 5.62.1 due to a bug in its lint.js
2021-07-21 13:01:39 +03:00
tophf
2d9d0ad1f8 synchronize version 2021-07-21 13:00:27 +03:00
Gusted
61c7d4f08c
Don't rely on _isUswLinked (#1288) 2021-07-20 15:56:45 +03:00
Gusted
69ccdb0591
Show error to user when USw returns error (#1286)
Related USw commit: d4306f2f71

Co-authored-by: tophf <tophf@gmx.com>
2021-07-18 07:25:32 +03:00
Gusted
8abcf9e754
Fix handleSave and sections TOC (#1284) 2021-07-17 02:57:50 +03:00
Rob Garrison
654403eb00 1.5.19 2021-07-15 08:28:07 -05:00
Gusted
ffcdf47ab5
Passing object -> Passing property (#1277)
- Resolves #1276
- Just pass the parameter which is either undefined or an number. No need to get it from a object, while we can just pass the property already.
2021-07-10 04:10:11 -04:00
Rob Garrison
9e9c9061dc 1.5.18 2021-07-07 22:06:55 -05:00
narcolepticinsomniac
ab44c60522
More specific name 2021-07-07 10:07:29 -04:00
narcolepticinsomniac
e8e18abe58
More specific name 2021-07-07 07:51:00 -04:00
Gusted
58fc531515
Fix integration (#1275)
* Fix integration

- Don't use sourceCode as "temporary" value as it's being used as legit value(consider that we delete it after it's non longer needed).
- Wrap the metadata into a `try {}` as some styles doesn't have any metadata.

* Typos adios magios and bonjour gutentag
2021-07-06 18:39:52 -04:00
tophf
44b08dc089
randomize sync interval to avoid infinite deadlocks (#1250)
* randomize sync interval to avoid infinite deadlocks

* autoretry with a randomly increasing delay
2021-07-06 17:19:18 -04:00
Gusted
264544dfa9
Allow dragging the message box (#1274)
* Allow dragging the message box

- Allows the message box to be dragged to any position on the screen, so the user can set it to their wishes.
- Something as discussed in #1270.

* Use CSS to prevent user select

* Remove whitespace

* Imagine wasted new lines

* Make it more niece

* Remove unnecessary check

* Fix calculation code

* cursor: move;

* fixup

* Don't declare variable here

* Cap the move to 30px in each side

* I should pay attention to my english lessons

Co-authored-by: tophf <tophf@gmx.com>
2021-07-06 17:19:00 -04:00
Gusted
a2c8953e63
Use flex to center #message-box (#1271)
* Use flex to center #message-box

- Resolves #1270
- See details on the issue mentioned.

* Use existing .center

* Remove center rule at non-center selector
2021-07-06 17:18:42 -04:00
Gusted
9e72784b2a
Change USw endpoints (#1269)
- Changes the necessary endpoints for OAuth to newer ones that make more sense.
- Related commit on USw: cf6384cf4d
2021-07-01 20:12:24 -04:00
tophf
fa43c6d94d
fix flicker when hovering buttons in popup [Firefox] (#1267) 2021-06-29 05:37:30 -04:00
Gusted
fe45781545
Linking Styles to USW (#1256)
* Prototype

Just able to log the token for the requested style.

* Store USw Token

* Fix linting

* Add revoke capabilities

* Add upload capabilities + UI?

* Add credentials for production server

* Patch up several things

* Send styleInfo

We will be adding the feature to add style based of the currentStyle, see paring commit 31813da300

* Fix clientSecret

* Pass styleInfo trough usw's hook

Related commit on USW: 461ddb03c7

* Adjusted behavior

Applied suggestions from Narco.

* Wait for `usw-ready`before sending style

* don't use `window.`

* Ensure correct style is pre-filled

* Send over metadata

Related USW commit: 7d8c4c1248

* Title Case => Title case

* _linking => _isUswLinked
2021-06-29 05:36:59 -04:00
tophf
ada46e8277 fix bookmarks being orphanized/stranded 2021-06-29 05:45:04 +03:00
tophf
4e87a060f5 use crypto.randomUUID if present 2021-06-17 05:12:25 +03:00
Gusted
83a6808c67
Disable Offscreencanvas for firefox (#1258)
Currently the offscreencanvas is being used for '2d' context, however Firefox doesn't implement this as for now, see: https://searchfox.org/mozilla-central/source/dom/canvas/OffscreenCanvas.cpp#105-110 and it will error out multiple times within the console.
2021-05-31 17:21:17 +03:00
tophf
4bd956f891 don't stretch the table in new browsers
fixes #1255
2021-05-27 18:38:42 +03:00
tophf
367ae56047 reuse openURL so that the opener tab id is set 2021-05-27 14:36:10 +03:00
tophf
18265b94c6 tab url may be empty for about:blank tabs
fixes #1254
2021-05-27 14:35:03 +03:00
Gusted
440a9f4763
Add USw hook to remove get stylus button (#1239) 2021-04-30 16:39:07 +03:00
tophf
d736a00bc1 csslint: allow globals like @import inside sections 2021-04-28 17:33:27 +03:00
tophf
be4fd17113 wait for just created script elements to load
fixes #1233
2021-04-24 16:37:30 +03:00
tophf
b0a03d53fc move lazyKeymaps definition to the base init 2021-04-24 16:26:14 +03:00
tophf
23aa036db5 show USW styles with 'stylus' category 2021-04-20 21:05:49 +03:00
tophf
0aa6d3b463 read css escapes per spec 2021-04-20 20:40:29 +03:00
Gusted
f10ebffeff
Add USw to the inline search and proper installing (#1223)
* remember sort order in popup, use "updated" by default
* add a USW link in the manager

Co-authored-by: tophf <tophf@gmx.com>
2021-04-20 20:40:04 +03:00
tophf
892b295897 CodeMirror 5.61.0 2021-04-20 11:29:24 +03:00
tophf
afcd8ebcf4 prevent FF from double-handling clicks, fixes #1232 2021-04-20 07:22:19 +03:00
tophf
711f6502b5 make transition suppressor rule more robust 2021-04-19 16:09:31 +03:00
Rob Garrison
43f1da1b4c copy policy from app details 2021-04-18 20:15:07 -05:00
Rob Garrison
baca54bf19 Add privacy policy 2021-04-18 20:06:24 -05:00
tophf
0b9b972338 update swatch's lines to match current code 2021-04-08 14:33:36 +03:00
silverwind
a934571dc7
update setup-node plugin to v2 (#1215) 2021-04-08 10:49:24 +03:00
vVde3S88xHW6EZaB63HyXQUipgFtaWooYGDs35g
82d2530669
fix installer for local files in FF (#1224) 2021-04-08 10:45:43 +03:00
tophf
33e7f920a3 fixup: load codemirror-themes.js earlier 2021-04-06 07:10:03 +03:00
tophf
fa2dec724a fix editor theme in FF containers, fixes #1222 2021-04-06 06:56:35 +03:00
tophf
2d5788766a fix Enter key clearing selected text, fixes #1219
regressed in 7a479edc
2021-04-02 07:12:44 +03:00
tophf
8c160ed40c CodeMirror 5.60.0 2021-03-20 14:23:49 +03:00
tophf
ed238183eb revert 4ae2c670's changes for the install button
as it's no longer required after 3f4fb061
2021-03-19 23:02:02 +03:00
tophf
3f4fb0617e reimplement #1192 using dummy links
as omitting href attribute is sufficient to avoid the useless tooltip
2021-03-19 22:53:52 +03:00
tophf
35e0a9d032 avoid FOUC in tabs on update/reload 2021-03-19 19:00:48 +03:00
tophf
21d902c48c deduplicate keys in prefs.subscribe 2021-03-14 20:30:44 +03:00
tophf
a7ae3fbc55 unbork selectByTokens option
...by moving custom options definitions before isEditorPref checks CodeMirror.defaults
2021-03-14 19:44:47 +03:00
tophf
db77e03e97 print stylus-lang's p() to editor console, fixes #894 2021-03-14 10:07:59 +03:00
tophf
692d3c9826
try to show applicable values in autocomplete for props (#1211)
+ restore proper toggling of autocompleteOnTyping
2021-03-14 08:33:26 +03:00
tophf
9531698dd7 show unreachableMozSiteHint on any site in FF
fixes #1210
2021-03-12 08:51:57 +03:00
tophf
9d19d61913
show time/weekday in tooltip for style entries (#1205) 2021-03-11 07:20:38 +03:00
tophf
ff63b84489 add a test for the transition suppressor rule 2021-03-09 19:24:37 +03:00
tophf
4ae2c67033 unbork install button style + dedupe svg-icon
regressed in b8f6f5db
2021-03-06 21:22:24 +03:00
tophf
65ac351699
preserve opacity in colorpicker for preprocessor uso config (#1200)
USO has always produced 6-digit #rrggbb so some styles rely on it
and use /*[[color]]*/11 notation to specify the transparency.
Now we will try to preserve the opacity customized by the user
via colorpicker unless the style specifies it inline.
2021-03-05 17:25:05 +03:00
tophf
7d08fea5e1 followup for 102121ad: apply suppressor on all pages 2021-03-01 08:46:47 +03:00
tophf
8eca4e9139 followup for 4228758c: only input should be clickable 2021-02-28 23:39:09 +03:00
tophf
102121ad8f
suppress transition bug on page open in Chrome, too (#1193) 2021-02-28 23:01:49 +03:00
tophf
4228758cec don't hide the input in onoffswitch
as it wasn't necessary and turns out it causes problems with event routing when the parent <label> includes a <button> which we use now instead of dummy links
2021-02-28 22:58:24 +03:00
tophf
c0eace302f inject styles only in visible frames
fixes #1033
2021-02-28 22:33:16 +03:00
tophf
a56e528b31 trust sender's URL in FF
fixes #1194
2021-02-28 18:01:26 +03:00
tophf
01e2e09fa5 ignore HTTP502 error when syncing 2021-02-28 08:37:56 +03:00
tophf
b8f6f5db8d
switch from dummy links to dummy buttons (#1192)
The reason is that dummy links like <a href="#"> cause the built-in tooltip pop up on hover which is just useless noise
2021-02-27 20:42:49 +03:00
tophf
41533e863d enforce eslint radix rule for parseInt 2021-02-26 13:02:54 +03:00
tophf
53c71614fc relaxed parsing of usercss @version
fixes #821
2021-02-26 10:03:32 +03:00
tophf
9b46da5846
shrink screenshots to make readme readmeable 2021-02-25 10:26:32 +03:00
tophf
81fa6b1e79 avoid a scrollbar due to rounding errors 2021-02-25 10:16:50 +03:00
Rob Garrison
a5d2d96717 1.5.17 2021-02-24 18:34:09 -06:00
tophf
69308d04e9 CodeMirror 5.59.4 2021-02-24 13:41:05 +03:00
tophf
75ae8e79bf replace webext-tx-fix with a local tools script 2021-02-24 13:37:57 +03:00
tophf
14efa1f052 fix locales 2021-02-24 11:27:39 +03:00
281 changed files with 50386 additions and 82136 deletions

View File

@ -203,7 +203,7 @@ rules:
prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
quote-props: [0]
quotes: [1, single, avoid-escape]
radix: [2, as-needed]
radix: [2, always]
require-jsdoc: [0]
require-yield: [2]
semi-spacing: [2, {before: false, after: true}]

View File

@ -24,6 +24,9 @@ If not, then provide details describing which page the feature will effect, e.g.
## Adding translations
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus).
Only the languages supported by the web store are allowed:
https://developer.chrome.com/docs/webstore/i18n/#localeTable
## Pull requests

View File

@ -1,9 +0,0 @@
* **Browser**:
* **Operating System**:
* **Stylus Version**:
* **Screenshot**:
<!--
Please make sure you checked that your issue wasn't already addressed.
If the issue persists, please help us identifying the cause by providing the above details.
-->

48
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@ -0,0 +1,48 @@
---
name: Bug Report
about: Create a report about a bug you experienced while using Stylus.
title: "[Bug] Replace with title"
assignees: ''
---
<!--
⚠⚠ Do not delete this issue template! ⚠⚠
Reported issues must use this template and have all the necessary information provided.
Incomplete reports are likely to be ignored and closed.
-->
<!--
Thank you for taking the time to create a report about a bug.
Ensure that there are no other existing reports for this bug.
Please check if the issue is resolved after a restart of the browser.
Additionally, you should check if the issue persists in a new browser profile.
Remember to fill out every section on this report and remove any that are not needed.
Finally, place a brief description in the title of this report.
-->
# Bug Report
### Bug Description
<!-- Provide a clear and concise description, which will allow us to properly troubleshoot this bug. -->
### Screenshots
<!-- If applicable, add screenshots to help explain this bug. -->
### CSS Code
<!--
If the bug is related to (user)CSS or the editor,
please post the code (with a service like pastebin) in this bug report.
-->
### System Information
<!--
Specify the browser name and version as well as the Stylus version you are using.
Please do an online search for help if you are not familiar with how to get this information.
-->
- OS: <!-- e.g. Windows, macOS, Linux -->
- Browser: <!-- e.g. Chrome 91, Firefox 90, Edge 91, Safari 14 -->
- Stylus Version: <!-- e.g. 1.5.21 -->
### Additional Context
<!-- Provide any additional information about this bug. -->

View File

@ -7,7 +7,7 @@ jobs:
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v1
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install

10
.gitignore vendored
View File

@ -1,8 +1,8 @@
.DS_Store
pull_locales_login.rb
.vscode
node_modules/
yarn.lock
*.zip
.DS_Store
.eslintcache
.transifexrc
.vscode
desktop.ini
node_modules/
yarn.lock

View File

@ -21,12 +21,9 @@ Stylus is a fork of Stylish for Chrome, also compatible with Firefox as a WebExt
## Screenshots
![Style manager](https://user-images.githubusercontent.com/1310400/34453460-214eaa5c-ed67-11e7-843b-d8960b71db6e.png)
![Style editor](https://user-images.githubusercontent.com/1310400/34459585-3932cd94-ee05-11e7-9a1b-679522dddfb3.png)
![Popup config for usercss](https://user-images.githubusercontent.com/1310400/34453462-218a589a-ed67-11e7-9040-7d0469eeadc3.png)
![Popup inline search](https://user-images.githubusercontent.com/1310400/34453463-21a44368-ed67-11e7-93b2-e1c8f5aac868.png)
![Style manager config for usercss](https://user-images.githubusercontent.com/1310400/34453464-21bdaf9c-ed67-11e7-8517-62d2f02e1918.png)
![Options](https://user-images.githubusercontent.com/1310400/34453461-216aee4c-ed67-11e7-92db-ea21c1da5826.png)
Manager | Editor | Popup search | Popup config | Manager config | Options
-|-|-|-|-|-
![Style manager](https://user-images.githubusercontent.com/1310400/34453460-214eaa5c-ed67-11e7-843b-d8960b71db6e.png) | ![Style editor](https://user-images.githubusercontent.com/1310400/34459585-3932cd94-ee05-11e7-9a1b-679522dddfb3.png) | ![Popup inline search](https://user-images.githubusercontent.com/1310400/34453463-21a44368-ed67-11e7-93b2-e1c8f5aac868.png) | ![Popup config for usercss](https://user-images.githubusercontent.com/1310400/34453462-218a589a-ed67-11e7-9040-7d0469eeadc3.png) | ![Style manager config for usercss](https://user-images.githubusercontent.com/1310400/34453464-21bdaf9c-ed67-11e7-8517-62d2f02e1918.png) | ![Options](https://user-images.githubusercontent.com/1310400/34453461-216aee4c-ed67-11e7-92db-ea21c1da5826.png)
## Help
@ -53,7 +50,7 @@ Copyright &copy; 2005-2014 [Jason Barnabe](jason.barnabe@gmail.com)
Current Stylus:
Copyright &copy; 2017-2019 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
Copyright &copy; 2017-2022 [Stylus Team](https://github.com/openstyles/stylus/graphs/contributors)
**[GNU GPLv3](./LICENSE)**

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,8 @@
{
"InaccessibleFileHint": {
"message": "Stylus can not access some file types (e.g. pdf & json files).",
"description": "Note in the toolbar popup for some file types that cannot be accessed"
},
"addStyleLabel": {
"message": "Write new style",
"description": "Label for the button to go to the add style page"
@ -89,8 +93,8 @@
"description": "Heading for backup"
},
"backupMessage": {
"message": "Select a file or drag and drop to this page.",
"description": "Message for backup"
"message": "To import the backup file, drag'n'drop it into this page or click the Import button.\n\nTo export a compatible backup for Stylus older than 1.5.18, right-click or shift-click the Export button.",
"description": "Text for Backup section's (i) in the manager"
},
"bckpInstStyles": {
"message": "Export styles"
@ -182,6 +186,10 @@
"message": "Theme",
"description": "Label for the style editor's CSS theme."
},
"cm_arrowKeysTraverse": {
"message": "Arrow keys ↑↓ traverse sections",
"description": "Label for the option in the editor."
},
"colorpickerPaletteHint": {
"message": "Right-click a swatch to cycle through its source lines"
},
@ -245,6 +253,12 @@
"message": "Yes",
"description": "'Yes' button in a confirm dialog"
},
"connectingDropbox": {
"message": "Connecting Dropbox..."
},
"connectingDropboxNotAllowed": {
"message": "Connecting to Dropbox is only available in apps installed directly from the webstore"
},
"copied": {
"message": "Copied to clipboard",
"description": "Message shown when content has been copied to the clipboard"
@ -262,39 +276,39 @@
},
"dateAbbrDay": {
"message": "$value$d",
"description": "Day suffix in a short relative date, for example: 8d",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"description": "Day suffix in a short relative date, for example: 8d"
},
"dateAbbrHour": {
"message": "$value$h",
"description": "Hour suffix in a short relative date, for example: 8h",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"description": "Hour suffix in a short relative date, for example: 8h"
},
"dateAbbrMonth": {
"message": "$value$m",
"description": "Month suffix in a short relative date, for example: 8m",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"description": "Month suffix in a short relative date, for example: 8m"
},
"dateAbbrYear": {
"message": "$value$y",
"description": "Year suffix in a short relative date, for example: 8y",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"description": "Year suffix in a short relative date, for example: 8y"
},
"dateInstalled": {
"message": "Date installed",
@ -326,12 +340,29 @@
},
"disableAllStyles": {
"message": "Turn all styles off",
"description": "Label for the checkbox that turns all enabled styles off."
"description": "Label for the checkbox that turns all styles off."
},
"disableAllStylesOff": {
"message": "Styles are turned off",
"description": "Label for the checkbox that turns all styles off when it's checked."
},
"disableStyleLabel": {
"message": "Disable",
"description": "Label for the button to disable a style"
},
"draftTitle": {
"message": "Draft recovery, created $date$",
"placeholders": {
"date": {
"content": "$1"
}
},
"description": "Title of the modal displayed in the editor when an unsaved draft is found, the $date$ looks like '1 hour ago' in user's current UI language"
},
"draftAction": {
"message": "Choose 'Yes' to load this draft or 'No' to discard it.",
"description": "Displayed in the editor after the browser/extension crashed"
},
"dragDropMessage": {
"message": "Drop your backup file anywhere on this page to import.",
"description": "Drag'n'drop message"
@ -365,6 +396,9 @@
},
"description": "Title of the page for editing styles"
},
"editorSettings": {
"message": "Editor settings"
},
"enableStyleLabel": {
"message": "Enable",
"description": "Label for the button to enable a style"
@ -379,6 +413,12 @@
"message": "Export",
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
},
"exportCompatible": {
"message": "Export (compatible mode)"
},
"exportSavedSuccess": {
"message": "File saved with success"
},
"externalFeedback": {
"message": "Feedback",
"description": "Label for the external link to send feedback for the style"
@ -419,18 +459,6 @@
"message": "Find styles",
"description": "Text for a link that gets a list of styles for the current site"
},
"findStylesForSite": {
"message": "Find more styles for this site",
"description": "Text for a link that gets a list of styles for the current site"
},
"findStylesInline": {
"message": "Inline",
"description": "Text for a checkbox that opens search results 'inline' (within the Stylus popup window)"
},
"findStylesInlineTooltip": {
"message": "Display search results inside this window.",
"description": "Text for a checkbox that displays search results within the Stylus popup."
},
"genericAdd": {
"message": "Add",
"description": "Used in various places for an action that adds something"
@ -439,6 +467,9 @@
"message": "Clone",
"description": "Used in various places for an action that clones something"
},
"genericDescription": {
"message": "Description"
},
"genericDisabledLabel": {
"message": "Disabled",
"description": "Used in various lists/options to indicate that something is disabled"
@ -471,6 +502,13 @@
"message": "Saved",
"description": "Used in various parts of the UI to indicate that something was saved"
},
"genericSize": {
"message": "Size"
},
"genericTest": {
"message": "Test",
"description": "Label for the action that runs some test e.g. opens the regexp tester panel in the editor"
},
"genericTitle": {
"message": "Title",
"description": "Used in various parts of the UI to indicate the title of something"
@ -479,6 +517,13 @@
"message": "Unknown",
"description": "Used in various parts of the UI to indicate if something is unknown (e.g. an unknown date)"
},
"gettingStyles": {
"message": "Getting all styles..."
},
"headerResizerHint": {
"message": "Hold Shift to resize only in this type of UI, i.e. editor, manager, installer",
"description": "Tooltip for the header panel resizer"
},
"helpAlt": {
"message": "Help",
"description": "Alternate text for help buttons"
@ -566,7 +611,7 @@
"description": "Label for install button"
},
"installButtonInstalled": {
"message": "Style installed",
"message": "Style is installed",
"description": "Text displayed when the style is successfully installed"
},
"installButtonReinstall": {
@ -606,6 +651,18 @@
"message": "Get styles",
"description": "Help link text on the manage page e.g. https://userstyles.org"
},
"linkGetStylesInfo": {
"message": "This archive site was created by a userstyle community member to back up the slow and unresponsive userstyles.org. The archive updates its contents approximately once a day.",
"description": "Info shown when clicking the (i) icon of the uso-archive link in the manager"
},
"linkGetShareStyles": {
"message": "Get and share styles",
"description": "Link text for https://userstyles.world/ on the manage page"
},
"linkGetShareStylesInfo": {
"message": "The new community-driven userstyles.world site is created by userstyle authors in order to replace userstyles.org, which has been so slow and unresponsive for the past year that many authors stopped updating their styles.",
"description": "Info shown when clicking the (i) icon of the userstyles.world link in the manager"
},
"linkStylusWiki": {
"message": "Wiki",
"description": "Wiki link text on the manage page e.g. https://github.com/openstyles/stylus/wiki"
@ -694,7 +751,7 @@
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"manageFaviconsHelp": {
"message": "Stylus uses an external service https://www.google.com/s2/favicons",
"message": "Stylus uses an external service https://icons.duckduckgo.com",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
},
"manageFilters": {
@ -709,6 +766,9 @@
"message": "Number of applies-to items",
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
},
"manageMinColumnWidth": {
"message": "Minimum column width (in pixels; 9999 disables multi-column mode)"
},
"manageNewStyleAsUsercss": {
"message": "as Usercss",
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
@ -763,105 +823,105 @@
},
"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"
}
}
},
"description": "Error displayed when the value of @var color is invalid"
},
"meta_invalidNumber": {
"message": "Expect a number",
"description": "Error displayed when the value is expected to be a number"
},
"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"
}
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"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"
}
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"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"
}
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeMin": {
"message": "Invalid @var $type$: default value is lower than the minimum",
"placeholders": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeMultipleUnits": {
"message": "Invalid @var $type$: multiple units are defined",
"placeholders": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"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"
}
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeTooManyValues": {
"message": "Invalid @var $type$: the array contains too many items",
"placeholders": {
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeUnits": {
"message": "Invalid @var $type$: '$units$' is not a valid unit",
"description": "Error displayed when the value of @var range or @var number is invalid",
"placeholders": {
"units": {
"content": "$2"
},
"type": {
"content": "$1"
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidRangeValue": {
"message": "Invalid @var $type$: items in the array must be number, string, or null",
"placeholders": {
"type": {
"content": "$1"
},
"units": {
"content": "$2"
}
}
},
"description": "Error displayed when the value of @var range or @var number is invalid"
},
"meta_invalidSelect": {
"message": "Invalid @var select: the default value must be an array or an object",
"description": "Error displayed when the value of @var select is invalid"
},
"meta_invalidSelectValue": {
"message": "Invalid @var select: values inside the array/object must be a string",
"description": "Error displayed when the value of @var select is invalid"
},
"meta_invalidSelectEmptyOptions": {
"message": "Invalid @var select: options list is empty",
"description": "Error displayed when the value of @var select is invalid"
@ -878,35 +938,30 @@
"message": "Invalid @var select: option name is duplicated",
"description": "Error displayed when the value of @var select is invalid"
},
"meta_invalidSelectValue": {
"message": "Invalid @var select: values inside the array/object must be a string",
"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_invalidString": {
"message": "Expect a quoted string",
"description": "Error displayed when the value is expected to be a quoted string"
},
"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"
}
}
},
"description": "Error displayed when the protocol of the URL is invalid"
},
"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"
"message": "Invalid version number",
"description": "Error displayed when @version is invalid"
},
"meta_invalidWord": {
"message": "Expect a word",
@ -914,12 +969,12 @@
},
"meta_missingChar": {
"message": "Expect characters: $chars$",
"description": "Error displayed when the value is expected to be some characters",
"placeholders": {
"chars": {
"content": "$1"
}
}
},
"description": "Error displayed when the value is expected to be some characters"
},
"meta_missingEOT": {
"message": "Expect EOT data",
@ -927,51 +982,66 @@
},
"meta_missingMandatory": {
"message": "Missing mandatory metadata: $keys$",
"description": "Error displayed when mandatory keys are missing",
"placeholders": {
"keys": {
"content": "$1"
}
}
},
"description": "Error displayed when mandatory keys are missing"
},
"meta_unknownJSONLiteral": {
"message": "Invalid JSON: $literal$ is not a valid JSON literal",
"description": "Error displayed when JSON value is invalid",
"placeholders": {
"literal": {
"content": "$1"
}
}
},
"description": "Error displayed when JSON value is invalid"
},
"meta_unknownMeta": {
"message": "Unknown metadata: $key$",
"description": "Error displayed when unknown metadata is parsed",
"placeholders": {
"key": {
"content": "$1"
}
}
},
"description": "Error displayed when unknown metadata is parsed"
},
"meta_unknownVarType": {
"message": "Unknown @$varkey$ type: $vartype$",
"description": "Error displayed when unknown variable type is parsed",
"meta_unknownMetaTypo": {
"message": "Maybe @$keyOk$? Unknown metadata: @$keyErr$",
"placeholders": {
"varkey": {
"keyErr": {
"content": "$1"
},
"vartype": {
"keyOk": {
"content": "$2"
}
}
},
"description": "Try translating it so that at least the first placeholder is visible in our narrow panel. This is the error displayed when an unknown metadata key was sufficiently similar to a known one to consider it a typo."
},
"meta_unknownPreprocessor": {
"message": "Unknown @preprocessor: $preprocessor$",
"description": "Error displayed when unknown @preprocessor is parsed",
"placeholders": {
"preprocessor": {
"content": "$1"
}
}
},
"description": "Error displayed when unknown @preprocessor is parsed"
},
"meta_unknownVarType": {
"message": "Unknown @$varkey$ type: $vartype$",
"placeholders": {
"vartype": {
"content": "$2"
},
"varkey": {
"content": "$1"
}
},
"description": "Error displayed when unknown variable type is parsed"
},
"noFileToImport": {
"message": "To import your styles, you should export it first."
},
"noStylesForSite": {
"message": "No styles installed for this site.",
@ -1009,9 +1079,24 @@
"message": "Exposes the top site domain in each iframe.\nEnables writing iframe-specific CSS like this:\nhtml[stylus-iframe$$=\"twitter.com\"] h1 { display:none }",
"description": "Add attribute to iframe; make sure to include the double $$ in the css example, or the `$=` will be omitted in the displayed text."
},
"optionsAdvancedExposeStyleName": {
"message": "Expose style name"
},
"optionsAdvancedExposeStyleNameNote": {
"message": "Exposes the style name in the page to facilitate debugging of styles in devtools. Please reload the tab(s) to apply the new setting."
},
"optionsAdvancedNewStyleAsUsercss": {
"message": "Write new style as usercss"
},
"optionsAdvancedAutoSwitchSchemeNever": {
"message": "Disabled. The dark/light setting in styles is ignored."
},
"optionsAdvancedAutoSwitchSchemeBySystem": {
"message": "By system preference"
},
"optionsAdvancedAutoSwitchSchemeByTime": {
"message": "By night time:"
},
"optionsAdvancedPatchCsp": {
"message": "Patch <code>CSP</code> to allow style assets"
},
@ -1045,16 +1130,19 @@
"optionsCustomizePopup": {
"message": "Popup"
},
"optionsCustomizeUpdate": {
"message": "Updates"
},
"optionsCustomizeSync": {
"message": "Sync to cloud"
},
"optionsCustomizeUpdate": {
"message": "Updates"
},
"optionsHeading": {
"message": "Options",
"description": "Heading for options section on manage page."
},
"optionsIconAuto": {
"message": "Match the Dark/Light mode"
},
"optionsIconDark": {
"message": "Dark browser themes"
},
@ -1077,33 +1165,36 @@
"message": "Reset options"
},
"optionsStylusThemes": {
"message": "Find a Stylus UI theme"
"message": "Click Stylus icon in the browser toolbar on any Stylus page including this one, then click 'Find styles'"
},
"optionsSubheading": {
"message": "More Options",
"description": "Subheading for options section on manage page."
},
"optionsUpdateImportNote": {
"message": "When importing style backups from old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
},
"optionsUpdateInterval": {
"message": "Userstyle autoupdate interval in hours (specify 0 to disable)"
},
"optionsSyncNone": {
"message": "None"
},
"optionsSyncConnect": {
"message": "Connect"
},
"optionsSyncDisconnect": {
"message": "Disconnect"
},
"optionsSyncSyncNow": {
"message": "Sync now"
},
"optionsSyncLogin": {
"message": "Login"
},
"optionsSyncNone": {
"message": "None"
},
"optionsSyncStatusConnected": {
"message": "Connected"
},
"optionsSyncStatusConnecting": {
"message": "Connecting..."
},
"optionsSyncStatusDisconnected": {
"message": "Disconnected"
},
"optionsSyncStatusDisconnecting": {
"message": "Disconnecting..."
},
"optionsSyncStatusPull": {
"message": "Pulling style $loaded$ of $total$",
"placeholders": {
@ -1126,24 +1217,33 @@
}
}
},
"optionsSyncStatusSyncing": {
"message": "Syncing..."
"optionsSyncUsername": {
"message": "Username"
},
"optionsSyncStatusConnecting": {
"message": "Connecting..."
"optionsSyncPassword": {
"message": "Password"
},
"optionsSyncStatusConnected": {
"message": "Connected"
},
"optionsSyncStatusDisconnecting": {
"message": "Disconnecting..."
},
"optionsSyncStatusDisconnected": {
"message": "Disconnected"
"optionsSyncUrl": {
"message": "URL"
},
"optionsSyncStatusRelogin": {
"message": "Session expired, please login again."
},
"optionsSyncStatusSyncing": {
"message": "Syncing..."
},
"optionsSyncSyncNow": {
"message": "Sync now"
},
"optionsUpdateImportNote": {
"message": "When importing style backups from old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
},
"optionsUpdateInterval": {
"message": "Userstyle autoupdate interval in hours (specify 0 to disable)"
},
"overwriteFileExport": {
"message": "Do you want to overwrite an existing file?"
},
"paginationCurrent": {
"message": "Current page",
"description": "Tooltip for the current page index in search results"
@ -1185,6 +1285,10 @@
"message": "Click to see available hotkeys",
"description": "Tooltip displayed when hovering the right edge of the extension popup"
},
"popupManageSiteStyles": {
"message": "Manage site styles",
"description": "Item in the dropdown menu for the 'Manage' button in the popup that opens manager with styles applicable for current site."
},
"popupManageTooltip": {
"message": "Shift-click or right-click opens manager with styles applicable for current site",
"description": "Tooltip for the 'Manage' button in the popup."
@ -1209,6 +1313,21 @@
"message": "Styles before commands",
"description": "Label for the checkbox controlling section order in the popup."
},
"preferScheme": {
"message": "Dark/Light mode preference"
},
"preferSchemeAlways": {
"message": "Currently ignored (the style always applies) because the global Dark/Light mode is disabled"
},
"preferSchemeDark": {
"message": "Dark"
},
"preferSchemeLight": {
"message": "Light"
},
"preferSchemeNone": {
"message": "None (always applied)"
},
"prefShowBadge": {
"message": "Number of styles active for the current site",
"description": "Label for the checkbox controlling toolbar badge text."
@ -1221,9 +1340,34 @@
"message": "Temporarily applies the changes without saving.\nSave the style to make the changes permanent.",
"description": "Tooltip for the checkbox in style editor to enable live preview while editing."
},
"publish": {
"message": "Publish",
"description": "Header for the section to link the style with userStyles.world"
},
"publishPush": {
"message": "Push update",
"description": "The 'Publish style' button's new name when a connection is established"
},
"publishReconnect": {
"message": "Try disconnecting then publish again"
},
"publishRetry": {
"message": "Stylus is still trying to publish this style, but you can retry if you see no authentication activity or popups. Retry now?"
},
"publishStyle": {
"message": "Publish style",
"description": "Publish the current style to userstyles.world"
},
"publishUsw": {
"message": "Using <userstyles.world>",
"description": "Name of the link to https://userstyles.world in the editor"
},
"readingStyles": {
"message": "Reading styles..."
},
"reload": {
"message": "Reload Stylus extension",
"description": "Context menu reload"
"message": "Reload",
"description": "Context menu to reload the extension when installed in developer mode"
},
"replace": {
"message": "Replace",
@ -1237,9 +1381,18 @@
"message": "Replace with",
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
},
"restoreTemplate": {
"message": "Restore the default template.\n\n(The currently open editor pages won't change.)"
},
"retrieveBckp": {
"message": "Import styles"
},
"retrieveDropboxSync": {
"message": "Dropbox Import"
},
"saveAsTemplate": {
"message": "Save as template"
},
"search": {
"message": "Search",
"description": "Label before the search input field in the editor shown on Ctrl-F"
@ -1260,10 +1413,6 @@
"message": "Number of matches in code and applies-to values",
"description": "Tooltip for the number of found search results in the editor shown on Ctrl-F"
},
"searchStyleQueryHint": {
"message": "Search style names case-insensitively:\nsome words - all words in any order\n\"some phrase\" - this exact phrase without quotes\n2020 - a year like this also shows styles updated in 2020",
"description": "Tooltip shown for the text input in the popup's inline style finder"
},
"searchRegexp": {
"message": "Use /re/ syntax for regexp search",
"description": "Label after the search input field in the editor shown on Ctrl-F"
@ -1294,6 +1443,10 @@
"message": "Weekly installs",
"description": "Text for label that shows the number of times a search result was installed during last week"
},
"searchStyleQueryHint": {
"message": "Search style names (case-sensitively if an uppercase letter is used):\nsome words - all these words in any order\n\"some phrase\" - this exact phrase without quotes\n/foo.*bar/i - regular expression without spaces (use \\s instead)",
"description": "Tooltip shown for the text input in the popup's inline style finder"
},
"searchStylesAll": {
"message": "All",
"description": "Option for `find styles` scope selector in the manager."
@ -1338,6 +1491,10 @@
"message": "Sections",
"description": "Header for the table of contents block listing style section names in the left panel of the classic editor"
},
"settings": {
"message": "Settings",
"description": "Generic label/title for settings"
},
"shortcuts": {
"message": "Shortcuts",
"description": "Go to shortcut configuration"
@ -1345,6 +1502,9 @@
"shortcutsNote": {
"message": "Define keyboard shortcuts"
},
"shortcutsNoteFF": {
"message": "In Firefox 66+ you can open the built-in shortcuts UI manually:\n1) right-click Stylus icon in the toolbar and choose 'Manage'\n(alternatively, open about:addons via the main menu or Ctrl-Shift-A),\n2) in the page that opens click the cog wheel icon in the top right corner,\n3) choose 'Manage extension shortcuts'.\n\nYou can also customize the shortcuts here."
},
"sortDateNewestFirst": {
"message": "newest first",
"description": "Text added to indicate that sorting a date would add the newest entries at the top"
@ -1454,10 +1614,19 @@
"message": "Mozilla Format",
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
},
"styleName": {
"message": "Style name"
},
"styleNotAppliedRegexpProblemTooltip": {
"message": "Style was not applied due to its incorrect usage of 'regexp()'",
"description": "Tooltip in the popup for styles that were not applied at all"
},
"styleNotAppliedSchemeDark": {
"message": "This style is only applied in Dark Mode"
},
"styleNotAppliedSchemeLight": {
"message": "This style is only applied in Light Mode"
},
"styleRegexpInvalidExplanation": {
"message": "Some 'regexp()' rules that could not be compiled at all."
},
@ -1468,10 +1637,6 @@
"message": "Number of sections not applied due to incorrect usage of 'regexp()'",
"description": "Tooltip in the popup for styles that were applied only partially"
},
"styleRegexpTestButton": {
"message": "RegExp test",
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
},
"styleRegexpTestFull": {
"message": "Matching tabs",
"description": "RegExp test report: label for the fully matching expressions"
@ -1500,6 +1665,10 @@
"message": "Save",
"description": "Label for save button for style editing"
},
"styleSettings": {
"message": "Style settings",
"description": "Label/title for style settings dialog"
},
"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"
@ -1529,18 +1698,59 @@
"message": "As a security precaution, the browser prohibits extensions from affecting its built-in pages (like chrome://version, the standard new tab page as of Chrome 61, about:addons, and so on) as well as other extensions' pages. Each browser also restricts access to its own extensions gallery (like Chrome Web Store or AMO).",
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
},
"syncStorageErrorSaving": {
"message": "The value cannot be saved. Try reducing the amount of text.",
"description": "Displayed when trying to save an excessively big value via storage.sync API"
"styleUpdateUrlLabel": {
"message": "Update URL"
},
"stylePreferSchemeLabel": {
"message": "Dark/Light mode"
},
"styleIncludeLabel": {
"message": "Custom included sites"
},
"styleInjectionImportance": {
"message": "Toggle style's importance"
},
"styleInjectionOrder": {
"message": "Style injection order",
"description": "Tooltip for the button in the manager to open the dialog and also the title of this dialog"
},
"styleInjectionOrderHint": {
"message": "Drag'n'drop a style to change its position. Styles are injected sequentially in the order shown below so a style further down the list can override the earlier styles.",
"description": "Hint in the injection order dialog in the manager"
},
"styleInjectionOrderHint_prio": {
"message": "Important styles listed below are always injected last so they can override any newly installed styles. Click the style's mark to toggle its importance.",
"description": "Hint at the bottom of the injection order dialog in the manager"
},
"styleExcludeLabel": {
"message": "Custom excluded sites"
},
"syncDropboxDeprecated": {
"message": "Dropbox import/export is replaced by a more advanced style sync in the options page."
},
"syncDropboxStyles": {
"message": "Dropbox Export"
},
"syncError": {
"message": "Sync failed",
"description": "Tooltip for the toolbar icon"
},
"syncErrorRelogin": {
"message": "Sync failed.\nTry to re-login in Stylus options:\nclick 'disconnect' first, then 'connect'.",
"message": "Sync failed. You have been logged out.\nTry to re-login in Stylus options.",
"description": "Tooltip for the toolbar icon"
},
"syncErrorLock": {
"message": "The database is already in use. The lock will expire at $TIME$",
"placeholders": {
"time": {
"content": "$1"
}
}
},
"syncStorageErrorSaving": {
"message": "The value cannot be saved. Try reducing the amount of text.",
"description": "Displayed when trying to save an excessively big value via storage.sync API"
},
"toggleStyle": {
"message": "Toggle style",
"description": "Label for the checkbox to enable/disable a style"
@ -1561,14 +1771,6 @@
"message": "To allow access open <about:config>, right-click the list, click 'New', then 'Boolean', paste <privacy.resistFingerprinting.block_mozAddonManager> and click OK, <true>, OK, reload the <addons.mozilla.org> page.",
"description": "Note in the popup when opened on addons.mozilla.org in Firefox >= 59"
},
"unreachableAMOHintNewFF": {
"message": "In Firefox 60 and newer you'll also have to remove AMO domain from <extensions.webextensions.restrictedDomains> in <about:config>.",
"description": "Note in the popup when opened on addons.mozilla.org in Firefox >= 59"
},
"unreachableAMOHintOldFF": {
"message": "Only Firefox 59 and newer can be configured to allow WebExtensions to add style elements on CSP-protected sites such as this one.",
"description": "Note in the popup when opened on addons.mozilla.org in Firefox < 59"
},
"unreachableContentScript": {
"message": "Could not communicate with the page. Try reloading the tab.",
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
@ -1577,9 +1779,16 @@
"message": "Stylus can access file:// URLs only if you enable the corresponding checkbox for Stylus extension on chrome://extensions page.",
"description": "Note in the toolbar popup for file:// URLs"
},
"InaccessibleFileHint": {
"message": "Stylus can not access some file types (e.g. pdf & json files).",
"description": "Note in the toolbar popup for some file types that cannot be accessed"
"unreachableMozSiteHint": {
"message": "In Firefox 60 and newer you need to remove this domain from <extensions.webextensions.restrictedDomains> in <about:config>.",
"description": "Note in the popup when opened on a restricted mozilla site in Firefox >= 60"
},
"unreachableMozSiteHintOldFF": {
"message": "Only Firefox 59 and newer can be configured to allow WebExtensions to add style elements on CSP-protected sites such as this one.",
"description": "Note in the popup when opened on a restricted mozilla site in Firefox < 59"
},
"unzipStyles": {
"message": "Unzipping styles..."
},
"updateAllCheckSucceededNoUpdate": {
"message": "No updates found.",
@ -1633,6 +1842,9 @@
"message": "Updates installed:",
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
},
"uploadingFile": {
"message": "Uploading File..."
},
"usercssAvoidOverwriting": {
"message": "Please change the value of @name or @namespace to avoid overwriting an existing style.",
"description": "Shown in a message box when attempting to save a new Usercss style that would overwrite an existing one."
@ -1647,10 +1859,6 @@
"usercssReplaceTemplateConfirmation": {
"message": "Replace the default template for new Usercss styles with the current code?"
},
"usercssReplaceTemplateName": {
"message": "Empty @name replaces the default template",
"description": "The text shown after @name when creating a new Usercss style"
},
"usercssReplaceTemplateSectionBody": {
"message": "Insert code here...",
"description": "The code placeholder comment in a new style created by clicking 'Write style' in the popup"
@ -1667,43 +1875,7 @@
"message": "this URL",
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
},
"syncDropboxStyles": {
"message": "Dropbox Export"
},
"syncDropboxDeprecated": {
"message": "Dropbox import/export is replaced by a more advanced style sync in the options page."
},
"retrieveDropboxSync": {
"message": "Dropbox Import"
},
"overwriteFileExport": {
"message": "Do you want to overwrite an existing file?"
},
"exportSavedSuccess": {
"message": "File saved with success"
},
"noFileToImport": {
"message": "To import your styles, you should export it first."
},
"connectingDropbox": {
"message": "Connecting Dropbox..."
},
"connectingDropboxNotAllowed": {
"message": "Connecting to Dropbox is only available in apps installed directly from the webstore"
},
"gettingStyles": {
"message": "Getting all styles..."
},
"zipStyles": {
"message": "Zipping styles..."
},
"unzipStyles": {
"message": "Unzipping styles..."
},
"readingStyles": {
"message": "Reading styles..."
},
"uploadingFile": {
"message": "Uploading File..."
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

379
_locales/vi/messages.json Normal file
View File

@ -0,0 +1,379 @@
{
"InaccessibleFileHint": {
"message": "Stylus không thể truy cập một số kiểu tập tin (chẳng hạn như PDF và JSON)."
},
"addStyleLabel": {
"message": "Viết bảng định kiểu mới"
},
"addStyleTitle": {
"message": "Thêm bảng định kiểu"
},
"alphaChannel": {
"message": "Độ mờ"
},
"appliesAdd": {
"message": "Thêm"
},
"appliesDisplay": {
"message": "Áp dụng với: $applies$",
"placeholders": {
"applies": {
"content": "$1"
}
}
},
"appliesDisplayTruncatedSuffix": {
"message": "và một số khác"
},
"appliesDomainOption": {
"message": "Các địa chỉ thuộc tên miền này"
},
"appliesHelp": {
"message": "Dùng tuỳ chọn \"Áp dụng với\" để giới hạn các địa chỉ cho đoạn mã này"
},
"appliesLabel": {
"message": "Áp dụng với"
},
"appliesLineWidgetLabel": {
"message": "Hiển thị thông tin \"Áp dụng với\""
},
"appliesLineWidgetWarning": {
"message": "Không hoạt động với CSS tối giản"
},
"appliesRegexpOption": {
"message": "URL khớp với biểu thức chính quy"
},
"appliesRemove": {
"message": "Xoá"
},
"appliesRemoveError": {
"message": "Không thể xoá mục \"Áp dụng với\" cuối cùng"
},
"appliesSpecify": {
"message": "Chỉ định"
},
"appliesToEverything": {
"message": "Tất cả"
},
"appliesUrlPrefixOption": {
"message": "URL bắt đầu bằng"
},
"author": {
"message": "Tác giả"
},
"backupButtons": {
"message": "Sao lưu"
},
"backupMessage": {
"message": "Để nhập tập tin sao lưu, kéo và thả nó vào trang này hoặc nhấp vào nút Nhập.\n\nĐể xuất một bản sao lưu tương thích với Stylus trước phiên bản 1.5.18, nhấp chuột phải hoặc nhấn giữ Shift khi nhấp chuột trái vào nút Xuất."
},
"bckpInstStyles": {
"message": "Xuất bảng định kiểu"
},
"checkForUpdate": {
"message": "Kiểm tra bản cập nhật mới"
},
"checkingForUpdate": {
"message": "Đang kiểm tra..."
},
"clickToUninstall": {
"message": "Nhấp để huỷ kích hoạt"
},
"cm_autoCloseBrackets": {
"message": "Tự động đóng ngoặc và nháy"
},
"cm_autoCloseBracketsTooltip": {
"message": "Tự động thêm dấu đóng tương ứng khi nhập một trong các dấu (, [, {, ' và \"."
},
"cm_autocompleteOnTyping": {
"message": "Tự động hoàn thành"
},
"cm_colorpicker": {
"message": "Bộ chọn màu cho màu CSS"
},
"cm_indentWithTabs": {
"message": "Lùi đầu dòng thông minh bằng tab"
},
"cm_lineWrapping": {
"message": "Gập dòng dài"
},
"cm_linter": {
"message": "Trình phân tích cú pháp"
},
"cm_matchHighlight": {
"message": "Làm nổi"
},
"cm_matchHighlightSelection": {
"message": "Chỉ vùng được chọn"
},
"cm_matchHighlightToken": {
"message": "Token nằm dưới con trỏ văn bản"
},
"cm_selectByTokens": {
"message": "Nhấp đúp để chọn token"
},
"cm_selectByTokensTooltip": {
"message": "Ví dụ về token: .foo-bar-2 #aabbcc 0.32 !important\nKhi tắt: Chọn từ (phân tách bằng dấu câu)."
},
"cm_smartIndent": {
"message": "Lùi đầu dòng thông minh"
},
"cm_tabSize": {
"message": "Chiều rộng tab"
},
"cm_theme": {
"message": "Chủ đề"
},
"colorpickerSwitchFormatTooltip": {
"message": "Đổi định dạng: HEX → RGB → HSL.\nNhấn giữ phím Shift khi nhấp để đảo thứ tự.\nPhím tắt: PgUp và PgDn."
},
"colorpickerTooltip": {
"message": "Mở bộ chọn màu"
},
"configOnChange": {
"message": "khi thay đổi"
},
"configOnChangeTooltip": {
"message": "Tự động lưu và áp dụng"
},
"configureStyle": {
"message": "Thiết lập"
},
"configureStyleOnHomepage": {
"message": "Thiết lập trên trang chủ"
},
"confirmCancel": {
"message": "Huỷ"
},
"confirmClose": {
"message": "Đóng"
},
"confirmDefault": {
"message": "Dùng mặc định"
},
"confirmDelete": {
"message": "Xoá"
},
"confirmDiscardChanges": {
"message": "Huỷ thay đổi?"
},
"confirmNo": {
"message": "Không"
},
"confirmSave": {
"message": "Lưu"
},
"confirmStop": {
"message": "Dừng"
},
"confirmYes": {
"message": "Có"
},
"connectingDropbox": {
"message": "Đang kết nối với Dropbox..."
},
"connectingDropboxNotAllowed": {
"message": "Tính năng kết nối với Dropbox chỉ khả dụng khi cài ứng dụng trực tiếp từ cửa hàng web"
},
"copied": {
"message": "Đã chép vào khay nhớ tạm"
},
"copy": {
"message": "Chép vào khay nhớ tạm"
},
"dateAbbrDay": {
"message": "$value$ ngày",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrHour": {
"message": "$value$ giờ",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrMonth": {
"message": "$value$ tháng",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrYear": {
"message": "$value$ năm",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateInstalled": {
"message": "Ngày cài đặt"
},
"dateUpdated": {
"message": "Ngày cập nhật"
},
"defaultTheme": {
"message": "mặc định"
},
"deleteStyleConfirm": {
"message": "Bạn có chắc chắn muốn xoá bảng định kiểu này không?"
},
"deleteStyleLabel": {
"message": "Xoá"
},
"disableAllStyles": {
"message": "Tắt tất cả bảng định kiểu"
},
"disableAllStylesOff": {
"message": "Bật tất cả bảng định kiểu"
},
"disableStyleLabel": {
"message": "Vô hiệu hoá"
},
"draftAction": {
"message": "Chọn \"Có\" để tải bản nháp hoặc \"Không\" để xoá nó đi."
},
"editDeleteText": {
"message": "Xoá"
},
"editGotoLine": {
"message": "Đi đến dòng (hoặc dòng:cột)"
},
"editStyleHeading": {
"message": "Sửa bảng định kiểu"
},
"editStyleLabel": {
"message": "Sửa"
},
"editStyleTitle": {
"message": "Sửa bảng định kiểu $stylename$",
"placeholders": {
"stylename": {
"content": "$1"
}
}
},
"editorSettings": {
"message": "Cài đặt trình soạn thảo"
},
"enableStyleLabel": {
"message": "Kích hoạt"
},
"exportCompatible": {
"message": "Xuất (chế độ tương thích)"
},
"exportLabel": {
"message": "Xuất"
},
"exportSavedSuccess": {
"message": "Lưu thành công"
},
"externalFeedback": {
"message": "Phản hồi"
},
"externalHomepage": {
"message": "Trang chủ"
},
"externalLink": {
"message": "Liên kết ngoài"
},
"externalSupport": {
"message": "Ủng hộ"
},
"genericAdd": {
"message": "Thêm"
},
"genericDescription": {
"message": "Mô tả"
},
"genericDisabledLabel": {
"message": "Vô hiệu hoá"
},
"genericEnabledLabel": {
"message": "Kích hoạt"
},
"genericError": {
"message": "Lỗi"
},
"genericHistoryLabel": {
"message": "Lịch sử"
},
"genericNext": {
"message": "Sau"
},
"genericPrevious": {
"message": "Trước"
},
"genericResetLabel": {
"message": "Đặt lại"
},
"genericSavedMessage": {
"message": "Đã lưu"
},
"genericTest": {
"message": "Thứ"
},
"genericTitle": {
"message": "Tiêu đề"
},
"genericUnknown": {
"message": "Không xác định"
},
"helpAlt": {
"message": "Trợ giúp"
},
"importLabel": {
"message": "Nhập"
},
"importReportLegendAdded": {
"message": "đã thêm"
},
"importReportLegendIdentical": {
"message": "Đã bỏ qua các tệp trùng lặp"
},
"importReportLegendInvalid": {
"message": "Đã bỏ qua các tệp không hợp lệ"
},
"importReportLegendUpdatedBoth": {
"message": "đã cập nhật siêu thông tin và mã"
},
"importReportLegendUpdatedCode": {
"message": "đã cập nhật mã"
},
"importReportLegendUpdatedMeta": {
"message": "đã cập nhật siêu thông tin"
},
"importReportTitle": {
"message": "Đã nhập xong"
},
"installUpdateFromLabel": {
"message": "Kiểm tra bản cập nhật mới"
},
"license": {
"message": "Giấy phép"
},
"linterCSSLintIncompatible": {
"message": "CSSLint không hỗ trợ bộ tiền xử lý $preprocessorname$",
"placeholders": {
"preprocessorname": {
"content": "$1"
}
}
},
"linterJSONError": {
"message": "Định dạng JSON không hợp lệ"
},
"styleEnabledLabel": {
"message": "Kích hoạt"
},
"styleSaveLabel": {
"message": "Lưu"
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -6,35 +6,41 @@
/* global syncMan */
/* global updateMan */
/* global usercssMan */
/* global
FIREFOX
URLS
activateTab
download
findExistingTab
getActiveTab
isTabReplaceable
openURL
*/ // toolbox.js
/* global usoApi */
/* global uswApi */
/* global FIREFOX UA activateTab openURL */ // toolbox.js
/* global colorScheme */ // color-scheme.js
'use strict';
//#region API
addAPI(/** @namespace API */ {
/** Temporary storage for data needed elsewhere e.g. in a content script */
data: ((data = {}) => ({
del: key => delete data[key],
get: key => data[key],
has: key => key in data,
pop: key => {
const val = data[key];
delete data[key];
return val;
},
set: (key, val) => {
data[key] = val;
},
}))(),
styles: styleMan,
sync: syncMan,
updater: updateMan,
usercss: usercssMan,
uso: usoApi,
usw: uswApi,
colorScheme,
/** @type {BackgroundWorker} */
worker: createWorker({url: '/background/background-worker'}),
download(url, opts) {
return typeof url === 'string' && url.startsWith(URLS.uso) &&
this.sender.url.startsWith(URLS.uso) &&
download(url, opts || {});
},
/** @returns {string} */
getTabUrlPrefix() {
return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
@ -52,10 +58,24 @@ addAPI(/** @namespace API */ {
async openEditor(params) {
const u = new URL(chrome.runtime.getURL('edit.html'));
u.search = new URLSearchParams(params);
const wnd = prefs.get('openEditInWindow');
const wnd = chrome.windows && prefs.get('openEditInWindow');
const wndPos = wnd && prefs.get('windowPosition');
const wndBase = wnd && prefs.get('openEditInWindow.popup') ? {type: 'popup'} : {};
const ffBug = wnd && FIREFOX; // https://bugzil.la/1271047
if (wndPos) {
const {left, top, width, height} = wndPos;
const r = left + width;
const b = top + height;
const peek = 32;
if (isNaN(r) || r < peek || left > screen.availWidth - peek || width < 100) {
delete wndPos.left;
delete wndPos.width;
}
if (isNaN(b) || b < peek || top > screen.availHeight - peek || height < 100) {
delete wndPos.top;
delete wndPos.height;
}
}
const tab = await openURL({
url: `${u}`,
currentWindow: null,
@ -67,30 +87,25 @@ addAPI(/** @namespace API */ {
/** @returns {Promise<chrome.tabs.Tab>} */
async openManage({options = false, search, searchMode} = {}) {
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}&searchMode=${searchMode}`;
const setUrlParams = url => {
const u = new URL(url);
if (search) u.searchParams.set('search', search);
if (searchMode) u.searchParams.set('searchMode', searchMode);
if (options) u.hash = '#stylus-options';
return u.href;
};
const base = chrome.runtime.getURL('manage.html');
const url = setUrlParams(base);
const tabs = await browser.tabs.query({url: base + '*'});
const same = tabs.find(t => t.url === url);
let tab = same || tabs[0];
if (!tab) {
API.prefsDb.get('badFavs'); // prime the cache to avoid flicker/delay when opening the page
tab = await openURL({url, newTab: true});
} else if (!same) {
msg.sendTab(tab.id, {method: 'pushState', url: setUrlParams(tab.url)});
}
if (options) {
url += '#stylus-options';
}
let tab = await findExistingTab({
url,
currentWindow: null,
ignoreHash: true,
ignoreSearch: true,
});
if (tab) {
await activateTab(tab);
if (url !== (tab.pendingUrl || tab.url)) {
await msg.sendTab(tab.id, {method: 'pushState', url}).catch(console.error);
}
return tab;
}
tab = await getActiveTab();
return isTabReplaceable(tab, url)
? activateTab(tab, {url})
: browser.tabs.create({url}).then(activateTab); // activateTab unminimizes the window
return activateTab(tab); // activateTab unminimizes the window
},
/**
@ -143,10 +158,25 @@ if (chrome.commands) {
}
chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
if (reason === 'update') {
const [a, b, c] = (previousVersion || '').split('.');
if (a <= 1 && b <= 5 && c <= 13) { // 1.5.13
require(['/background/remove-unused-storage']);
if (reason === 'install') {
if (UA.mobile) prefs.set('manage.newUI', false);
if (UA.windows) prefs.set('editor.keyMap', 'sublime');
}
// TODO: remove this before 1.5.23 as it's only for a few users who installed git 26b75e77
if (reason === 'update' && previousVersion === '1.5.22') {
for (const dbName of ['drafts', prefs.STORAGE_KEY]) {
try {
indexedDB.open(dbName).onsuccess = async e => {
const idb = /** @type IDBDatabase */ e.target.result;
const ta = idb.objectStoreNames[0] === 'data' && idb.transaction(['data']);
if (ta && ta.objectStore('data').autoIncrement) {
ta.abort();
idb.close();
await new Promise(setTimeout);
indexedDB.deleteDatabase(dbName);
}
};
} catch (e) {}
}
}
});
@ -163,6 +193,8 @@ msg.on((msg, sender) => {
//#endregion
Promise.all([
browser.extension.isAllowedFileSchemeAccess()
.then(res => API.data.set('hasFileAccess', res)),
bgReady.styles,
/* These are loaded conditionally.
Each item uses `require` individually so IDE can jump to the source and track usage. */
@ -176,6 +208,6 @@ Promise.all([
require(['/background/context-menus']),
]).then(() => {
bgReady._resolveAll();
msg.isBgReady = true;
msg.ready = true;
msg.broadcast({method: 'backgroundReady'});
});

View File

@ -0,0 +1,91 @@
/* global prefs */
/* exported colorScheme */
'use strict';
const colorScheme = (() => {
const changeListeners = new Set();
const kSTATE = 'schemeSwitcher.enabled';
const kSTART = 'schemeSwitcher.nightStart';
const kEND = 'schemeSwitcher.nightEnd';
const SCHEMES = ['dark', 'light'];
const isDark = {
never: null,
dark: true,
light: false,
system: false,
time: false,
};
let isDarkNow = false;
prefs.subscribe(kSTATE, () => update());
prefs.subscribe([kSTART, kEND], (key, value) => {
updateTimePreferDark();
createAlarm(key, value);
}, {runNow: true});
chrome.alarms.onAlarm.addListener(({name}) => {
if (name === kSTART || name === kEND) {
updateTimePreferDark();
}
});
return {
SCHEMES,
onChange(listener) {
changeListeners.add(listener);
},
isDark: () => isDarkNow,
/** @param {StyleObj} _ */
shouldIncludeStyle({preferScheme: ps}) {
return prefs.get(kSTATE) === 'never' ||
!SCHEMES.includes(ps) ||
isDarkNow === (ps === 'dark');
},
updateSystemPreferDark(val) {
update('system', val);
return true;
},
};
function calcTime(key) {
const [h, m] = prefs.get(key).split(':');
return (h * 3600 + m * 60) * 1000;
}
function createAlarm(key, value) {
const date = new Date();
const [h, m] = value.split(':');
date.setHours(h, m, 0, 0);
if (date.getTime() < Date.now()) {
date.setDate(date.getDate() + 1);
}
chrome.alarms.create(key, {
when: date.getTime(),
periodInMinutes: 24 * 60,
});
}
function updateTimePreferDark() {
const now = Date.now() - new Date().setHours(0, 0, 0, 0);
const start = calcTime(kSTART);
const end = calcTime(kEND);
const val = start > end ?
now >= start || now < end :
now >= start && now < end;
update('time', val);
}
function update(type, val) {
if (type) {
if (isDark[type] === val) return;
isDark[type] = val;
}
val = isDark[prefs.get(kSTATE)];
if (isDarkNow !== val) {
isDarkNow = val;
for (const listener of changeListeners) {
listener(isDarkNow);
}
}
}
})();

View File

@ -5,16 +5,19 @@
* Common stuff that's loaded first so it's immediately available to all background scripts
*/
/* exported
addAPI
bgReady
compareRevision
*/
const bgReady = {};
window.bgReady = {}; /* global bgReady */
bgReady.styles = new Promise(r => (bgReady._resolveStyles = r));
bgReady.all = new Promise(r => (bgReady._resolveAll = r));
const uuidIndex = Object.assign(new Map(), {
custom: {},
/** `obj` must have a unique `id`, a UUIDv4 `_id`, and Date.now() for `_rev`. */
addCustomId(obj, {get = () => obj, set}) {
Object.defineProperty(uuidIndex.custom, obj.id, {get, set});
},
});
/* exported addAPI */
function addAPI(methods) {
for (const [key, val] of Object.entries(methods)) {
const old = API[key];
@ -26,6 +29,64 @@ function addAPI(methods) {
}
}
function compareRevision(rev1, rev2) {
return rev1 - rev2;
/* exported createCache */
/** Creates 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(id) {
const item = map.get(id);
return item && item.data;
},
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;
},
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;
},
clear() {
map.clear();
index = lastIndex = 0;
},
has: id => map.has(id),
*entries() {
for (const [id, item] of map) {
yield [id, item.data];
}
},
*values() {
for (const item of map.values()) {
yield item.data;
}
},
get size() {
return map.size;
},
};
}

View File

@ -1,80 +1,61 @@
/* global browserCommands */// background.js
/* global msg */
/* global prefs */
/* global CHROME FIREFOX URLS ignoreChromeError */// toolbox.js
/* global CHROME URLS ignoreChromeError */// toolbox.js
'use strict';
(() => {
const contextMenus = {
chrome.management.getSelf(ext => {
const contextMenus = Object.assign({
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
click: togglePref,
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
title: 'optionsOpenManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
}, ext.installType === 'development' && {
'reload': {
presentIf: async () => (await browser.management.getSelf()).installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
}, CHROME && {
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
documentUrlPatterns: [URLS.ownOrigin + '*'],
click: (info, tab) => {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError);
},
},
};
// "Delete" item in context menu for browsers that don't have it
if (CHROME &&
// looking at the end of UA string
/(Vivaldi|Safari)\/[\d.]+$/.test(navigator.userAgent) &&
// skip forks with Flash as those are likely to have the menu e.g. CentBrowser
!Array.from(navigator.plugins).some(p => p.name === 'Shockwave Flash')) {
prefs.__defaults['editor.contextDelete'] = true;
}
const keys = Object.keys(contextMenus);
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'),
CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark);
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf && prefs.knownKeys.includes(id)),
togglePresence);
createContextMenus(keys);
});
createContextMenus(Object.keys(contextMenus));
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
async function createContextMenus(ids) {
function createContextMenus(ids) {
for (const id of ids) {
let item = contextMenus[id];
if (item.presentIf && !await item.presentIf()) {
continue;
}
item = Object.assign({id}, item);
delete item.presentIf;
const item = Object.assign({id, contexts: ['browser_action']}, contextMenus[id]);
item.title = chrome.i18n.getMessage(item.title);
if (!item.type && typeof prefs.defaults[id] === 'boolean') {
item.type = 'checkbox';
item.checked = prefs.get(id);
}
if (!item.contexts) {
item.contexts = ['browser_action'];
if (typeof prefs.defaults[id] === 'boolean') {
if (item.type) {
prefs.subscribe(id, togglePresence);
} else {
item.type = 'checkbox';
item.checked = prefs.get(id);
prefs.subscribe(id, CHROME >= 62 && CHROME <= 64 ? toggleCheckmarkBugged : toggleCheckmark);
}
}
delete item.click;
chrome.contextMenus.create(item, ignoreChromeError);
@ -91,6 +72,11 @@
createContextMenus([id]);
}
/** @param {chrome.contextMenus.OnClickData} info */
function togglePref(info) {
prefs.set(info.menuItemId, info.checked);
}
function togglePresence(id, checked) {
if (checked) {
createContextMenus([id]);
@ -98,4 +84,4 @@
chrome.contextMenus.remove(id, ignoreChromeError);
}
}
})();
});

View File

@ -2,17 +2,17 @@
'use strict';
/* exported createChromeStorageDB */
function createChromeStorageDB() {
function createChromeStorageDB(PREFIX) {
let INC;
const isMain = !PREFIX;
if (!PREFIX) PREFIX = 'style-';
const PREFIX = 'style-';
const METHODS = {
return {
delete(id) {
return chromeLocal.remove(PREFIX + id);
},
// FIXME: we don't use this method at all. Should we remove this?
get(id) {
return chromeLocal.getValue(PREFIX + id);
},
@ -21,7 +21,9 @@ function createChromeStorageDB() {
const all = await chromeLocal.get();
if (!INC) prepareInc(all);
return Object.entries(all)
.map(([key, val]) => key.startsWith(PREFIX) && Number(key.slice(PREFIX.length)) && val)
.map(([key, val]) => key.startsWith(PREFIX) &&
(!isMain || Number(key.slice(PREFIX.length))) &&
val)
.filter(Boolean);
},
@ -59,8 +61,4 @@ function createChromeStorageDB() {
}
}
}
return function dbExecChromeStorage(method, ...args) {
return METHODS[method](...args);
};
}

View File

@ -1,5 +1,8 @@
/* global addAPI */// common.js
/* global chromeLocal */// storage-util.js
/* global cloneError */// worker-util.js
/* global deepCopy */// toolbox.js
/* global prefs */
'use strict';
/*
@ -11,16 +14,49 @@
/* exported db */
const db = (() => {
const DATABASE = 'stylish';
const STORE = 'styles';
let exec = async (...args) => (
exec = await tryUsingIndexedDB().catch(useChromeStorage)
)(...args);
const DB = 'stylish';
const FALLBACK = 'dbInChromeStorage';
const dbApi = {
async exec(...args) {
dbApi.exec = await tryUsingIndexedDB().catch(useChromeStorage);
return dbApi.exec(...args);
},
const ID_AS_KEY = {[DB]: true};
const getStoreName = dbName => dbName === DB ? 'styles' : 'data';
const cache = {};
const proxies = {};
const proxyHandler = {
get: ({dbName}, cmd) =>
(...args) =>
(dbName === DB ? exec : cachedExec)(dbName, cmd, ...args),
};
return dbApi;
/**
* @param {string} dbName
* @return {IDBObjectStore | {putMany: function(items:?[]):Promise<?[]>}}
*/
const getProxy = dbName => proxies[dbName] || (
(proxies[dbName] = new Proxy({dbName}, proxyHandler))
);
addAPI(/** @namespace API */ {
drafts: getProxy('drafts'),
/** Storage for big items that may exceed 8kB limit of chrome.storage.sync.
* To make an item syncable register it with uuidIndex.addCustomId. */
prefsDb: getProxy(prefs.STORAGE_KEY),
});
return {
styles: getProxy(DB),
};
async function cachedExec(dbName, cmd, a, b) {
const hub = cache[dbName] || (cache[dbName] = {});
const res = cmd === 'get' && a in hub ? hub[a] : await exec(...arguments);
if (cmd === 'get') {
hub[a] = deepCopy(res);
} else if (cmd === 'put') {
hub[ID_AS_KEY[dbName] ? a.id : b] = deepCopy(a);
} else if (cmd === 'delete') {
delete hub[a];
}
return res;
}
async function tryUsingIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data,
@ -40,9 +76,9 @@ const db = (() => {
async function testDB() {
const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
await dbExecIndexedDB('put', {id});
const e = await dbExecIndexedDB('get', id);
await dbExecIndexedDB('delete', e.id); // throws if `e` or id is null
await dbExecIndexedDB(DB, 'put', {id});
const e = await dbExecIndexedDB(DB, 'get', id);
await dbExecIndexedDB(DB, 'delete', e.id); // throws if `e` or id is null
}
async function useChromeStorage(err) {
@ -52,12 +88,18 @@ const db = (() => {
console.warn('Failed to access indexedDB. Switched to storage API.', err);
}
await require(['/background/db-chrome-storage']); /* global createChromeStorageDB */
return createChromeStorageDB();
const BASES = {};
return (dbName, method, ...args) => (
BASES[dbName] || (
BASES[dbName] = createChromeStorageDB(dbName !== DB && `${dbName}-`)
)
)[method](...args);
}
async function dbExecIndexedDB(method, ...args) {
async function dbExecIndexedDB(dbName, method, ...args) {
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const store = (await open()).transaction([STORE], mode).objectStore(STORE);
const storeName = getStoreName(dbName);
const store = (await open(dbName)).transaction([storeName], mode).objectStore(storeName);
const fn = method === 'putMany' ? putMany : storeRequest;
return fn(store, method, ...args);
}
@ -75,21 +117,34 @@ const db = (() => {
return Promise.all(items.map(item => storeRequest(store, 'put', item)));
}
function open() {
function open(name) {
return new Promise((resolve, reject) => {
const request = indexedDB.open(DATABASE, 2);
request.onsuccess = () => resolve(request.result);
const request = indexedDB.open(name, 2);
request.onsuccess = e => resolve(create(e));
request.onerror = reject;
request.onupgradeneeded = create;
});
}
function create(event) {
if (event.oldVersion === 0) {
event.target.result.createObjectStore(STORE, {
/** @type IDBDatabase */
const idb = event.target.result;
const dbName = idb.name;
const sn = getStoreName(dbName);
if (!idb.objectStoreNames.contains(sn)) {
if (event.type === 'success') {
idb.close();
return new Promise(resolve => {
indexedDB.deleteDatabase(dbName).onsuccess = () => {
resolve(open(dbName));
};
});
}
idb.createObjectStore(sn, ID_AS_KEY[dbName] ? {
keyPath: 'id',
autoIncrement: true,
});
} : undefined);
}
return idb;
}
})();

View File

@ -1,18 +1,22 @@
/* global API */// msg.js
/* global addAPI bgReady */// common.js
/* global colorScheme */
/* global prefs */
/* global tabMan */
/* global CHROME FIREFOX VIVALDI debounce ignoreChromeError */// toolbox.js
/* global CHROME FIREFOX UA debounce ignoreChromeError */// toolbox.js
'use strict';
/* exported iconMan */
const iconMan = (() => {
const ICON_SIZES = FIREFOX || CHROME >= 55 && !VIVALDI ? [16, 32] : [19, 38];
const ICON_SIZES = FIREFOX || CHROME && !UA.vivaldi ? [16, 32] : [19, 38];
const staleBadges = new Set();
const imageDataCache = new Map();
const badgeOvr = {color: '', text: ''};
// https://github.com/openstyles/stylus/issues/1287 Fenix can't use custom ImageData
const FIREFOX_ANDROID = FIREFOX && UA.mobile;
let isDark;
// https://github.com/openstyles/stylus/issues/335
let hasCanvas = loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
let hasCanvas = FIREFOX_ANDROID ? false : loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
.then(({data}) => (hasCanvas = data.some(b => b !== 255)));
addAPI(/** @namespace API */ {
@ -34,13 +38,17 @@ const iconMan = (() => {
chrome.webNavigation.onCommitted.addListener(({tabId, frameId}) => {
if (!frameId) tabMan.set(tabId, 'styleIds', undefined);
});
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'iframe') {
port.onDisconnect.addListener(onPortDisconnected);
}
});
colorScheme.onChange(val => {
isDark = val;
if (prefs.get('iconset') === -1) {
debounce(refreshAllIcons);
}
});
bgReady.all.then(() => {
prefs.subscribe([
'disableAll',
@ -92,9 +100,10 @@ const iconMan = (() => {
}
function getIconName(hasStyles = false) {
const iconset = prefs.get('iconset') === 1 ? 'light/' : '';
const i = prefs.get('iconset');
const prefix = i === 0 || i === -1 && isDark ? '' : 'light/';
const postfix = prefs.get('disableAll') ? 'x' : !hasStyles ? 'w' : '';
return `${iconset}$SIZE$${postfix}`;
return `${prefix}$SIZE$${postfix}`;
}
function refreshIcon(tabId, force = false) {
@ -132,7 +141,7 @@ const iconMan = (() => {
// Caches imageData for icon paths
async function loadImage(url) {
const {OffscreenCanvas} = self.createImageBitmap && self || {};
const {OffscreenCanvas} = !FIREFOX && self.createImageBitmap && self || {};
const img = OffscreenCanvas
? await createImageBitmap(await (await fetch(url)).blob())
: await new Promise((resolve, reject) =>

View File

@ -42,8 +42,9 @@ const navMan = (() => {
/** @this {string} type */
function onFakeNavigation(data) {
const {url, frameId} = data;
onNavigation.call(this, data);
msg.sendTab(data.tabId, {method: 'urlChanged'}, {frameId: data.frameId})
msg.sendTab(data.tabId, {method: 'urlChanged', url}, {frameId})
.catch(msg.ignoreError);
}
})();
@ -65,6 +66,21 @@ bgReady.all.then(() => {
{hostEquals: 'sleazyfork.org', urlMatches},
],
});
/*
* Removes the Get Stylus button on style pages.
* Not using manifest.json as adding a content script disables the extension on update.
*/
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
chrome.tabs.executeScript(tabId, {
file: '/content/install-hook-userstylesworld.js',
runAt: 'document_start',
});
}, {
url: [
{hostEquals: 'userstyles.world'},
],
});
/*
* FF misses some about:blank iframes so we inject our content script explicitly
*/

View File

@ -1,103 +0,0 @@
/* global addAPI */// common.js
'use strict';
/* CURRENTLY UNUSED */
(() => {
// begin:nanographql - Tiny graphQL client library
// Author: yoshuawuyts (https://github.com/yoshuawuyts)
// License: MIT
// Modified by DecentM to fit project standards
const getOpname = /(query|mutation) ?([\w\d-_]+)? ?\(.*?\)? \{/;
const gql = str => {
str = Array.isArray(str) ? str.join('') : str;
const name = getOpname.exec(str);
return variables => {
const data = {query: str};
if (variables) data.variables = JSON.stringify(variables);
if (name && name.length) {
const operationName = name[2];
if (operationName) data.operationName = name[2];
}
return JSON.stringify(data);
};
};
// end:nanographql
const api = 'https://api.openusercss.org';
const doQuery = async ({id}, queryString) => {
const query = gql(queryString);
return (await fetch(api, {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json',
}),
body: query({
id,
}),
})).json();
};
addAPI(/** @namespace- API */ { // TODO: remove "-" when this is implemented
/**
* This function can be used to retrieve a theme object from the
* GraphQL API, set above
*
* Example:
* chrome.runtime.sendMessage({
* 'method': 'oucThemeById',
* 'id': '5a2f819f7c57c751001b49df'
* }, console.log);
*
* @param {ID} $0.id MongoDB style ID
* @returns {Promise.<{data: object}>} The GraphQL result with the `theme` object
*/
oucThemeById: params => doQuery(params, `
query($id: ID!) {
theme(id: $id) {
_id
title
description
createdAt
lastUpdate
version
screenshots
user {
_id
displayname
}
}
}
`),
/**
* This function can be used to retrieve a user object from the
* GraphQL API, set above
*
* Example:
* chrome.runtime.sendMessage({
* 'method': 'oucUserById',
* 'id': '5a2f0361ba666f0b00b9c827'
* }, console.log);
*
* @param {ID} $0.id MongoDB style ID
* @returns {Promise.<{data: object}>} The GraphQL result with the `user` object
*/
oucUserById: params => doQuery(params, `
query($id: ID!) {
user(id: $id) {
_id
displayname
avatarUrl
smallAvatarUrl
bio
}
}
`),
});
})();

View File

@ -1,15 +0,0 @@
/* global chromeLocal */// storage-util.js
'use strict';
// Removing unused stuff from storage on extension update
// TODO: delete this by the middle of 2021
try {
localStorage.clear();
} catch (e) {}
setTimeout(async () => {
const del = Object.keys(await chromeLocal.get())
.filter(key => key.startsWith('usoSearchCache'));
if (del.length) chromeLocal.remove(del);
}, 15e3);

View File

@ -1,10 +1,12 @@
/* global API msg */// msg.js
/* global URLS stringAsRegExp tryRegExp */// toolbox.js
/* global bgReady compareRevision */// common.js
/* global calcStyleDigest styleCodeEmpty styleSectionGlobal */// sections-util.js
/* global CHROME URLS deepEqual isEmptyObj mapObj stringAsRegExp tryRegExp tryURL */// toolbox.js
/* global bgReady createCache uuidIndex */// common.js
/* global calcStyleDigest styleCodeEmpty */// sections-util.js
/* global db */
/* global prefs */
/* global tabMan */
/* global usercssMan */
/* global colorScheme */
'use strict';
/*
@ -16,18 +18,26 @@ The live preview feature relies on `runtime.connect` and `port.onDisconnect`
to cleanup the temporary code. See livePreview in /edit.
*/
const styleUtil = {};
/* exported styleMan */
const styleMan = (() => {
Object.assign(styleUtil, {
id2style,
handleSave,
uuid2style,
});
//#region Declarations
/** @typedef {{
style: StyleObj
preview?: StyleObj
appliesTo: Set<string>
style: StyleObj,
preview?: StyleObj,
appliesTo: Set<string>,
}} StyleMapData */
/** @type {Map<number,StyleMapData>} */
const dataMap = new Map();
const uuidIndex = new Map();
/** @typedef {Object<styleId,{id: number, code: string[]}>} StyleSectionsToApply */
/** @type {Map<string,{maybeMatch: Set<styleId>, sections: StyleSectionsToApply}>} */
const cachedStyleForUrl = createCache({
@ -42,16 +52,69 @@ const styleMan = (() => {
const compileRe = createCompiler(text => `^(${text})$`);
const compileSloppyRe = createCompiler(text => `^${text}$`);
const compileExclusion = createCompiler(buildExclusion);
const uuidv4 = crypto.randomUUID ? crypto.randomUUID.bind(crypto) : (() => {
const seeds = crypto.getRandomValues(new Uint16Array(8));
// 00001111-2222-M333-N444-555566667777
seeds[3] = seeds[3] & 0x0FFF | 0x4000; // UUID version 4, M = 4
seeds[4] = seeds[4] & 0x3FFF | 0x8000; // UUID variant 1, N = 8..0xB
return Array.from(seeds, hex4dashed).join('');
});
const MISSING_PROPS = {
name: style => `ID: ${style.id}`,
_id: () => uuidv4(),
_rev: () => Date.now(),
};
const DELETE_IF_NULL = ['id', 'customName', 'md5Url', 'originalMd5'];
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = init();
const INJ_ORDER = 'injectionOrder';
const order = {main: {}, prio: {}};
const orderWrap = {
id: INJ_ORDER,
value: mapObj(order, () => []),
_id: `${chrome.runtime.id}-${INJ_ORDER}`,
_rev: 0,
};
uuidIndex.addCustomId(orderWrap, {set: setOrder});
chrome.runtime.onConnect.addListener(handleLivePreview);
class MatchQuery {
constructor(url) {
this.url = url;
}
get urlWithoutHash() {
return this._set('urlWithoutHash', this.url.split('#', 1)[0]);
}
get urlWithoutParams() {
return this._set('urlWithoutParams', this.url.split(/[?#]/, 1)[0]);
}
get domain() {
return this._set('domain', tryURL(this.url).hostname);
}
get isOwnPage() {
return this._set('isOwnPage', this.url.startsWith(URLS.ownOrigin));
}
_set(name, value) {
Object.defineProperty(this, name, {value});
return value;
}
}
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = Promise.all([init(), prefs.ready]);
chrome.runtime.onConnect.addListener(port => {
if (port.name === 'livePreview') {
handleLivePreview(port);
} else if (port.name.startsWith('draft:')) {
handleDraft(port);
}
});
colorScheme.onChange(value => {
msg.broadcastExtension({method: 'colorScheme', value});
for (const {style} of dataMap.values()) {
if (colorScheme.SCHEMES.includes(style.preferScheme)) {
broadcastStyleUpdated(style, 'colorScheme');
}
}
});
//#endregion
//#region Exports
@ -61,17 +124,28 @@ const styleMan = (() => {
/** @returns {Promise<number>} style id */
async delete(id, reason) {
if (ready.then) await ready;
const data = id2data(id);
await db.exec('delete', id);
if (reason !== 'sync') {
API.sync.delete(data.style._id, Date.now());
}
for (const url of data.appliesTo) {
const {style, appliesTo} = dataMap.get(id);
const sync = reason !== 'sync';
const uuid = style._id;
db.styles.delete(id);
if (sync) API.sync.delete(uuid, Date.now());
for (const url of appliesTo) {
const cache = cachedStyleForUrl.get(url);
if (cache) delete cache.sections[id];
}
dataMap.delete(id);
uuidIndex.delete(data.style._id);
uuidIndex.delete(uuid);
mapObj(orderWrap.value, (group, type) => {
delete order[type][id];
const i = group.indexOf(uuid);
if (i >= 0) group.splice(i, 1);
});
setOrder(orderWrap, {calc: false});
if (style._usw && style._usw.token) {
// Must be called after the style is deleted from dataMap
API.usw.revoke(id);
}
API.drafts.delete(id);
await msg.broadcast({
method: 'styleDeleted',
style: {id},
@ -79,32 +153,23 @@ const styleMan = (() => {
return id;
},
/** @returns {Promise<number>} style id */
async deleteByUUID(_id, rev) {
if (ready.then) await ready;
const id = uuidIndex.get(_id);
const oldDoc = id && id2style(id);
if (oldDoc && compareRevision(oldDoc._rev, rev) <= 0) {
// FIXME: does it make sense to set reason to 'sync' in deleteByUUID?
return styleMan.delete(id, 'sync');
}
},
/** @returns {Promise<StyleObj>} */
async editSave(style) {
if (ready.then) await ready;
style = mergeWithMapped(style);
style.updateDate = Date.now();
return handleSave(await saveStyle(style), 'editSave');
return saveStyle(style, {reason: 'editSave'});
},
/** @returns {Promise<?StyleObj>} */
async find(filter) {
async find(...filters) {
if (ready.then) await ready;
const filterEntries = Object.entries(filter);
for (const {style} of dataMap.values()) {
if (filterEntries.every(([key, val]) => style[key] === val)) {
return style;
for (const filter of filters) {
const filterEntries = Object.entries(filter);
for (const {style} of dataMap.values()) {
if (filterEntries.every(([key, val]) => style[key] === val)) {
return style;
}
}
}
return null;
@ -113,25 +178,58 @@ const styleMan = (() => {
/** @returns {Promise<StyleObj[]>} */
async getAll() {
if (ready.then) await ready;
return Array.from(dataMap.values(), data2style);
return getAllAsArray();
},
/** @returns {Promise<StyleObj>} */
async getByUUID(uuid) {
/** @returns {Promise<Object<string,StyleObj[]>>}>} */
async getAllOrdered(keys) {
if (ready.then) await ready;
return id2style(uuidIndex.get(uuid));
const res = mapObj(orderWrap.value, group => group.map(uuid2style).filter(Boolean));
if (res.main.length + res.prio.length < dataMap.size) {
for (const {style} of dataMap.values()) {
if (!(style.id in order.main) && !(style.id in order.prio)) {
res.main.push(style);
}
}
}
return keys
? mapObj(res, group => group.map(style => mapObj(style, null, keys)))
: res;
},
getOrder: () => orderWrap.value,
/** @returns {Promise<string | {[remoteId:string]: styleId}>}>} */
async getRemoteInfo(id) {
if (ready.then) await ready;
if (id) return calcRemoteId(id2style(id));
const res = {};
for (const {style} of dataMap.values()) {
const [rid, vars] = calcRemoteId(style);
if (rid) res[rid] = [style.id, vars];
}
return res;
},
/** @returns {Promise<StyleSectionsToApply>} */
async getSectionsByUrl(url, id, isInitialApply) {
if (ready.then) await ready;
if (isInitialApply && prefs.get('disableAll')) {
return {disableAll: true};
return {
cfg: {
disableAll: true,
},
};
}
// TODO: enable in FF when it supports sourceURL comment in style elements (also options.html)
const {exposeStyleName} = CHROME && prefs.__values;
const sender = CHROME && this && this.sender || {};
if (sender.frameId === 0) {
/* Chrome hides text frament from location.href of the page e.g. #:~:text=foo
so we'll use the real URL reported by webNavigation API.
TODO: if FF will do the same, this won't work as is: FF reports onCommitted too late */
url = tabMan.get(sender.tab.id, 'url', 0) || url;
}
/* Chrome hides text frament from location.href of the page e.g. #:~:text=foo
so we'll use the real URL reported by webNavigation API */
const {tab, frameId} = this && this.sender || {};
url = tab && tabMan.get(tab.id, 'url', frameId) || url;
let cache = cachedStyleForUrl.get(url);
if (!cache) {
cache = {
@ -143,9 +241,9 @@ const styleMan = (() => {
} else if (cache.maybeMatch.size) {
buildCache(cache, url, Array.from(cache.maybeMatch, id2data).filter(Boolean));
}
return id
? cache.sections[id] ? {[id]: cache.sections[id]} : {}
: cache.sections;
return Object.assign({cfg: {exposeStyleName, order}},
id ? mapObj(cache.sections, null, [id])
: cache.sections);
},
/** @returns {Promise<StyleObj>} */
@ -162,10 +260,12 @@ const styleMan = (() => {
const result = [];
const styles = id
? [id2style(id)].filter(Boolean)
: Array.from(dataMap.values(), data2style);
const query = createMatchQuery(url);
: getAllAsArray();
const query = new MatchQuery(url);
for (const style of styles) {
let excluded = false;
let excludedScheme = false;
let included = false;
let sloppy = false;
let sectionMatched = false;
const match = urlMatchStyle(query, style);
@ -173,14 +273,17 @@ const styleMan = (() => {
// if (match === false) {
// continue;
// }
if (match === 'included') {
included = true;
}
if (match === 'excluded') {
excluded = true;
}
if (match === 'excludedScheme') {
excludedScheme = true;
}
for (const section of style.sections) {
if (styleSectionGlobal(section) && styleCodeEmpty(section.code)) {
continue;
}
const match = urlMatchSection(query, section);
const match = urlMatchSection(query, section, true);
if (match) {
if (match === 'sloppy') {
sloppy = true;
@ -189,8 +292,9 @@ const styleMan = (() => {
break;
}
}
if (sectionMatched) {
result.push(/** @namespace StylesByUrlResult */ {style, excluded, sloppy});
if (sectionMatched || included) {
result.push(/** @namespace StylesByUrlResult */ {
style, excluded, sloppy, excludedScheme, sectionMatched, included});
}
}
return result;
@ -199,18 +303,16 @@ const styleMan = (() => {
/** @returns {Promise<StyleObj[]>} */
async importMany(items) {
if (ready.then) await ready;
items.forEach(beforeSave);
const events = await db.exec('putMany', items);
return Promise.all(items.map((item, i) => {
afterSave(item, events[i]);
return handleSave(item, 'import');
}));
},
/** @returns {Promise<StyleObj>} */
async import(data) {
if (ready.then) await ready;
return handleSave(await saveStyle(data), 'import');
for (const style of items) {
beforeSave(style);
if (style.sourceCode && style.usercssData) {
await usercssMan.buildCode(style);
}
}
const events = await db.styles.putMany(items);
return Promise.all(items.map((item, i) =>
handleSave(item, {reason: 'import'}, events[i])
));
},
/** @returns {Promise<StyleObj>} */
@ -218,46 +320,23 @@ const styleMan = (() => {
if (ready.then) await ready;
reason = reason || dataMap.has(style.id) ? 'update' : 'install';
style = mergeWithMapped(style);
const url = !style.url && style.updateUrl && (
URLS.extractUsoArchiveInstallUrl(style.updateUrl) ||
URLS.extractGreasyForkInstallUrl(style.updateUrl)
);
if (url) style.url = style.installationUrl = url;
style.originalDigest = await calcStyleDigest(style);
// FIXME: update updateDate? what about usercss config?
return handleSave(await saveStyle(style), reason);
return saveStyle(style, {reason});
},
/** @returns {Promise<?StyleObj>} */
async putByUUID(doc) {
save: saveStyle,
async setOrder(value) {
if (ready.then) await ready;
const id = uuidIndex.get(doc._id);
if (id) {
doc.id = id;
} else {
delete doc.id;
}
const oldDoc = id && id2style(id);
let diff = -1;
if (oldDoc) {
diff = compareRevision(oldDoc._rev, doc._rev);
if (diff > 0) {
API.sync.put(oldDoc._id, oldDoc._rev);
return;
}
}
if (diff < 0) {
doc.id = await db.exec('put', doc);
uuidIndex.set(doc._id, doc.id);
return handleSave(doc, 'sync');
}
return setOrder({value}, {broadcast: true, sync: true});
},
/** @returns {Promise<number>} style id */
async toggle(id, enabled) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id), {enabled});
handleSave(await saveStyle(style), 'toggle', false);
await saveStyle(style, {reason: 'toggle'});
return id;
},
@ -271,6 +350,14 @@ const styleMan = (() => {
removeExclusion: removeIncludeExclude.bind(null, 'exclusions'),
/** @returns {Promise<?StyleObj>} */
removeInclusion: removeIncludeExclude.bind(null, 'inclusions'),
async config(id, prop, value) {
if (ready.then) await ready;
const style = Object.assign({}, id2style(id));
const {preview = {}} = dataMap.get(id);
style[prop] = preview[prop] = value;
return saveStyle(style, {reason: 'config'});
},
};
//#endregion
@ -283,12 +370,23 @@ const styleMan = (() => {
/** @returns {?StyleObj} */
function id2style(id) {
return (dataMap.get(id) || {}).style;
return (dataMap.get(Number(id)) || {}).style;
}
/** @returns {?StyleObj} */
function data2style(data) {
return data && data.style;
function uuid2style(uuid) {
return id2style(uuidIndex.get(uuid));
}
function calcRemoteId({md5Url, updateUrl, usercssData: ucd} = {}) {
let id;
id = (id = /\d+/.test(md5Url) || URLS.extractUsoArchiveId(updateUrl)) && `uso-${id}`
|| (id = URLS.extractUSwId(updateUrl)) && `usw-${id}`
|| '';
return id && [
id,
ucd && !isEmptyObj(ucd.vars),
];
}
/** @returns {StyleObj} */
@ -309,6 +407,7 @@ const styleMan = (() => {
style,
appliesTo: new Set(),
});
uuidIndex.set(style._id, style.id);
}
/** @returns {StyleObj} */
@ -318,10 +417,12 @@ const styleMan = (() => {
style);
}
function handleDraft(port) {
const id = port.name.split(':').pop();
port.onDisconnect.addListener(() => API.drafts.delete(Number(id) || id));
}
function handleLivePreview(port) {
if (port.name !== 'livePreview') {
return;
}
let id;
port.onMessage.addListener(style => {
if (!id) id = style.id;
@ -349,7 +450,7 @@ const styleMan = (() => {
throw new Error('The rule already exists');
}
style[type] = list.concat([rule]);
return handleSave(await saveStyle(style), 'styleSettings');
return saveStyle(style, {reason: 'config'});
}
async function removeIncludeExclude(type, id, rule) {
@ -360,10 +461,10 @@ const styleMan = (() => {
return;
}
style[type] = list.filter(r => r !== rule);
return handleSave(await saveStyle(style), 'styleSettings');
return saveStyle(style, {reason: 'config'});
}
function broadcastStyleUpdated(style, reason, method = 'styleUpdated', codeIsUpdated = true) {
function broadcastStyleUpdated(style, reason, method = 'styleUpdated') {
const {id} = style;
const data = id2data(id);
const excluded = new Set();
@ -373,10 +474,10 @@ const styleMan = (() => {
cache.maybeMatch.add(id);
continue;
}
const code = getAppliedCode(createMatchQuery(url), style);
const code = getAppliedCode(new MatchQuery(url), style);
if (code) {
updated.add(url);
cache.sections[id] = {id, code};
buildCacheEntry(cache, style, code);
} else {
excluded.add(url);
delete cache.sections[id];
@ -386,7 +487,6 @@ const styleMan = (() => {
return msg.broadcast({
method,
reason,
codeIsUpdated,
style: {
id,
md5Url: style.md5Url,
@ -408,39 +508,39 @@ const styleMan = (() => {
style._id = uuidv4();
}
style._rev = Date.now();
fixUsoMd5Issue(style);
fixKnownProblems(style);
}
function afterSave(style, newId) {
if (style.id == null) {
style.id = newId;
}
uuidIndex.set(style._id, style.id);
API.sync.put(style._id, style._rev);
}
async function saveStyle(style) {
async function saveStyle(style, handlingOptions) {
beforeSave(style);
const newId = await db.exec('put', style);
afterSave(style, newId);
return style;
const newId = await db.styles.put(style);
return handleSave(style, handlingOptions, newId);
}
function handleSave(style, reason, codeIsUpdated) {
const data = id2data(style.id);
function handleSave(style, {reason, broadcast = true}, id = style.id) {
if (style.id == null) style.id = id;
const data = id2data(id);
const method = data ? 'styleUpdated' : 'styleAdded';
if (!data) {
storeInMap(style);
} else {
data.style = style;
}
broadcastStyleUpdated(style, reason, method, codeIsUpdated);
if (reason !== 'sync') {
API.sync.putDoc(style);
}
if (broadcast) broadcastStyleUpdated(style, reason, method);
return style;
}
// get styles matching a URL, including sloppy regexps and excluded items.
function getAppliedCode(query, data) {
if (urlMatchStyle(query, data) !== true) {
const result = urlMatchStyle(query, data);
if (result === 'included') {
// return all sections
return data.sections.map(s => s.code);
}
if (result !== true) {
return;
}
const code = [];
@ -453,23 +553,19 @@ const styleMan = (() => {
}
async function init() {
const styles = await db.exec('getAll') || [];
const updated = styles.filter(style =>
addMissingProps(style) +
addCustomName(style));
const orderPromise = API.prefsDb.get(orderWrap.id);
const styles = await db.styles.getAll() || [];
const updated = await Promise.all(styles.map(fixKnownProblems).filter(Boolean));
if (updated.length) {
await db.exec('putMany', updated);
}
for (const style of styles) {
fixUsoMd5Issue(style);
storeInMap(style);
uuidIndex.set(style._id, style.id);
await db.styles.putMany(updated);
}
setOrder(await orderPromise, {store: false});
styles.forEach(storeInMap);
ready = true;
bgReady._resolveStyles();
}
function addMissingProps(style) {
function fixKnownProblems(style, initIndex, initArray) {
let res = 0;
for (const key in MISSING_PROPS) {
if (!style[key]) {
@ -477,22 +573,53 @@ const styleMan = (() => {
res = 1;
}
}
return res;
}
/** Upgrades the old way of customizing local names */
function addCustomName(style) {
let res = 0;
/* Upgrade the old way of customizing local names */
const {originalName} = style;
if (originalName) {
res = 1;
if (originalName !== style.name) {
style.customName = style.name;
style.name = originalName;
}
delete style.originalName;
res = 1;
}
return res;
/* wrong homepage url in 1.5.20-1.5.21 due to commit 1e5f118d */
for (const key of ['url', 'installationUrl']) {
const url = style[key];
const fixedUrl = url && url.replace(/([^:]\/)\//, '$1');
if (fixedUrl !== url) {
res = 1;
style[key] = fixedUrl;
}
}
let url;
/* USO bug, duplicate "update" subdomain, see #523 */
if ((url = style.md5Url) && url.includes('update.update.userstyles')) {
res = style.md5Url = url.replace('update.update.userstyles', 'update.userstyles');
}
/* Default homepage URL for external styles installed from a known distro */
if (
(!style.url || !style.installationUrl) &&
(url = style.updateUrl) &&
(url = URLS.extractGreasyForkInstallUrl(url) ||
URLS.extractUsoArchiveInstallUrl(url) ||
URLS.extractUSwInstallUrl(url)
)
) {
if (!style.url) res = style.url = url;
if (!style.installationUrl) res = style.installationUrl = url;
}
/* @import must precede `vars` that we add at beginning */
if (
initArray &&
!isEmptyObj((style.usercssData || {}).vars) &&
style.sections.some(({code}) =>
code.startsWith(':root {\n --') &&
/@import\s/i.test(code))
) {
return usercssMan.buildCode(style);
}
return res && style;
}
function urlMatchStyle(query, style) {
@ -505,43 +632,68 @@ const styleMan = (() => {
if (!style.enabled) {
return 'disabled';
}
if (!colorScheme.shouldIncludeStyle(style)) {
return 'excludedScheme';
}
if (
style.inclusions &&
style.inclusions.some(r => compileExclusion(r).test(query.urlWithoutParams))
) {
return 'included';
}
return true;
}
function urlMatchSection(query, section) {
function urlMatchSection(query, section, skipEmptyGlobal) {
let dd, ddL, pp, ppL, rr, rrL, uu, uuL;
if (
section.domains &&
section.domains.some(d => d === query.domain || query.domain.endsWith(`.${d}`))
(dd = section.domains) && (ddL = dd.length) && dd.some(urlMatchDomain, query) ||
(pp = section.urlPrefixes) && (ppL = pp.length) && pp.some(urlMatchPrefix, query) ||
/* Per the specification 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 */
(uu = section.urls) && (uuL = uu.length) && (
uu.includes(query.url) ||
uu.includes(query.urlWithoutHash)
) ||
(rr = section.regexps) && (rrL = rr.length) && rr.some(urlMatchRegexp, query)
) {
return true;
}
if (section.urlPrefixes && section.urlPrefixes.some(p => p && query.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(query.url) ||
section.urls.includes(query.urlWithoutHash)
)) {
return true;
}
if (section.regexps && section.regexps.some(r => compileRe(r).test(query.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(query.url))) {
if (rrL && rr.some(urlMatchRegexpSloppy, query)) {
return 'sloppy';
}
// TODO: check for invalid regexps?
return styleSectionGlobal(section);
return !rrL && !ppL && !uuL && !ddL &&
!query.isOwnPage && // We allow only intentionally targeted sections for own pages
(!skipEmptyGlobal || !styleCodeEmpty(section.code));
}
/** @this {MatchQuery} */
function urlMatchDomain(d) {
const _d = this.domain;
return d === _d ||
_d[_d.length - d.length - 1] === '.' && _d.endsWith(d);
}
/** @this {MatchQuery} */
function urlMatchPrefix(p) {
return p && this.url.startsWith(p);
}
/** @this {MatchQuery} */
function urlMatchRegexp(r) {
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
compileRe(r).test(this.url);
}
/** @this {MatchQuery} */
function urlMatchRegexpSloppy(r) {
return (!this.isOwnPage || /\bextension\b/.test(r)) &&
compileSloppyRe(r).test(this.url);
}
function createCompiler(compile) {
@ -581,82 +733,28 @@ const styleMan = (() => {
'$';
}
// 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 createMatchQuery(url) {
let urlWithoutHash;
let urlWithoutParams;
let domain;
return {
url,
get urlWithoutHash() {
if (!urlWithoutHash) {
urlWithoutHash = url.split('#')[0];
}
return urlWithoutHash;
},
get urlWithoutParams() {
if (!urlWithoutParams) {
const u = createURL(url);
urlWithoutParams = u.origin + u.pathname;
}
return urlWithoutParams;
},
get domain() {
if (!domain) {
const u = createURL(url);
domain = u.hostname;
}
return domain;
},
};
}
function buildCache(cache, url, styleList) {
const query = createMatchQuery(url);
const query = new MatchQuery(url);
for (const {style, appliesTo, preview} of styleList) {
const code = getAppliedCode(query, preview || style);
if (code) {
const id = style.id;
cache.sections[id] = {id, code};
buildCacheEntry(cache, style, code);
appliesTo.add(url);
}
}
}
function createURL(url) {
try {
return new URL(url);
} catch (err) {
return {
hash: '',
host: '',
hostname: '',
href: '',
origin: '',
password: '',
pathname: '',
port: '',
protocol: '',
search: '',
searchParams: new URLSearchParams(),
username: '',
};
}
function buildCacheEntry(cache, style, code = style.code) {
cache.sections[style.id] = {
code,
id: style.id,
name: style.customName || style.name,
};
}
function uuidv4() {
const seeds = crypto.getRandomValues(new Uint16Array(8));
// 00001111-2222-M333-N444-555566667777
seeds[3] = seeds[3] & 0x0FFF | 0x4000; // UUID version 4, M = 4
seeds[4] = seeds[4] & 0x3FFF | 0x8000; // UUID variant 1, N = 8..0xB
return Array.from(seeds, hex4dashed).join('');
/** @returns {StyleObj[]} */
function getAllAsArray() {
return Array.from(dataMap.values(), v => v.style);
}
/** uuidv4 helper: converts to a 4-digit hex string and adds "-" at required positions */
@ -664,66 +762,30 @@ const styleMan = (() => {
return (num + 0x10000).toString(16).slice(-4) + (i >= 1 && i <= 4 ? '-' : '');
}
async function setOrder(data, {broadcast, calc = true, store = true, sync} = {}) {
if (!data || !data.value || deepEqual(data.value, orderWrap.value)) {
return;
}
Object.assign(orderWrap, data, sync && {_rev: Date.now()});
if (calc) {
for (const [type, group] of Object.entries(data.value)) {
const dst = order[type] = {};
group.forEach((uuid, i) => {
const id = uuidIndex.get(uuid);
if (id) dst[id] = i;
});
}
}
if (broadcast) {
msg.broadcast({method: 'styleSort', order});
}
if (store) {
await API.prefsDb.put(orderWrap, orderWrap.id);
}
if (sync) {
API.sync.putDoc(orderWrap);
}
}
//#endregion
})();
/** Creates 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(id) {
const item = map.get(id);
return item && item.data;
},
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;
},
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;
},
clear() {
map.clear();
index = lastIndex = 0;
},
has: id => map.has(id),
*entries() {
for (const [id, item] of map) {
yield [id, item.data];
}
},
*values() {
for (const item of map.values()) {
yield item.data;
}
},
get size() {
return map.size;
},
};
}

View File

@ -56,6 +56,7 @@
return NOP;
}
return API.styles.getSectionsByUrl(url, id).then(sections => {
delete sections.cfg;
const tasks = [];
for (const section of Object.values(sections)) {
const styleId = section.id;

View File

@ -1,5 +1,5 @@
/* global API */// msg.js
/* global CHROME ignoreChromeError */// toolbox.js
/* global CHROME URLS ignoreChromeError */// toolbox.js
/* global prefs */
'use strict';
@ -49,6 +49,12 @@
if (CHROME && !off) {
chrome.webNavigation.onCommitted.addListener(injectData, {url: [{urlPrefix: 'http'}]});
}
if (CHROME) {
chrome.webRequest.onBeforeRequest.addListener(openNamedStyle, {
urls: [URLS.ownOrigin + '*.user.css'],
types: ['main_frame'],
}, ['blocking']);
}
state.csp = csp;
state.off = off;
state.xhr = xhr;
@ -112,8 +118,8 @@
// Allow style assets
patchCspSrc(src, 'img-src', 'data:', '*');
patchCspSrc(src, 'font-src', 'data:', '*');
// Allow our DOM styles
patchCspSrc(src, 'style-src', "'unsafe-inline'");
// Allow our DOM styles, allow @import from any URL
patchCspSrc(src, 'style-src', "'unsafe-inline'", '*');
// Allow our XHR cookies in CSP sandbox (known case: raw github urls)
if (src.sandbox && !src.sandbox.includes('allow-same-origin')) {
src.sandbox.push('allow-same-origin');
@ -146,6 +152,14 @@
}
}
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
function openNamedStyle(req) {
if (!req.url.includes('?')) { // skipping our usercss installer
chrome.tabs.update(req.tabId, {url: 'edit.html?id=' + req.url.split('#')[1]});
return {cancel: true};
}
}
function req2key(req) {
return req.tabId + ':' + req.frameId;
}

View File

@ -1,8 +1,10 @@
/* global API msg */// msg.js
/* global chromeLocal */// storage-util.js
/* global compareRevision */// common.js
/* global bgReady uuidIndex */// common.js
/* global chromeLocal chromeSync */// storage-util.js
/* global db */
/* global iconMan */
/* global prefs */
/* global styleUtil */
/* global tokenMan */
'use strict';
@ -18,6 +20,7 @@ const syncMan = (() => {
disconnecting: 'disconnecting',
});
const STORAGE_KEY = 'sync/state/';
const NO_LOGIN = ['webdav'];
const status = /** @namespace SyncManager.Status */ {
STATES,
state: STATES.disconnected,
@ -27,11 +30,12 @@ const syncMan = (() => {
errorMessage: null,
login: false,
};
const compareRevision = (rev1, rev2) => rev1 - rev2;
let lastError = null;
let ctrl;
let currentDrive;
/** @type {Promise|boolean} will be `true` to avoid wasting a microtask tick on each `await` */
let ready = prefs.ready.then(() => {
let ready = bgReady.styles.then(() => {
ready = true;
prefs.subscribe('sync.enabled',
(_, val) => val === 'none'
@ -40,9 +44,9 @@ const syncMan = (() => {
{runNow: true});
});
chrome.alarms.onAlarm.addListener(info => {
if (info.name === 'syncNow') {
syncMan.syncNow();
chrome.alarms.onAlarm.addListener(async ({name}) => {
if (name === 'syncNow') {
await syncMan.syncNow();
}
});
@ -78,11 +82,21 @@ const syncMan = (() => {
}
},
async put(...args) {
async putDoc({_id, _rev}) {
if (ready.then) await ready;
if (!currentDrive) return;
schedule();
return ctrl.put(...args);
return ctrl.put(_id, _rev);
},
async setDriveOptions(driveName, options) {
const key = `secure/sync/driveOptions/${driveName}`;
await chromeSync.setValue(key, options);
},
async getDriveOptions(driveName) {
const key = `secure/sync/driveOptions/${driveName}`;
return await chromeSync.getValue(key) || {};
},
async start(name, fromPref = false) {
@ -90,14 +104,14 @@ const syncMan = (() => {
if (!ctrl) await initController();
if (currentDrive) return;
currentDrive = getDrive(name);
currentDrive = await getDrive(name);
ctrl.use(currentDrive);
status.state = STATES.connecting;
status.currentDriveName = currentDrive.name;
emitStatusChange();
if (fromPref) {
if (fromPref || NO_LOGIN.includes(currentDrive.name)) {
status.login = true;
} else {
try {
@ -150,9 +164,9 @@ const syncMan = (() => {
status.errorMessage = null;
lastError = null;
} catch (err) {
err.message = translateErrorMessage(err);
status.errorMessage = err.message;
lastError = err;
if (isGrantError(err)) {
status.login = false;
}
@ -165,19 +179,35 @@ const syncMan = (() => {
//#region Utils
async function initController() {
await require(['/vendor/db-to-cloud/db-to-cloud.min']); /* global dbToCloud */
await require(['/vendor/db-to-cloud/db-to-cloud']); /* global dbToCloud */
ctrl = dbToCloud.dbToCloud({
onGet(id) {
return API.styles.getByUUID(id);
onGet: styleUtil.uuid2style,
async onPut(doc) {
const id = uuidIndex.get(doc._id);
const oldCust = uuidIndex.custom[id];
const oldDoc = oldCust || styleUtil.id2style(id);
const diff = oldDoc ? compareRevision(oldDoc._rev, doc._rev) : -1;
if (!diff) return;
if (diff > 0) {
syncMan.putDoc(oldDoc);
} else if (oldCust) {
uuidIndex.custom[id] = doc;
} else {
delete doc.id;
if (id) doc.id = id;
doc.id = await db.styles.put(doc);
await styleUtil.handleSave(doc, {reason: 'sync'});
}
},
onPut(doc) {
return API.styles.putByUUID(doc);
},
onDelete(id, rev) {
return API.styles.deleteByUUID(id, rev);
onDelete(_id, rev) {
const id = uuidIndex.get(_id);
const oldDoc = styleUtil.id2style(id);
return oldDoc &&
compareRevision(oldDoc._rev, rev) <= 0 &&
API.styles.delete(id, 'sync');
},
async onFirstSync() {
for (const i of await API.styles.getAll()) {
for (const i of Object.values(uuidIndex.custom).concat(await API.styles.getAll())) {
ctrl.put(i._id, i._rev);
}
},
@ -199,6 +229,9 @@ const syncMan = (() => {
setState(drive, state) {
return chromeLocal.setValue(STORAGE_KEY + drive.name, state);
},
retryMaxAttempts: 10,
retryExp: 1.2,
retryDelay: 6,
});
}
@ -208,12 +241,16 @@ const syncMan = (() => {
}
function isNetworkError(err) {
return err.name === 'TypeError' && /networkerror|failed to fetch/i.test(err.message);
return (
err.name === 'TypeError' && /networkerror|failed to fetch/i.test(err.message) ||
err.code === 502
);
}
function isGrantError(err) {
if (err.code === 401) return true;
if (err.code === 400 && /invalid_grant/.test(err.message)) return true;
if (err.name === 'TokenError') return true;
return false;
}
@ -233,21 +270,38 @@ const syncMan = (() => {
}
}
function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
return dbToCloud.drive[name]({
getAccessToken: () => tokenMan.getToken(name),
});
async function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive' || name === 'webdav') {
const options = await syncMan.getDriveOptions(name);
options.getAccessToken = () => tokenMan.getToken(name);
options.fetch = name === 'webdav' ? fetchWebDAV.bind(options) : fetch;
return dbToCloud.drive[name](options);
}
throw new Error(`unknown cloud name: ${name}`);
}
/** @this {Object} DriveOptions */
function fetchWebDAV(url, init = {}) {
init.credentials = 'omit'; // circumventing nextcloud CSRF token error
init.headers = Object.assign({}, init.headers, {
Authorization: `Basic ${btoa(`${this.username || ''}:${this.password || ''}`)}`,
});
return fetch(url, init);
}
function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', {
delayInMinutes: delay,
delayInMinutes: delay, // fractional values are supported
periodInMinutes: SYNC_INTERVAL,
});
}
function translateErrorMessage(err) {
if (err.name === 'LockError') {
return browser.i18n.getMessage('syncErrorLock', new Date(err.expire).toLocaleString([], {timeStyle: 'short'}));
}
return err.message || String(err);
}
//#endregion
})();

View File

@ -1,4 +1,4 @@
/* global FIREFOX getActiveTab waitForTabUrl */// toolbox.js
/* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js
/* global chromeLocal */// storage-util.js
'use strict';
@ -32,10 +32,11 @@ const tokenMan = (() => {
},
tokenURL: 'https://oauth2.googleapis.com/token',
scopes: ['https://www.googleapis.com/auth/drive.appdata'],
revoke: token => {
const params = {token};
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
},
// FIXME: https://github.com/openstyles/stylus/issues/1248
// revoke: token => {
// const params = {token};
// return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
// },
},
onedrive: {
flow: 'code',
@ -43,23 +44,42 @@ const tokenMan = (() => {
clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w',
authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
redirect_uri: FIREFOX ?
'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' :
'https://' + location.hostname + '.chromiumapp.org/',
scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
},
userstylesworld: {
flow: 'code',
clientId: 'zeDmKhJIfJqULtcrGMsWaxRtWHEimKgS',
clientSecret: 'wqHsvTuThQmXmDiVvOpZxPwSIbyycNFImpAOTxjaIRqDbsXcTOqrymMJKsOMuibFaij' +
'ZZAkVYTDbLkQuYFKqgpMsMlFlgwQOYHvHFbgxQHDTwwdOroYhOwFuekCwXUlk',
authURL: URLS.usw + 'api/oauth/style/link',
tokenURL: URLS.usw + 'api/oauth/token',
redirect_uri: 'https://gusted.xyz/callback_helper/',
},
};
const NETWORK_LATENCY = 30; // seconds
const DEFAULT_REDIRECT_URI = 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/';
let alwaysUseTab = FIREFOX ? false : null;
let alwaysUseTab = !chrome.windows || (FIREFOX ? false : null);
class TokenError extends Error {
constructor(provider, message) {
super(`[${provider}] ${message}`);
this.name = 'TokenError';
this.provider = provider;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TokenError);
}
}
}
return {
buildKeys(name) {
buildKeys(name, hooks) {
const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`;
const k = {
TOKEN: `secure/token/${name}/token`,
EXPIRE: `secure/token/${name}/expire`,
REFRESH: `secure/token/${name}/refresh`,
TOKEN: `${prefix}token`,
EXPIRE: `${prefix}expire`,
REFRESH: `${prefix}refresh`,
};
k.LIST = Object.values(k);
return k;
@ -69,8 +89,8 @@ const tokenMan = (() => {
return AUTH[name].clientId;
},
async getToken(name, interactive) {
const k = tokenMan.buildKeys(name);
async getToken(name, interactive, hooks) {
const k = tokenMan.buildKeys(name, hooks);
const obj = await chromeLocal.get(k.LIST);
if (obj[k.TOKEN]) {
if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
@ -81,14 +101,14 @@ const tokenMan = (() => {
}
}
if (!interactive) {
throw new Error(`Invalid token: ${name}`);
throw new TokenError(name, 'Token is missing');
}
return authUser(name, k, interactive);
return authUser(k, name, interactive, hooks);
},
async revokeToken(name) {
async revokeToken(name, hooks) {
const provider = AUTH[name];
const k = tokenMan.buildKeys(name);
const k = tokenMan.buildKeys(name, hooks);
if (provider.revoke) {
try {
const token = await chromeLocal.getValue(k.TOKEN);
@ -103,7 +123,7 @@ const tokenMan = (() => {
async function refreshToken(name, k, obj) {
if (!obj[k.REFRESH]) {
throw new Error('No refresh token');
throw new TokenError(name, 'No refresh token');
}
const provider = AUTH[name];
const body = {
@ -123,15 +143,15 @@ const tokenMan = (() => {
return handleTokenResult(result, k);
}
async function authUser(name, k, interactive = false) {
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow.min']);
async function authUser(keys, name, interactive = false, hooks = null) {
await require(['/vendor/webext-launch-web-auth-flow/webext-launch-web-auth-flow']);
/* global webextLaunchWebAuthFlow */
const provider = AUTH[name];
const state = Math.random().toFixed(8).slice(2);
const query = {
response_type: provider.flow,
client_id: provider.clientId,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(),
redirect_uri: provider.redirect_uri || DEFAULT_REDIRECT_URI,
state,
};
if (provider.scopes) {
@ -143,17 +163,25 @@ const tokenMan = (() => {
if (alwaysUseTab == null) {
alwaysUseTab = await detectVivaldiWebRequestBug();
}
if (hooks) hooks.query(query);
const url = `${provider.authURL}?${new URLSearchParams(query)}`;
const width = Math.min(screen.availWidth - 100, 800);
const height = Math.min(screen.availHeight - 100, 800);
const wnd = !alwaysUseTab && await browser.windows.getLastFocused();
const finalUrl = await webextLaunchWebAuthFlow({
url,
alwaysUseTab,
interactive,
redirect_uri: query.redirect_uri,
windowOptions: {
windowOptions: wnd && Object.assign({
state: 'normal',
width: Math.min(screen.width - 100, 800),
height: Math.min(screen.height - 100, 800),
},
width,
height,
}, wnd.state !== 'minimized' && {
// Center the popup to the current window
top: Math.ceil(wnd.top + (wnd.height - width) / 2),
left: Math.ceil(wnd.left + (wnd.width - width) / 2),
}),
});
const params = new URLSearchParams(
provider.flow === 'token' ?
@ -161,7 +189,7 @@ const tokenMan = (() => {
new URL(finalUrl).search.slice(1)
);
if (params.get('state') !== state) {
throw new Error(`Unexpected state: ${params.get('state')}, expected: ${state}`);
throw new TokenError(name, `Unexpected state: ${params.get('state')}, expected: ${state}`);
}
let result;
if (provider.flow === 'token') {
@ -177,13 +205,14 @@ const tokenMan = (() => {
grant_type: 'authorization_code',
client_id: provider.clientId,
redirect_uri: query.redirect_uri,
state,
};
if (provider.clientSecret) {
body.client_secret = provider.clientSecret;
}
result = await postQuery(provider.tokenURL, body);
}
return handleTokenResult(result, k);
return handleTokenResult(result, keys);
}
async function handleTokenResult(result, k) {
@ -219,7 +248,7 @@ const tokenMan = (() => {
// Workaround for https://github.com/openstyles/stylus/issues/1182
// Note that modern Vivaldi isn't exposed in `navigator.userAgent` but it adds `extData` to tabs
const anyTab = await getActiveTab() || (await browser.tabs.query({}))[0];
if (anyTab && !anyTab.extData) {
if (anyTab && !(anyTab.extData || anyTab.vivExtData)) {
return false;
}
let bugged = true;

View File

@ -1,7 +1,8 @@
/* global API */// msg.js
/* global RX_META URLS debounce download ignoreChromeError */// toolbox.js
/* global calcStyleDigest styleJSONseemsValid styleSectionsEqual */ // sections-util.js
/* global RX_META URLS debounce deepMerge download ignoreChromeError */// toolbox.js
/* global calcStyleDigest styleSectionsEqual */ // sections-util.js
/* global chromeLocal */// storage-util.js
/* global compareVersion */// cmpver.js
/* global db */
/* global prefs */
'use strict';
@ -22,13 +23,14 @@ const updateMan = (() => {
ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style',
};
const USO_STYLES_API = `${URLS.uso}api/v1/styles/`;
const RH_ETAG = {responseHeaders: ['etag']}; // a hashsum of file contents
const RX_DATE2VER = new RegExp([
/^(\d{4})/,
/(1(?:0|[12](?=\d\d))?|[2-9])/, // in ambiguous cases like yyyy123 the month will be 1
/([1-2][0-9]?|3[0-1]?|[4-9])/,
/\.(0|1[0-9]?|2[0-3]?|[3-9])/,
/\.(0|[1-5][0-9]?|[6-9])$/,
/(0[1-9]|1(?:0|[12](?=\d\d))?|[2-9])/, // in ambiguous cases like yyyy123 the month will be 1
/(0[1-9]|[1-2][0-9]?|3[0-1]?|[4-9])/,
/\.([01][0-9]?|2[0-3]?|[3-9])/,
/\.([0-5][0-9]?|[6-9])$/,
].map(rx => rx.source).join(''));
const ALARM_NAME = 'scheduledUpdate';
const MIN_INTERVAL_MS = 60e3;
@ -36,6 +38,7 @@ const updateMan = (() => {
503, // service unavailable
429, // too many requests
];
let usoReferers = 0;
let lastUpdateTime;
let checkingAll = false;
let logQueue = [];
@ -62,7 +65,7 @@ const updateMan = (() => {
checkingAll = true;
const port = observe && chrome.runtime.connect({name: 'updater'});
const styles = (await API.styles.getAll())
.filter(style => style.updateUrl);
.filter(style => style.updateUrl && style.updatable !== false);
if (port) port.postMessage({count: styles.length});
log('');
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
@ -77,17 +80,17 @@ const updateMan = (() => {
/**
* @param {{
id?: number
style?: StyleObj
port?: chrome.runtime.Port
save?: boolean = true
ignoreDigest?: boolean
id?: number,
style?: StyleObj,
port?: chrome.runtime.Port,
save?: boolean,
ignoreDigest?: boolean,
}} opts
* @returns {{
style: StyleObj
updated?: boolean
error?: any
STATES: UpdaterStates
style: StyleObj,
updated?: boolean,
error?: any,
STATES: UpdaterStates,
}}
Original style digests are calculated in these cases:
@ -112,12 +115,13 @@ const updateMan = (() => {
save,
} = opts;
if (!id) id = style.id;
const ucd = style.usercssData;
const {md5Url} = style;
let {usercssData: ucd, updateUrl} = style;
let res, state;
try {
await checkIfEdited();
res = {
style: await (ucd ? updateUsercss : updateUSO)().then(maybeSave),
style: await (ucd && !md5Url ? updateUsercss : updateUSO)().then(maybeSave),
updated: true,
};
state = STATES.UPDATED;
@ -126,7 +130,7 @@ const updateMan = (() => {
err && err.message ||
err;
res = {error, style, STATES};
state = `${STATES.SKIPPED} (${error})`;
state = `${STATES.SKIPPED} (${Array.isArray(err) ? err[0].message : error})`;
}
log(`${state} #${id} ${style.customName || style.name}`);
if (port) port.postMessage(res);
@ -141,90 +145,59 @@ const updateMan = (() => {
}
async function updateUSO() {
const url = URLS.makeUsoArchiveCodeUrl(style.md5Url.match(/\d+/)[0]);
const req = await tryDownload(url, RH_ETAG).catch(() => null);
if (req) {
return updateToUSOArchive(url, req);
}
const md5 = await tryDownload(style.md5Url);
const md5 = await tryDownload(md5Url);
if (!md5 || md5.length !== 32) {
return Promise.reject(STATES.ERROR_MD5);
}
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.SAME_MD5);
}
const json = await tryDownload(style.updateUrl, {responseType: 'json'});
if (!styleJSONseemsValid(json)) {
return Promise.reject(STATES.ERROR_JSON);
let varsUrl = '';
if (!ucd) {
ucd = {};
varsUrl = updateUrl;
updateUrl = style.updateUrl = `${USO_STYLES_API}${md5Url.match(/\/(\d+)/)[1]}`;
}
usoSpooferStart();
let json;
try {
json = await tryDownload(style.updateUrl, {responseType: 'json'});
json = await updateUsercss(json.css) ||
(await API.uso.toUsercss(json)).style;
if (varsUrl) await API.uso.useVarsUrl(json, varsUrl);
} finally {
usoSpooferStop();
}
// USO may not provide a correctly updated originalMd5 (#555)
json.originalMd5 = md5;
return json;
}
async function updateToUSOArchive(url, req) {
// UserCSS metadata may be embedded in the original USO style so let's use its updateURL
const [meta2] = req.response.replace(RX_META, '').match(RX_META) || [];
if (meta2 && meta2.includes('@updateURL')) {
const {updateUrl} = await API.usercss.buildMeta({sourceCode: meta2}).catch(() => ({}));
if (updateUrl) {
url = updateUrl;
req = await tryDownload(url, RH_ETAG);
}
async function updateUsercss(css) {
let oldVer = ucd.version;
let {etag: oldEtag, updateUrl} = style;
const m2 = (css || URLS.extractUsoArchiveId(updateUrl)) &&
await getUsoEmbeddedMeta(css);
if (m2 && m2.updateUrl) {
updateUrl = m2.updateUrl;
oldVer = m2.usercssData.version || '0';
oldEtag = '';
} else if (css) {
return;
}
const json = await API.usercss.buildMeta({
id,
etag: req.headers.etag,
md5Url: null,
originalMd5: null,
sourceCode: req.response,
updateUrl: url,
url: URLS.extractUsoArchiveInstallUrl(url),
});
const varUrlValues = style.updateUrl.split('?')[1];
const varData = json.usercssData.vars;
if (varUrlValues && varData) {
const IK = 'ik-';
const IK_LEN = IK.length;
for (let [key, val] of new URLSearchParams(varUrlValues)) {
if (!key.startsWith(IK)) continue;
key = key.slice(IK_LEN);
const varDef = varData[key];
if (!varDef) continue;
if (varDef.options) {
let sel = val.startsWith(IK) && getVarOptByName(varDef, val.slice(IK_LEN));
if (!sel) {
key += '-custom';
sel = getVarOptByName(varDef, key + '-dropdown');
if (sel) varData[key].value = val;
}
if (sel) varDef.value = sel.name;
} else {
varDef.value = val;
}
}
}
return API.usercss.buildCode(json);
}
async function updateUsercss() {
if (style.etag && style.etag === await downloadEtag()) {
if (oldEtag && oldEtag === await downloadEtag()) {
return Promise.reject(STATES.SAME_CODE);
}
// TODO: when sourceCode is > 100kB use http range request(s) for version check
const {headers: {etag}, response} = await tryDownload(style.updateUrl, RH_ETAG);
/* There's a bug? in Chrome which occurs only in a packaged crx:
* DOM script for semver fires 'load' event before the script actually runs.
* Since the conditions for the bug are rare we'll simply load in parallel */
const [json] = await Promise.all([
API.usercss.buildMeta({sourceCode: response, etag}),
require(['/vendor/semver-bundle/semver']), /* global semverCompare */
]);
const delta = semverCompare(json.usercssData.version, ucd.version);
const {headers: {etag}, response} = await tryDownload(updateUrl, RH_ETAG);
const json = await API.usercss.buildMeta({sourceCode: response, etag, updateUrl});
const delta = compareVersion(json.usercssData.version, oldVer);
let err;
if (!delta && !ignoreDigest) {
// re-install is invalid in a soft upgrade
err = response === style.sourceCode ? STATES.SAME_CODE : STATES.SAME_VERSION;
err = response === style.sourceCode
? STATES.SAME_CODE
: !URLS.isLocalhost(updateUrl) && STATES.SAME_VERSION;
}
if (delta < 0) {
// downgrade is always invalid
@ -233,7 +206,7 @@ const updateMan = (() => {
if (err && etag && !style.etag) {
// first check of ETAG, gonna write it directly to DB as it's too trivial to sync or announce
style.etag = etag;
await db.exec('put', style);
await db.styles.put(style);
}
return err
? Promise.reject(err)
@ -263,6 +236,7 @@ const updateMan = (() => {
let {retryDelay = 1000} = opts;
while (true) {
try {
params = deepMerge(params || {}, {headers: {'Cache-Control': 'no-cache'}});
return await download(url, params);
} catch (code) {
if (!RETRY_ERRORS.includes(code) ||
@ -282,16 +256,18 @@ const updateMan = (() => {
}
function getDateFromVer(style) {
const m = style.updateUrl.startsWith(URLS.usoArchiveRaw) &&
style.usercssData.version.match(RX_DATE2VER);
const m = RX_DATE2VER.exec((style.usercssData || {}).version);
if (m) {
m[2]--; // month is 0-based in `Date` constructor
return new Date(...m.slice(1)).getTime();
}
}
function getVarOptByName(varDef, name) {
return varDef.options.find(o => o.name === name);
/** UserCSS metadata may be embedded in the original USO style so let's use its updateURL */
function getUsoEmbeddedMeta(code = style.sourceCode) {
const isRaw = arguments[0];
const m = code.includes('@updateURL') && (isRaw ? code : code.replace(RX_META, '')).match(RX_META);
return m && API.usercss.buildMeta({sourceCode: m[0]}).catch(() => null);
}
}
@ -341,4 +317,32 @@ const updateMan = (() => {
logLastWriteTime = Date.now();
logQueue = [];
}
function usoSpooferStart() {
if (++usoReferers === 1) {
chrome.webRequest.onBeforeSendHeaders.addListener(
usoSpoofer,
{types: ['xmlhttprequest'], urls: [USO_STYLES_API + '*']},
['blocking', 'requestHeaders', chrome.webRequest.OnBeforeSendHeadersOptions.EXTRA_HEADERS]
.filter(Boolean));
}
}
function usoSpooferStop() {
if (--usoReferers <= 0) {
usoReferers = 0;
chrome.webRequest.onBeforeSendHeaders.removeListener(usoSpoofer);
}
}
/** @param {chrome.webRequest.WebResponseHeadersDetails | browser.webRequest._OnBeforeSendHeadersDetails} info */
function usoSpoofer(info) {
if (info.tabId < 0 && URLS.ownOrigin.startsWith(info.initiator || info.originUrl || '')) {
const {requestHeaders: hh} = info;
const i = (hh.findIndex(h => /^referer$/i.test(h.name)) + 1 || hh.push({})) - 1;
hh[i].name = 'referer';
hh[i].value = URLS.uso;
return {requestHeaders: hh};
}
}
})();

View File

@ -35,9 +35,9 @@ bgReady.all.then(() => {
chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, {
urls: [
URLS.usoArchiveRaw + 'usercss/*.user.css',
'*://greasyfork.org/scripts/*/code/*.user.css',
'*://sleazyfork.org/scripts/*/code/*.user.css',
URLS.usw + 'api/style/*.user.css',
...URLS.usoArchiveRaw.map(s => s + 'usercss/*.user.css'),
...['greasy', 'sleazy'].map(s => `*://${s}fork.org/scripts/*/code/*.user.css`),
...[].concat(
...Object.entries(maybeDistro)
.map(([host, {glob}]) => makeUsercssGlobs(host, glob))),
@ -74,6 +74,10 @@ bgReady.all.then(() => {
) && download(url);
}
function makeInstallerUrl(url) {
return `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
}
function makeUsercssGlobs(host, path) {
return '%css,%css?*,%styl,%styl?*'.replace(/%/g, `*://${host}${path}.user.`).split(',');
}
@ -82,11 +86,11 @@ bgReady.all.then(() => {
if (url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!oldUrl.startsWith(URLS.installUsercss)) {
!oldUrl.startsWith(makeInstallerUrl(url))) {
const inTab = url.startsWith('file:') && !chrome.app;
const code = await (inTab ? loadFromFile : loadFromUrl)(tabId, url);
if (!/^\s*</.test(code) && RX_META.test(code)) {
openInstallerPage(tabId, url, {code, inTab});
await openInstallerPage(tabId, url, {code, inTab});
}
}
}
@ -99,25 +103,33 @@ bgReady.all.then(() => {
openInstallerPage(tabId, url, {});
// Silently suppress navigation.
// Don't redirect to the install URL as it'll flash the text!
return {redirectUrl: 'javascript:void 0'}; // eslint-disable-line no-script-url
return {cancel: true};
}
}
function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
async function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = makeInstallerUrl(url);
if (inTab) {
browser.tabs.get(tabId).then(tab =>
openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
}));
} else {
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
chrome.tabs.update(tabId, {url: newUrl});
const tab = await browser.tabs.get(tabId);
return openURL({
url: `${newUrl}&tabId=${tabId}`,
active: tab.active,
index: tab.index + 1,
openerTabId: tabId,
currentWindow: null,
});
}
const timer = setTimeout(clearInstallCode, 10e3, url);
installCodeCache[url] = {code, timer};
try {
await browser.tabs.update(tabId, {url: newUrl});
} catch (err) {
// FIXME: remove this when kiwi supports tabs.update
// https://github.com/openstyles/stylus/issues/1367
if (/Tabs cannot be edited right now/i.test(err.message)) {
return browser.tabs.create({url: newUrl});
}
throw err;
}
}

View File

@ -12,10 +12,12 @@ const usercssMan = {
name: null,
}),
async assignVars(style, oldStyle) {
/** `src` is a style or vars */
async assignVars(style, src) {
const meta = style.usercssData;
const vars = meta.vars;
const oldVars = (oldStyle.usercssData || {}).vars;
const meta2 = src.usercssData;
const {vars} = meta;
const oldVars = meta2 ? meta2.vars : src;
if (vars && oldVars) {
// The type of var might be changed during the update. Set value to null if the value is invalid.
for (const [key, v] of Object.entries(vars)) {
@ -40,13 +42,15 @@ const usercssMan = {
const style = await usercssMan.buildMeta({sourceCode});
const dup = (checkDup || assignVars) &&
await usercssMan.find(styleId ? {id: styleId} : style);
let log;
if (!metaOnly) {
if (vars || assignVars) {
await usercssMan.assignVars(style, vars ? {usercssData: {vars}} : dup);
await usercssMan.assignVars(style, vars || dup);
}
await usercssMan.buildCode(style);
log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
}
return {style, dup};
return {style, dup, log};
},
async buildCode(style) {
@ -55,12 +59,14 @@ const usercssMan = {
const i = match.index;
const j = i + match[0].length;
const codeNoMeta = code.slice(0, i) + blankOut(code, i, j) + code.slice(j);
const {sections, errors} = await API.worker.compileUsercss(preprocessor, codeNoMeta, vars);
const {sections, errors, log} = await API.worker.compileUsercss(preprocessor, codeNoMeta, vars);
const recoverable = errors.every(e => e.recoverable);
if (!sections.length || !recoverable) {
throw !recoverable ? errors : 'Style does not contain any actual CSS to apply.';
}
style.sections = sections;
// adding a non-enumerable prop so it won't be written to storage
if (log) Object.defineProperty(style, 'log', {value: log});
return style;
},
@ -111,7 +117,11 @@ const usercssMan = {
},
async editSave(style) {
return API.styles.editSave(await usercssMan.parse(style));
style = await usercssMan.parse(style);
return {
log: style.log, // extracting the non-enumerable prop, otherwise it won't survive messaging
style: await API.styles.editSave(style),
};
},
async find(styleOrData) {
@ -129,17 +139,18 @@ const usercssMan = {
}
},
async install(style) {
return API.styles.install(await usercssMan.parse(style));
async install(style, opts) {
return API.styles.install(await usercssMan.parse(style, opts));
},
async parse(style) {
async parse(style, {dup, vars} = {}) {
style = await usercssMan.buildMeta(style);
// preserve style.vars during update
const dup = await usercssMan.find(style);
if (dup) {
if (dup || (dup = await usercssMan.find(style))) {
style.id = dup.id;
await usercssMan.assignVars(style, dup);
}
if (vars || (vars = dup)) {
await usercssMan.assignVars(style, vars);
}
return usercssMan.buildCode(style);
},

158
background/uso-api.js Normal file
View File

@ -0,0 +1,158 @@
/* global URLS stringAsRegExp */// toolbox.js
/* global usercssMan */
'use strict';
const usoApi = {};
(() => {
const pingers = {};
usoApi.pingback = (usoId, delay) => {
clearTimeout(pingers[usoId]);
delete pingers[usoId];
if (delay > 0) {
return new Promise(resolve => (pingers[usoId] = setTimeout(ping, delay, usoId, resolve)));
} else if (delay !== false) {
return ping(usoId);
}
};
/**
* Replicating USO-Archive format
* https://github.com/33kk/uso-archive/blob/flomaster/lib/uso.js
* https://github.com/33kk/uso-archive/blob/flomaster/lib/converters.js
*/
usoApi.toUsercss = async (data, {metaOnly = true, varsUrl} = {}) => {
const badKeys = {};
const newKeys = [];
const descr = JSON.stringify(data.description.trim());
const vars = (data.style_settings || []).map(makeVar, {badKeys, newKeys}).join('');
const sourceCode = `\
/* ==UserStyle==
@name ${data.name}
@namespace USO Archive
@version ${data.updated.replace(/-/g, '').replace(/[T:]/g, '.').slice(0, 14)}
@description ${/^"['`]|\\/.test(descr) ? descr : descr.slice(1, -1)}
@author ${(data.user || {}).name || '?'}
@license ${makeLicense(data.license)}${vars ? '\n@preprocessor uso' + vars : ''}`
.replace(/\*\//g, '*\\/') +
`==/UserStyle== */\n${newKeys[0] ? useNewKeys(data.css, badKeys) : data.css}`;
const {style} = await usercssMan.build({sourceCode, metaOnly});
usoApi.useVarsUrl(style, varsUrl);
return {style, badKeys, newKeys};
};
usoApi.useVarsUrl = (style, url) => {
if (!/\?ik-/.test(url)) {
return;
}
const cfg = {badKeys: {}, newKeys: []};
const {vars} = style.usercssData;
if (!vars) {
return;
}
for (let [key, val] of new URLSearchParams(url.split('?')[1])) {
if (!key.startsWith('ik-')) continue;
key = makeKey(key.slice(3), cfg);
const v = vars[key];
if (!v) continue;
if (v.options) {
let sel = val.startsWith('ik-') && optByName(v, makeKey(val.slice(3), cfg));
if (!sel) {
key += '-custom';
sel = optByName(v, key + '-dropdown');
if (sel) vars[key].value = val;
}
if (sel) v.value = sel.name;
} else {
v.value = val;
}
}
return true;
};
async function ping(id, resolve) {
await fetch(`${URLS.uso}styles/install/${id}?source=stylish-ch`);
if (resolve) resolve(true);
return true;
}
function makeKey(key, {badKeys, newKeys}) {
let res = badKeys[key];
if (!res) {
res = key.replace(/[^-\w]/g, '-');
res += newKeys.includes(res) ? '-' : '';
if (key !== res) {
badKeys[key] = res;
newKeys.push(res);
}
}
return res;
}
function makeLicense(s) {
return !s ? 'NO-REDISTRIBUTION' :
s === 'publicdomain' ? 'CC0-1.0' :
s.startsWith('ccby') ? `${s.toUpperCase().match(/(..)/g).join('-')}-4.0` :
s;
}
function makeVar({
label,
setting_type: type,
install_key: ik,
style_setting_options: opts,
}) {
const cfg = this;
let value, suffix;
ik = makeKey(ik, cfg);
label = JSON.stringify(label);
switch (type) {
case 'color':
value = opts[0].value;
break;
case 'text':
value = JSON.stringify(opts[0].value);
break;
case 'image': {
const ikCust = `${ik}-custom`;
opts.push({
label: 'Custom',
install_key: `${ikCust}-dropdown`,
value: `/*[[${ikCust}]]*/`,
});
suffix = `\n@advanced text ${ikCust} ${label.slice(0, -1)} (Custom)" "https://foo.com/123.jpg"`;
type = 'dropdown';
} // fallthrough
case 'dropdown':
value = '';
for (const o of opts) {
const def = o.default ? '*' : '';
const val = o.value;
const s = ` ${makeKey(o.install_key, cfg)} ${JSON.stringify(o.label + def)} <<<EOT${
val.includes('\n') ? '\n' : ' '}${val} EOT;\n`;
value = def ? s + value : value + s;
}
value = `{\n${value}}`;
break;
default:
value = '"ERROR: unknown type"';
}
return `\n@advanced ${type} ${ik} ${label} ${value}${suffix || ''}`;
}
function optByName(v, name) {
return v.options.find(o => o.name === name);
}
function useNewKeys(css, badKeys) {
const rxsKeys = stringAsRegExp(Object.keys(badKeys).join('\n'), '', true).replace(/\n/g, '|');
const rxUsoVars = new RegExp(`(/\\*\\[\\[)(${rxsKeys})(?=]]\\*/)`, 'g');
return css.replace(rxUsoVars, (s, a, key) => a + badKeys[key]);
}
})();

120
background/usw-api.js Normal file
View File

@ -0,0 +1,120 @@
/* global API msg */// msg.js
/* global URLS */ // toolbox.js
/* global tokenMan */
'use strict';
const uswApi = (() => {
//#region Internals
class TokenHooks {
constructor(id) {
this.id = id;
}
keyName(name) {
return `${name}/${this.id}`;
}
query(query) {
return Object.assign(query, {vendor_data: this.id});
}
}
function fakeUsercssHeader(style) {
const {name, _usw: u = {}} = style;
const meta = Object.entries({
'@name': u.name || name || '?',
'@version': // Same as USO-archive version: YYYYMMDD.hh.mm
new Date().toISOString().replace(/^(\d+)-(\d+)-(\d+)T(\d+):(\d+).+/, '$1$2$3.$4.$5'),
'@namespace': u.namespace !== '?' && u.namespace ||
u.username && `userstyles.world/user/${u.username}` ||
'?',
'@description': u.description,
'@author': u.username,
'@license': u.license,
});
const maxKeyLen = meta.reduce((res, [k]) => Math.max(res, k.length), 0);
return [
'/* ==UserStyle==',
...meta.map(([k, v]) => v && `${k}${' '.repeat(maxKeyLen - k.length + 2)}${v}`).filter(Boolean),
'==/UserStyle== */',
].join('\n') + '\n\n';
}
async function linkStyle(style, sourceCode) {
const {id} = style;
const metadata = await API.worker.parseUsercssMeta(sourceCode).catch(console.warn) || {};
const uswData = Object.assign({}, style, {metadata, sourceCode});
API.data.set('usw' + id, uswData);
const token = await tokenMan.getToken('userstylesworld', true, new TokenHooks(id));
const info = await uswFetch('style', token);
const data = style._usw = Object.assign({token}, info);
style.url = style.url || data.homepage || `${URLS.usw}style/${data.id}`;
await uswSave(style);
return data;
}
async function uswFetch(path, token, opts) {
opts = Object.assign({credentials: 'omit'}, opts);
opts.headers = Object.assign({Authorization: `Bearer ${token}`}, opts.headers);
return (await (await fetch(`${URLS.usw}api/${path}`, opts)).json()).data;
}
/** Uses a custom method when broadcasting and avoids needlessly sending the entire style */
async function uswSave(style) {
const {id, _usw} = style;
await API.styles.save(style, {broadcast: false});
msg.broadcastExtension({method: 'uswData', style: {id, _usw}});
}
//#endregion
//#region Exports
return {
/**
* @param {number} id
* @param {string} sourceCode
* @return {Promise<string>}
*/
async publish(id, sourceCode) {
const style = await API.styles.get(id);
const code = style.usercssData ? sourceCode
: fakeUsercssHeader(style) + sourceCode;
const data = (style._usw || {}).token
? style._usw
: await linkStyle(style, code);
return uswFetch(`style/${data.id}`, data.token, {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({code}),
});
},
/**
* @param {number} id
* @return {Promise<void>}
*/
async revoke(id) {
await tokenMan.revokeToken('userstylesworld', new TokenHooks(id));
const style = await API.styles.get(id);
if (style) {
style._usw = {};
await uswSave(style);
}
},
};
//#endregion
})();
/* Doing this outside so we don't break IDE's recognition of the exported methods in IIFE */
for (const [k, fn] of Object.entries(uswApi)) {
uswApi[k] = async (id, ...args) => {
API.data.set('usw' + id, true);
try {
/* Awaiting inside `try` so that `finally` runs when done */
return await fn(id, ...args);
} finally {
API.data.del('usw' + id);
}
};
}

View File

@ -5,6 +5,7 @@
(() => {
if (window.INJECTED === 1) return;
window.INJECTED = 1;
/** true -> when the page styles are received,
* false -> when disableAll mode is on at start, the styles won't be sent
@ -13,15 +14,21 @@
let hasStyles = false;
let isDisabled = false;
let isTab = !chrome.tabs || location.pathname !== '/popup.html';
const order = {main: [], prio: []};
const calcOrder = ({id}) =>
(order.prio[id] || 0) * 1e6 ||
order.main[id] ||
id + .5e6; // no order = at the end of `main`
const isFrame = window !== parent;
const isFrameAboutBlank = isFrame && location.href === 'about:blank';
const isUnstylable = !chrome.app && document instanceof XMLDocument;
const styleInjector = StyleInjector({
compare: (a, b) => a.id - b.id,
compare: (a, b) => calcOrder(a) - calcOrder(b),
onUpdate: onInjectorUpdate,
});
// dynamic about: and javascript: iframes don't have a URL yet so we'll use their parent
const matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href) || location.href;
// dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
let matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href.split('#')[0]) ||
location.href;
// save it now because chrome.runtime will be unavailable in the orphaned script
const orphanEventId = chrome.runtime.id;
@ -34,8 +41,26 @@
let lazyBadge = isFrame;
let parentDomain;
/* about:blank iframes are often used by sites for file upload or background tasks
* and they may break if unexpected DOM stuff is present at `load` event
* so we'll add the styles only if the iframe becomes visible */
const xoEventId = `${Math.random()}`;
/** @type IntersectionObserver */
let xo;
window[Symbol.for('xo')] = (el, cb) => {
if (!xo) xo = new IntersectionObserver(onIntersect, {rootMargin: '100%'});
el.addEventListener(xoEventId, cb, {once: true});
xo.observe(el);
};
// FIXME: move this to background page when following bugs are fixed:
// https://bugzil.la/1587723, https://crbug.com/968651
const mqDark = matchMedia('(prefers-color-scheme: dark)');
mqDark.onchange = e => API.colorScheme.updateSystemPreferDark(e.matches);
mqDark.onchange(mqDark);
// Declare all vars before init() or it'll throw due to "temporal dead zone" of const/let
const ready = init();
init();
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
if (!isTab) {
@ -46,6 +71,11 @@
}
msg.onTab(applyOnMessage);
window.addEventListener('pageshow', e => {
if (e.isTrusted && e.persisted) { // bfcache
updateCount();
}
});
if (!chrome.tabs) {
window.dispatchEvent(new CustomEvent(orphanEventId));
@ -74,13 +104,13 @@
tryCatch(() => parent[parent.Symbol.for(SYM_ID)]);
const styles =
window[SYM] ||
/* about:blank iframes are often used by sites for file upload or background tasks
* and they may break if unexpected DOM stuff is present at `load` event
* so we'll add the styles in the next tick */
parentStyles && await new Promise(requestAnimationFrame) && parentStyles ||
parentStyles && await new Promise(onFrameElementInView) && parentStyles ||
!isFrameAboutBlank && chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr) ||
await API.styles.getSectionsByUrl(matchUrl, null, true);
isDisabled = styles.disableAll;
if (styles.cfg) {
isDisabled = styles.cfg.disableAll;
Object.assign(order, styles.cfg.order);
}
hasStyles = !isDisabled;
if (hasStyles) {
window[SYM] = styles;
@ -146,21 +176,20 @@
}
break;
case 'styleSort':
Object.assign(order, request.order);
styleInjector.sort();
break;
case 'urlChanged':
if (!hasStyles && isDisabled) break;
if (!hasStyles && isDisabled || matchUrl === request.url) break;
matchUrl = request.url;
API.styles.getSectionsByUrl(matchUrl).then(sections => {
hasStyles = true;
styleInjector.replace(sections);
});
break;
case 'backgroundReady':
ready.catch(err =>
msg.isIgnorableError(err)
? init()
: console.error(err));
break;
case 'updateCount':
updateCount();
break;
@ -209,6 +238,20 @@
).catch(msg.ignoreError);
}
function onFrameElementInView(cb) {
parent[parent.Symbol.for('xo')](frameElement, cb);
}
/** @param {IntersectionObserverEntry[]} entries */
function onIntersect(entries) {
for (const e of entries) {
if (e.isIntersecting) {
xo.unobserve(e.target);
e.target.dispatchEvent(new Event(xoEventId));
}
}
}
function tryCatch(func, ...args) {
try {
return func(...args);
@ -216,12 +259,13 @@
}
function orphanCheck() {
if (tryCatch(() => chrome.i18n.getUILanguage())) return;
if (chrome.runtime.id) return;
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
window.removeEventListener(orphanEventId, orphanCheck, true);
mqDark.onchange = null;
isOrphaned = true;
styleInjector.clear();
setTimeout(styleInjector.clear, 1000); // avoiding FOUC
tryCatch(msg.off, applyOnMessage);
}
})();

View File

@ -1,174 +0,0 @@
/* global API */// msg.js
'use strict';
(() => {
const manifest = chrome.runtime.getManifest();
const allowedOrigins = [
'https://openusercss.org',
'https://openusercss.com',
];
const sendPostMessage = message => {
if (allowedOrigins.includes(location.origin)) {
window.postMessage(message, location.origin);
}
};
const askHandshake = () => {
// Tell the page that we exist and that it should send the handshake
sendPostMessage({
type: 'ouc-begin-handshake',
});
};
// Listen for queries by the site and respond with a callback object
const sendInstalledCallback = styleData => {
sendPostMessage({
type: 'ouc-is-installed-response',
style: styleData,
});
};
const installedHandler = event => {
if (event.data
&& event.data.type === 'ouc-is-installed'
&& allowedOrigins.includes(event.origin)
) {
API.usercss.find({
name: event.data.name,
namespace: event.data.namespace,
}).then(style => {
const data = {event};
const callbackObject = {
installed: Boolean(style),
enabled: style.enabled,
name: data.name,
namespace: data.namespace,
};
sendInstalledCallback(callbackObject);
});
}
};
const attachInstalledListeners = () => {
window.addEventListener('message', installedHandler);
};
const doHandshake = event => {
// This is a representation of features that Stylus is capable of
const implementedFeatures = [
'install-usercss',
'event:install-usercss',
'event:is-installed',
'configure-after-install',
'builtin-editor',
'create-usercss',
'edit-usercss',
'import-moz-export',
'export-moz-export',
'update-manual',
'update-auto',
'export-json-backups',
'import-json-backups',
'manage-local',
];
const reportedFeatures = [];
// The handshake question includes a list of required and optional features
// we match them with features we have implemented, and build a union array.
event.data.featuresList.required.forEach(feature => {
if (implementedFeatures.includes(feature)) {
reportedFeatures.push(feature);
}
});
event.data.featuresList.optional.forEach(feature => {
if (implementedFeatures.includes(feature)) {
reportedFeatures.push(feature);
}
});
// We send the handshake response, which includes the key we got, plus some
// additional metadata
sendPostMessage({
type: 'ouc-handshake-response',
key: event.data.key,
extension: {
name: manifest.name,
capabilities: reportedFeatures,
},
});
};
const handshakeHandler = event => {
if (event.data
&& event.data.type === 'ouc-handshake-question'
&& allowedOrigins.includes(event.origin)
) {
doHandshake(event);
}
};
const attachHandshakeListeners = () => {
// Wait for the handshake request, then start it
window.addEventListener('message', handshakeHandler);
};
const sendInstallCallback = data => {
// Send an install callback to the site in order to let it know
// we were able to install the theme and it may display a success message
sendPostMessage({
type: 'ouc-install-callback',
key: data.key,
});
};
const installHandler = event => {
if (event.data
&& event.data.type === 'ouc-install-usercss'
&& allowedOrigins.includes(event.origin)
) {
API.usercss.install({
name: event.data.title,
sourceCode: event.data.code,
}).then(style => {
sendInstallCallback({
enabled: style.enabled,
key: event.data.key,
});
});
}
};
const attachInstallListeners = () => {
// Wait for an install event, then save the theme
window.addEventListener('message', installHandler);
};
const orphanCheck = () => {
const eventName = chrome.runtime.id + '-install-hook-openusercss';
const orphanCheckRequest = () => {
// If we can't get the UI language, it means we are orphaned, and should
// remove our event handlers
if (chrome.i18n && chrome.i18n.getUILanguage()) return true;
window.removeEventListener('message', installHandler);
window.removeEventListener('message', handshakeHandler);
window.removeEventListener('message', installedHandler);
window.removeEventListener(eventName, orphanCheckRequest, true);
};
// Send the event before we listen for it, for other possible
// running instances of the content script.
dispatchEvent(new Event(eventName));
addEventListener(eventName, orphanCheckRequest, true);
};
orphanCheck();
attachHandshakeListeners();
attachInstallListeners();
attachInstalledListeners();
askHandshake();
})();

View File

@ -1,378 +1,324 @@
/* global API msg */// msg.js
/* global API */// msg.js
'use strict';
// eslint-disable-next-line no-unused-expressions
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (() => {
const styleId = RegExp.$1;
/^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (async () => {
if (window.INJECTED_USO === 1) return;
window.INJECTED_USO = 1;
const usoId = RegExp.$1;
const USO = 'https://userstyles.org';
const apiUrl = `${USO}/api/v1/styles/${usoId}`;
const md5Url = `https://update.userstyles.org/${usoId}.md5`;
const CLICK = [
['#install_stylish_style_button', onInstall],
['#update_stylish_style_button', onInstall],
['.customize_style_button', onCustomize],
['.uninstall_stylish_style_button', onUninstall],
];
const pageEventId = `${performance.now()}${Math.random()}`;
const contentEventId = pageEventId + ':';
const orphanEventId = chrome.runtime.id; // id won't be available in the orphaned script
const $ = (sel, base = document) => base.querySelector(sel);
const toggleListener = (isOn, ...args) => (isOn ? addEventListener : removeEventListener)(...args);
const togglePageListener = isOn => toggleListener(isOn, contentEventId, onPageEvent, true);
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install'));
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true);
const mo = new MutationObserver(onMutation);
const observeColors = isOn =>
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
: mo.disconnect();
document.addEventListener('stylishInstallChrome', onClick);
document.addEventListener('stylishUpdateChrome', onClick);
let style, dup, md5, pageData, badKeys;
msg.on(onMessage);
runInPage(inPageContext, pageEventId, contentEventId, usoId, apiUrl);
addEventListener(orphanEventId, orphanCheck, true);
addEventListener('click', onClick, true);
togglePageListener(true);
let currentMd5;
const md5Url = getMeta('stylish-md5-url') || `https://update.userstyles.org/${styleId}.md5`;
Promise.all([
API.styles.find({md5Url}),
getResource(md5Url),
onDOMready(),
]).then(checkUpdatability);
[md5, dup] = await Promise.all([
fetch(md5Url).then(r => r.text()),
API.styles.find({md5Url}, {installationUrl: `https://uso.kkx.one/style/${usoId}`})
.then(sendVarsToPage),
document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
]);
document.documentElement.appendChild(
Object.assign(document.createElement('script'), {
textContent: `(${inPageContext})('${pageEventId}')`,
}));
function onMessage(msg) {
switch (msg.method) {
case 'ping':
// orphaned content script check
return true;
case 'openSettings':
openSettings();
return true;
}
if (!dup) {
sendStylishEvent('styleCanBeInstalledChrome');
} else if (dup.originalMd5 && dup.originalMd5 !== md5 || !dup.usercssData || !dup.md5Url) {
// allow update if 1) changed, 2) is a classic USO style, 3) is from USO-archive
sendStylishEvent('styleCanBeUpdatedChrome');
} else {
sendStylishEvent('styleAlreadyInstalledChrome');
}
/* since we are using "stylish-code-chrome" meta key on all browsers and
US.o does not provide "advanced settings" on this url if browser is not Chrome,
we need to fix this URL using "stylish-update-url" meta key
*/
function getStyleURL() {
const textUrl = getMeta('stylish-update-url') || '';
const jsonUrl = getMeta('stylish-code-chrome') ||
textUrl.replace(/styles\/(\d+)\/[^?]*/, 'styles/chrome/$1.json');
const paramsMissing = !jsonUrl.includes('?') && textUrl.includes('?');
return jsonUrl + (paramsMissing ? textUrl.replace(/^[^?]+/, '') : '');
}
function checkUpdatability([installedStyle, md5]) {
// TODO: remove the following statement when USO is fixed
document.dispatchEvent(new CustomEvent(pageEventId, {
detail: installedStyle && installedStyle.updateUrl,
}));
currentMd5 = md5;
if (!installedStyle) {
sendEvent({type: 'styleCanBeInstalledChrome'});
return;
}
const isCustomizable = /\?/.test(installedStyle.updateUrl);
const md5Url = getMeta('stylish-md5-url');
if (md5Url && installedStyle.md5Url && installedStyle.originalMd5) {
reportUpdatable(isCustomizable || md5 !== installedStyle.originalMd5);
} else {
getStyleJson().then(json => {
reportUpdatable(
isCustomizable ||
!json ||
!styleSectionsEqual(json, installedStyle));
});
}
function prepareInstallButton() {
return new Promise(resolve => {
const observer = new MutationObserver(check);
observer.observe(document.documentElement, {
childList: true,
subtree: true,
});
check();
function check() {
if (document.querySelector('#install_style_button')) {
resolve();
observer.disconnect();
}
}
});
}
function reportUpdatable(isUpdatable) {
prepareInstallButton().then(() => {
sendEvent({
type: isUpdatable
? 'styleCanBeUpdatedChrome'
: 'styleAlreadyInstalledChrome',
detail: {
updateUrl: installedStyle.updateUrl,
},
});
});
}
}
function sendEvent(event) {
sendEvent.lastEvent = event;
let {type, detail = null} = event;
if (typeof cloneInto !== 'undefined') {
// Firefox requires explicit cloning, however USO can't process our messages anyway
// because USO tries to use a global "event" variable deprecated in Firefox
detail = cloneInto({detail}, document); /* global cloneInto */
} else {
detail = {detail};
}
document.dispatchEvent(new CustomEvent(type, detail));
}
function onClick(event) {
if (onClick.processing || !orphanCheck()) {
return;
}
onClick.processing = true;
doInstall()
.then(() => {
if (!event.type.includes('Update')) {
// FIXME: sometimes the button is broken i.e. the button sends
// 'install' instead of 'update' event while the style is already
// install.
// This triggers an incorrect install count but we don't really care.
return getResource(getMeta('stylish-install-ping-url-chrome'));
}
})
.catch(console.error)
.then(done);
function done() {
setTimeout(() => {
onClick.processing = false;
});
}
}
function doInstall() {
let oldStyle;
return API.styles.find({
md5Url: getMeta('stylish-md5-url') || location.href,
})
.then(_oldStyle => {
oldStyle = _oldStyle;
return oldStyle ?
oldStyle.name :
getResource(getMeta('stylish-description'));
})
.then(name => {
const props = {};
if (oldStyle) {
props.id = oldStyle.id;
}
return saveStyleCode(oldStyle ? 'styleUpdate' : 'styleInstall', name, props);
});
}
async function saveStyleCode(message, name, addProps = {}) {
const isNew = message === 'styleInstall';
const needsConfirmation = isNew || !saveStyleCode.confirmed;
if (needsConfirmation && !confirm(chrome.i18n.getMessage(message, [name]))) {
return Promise.reject();
}
saveStyleCode.confirmed = true;
enableUpdateButton(false);
const json = await getStyleJson();
if (!json) {
prompt(chrome.i18n.getMessage('styleInstallFailed', ''),
'https://github.com/openstyles/stylus/issues/195');
return;
}
// Update originalMd5 since USO changed it (2018-11-11) to NOT match the current md5
const style = await API.styles.install(Object.assign(json, addProps, {originalMd5: currentMd5}));
if (!isNew && style.updateUrl.includes('?')) {
enableUpdateButton(true);
} else {
sendEvent({type: 'styleInstalledChrome'});
}
function enableUpdateButton(state) {
const important = s => s.replace(/;/g, '!important;');
const button = document.getElementById('update_style_button');
if (button) {
button.style.cssText = state ? '' : important('pointer-events: none; opacity: .35;');
const icon = button.querySelector('img[src*=".svg"]');
if (icon) {
icon.style.cssText = state ? '' : important('transition: transform 5s; transform: rotate(0);');
if (state) {
setTimeout(() => (icon.style.cssText += important('transform: rotate(10turn);')));
}
}
async function onClick(e) {
for (const [sel, fn] of CLICK) {
const el = e.target.closest(sel);
if (!el) continue;
try {
el.disabled = true;
await fn(e);
} catch (e) {
alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
} finally {
el.disabled = false;
}
}
}
function getMeta(name) {
const e = document.querySelector(`link[rel="${name}"]`);
return e ? e.getAttribute('href') : null;
function onCustomize() {
const ss = $('#style-settings');
const willShow = !ss || !ss.offsetHeight;
observeColors(willShow);
toggleListener(willShow, 'change', onChange);
}
async function getResource(url, opts) {
try {
return url.startsWith('#')
? document.getElementById(url.slice(1)).textContent
: await API.download(url, opts);
} catch (error) {
alert('Error\n' + error.message);
return Promise.reject(error);
async function onInstall(e) {
const {id} = dup;
e.stopPropagation();
if (!style) await buildStyle();
style = dup = await API.usercss.install(style, {
dup: {id},
vars: getPageVars(),
});
sendStylishEvent('styleInstalledChrome');
API.uso.pingback(id);
}
function onUninstall() {
const {id} = dup;
dup = style = false;
observeColors(false);
removeEventListener('change', onChange);
return API.styles.delete(id);
}
function onChange({target: el}) {
if (dup && el.matches('[name^="ik-"], [type=file]')) {
API.usercss.configVars(dup.id, getPageVars());
}
}
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5"
// instead of "https://update.userstyles.org/#####.md5"
async function getStyleJson() {
try {
const style = await getResource(getStyleURL(), {responseType: 'json'});
const codeElement = document.getElementById('stylish-code');
if (!style || !Array.isArray(style.sections) || style.sections.length ||
codeElement && !codeElement.textContent.trim()) {
return style;
function onMutation(mutations) {
for (const {target: el} of mutations) {
if (el.style.display === 'none' &&
/^ik-/.test(el.name) &&
/^#[\da-f]{6}$/.test(el.value)) {
onChange({target: el});
}
const code = await getResource(getMeta('stylish-update-url'));
style.sections = (await API.worker.parseMozFormat({code})).sections;
if (style.md5Url) style.md5Url = style.md5Url.replace('update.update', 'update');
return style;
} catch (e) {}
}
/**
* The sections are checked in successive order because it matters when many sections
* match the same URL and they have rules with the same CSS specificity
* @param {Object} a - first style object
* @param {Object} b - second style object
* @returns {?boolean}
*/
function styleSectionsEqual({sections: a}, {sections: b}) {
const targets = ['urls', 'urlPrefixes', 'domains', 'regexps'];
return a && b && a.length === b.length && a.every(sameSection);
function sameSection(secA, i) {
return equalOrEmpty(secA.code, b[i].code, 'string', (a, b) => a === b) &&
targets.every(target => equalOrEmpty(secA[target], b[i][target], 'array', arrayMirrors));
}
function equalOrEmpty(a, b, type, comparator) {
const typeA = type === 'array' ? Array.isArray(a) : typeof a === type;
const typeB = type === 'array' ? Array.isArray(b) : typeof b === type;
return typeA && typeB && comparator(a, b) ||
(a == null || typeA && !a.length) &&
(b == null || typeB && !b.length);
}
function arrayMirrors(a, b) {
return a.length === b.length &&
a.every(el => b.includes(el)) &&
b.every(el => a.includes(el));
}
}
function onDOMready() {
return document.readyState !== 'loading'
? Promise.resolve()
: new Promise(resolve => document.addEventListener('DOMContentLoaded', resolve, {once: true}));
function onPageEvent(e) {
pageData = e.detail;
togglePageListener(false);
}
function openSettings(countdown = 10e3) {
const button = document.querySelector('.customize_button');
if (button) {
button.dispatchEvent(new MouseEvent('click', {bubbles: true}));
setTimeout(function pollArea(countdown = 2000) {
const area = document.getElementById('advancedsettings_area');
if (area || countdown < 0) {
(area || button).scrollIntoView({behavior: 'smooth', block: area ? 'end' : 'center'});
async function buildStyle() {
if (!pageData) pageData = await (await fetch(apiUrl)).json();
({style, badKeys} = await API.uso.toUsercss(pageData, {varsUrl: dup.updateUrl}));
Object.assign(style, {
md5Url,
id: dup.id,
originalMd5: md5,
updateUrl: apiUrl,
});
}
function getPageVars() {
const {vars} = (style || dup).usercssData;
for (const el of document.querySelectorAll('[name^="ik-"]')) {
const name = el.name.slice(3); // dropping "ik-"
const ik = (badKeys || {})[name] || name;
const v = vars[ik] || false;
const isImage = el.type === 'radio';
if (v && (!isImage || el.checked)) {
const val = el.value;
const isFile = val === 'user-upload';
if (isImage && (isFile || val === 'user-url')) {
const el2 = $(`[type=${isFile ? 'file' : 'url'}]`, el.parentElement);
const ikCust = `${ik}-custom`;
v.value = `${ikCust}-dropdown`;
vars[ikCust].value = isFile ? getFileUriFromPage(el2) : el2.value;
} else {
setTimeout(pollArea, 100, countdown - 100);
v.value = v.type === 'select' ? val.replace(/^ik-/, '') : val;
}
}, 500);
} else if (countdown > 0) {
setTimeout(openSettings, 100, countdown - 100);
}
}
return vars;
}
function getFileUriFromPage(el) {
togglePageListener(true);
sendPageEvent(el);
return pageData;
}
function runInPage(fn, ...args) {
const div = document.createElement('div');
div.attachShadow({mode: 'closed'})
.appendChild(document.createElement('script'))
.textContent = `(${fn})(${JSON.stringify(args).slice(1, -1)})`;
document.documentElement.appendChild(div).remove();
}
function sendPageEvent(data) {
dispatchEvent(data instanceof Node
? new MouseEvent(pageEventId, {relatedTarget: data})
: new CustomEvent(pageEventId, {detail: data}));
//* global cloneInto */// WARNING! Firefox requires cloning of an object `detail`
}
function sendStylishEvent(type) {
document.dispatchEvent(new Event(type));
}
function sendVarsToPage(style) {
if (style) {
const vars = (style.usercssData || {}).vars || `${style.updateUrl}`.split('?')[1];
if (vars) sendPageEvent('vars:' + JSON.stringify(vars));
}
return style || false;
}
function orphanCheck() {
try {
if (chrome.i18n.getUILanguage()) {
return true;
}
} catch (e) {}
// In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true);
document.removeEventListener('stylishInstallChrome', onClick);
document.removeEventListener('stylishUpdateChrome', onClick);
try {
msg.off(onMessage);
} catch (e) {}
if (chrome.runtime.id) return true;
removeEventListener(orphanEventId, orphanCheck, true);
removeEventListener('click', onClick, true);
removeEventListener('change', onChange);
sendPageEvent('quit');
observeColors(false);
togglePageListener(false);
}
})();
function inPageContext(eventId) {
document.currentScript.remove();
window.isInstalled = true;
const origMethods = {
json: Response.prototype.json,
byId: document.getElementById,
};
let vars;
// USO bug workaround: prevent errors in console after install and busy cursor
document.getElementById = id =>
origMethods.byId.call(document, id) ||
(/^(stylish-code|stylish-installed-style-installed-\w+|post-install-ad|style-install-unknown)$/.test(id)
? Object.assign(document.createElement('p'), {className: 'afterdownload-ad'})
: null);
// USO bug workaround: use the actual image data in customized settings
document.addEventListener(eventId, ({detail}) => {
vars = /\?/.test(detail) && new URL(detail).searchParams;
if (!vars) Response.prototype.json = origMethods.json;
}, {once: true});
Response.prototype.json = async function () {
const json = await origMethods.json.apply(this, arguments);
if (vars && json && Array.isArray(json.style_settings)) {
Response.prototype.json = origMethods.json;
const images = new Map();
for (const ss of json.style_settings) {
let value = vars.get('ik-' + ss.install_key);
if (!value || !(ss.style_setting_options || [])[0]) {
continue;
function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
let done, orphaned, vars;
// `chrome` may be empty if no extensions use externally_connectable but USO needs it
if (!window.chrome) window.chrome = {runtime: {sendMessage: () => {}}};
const EXT_ID = 'fjnbnpbmkenffdnngjfgmeleoegfcffe';
const {defineProperty} = Object;
const {dispatchEvent, CustomEvent, removeEventListener} = window;
const apply = Map.call.bind(Map.apply);
const OVR = [
[chrome.runtime, 'sendMessage', (fn, me, args) => {
const [id, /*msg*/, opts, cb = opts] = args;
if (id !== EXT_ID) return apply(fn, me, args);
if (typeof cb !== 'function') return Promise.resolve(true);
cb(true);
}],
[Response.prototype, 'json', async (fn, me, args) => {
const res = await apply(fn, me, args);
try {
if (!done && me.url === apiUrl) {
done = true;
send(res);
setVars(res);
}
if (value.startsWith('ik-')) {
value = value.replace(/^ik-/, '');
const def = ss.style_setting_options.find(item => item.default);
if (!def || def.install_key !== value) {
if (def) def.default = false;
for (const item of ss.style_setting_options) {
if (item.install_key === value) {
item.default = true;
break;
}
}
}
} else if (ss.setting_type === 'image') {
let isListed;
for (const opt of ss.style_setting_options) {
isListed |= opt.default = (opt.value === value);
}
images.set(ss.install_key, {url: value, isListed});
} else {
const item = ss.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
} catch (e) {}
return res;
}],
[window, 'fetch', (fn, me, args) =>
args[0] === `chrome-extension://${EXT_ID}/index.html`
? Promise.resolve(new Response('<!doctype html><html lang="en"></html>'))
: apply(fn, me, args),
],
];
OVR.forEach(([obj, name, caller], i) => {
const orig = obj[name];
const ovr = new Proxy(orig, {
apply(fn, me, args) {
if (orphaned) restore(obj, name, ovr, fn);
return (orphaned ? apply : caller)(fn, me, args);
},
});
defineProperty(obj, name, {value: ovr});
OVR[i] = [obj, name, ovr, orig]; // same args as restore()
});
/* We set `isInstalled` at page start intentionally not trying to replicate Stylish login events.
* This difference allows USO site to detect presence of Stylus (or another similar extension). */
window.isInstalled = true;
addEventListener(eventId, onCommand, true);
function onCommand(e) {
if (e.detail === 'quit') {
removeEventListener(eventId, onCommand, true);
OVR.forEach(restore);
done = orphaned = true;
} else if (/^vars:/.test(e.detail)) {
vars = JSON.parse(e.detail.slice(5));
} else if (e.relatedTarget) {
send(e.relatedTarget.uploadedData);
}
}
function restore(obj, name, ovr, orig) { // same order as OVR after patching
if (obj[name] === ovr) {
defineProperty(obj, name, {value: orig});
}
}
function send(data) {
dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
}
function setVars(json) {
const images = new Map();
const isNew = typeof vars === 'object';
const badKeys = {};
const newKeys = [];
const makeKey = ({install_key: key}) => {
let res = isNew ? badKeys[key] : key;
if (!res) {
res = key.replace(/[^-\w]/g, '-');
res += newKeys.includes(res) ? '-' : '';
if (key !== res) {
badKeys[key] = res;
newKeys.push(res);
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (document.getElementById('style-settings')) {
observer.disconnect();
for (const [name, {url, isListed}] of images) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio &&
document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elRadio.checked = !isListed;
elUrl.value = url;
}
return res;
};
if (!isNew) vars = new URLSearchParams(vars);
for (const ss of json.style_settings || []) {
const ik = makeKey(ss);
let value = isNew ? (vars[ik] || {}).value : vars.get('ik-' + ik);
if (value == null || !(ss.style_setting_options || [])[0]) {
continue;
}
if (ss.setting_type === 'image') {
let isListed;
for (const opt of ss.style_setting_options) {
isListed |= opt.default = (opt.install_key === value);
}
images.set(ik, {url: isNew && !isListed ? vars[`${ik}-custom`].value : value, isListed});
} else if (value.startsWith('ik-') || isNew && vars[ik].type === 'select') {
value = value.replace(/^ik-/, '');
const def = ss.style_setting_options.find(item => item.default);
if (!def || makeKey(def) !== value) {
if (def) def.default = false;
for (const item of ss.style_setting_options) {
if (makeKey(item) === value) {
item.default = true;
break;
}
}
}).observe(document, {childList: true, subtree: true});
}
} else {
const item = ss.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
return json;
};
if (!images.size) return;
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) return;
observer.disconnect();
for (const [name, {url, isListed}] of images) {
const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
if (elUrl) {
elRadio.checked = !isListed;
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
}

View File

@ -0,0 +1,43 @@
/* global API */// msg.js
'use strict';
(() => {
const ORIGIN = 'https://userstyles.world';
const HANDLERS = Object.assign(Object.create(null), {
async 'usw-ready'() {
send({type: 'usw-remove-stylus-button'});
if (location.pathname === '/api/oauth/style/new') {
const styleId = Number(new URLSearchParams(location.search).get('vendor_data'));
const data = await API.data.pop('usw' + styleId);
send({type: 'usw-fill-new-style', data});
}
},
async 'usw-style-info-request'(data) {
switch (data.requestType) {
case 'installed': {
const updateUrl = `${ORIGIN}/api/style/${data.styleID}.user.css`;
const style = await API.styles.find({updateUrl});
send({
type: 'usw-style-info-response',
data: {installed: Boolean(style), requestType: 'installed'},
});
break;
}
}
},
});
window.addEventListener('message', ({data, source, origin}) => {
// Some browsers don't reveal `source` to extensions e.g. Firefox
if (data && (source ? source === window : origin === ORIGIN)) {
const fn = HANDLERS[data.type];
if (fn) fn(data);
}
});
function send(msg) {
window.postMessage(msg, ORIGIN);
}
})();

View File

@ -9,12 +9,14 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
const PATCH_ID = 'transition-patch';
// styles are out of order if any of these elements is injected between them
const ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
const docRewriteObserver = RewriteObserver(_sort);
const docRootObserver = RootObserver(_sortIfNeeded);
const docRewriteObserver = RewriteObserver(sort);
const docRootObserver = RootObserver(sortIfNeeded);
const toSafeChar = c => String.fromCharCode(0xFF00 + c.charCodeAt(0) - 0x20);
const list = [];
const table = new Map();
let isEnabled = true;
let isTransitionPatched;
let isTransitionPatched = chrome.app && CSS.supports('accent-color', 'red'); // Chrome 93
let exposeStyleName;
// will store the original method refs because the page can override them
let creationDoc, createElement, createElementNS;
@ -23,24 +25,24 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
list,
async apply(styleMap) {
const styles = _styleMapToArray(styleMap);
const styles = styleMapToArray(styleMap);
const value = !styles.length
? []
: await docRootObserver.evade(() => {
if (!isTransitionPatched && isEnabled) {
_applyTransitionPatch(styles);
applyTransitionPatch(styles);
}
return styles.map(_addUpdate);
return styles.map(addUpdate);
});
_emitUpdate();
emitUpdate();
return value;
},
clear() {
_addRemoveElements(false);
addRemoveElements(false);
list.length = 0;
table.clear();
_emitUpdate();
emitUpdate();
},
clearOrphans() {
@ -53,12 +55,12 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
},
remove(id) {
_remove(id);
_emitUpdate();
remove(id);
emitUpdate();
},
replace(styleMap) {
const styles = _styleMapToArray(styleMap);
const styles = styleMapToArray(styleMap);
const added = new Set(styles.map(s => s.id));
const removed = [];
for (const style of list) {
@ -66,22 +68,24 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
removed.push(style.id);
}
}
styles.forEach(_addUpdate);
removed.forEach(_remove);
_emitUpdate();
styles.forEach(addUpdate);
removed.forEach(remove);
emitUpdate();
},
toggle(enable) {
if (isEnabled === enable) return;
isEnabled = enable;
if (!enable) _toggleObservers(false);
_addRemoveElements(enable);
if (enable) _toggleObservers(true);
if (!enable) toggleObservers(false);
addRemoveElements(enable);
if (enable) toggleObservers(true);
},
sort: sort,
};
function _add(style) {
const el = style.el = _createStyle(style.id, style.code);
function add(style) {
const el = style.el = createStyle(style);
const i = list.findIndex(item => compare(item, style) > 0);
table.set(style.id, style);
if (isEnabled) {
@ -91,7 +95,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
return el;
}
function _addRemoveElements(add) {
function addRemoveElements(add) {
for (const {el} of list) {
if (add) {
document.documentElement.appendChild(el);
@ -101,11 +105,11 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
}
}
function _addUpdate(style) {
return table.has(style.id) ? _update(style) : _add(style);
function addUpdate(style) {
return table.has(style.id) ? update(style) : add(style);
}
function _applyTransitionPatch(styles) {
function applyTransitionPatch(styles) {
isTransitionPatched = true;
// CSS transition bug workaround: since we insert styles asynchronously,
// the browsers, especially Firefox, may apply all transitions on page load
@ -114,19 +118,20 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
!styles.some(s => s.code.includes('transition'))) {
return;
}
const el = _createStyle(PATCH_ID, `
const el = createStyle({id: PATCH_ID, code: `
:root:not(#\\0):not(#\\0) * {
transition: none !important;
}
`);
`});
document.documentElement.appendChild(el);
// wait for the next paint to complete
// note: requestAnimationFrame won't fire in inactive tabs
requestAnimationFrame(() => setTimeout(() => el.remove()));
}
function _createStyle(id, code = '') {
if (!creationDoc) _initCreationDoc();
function createStyle(style = {}) {
const {id} = style;
if (!creationDoc) initCreationDoc();
let el;
if (document.documentElement instanceof SVGSVGElement) {
// SVG document style
@ -146,18 +151,27 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
el.type = 'text/css';
// SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus');
el.textContent = code;
setTextAndName(el, style);
return el;
}
function _toggleObservers(shouldStart) {
function setTextAndName(el, {id, code = '', name}) {
if (exposeStyleName && name) {
el.dataset.name = name;
name = encodeURIComponent(name.replace(/[?#/']/g, toSafeChar));
code += `\n/*# sourceURL=${chrome.runtime.getURL(name)}.user.css#${id} */`;
}
el.textContent = code;
}
function toggleObservers(shouldStart) {
const onOff = shouldStart && isEnabled ? 'start' : 'stop';
docRewriteObserver[onOff]();
docRootObserver[onOff]();
}
function _emitUpdate() {
_toggleObservers(list.length);
function emitUpdate() {
toggleObservers(list.length);
onUpdate();
}
@ -168,11 +182,11 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
and since userAgent.navigator can be spoofed via about:config or devtools,
we're checking for getPreventDefault that was removed in FF59
*/
function _initCreationDoc() {
function initCreationDoc() {
creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject;
if (creationDoc) {
({createElement, createElementNS} = creationDoc);
const el = document.documentElement.appendChild(_createStyle());
const el = document.documentElement.appendChild(createStyle());
const isApplied = el.sheet;
el.remove();
if (isApplied) return;
@ -181,7 +195,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
({createElement, createElementNS} = document);
}
function _remove(id) {
function remove(id) {
const style = table.get(id);
if (!style) return;
table.delete(id);
@ -189,14 +203,14 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
style.el.remove();
}
function _sort() {
function sort() {
docRootObserver.evade(() => {
list.sort(compare);
_addRemoveElements(true);
addRemoveElements(true);
});
}
function _sortIfNeeded() {
function sortIfNeeded() {
let needsSort;
let el = list.length && list[0].el;
if (!el) {
@ -217,22 +231,29 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
// some styles are not injected to the document
if (i < list.length) needsSort = true;
}
if (needsSort) _sort();
if (needsSort) sort();
return needsSort;
}
function _styleMapToArray(styleMap) {
return Object.values(styleMap).map(s => ({
id: s.id,
code: s.code.join(''),
function styleMapToArray(styleMap) {
if (styleMap.cfg) {
({exposeStyleName} = styleMap.cfg);
delete styleMap.cfg;
}
return Object.values(styleMap).map(({id, code, name}) => ({
id,
name,
code: code.join(''),
}));
}
function _update({id, code}) {
function update(newStyle) {
const {id, code} = newStyle;
const style = table.get(id);
if (style.code !== code) {
if (style.code !== code ||
style.name !== newStyle.name && exposeStyleName) {
style.code = code;
style.el.textContent = code;
setTextAndName(style.el, newStyle);
}
}
@ -241,14 +262,14 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
let root;
let observing = false;
let timer;
const observer = new MutationObserver(_check);
const observer = new MutationObserver(check);
return {start, stop};
function start() {
if (observing) return;
// detect dynamic iframes rewritten after creation by the embedder i.e. externally
root = document.documentElement;
timer = setTimeout(_check);
timer = setTimeout(check);
observer.observe(document, {childList: true});
observing = true;
}
@ -260,7 +281,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
observing = false;
}
function _check() {
function check() {
if (root !== document.documentElement) {
root = document.documentElement;
onChange();
@ -290,7 +311,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
function evade(fn) {
const restore = observing && start;
stop();
return new Promise(resolve => _run(fn, resolve, _waitForRoot))
return new Promise(resolve => run(fn, resolve, waitForRoot))
.then(restore);
}
@ -308,7 +329,7 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
observing = false;
}
function _run(fn, resolve, wait) {
function run(fn, resolve, wait) {
if (document.documentElement) {
resolve(fn());
return true;
@ -316,8 +337,8 @@ window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
if (wait) wait(fn, resolve);
}
function _waitForRoot(...args) {
new MutationObserver((_, observer) => _run(...args) && observer.disconnect())
function waitForRoot(...args) {
new MutationObserver((_, observer) => run(...args) && observer.disconnect())
.observe(document, {childList: true});
}
}

242
edit.html
View File

@ -1,22 +1,12 @@
<!DOCTYPE html>
<html id="stylus">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="global.css" rel="stylesheet">
<style id="firefox-transitions-bug-suppressor">
/* restrict to FF */
@supports (-moz-appearance:none) {
/* increased specificity to override sane selectors in user styles */
html#stylus.firefox #stylus-edit #header *,
html#stylus.firefox #stylus-edit #sections * {
transition: none !important;
}
}
</style>
<link id="cm-theme" rel="stylesheet">
<link href="global-dark.css" rel="stylesheet">
<style id="cm-theme"></style>
<script src="js/polyfill.js"></script>
<script src="js/toolbox.js"></script>
@ -28,6 +18,8 @@
<script src="content/apply.js"></script>
<script src="js/sections-util.js"></script>
<script src="js/storage-util.js"></script>
<script src="edit/codemirror-themes.js"></script> <!-- must precede base.js -->
<script src="edit/base.js"></script>
<script src="vendor/codemirror/lib/codemirror.js"></script>
@ -49,19 +41,18 @@
<script src="vendor/codemirror/addon/lint/lint.js"></script>
<script src="vendor/codemirror/addon/hint/show-hint.js"></script>
<script src="vendor/codemirror/addon/hint/css-hint.js"></script>
<script src="vendor/codemirror/keymap/emacs.js"></script>
<script src="vendor/codemirror/keymap/sublime.js"></script>
<script src="vendor/codemirror/keymap/vim.js"></script>
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
<script src="vendor/lz-string-unsafe/lz-string-unsafe.min.js"></script>
<script src="js/color/color-converter.js"></script>
<script src="js/color/color-mimicry.js"></script>
<script src="js/color/color-picker.js"></script>
<script src="js/color/color-view.js"></script>
<script src="js/storage-util.js"></script>
<script src="js/worker-util.js"></script>
<script src="edit/util.js"></script>
<script src="edit/codemirror-themes.js"></script>
<script src="edit/codemirror-default.js"></script>
<script src="edit/codemirror-factory.js"></script>
<script src="edit/moz-section-finder.js"></script>
@ -71,25 +62,25 @@
<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="edit/usw-integration.js"></script>
<template data-id="appliesTo">
<li class="applies-to-item">
<div class="select-resizer">
<select name="applies-type" class="applies-type style-contributor">
<option value="url" i18n-text="appliesUrlOption"></option>
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option>
<option value="domain" i18n-text="appliesDomainOption"></option>
<option value="regexp" i18n-text="appliesRegexpOption"></option>
<option value="url" i18n="appliesUrlOption"></option>
<option value="url-prefix" i18n="appliesUrlPrefixOption"></option>
<option value="domain" i18n="appliesDomainOption"></option>
<option value="regexp" i18n="appliesRegexpOption"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<div class="applies-value-wrapper">
<input name="applies-value" class="applies-value style-contributor" spellcheck="false">
<a class="remove-applies-to" href="#" i18n-text="appliesRemove" i18n-title="appliesRemove">
<a class="remove-applies-to" i18n="appliesRemove, title:appliesRemove" tabindex="0">
<svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
</a>
<a class="add-applies-to" href="#" i18n-text="appliesAdd" i18n-title="appliesAdd">
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
</a>
</div>
@ -97,8 +88,8 @@
</template>
<template data-id="appliesToEverything">
<li class="applies-to-everything" i18n-text="appliesToEverything">
<a class="add-applies-to" i18n-text="appliesAdd" i18n-title="appliesAdd" href="#">
<li class="applies-to-everything" i18n="appliesToEverything">
<a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
</a>
</li>
@ -108,25 +99,25 @@
<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>
<button class="restore-section" i18n="sectionRestore"></button>
</p>
<label i18n-text="sectionCode" class="code-label"></label>
<label i18n="sectionCode" class="code-label"></label>
<div class="applies-to">
<label i18n-text="appliesLabel">
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0">
<label i18n="appliesLabel, title:appliesHelp" data-cmd="note">
<a class="svg-inline-wrapper applies-to-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</label>
<ul class="applies-to-list"></ul>
</div>
<div class="edit-actions">
<button class="remove-section" i18n-text="sectionRemove"></button>
<button class="add-section" i18n-long-text="sectionAdd" i18n-short-text="genericAdd"></button>
<button class="clone-section" i18n-text="genericClone"></button>
<button class="remove-section" i18n="sectionRemove"></button>
<button class="add-section" i18n="long-text:sectionAdd, short-text:genericAdd"></button>
<button class="clone-section" i18n="genericClone"></button>
<button class="move-section-up"></button>
<button class="move-section-down"></button>
<button class="beautify-section" i18n-text="styleBeautify"></button>
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button>
<button class="beautify-section" i18n="styleBeautify"></button>
<button class="test-regexp" i18n="genericTest"></button>
</div>
</div>
</template>
@ -136,27 +127,27 @@
<div data-type="main">
<div data-type="content"></div>
<div data-type="actions">
<a data-action="case" i18n-title="searchCaseSensitive" href="#" tabindex="0">Aa</a>
<a data-action="prev" i18n-title="genericPrevious" href="#" data-hotkey-tooltip="findPrev" tabindex="0">
<a data-action="case" i18n="title:searchCaseSensitive" tabindex="0">Aa</a>
<a data-action="prev" i18n="title:genericPrevious" data-hotkey-tooltip="findPrev" tabindex="0">
<svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
</a>
<a data-action="next" i18n-title="genericNext" href="#" data-hotkey-tooltip="findNext" tabindex="0">
<a data-action="next" i18n="title:genericNext" data-hotkey-tooltip="findNext" tabindex="0">
<svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
</a>
<a data-action="close" i18n-title="confirmClose" href="#" data-hotkey-tooltip="=Esc" tabindex="0">
<a data-action="close" i18n="title:confirmClose" data-hotkey-tooltip="=Esc" tabindex="0">
<svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
</a>
</div>
</div>
<div data-type="status">
<div class="CodeMirror-search-hint" i18n-text="searchRegexp"></div>
<div data-type="tally" i18n-title="searchNumberOfResults"></div>
<div data-type="tally" i18n="title:searchNumberOfResults"></div>
</div>
</div>
</template>
<template data-id="clearSearch">
<div data-type="hover" i18n-title="confirmDelete">
<div data-type="hover" i18n="title:confirmDelete">
<svg data-action="clear" class="svg-icon"><use xlink:href="#svg-icon-close"></use></svg>
</div>
</template>
@ -165,7 +156,7 @@
<div data-type="content">
<div data-type="input-wrapper">
<textarea class="CodeMirror-search-field" rows="1" spellcheck="false" required
i18n-placeholder="search"></textarea>
i18n="placeholder:search"></textarea>
</div>
</div>
</template>
@ -174,36 +165,36 @@
<div data-type="content">
<div data-type="input-wrapper">
<textarea data-type="replace-from"
i18n-placeholder="replace"
i18n="placeholder:replace"
class="CodeMirror-search-field" rows="1" required
spellcheck="false"></textarea>
</div>
<div data-type="input-wrapper">
<textarea data-type="replace-to"
i18n-placeholder="replaceWith"
i18n="placeholder:replaceWith"
class="CodeMirror-search-field" rows="1" required
spellcheck="false"></textarea>
</div>
<button data-action="replace" i18n-text="replace" disabled></button>
<button data-action="replaceAll" i18n-text="replaceAll" disabled></button>
<button data-action="undo" i18n-text="undo" disabled></button>
<button data-action="replace" i18n="replace" disabled></button>
<button data-action="replaceAll" i18n="replaceAll" disabled></button>
<button data-action="undo" i18n="undo" disabled></button>
<!--
Using a separate set of buttons because
1. FF can display tooltips only when specified on the <button>, ignores the nested <title> in <svg>
2. the icon doesn't fill the entire button area so tooltips aren't shown when the edges are hovered
-->
<button class="hidden" data-action="replace" i18n-title="replace" disabled>
<button class="hidden" data-action="replace" i18n="title:replace" disabled>
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.83 4.75 8.76 11.82 5.2 8.26 3.51 9.95 8.76 15.19 17.52 6.43 15.83 4.75"/>
</svg>
</button>
<button class="hidden" data-action="replaceAll" i18n-title="replaceAll" disabled>
<button class="hidden" data-action="replaceAll" i18n="title:replaceAll" disabled>
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="15.8,1.8 8.8,8.8 5.2,5.3 3.5,6.9 8.8,12.2 17.5,3.4 "/>
<polygon points="15.8,7.8 8.8,14.8 5.2,11.3 3.5,12.9 8.8,18.2 17.5,9.4 "/>
</svg>
</button>
<button class="hidden" data-action="undo" i18n-title="undo" disabled>
<button class="hidden" data-action="undo" i18n="title:undo" disabled>
<svg class="svg-icon" viewBox="0 0 20 20">
<path d="M11.3,5.5H8.7V1.4L1.9,6.5l6.8,5.1V7.5h2.6c1.8,0,3.2,1.4,3.2,3.2s-1.4,3.2-3.2,3.2H7.8v2h3.5c2.9,0,5.2-2.3,5.2-5.2S14.2,5.5,11.3,5.5z"/>
</svg>
@ -212,7 +203,7 @@
</template>
<template data-id="jumpToLine">
<span i18n-text="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
<span i18n="editGotoLine">: <input class="CodeMirror-jump-field" type="text"></span>
</template>
<template data-id="regexpTestPartial">
@ -220,15 +211,15 @@
</template>
<template data-id="resizeGrip">
<div class="resize-grip" i18n-title="cm_resizeGripHint"></div>
<div class="resize-grip" i18n="title:cm_resizeGripHint"></div>
</template>
<template data-id="keymapHelp">
<table class="keymap-list">
<thead>
<tr>
<th><input i18n-placeholder="helpKeyMapHotkey" type="search" class="can-close-on-esc"></th>
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc" spellcheck="false"></th>
<th><input i18n="placeholder:helpKeyMapHotkey" type="search"></th>
<th><input i18n="placeholder:helpKeyMapCommand" type="search"></th>
</tr>
</thead>
<tbody>
@ -248,16 +239,20 @@
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
<link href="js/color/color-picker.css" rel="stylesheet">
<link href="edit/codemirror-default.css" rel="stylesheet">
<link href="edit/edit.css" rel="stylesheet">
</head>
<body id="stylus-edit">
<template data-id="body"> <!-- https://crbug.com/1288447 -->
<div id="header">
<h1 id="heading">&nbsp;</h1> <!-- nbsp allocates the actual height which prevents page shift -->
<h1 id="heading" i18n="data-edit:editStyleHeading, data-add:addStyleTitle">
<a class="usercss-only"
href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n="title:externalUsercssDocument" target="_blank">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</h1>
<section id="basic-info">
<div id="basic-info-name">
<input id="name" class="style-contributor" spellcheck="false">
<a id="reset-name" href="#" i18n-title="customNameResetHint" tabindex="0" hidden>
<a id="reset-name" i18n="title:customNameResetHint" tabindex="0" hidden>
<svg class="svg-icon" viewBox="0 0 20 20">
<polygon points="16.2,5.5 14.5,3.8 10,8.3 5.5,3.8 3.8,5.5 8.3,10 3.8,14.5
5.5,16.2 10,11.7 14.5,16.2 16.2,14.5 11.7,10 "/>
@ -267,87 +262,99 @@
</div>
<div id="basic-info-enabled">
<label id="enabled-label"
i18n-text="styleEnabledLabel"
i18n-title="toggleStyle"
i18n="styleEnabledLabel, title:toggleStyle"
data-hotkey-tooltip="toggleStyle">
<input type="checkbox" id="enabled" class="style-contributor">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<label id="preview-label" i18n-text="previewLabel" i18n-title="previewTooltip" class="hidden">
<label id="preview-label" i18n="previewLabel, title:previewTooltip">
<input type="checkbox" id="editor.livePreview">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<span id="preview-errors" class="hidden">!</span>
<label id="disableAll-label" i18n="data-on:disableAllStyles, data-off:disableAllStylesOff">
<input id="disableAll" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<span id="preview-errors" hidden>!</span>
</div>
</section>
<section id="actions">
<div>
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save" disabled></button>
<button id="beautify" i18n-text="styleBeautify"></button>
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a>
<div class="buttons">
<div class="split-btn">
<button id="save-button" i18n="styleSaveLabel" data-hotkey-tooltip="save" disabled></button
><button class="split-btn-pedal usercss-only" i18n="menu-tpl:saveAsTemplate"></button>
</div>
<button id="beautify" i18n="styleBeautify"></button>
<button id="style-settings-btn" i18n="settings"></button>
<button id="cancel-button" i18n="title:styleCancelEditLabel"></button>
</div>
<div id="mozilla-format-buttons" class="sectioned-only">
<button id="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button>
<a id="to-mozilla-help" class="svg-inline-wrapper" href="#" tabindex="0"
i18n-title="styleMozillaFormatHeading">
<div id="mozilla-format-buttons" class="buttons sectioned-only">
<button id="from-mozilla" i18n="importLabel"></button>
<button id="to-mozilla" i18n="exportLabel"></button>
<a id="to-mozilla-help" class="svg-inline-wrapper" tabindex="0"
i18n="title:styleMozillaFormatHeading">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
</section>
<div id="details-wrapper">
<details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary>
<summary><h2 id="options-heading" i18n="editorSettings"></h2></summary>
<div id="options-wrapper">
<div class="options-column">
<div class="option">
<label id="lineWrapping-label" i18n-text="cm_lineWrapping">
<label id="lineWrapping-label" i18n="cm_lineWrapping">
<input id="editor.lineWrapping" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label id="smartIndent-label" i18n-text="cm_smartIndent">
<label id="smartIndent-label" i18n="cm_smartIndent">
<input id="editor.smartIndent" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label id="indentWithTabs-label" i18n-text="cm_indentWithTabs">
<label id="indentWithTabs-label" i18n="cm_indentWithTabs">
<input id="editor.indentWithTabs" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label i18n-text="cm_autoCloseBrackets" i18n-title="cm_autoCloseBracketsTooltip">
<label i18n="cm_autoCloseBrackets, title:cm_autoCloseBracketsTooltip">
<input id="editor.autoCloseBrackets" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label i18n-text="cm_autocompleteOnTyping">
<label i18n="cm_autocompleteOnTyping">
<input id="editor.autocompleteOnTyping" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label i18n-text="cm_selectByTokens"
i18n-title="cm_selectByTokensTooltip">
<label i18n="cm_selectByTokens, title:cm_selectByTokensTooltip">
<input id="editor.selectByTokens" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option sectioned-only">
<label i18n="cm_arrowKeysTraverse">
<input id="editor.arrowKeysTraverse" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label i18n-text="cm_colorpicker">
<label i18n="cm_colorpicker">
<input id="editor.colorpicker" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<a id="colorpicker-settings" href="#" class="svg-inline-wrapper" i18n-title="shortcutsNote" tabindex="0">
<a id="colorpicker-settings" class="svg-inline-wrapper" i18n="title:shortcutsNote" tabindex="0">
<svg class="svg-icon settings"><use xlink:href="#svg-icon-config"/></svg>
</a>
</div>
<div class="option usercss-only">
<label i18n-text="appliesLineWidgetLabel" i18n-title="appliesLineWidgetWarning">
<label i18n="appliesLineWidgetLabel, title:appliesLineWidgetWarning">
<input id="editor.appliesToLineWidget" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
@ -355,74 +362,89 @@
</div>
<div class="options-column">
<div class="option aligned">
<label id="tabSize-label" for="editor.tabSize" i18n-text="cm_tabSize"></label>
<label id="tabSize-label" for="editor.tabSize" i18n="cm_tabSize"></label>
<input id="editor.tabSize" type="number" min="0">
</div>
<div class="option aligned">
<label id="keyMap-label" for="editor.keyMap" i18n-text="cm_keyMap"></label>
<label id="keyMap-label" for="editor.keyMap" i18n="cm_keyMap"></label>
<div class="select-resizer">
<select id="editor.keyMap"></select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<a id="keyMap-help" href="#" class="svg-inline-wrapper" tabindex="0">
<a id="keyMap-help" class="svg-inline-wrapper" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</div>
<div class="option aligned">
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label>
<label id="theme-label" for="editor.theme" i18n="cm_theme"></label>
<div class="select-resizer">
<select id="editor.theme"></select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<div class="option aligned">
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label>
<label id="highlight-label" for="editor.matchHighlight" i18n="cm_matchHighlight"></label>
<div class="select-resizer">
<select id="editor.matchHighlight">
<option i18n-text="cm_matchHighlightToken" value="token">
<option i18n-text="cm_matchHighlightSelection" value="selection">
<option i18n-text="genericDisabledLabel" value="">
<option i18n="cm_matchHighlightToken" value="token">
<option i18n="cm_matchHighlightSelection" value="selection">
<option i18n="genericDisabledLabel" value="">
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
</div>
<div class="option aligned">
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
<label id="linter-label" for="editor.linter" i18n="cm_linter"></label>
<div class="select-resizer">
<select id="editor.linter">
<option value="csslint" selected>CSSLint</option>
<option value="stylelint">Stylelint</option>
<option value="" i18n-text="genericDisabledLabel"></option>
<option value="" i18n="genericDisabledLabel"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<a id="linter-settings" href="#" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0">
<a id="linter-settings" class="svg-inline-wrapper" i18n="title:linterConfigTooltip" tabindex="0">
<svg class="svg-icon settings"><use xlink:href="#svg-icon-config"/></svg>
</a>
</div>
</div>
</div>
</details>
<details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
<summary><h2 i18n="publish"></h2></summary>
<div>
<a id="usw-url" href="https://userstyles.world" target="_blank">&nbsp;</a>
<div id="usw-link-info">
<dl><dt i18n="styleName"></dt><dd data-usw="name"></dd></dl>
<dl><dt i18n="genericDescription"></dt><dd data-usw="description"></dd></dl>
</div>
<div>
<button id="usw-publish-style"
i18n="data-publish:publishStyle, data-push:publishPush"></button>
<button id="usw-disconnect" i18n="optionsSyncDisconnect"></button>
<span id="usw-progress"></span>
</div>
</div>
</details>
<details id="sections-list" data-pref="editor.toc.expanded" class="ignore-pref-if-compact">
<summary><h2 i18n-text="sections"></h2></summary>
<summary><h2 i18n="sections"></h2></summary>
<ol id="toc"></ol>
</details>
<details id="lint" data-pref="editor.lint.expanded" class="hidden-unless-compact ignore-pref-if-compact">
<details id="lint" data-pref="editor.lint.expanded" class="ignore-pref-if-compact" hidden>
<summary>
<h2 i18n-text="linterIssues">: <span id="issue-count"></span>
<a id="lint-help" href="#" class="svg-inline-wrapper intercepts-click" tabindex="0">
<h2><span i18n="linterIssues"></span><span id="issue-count"></span>
<a id="lint-help" class="svg-inline-wrapper intercepts-click" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</h2>
</summary>
<div class="lint-scroll-container">
<div class="lint-report-container"></div>
</div>
<div class="lint-report-container"></div>
</details>
</div>
<div id="header-resizer" i18n="title:headerResizerHint"></div>
<div id="footer" class="hidden">
<a href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n-text="externalUsercssDocument"
i18n="externalUsercssDocument"
target="_blank"></a>
</div>
</div>
@ -438,8 +460,10 @@
<path d="M0 0v8h8v-2h-1v1h-6v-6h1v-1h-2zm4 0l1.5 1.5-2.5 2.5 1 1 2.5-2.5 1.5 1.5v-4h-4z"></path>
</symbol>
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n-alt="helpAlt">
<path fill-rule="evenodd" d="M6.3 5.69a.942.942 0 0 1-.28-.7c0-.28.09-.52.28-.7.19-.18.42-.28.7-.28.28 0 .52.09.7.28.18.19.28.42.28.7 0 .28-.09.52-.28.7a1 1 0 0 1-.7.3c-.28 0-.52-.11-.7-.3zM8 7.99c-.02-.25-.11-.48-.31-.69-.2-.19-.42-.3-.69-.31H6c-.27.02-.48.13-.69.31-.2.2-.3.44-.31.69h1v3c.02.27.11.5.31.69.2.2.42.31.69.31h1c.27 0 .48-.11.69-.31.2-.19.3-.42.31-.69H8V7.98v.01zM7 2.3c-3.14 0-5.7 2.54-5.7 5.68 0 3.14 2.56 5.7 5.7 5.7s5.7-2.55 5.7-5.7c0-3.15-2.56-5.69-5.7-5.69v.01zM7 .98c3.86 0 7 3.14 7 7s-3.14 7-7 7-7-3.12-7-7 3.14-7 7-7z"></path>
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n="alt:helpAlt">
<circle cx="7" cy="5" r="1"/>
<path d="M8,8c0-0.5-0.5-1-1-1H6C5.5,7,5,7.4,5,8h1v3c0,0.5,0.5,1,1,1h1c0.5,0,1-0.4,1-1H8V8z"/>
<path d="M7,1c3.9,0,7,3.1,7,7s-3.1,7-7,7s-7-3.1-7-7S3.1,1,7,1z M7,2.3C3.9,2.3,1.3,4.9,1.3,8s2.6,5.7,5.7,5.7s5.7-2.6,5.7-5.7S10.1,2.3,7,2.3C7,2.3,7,2.3,7,2.3z"/>
</symbol>
<symbol id="svg-icon-close" viewBox="0 0 12 16">
@ -463,13 +487,21 @@
</symbol>
<symbol id="svg-icon-plus" viewBox="0 0 8 8">
<path fill-rule="evenodd" d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
<path d="M3 0v3h-3v2h3v3h2v-3h3v-2h-3v-3h-2z"/>
</symbol>
<symbol id="svg-icon-minus" viewBox="0 0 8 8">
<path fill-rule="evenodd" d="M0 3v2h8v-2h-8z"/>
<path d="M0 3v2h8v-2h-8z"/>
</symbol>
</svg>
</template>
<link href="edit/edit.css" rel="stylesheet">
<script src="js/dark-themer.js"></script> <!-- must be last in HEAD to avoid FOUC -->
</head>
<body id="stylus-edit">
<script src="edit/edit.js"></script>
</body>
</html>

View File

@ -2,6 +2,7 @@
/* global cmFactory */
/* global debounce */// toolbox.js
/* global editor */
/* global linterMan */
/* global prefs */
'use strict';
@ -11,6 +12,7 @@
const USO_VAR = 'uso-variable';
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
const USO_INVALID_VAR = 'error ' + USO_VAR;
const rxPROP = /^(prop(erty)?|variable-2|string-2)\b/;
const rxVAR = /(^|[^-.\w\u0080-\uFFFF])var\(/iyu;
const rxCONSUME = /([-\w]*\s*:\s?)?/yu;
const cssMime = CodeMirror.mimeModes['text/css'];
@ -18,27 +20,28 @@
const {tokenHooks} = cssMime;
const originalCommentHook = tokenHooks['/'];
const originalHelper = CodeMirror.hint.css || (() => {});
let cssProps, cssMedia;
let cssMedia, cssProps, cssValues;
const aot = prefs.get('editor.autocompleteOnTyping');
CodeMirror.defineOption('autocompleteOnTyping', aot, aotToggled);
if (aot) cmFactory.globalSetOption('autocompleteOnTyping', true);
const AOT_ID = 'autocompleteOnTyping';
const AOT_PREF_ID = 'editor.' + AOT_ID;
const aot = prefs.get(AOT_PREF_ID);
CodeMirror.defineOption(AOT_ID, aot, (cm, value) => {
cm[value ? 'on' : 'off']('changes', autocompleteOnTyping);
cm[value ? 'on' : 'off']('pick', autocompletePicked);
});
prefs.subscribe(AOT_PREF_ID, (key, val) => cmFactory.globalSetOption(AOT_ID, val), {runNow: aot});
CodeMirror.registerHelper('hint', 'css', helper);
CodeMirror.registerHelper('hint', 'stylus', helper);
tokenHooks['/'] = tokenizeUsoVariables;
function aotToggled(cm, value) {
cm[value ? 'on' : 'off']('changes', autocompleteOnTyping);
cm[value ? 'on' : 'off']('pick', autocompletePicked);
}
function helper(cm) {
async function helper(cm) {
const pos = cm.getCursor();
const {line, ch} = pos;
const {styles, text} = cm.getLineHandle(line);
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
const isLessLang = cm.doc.mode.helperType === 'less';
const isStylusLang = cm.doc.mode.name === 'stylus';
const type = style && style.split(' ', 1)[0] || 'prop?';
if (!type || type === 'comment' || type === 'string') {
@ -64,7 +67,7 @@
const str = text.slice(prev, end);
const left = text.slice(prev, ch).trim();
let leftLC = left.toLowerCase();
let list = [];
let list;
switch (leftLC[0]) {
case '!':
@ -84,6 +87,7 @@
'@supports',
'@viewport',
];
if (isLessLang) list = findAllCssVars(cm, left, '\\s*:').concat(list);
break;
case '#': // prevents autocomplete for #hex colors
@ -125,10 +129,31 @@
// fallthrough to `default`
default:
// property values
if (isStylusLang || getTokenState() === 'prop') {
while (i > 0 && !rxPROP.test(styles[i + 1])) i -= 2;
const propEnd = styles[i];
let prop;
if (propEnd > text.lastIndexOf(';', ch - 1)) {
while (i > 0 && rxPROP.test(styles[i + 1])) i -= 2;
prop = text.slice(styles[i] || 0, propEnd).match(/([-\w]+)?$/u)[1];
}
if (prop) {
if (/[^-\w]/.test(leftLC)) {
prev += execAt(/[\s:()]*/y, prev, text)[0].length;
leftLC = leftLC.replace(/^[^\w\s]\s*/, '');
}
if (prop.startsWith('--')) prop = 'color'; // assuming 90% of variables are colors
if (!cssProps) await initCssProps();
list = [...new Set([...cssValues.all[prop] || [], ...cssValues.global])];
end = prev + execAt(/(\s*[-a-z(]+)?/y, prev, text)[0].length;
}
}
// properties and media features
if (/^(prop(erty|\?)|atom|error)/.test(type) &&
if (!list &&
/^(prop(erty|\?)|atom|error|tag)/.test(type) &&
/^(block|atBlock_parens|maybeprop)/.test(getTokenState())) {
if (!cssProps) initCssProps();
if (!cssProps) await initCssProps();
if (type === 'prop?') {
prev += leftLC.length;
leftLC = '';
@ -136,7 +161,9 @@
list = state === 'atBlock_parens' ? cssMedia : cssProps;
end -= /\W$/u.test(str); // e.g. don't consume ) when inside ()
end += execAt(rxCONSUME, end, text)[0].length;
} else {
}
if (!list) {
return isStylusLang
? CodeMirror.hint.fromList(cm, {words: CodeMirror.hintWords.stylus})
: originalHelper(cm);
@ -149,8 +176,9 @@
};
}
function initCssProps() {
cssProps = addSuffix(cssMime.propertyKeywords);
async function initCssProps() {
cssValues = await linterMan.worker.getCssPropsValues();
cssProps = addSuffix(cssValues.all);
cssMedia = [].concat(...Object.entries(cssMime).map(getMediaKeys).filter(Boolean)).sort();
}
@ -170,13 +198,15 @@
!style.startsWith(USO_VALID_VAR) && !style.startsWith(USO_INVALID_VAR);
}
function findAllCssVars(cm, leftPart) {
function findAllCssVars(cm, leftPart, rightPart = '') {
// simplified regex without CSS escapes
const [, prefixed, named] = leftPart.match(/^(--|@)?(\S)?/);
const rx = new RegExp(
'(?:^|[\\s/;{])(' +
(leftPart.startsWith('--') ? leftPart : '--') +
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
'[-0-9a-zA-Z_\u0080-\uFFFF]*)',
(prefixed ? leftPart : '--') +
(named ? '' : '[a-zA-Z_\u0080-\uFFFF]') +
'[-0-9a-zA-Z_\u0080-\uFFFF]*)' +
rightPart,
'g');
const list = new Set();
cm.eachLine(({text}) => {

View File

@ -1,19 +1,13 @@
/* global $ $$ $create setupLivePrefs waitForSelector */// dom.js
/* global $$ $ $create messageBoxProxy setInputValue setupLivePrefs */// dom.js
/* global API */// msg.js
/* global CODEMIRROR_THEMES */
/* global CodeMirror */
/* global MozDocMapper */// sections-util.js
/* global chromeSync */// storage-util.js
/* global initBeautifyButton */// beautify.js
/* global prefs */
/* global t */// localization.js
/* global
FIREFOX
debounce
getOwnTab
sessionStore
tryCatch
tryJSONparse
*/// toolbox.js
/* global FIREFOX getOwnTab sessionStore tryJSONparse tryURL */// toolbox.js
'use strict';
/**
@ -21,15 +15,32 @@
* @namespace Editor
*/
const editor = {
style: null,
dirty: DirtyReporter(),
isUsercss: false,
isWindowed: false,
livePreview: null,
livePreview: LivePreview(),
/** @type {'customName'|'name'} */
nameTarget: 'name',
previewDelay: 200, // Chrome devtools uses 200
saving: false,
scrollInfo: null,
cancel: () => location.assign('/manage.html'),
updateClass() {
$.rootCL.toggle('is-new-style', !editor.style.id);
},
updateTheme(name) {
if (!CODEMIRROR_THEMES[name]) {
name = 'default';
prefs.set('editor.theme', name);
}
$('#cm-theme').dataset.theme = name;
$('#cm-theme').textContent = CODEMIRROR_THEMES[name] || '';
},
updateTitle(isDirty = editor.dirty.isDirty()) {
const {customName, name} = editor.style;
document.title = `${
@ -42,39 +53,22 @@ const editor = {
//#region pre-init
const baseInit = (() => {
const lazyKeymaps = {
emacs: '/vendor/codemirror/keymap/emacs',
vim: '/vendor/codemirror/keymap/vim',
};
const domReady = waitForSelector('#sections');
return {
domReady,
ready: Promise.all([
domReady,
loadStyle(),
prefs.ready.then(() =>
Promise.all([
loadTheme(),
loadKeymaps(),
])),
]),
};
/** Preloads vim/emacs keymap only if it's the active one, otherwise will load later */
function loadKeymaps() {
const km = prefs.get('editor.keyMap');
return /emacs/i.test(km) && require([lazyKeymaps.emacs]) ||
/vim/i.test(km) && require([lazyKeymaps.vim]);
}
(() => {
const mqCompact = matchMedia('(max-width: 850px)');
const toggleCompact = mq => $.rootCL.toggle('compact-layout', mq.matches);
mqCompact.on('change', toggleCompact);
toggleCompact(mqCompact);
Object.assign(editor, /** @namespace Editor */ {
mqCompact,
styleReady: prefs.ready.then(loadStyle),
});
async function loadStyle() {
const params = new URLSearchParams(location.search);
const id = Number(params.get('id'));
let id = Number(params.get('id'));
const style = id && await API.styles.get(id) || {
id: id = null, // resetting the non-existent id
name: params.get('domain') ||
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
tryURL(params.get('url-prefix')).hostname ||
'',
enabled: true,
sections: [
@ -82,89 +76,37 @@ const baseInit = (() => {
],
};
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
editor.isUsercss = Boolean(style.usercssData || !style.id && prefs.get('newStyleAsUsercss'));
editor.lazyKeymaps = lazyKeymaps;
editor.style = style;
const isUC = Boolean(style.usercssData || !id && prefs.get('newStyleAsUsercss'));
Object.assign(editor, /** @namespace Editor */ {
style,
isUsercss: isUC,
template: isUC && !id && chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate), // promise
});
editor.updateClass();
editor.updateTheme(prefs.get('editor.theme'));
editor.updateTitle(false);
document.documentElement.classList.toggle('usercss', editor.isUsercss);
sessionStore.justEditedStyleId = style.id || '';
$.rootCL.add(isUC ? 'usercss' : 'sectioned');
sessionStore.justEditedStyleId = id || '';
// no such style so let's clear the invalid URL parameters
if (!style.id) history.replaceState({}, '', location.pathname);
}
/** Preloads the theme so CodeMirror can use the correct metrics in its first render */
async function loadTheme() {
const theme = prefs.get('editor.theme');
if (theme !== 'default') {
const el = $('#cm-theme');
const el2 = await require([`/vendor/codemirror/theme/${theme}.css`]);
el2.id = el.id;
el.remove();
if (!el2.sheet) {
prefs.set('editor.theme', 'default');
}
}
if (!id) history.replaceState({}, '', location.pathname);
}
})();
//#endregion
//#region init layout/resize
baseInit.domReady.then(() => {
let headerHeight;
detectLayout(true);
window.on('resize', () => detectLayout());
function detectLayout(now) {
const compact = window.innerWidth <= 850;
if (compact) {
document.body.classList.add('compact-layout');
if (!editor.isUsercss) {
if (now) fixedHeader();
else debounce(fixedHeader, 250);
window.on('scroll', fixedHeader, {passive: true});
}
} else {
document.body.classList.remove('compact-layout', 'fixed-header');
window.off('scroll', fixedHeader);
}
for (const type of ['options', 'toc', 'lint']) {
const el = $(`details[data-pref="editor.${type}.expanded"]`);
el.open = compact ? false : prefs.get(el.dataset.pref);
}
}
function fixedHeader() {
const headerFixed = $('.fixed-header');
if (!headerFixed) headerHeight = $('#header').clientHeight;
const scrollPoint = headerHeight - 43;
if (window.scrollY >= scrollPoint && !headerFixed) {
$('body').style.setProperty('--fixed-padding', ` ${headerHeight}px`);
$('body').classList.add('fixed-header');
} else if (window.scrollY < scrollPoint && headerFixed) {
$('body').classList.remove('fixed-header');
}
}
});
//#endregion
//#region init header
baseInit.ready.then(() => {
/* exported EditorHeader */
function EditorHeader() {
initBeautifyButton($('#beautify'));
initKeymapElement();
initNameArea();
initThemeElement();
setupLivePrefs();
$('#heading').textContent = t(editor.style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#preview-label').classList.toggle('hidden', !editor.style.id);
require(Object.values(editor.lazyKeymaps), () => {
initKeymapElement();
window.on('load', () => {
prefs.subscribe('editor.keyMap', showHotkeyInTooltip, {runNow: true});
window.on('showHotkeyInTooltip', showHotkeyInTooltip);
});
}, {once: true});
function findKeyForCommand(command, map) {
if (typeof map === 'string') map = CodeMirror.keyMap[map];
@ -190,19 +132,12 @@ baseInit.ready.then(() => {
nameEl.title = isCustomName ? t('customNameHint') : '';
nameEl.on('input', () => {
editor.updateName(true);
resetEl.hidden = false;
resetEl.hidden = !editor.style.customName;
});
resetEl.hidden = !editor.style.customName;
resetEl.onclick = () => {
const {style} = editor;
nameEl.focus();
nameEl.select();
// trying to make it undoable via Ctrl-Z
if (!document.execCommand('insertText', false, style.name)) {
nameEl.value = style.name;
editor.updateName(true);
}
style.customName = null; // to delete it from db
editor.style.customName = null; // to delete it from db
setInputValue(nameEl, editor.style.name);
resetEl.hidden = true;
};
const enabledEl = $('#enabled');
@ -212,7 +147,7 @@ baseInit.ready.then(() => {
function initThemeElement() {
$('#editor.theme').append(...[
$create('option', {value: 'default'}, t('defaultTheme')),
...CODEMIRROR_THEMES.map(s => $create('option', s)),
...Object.keys(CODEMIRROR_THEMES).map(s => $create('option', s)),
]);
// move the theme after built-in CSS so that its same-specificity selectors win
document.head.appendChild($('#cm-theme'));
@ -268,7 +203,7 @@ baseInit.ready.then(() => {
}
}
}
});
}
//#endregion
//#region init windowed mode
@ -285,22 +220,17 @@ baseInit.ready.then(() => {
}
}
getOwnTab().then(async tab => {
getOwnTab().then(tab => {
ownTabId = tab.id;
// use browser history back when 'back to manage' is clicked
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
await baseInit.domReady;
$('#cancel-button').onclick = event => {
event.stopPropagation();
event.preventDefault();
history.back();
};
editor.cancel = () => history.back();
}
});
async function initWindowedMode() {
chrome.tabs.onAttached.addListener(onTabAttached);
const isSimple = (await browser.windows.getCurrent()).type === 'popup';
// Chrome 96+ bug: the type is 'app' for a window that was restored via Ctrl-Shift-T
const isSimple = ['app', 'popup'].includes((await browser.windows.getCurrent()).type);
if (isSimple) require(['/edit/embedded-popup']);
editor.isWindowed = isSimple || (
history.length === 1 &&
@ -336,9 +266,15 @@ baseInit.ready.then(() => {
function DirtyReporter() {
const data = new Map();
const listeners = new Set();
const dataListeners = new Set();
const notifyChange = wasDirty => {
if (wasDirty !== (data.size > 0)) {
listeners.forEach(cb => cb());
const isDirty = data.size > 0;
const flipped = isDirty !== wasDirty;
if (flipped) {
listeners.forEach(cb => cb(isDirty));
}
if (flipped || isDirty) {
dataListeners.forEach(cb => cb(isDirty));
}
};
/** @namespace DirtyReporter */
@ -355,17 +291,19 @@ function DirtyReporter() {
saved.newValue = value;
saved.type = 'modify';
}
} else {
return;
}
notifyChange(wasDirty);
},
clear(obj) {
const wasDirty = data.size > 0;
if (obj === undefined) {
data.clear();
} else {
data.delete(obj);
clear(...objs) {
if (data.size && (
objs.length
? objs.map(data.delete, data).includes(true)
: (data.clear(), true)
)) {
notifyChange(true);
}
notifyChange(wasDirty);
},
has(key) {
return data.has(key);
@ -379,6 +317,8 @@ function DirtyReporter() {
if (!saved) {
if (oldValue !== newValue) {
data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
} else {
return;
}
} else if (saved.type === 'modify') {
if (saved.savedValue === newValue) {
@ -388,12 +328,17 @@ function DirtyReporter() {
}
} else if (saved.type === 'add') {
saved.newValue = newValue;
} else {
return;
}
notifyChange(wasDirty);
},
onChange(cb, add = true) {
listeners[add ? 'add' : 'delete'](cb);
},
onDataChange(cb, add = true) {
dataListeners[add ? 'add' : 'delete'](cb);
},
remove(obj, value) {
const wasDirty = data.size > 0;
const saved = data.get(obj);
@ -403,10 +348,80 @@ function DirtyReporter() {
data.delete(obj);
} else if (saved.type === 'modify') {
saved.type = 'remove';
} else {
return;
}
notifyChange(wasDirty);
},
};
}
function LivePreview() {
let el;
let data;
let port;
let preprocess;
let enabled = prefs.get('editor.livePreview');
prefs.subscribe('editor.livePreview', (key, value) => {
if (!value) {
if (port) {
port.disconnect();
port = null;
}
} else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
createPreviewer();
updatePreviewer(data);
}
enabled = value;
});
return {
/**
* @param {Function} [fn] - preprocessor
*/
init(fn) {
preprocess = fn;
},
update(newData) {
data = newData;
if (!port) {
if (!data.id || !data.enabled || !enabled) {
return;
}
createPreviewer();
}
updatePreviewer(data);
},
};
function createPreviewer() {
port = chrome.runtime.connect({name: 'livePreview'});
port.onDisconnect.addListener(err => {
throw err;
});
el = $('#preview-errors');
el.onclick = () => messageBoxProxy.alert(el.title, 'pre');
}
async function updatePreviewer(data) {
try {
port.postMessage(preprocess ? await preprocess(data) : data);
el.hidden = true;
} catch (err) {
if (Array.isArray(err)) {
err = err.map(e => e.message || e).join('\n');
} else if (err && err.index != null) {
// 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 || err}`;
}
el.title = err.message || `${err}`;
el.hidden = false;
}
}
}
//#endregion

View File

@ -65,7 +65,7 @@ function beautifyEditor(cm, options, ui) {
window.scrollTo(scrollX, scrollY);
cm.beautifyChange[cm.changeGeneration()] = true;
if (ui) {
$('#help-popup button[role="close"]').disabled = false;
$('button[role="close"]', helpPopup.div).disabled = false;
}
}
}
@ -82,10 +82,14 @@ function createBeautifyUI(scope, options) {
$createOption('}', 'newline_between_rules'),
$createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'),
$createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'),
editor.isUsercss && $createLabeledCheckbox('indent_mozdoc', '', '... @-moz-document'),
]),
$create('p.beautify-hint', [
$create('span', t('styleBeautifyHint') + '\u00A0'),
createHotkeyInput('editor.beautify.hotkey', () => moveFocus($('#help-popup'), 0)),
createHotkeyInput('editor.beautify.hotkey', {
buttons: false,
onDone: () => moveFocus(helpPopup.div, 0),
}),
]),
$create('.buttons', [
$create('button', {
@ -110,16 +114,16 @@ function createBeautifyUI(scope, options) {
},
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
]),
]));
$('#help-popup').className = 'wide';
]),
{
className: 'wide',
});
$('.beautify-options').onchange = ({target}) => {
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
const elLine = target.closest('[newline]');
if (elLine) elLine.setAttribute('newline', value);
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
if (target.parentNode.hasAttribute('newline')) {
target.parentNode.setAttribute('newline', value.toString());
}
beautify(scope, false);
};
@ -145,7 +149,7 @@ function createBeautifyUI(scope, options) {
);
}
function $createLabeledCheckbox(optionName, i18nKey) {
function $createLabeledCheckbox(optionName, i18nKey, text) {
return (
$create('label', {style: 'display: block; clear: both;'}, [
$create('input', {
@ -155,7 +159,7 @@ function createBeautifyUI(scope, options) {
}),
$create('SVG:svg.svg-icon.checked',
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
t(i18nKey),
i18nKey ? t(i18nKey) : text,
])
);
}

View File

@ -4,13 +4,23 @@
z-index: 999;
}
.CodeMirror-hint:hover {
color: white;
color: var(--bg);
background: #08f;
}
.CodeMirror {
border: solid #CCC 1px;
border: solid var(--c80) 1px;
transition: box-shadow .1s;
}
.CodeMirror {
color: inherit;
background-color: inherit;
border: solid var(--c80) 1px;
transition: box-shadow .1s;
}
.CodeMirror-gutters {
background-color: var(--c95);
border-color: var(--c85);
}
#stylus#stylus .CodeMirror {
/* Using a specificity hack to override userstyles */
/* Not using the ring-color hack as it became ugly in new Chrome */
@ -26,7 +36,7 @@
width: 5em;
}
.CodeMirror-search-hint {
color: #888;
color: var(--c50);
}
.CodeMirror-activeline .applies-to:before {
background-color: hsla(214, 100%, 90%, 0.15);
@ -65,6 +75,10 @@
.CodeMirror-linenumber {
cursor: pointer; /* for bookmarking */
}
.cm-matchhighlight,
.CodeMirror-selection-highlight-scrollbar {
background: hsla(200, 100%, 50%, var(--match-hl-opacity, .1));
}
/* Custom stuff we add to CodeMirror */
@ -74,3 +88,61 @@
.gutter-bookmark {
background: linear-gradient(0deg, hsla(180, 100%, 30%, .75) 2px, hsla(180, 100%, 30%, .2) 2px);
}
@media screen and (prefers-color-scheme: dark), dark {
.CodeMirror {
--match-hl-opacity: .18;
}
.CodeMirror-dialog {
background-color: #333;
}
.CodeMirror-dialog-top {
border-color: #555;
}
.CodeMirror-activeline-background {
background: hsl(180, 21%, 18%);
}
.CodeMirror-selected,
.CodeMirror-focused .CodeMirror-selected,
.CodeMirror-line::selection,
.CodeMirror-line > span::selection,
.CodeMirror-line > span > span::selection {
background: #444;
}
.CodeMirror-line::-moz-selection,
.CodeMirror-line > span::-moz-selection,
.CodeMirror-line > span > span::-moz-selection {
/* TODO: remove this when strict_min_version >= 62 */
background: #444;
}
.cm-s-default div.CodeMirror-cursor {
border-left: 1px solid #fff;
}
/* Using Chromium's dark devtools colors */
.cm-s-default .cm-atom,
.cm-s-default .cm-number { color: #a1f7b5 }
.cm-s-default .cm-attribute { color: #6194c6 }
.cm-s-default .cm-bracket { color: #997 }
.cm-s-default .cm-builtin,
.cm-s-default .cm-link { color: #9fb4d6 }
.cm-s-default .cm-comment { color: #747474 }
.cm-s-default .cm-qualifier { color: #ffa34f }
.cm-s-default .cm-def,
.cm-s-default .cm-header,
.cm-s-default .cm-tag,
.cm-s-default .cm-type { color: #5db0d7 }
.cm-s-default .cm-hr { color: #999 }
.cm-s-default .cm-keyword { color: #9a7fd5 }
.cm-s-default .cm-meta { color: #ddfb55 }
.cm-s-default .cm-operator { color: #d2c057 }
.cm-s-default .cm-string { color: #f28b54 }
.cm-s-default .cm-variable { color: #d9d9d9 }
.cm-s-default .cm-variable-2 { color: #72b9ff }
.cm-s-default .cm-variable-3 { color: #9bbbdc }
@keyframes highlight {
from {
background-color: #888;
}
}
}

View File

@ -1,5 +1,6 @@
/* global $ */// dom.js
/* global CodeMirror */
/* global UA */// toolbox.js
/* global editor */
/* global prefs */
/* global t */// localization.js
@ -25,7 +26,7 @@
matchBrackets: true,
hintOptions: {},
lintReportDelay: prefs.get('editor.lintReportDelay'),
styleActiveLine: true,
styleActiveLine: {nonEmpty: true},
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap'),
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
@ -41,7 +42,7 @@
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
// Adding hotkeys to some keymaps except 'basic' which is primitive by design
require(Object.values(typeof editor === 'object' && editor.lazyKeymaps || {}), () => {
{
const KM = CodeMirror.keyMap;
const extras = Object.values(CodeMirror.defaults.extraKeys);
if (!extras.includes('jumpToLine')) {
@ -62,7 +63,7 @@
if (!extras.includes('blockComment')) {
KM.sublime['Shift-Ctrl-/'] = 'commentSelection';
}
if (navigator.appVersion.includes('Windows')) {
if (UA.windows) {
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
if (!extras.includes('findNext')) KM.pcDefault['F3'] = 'findNext';
if (!extras.includes('findPrev')) KM.pcDefault['Shift-F3'] = 'findPrev';
@ -89,33 +90,8 @@
}
}
}
});
}
const cssMime = CodeMirror.mimeModes['text/css'];
Object.assign(cssMime.propertyKeywords, {
'content-visibility': true,
'overflow-anchor': true,
'overscroll-behavior': true,
});
Object.assign(cssMime.colorKeywords, {
'darkgrey': true,
'darkslategrey': true,
'dimgrey': true,
'lightgrey': true,
'lightslategrey': true,
'slategrey': true,
});
Object.assign(cssMime.valueKeywords, {
'blur': true,
'brightness': true,
'contrast': true,
'cubic-bezier': true,
'drop-shadow': true,
'fit-content': true,
'hue-rotate': true,
'saturate': true,
'sepia': true,
});
Object.assign(CodeMirror.prototype, {
/**
* @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
@ -126,6 +102,7 @@
const m = this.doc.mode;
if (force || (m.helperType ? m.helperType !== pp : m.name !== name)) {
this.setOption('mode', name);
this.doc.mode.lineComment = ''; // stylelint chokes on line comments a lot
}
},
/** Superfast GC-friendly check that runs until the first non-space line */

View File

@ -1,4 +1,3 @@
/* global $ */// dom.js
/* global CodeMirror */
/* global editor */
/* global prefs */
@ -32,7 +31,7 @@
globalSetOption(key, value) {
CodeMirror.defaults[key] = value;
if (cms.size > 4 && lazyOpt && lazyOpt.names.includes(key)) {
if (cms.size > 4 && lazyOpt.names.includes(key)) {
lazyOpt.set(key, value);
} else {
cms.forEach(cm => cm.setOption(key, value));
@ -40,14 +39,18 @@
},
};
// focus and blur
const onCmFocus = cm => {
rerouteHotkeys.toggle(false);
cm.display.wrapper.classList.add('CodeMirror-active');
cm.lastActive = Date.now();
};
const onCmBlur = cm => {
rerouteHotkeys.toggle(true);
setTimeout(() => {
/* Delaying to next tick to avoid double-processing of the currently processed keyboard event
* when it bubbles up from CodeMirror to `document` where the rerouter listens */
rerouteHotkeys.toggle(true);
const {wrapper} = cm.display;
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement));
});
@ -57,36 +60,50 @@
cm.on('blur', onCmBlur);
});
const handledPrefs = {
'editor.colorpicker'() {}, // handled in colorpicker-helper.js
async 'editor.theme'(key, value) {
let el2;
const el = $('#cm-theme');
if (value === 'default') {
el.href = '';
} else {
const path = `/vendor/codemirror/theme/${value}.css`;
if (el.href !== location.origin + path) {
// avoid flicker: wait for the second stylesheet to load, then apply the theme
el2 = await require([path]);
}
}
cmFactory.globalSetOption('theme', value);
if (el2) {
el.remove();
el2.id = el.id;
}
},
};
const pref2opt = k => k.slice('editor.'.length);
const mirroredPrefs = prefs.knownKeys.filter(k =>
!handledPrefs[k] &&
k.startsWith('editor.') &&
Object.hasOwnProperty.call(CodeMirror.defaults, pref2opt(k)));
prefs.subscribe(mirroredPrefs, (k, val) => cmFactory.globalSetOption(pref2opt(k), val));
prefs.subscribeMany(handledPrefs);
// propagated preferences
lazyOpt = window.IntersectionObserver && {
const prefToCmOpt = k =>
k.startsWith('editor.') &&
k.slice('editor.'.length);
const prefKeys = prefs.knownKeys.filter(k =>
k !== 'editor.colorpicker' && // handled in colorpicker-helper.js
k !== 'editor.arrowKeysTraverse' && // handled in sections-editor.js
prefToCmOpt(k) in CodeMirror.defaults);
const {insertTab, insertSoftTab} = CodeMirror.commands;
for (const [key, fn] of Object.entries({
'editor.tabSize'(cm, value) {
cm.setOption('indentUnit', Number(value));
},
'editor.indentWithTabs'(cm, value) {
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
},
'editor.matchHighlight'(cm, value) {
const showToken = value === 'token' && /[#.\-\w]/;
const opt = (showToken || value === 'selection') && {
showToken,
annotateScrollbar: true,
delay: 0,
onUpdate: updateMatchHighlightCount,
};
cm.setOption('highlightSelectionMatches', opt || null);
},
'editor.selectByTokens'(cm, value) {
cm.setOption('configureMouse', value ? configureMouseFn : null);
},
})) {
CodeMirror.defineOption(prefToCmOpt(key), prefs.get(key), fn);
prefKeys.push(key);
}
prefs.subscribe(prefKeys, (key, val) => {
if (key === 'editor.theme') editor.updateTheme(val);
cmFactory.globalSetOption(prefToCmOpt(key), val);
});
// lazy propagation
lazyOpt = {
names: ['theme', 'lineWrapping'],
set(key, value) {
const {observer, queue} = lazyOpt;
@ -160,30 +177,6 @@
//#endregion
//#region CM option handlers
const {insertTab, insertSoftTab} = CodeMirror.commands;
Object.entries({
tabSize(cm, value) {
cm.setOption('indentUnit', Number(value));
},
indentWithTabs(cm, value) {
CodeMirror.commands.insertTab = value ? insertTab : insertSoftTab;
},
matchHighlight(cm, value) {
const showToken = value === 'token' && /[#.\-\w]/;
const opt = (showToken || value === 'selection') && {
showToken,
annotateScrollbar: true,
onUpdate: updateMatchHighlightCount,
};
cm.setOption('highlightSelectionMatches', opt || null);
},
selectByTokens(cm, value) {
cm.setOption('configureMouse', value ? configureMouseFn : null);
},
}).forEach(([name, fn]) => {
CodeMirror.defineOption(name, prefs.get('editor.' + name), fn);
});
function updateMatchHighlightCount(cm, state) {
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
}
@ -251,7 +244,20 @@
const BM_CLS = 'gutter-bookmark';
const BM_BRAND = 'sublimeBookmark';
const BM_CLICKER = 'CodeMirror-linenumbers';
const {markText} = CodeMirror.prototype;
const BM_DATA = Symbol('data');
// TODO: revisit when https://github.com/codemirror/CodeMirror/issues/6716 is fixed
const tmProto = CodeMirror.TextMarker.prototype;
const tmProtoOvr = {};
for (const k of ['clear', 'attachLine', 'detachLine']) {
tmProtoOvr[k] = function (line) {
const {cm} = this.doc;
const withOp = !cm.curOp;
if (withOp) cm.startOperation();
tmProto[k].apply(this, arguments);
cm.curOp.ownsGroup.delayedCallbacks.push(toggleMark.bind(this, this.lines[0], line));
if (withOp) cm.endOperation();
};
}
for (const name of ['prevBookmark', 'nextBookmark']) {
const cmdFn = CodeMirror.commands[name];
CodeMirror.commands[name] = cm => {
@ -263,29 +269,9 @@
CodeMirror.defineInitHook(cm => {
cm.on('gutterClick', onGutterClick);
cm.on('gutterContextMenu', onGutterContextMenu);
cm.on('markerAdded', onMarkAdded);
});
// TODO: reimplement bookmarking so next/prev order is decided solely by the line numbers
Object.assign(CodeMirror.prototype, {
markText() {
const marker = markText.apply(this, arguments);
if (marker[BM_BRAND]) {
this.doc.addLineClass(marker.lines[0], 'gutter', BM_CLS);
marker.clear = clearMarker;
}
return marker;
},
});
function clearMarker() {
const line = this.lines[0];
const spans = line.markedSpans;
delete this.clear; // removing our patch from the instance...
this.clear(); // ...and using the original prototype
if (!spans || spans.some(span => span.marker[BM_BRAND])) {
this.doc.removeLineClass(line, 'gutter', BM_CLS);
}
}
function onGutterClick(cm, line, name, e) {
switch (name === BM_CLICKER && e.button) {
case 0: {
@ -301,13 +287,27 @@
break;
}
}
function onGutterContextMenu(cm, line, name, e) {
if (name === BM_CLICKER) {
cm.execCommand(e.ctrlKey ? 'prevBookmark' : 'nextBookmark');
e.preventDefault();
}
}
function onMarkAdded(cm, mark) {
if (mark[BM_BRAND]) {
// CM bug workaround to keep the mark at line start when the above line is removed
mark.inclusiveRight = true;
Object.assign(mark, tmProtoOvr);
toggleMark.call(mark, true, mark[BM_DATA] = mark.lines[0]);
}
}
function toggleMark(state, line = this[BM_DATA]) {
this.doc[state ? 'addLineClass' : 'removeLineClass'](line, 'gutter', BM_CLS);
if (state) {
const bms = this.doc.cm.state.sublimeBookmarks;
if (!bms.includes(this)) bms.push(this);
}
}
//#endregion
})();

File diff suppressed because one or more lines are too long

68
edit/drafts.js Normal file
View File

@ -0,0 +1,68 @@
/* global messageBoxProxy */// dom.js
/* global API */// msg.js
/* global clamp debounce */// toolbox.js
/* global editor */
/* global prefs */
/* global t */// localization.js
'use strict';
(async function AutosaveDrafts() {
const makeId = () => editor.style.id || 'new';
let delay;
let port;
connectPort();
const draft = await API.drafts.get(makeId());
if (draft && draft.isUsercss === editor.isUsercss) {
const date = makeRelativeDate(draft.date);
if (await messageBoxProxy.confirm(t('draftAction'), 'danger', t('draftTitle', date))) {
await editor.replaceStyle(draft.style, draft);
} else {
API.drafts.delete(makeId());
}
}
editor.dirty.onChange(isDirty => isDirty ? connectPort() : port.disconnect());
editor.dirty.onDataChange(isDirty => debounce(updateDraft, isDirty ? delay : 0));
prefs.subscribe('editor.autosaveDraft', (key, val) => {
delay = clamp(val * 1000 | 0, 1000, 2 ** 32 - 1);
const t = debounce.timers.get(updateDraft);
if (t != null) debounce(updateDraft, t ? delay : 0);
}, {runNow: true});
function connectPort() {
port = chrome.runtime.connect({name: 'draft:' + makeId()});
}
function makeRelativeDate(date) {
let delta = (Date.now() - date) / 1000;
if (delta >= 0 && Intl.RelativeTimeFormat) {
for (const [span, unit, frac = 1] of [
[60, 'second', 0],
[60, 'minute', 0],
[24, 'hour'],
[7, 'day'],
[4, 'week'],
[12, 'month'],
[1e99, 'year'],
]) {
if (delta < span) {
return new Intl.RelativeTimeFormat({style: 'short'}).format(-delta.toFixed(frac), unit);
}
delta /= span;
}
}
return date.toLocaleString();
}
function updateDraft(isDirty = editor.dirty.isDirty()) {
if (!isDirty) return;
API.drafts.put({
date: Date.now(),
isUsercss: editor.isUsercss,
style: editor.getValue(true),
si: editor.makeScrollInfo(),
}, makeId());
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -1,13 +1,12 @@
/* global $ $create messageBoxProxy waitForSheet */// dom.js
/* global $$ $ $create */// dom.js
/* global API msg */// msg.js
/* global CodeMirror */
/* global SectionsEditor */
/* global SourceEditor */
/* global baseInit */
/* global clipString createHotkeyInput helpPopup */// util.js
/* global closeCurrentTab deepEqual sessionStore tryJSONparse */// toolbox.js
/* global closeCurrentTab deepEqual mapObj sessionStore tryJSONparse */// toolbox.js
/* global cmFactory */
/* global editor */
/* global editor EditorHeader */// base.js
/* global linterMan */
/* global prefs */
/* global t */// localization.js
@ -15,25 +14,29 @@
//#region init
baseInit.ready.then(async () => {
await waitForSheet();
(editor.isUsercss ? SourceEditor : SectionsEditor)();
await editor.ready;
editor.ready = true;
editor.dirty.onChange(editor.updateDirty);
document.body.appendChild(t.template.body);
prefs.subscribe('editor.toc.expanded', (k, val) => val && editor.updateToc(), {runNow: true});
prefs.subscribe('editor.linter', (key, value) => {
document.body.classList.toggle('linter-disabled', value === '');
linterMan.run();
});
EditorMethods();
editor.styleReady.then(async () => {
EditorHeader();
dispatchEvent(new Event('domReady'));
await (editor.isUsercss ? SourceEditor : SectionsEditor)();
editor.dirty.onChange(editor.updateDirty);
prefs.subscribe('editor.linter', () => linterMan.run());
// enabling after init to prevent flash of validation failure on an empty name
$('#name').required = !editor.isUsercss;
$('#save-button').onclick = editor.save;
$('#cancel-button').onclick = editor.cancel;
const elSec = $('#sections-list');
// editor.toc.expanded pref isn't saved in compact-layout so prefs.subscribe won't work
$('#sections-list').on('click', () => $('.compact-layout') && setTimeout(editor.updateToc),
{once: true});
if (elSec.open) editor.updateToc();
// and we also toggle `open` directly in other places e.g. in detectLayout()
new MutationObserver(() => elSec.open && editor.updateToc())
.observe(elSec, {attributes: true, attributeFilter: ['open']});
$('#toc').onclick = e =>
editor.jumpToEditor([...$('#toc').children].indexOf(e.target));
$('#keyMap-help').onclick = () =>
@ -42,20 +45,65 @@ baseInit.ready.then(async () => {
require(['/edit/linter-dialogs'], () => linterMan.showLintConfig());
$('#lint-help').onclick = () =>
require(['/edit/linter-dialogs'], () => linterMan.showLintHelp());
$('#style-settings-btn').onclick = () => require([
'/edit/settings.css',
'/edit/settings', /* global StyleSettings */
], () => StyleSettings());
require([
'/edit/autocomplete',
'/edit/drafts',
'/edit/global-search',
]);
});
editor.styleReady.then(async () => {
// Set up mini-header on scroll
const {isUsercss} = editor;
const el = $create({
style: `
top: 0;
height: 1px;
position: absolute;
visibility: hidden;
`.replace(/;/g, '!important;'),
});
const scroller = isUsercss ? $('.CodeMirror-scroll') : document.body;
const xoRoot = isUsercss ? scroller : undefined;
const xo = new IntersectionObserver(onScrolled, {root: xoRoot});
scroller.appendChild(el);
onCompactToggled(editor.mqCompact);
editor.mqCompact.on('change', onCompactToggled);
/** @param {MediaQueryList} mq */
function onCompactToggled(mq) {
for (const el of $$('details[data-pref]')) {
el.open = mq.matches ? false : prefs.get(el.dataset.pref);
}
if (mq.matches) {
xo.observe(el);
} else {
xo.disconnect();
}
}
/** @param {IntersectionObserverEntry[]} entries */
function onScrolled(entries) {
const h = $('#header');
const sticky = !entries.pop().isIntersecting;
if (!isUsercss) scroller.style.paddingTop = sticky ? h.offsetHeight + 'px' : '';
h.classList.toggle('sticky', sticky);
}
});
//#endregion
//#region events
msg.onExtension(request => {
const {style} = request;
switch (request.method) {
case 'styleUpdated':
if (editor.style.id === style.id &&
!['editPreview', 'editPreviewEnd', 'editSave', 'config'].includes(request.reason)) {
Promise.resolve(request.codeIsUpdated === false ? style : API.styles.get(style.id))
.then(newStyle => editor.replaceStyle(newStyle, request.codeIsUpdated));
if (editor.style.id === style.id) {
handleExternalUpdate(request);
}
break;
case 'styleDeleted':
@ -63,17 +111,47 @@ msg.onExtension(request => {
closeCurrentTab();
}
break;
case 'editDeleteText':
document.execCommand('delete');
break;
}
});
async function handleExternalUpdate({style, reason}) {
if (reason === 'editPreview' ||
reason === 'editPreviewEnd') {
return;
}
if (reason === 'editSave' && editor.saving) {
editor.saving = false;
return;
}
if (reason === 'toggle') {
if (editor.dirty.isDirty()) {
editor.toggleStyle(style.enabled);
} else {
Object.assign(editor.style, style);
}
editor.updateMeta();
editor.updateLivePreview();
return;
}
style = await API.styles.get(style.id);
if (reason === 'config') {
delete style.sourceCode;
delete style.sections;
delete style.name;
delete style.enabled;
Object.assign(editor.style, style);
} else {
await editor.replaceStyle(style);
}
window.dispatchEvent(new Event('styleSettings'));
}
window.on('beforeunload', e => {
let pos;
if (editor.isWindowed &&
document.visibilityState === 'visible' &&
prefs.get('openEditInWindow') &&
screenX !== -32000 && // Chrome uses this value for minimized windows
( // only if not maximized
screenX > 0 || outerWidth < screen.availWidth ||
screenY > 0 || outerHeight < screen.availHeight ||
@ -90,16 +168,7 @@ window.on('beforeunload', e => {
prefs.set('windowPosition', pos);
}
sessionStore.windowPos = JSON.stringify(pos || {});
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify({
scrollY: window.scrollY,
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()),
focus: cm.hasFocus(),
height: cm.display.wrapper.style.height.replace('100vh', ''),
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
sel: cm.isClean() && [cm.doc.sel.ranges, cm.doc.sel.primIndex],
})),
});
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify(editor.makeScrollInfo());
const activeElement = document.activeElement;
if (activeElement) {
// blurring triggers 'change' or 'input' event if needed
@ -116,7 +185,7 @@ window.on('beforeunload', e => {
//#endregion
//#region editor methods
(() => {
function EditorMethods() {
const toc = [];
const {dirty} = editor;
let {style} = editor;
@ -138,17 +207,37 @@ window.on('beforeunload', e => {
applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
if (si && si.sel) {
const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js
cm.operation(() => {
cm.setSelections(...si.sel, {scroll: false});
cm.scrollIntoView(cm.getCursor(), si.parentHeight / 2);
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
});
cm.setSelections(...si.sel, {scroll: false});
cm.state.sublimeBookmarks = si.bookmarks.map(b => cm.markText(b.from, b.to, bmOpts));
Object.assign(cm.display.scroller, si.scroll); // for source editor
Object.assign(cm.doc, si.scroll); // for sectioned editor
}
},
toggleStyle() {
$('#enabled').checked = !style.enabled;
editor.updateEnabledness(!style.enabled);
makeScrollInfo() {
return {
scrollY: window.scrollY,
cms: editor.getEditors().map(cm => /** @namespace EditorScrollInfo */({
bookmarks: (cm.state.sublimeBookmarks || []).map(b => b.find()),
focus: cm.hasFocus(),
height: cm.display.wrapper.style.height.replace('100vh', ''),
parentHeight: cm.display.wrapper.parentElement.offsetHeight,
scroll: mapObj(cm.doc, null, ['scrollLeft', 'scrollTop']),
sel: [cm.doc.sel.ranges, cm.doc.sel.primIndex],
})),
};
},
async save() {
if (dirty.isDirty()) {
editor.saving = true;
await editor.saveImpl();
}
},
toggleStyle(enabled = !style.enabled) {
$('#enabled').checked = enabled;
editor.updateEnabledness(enabled);
},
updateDirty() {
@ -214,87 +303,18 @@ window.on('beforeunload', e => {
el.classList.add(cls);
}
},
});
})();
//#endregion
//#region editor livePreview
editor.livePreview = (() => {
let data;
let port;
let preprocess;
let enabled = prefs.get('editor.livePreview');
prefs.subscribe('editor.livePreview', (key, value) => {
if (!value) {
if (port) {
port.disconnect();
port = null;
useSavedStyle(newStyle) {
if (style.id !== newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`);
}
} else if (data && data.id && (data.enabled || editor.dirty.has('enabled'))) {
createPreviewer();
updatePreviewer(data);
}
enabled = value;
});
return {
/**
* @param {Function} [fn] - preprocessor
* @param {boolean} [show]
*/
init(fn, show) {
preprocess = fn;
if (show != null) toggle(show);
sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle);
editor.updateClass();
editor.updateMeta();
},
toggle,
update(newData) {
data = newData;
if (!port) {
if (!data.id || !data.enabled || !enabled) {
return;
}
createPreviewer();
}
updatePreviewer(data);
},
};
function createPreviewer() {
port = chrome.runtime.connect({name: 'livePreview'});
port.onDisconnect.addListener(err => {
throw err;
});
}
function toggle(state) {
$('#preview-label').classList.toggle('hidden', !state);
}
async function updatePreviewer(data) {
const errorContainer = $('#preview-errors');
try {
port.postMessage(preprocess ? await preprocess(data) : data);
errorContainer.classList.add('hidden');
} catch (err) {
if (Array.isArray(err)) {
err = err.join('\n');
} else if (err && err.index != null) {
// 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 || err}`;
}
errorContainer.classList.remove('hidden');
errorContainer.onclick = () => {
messageBoxProxy.alert(err.message || `${err}`, 'pre');
};
}
}
})();
});
}
//#endregion
//#region colorpickerHelper
@ -352,17 +372,15 @@ editor.livePreview = (() => {
cmFactory.globalSetOption('colorpicker', defaults.colorpicker);
}, {runNow: true});
await baseInit.domReady;
$('#colorpicker-settings').onclick = function (event) {
event.preventDefault();
const input = createHotkeyInput('editor.colorpicker.hotkey', () => helpPopup.close());
const input = createHotkeyInput('editor.colorpicker.hotkey', {onDone: () => helpPopup.close()});
const popup = helpPopup.show(t('helpKeyMapHotkey'), input);
const bounds = this.getBoundingClientRect();
popup.style.left = bounds.right + 10 + 'px';
popup.style.top = bounds.top - popup.clientHeight / 2 + 'px';
popup.style.right = 'auto';
input.focus();
$('input', popup).focus();
};
function invokeColorpicker(cm) {

View File

@ -2,9 +2,7 @@
'use strict';
(() => {
const hasCurlyBraceError = warning =>
warning.text === 'Unnecessary curly bracket (CssSyntaxError)';
let sugarssFallback;
let sugarss = false;
/** @namespace EditorWorker */
createWorkerApi({
@ -16,6 +14,38 @@
.map(m => Object.assign(m, {rule: {id: m.rule.id}}));
},
getCssPropsValues() {
require(['/js/csslint/parserlib']); /* global parserlib */
const {
css: {Colors, GlobalKeywords, Properties},
util: {describeProp},
} = parserlib;
const namedColors = Object.keys(Colors);
const rxNonWord = /(?:<.+?>|[^-\w<(]+\d*)+/g;
const res = {};
// moving vendor-prefixed props to the end
const cmp = (a, b) => a[0] === '-' && b[0] !== '-' ? 1 : a < b ? -1 : a > b;
for (const [k, v] of Object.entries(Properties)) {
res[k] = false;
if (typeof v === 'string') {
let last = '';
const uniq = [];
// strip definitions of function arguments
const desc = describeProp(v).replace(/([-\w]+)\(.*?\)/g, 'z-$1');
const descNoColors = desc.replace(/<named-color>/g, '');
// add a prefix to functions to group them at the end
const words = descNoColors.split(rxNonWord).sort(cmp);
for (let w of words) {
if (w.startsWith('z-')) w = w.slice(2) + '(';
if (w !== last) uniq.push(last = w);
}
if (desc !== descNoColors) uniq.push(...namedColors);
if (uniq.length) res[k] = uniq;
}
}
return {all: res, global: GlobalKeywords};
},
getRules(linter) {
return ruleRetriever[linter](); // eslint-disable-line no-use-before-define
},
@ -35,23 +65,32 @@
async stylelint(opts) {
require(['/vendor/stylelint-bundle/stylelint-bundle.min']); /* global stylelint */
try {
let res;
let pass = 0;
/* sugarss is used for stylus-lang by default,
but it fails on normal css syntax so we retry in css mode. */
const isSugarSS = opts.syntax === 'sugarss';
if (sugarssFallback && isSugarSS) opts.syntax = sugarssFallback;
while (
++pass <= 2 &&
(res = (await stylelint.lint(opts)).results[0]) &&
isSugarSS && res.warnings.some(hasCurlyBraceError)
) sugarssFallback = opts.syntax = 'css';
delete res._postcssResult; // huge and unused
return res;
} catch (e) {
delete e.postcssNode; // huge, unused, non-transferable
throw e;
// Stylus-lang allows a trailing ";" but sugarss doesn't, so we monkeypatch it
stylelint.SugarSSParser.prototype.checkSemicolon = tt => {
while (tt.length && tt[tt.length - 1][0] === ';') tt.pop();
};
for (const pass of opts.mode === 'stylus' ? [sugarss, !sugarss] : [-1]) {
/* We try sugarss (for indented stylus-lang), then css mode, switching them on failure,
* so that the succeeding syntax will be used next time first. */
opts.config.customSyntax = !pass ? 'sugarss' : '';
try {
const res = await stylelint.createLinter(opts)._lintSource(opts);
if (pass !== -1) sugarss = pass;
return collectStylelintResults(res, opts);
} catch (e) {
const fatal = pass === -1 ||
!pass && !/^CssSyntaxError:.+?Unnecessary curly bracket/.test(e) ||
pass && !/^CssSyntaxError:.+?Unknown word[\s\S]*?\.decl\s/.test(`${e}${e.stack}`);
if (fatal) {
return [{
from: {line: e.line - 1, ch: e.column - 1},
to: {line: e.line - 1, ch: e.column - 1},
message: e.reason,
severity: 'error',
rule: e.name,
}];
}
}
}
},
});
@ -106,4 +145,32 @@
return options;
},
};
function collectStylelintResults({messages}, {mode}) {
/* We hide nonfatal "//" warnings since we lint with sugarss without applying @preprocessor.
* We can't easily pre-remove "//" comments which may be inside strings, comments, url(), etc.
* And even if we did, it'd be wrong to hide potential bugs in stylus-lang like #1460 */
const isLess = mode === 'text/x-less';
const slashCommentAllowed = isLess || mode === 'stylus';
const res = [];
for (const m of messages) {
if (/deprecation|invalidOption/.test(m.stylelintType)) {
continue;
}
const {rule} = m;
const msg = m.text.replace(/^Unexpected\s+/, '').replace(` (${rule})`, '');
if (slashCommentAllowed && msg.includes('"//"') ||
isLess && /^unknown at-rule "@[-\w]+:"/.test(msg) /* LESS variables */) {
continue;
}
res.push({
from: {line: m.line - 1, ch: m.column - 1},
to: {line: m.endLine - 1, ch: m.endColumn - 1},
message: msg[0].toUpperCase() + msg.slice(1),
severity: m.severity,
rule,
});
}
return res;
}
})();

View File

@ -1,6 +1,5 @@
/* global $ $create $remove getEventKeyName */// dom.js
/* global CodeMirror */
/* global baseInit */// base.js
/* global prefs */
/* global t */// localization.js
'use strict';
@ -20,12 +19,13 @@
title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY,
onclick: embedPopup,
});
document.documentElement.appendChild(btn);
baseInit.domReady.then(() => {
$.root.appendChild(btn);
$.rootCL.add('popup-window');
window.on('domReady', () => {
document.body.appendChild(btn);
// Adding a dummy command to show in keymap help popup
CodeMirror.defaults.extraKeys[POPUP_HOTKEY] = 'openStylusPopup';
});
}, {once: true});
prefs.subscribe('iconset', (_, val) => {
const prefix = `images/icon/${val ? 'light/' : ''}`;
@ -60,14 +60,9 @@
const body = pw.document.body;
pw.on('keydown', removePopupOnEsc);
pw.close = removePopup;
if (pw.IntersectionObserver) {
new pw.IntersectionObserver(onIntersect).observe(body.appendChild(
$create('div', {style: {height: '1px', marginTop: '-1px'}})
));
} else {
frame.dataset.loaded = '';
frame.height = body.scrollHeight;
}
new pw.IntersectionObserver(onIntersect).observe(body.appendChild(
$create('div', {style: {height: '1px', marginTop: '-1px'}})
));
new pw.MutationObserver(onMutation).observe(body, {
attributes: true,
attributeFilter: ['style'],

View File

@ -1,4 +1,4 @@
/* global $ $$ $create $remove focusAccessibility */// dom.js
/* global $ $$ $create $remove focusAccessibility setInputValue toggleDataset */// dom.js
/* global CodeMirror */
/* global chromeLocal */// storage-util.js
/* global colorMimicry */
@ -54,7 +54,7 @@
undoHistory: [],
searchInApplies: !document.documentElement.classList.contains('usercss'),
searchInApplies: !editor.isUsercss,
};
//endregion
@ -70,7 +70,9 @@
if (found) {
const target = $('.' + TARGET_CLASS);
const cm = target.CodeMirror;
(cm || target).focus();
/* Since this runs in `keydown` event we have to delay focusing
* to prevent CodeMirror from seeing and handling the key */
setTimeout(() => (cm || target).focus());
if (cm) {
const {from, to} = cm.state.search.searchPos;
cm.jumpToPos(from, to);
@ -586,7 +588,7 @@
input: colorMimicry($('input:not(:disabled)'), {bg: 'backgroundColor'}),
icon: colorMimicry($$('svg.info')[1], {fill: 'fill'}),
};
document.documentElement.appendChild(
$.root.appendChild(
$(DIALOG_STYLE_SELECTOR) ||
$create('style' + DIALOG_STYLE_SELECTOR)
).textContent = `
@ -605,10 +607,10 @@
}
#search-replace-dialog[data-type="replace"] button:hover svg,
#search-replace-dialog svg:hover {
fill: inherit;
fill: var(--cmin);
}
#search-replace-dialog [data-action="case"]:hover {
color: inherit;
color: var(--cmin);
}
#search-replace-dialog [data-action="clear"] {
background-color: ${colors.input.bg.replace(/[^,]+$/, '') + '.75)'};
@ -678,7 +680,7 @@
el.style.width = newWidth + 'px';
}
const numLines = el.value.split('\n').length;
if (numLines !== parseInt(el.rows)) {
if (numLines !== Number(el.rows)) {
el.rows = numLines;
}
el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
@ -874,15 +876,6 @@
}
function toggleDataset(el, prop, state) {
if (state) {
el.dataset[prop] = '';
} else {
delete el.dataset[prop];
}
}
function saveWindowScrollPos() {
state.scrollX = window.scrollX;
state.scrollY = window.scrollY;
@ -937,18 +930,5 @@
})));
}
function setInputValue(input, value) {
input.focus();
input.select();
// using execCommand to add to the input's undo history
document.execCommand(value ? 'insertText' : 'delete', false, value);
// some versions of Firefox ignore execCommand
if (input.value !== value) {
input.value = value;
input.dispatchEvent(new Event('input', {bubbles: true}));
}
}
//endregion
})();

View File

@ -90,7 +90,7 @@
return rule &&
$create('li', [
$create('b', ruleID + ': '),
rule.url ? $createLink(`"${rule.url}"`, rule.name) : $create('span', `"${rule.name}"`),
rule.url ? $createLink(rule.url, rule.name) : $create('span', `"${rule.name}"`),
$create('p', rule.desc),
]);
};

View File

@ -79,7 +79,7 @@ const linterMan = (() => {
function getCachedAnnotations(code, opt, cm) {
const results = cms.get(cm);
cms.set(cm, null);
cm.options.lint.getAnnotations = getAnnotations;
cm.state.lint.options.getAnnotations = getAnnotations;
return results;
}
@ -131,7 +131,9 @@ linterMan.DEFAULTS = {
'duplicate-properties': 1,
'empty-rules': 1,
'errors': 1,
'globals-in-document': 1,
'known-properties': 1,
'known-pseudos': 1,
'selector-newline': 1,
'shorthand-overrides': 1,
'simple-not': 1,
@ -200,35 +202,7 @@ linterMan.DEFAULTS = {
getConfig: config => ({
rules: Object.assign({}, DEFAULTS.stylelint.rules, config && config.rules),
}),
async lint(code, config, mode) {
const isLess = mode === 'text/x-less';
const isStylus = mode === 'stylus';
const syntax = isLess ? 'less' : isStylus ? 'sugarss' : 'css';
const raw = await worker.stylelint({code, config, syntax});
if (!raw) {
return [];
}
// Hiding the errors about "//" comments as we're preprocessing only when saving/applying
// and we can't just pre-remove the comments since "//" may be inside a string token
const slashCommentAllowed = isLess || isStylus;
const res = [];
for (const w of raw.warnings) {
const msg = w.text.match(/^(?:Unexpected\s+)?(.*?)\s*\([^()]+\)$|$/)[1] || w.text;
if (!slashCommentAllowed || !(
w.rule === 'no-invalid-double-slash-comments' ||
w.rule === 'property-no-unknown' && msg.includes('"//"')
)) {
res.push({
from: {line: w.line - 1, ch: w.column - 1},
to: {line: w.line - 1, ch: w.column},
message: msg.slice(0, 1).toUpperCase() + msg.slice(1),
severity: w.severity,
rule: w.rule,
});
}
}
return res;
},
lint: (code, config, mode) => worker.stylelint({code, config, mode}),
},
};
@ -313,7 +287,7 @@ linterMan.DEFAULTS = {
function updateCount() {
const issueCount = Array.from(tables.values())
.reduce((sum, table) => sum + table.trs.length, 0);
$('#lint').classList.toggle('hidden-unless-compact', issueCount === 0);
$('#lint').hidden = !issueCount;
$('#issue-count').textContent = issueCount;
}
@ -329,19 +303,20 @@ linterMan.DEFAULTS = {
}
function createTable(cm) {
const caption = $create('caption');
const tbody = $create('tbody');
const table = $create('table', [caption, tbody]);
const caption = $create('.caption');
const table = $create('table');
const report = $create('.report', [caption, table]);
const trs = [];
return {
element: table,
element: report,
trs,
updateAnnotations,
updateCaption,
};
function updateCaption() {
caption.textContent = editor.getEditorTitle(cm);
const t = editor.getEditorTitle(cm);
Object.assign(caption, typeof t == 'string' ? {textContent: t} : t);
}
function updateAnnotations(lines) {
@ -353,20 +328,20 @@ linterMan.DEFAULTS = {
} else {
tr = createTr();
trs.push(tr);
tbody.append(tr.element);
table.appendChild(tr.element);
}
tr.update(anno);
i++;
}
if (i === 0) {
trs.length = 0;
tbody.textContent = '';
table.textContent = '';
} else {
while (trs.length > i) {
trs.pop().element.remove();
}
}
table.classList.toggle('empty', trs.length === 0);
report.classList.toggle('empty', !trs.length);
function *getAnnotations() {
for (const line of lines.filter(Boolean)) {

View File

@ -4,9 +4,7 @@
/* global colorMimicry */
/* global editor */
/* global msg */
/* global prefs */
/* global t */// localization.js
/* global tryCatch */// toolbox.js
'use strict';
/* exported MozSectionWidget */
@ -40,11 +38,12 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
};
function init() {
const hint = {title: t('appliesHelp')};
enabled = true;
TPL = {
container:
$create('div' + C_CONTAINER, [
$create(C_LABEL, t('appliesLabel')),
$create(C_LABEL, hint, t('appliesLabel')),
$create('ul' + C_LIST),
]),
listItem:
@ -53,8 +52,9 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
$create('li.applies-to-everything', t('appliesToEverything')),
};
Object.assign($(C_TYPE, TPL.listItem), hint);
$(C_VALUE, TPL.listItem).after(
$create('button.test-regexp', t('styleRegexpTestButton')));
$create('button.test-regexp', t('genericTest')));
CLICK_ROUTE = {
'.test-regexp': showRegExpTester,
@ -129,7 +129,8 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
update(finder.sections, []);
}
finder.on(update);
requestAnimationFrame(updateWidgetStyle);
updateWidgetStyle(); // updating in this paint frame to avoid FOUC for dark themes
cm.display.wrapper.style.setProperty('--cm-bar-width', cm.display.barWidth + 'px');
}
function destroy() {
@ -155,6 +156,7 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
}
if (msg.style || msg.styles ||
msg.prefs && 'disableAll' in msg.prefs ||
msg.method === 'colorScheme' ||
msg.method === 'styleDeleted') {
requestAnimationFrame(updateWidgetStyle);
}
@ -162,11 +164,6 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
function updateWidgetStyle() {
funcHeight = 0;
if (prefs.get('editor.theme') !== 'default' &&
!tryCatch(() => $('#cm-theme').sheet.cssRules)) {
requestAnimationFrame(updateWidgetStyle);
return;
}
const MIN_LUMA = .05;
const MIN_LUMA_DIFF = .4;
const color = {
@ -202,7 +199,6 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
color: ${fore};
}
${C_CONTAINER} input,
${C_CONTAINER} button,
${C_CONTAINER} select {
background: rgba(255, 255, 255, ${
Math.max(MIN_LUMA, Math.pow(Math.max(0, color.gutter.bgLuma - MIN_LUMA * 2), 2)).toFixed(2)
@ -216,7 +212,7 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
transition: none;
}
`;
document.documentElement.appendChild(actualStyle);
$.root.appendChild(actualStyle);
}
/**
@ -273,19 +269,27 @@ function MozSectionWidget(cm, finder = MozSectionFinder(cm)) {
let widget = old && old.widget;
const height = Math.round(funcHeight * (sec.funcs.length || 1)) || undefined;
const node = renderContainer(sec, widget);
if (widget) {
if (widget && widget.line.lineNo() === sec.start.line) {
widget.node = node;
if (height && height !== widget.height) {
widget.height = height;
widget.changed();
}
} else {
if (widget) widget.clear();
widget = cm.addLineWidget(sec.start.line, node, {
coverGutter: true,
noHScroll: true,
above: true,
height,
});
widget.on('redraw', () => {
const value = cm.display.barWidth + 'px';
if (widget[KEY] !== value) {
widget[KEY] = value;
node.style.setProperty('--cm-bar-width', value);
}
});
}
if (!funcHeight) {
funcHeight = node.offsetHeight / (sec.funcs.length || 1);

View File

@ -5,24 +5,23 @@
'use strict';
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();
let currentRegexps = [];
let isWatching = false;
let isShown = false;
window.on('closeHelp', () => regexpTester.toggle(false));
return {
toggle(state = !isShown) {
if (state && !isShown) {
if (!isWatching) {
isWatching = true;
chrome.tabs.onUpdated.addListener(onTabUpdate);
chrome.tabs.onRemoved.addListener(onTabRemoved);
chrome.tabs.onUpdated.addListener(onTabUpdated);
}
helpPopup.show('', $create('.regexp-report'));
window.on('closeHelp', () => regexpTester.toggle(false), {once: true});
isShown = true;
} else if (!state && isShown) {
unwatch();
@ -93,15 +92,15 @@ const regexpTester = (() => {
for (const [url, match] of urls.entries()) {
const faviconUrl = url.startsWith(URLS.ownOrigin)
? OWN_ICON
: GET_FAVICON_URL + new URL(url).hostname;
: URLS.favicon(new URL(url).hostname);
const icon = $create('img', {src: faviconUrl});
if (match.text.length === url.length) {
full.push($create('a', {href: '#'}, [
full.push($create('a', {tabIndex: 0}, [
icon,
url,
]));
} else {
partial.push($create('a', {href: '#'}, [
partial.push($create('a', {tabIndex: 0}, [
icon,
url.substr(0, match.pos),
$create('mark', match.text),
@ -136,8 +135,7 @@ const regexpTester = (() => {
block.appendChild(
$create('details', {open: true}, [
$create('summary', text),
// 3rd level: tab urls
...urls,
$create('div', urls),
]));
} else {
// type is none or invalid
@ -154,28 +152,26 @@ const regexpTester = (() => {
.split(/(\\+)/)
.map(s => (s.startsWith('\\') ? $create('code', s) : s)));
report.appendChild(note);
adjustNote(report, note);
report.style.paddingBottom = note.offsetHeight + 'px';
},
};
function adjustNote(report, note) {
report.style.paddingBottom = note.offsetHeight + 'px';
}
function onClick(event) {
const a = event.target.closest('a');
const a = event.target.closest('a, button');
if (a) {
event.preventDefault();
openURL({
url: a.href && a.getAttribute('href') !== '#' && a.href || a.textContent,
url: a.href || a.textContent,
currentWindow: null,
});
} else if (event.target.closest('details')) {
setTimeout(adjustNote);
}
}
function onTabUpdate(tabId, info) {
function onTabRemoved() {
regexpTester.update();
}
function onTabUpdated(tabId, info) {
if (info.url) {
regexpTester.update();
}
@ -183,7 +179,8 @@ const regexpTester = (() => {
function unwatch() {
if (isWatching) {
chrome.tabs.onUpdated.removeListener(onTabUpdate);
chrome.tabs.onRemoved.removeListener(onTabRemoved);
chrome.tabs.onUpdated.removeListener(onTabUpdated);
isWatching = false;
}
}

View File

@ -1,4 +1,4 @@
/* global $ */// dom.js
/* global $ toggleDataset */// dom.js
/* global MozDocMapper trimCommentLabel */// util.js
/* global cmFactory */
/* global debounce tryRegExp */// toolbox.js
@ -23,7 +23,7 @@ function createSection(originalSection, genId, si) {
const elLabel = $('.code-label', el);
const cm = cmFactory.create(wrapper => {
// making it tall during initial load so IntersectionObserver sees only one adjacent CM
if (editor.ready !== true) {
if (editor.loading) {
wrapper.style.height = si ? si.height : '100vh';
}
elLabel.after(wrapper);
@ -31,11 +31,13 @@ function createSection(originalSection, genId, si) {
value: originalSection.code,
});
el.CodeMirror = cm; // used by getAssociatedEditor
cm.el = el;
editor.applyScrollInfo(cm, si);
const changeListeners = new Set();
const appliesToContainer = $('.applies-to-list', el);
const appliesToContainer = $('.applies-to', el);
const appliesToList = $('.applies-to-list', el);
const appliesTo = [];
MozDocMapper.forEachProp(originalSection, (type, value) =>
insertApplyAfter({type, value}));
@ -113,66 +115,21 @@ function createSection(originalSection, genId, si) {
changeGeneration = newGeneration;
emitSectionChange('code');
});
cm.display.wrapper.on('keydown', event => handleKeydown(cm, event), true);
$('.test-regexp', el).onclick = () => updateRegexpTester(true);
initBeautifyButton($('.beautify-section', el), [cm]);
}
function handleKeydown(cm, event) {
if (event.shiftKey || event.altKey || event.metaKey) {
return;
}
const {key} = event;
const {line, ch} = cm.getCursor();
switch (key) {
case 'ArrowLeft':
if (line || ch) {
return;
}
// fallthrough
case 'ArrowUp':
cm = line === 0 && editor.prevEditor(cm, false);
if (!cm) {
return;
}
event.preventDefault();
event.stopPropagation();
cm.setCursor(cm.doc.size - 1, key === 'ArrowLeft' ? 1e20 : ch);
break;
case 'ArrowRight':
if (line < cm.doc.size - 1 || ch < cm.getLine(line).length - 1) {
return;
}
// fallthrough
case 'ArrowDown':
cm = line === cm.doc.size - 1 && editor.nextEditor(cm, false);
if (!cm) {
return;
}
event.preventDefault();
event.stopPropagation();
cm.setCursor(0, 0);
break;
}
}
async function updateRegexpTester(toggle) {
const isLoaded = typeof regexpTester === 'object';
if (toggle && !isLoaded) {
await require(['/edit/regexp-tester']); /* global regexpTester */
}
if (toggle != null && isLoaded) {
const isLoaded = typeof regexpTester === 'object' ||
toggle && await require(['/edit/regexp-tester']); /* global regexpTester */
if (toggle != null) {
regexpTester.toggle(toggle);
}
const regexps = appliesTo.filter(a => a.type === 'regexp')
.map(a => a.value);
if (regexps.length) {
el.classList.add('has-regexp');
if (isLoaded) regexpTester.update(regexps);
} else {
el.classList.remove('has-regexp');
if (isLoaded) regexpTester.toggle(false);
}
const hasRe = regexps.length > 0;
if (hasRe && isLoaded) regexpTester.update(regexps);
el.classList.toggle('has-regexp', hasRe);
}
function updateTocEntry(origin) {
@ -240,11 +197,13 @@ function createSection(originalSection, genId, si) {
function insertApplyAfter(init, base) {
const apply = createApply(init);
appliesTo.splice(base ? appliesTo.indexOf(base) + 1 : appliesTo.length, 0, apply);
appliesToContainer.insertBefore(apply.el, base ? base.el.nextSibling : null);
appliesToList.insertBefore(apply.el, base ? base.el.nextSibling : null);
toggleDataset(appliesToContainer, 'all', init.all);
dirty.add(apply, apply);
if (appliesTo.length > 1 && appliesTo[0].all) {
removeApply(appliesTo[0]);
}
if (base) requestAnimationFrame(shrinkSectionBy1);
emitSectionChange('apply');
return apply;
}
@ -353,6 +312,15 @@ function createSection(originalSection, genId, si) {
dirty.add(`${dirtyPrefix}.value`, value);
}
}
function shrinkSectionBy1() {
const cmEl = cm.display.wrapper;
const cmH = cmEl.offsetHeight;
const viewH = el.parentElement.offsetHeight;
if (el.offsetHeight > viewH && cmH > Math.min(viewH / 2, cm.display.sizer.offsetHeight + 30)) {
cmEl.style.height = (cmH - appliesToContainer.offsetHeight / (appliesTo.length || 1) | 0) + 'px';
}
}
}
function createResizeGrip(cm) {
@ -381,8 +349,7 @@ function createResizeGrip(cm) {
cm.display.lineDiv.offsetParent.offsetTop +
/* borders */
wrapper.offsetHeight - wrapper.clientHeight;
wrapper.style.pointerEvents = 'none';
document.body.style.cursor = 's-resize';
document.body.classList.add('resizing-v');
document.on('mousemove', resize);
document.on('mouseup', resizeStop);
@ -398,8 +365,7 @@ function createResizeGrip(cm) {
function resizeStop() {
document.off('mouseup', resizeStop);
document.off('mousemove', resize);
wrapper.style.pointerEvents = '';
document.body.style.cursor = '';
document.body.classList.remove('resizing-v');
}
};

View File

@ -1,12 +1,13 @@
/* global $ $$ $create $remove messageBoxProxy */// dom.js
/* global $ $create $remove messageBoxProxy */// dom.js
/* global API */// msg.js
/* global CodeMirror */
/* global FIREFOX RX_META debounce ignoreChromeError sessionStore */// toolbox.js
/* global RX_META debounce */// toolbox.js
/* global MozDocMapper clipString helpPopup rerouteHotkeys showCodeMirrorPopup */// util.js
/* global createSection */// sections-editor-section.js
/* global editor */
/* global linterMan */
/* global prefs */
/* global styleSectionsEqual */ // sections-util.js
/* global t */// localization.js
'use strict';
@ -16,25 +17,25 @@ function SectionsEditor() {
const container = $('#sections');
/** @type {EditorSection[]} */
const sections = [];
const xo = window.IntersectionObserver &&
new IntersectionObserver(refreshOnViewListener, {rootMargin: '100%'});
const xo = new IntersectionObserver(refreshOnViewListener, {rootMargin: '100%'});
let INC_ID = 0; // an increment id that is used by various object to track the order
let sectionOrder = '';
let headerOffset; // in compact mode the header is at the top so it reduces the available height
let cmExtrasHeight; // resize grip + borders
let upDownJumps;
updateHeader();
updateMeta();
rerouteHotkeys.toggle(true); // enabled initially because we don't always focus a CodeMirror
editor.livePreview.init(null, style.id);
container.classList.add('section-editor');
editor.livePreview.init();
$('#to-mozilla').on('click', showMozillaFormat);
$('#to-mozilla-help').on('click', showToMozillaHelp);
$('#from-mozilla').on('click', () => showMozillaFormatImport());
document.on('wheel', scrollEntirePageOnCtrlShift, {passive: false});
CodeMirror.defaults.extraKeys['Shift-Ctrl-Wheel'] = 'scrollWindow';
if (!FIREFOX) {
$$('input:not([type]), input[type=text], input[type=search], input[type=number]')
.forEach(e => e.on('mousedown', toggleContextMenuDelete));
}
prefs.subscribe('editor.arrowKeysTraverse', (_, val) => {
for (const {cm} of sections) handleKeydownSetup(cm, val);
upDownJumps = val;
}, {runNow: true});
/** @namespace Editor */
Object.assign(editor, {
@ -43,14 +44,23 @@ function SectionsEditor() {
closestVisible,
updateLivePreview,
updateMeta,
getEditors() {
return sections.filter(s => !s.removed).map(s => s.cm);
},
getEditorTitle(cm) {
const index = editor.getEditors().indexOf(cm);
return `${t('sectionCode')} ${index + 1}`;
const index = editor.getEditors().indexOf(cm) + 1;
return {
textContent: `#${index}`,
title: `${t('sectionCode')} ${index}`,
};
},
getValue(asObject) {
const st = getModel();
return asObject ? st : MozDocMapper.styleToCss(st);
},
getSearchableInputs(cm) {
@ -66,62 +76,62 @@ function SectionsEditor() {
}
},
nextEditor(cm, cycle = true) {
return cycle || cm !== findLast(sections, s => !s.removed).cm
? nextPrevEditor(cm, 1)
nextEditor(cm, upDown) {
return !upDown || cm !== findLast(sections, s => !s.removed).cm
? nextPrevEditor(cm, 1, upDown)
: null;
},
prevEditor(cm, cycle = true) {
return cycle || cm !== sections.find(s => !s.removed).cm
? nextPrevEditor(cm, -1)
prevEditor(cm, upDown) {
return !upDown || cm !== sections.find(s => !s.removed).cm
? nextPrevEditor(cm, -1, upDown)
: null;
},
async replaceStyle(newStyle, codeIsUpdated) {
dirty.clear('name');
async replaceStyle(newStyle, draft) {
const sameCode = styleSectionsEqual(newStyle, getModel());
if (!sameCode && !draft && !await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
return;
}
if (!draft) {
dirty.clear();
}
// FIXME: avoid recreating all editors?
if (codeIsUpdated !== false) {
await initSections(newStyle.sections, {replace: true});
if (!sameCode) {
await initSections(newStyle.sections, {
keepDirty: draft,
replace: true,
si: draft && draft.si,
});
}
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');
}
editor.livePreview.toggle(Boolean(style.id));
editor.useSavedStyle(newStyle);
updateLivePreview();
},
async save() {
if (!dirty.isDirty()) {
return;
}
async saveImpl() {
let newStyle = getModel();
if (!validate(newStyle)) {
return;
}
newStyle = await API.styles.editSave(newStyle);
destroyRemovedSections();
sessionStore.justEditedStyleId = newStyle.id;
editor.replaceStyle(newStyle, false);
dirty.clear();
editor.useSavedStyle(newStyle);
},
scrollToEditor(cm) {
const {el} = sections.find(s => s.cm === cm);
const r = el.getBoundingClientRect();
const h = window.innerHeight;
if (r.bottom > h && r.top > 0 ||
r.bottom < h && r.top < 0) {
window.scrollBy(0, (r.top + r.bottom - h) / 2 | 0);
}
scrollToEditor(cm, partial) {
const cc = partial && cm.cursorCoords(true, 'window');
const {top: y1, bottom: y2} = cm.el.getBoundingClientRect();
const rc = container.getBoundingClientRect();
const rcY1 = Math.max(rc.top, 0);
const rcY2 = Math.min(rc.bottom, innerHeight);
const bad = partial
? cc.top < rcY1 || cc.top > rcY2 - 30
: y1 >= rcY1 ^ y2 <= rcY2;
if (bad) window.scrollBy(0, (y1 + y2 - rcY2 + rcY1) / 2 | 0);
},
});
editor.ready = initSections(style.sections);
return initSections(style.sections);
/** @param {EditorSection} section */
function fitToContent(section) {
@ -138,13 +148,17 @@ function SectionsEditor() {
return;
}
if (headerOffset == null) {
headerOffset = container.getBoundingClientRect().top + scrollY | 0;
headerOffset = Math.ceil(container.getBoundingClientRect().top + scrollY);
}
contentHeight += 9; // border & resize grip
if (cmExtrasHeight == null) {
cmExtrasHeight = $('.resize-grip', wrapper).offsetHeight + // grip
wrapper.offsetHeight - wrapper.clientHeight; // borders
}
contentHeight += cmExtrasHeight;
cm.off('update', resize);
const cmHeight = wrapper.offsetHeight;
const appliesToHeight = Math.min(section.el.offsetHeight - cmHeight, window.innerHeight / 2);
const maxHeight = (window.innerHeight - headerOffset) - appliesToHeight;
const maxHeight = Math.floor(window.innerHeight - headerOffset - appliesToHeight);
const fit = Math.min(contentHeight, maxHeight);
if (Math.abs(fit - cmHeight) > 1) {
cm.setSize(null, fit);
@ -285,10 +299,36 @@ function SectionsEditor() {
}
}
function nextPrevEditor(cm, direction) {
function handleKeydown(event) {
if (event.shiftKey || event.altKey || event.metaKey ||
event.key !== 'ArrowUp' && event.key !== 'ArrowDown') {
return;
}
let pos;
let cm = this.CodeMirror;
const {line, ch} = cm.getCursor();
if (event.key === 'ArrowUp') {
cm = line === 0 && editor.prevEditor(cm, true);
pos = cm && [cm.doc.size - 1, ch];
} else {
cm = line === cm.doc.size - 1 && editor.nextEditor(cm, true);
pos = cm && [0, 0];
}
if (cm) {
cm.setCursor(...pos);
event.preventDefault();
event.stopPropagation();
}
}
function handleKeydownSetup(cm, state) {
cm.display.wrapper[state ? 'on' : 'off']('keydown', handleKeydown, true);
}
function nextPrevEditor(cm, direction, upDown) {
const editors = editor.getEditors();
cm = editors[(editors.indexOf(cm) + direction + editors.length) % editors.length];
editor.scrollToEditor(cm);
editor.scrollToEditor(cm, upDown);
cm.focus();
return cm;
}
@ -318,7 +358,7 @@ function SectionsEditor() {
function showMozillaFormat() {
const popup = showCodeMirrorPopup(t('styleToMozillaFormatTitle'), '', {readOnly: true});
popup.codebox.setValue(MozDocMapper.styleToCss(getModel()));
popup.codebox.setValue(editor.getValue());
popup.codebox.execCommand('selectAll');
}
@ -390,7 +430,7 @@ function SectionsEditor() {
}
function lockPageUI(locked) {
document.documentElement.style.pointerEvents = locked ? 'none' : '';
$.root.style.pointerEvents = locked ? 'none' : '';
if (popup.codebox) {
popup.classList.toggle('ready', locked ? false : !popup.codebox.isBlank());
popup.codebox.options.readOnly = locked;
@ -420,7 +460,7 @@ function SectionsEditor() {
editor.updateToc();
}
/** @returns {Style} */
/** @returns {StyleObj} */
function getModel() {
return Object.assign({}, style, {
sections: sections.filter(s => !s.removed).map(s => s.getModel()),
@ -446,19 +486,7 @@ function SectionsEditor() {
return true;
}
function destroyRemovedSections() {
for (let i = 0; i < sections.length;) {
if (!sections[i].removed) {
i++;
continue;
}
sections[i].destroy();
sections[i].el.remove();
sections.splice(i, 1);
}
}
function updateHeader() {
function updateMeta() {
$('#name').value = style.customName || style.name || '';
$('#enabled').checked = style.enabled !== false;
$('#url').href = style.url || '';
@ -476,14 +504,15 @@ function SectionsEditor() {
async function initSections(src, {
focusOn = 0,
replace = false,
keepDirty = false, // used by import
keepDirty = false,
si = editor.scrollInfo,
} = {}) {
Object.assign(editor, /** @namespace Editor */ {loading: true});
if (replace) {
sections.forEach(s => s.remove(true));
sections.length = 0;
container.textContent = '';
}
let si = editor.scrollInfo;
if (si && si.cms && si.cms.length === src.length) {
si.scrollY2 = si.scrollY + window.innerHeight;
container.style.height = si.scrollY2 + 'px';
@ -511,9 +540,12 @@ function SectionsEditor() {
if (!keepDirty) dirty.clear();
if (i === focusOn) sections[i].cm.focus();
}
if (!si) requestAnimationFrame(fitToAvailableSpace);
if (!si || si.cms.every(cm => !cm.height)) {
requestAnimationFrame(fitToAvailableSpace);
}
container.style.removeProperty('height');
setGlobalProgress();
editor.loading = false;
}
/** @param {EditorSection} section */
@ -579,6 +611,9 @@ function SectionsEditor() {
cm.focus();
editor.scrollToEditor(cm);
}
if (upDownJumps) {
handleKeydownSetup(cm, true);
}
updateSectionOrder();
updateLivePreview();
section.onChange(updateLivePreview);
@ -611,7 +646,6 @@ function SectionsEditor() {
/** @param {EditorSection} section */
function registerEvents(section) {
const {el, cm} = section;
$('.applies-to-help', el).onclick = () => helpPopup.show(t('appliesLabel'), t('appliesHelp'));
$('.remove-section', el).onclick = () => removeSection(section);
$('.add-section', el).onclick = () => insertSectionAfter(undefined, section);
$('.clone-section', el).onclick = () => insertSectionAfter(section.getModel(), section);
@ -619,16 +653,13 @@ function SectionsEditor() {
$('.move-section-down', el).onclick = () => moveSectionDown(section);
$('.restore-section', el).onclick = () => restoreSection(section);
cm.on('paste', maybeImportOnPaste);
if (!FIREFOX) {
cm.on('mousedown', (cm, event) => toggleContextMenuDelete.call(cm, event));
}
}
function maybeImportOnPaste(cm, event) {
const text = event.clipboardData.getData('text') || '';
if (/@-moz-document/i.test(text) &&
/@-moz-document\s+(url|url-prefix|domain|regexp)\(/i
.test(text.replace(/\/\*([^*]|\*(?!\/))*(\*\/|$)/g, ''))
.test(text.replace(/\/\*([^*]+|\*(?!\/))*(\*\/|$)/g, ''))
) {
event.preventDefault();
showMozillaFormatImport(text);
@ -639,7 +670,7 @@ function SectionsEditor() {
if (code) {
linterMan.enableForEditor(cm, code);
}
if (force || !xo) {
if (force) {
refreshOnViewNow(cm);
} else {
xo.observe(cm.display.wrapper);
@ -666,15 +697,4 @@ function SectionsEditor() {
linterMan.enableForEditor(cm);
cm.refresh();
}
function toggleContextMenuDelete(event) {
if (chrome.contextMenus && event.button === 2 && prefs.get('editor.contextDelete')) {
chrome.contextMenus.update('editor.contextDelete', {
enabled: Boolean(
this.selectionStart !== this.selectionEnd ||
this.somethingSelected && this.somethingSelected()
),
}, ignoreChromeError);
}
}
}

47
edit/settings.css Normal file
View File

@ -0,0 +1,47 @@
#help-popup.style-settings-popup.dirty .title::after {
content: ' *';
}
.compact-layout #help-popup.style-settings-popup {
width: 90%;
}
.style-settings {
padding: 0 1px; /* for focus outline */
border: 0;
margin: 0;
}
.style-settings > * {
display: block;
margin: 1rem 0;
padding: 0;
}
.style-settings > :first-child {
margin-top: 0;
}
.style-settings > :last-child {
margin-bottom: 0;
}
.style-settings input:disabled ~ label {
opacity: .5;
}
.style-settings .w100 {
display: block;
width: 100%;
margin-top: .25em;
box-sizing: border-box;
}
.style-settings textarea {
resize: vertical;
min-width: 33vw;
min-height: 2.5em;
max-height: 50vh;
}
.style-settings textarea:not(:placeholder-shown) {
min-width: 50vw;
}
.style-settings .radio-wrapper {
display: inline-flex;
padding: 0 .8em 0 0;
}
a[data-cmd=note] {
vertical-align: text-bottom;
}

40
edit/settings.html Normal file
View File

@ -0,0 +1,40 @@
<div>
<fieldset class="style-settings">
<div class="rel">
<input id="ss-updatable" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<label i18n="installUpdateFromLabel" for="ss-updatable"></label>
<input id="ss-update-url" type="url" class="w100" i18n="placeholder:styleUpdateUrlLabel">
</div>
<div id="ss-scheme">
<div i18n="preferScheme">
<div><small id="ss-scheme-off" i18n="preferSchemeAlways" hidden></small></div>
</div>
<label i18n="+preferSchemeNone" class="radio-wrapper">
<input name="ss-scheme" type="radio" value="none">
</label>
<label i18n="+preferSchemeDark" class="radio-wrapper">
<input name="ss-scheme" type="radio" value="dark">
</label>
<label i18n="+preferSchemeLight" class="radio-wrapper">
<input name="ss-scheme" type="radio" value="light">
</label>
</div>
<label i18n="styleIncludeLabel">
<textarea id="ss-inclusions" spellcheck="false" class="w100"
placeholder="*://site1.com/*&#10;*://site2.com/*"></textarea>
</label>
<label i18n="styleExcludeLabel">
<textarea id="ss-exclusions" spellcheck="false" class="w100"
placeholder="*://site1.com/*&#10;*://site2.com/*"></textarea>
</label>
</fieldset>
<div class="buttons">
<button id="ss-save" i18n="confirmSave" disabled></button>
<label i18n="+configOnChange, title:configOnChangeTooltip">
<input id="config.autosave" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
<button id="ss-close" i18n="confirmClose"></button>
</div>
</div>

124
edit/settings.js Normal file
View File

@ -0,0 +1,124 @@
/* global $ moveFocus setupLivePrefs */// dom.js
/* global API */// msg.js
/* global editor */
/* global helpPopup */// util.js
/* global prefs */
/* global t */// localization.js
/* global debounce tryURL */// toolbox.js
'use strict';
/* exported StyleSettings */
async function StyleSettings() {
const AUTOSAVE_DELAY = 500; // same as config-dialog.js
const SS_ID = 'styleSettings';
const PASS = val => val;
await t.fetchTemplate('/edit/settings.html', SS_ID);
const {style} = editor;
const ui = t.template[SS_ID].cloneNode(true);
const elAuto = $('#config\\.autosave', ui);
const elSave = $('#ss-save', ui);
const elUpd = $('#ss-updatable', ui);
const pendingSetters = new Map();
const updaters = [
initCheckbox(elUpd, 'updatable', tryURL(style.updateUrl).href),
initInput('#ss-update-url', 'updateUrl', '', {
validate(el) {
elUpd.disabled = !el.value || !el.validity.valid;
return el.validity.valid;
},
}),
initRadio('ss-scheme', 'preferScheme', 'none'),
initArea('inclusions'),
initArea('exclusions'),
];
update();
prefs.subscribe('schemeSwitcher.enabled', (_, val) => {
$('#ss-scheme-off', ui).hidden = val !== 'never';
}, {runNow: true});
window.on(SS_ID, update);
window.on('closeHelp', () => window.off(SS_ID, update), {once: true});
helpPopup.show(t(SS_ID), ui, {
className: 'style-settings-popup',
});
elSave.onclick = save;
$('#ss-close', ui).onclick = helpPopup.close;
setupLivePrefs([elAuto.id]);
moveFocus(ui, 0);
function autosave(el, setter) {
pendingSetters.set(el, setter);
helpPopup.div.classList.add('dirty');
elSave.disabled = false;
if (elAuto.checked) debounce(save, AUTOSAVE_DELAY);
}
function initArea(type) {
return initInput(`#ss-${type}`, type, [], {
get: textToList,
set: list => list.join('\n'),
validate(el) {
const val = el.value;
el.rows = val.match(/^/gm).length + !val.endsWith('\n');
},
});
}
function initCheckbox(el, key, defVal) {
return initInput(el, key, Boolean(defVal), {dom: 'checked'});
}
function initInput(el, key, defVal, {
dom = 'value', // DOM property name
get = PASS, // transformer function(val) after getting DOM value
set = PASS, // transformer function(val) before setting DOM value
validate = PASS, // function(el) - return `false` to prevent saving
} = {}) {
if (typeof el === 'string') {
el = $(el, ui);
}
el.oninput = () => {
if (validate(el) !== false) {
autosave(el, {dom, get, key});
}
};
return () => {
let val = style[key];
val = set(val != null ? val : defVal);
// Skipping if unchanged to preserve the Undo history of the input
if (el[dom] !== val) el[dom] = val;
validate(el);
};
}
function initRadio(name, key, defVal) {
$(`#${name}`, ui).oninput = e => {
if (e.target.checked) {
autosave(e.target, {key});
}
};
return () => {
const val = style[key] || defVal;
const el = $(`[name="${name}"][value="${val}"]`, ui);
el.checked = true;
};
}
function save() {
pendingSetters.forEach(saveValue);
pendingSetters.clear();
helpPopup.div.classList.remove('dirty');
elSave.disabled = true;
}
function saveValue({dom = 'value', get = PASS, key}, el) {
return API.styles.config(style.id, key, get(el[dom]));
}
function textToList(text) {
return text.split(/\n/).map(s => s.trim()).filter(Boolean);
}
function update() {
updaters.forEach(fn => fn());
}
}

View File

@ -4,7 +4,7 @@
/* global MozDocMapper */// util.js
/* global MozSectionFinder */
/* global MozSectionWidget */
/* global RX_META debounce sessionStore */// toolbox.js
/* global RX_META debounce */// toolbox.js
/* global chromeSync */// storage-util.js
/* global cmFactory */
/* global editor */
@ -14,23 +14,31 @@
'use strict';
/* exported SourceEditor */
function SourceEditor() {
async function SourceEditor() {
const {style, /** @type DirtyReporter */dirty} = editor;
const DEFAULT_TEMPLATE = `
/* ==UserStyle==
@name ${''/* a trick to preserve the trailing spaces */}
@namespace github.com/openstyles/stylus
@version 1.0.0
@description A new userstyle
@author Me
==/UserStyle== */
`.replace(/^\s+/gm, '');
let savedGeneration;
let placeholderName = '';
let prevMode = NaN;
$$remove('.sectioned-only');
$('#header').on('wheel', headerOnScroll);
$('#sections').textContent = '';
$('#sections').appendChild($create('.single-editor'));
if (!style.id) setupNewStyle(style);
$('#save-button').on('split-btn', saveTemplate);
const cm = cmFactory.create($('.single-editor'));
const sectionFinder = MozSectionFinder(cm);
const sectionWidget = MozSectionWidget(cm, sectionFinder);
editor.livePreview.init(preprocess, style.id);
editor.livePreview.init(preprocess);
if (!style.id) setupNewStyle(await editor.template);
createMetaCompiler(meta => {
style.usercssData = meta;
style.name = meta.name;
@ -45,9 +53,17 @@ function SourceEditor() {
sections: sectionFinder.sections,
replaceStyle,
updateLivePreview,
updateMeta,
closestVisible: () => cm,
getEditors: () => [cm],
getEditorTitle: () => '',
getValue: asObject => asObject
? {
customName: style.customName,
enabled: style.enabled,
sourceCode: cm.getValue(),
}
: cm.getValue(),
getSearchableInputs: () => [],
prevEditor: nextPrevSection.bind(null, -1),
nextEditor: nextPrevSection.bind(null, 1),
@ -56,28 +72,24 @@ function SourceEditor() {
if (sec) {
sectionFinder.updatePositions(sec);
cm.jumpToPos(sec.start);
cm.focus();
}
},
async save() {
if (!dirty.isDirty()) return;
async saveImpl() {
const sourceCode = cm.getValue();
try {
const {customName, enabled, id} = style;
if (!id &&
(await API.usercss.build({sourceCode, checkDup: true, metaOnly: true})).dup) {
let res = !id && await API.usercss.build({sourceCode, checkDup: true, metaOnly: true});
if (res && res.dup) {
messageBoxProxy.alert(t('usercssAvoidOverwriting'), 'danger', t('genericError'));
} else {
await replaceStyle(
await API.usercss.editSave({customName, enabled, id, sourceCode}));
res = await API.usercss.editSave({customName, enabled, id, sourceCode});
// Awaiting inside `try` so that exceptions go to our `catch`
await replaceStyle(res.style);
}
showLog(res);
} catch (err) {
const i = err.index;
const isNameEmpty = i > 0 &&
err.code === 'missingValue' &&
sourceCode.slice(sourceCode.lastIndexOf('\n', i - 1), i).trim().endsWith('@name');
return isNameEmpty
? saveTemplate(sourceCode)
: showSaveError(err);
showSaveError(err);
}
},
scrollToEditor: () => {},
@ -89,7 +101,6 @@ function SourceEditor() {
'editor.toc.expanded': (k, val) => sectionFinder.onOff(editor.updateToc, val),
}, {runNow: true});
editor.applyScrollInfo(cm);
cm.clearHistory();
cm.markClean();
savedGeneration = cm.changeGeneration();
@ -109,15 +120,23 @@ function SourceEditor() {
if (!$isTextInput(document.activeElement)) {
cm.focus();
}
editor.applyScrollInfo(cm); // WARNING! Place it after all cm.XXX calls that change scroll pos
async function preprocess(style) {
const {style: newStyle} = await API.usercss.build({
const res = await API.usercss.build({
styleId: style.id,
sourceCode: style.sourceCode,
assignVars: true,
});
delete newStyle.enabled;
return Object.assign(style, newStyle);
showLog(res);
delete res.style.enabled;
return Object.assign(style, res.style);
}
/** Shows the console.log output from the background worker stored in `log` property */
function showLog(data) {
if (data.log) data.log.forEach(args => console.log(...args));
return data;
}
function updateLivePreview() {
@ -149,34 +168,21 @@ function SourceEditor() {
return name;
}
async function setupNewStyle(style) {
style.sections[0].code = ' '.repeat(prefs.get('editor.tabSize')) +
`/* ${t('usercssReplaceTemplateSectionBody')} */`;
let section = MozDocMapper.styleToCss(style);
if (!section.includes('@-moz-document')) {
style.sections[0].domains = ['example.com'];
section = MozDocMapper.styleToCss(style);
function setupNewStyle(tpl) {
const comment = `/* ${t('usercssReplaceTemplateSectionBody')} */`;
const sec0 = style.sections[0];
sec0.code = ' '.repeat(prefs.get('editor.tabSize')) + comment;
if (Object.keys(sec0).length === 1) { // the only key is 'code'
sec0.domains = ['example.com'];
}
const DEFAULT_CODE = `
/* ==UserStyle==
@name ${''/* a trick to preserve the trailing spaces */}
@namespace github.com/openstyles/stylus
@version 1.0.0
@description A new userstyle
@author Me
==/UserStyle== */
`.replace(/^\s+/gm, '');
dirty.clear('sourceGeneration');
style.sourceCode = '';
placeholderName = `${style.name || t('usercssReplaceTemplateName')} - ${new Date().toLocaleString()}`;
let code = await chromeSync.getLZValue(chromeSync.LZ_KEY.usercssTemplate);
code = code || DEFAULT_CODE;
code = code.replace(/@name(\s*)(?=[\r\n])/, (str, space) =>
`${str}${space ? '' : ' '}${placeholderName}`);
// strip the last dummy section if any, add an empty line followed by the section
style.sourceCode = code.replace(/\s*@-moz-document[^{]*{[^}]*}\s*$|\s+$/g, '') + '\n\n' + section;
style.name = [style.name, new Date().toLocaleString()].filter(Boolean).join(' - ');
style.sourceCode = (tpl || DEFAULT_TEMPLATE)
.replace(/(@name)(?:([\t\x20]+).*|\n)/, (_, k, space) => `${k}${space || ' '}${style.name}`)
.replace(/\s*@-moz-document[^{]*{([^}]*)}\s*$/g, // stripping dummy sections
(s, body) => body.trim() === comment ? '\n\n' : s)
.trim() +
'\n\n' +
MozDocMapper.styleToCss(style);
cm.startOperation();
cm.setValue(style.sourceCode);
cm.clearHistory();
@ -188,60 +194,60 @@ function SourceEditor() {
function updateMeta() {
const name = style.customName || style.name;
if (name !== placeholderName) {
$('#name').value = name;
}
$('#name').value = name;
$('#enabled').checked = style.enabled;
$('#url').href = style.url;
editor.updateName();
cm.setPreprocessor((style.usercssData || {}).preprocessor);
}
function replaceStyle(newStyle, codeIsUpdated) {
async function replaceStyle(newStyle, draft) {
dirty.clear('name');
const sameCode = newStyle.sourceCode === cm.getValue();
if (sameCode) {
savedGeneration = cm.changeGeneration();
dirty.clear('sourceGeneration');
}
if (codeIsUpdated === false || sameCode) {
updateEnvironment();
editor.useSavedStyle(newStyle);
dirty.clear('enabled');
updateLivePreview();
return;
}
Promise.resolve(messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))).then(ok => {
if (!ok) return;
updateEnvironment();
if (draft || await messageBoxProxy.confirm(t('styleUpdateDiscardChanges'))) {
editor.useSavedStyle(newStyle);
if (!sameCode) {
const cursor = cm.getCursor();
const si0 = draft && draft.si.cms[0];
const cursor = !si0 && cm.getCursor();
cm.setValue(style.sourceCode);
cm.setCursor(cursor);
if (si0) {
editor.applyScrollInfo(cm, si0);
} else {
cm.setCursor(cursor);
}
savedGeneration = cm.changeGeneration();
}
if (sameCode) {
// the code is same but the environment is changed
updateLivePreview();
}
dirty.clear();
});
function updateEnvironment() {
if (style.id !== newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`);
if (!draft) {
dirty.clear();
}
sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle);
$('#preview-label').classList.remove('hidden');
updateMeta();
editor.livePreview.toggle(Boolean(style.id));
}
}
async function saveTemplate(code) {
if (await messageBoxProxy.confirm(t('usercssReplaceTemplateConfirmation'))) {
async function saveTemplate() {
const res = await messageBoxProxy.show({
contents: t('usercssReplaceTemplateConfirmation'),
className: 'center',
buttons: [t('confirmYes'), t('confirmNo'), {
textContent: t('genericResetLabel'),
title: t('restoreTemplate'),
}],
});
if (res.enter || res.button !== 1) {
const key = chromeSync.LZ_KEY.usercssTemplate;
const code = res.button === 2 ? DEFAULT_TEMPLATE : cm.getValue();
await chromeSync.setLZValue(key, code);
if (await chromeSync.getLZValue(key) !== code) {
messageBoxProxy.alert(t('syncStorageErrorSaving'));
@ -318,13 +324,17 @@ function SourceEditor() {
if (errors.every(err => err.code === 'unknownMeta')) {
onUpdated(metadata);
}
cache = errors.map(err => ({
from: cm.posFromIndex((err.index || 0) + match.index),
to: cm.posFromIndex((err.index || 0) + match.index),
message: err.code && t(`meta_${err.code}`, err.args, false) || err.message,
severity: err.code === 'unknownMeta' ? 'warning' : 'error',
rule: err.code,
}));
cache = errors.map(({code, index, args, message}) => {
const isUnknownMeta = code === 'unknownMeta';
const typo = isUnknownMeta && args[1] ? 'Typo' : ''; // args[1] may be present but undefined
return ({
from: cm.posFromIndex((index || 0) + match.index),
to: cm.posFromIndex((index || 0) + match.index),
message: code && t(`meta_${code}${typo}`, args, false) || message,
severity: isUnknownMeta ? 'warning' : 'error',
rule: code,
});
});
meta = match[0];
metaIndex = match.index;
return cache;

96
edit/usw-integration.js Normal file
View File

@ -0,0 +1,96 @@
/* global $ $create $remove messageBoxProxy showSpinner toggleDataset */// dom.js
/* global API msg */// msg.js
/* global URLS */// toolbox.js
/* global editor */
/* global t */// localization.js
'use strict';
(() => {
//#region Main
const ERROR_TITLE = 'UserStyles.world ' + t('genericError');
const PROGRESS = '#usw-progress';
let spinnerTimer = 0;
let prevCode = '';
msg.onExtension(request => {
if (request.method === 'uswData' &&
request.style.id === editor.style.id) {
Object.assign(editor.style, request.style);
updateUI();
}
});
window.on('domReady', () => {
updateUI();
$('#usw-publish-style').onclick = disableWhileActive(publishStyle);
$('#usw-disconnect').onclick = disableWhileActive(disconnect);
}, {once: true});
async function publishStyle() {
const {id} = editor.style;
if (await API.data.has('usw' + id) &&
!await messageBoxProxy.confirm(t('publishRetry'), 'danger', ERROR_TITLE)) {
return;
}
const code = editor.getValue();
const isDiff = code !== prevCode;
const res = isDiff ? await API.usw.publish(id, code) : t('importReportUnchanged');
const title = `${new Date().toLocaleString()}\n${res}`;
const failed = /^Error:/.test(res);
$(PROGRESS).append(...failed && [
$create('div.error', {title}, res),
$create('div', t('publishReconnect')),
] || [
$create(`span.${isDiff ? 'success' : 'unchanged'}`, {title}),
]);
if (!failed) prevCode = code;
}
async function disconnect() {
await API.usw.revoke(editor.style.id);
prevCode = null; // to allow the next publishStyle to upload style
}
function updateUI(style = editor.style) {
const usw = style._usw || {};
const section = $('#publish');
toggleDataset(section, 'connected', usw.token);
for (const type of ['name', 'description']) {
const el = $(`dd[data-usw="${type}"]`, section);
el.textContent = el.title = usw[type] || '';
}
const elUrl = $('#usw-url');
elUrl.href = `${URLS.usw}${usw.id ? `style/${usw.id}` : ''}`;
elUrl.textContent = t('publishUsw').replace(/<(.+)>/, `$1${usw.id ? `#${usw.id}` : ''}`);
}
//#endregion
//#region Utility
function disableWhileActive(fn) {
/** @this {Element} */
return async function () {
this.disabled = true;
timerOn();
await fn().catch(console.error);
timerOff();
this.disabled = false;
};
}
function timerOn() {
if (!spinnerTimer) {
$(PROGRESS).textContent = '';
spinnerTimer = setTimeout(showSpinner, 250, PROGRESS);
}
}
function timerOff() {
$remove(`${PROGRESS} .lds-spinner`);
clearTimeout(spinnerTimer);
spinnerTimer = 0;
}
//#endregion
})();

View File

@ -7,47 +7,54 @@
const helpPopup = {
show(title = '', body) {
/**
* @param {string} title - plain text
* @param {string|Node} body - Node, html or plain text
* @param {Node} [props] - DOM props for the popup element
* @returns {Element} the popup
*/
show(title = '', body, props) {
const div = $('#help-popup');
const contents = $('.contents', div);
div.style = '';
div.className = '';
contents.textContent = '';
Object.assign(div, props);
if (body) {
contents.appendChild(typeof body === 'string' ? t.HTML(body) : body);
}
$('.title', div).textContent = title;
$('.dismiss', div).onclick = helpPopup.close;
window.on('keydown', helpPopup.close, true);
// reset any inline styles
div.style = 'display: block';
div.style.display = 'block';
helpPopup.originalFocus = document.activeElement;
helpPopup.div = div;
moveFocus(div, 0);
return div;
},
close(event) {
let el;
const canClose =
!event ||
event.type === 'click' || (
getEventKeyName(event) === 'Escape' &&
!$('.CodeMirror-hints, #message-box') && (
!document.activeElement ||
!document.activeElement.closest('#search-replace-dialog') &&
document.activeElement.matches(':not(input), .can-close-on-esc')
)
event.type === 'click' ||
getEventKeyName(event) === 'Escape' && !$('.CodeMirror-hints, #message-box') && (
!(el = document.activeElement) ||
!el.closest('#search-replace-dialog')
);
const div = $('#help-popup');
const {div} = helpPopup;
if (!canClose || !div) {
return;
}
if (event && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
if (event && (el = div.codebox) && !el.options.readOnly && !el.isClean()) {
setTimeout(async () => {
const ok = await messageBoxProxy.confirm(t('confirmDiscardChanges'));
return ok && helpPopup.close();
});
return;
}
if (div.contains(document.activeElement) && helpPopup.originalFocus) {
helpPopup.originalFocus.focus();
if (div.contains(document.activeElement) && (el = helpPopup.originalFocus)) {
el.focus();
}
const contents = $('.contents', div);
div.style.display = '';
@ -103,55 +110,74 @@ function clipString(str, limit = 100) {
}
/* exported createHotkeyInput */
function createHotkeyInput(prefId, onDone = () => {}) {
return $create('input', {
type: 'search',
function createHotkeyInput(prefId, {buttons = true, onDone}) {
const RX_ERR = new RegExp('^(' + [
/Space/,
/(Shift-)?./, // a single character
/(?=.)(Shift-?|Ctrl-?|Control-?|Alt-?|Meta-?)*(Escape|Tab|Page(Up|Down)|Arrow(Up|Down|Left|Right)|Home|End)?/,
].map(r => r.source || r).join('|') + ')$', 'i');
const initialValue = prefs.get(prefId);
const input = $create('input', {
spellcheck: false,
value: prefs.get(prefId),
onkeydown(event) {
const key = CodeMirror.keyName(event);
if (key === 'Tab' || key === 'Shift-Tab') {
return;
}
event.preventDefault();
event.stopPropagation();
switch (key) {
case 'Enter':
if (this.checkValidity()) onDone(true);
return;
case 'Esc':
onDone(false);
return;
default:
// disallow: [Shift?] characters, modifiers-only, [modifiers?] + Esc, Tab, nav keys
if (!key || new RegExp('^(' + [
'(Back)?Space',
'(Shift-)?.', // a single character
'(Shift-?|Ctrl-?|Alt-?|Cmd-?){0,2}(|Esc|Tab|(Page)?(Up|Down)|Left|Right|Home|End|Insert|Delete)',
].join('|') + ')$', 'i').test(key)) {
this.value = key || this.value;
this.setCustomValidity('Not allowed');
return;
}
}
this.value = key;
this.setCustomValidity('');
prefs.set(prefId, key);
},
oninput() {
// fired on pressing "x" to clear the field
prefs.set(prefId, '');
},
onpaste(event) {
event.preventDefault();
},
onpaste: e => onkeydown(e, e.clipboardData.getData('text')),
onkeydown,
});
buttons = buttons && [
['confirmOK', 'Enter'],
['undo', initialValue],
['genericResetLabel', ''],
].map(([label, val]) =>
$create('button', {onclick: e => onkeydown(e, val)}, t(label)));
const [btnOk, btnUndo, btnReset] = buttons || [];
onkeydown(null, initialValue);
return buttons
? $create('fragment', [input, $create('.buttons', buttons)])
: input;
function onkeydown(e, key) {
let newValue;
if (e && e.type === 'keydown') {
key = getEventKeyName(e);
}
switch (e && key) {
case 'Tab':
case 'Shift-Tab':
return;
case 'BackSpace':
case 'Delete':
newValue = '';
break;
case 'Enter':
if (input.checkValidity() && onDone) onDone();
break;
case 'Escape':
if (onDone) onDone();
break;
default:
newValue = key.replace(/\b.$/, c => c.toUpperCase());
}
if (newValue != null) {
const error = RX_ERR.test(newValue) ? t('genericError') : '';
if (e && !error) prefs.set(prefId, newValue);
input.setCustomValidity(error);
input.value = newValue;
input.focus();
if (buttons) {
btnOk.disabled = Boolean(error);
btnUndo.disabled = newValue === initialValue;
btnReset.disabled = !newValue;
}
}
if (e) {
e.preventDefault();
e.stopPropagation();
}
}
}
/* exported showCodeMirrorPopup */
function showCodeMirrorPopup(title, html, options) {
const popup = helpPopup.show(title, html);
popup.classList.add('big');
const popup = helpPopup.show(title, html, {className: 'big'});
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
mode: 'css',
@ -166,7 +192,7 @@ function showCodeMirrorPopup(title, html, options) {
}, options));
cm.focus();
document.documentElement.style.pointerEvents = 'none';
$.root.style.pointerEvents = 'none';
popup.style.pointerEvents = 'auto';
const onKeyDown = event => {
@ -181,7 +207,7 @@ function showCodeMirrorPopup(title, html, options) {
window.on('closeHelp', () => {
window.off('keydown', onKeyDown, true);
document.documentElement.style.removeProperty('pointer-events');
$.root.style.removeProperty('pointer-events');
cm = popup.codebox = null;
}, {once: true});

160
global-dark.css Normal file
View File

@ -0,0 +1,160 @@
@media screen and (prefers-color-scheme: dark), dark {
:root {
/* Comfortable dark themes don't use absolutes so the range is compressed */
--c00: hsl(0, 0%, 80%);
--c10: hsl(0, 0%, 73.5%);
--c20: hsl(0, 0%, 66%);
--c30: hsl(0, 0%, 59.5%);
--c40: hsl(0, 0%, 53%);
--c45: hsl(0, 0%, 49.75%);
--c50: hsl(0, 0%, 46.5%);
--c60: hsl(0, 0%, 40%);
--c65: hsl(0, 0%, 36.75%);
--c70: hsl(0, 0%, 33.5%);
--c75: hsl(0, 0%, 30.25%);
--c80: hsl(0, 0%, 27%);
--c85: hsl(0, 0%, 23.75%);
--c90: hsl(0, 0%, 20.5%);
--c95: hsl(0, 0%, 17.25%);
--c100: hsl(0, 0%, 14%);
/* min/max are exposed in case we want to use an overdrive color for emphasis */
--cmin: hsl(0, 0%, 100%);
--cmax: hsl(0, 0%, 0%);
--accent-1: hsl(180, 100%, 95%);
--accent-3: hsl(180, 30%, 18%);
--input-bg: var(--c95);
--red1: hsl(0, 85%, 55%);
}
textarea,
input[type=url],
input[type=time] {
background-color: var(--input-bg);
color: var(--fg);
}
input::-webkit-inner-spin-button {
filter: invert(.8);
}
input[type=radio]:checked:after {
background-color: var(--fg);
}
input[type=time]::-webkit-calendar-picker-indicator {
filter: invert(1);
}
select {
background-color: var(--bg);
}
.onoffswitch {
--knob: var(--c50);
}
.CodeMirror-scrollbar-filler,
.CodeMirror-gutter-filler {
background-color: var(--bg) !important;
border: 0;
}
::-webkit-scrollbar {
width: 17px;
height: 17px;
background: var(--bg);
}
::-webkit-scrollbar-corner {
background: var(--bg);
border: 0;
}
/* buttons */
::-webkit-scrollbar-button:single-button {
height: 17px;
width: 17px;
background-size: 9px;
background-position: 4px 7px;
background-repeat: no-repeat;
}
::-webkit-scrollbar-button:horizontal:single-button {
background-position: 7px 4px;
}
/* up */
::-webkit-scrollbar-button:single-button:vertical:decrement {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='1,0 0,1 2,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:decrement:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='1,0 0,1 2,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:decrement:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='1,0 0,1 2,1'/></svg>");
}
/* down */
::-webkit-scrollbar-button:single-button:vertical:increment {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='0,0 2,0 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:increment:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='0,0 2,0 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:vertical:increment:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='0,0 2,0 1,1'/></svg>");
}
/* left */
::-webkit-scrollbar-button:single-button:horizontal:decrement {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='0,1 1,2 1,0'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:decrement:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='0,1 1,2 1,0'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:decrement:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='0,1 1,2 1,0'/></svg>");
}
/* right */
::-webkit-scrollbar-button:single-button:horizontal:increment {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 35%)'><polygon points='0,0 0,2 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:increment:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 45%)'><polygon points='0,0 0,2 1,1'/></svg>");
}
::-webkit-scrollbar-button:single-button:horizontal:increment:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='2' height='2' fill='hsl(0, 0%, 55%)'><polygon points='0,0 0,2 1,1'/></svg>");
}
::-webkit-scrollbar-track-piece {
background: hsl(0, 0%, 17%);
border: 1px solid var(--bg);
}
::-webkit-scrollbar-track-piece:hover {
background: hsl(0, 0%, 20%);
}
::-webkit-scrollbar-track-piece:active {
background: hsl(0, 0%, 25%);
}
::-webkit-scrollbar-thumb {
background: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1' fill='hsl(0, 0%, 30%)'><rect width='1' height='1'/></svg>") 2px 2px no-repeat;
}
::-webkit-scrollbar-thumb:horizontal {
background-size: 100% 13px;
}
::-webkit-scrollbar-thumb:vertical {
background-size: 13px 100%;
}
::-webkit-scrollbar-thumb:hover {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1' fill='hsl(0, 0%, 33%)'><rect width='1' height='1'/></svg>");
}
::-webkit-scrollbar-thumb:active {
background-image: url("data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' width='1' height='1' fill='hsl(0, 0%, 40%)'><rect width='1' height='1'/></svg>");
}
::-webkit-resizer {
background: var(--input-bg) linear-gradient(-45deg,
transparent 3px, #888 3px,
#888 4px, transparent 4px,
transparent 6px, #888 6px,
#888 7px, transparent 7px) no-repeat;
border: 2px solid transparent;
}
:-webkit-autofill {
box-shadow: 0 0 0 1000px var(--input-bg) inset;
-webkit-text-fill-color: #fff;
}
@supports (-moz-appearance: none) {
/* Workarounds for FF bugs/quirks */
textarea {
border: 1px solid var(--c65);
}
* {
scrollbar-color: var(--c75) var(--bg);
}
}
}

View File

@ -1,15 +1,52 @@
body {
font: normal 12px Arial, system-ui, sans-serif;
@supports not (accent-color: red) {
/* This suppresses a bug in all? browsers: they apply transitions during page load.
* It was fixed by crrev.com/886802 in Chrome 93, which we detect via `accent-color`.
* Using an increased specificity to override sane selectors in user styles.
* Using \1 to simplify js code because \0 is converted to \xFFFD per spec. */
html#stylus #header *:not(#\1transition-suppressor) {
transition: none !important;
}
}
:root {
--family: Arial, "Helvetica Neue", Helvetica, system-ui, sans-serif;
--input-height: 22px;
--cmin: hsl(0, 0%, 00%);
--c00: hsl(0, 0%, 00%);
--c10: hsl(0, 0%, 10%);
--c20: hsl(0, 0%, 20%);
--c30: hsl(0, 0%, 30%);
--c40: hsl(0, 0%, 40%);
--c45: hsl(0, 0%, 45%);
--c50: hsl(0, 0%, 50%);
--c60: hsl(0, 0%, 60%);
--c65: hsl(0, 0%, 65%);
--c70: hsl(0, 0%, 70%);
--c75: hsl(0, 0%, 75%);
--c80: hsl(0, 0%, 80%);
--c85: hsl(0, 0%, 85%);
--c90: hsl(0, 0%, 90%);
--c95: hsl(0, 0%, 95%);
--c100: hsl(0, 0%, 100%);
--cmax: hsl(0, 0%, 100%);
--bg: var(--c100);
--fg: var(--c00);
--accent-1: hsl(180, 100%, 15%);
--accent-2: hsl(180, 50%, 40%);
--accent-3: hsl(180, 40%, 69%);
--red1: hsl(0, 70%, 45%);
}
body {
font: normal 12px var(--family);
background-color: var(--bg);
color: var(--fg);
margin: 0;
}
body:lang(ja) {
font-family: Arial, 'Meiryo UI', 'MS Gothic', system-ui, sans-serif;
}
body:lang(zh-CN) {
font-family: Arial, 'Microsoft YaHei UI', 'Microsoft YaHei', system-ui, sans-serif;
}
body:lang(zh-TW),
body:lang(zh-HK) {
font-family: Arial, 'Microsoft JhengHei UI', 'Microsoft JhengHei', system-ui, sans-serif;
@ -24,11 +61,12 @@ button {
overflow: hidden;
text-overflow: ellipsis;
padding: 2px 7px;
border: 1px solid hsl(0, 0%, 62%);
border: 1px solid var(--c60);
font: inherit;
font-size: 13px;
color: #000;
background-color: hsl(0, 0%, 100%);
line-height: 1.2;
color: var(--fg);
background-color: var(--bg);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwGBBwIHvKt6QAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAL0lEQVQI12NoaGgQZ2JgYGBkYmBgYGZiYGBggrMY4VxsYsyoskQQCB2MWAxAMhkADVECDhlW9CoAAAAASUVORK5CYII=');
background-repeat: repeat-x;
background-size: 100% 100%;
@ -36,39 +74,79 @@ button {
}
button:not(:disabled):hover {
background-color: hsl(0, 0%, 95%);
border-color: hsl(0, 0%, 52%);
background-color: var(--c95);
border-color: var(--c50);
}
button:active {
background-color: hsl(0, 0%, 95%);
border-color: hsl(0, 0%, 52%);
background-color: var(--c95);
border-color: var(--c50);
background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAAeCAYAAADtlXTHAAAABmJLR0QA/wD/AP+gvaeTAAAACXBIWXMAAAsTAAALEwEAmpwYAAAAB3RJTUUH4QwJARIWJNZvuQAAAB1pVFh0Q29tbWVudAAAAAAAQ3JlYXRlZCB3aXRoIEdJTVBkLmUHAAAAMElEQVQI12NoaGgIZmJgYPjLxMDA8I+JgYHhP5z1Dy7xH5X7jxQCzWQ0A9DEILYBABm5HtJk2jPHAAAAAElFTkSuQmCC');
background-repeat: repeat-x;
background-size: 100% 100%;
}
button .svg-icon {
cursor: auto;
}
[data-ui-theme="light"] button .svg-icon {
/* Our svgs are pixel-aligned so the default #000 looks too strong */
fill: #333;
}
/* For some odd reason these hovers appear lighter than all other button hovers in every browser */
#message-box-buttons button:not(:disabled):hover {
background-color: hsl(0, 0%, 90%);
border-color: hsl(0, 0%, 50%);
background-color: var(--c90);
border-color: var(--c50);
}
input {
font: inherit;
border: 1px solid hsl(0, 0%, 66%);
border: 1px solid var(--c65);
transition: border-color .1s, box-shadow .1s;
}
input:not([type]),
input[type=text],
input[type=number],
input[type=search] {
background: #fff;
color: #000;
height: 22px;
min-height: 22px!important;
line-height: 22px;
background: var(--bg);
color: var(--fg);
height: var(--input-height);
min-height: var(--input-height)!important;
line-height: var(--input-height);
box-sizing: border-box;
padding: 0 3px;
border: 1px solid hsl(0, 0%, 66%);
border: 1px solid var(--c65);
}
input:invalid {
background-color: rgba(255, 0, 0, 0.1);
color: darkred;
}
.svg-icon {
cursor: pointer;
vertical-align: middle;
transition: fill .5s;
width: 20px;
height: 20px;
fill: var(--c40);
}
.svg-icon:hover {
fill: var(--fg);
}
.svg-icon.info {
width: 14px;
height: 16px;
margin-left: .5ex;
}
.svg-icon.config {
width: 16px;
height: 16px;
}
.svg-icon.checked {
@ -76,7 +154,7 @@ input[type=search] {
height: 8px;
width: 8px;
display: none;
fill: #000;
fill: var(--fg);
margin: 2px 0 0 2px;
}
@ -91,7 +169,7 @@ input[type="checkbox"]:not(.slider) {
position: absolute;
left: 0;
top: 0;
border: 1px solid hsl(0, 0%, 46%);
border: 1px solid var(--c45);
height: 12px;
width: 12px;
display: inline-flex;
@ -102,8 +180,8 @@ input[type="checkbox"]:not(.slider) {
}
input[type="checkbox"]:not(.slider):hover {
border-color: hsl(0, 0%, 32%);
background-color: hsl(0, 0%, 82%);
border-color: var(--c30);
background-color: var(--c80);
}
input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
@ -115,29 +193,33 @@ input[type="checkbox"]:not(.slider):checked + .svg-icon.checked {
input[type="checkbox"]:not(.slider):disabled {
background-color: transparent;
border-color: hsl(0, 0%, 50%);
border-color: var(--c50);
}
input[type="checkbox"]:not(.slider):disabled + .svg-icon.checked {
fill: hsl(0, 0%, 50%);
fill: var(--c50);
}
input[type="checkbox"]:not(.slider):disabled + .svg-icon.checked + span {
color: hsl(0, 0%, 50%);
color: var(--c50);
}
label {
transition: color .1s;
}
.checkbox-wrapper {
padding-left: 16px;
position: relative;
}
select {
-moz-appearance: none;
-webkit-appearance: none;
height: 22px;
height: var(--input-height);
font: inherit;
color: #000;
color: var(--fg);
background-color: transparent;
border: 1px solid hsl(0, 0%, 66%);
border: 1px solid var(--c65);
padding: 0 20px 0 6px;
transition: color .5s;
}
@ -155,7 +237,7 @@ select {
display: inline-flex;
height: 14px;
width: 14px;
fill: #000;
fill: var(--fg);
position: absolute;
top: 4px;
right: 4px;
@ -165,15 +247,15 @@ select {
input[type="radio"] {
-webkit-appearance: none;
-moz-appearance: none;
background: hsl(0, 0%, 88%);
background: var(--c90);
border-radius: 50%;
border: 1px solid hsl(0, 0%, 60%);
border: 1px solid var(--c60);
cursor: default;
height: 13px;
width: 13px;
position: relative;
margin: 0 4px 1px 0;
}
input[type="radio"]:after {
content: '';
background-color: transparent;
@ -187,11 +269,15 @@ input[type="radio"]:after {
top: 2px;
position: absolute;
}
input[type="radio"]:checked:after {
background-color: hsl(0, 0%, 30%);
background-color: var(--c30);
transform: scale(1);
}
.radio-wrapper {
display: flex;
align-items: center;
line-height: 1.5;
}
/* restore disabled state dimming */
button:disabled,
@ -203,13 +289,44 @@ select[disabled] > option {
select:disabled + .select-arrow,
select[disabled] + .select-arrow {
fill: hsl(0, 0%, 50%);
fill: var(--c50);
}
summary {
-moz-user-select: none;
user-select: none;
}
/* global stuff we use everywhere */
.hidden {
display: none !important;
}
.rel {
position: relative;
}
.abs {
position: absolute;
}
html:not(.all-disabled) body:not(#stylus-popup) #disableAll-label:not([data-persist]) {
display: none;
}
html:not(.all-disabled) #disableAll-label::before {
content: attr(data-on);
}
.all-disabled #disableAll-label::before {
content: attr(data-off);
}
.all-disabled #disableAll-label {
font-weight: bold;
color: var(--red1);
}
.all-disabled #disableAll-label .svg-icon {
fill: var(--red1);
}
.all-disabled #disableAll {
border-color: var(--red1);
}
:focus,
.CodeMirror-focused,
@ -217,6 +334,7 @@ select[disabled] + .select-arrow {
textarea[data-focused-via-click]:focus,
input:not([type])[data-focused-via-click]:focus, /* same as "text" */
input[type="text"][data-focused-via-click]:focus,
input[type="url"][data-focused-via-click]:focus,
input[type="search"][data-focused-via-click]:focus,
input[type="number"][data-focused-via-click]:focus {
/* Using box-shadow instead of the ugly outline in new Chrome */
@ -230,24 +348,95 @@ input[type="number"][data-focused-via-click]:focus {
box-shadow: none;
}
/* header resizer */
:root {
--header-width: 280px;
--header-resizer-width: 8px;
}
#header-resizer {
position: absolute;
top: 0;
right: 0;
bottom: 0;
width: var(--header-resizer-width);
box-sizing: border-box;
cursor: e-resize;
border-width: 0 1px;
border-style: solid;
color: hsla(0, 0%, 50%, .5);
border-color: currentColor;
pointer-events: auto;
}
#header-resizer:active {
border-color: var(--c50);
}
#header-resizer::after {
content: '';
position: absolute;
border-right: 2px dotted currentColor;
left: 2px;
width: 0;
height: 100%;
}
body.resizing-h {
cursor: e-resize;
}
body.resizing-v {
cursor: n-resize;
}
body.resizing-h > *,
body.resizing-v > * {
pointer-events: none;
-moz-user-select: none;
user-select: none;
}
/* header resizer - end */
.split-btn {
position: relative;
white-space: nowrap;
--menu-pad: .5em;
}
.split-btn-pedal {
margin-left: -1px !important;
padding-left: .25em !important;
padding-right: .25em !important;
min-width: 0 !important;
}
.split-btn-pedal::after {
--side: 4px;
content: '';
border: var(--side) solid transparent;
display: inline-block;
border-top: calc(var(--side) * 1.3) solid currentColor;
vertical-align: bottom;
}
.split-btn.active .split-btn-pedal {
box-shadow: inset 0 0 100px rgba(0, 0, 0, .2);
}
.split-btn-menu {
background: var(--bg);
position: absolute;
box-shadow: 2px 3px 7px rgba(0, 0, 0, .5);
border: 1px solid hsl(180deg, 50%, 50%);
white-space: nowrap;
cursor: pointer;
padding: .25em 0;
z-index: 1000;
}
.split-btn-menu > * {
padding: var(--menu-pad) 1em;
display: block;
}
.split-btn-menu > :hover {
background-color: hsla(180deg, 50%, 50%, .25);
color: var(--fg);
}
@supports (-moz-appearance: none) {
.moz-appearance-bug .svg-icon.checked,
.moz-appearance-bug .onoffswitch input,
.moz-appearance-bug input[type="radio"]:after {
display: none !important;
}
.moz-appearance-bug input[type="checkbox"] {
-moz-appearance: checkbox !important;
}
.moz-appearance-bug input[type="radio"] {
-moz-appearance: radio !important;
}
.firefox select {
padding: 0 20px 0 2px;
line-height: 22px!important;
line-height: var(--input-height)!important;
}
svg {
@ -257,9 +446,9 @@ input[type="number"][data-focused-via-click]:focus {
/* We can customize everything about number inputs except arrows. They're horrible in Linux FF, so we'll hide them unless hovered or focused. */
.firefox.non-windows input[type="number"] {
-moz-appearance: textfield;
background: #fff;
color: #000;
border: 1px solid hsl(0, 0%, 66%);
background: var(--bg);
color: var(--fg);
border: 1px solid var(--c65);
}
.firefox.non-windows input[type="number"]:not(:disabled):hover,
@ -268,18 +457,14 @@ input[type="number"][data-focused-via-click]:focus {
}
.firefox.non-windows input[type="color"] {
background: #fff;
border: 1px solid hsl(0, 0%, 66%);
background: var(--bg);
border: 1px solid var(--c65);
padding: 4px;
}
}
/* Firefox cannot handle fractions in font-size */
.firefox button:not(.install) {
line-height: 13px;
padding: 3px 7px;
}
.firefox.moz-appearance-bug button:not(.install) {
padding: 2px 4px;
@media (max-width: 850px) {
#header-resizer {
display: none !important;
}
}

BIN
images/eyedropper/16px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 275 B

BIN
images/eyedropper/32px.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 376 B

Some files were not shown because too many files have changed in this diff Show More