Compare commits

..

644 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
Rob Garrison
3e415460c6 1.5.16 2021-02-23 21:37:53 -06:00
Rob Garrison
ea6359307b Update locales 2021-02-23 21:35:26 -06:00
tophf
acaf12f694 ensure button panel is tall enough
see #1188
2021-02-23 15:13:01 +03:00
tophf
abd018d750 fix autocomplete for words starting with d/r/u
fixes #1188
2021-02-23 14:59:56 +03:00
tophf
cf1f51af0a
vivaldi bug workaround: open webAuth flow in a tab (#1186) 2021-02-22 15:12:19 +03:00
tophf
b61cd75b25 ignore style messages if started in disableAll mode 2021-02-22 00:36:40 +03:00
tophf
c5e2baaf87 pass disableAll to styleInjector, fixes #1187 2021-02-22 00:36:37 +03:00
tophf
76ee0992e7 update deps: CM 5.59.3, webAuth 0.1.1 2021-02-21 09:17:33 +03:00
tophf
a674874861 show global styles for frames if main page is blocked
fixes #1183
2021-02-16 17:03:18 +03:00
tophf
58ae4704b2 fix css autocomplete sort order 2021-02-14 21:48:44 +03:00
tophf
3102738cfb
improve linter info and config popup (#1171)
* improve linter info popup

* show rule id so the user can configure it
* add "configure" button to show the linter config UI
* add margins between items
* emphasize active rules in linter config dialog
2021-02-14 20:30:50 +03:00
eight
c17dddb0ee
Fix: less intrusive authorization (#1172)
* Update db-to-cloud

* Change: refactor sync logic, disallow implicit auth

* Add: better relog message in options page

* read prefs only when `ready`
* show the internal error text in icon tooltip
* show the internal error text in options fully
Co-authored-by: tophf <tophf@gmx.com>

* Update _locales/en/messages.json
Co-authored-by: Enrico Lamperti <910672+elamperti@users.noreply.github.com>
2021-02-14 18:24:49 +03:00
tophf
75db3601d0 fix #1177
regressed in fdbfb235
2021-02-10 19:47:07 +03:00
tophf
83adc5aa1e fix #1176 2021-02-10 18:29:34 +03:00
tophf
890ff395c0
use a new solid gear icon everywhere (#1173) 2021-02-10 12:11:52 +03:00
tophf
c00c748c1e simplify/deduplicate badge error logic
also correctly restore the real badge info when error is cleared
2021-02-09 12:53:03 +03:00
tophf
c60c764d34
add: indicate sync error as 'x' in icon badge (#1166)
* indicate sync error as 'x' in icon badge

* fix/simplify

* Add: show more errors on badge

Co-authored-by: eight04 <eight04@gmail.com>
2021-02-09 14:58:30 +08:00
tophf
1746235d0d fix showLintHelp dialog 2021-02-07 00:17:52 +03:00
tophf
94cf673f68 load semver faster, fixes #1167 2021-02-06 19:10:22 +03:00
tophf
ade8d1981b restore getStylesViaXhr check for about:blank frames
regressed in 50959354
2021-02-05 10:51:39 +03:00
tophf
50959354ec wait for next paint in about:blank frames, fixes #1165 2021-02-05 10:35:02 +03:00
tophf
784a1018f8 fixup: call getDateFromVer when updateUrl is set, #1159 2021-02-05 09:50:52 +03:00
tophf
34c067c3be hide USO-archive version in manager
* it's a hard-to-read blob of digits: 2021110.8.46
* it's somewhat redundant as we have the 'update age' column
2021-02-04 14:10:30 +03:00
tophf
c12d3fc5e3 convert USO styles to USO-archive on update 2021-02-04 14:10:30 +03:00
Rob Garrison
272dea01a2 1.5.15 2021-02-03 19:05:23 -06:00
Rob Garrison
5304805c63 Update locales 2021-02-03 19:04:35 -06:00
tophf
8ee964c045 simplify rerouteHotkeys to avoid enabling it twice 2021-02-02 22:49:00 +03:00
tophf
24a0077783 chrome bug: onCommitted is fired twice 2021-02-02 00:40:30 +03:00
tophf
c41a5f92c3 preserve dirty after importing moz-format [v2], fixes #1163 2021-02-02 00:33:56 +03:00
tophf
3a3104b30a update parserlib
* add aspect-ratio, color-adjust, forced-color-adjust
* remove ar units
2021-02-01 19:00:30 +03:00
tophf
afa4a1ac14 restore USO bug workaround for style settings
fixes #1158
2021-01-23 10:14:23 +03:00
tophf
2c9ea4fdc0 CodeMirror update leftovers 2021-01-23 10:14:16 +03:00
tophf
8cabf8a8aa CodeMirror 5.59.2 2021-01-20 14:59:31 +03:00
tophf
02bd682135 ignore executeScript errors e.g. due to frame removal 2021-01-19 09:36:53 +03:00
tophf
a88996be6f restore correct handling of openEditInWindow
regressed in fdbfb235
fixes #1156
2021-01-19 09:27:19 +03:00
tophf
a66a1f8ed6 code cosmetics 2021-01-18 09:50:27 +03:00
tophf
4338f67352 LESS: use math:parens-division
This is the default mode in LESS4.
Fixes #1154.
2021-01-16 09:18:54 +03:00
tophf
914943ed4c remove sliders
* can be already implemented as a userstyle
* will be exposed in usercss config dialog later
2021-01-15 13:24:21 +03:00
tophf
eb5fd90dc7 remember CM bookmarks on reload/revisit 2021-01-12 17:39:24 +03:00
tophf
b00e6d23fe revert CodeMirror to 5.59.0
5.59.1 is bugged https://github.com/codemirror/CodeMirror/issues/6558
2021-01-10 13:44:38 +03:00
tophf
fe176c9b62 [compact layout] show sections TOC on first click 2021-01-09 13:21:06 +03:00
tophf
ac7b33d7e0 fix resizing of last section if page is scrolled 2021-01-07 14:52:38 +03:00
tophf
39e03b0a9f fix autocomplete after var(, autocompleteOnTyping 2021-01-07 14:52:38 +03:00
tophf
312f444ec7 fix 'true' === true check in setupLivePrefs 2021-01-07 14:52:38 +03:00
tophf
1308efb8d0 fix/simplify fitSelectBox 2021-01-07 14:52:38 +03:00
tophf
11d311d1e8 avoid forced layout in highlightEditedStyle 2021-01-07 14:52:38 +03:00
tophf
767b2017e0 wait for main stylesheet to load before forcing layout 2021-01-05 19:54:49 +03:00
tophf
dfb9135f6a stylelint 13.8.0 2021-01-05 18:51:01 +03:00
tophf
7d6b4fc8ac csslint: code cosmetics 2021-01-05 18:27:46 +03:00
tophf
57233db546 csslint: add 'shorthand-overrides' rule 2021-01-05 12:14:26 +03:00
tophf
cb85fe9392 get the full list of stylelint rules 2021-01-04 09:28:59 +03:00
tophf
fd890f8e61 show "Options" heading in options UI 2021-01-02 23:34:42 +03:00
tophf
8807819f16 parserlib: update props 2021-01-01 19:49:38 +03:00
tophf
6563aca141 CodeMirror 5.59.1 + use emptyDirSync instead of shx
+ restore the original jsonlint with its trailing spaces, accidentally fixed by fdbfb235
2021-01-01 18:36:05 +03:00
tophf
d26dd92f44 use .meta.css in update check on greasyfork 2021-01-01 18:20:25 +03:00
tophf
c35302dff6 add margins for updateAllCheckSucceededSomeEdited 2021-01-01 18:20:25 +03:00
tophf
fdbfb23547
API groups + use executeScript for early injection (#1149)
* parserlib: fast section extraction, tweaks and speedups
* csslint: "simple-not" rule
* csslint: enable and fix "selector-newline" rule
* simplify db: resolve with result
* simplify download()
* remove noCode param as it wastes more time/memory on copying
* styleManager: switch style<->data names to reflect their actual contents
* inline method bodies to avoid indirection and enable better autocomplete/hint/jump support in IDE
* upgrade getEventKeyName to handle mouse clicks
* don't trust location.href as it hides text fragment
* getAllKeys is implemented since Chrome48, FF44
* allow recoverable css errors + async'ify usercss.js
* openManage: unminimize windows
* remove the obsolete Chrome pre-65 workaround
* fix temporal dead zone in apply.js
* ff bug workaround for simple editor window
* consistent window scrolling in scrollToEditor and jumpToPos
* rework waitForSelector and collapsible <details>
* blank paint frame workaround for new Chrome
* extract stuff from edit.js and load on demand
* simplify regexpTester::isShown
* move MozDocMapper to sections-util.js
* extract fitSelectBox()
* initialize router earlier
* use helpPopup.close()
* fix autofocus in popups, follow-up to 5bb1b5ef
* clone objects in prefs.get() + cosmetics
* reuse getAll result for INC
2021-01-01 17:27:58 +03:00
tophf
06823bd5b4 CodeMirror 5.59.0 + bump deps 2020-12-22 01:17:56 +03:00
tophf
1e614fa2bc don't count&title removed sections 2020-12-15 01:52:10 +03:00
tophf
7140993e6c parserlib: fix reading \\, regressed in 6d04c0e 2020-12-14 21:18:15 +03:00
tophf
775d77a3a8 clone change positions to avoid breaking CodeMirror 2020-12-10 23:25:07 +03:00
tophf
579750fbc0 fix linting when style was initially error-free
regressed in e6f94378
2020-12-10 11:00:24 +03:00
tophf
0b540fbabd parserlib: up to 10% faster first run
* inline generation of trivial tokens
* remove Tokens.CUSTOM_PROP
* allow fit-content keyword as it's used in the wild
* add color-scheme
* add path()
* don't try to validate props with vars
* auto-compile function grammar
* remove CSS4 color functions
* show full error stack
2020-12-10 01:30:17 +03:00
tophf
75cf46aaa7 fix search-target-editor style 2020-12-08 23:03:13 +03:00
tophf
26a539bd62 fix closestVisible nearby check 2020-12-08 18:50:14 +03:00
tophf
463c3b5139 fix subsequent clicks in colorpicker
seems like new CodeMirror modifies the position object
2020-12-08 14:46:03 +03:00
tophf
6d04c0eb7d optimize parserlib by ~10%
* add skipValidation option
* set `recoverable:true` on errors inside declarations
2020-11-29 15:11:31 +03:00
tophf
e6f94378bf re-enable linter after import + async'ify
regressed in 420733b9
2020-11-29 14:25:07 +03:00
tophf
207afccd65
improve autocomplete (#1136)
* fix interaction when search overlay splits `styles`
* handle @rules and @-moz-document functions
* show props in stylus-lang
2020-11-28 23:29:33 +03:00
tophf
6847681ed3 fix findSections for {} + detect reusedEnd 2020-11-27 15:10:03 +03:00
tophf
a87c49f0fc autoresize embedded popup for config dialogs 2020-11-27 15:09:16 +03:00
tophf
cb64a6bac9 use jumpToPos more, fix coords calc 2020-11-26 17:04:51 +03:00
tophf
6451eb533c switch to styleMissingName 2020-11-26 12:09:11 +03:00
tophf
355f240779 add a suffix in editor title 2020-11-26 12:04:42 +03:00
tophf
a91183e1bb fix scrolling in jumpToPos 2020-11-25 23:28:22 +03:00
tophf
a5848682b3 normalize linebreaks in parseMozFormat 2020-11-24 21:04:11 +03:00
tophf
7c2b46be83 fix PortDownloader::onDisconnect 2020-11-24 17:06:25 +03:00
tophf
c635f2e38c monkeypatch next/prevBookmark commands to use jumpToPos 2020-11-24 15:17:14 +03:00
tophf
657798d219 improve bookmarking + rework codemirror-factory.js
* pull editing-only stuff from codemirror-default.js
* switch throttledSetOption to IntersectionObserver
2020-11-24 13:16:51 +03:00
tophf
b4ca17c531 call rerouteHotkeys when restoring scrollInfo
...because we intentionally don't focus any CM in this case as it's bugged as hell
2020-11-24 13:16:51 +03:00
tophf
32cca90ddd limit button reposition to usercss (8b58b228 fixup) 2020-11-24 12:54:39 +03:00
tophf
8b58b22825 [simple-window editor] embed popup as iframe 2020-11-24 12:16:23 +03:00
tophf
b59737a012 $create: assign style property as a string/object 2020-11-24 12:16:23 +03:00
tophf
eb99101f35 inform if the found style doesn't match site url 2020-11-24 00:07:49 +03:00
tophf
5bb1b5ef35 dedup code for modals in popup 2020-11-24 00:07:49 +03:00
tophf
4d198f56e2 faster popup load to avoid resize flicker 2020-11-24 00:07:49 +03:00
tophf
51f125113d restore top margin for #no-styles after 2bf30ed1 2020-11-23 07:29:30 +03:00
tophf
00ae843f78 unicode-aware word breaking, fixes #1124 2020-11-22 14:53:10 +03:00
tophf
2bf30ed16d
tweaks and fixes (#1123)
* same color for disabled styles in popup/manager
* reduce slider knob
* fix click-to-edit in popup on slider and iframe badge
* indicate slider interactivity on hovering name
* remove 2px gap when first/last entry is striped
* stretch 'blocked' separator to full width
* unreachable dimming should not apply to frames
* restore CWS check in popup
* increase not-applied opacity on name as it's #999 now
* oldUI: restore 'disabled' bubble + show 'usercss' fully
* adjust disabled colors to match perception because transparent text is rendered using gamma-blending whereas colored text uses LCD-antialiasing so to match opacity .6 of #000 we need #888 not #666
2020-11-22 14:09:59 +03:00
tophf
e6988d2f9e update usercss-meta, #1108 2020-11-21 20:35:23 +03:00
tophf
008e33254d add option to use sliders in manager and popup 2020-11-21 10:13:30 +03:00
tophf
3dc684f85f reduce usercss bubble to UC + tweak CSS
* remove 'disabled' bubble
* simplify .style-info
* fix double padding between version and UC
* match manager's font-weight for disabled styles in popup
2020-11-21 10:13:30 +03:00
tophf
17a0bd69c0 fix applies-to expander's collapse-on-click 2020-11-20 18:08:04 +03:00
tophf
70e3ba15b7 use 'default' internally for the default theme element
* the pref won't be influenced by the current UI language
* also reset to 'default' if failed to load the theme's css file
2020-11-20 16:10:15 +03:00
tophf
420480887e update CodeMirror 5.58.3 2020-11-20 09:50:19 +03:00
tophf
1bd366beb9 restore styleSectionsEqual in updater for non-usercss
...which was broken in bc8d8b2
2020-11-19 18:18:05 +03:00
tophf
79cd6da824 increase connection timeout to 60sec 2020-11-19 18:18:05 +03:00
tophf
420733b93a
PatchCSP + tweaks/fixes/features (#1107)
* add Patch CSP option
* show style version, size, and update age in manager
* add scope selector to style search in manager
* keep scroll position and selections in tab's session
* directly install usercss from raw github links
* ditch localStorage, use on-demand SessionStore proxy
* simplify localization
* allow <code> tag in i18n-html
* keep &nbsp; nodes in HTML templates
* API.getAllStyles is actually faster with code untouched
* fix fitToContent when applies-to is taller than window
* dedupe linter.enableForEditor calls
* prioritize visible CMs in refreshOnViewListener
* don't scroll to last style on editing a new one
* delay colorview for invisible CMs
* eslint comma-dangle error + autofix files
* styleViaXhr: also toggle for disableAll pref
* styleViaXhr: allow cookies for sandbox CSP
* simplify notes in options
* simplify getStylesViaXhr
* oldUI fixups:
  * remove separator before 1st applies-to
  * center name bubbles
* fix updateToc focus on a newly added section
* fix fitToContent when cloning section
* remove CSS `contain` as it makes no difference
* replace overrides with declarative CSS + code cosmetics
* simplify adjustWidth and make it work in FF
2020-11-18 14:17:15 +03:00
Rob Garrison
7fa4d10fd6 1.5.14 2020-11-13 19:28:48 -06:00
Rob Garrison
a065039d50 update locales 2020-11-13 19:28:21 -06:00
tophf
0b3e027bfd auto-promisify browser.* methods on call 2020-11-13 20:07:43 +03:00
tophf
3db6662d2f fix 1px shift of applies-to text when favicon is added in FF 2020-11-11 20:42:13 +03:00
tophf
6259cc2e79 speed up manager: render more targets only on demand 2020-11-11 20:27:54 +03:00
tophf
a26115154a fix getPrefs error on browser startup in the active tab 2020-11-11 13:28:20 +03:00
narcolepticinsomniac
da6361637d
Replace USO link 2020-11-10 17:47:23 -05:00
tophf
d183779fb5 accept any content-type for text/ except text/html 2020-11-10 21:40:50 +03:00
tophf
3a8f47f4db revert d405bc64 - obsolete since stylus-lang 0.54.7 2020-11-10 20:40:47 +03:00
tophf
eb70e5a2aa update stylus-lang dependency to 0.54.7 2020-11-10 20:40:47 +03:00
tophf
30b9378d2c use mousewheel to change a focused input[type=range] 2020-11-09 22:59:42 +03:00
tophf
dc4819e7d0
Merge pull request #1101 from tophf/import-prefs
import/export options in backup json
2020-11-09 21:18:23 +03:00
tophf
ff1fa07267 import/export options in backup json
* import options on demand
* auto-grant declarativeContent
* include lint configs and usercss template
* simplify exportFile as crbug.com/798705 was fixed
2020-11-09 21:12:14 +03:00
tophf
7d18376cf2 always use deepCopy for prefs.values for safety 2020-11-09 21:08:50 +03:00
tophf
bc8d8b235c fix equalOrEmpty for empty strings 2020-11-09 21:08:47 +03:00
tophf
a94969e47d remove padding from linter report items
(icons already take care of proper spacing)
2020-11-08 20:29:10 +03:00
tophf
4ac92a4f9b unbork marks in linter report
CodeMirror 5.58+ uses an additional class name for common stuff
2020-11-08 20:29:09 +03:00
tophf
5e5fecbcfe
editor: section labels, TOC, tweaks (#1086)
* section labels, TOC, speedups and fixes

* show section numbers in widgets
* debounce livePreview in usercss editor
* better fixed header and compact layout compatibility
* fix section sizing for compact layout + layout speedup
* DocFuncMapper + cosmetics + fix Clone button
* don't run linter during initSections
* remove unused/unnecessary DOM polyfills
* report invalid @document function as parser error
* rewrite section finder
* simplify focusedViaClick
* simplify setPreprocessor and make it synchronous
* throttle offscreen line widgets in usercss with lots of sections
* add on, off aliases for add/removeEventListener + onOff
* use on/off aliases in changed files
* use getters in more places
2020-11-08 11:12:42 +03:00
tophf
71cabc2029 fix animateElement() when animation is disabled 2020-11-06 21:04:10 +03:00
tophf
aac0f476b2 tweaks/fixes for popup search link
* use a less specific category if the inline search wasn't used yet
* set a href in html to prevent transitions during init
2020-11-05 22:45:22 +03:00
tophf
635fc705f9 correctly clear gutter marks for sublime bookmarks 2020-11-04 17:47:41 +03:00
tophf
31558d5071 we use 'true' and 'false' strings as boolean T_T
fixup for b56dacb
2020-11-04 12:50:24 +03:00
tophf
97ad0753e0 restore direct fetching of styles in the options frame
regressed in bf40fa81
2020-11-02 22:20:41 +03:00
tophf
a997ecbe24 update CSSLint
* fix missing <zero>
* retry/consume attr()
* code cosmetics
2020-11-02 22:08:14 +03:00
tophf
32728b023b respond with null to avoid "port closed" errors 2020-11-01 22:48:42 +03:00
narcolepticinsomniac
ad44fe47c8
typo 2020-11-01 14:06:18 -05:00
narcolepticinsomniac
1c7e06e980
typos 2020-11-01 14:01:44 -05:00
tophf
21d4221df9 position colorpicker correctly, cosmetics (89431615 fixup) 2020-10-31 23:45:52 +03:00
tophf
972a83d5bc restore simpleDeepEqual (b56dacb6 fixup) 2020-10-31 21:00:19 +03:00
silverwind
caec255e16 Simplify CI action
Only run on a single node version, that way you won't get tripe error
annotations, and it's really not neccessary for this repo to test on
multiples anyways.
2020-10-30 08:22:53 +03:00
tophf
72cb5bdc9a don't spam console errors on contextMenu 'delete' command 2020-10-29 00:13:15 +03:00
tophf
b56dacb6b2 save prefs in bg to avoid data loss
* add `now` to simplify usage of prefs.subscribe
* tweak/simplify bits by separating bg/content concerns
2020-10-28 21:10:57 +03:00
tophf
be47cfc471 throttle colorview on page load 2020-10-28 21:05:19 +03:00
tophf
6d7bd650e9 strip stylelint warnings for // comments with @preprocessor 2020-10-28 13:19:17 +03:00
tophf
a81e1b8ac3 async'ify worker-util, reduce indirection 2020-10-28 13:19:17 +03:00
tophf
4764f91453 fix radiateArray when focusing search with extra CMs around 2020-10-28 13:08:00 +03:00
tophf
74364b9d63 hide incremental search textarea, 2a6850c0 fixup 2020-10-27 13:00:32 +03:00
tophf
2747d3930b simplify resizing of editor-in-new-window 2020-10-26 18:04:37 +03:00
tophf
bd3f630617 use a safe regexp for comments 2020-10-26 18:04:37 +03:00
tophf
2a6850c02e avoid scrollbar due to incremental search + subsequent filter 2020-10-26 18:04:36 +03:00
tophf
2c674bdc0c remove the extraneous margin between applies-to 2020-10-26 18:04:35 +03:00
tophf
2ed936af00 don't autojump to first match when opening search 2020-10-26 18:04:34 +03:00
tophf
89431615b3
improve colorpicker dialog (#1079)
* switch to a user-resizable palette
* allow moving
* remove hideDelay
2020-10-26 18:03:41 +03:00
tophf
bf40fa81e8
async'ify msg, don't throw for flow control (#1078) 2020-10-26 17:39:07 +03:00
tophf
1a7b51be6b
warn when paste-importing usercss with @preprocessor, #1082 2020-10-26 17:37:31 +03:00
tophf
4fade0fdfe async'ify replaceStyle 2020-10-26 17:33:23 +03:00
tophf
34ad3cfaef embed replaceSections as replace option of initSections 2020-10-26 17:24:11 +03:00
tophf
92fcb02a57 handle document.cookie exceptions in sandboxed frames 2020-10-26 07:49:08 +03:00
tophf
4eabdf3f57 warn when paste-importing usercss with @preprocessor 2020-10-25 22:36:41 +03:00
tophf
5ba111dce9 update parserlib
* min(), max(), clamp(), also in @media
* add/fix some props and units
* handle `attr()`
* use lowerCmp()
* approve functions with USO vars
2020-10-25 00:24:28 +03:00
tophf
3a615e4e06 CodeMirror 5.58.2 2020-10-23 22:35:14 +03:00
tophf
2af83ee846 remove more -webkit- prefixes 2020-10-23 21:32:02 +03:00
narcolepticinsomniac
8598b71a73
Options tweaks (#1077)
* Options tweaks

* account for last-child change
2020-10-23 09:51:46 -04:00
tophf
595b037ab1 remove unused IS_BG and wrong AMO condition
* the user may have allowed access to AMO via about:config
* the code was wrong anyway, should be `!FIREFOX`
2020-10-23 15:22:18 +03:00
tophf
5a5512aa0f use own implementation of UUIDv4 2020-10-23 09:27:33 +03:00
tophf
54605c838b set customName only on user input 2020-10-22 23:48:17 +03:00
tophf
e6e7d7d158 leave name input empty in new usercss style 2020-10-22 23:47:46 +03:00
tophf
2d9785be6e clear dirty upon swapping style 2020-10-22 23:31:09 +03:00
tophf
0199b2c0bb preserve dirty after importing moz-format, fixes #1075 2020-10-22 23:18:58 +03:00
tophf
6593d5c05a get disableAll pref earlier, fixes #1074 2020-10-22 22:58:45 +03:00
tophf
f9804036b2
instant style injection via synchronous XHR (#1070)
* don't run web-ext test as it fails on Chrome-only permissions

* generate stylus-firefox.zip without declarativeContent

* limit note's width in options

* run updateExposeIframes only in frames
2020-10-22 22:16:55 +03:00
tophf
7f15ae324d
Merge pull request #1054 from tophf/custom-name
fix local name customization for usercss/legacy
2020-10-22 15:07:33 +03:00
narcolepticinsomniac
76e2a90392 moz-format CM focus style 2020-10-22 15:05:54 +03:00
tophf
d1b9338707 make manager load real fast 2020-10-22 15:05:54 +03:00
tophf
bc6c9c826a make editor load even faster
* reorder scripts
* make style request earlier
2020-10-22 15:03:07 +03:00
tophf
2e1a903cc7 fix local name customization for usercss/legacy 2020-10-22 15:01:49 +03:00
tophf
34f899fc45 don't start incremental search on Space or Shift-Space 2020-10-22 08:51:48 +03:00
tophf
3cb9cbb862
show a palette for current editor in color picker (#1068)
also keep the dialog visible for 30 seconds instead of 5
2020-10-18 16:40:11 +03:00
tophf
e6d73be049
option to open editor in a simple window (no omnibox) (#1067) 2020-10-18 16:37:42 +03:00
tophf
d405bc64ae ignore empty documents produced by stylus-lang bug 2020-10-18 16:34:31 +03:00
tophf
5501efb1be expose version for styles installed from greasyfork/sleazyfork 2020-10-18 16:33:52 +03:00
tophf
3d0b733e9a parserlib: skip spaces before "," in @document foo() , bar() 2020-10-16 20:09:27 +03:00
tophf
56a8212fdf parserlib: add text to background-clip 2020-10-15 22:31:13 +03:00
tophf
492b75d84e parserlib: implement @supports selector() 2020-10-14 21:33:29 +03:00
tophf
a71b621bf9 remove -webkit- prefix on standardized features 2020-10-14 19:48:59 +03:00
tophf
9e487b03e5
tweak editor (#1063)
* also apply live-preview if an unsaved style was disabled

* use box-shadow instead of outline for focus everywhere

* allow focus outline on click in text/search input or textarea

* search inputs should use the same style as text inputs

* also use box-shadow focus on delete buttons

* remove URLSearchParams workaround, not needed since Chrome 55

* use `once` in addEventListener, available since Chrome 55

* update USO bug workarounds, remove obsolete ones

* ping/pong to fix openURL with `message` in FF

* use unprefixed CSS filter, available since Chrome 53

* use unprefixed CSS user-select, available since Chrome 54

* focus tweaks

* also use text query in inline search for Stylus category

* use event.key, available since Chrome 51

Co-authored-by: narcolepticinsomniac
2020-10-13 21:14:54 +03:00
tophf
60fc6f2456
Editor fixes, make sectioned editor open quickly again (#1061)
* make usercss editor full-height again

* make sectioned editor open quickly again

* remove leftovers

* autofocus when add/clone button is clicked

* don't fit to content on clicking the add button

* scroll the window to show a manually added section entirely

* autofocus on a manually added applies-to

* disable Save button while loading

* use standard CSS for a focused CodeMirror outline

* trigger refresh sooner by one viewport in advance

* declare refreshOnView as a standard function

* run fixedHeader asynchronously to prevent self-triggering

* account for header in compact mode when fitting to content

* code cosmetics
2020-10-11 17:13:25 +03:00
tophf
ad24ee0c15
switch to USO-archive for inline search in popup, #1056 2020-10-11 16:53:42 +03:00
tophf
740a16a563 disconnect port explicitly in FF 2020-10-11 14:37:55 +03:00
tophf
4d1110986c update CSSLint
* Scroll Snap L1 (CR 2020-09-18)
* dedupe border*
* fix font-variation-settings grammar
2020-10-11 09:59:05 +03:00
tophf
af726405e1 also search in global styles 2020-10-10 14:25:43 +03:00
tophf
11ce144efb remove the redundant stylus-lang warning filter 2020-10-09 19:40:11 +03:00
tophf
707cd6576f process current contents when live-reload is enabled 2020-10-09 19:22:13 +03:00
tophf
4913da2e19 use installation url on known sites as homepage 2020-10-09 13:47:58 +03:00
tophf
78b0e33ba4 faster install from known sites 2020-10-09 13:47:57 +03:00
tophf
0162f39163 switch to USO-archive for inline search in popup
feature: retry sub.domain.tld as domain.tld if no styles are found

old bug fix: show newly added style in popup

dedupe/simplify bits of popup.js
2020-10-09 13:47:57 +03:00
tophf
5196f96ee3 trigger change on wheeling inside <select> 2020-10-09 13:47:57 +03:00
tophf
b840d4897d cleanup usoSearchCache + tidy up db.js 2020-10-09 13:47:57 +03:00
tophf
9994811819 recognize 'backdrop-filter' 2020-10-08 12:19:14 +03:00
tophf
a01bd3cd61 update polyfill for Chrome>=55 2020-10-08 11:19:18 +03:00
tophf
7c205880d2 require Chrome 55 and allow native async/await syntax 2020-10-08 11:19:18 +03:00
tophf
6b2dff6687 treat empty url-prefix() as non-matching 2020-10-08 11:07:13 +03:00
tophf
cb89be8682 ignore empty code only in global (non-targeted) sections 2020-10-08 11:05:07 +03:00
tophf
7c89f7b21d autosort style elements on own pages too 2020-10-05 21:08:39 +03:00
tophf
15d854f913 fixup for c416fa7c: remove the leftovers, take 2 2020-10-05 16:19:18 +03:00
tophf
e7a6e86b6c fixup for c416fa7c: remove the leftovers 2020-10-05 13:05:35 +03:00
tophf
c416fa7ca0
rework and move newUI+theme to options.html (#1050)
* rework and move newUI+theme to options.html

* rephrase/clarify the find styles label

* switch to USO-archive

* search for 'Stylus' keyword to filter out Stylish crud

* use archive's default search order
2020-10-02 11:10:52 -04:00
tophf
3f6c85637c Containment Module L2 (WD, 2020-06-03) 2020-10-02 12:41:57 +03:00
tophf
e0a7372f4f enable starHack option 2020-10-02 12:41:57 +03:00
narcolepticinsomniac
038629517e
remove dropbox disabled (#1041) 2020-09-22 12:04:19 -04:00
tophf
ee30aa1407
convert colors in uso preprocessor to match USO site (#997)
#rrggbb for /*[[color]]*/
r,g,b for /*[[color-rgb]]*/

(no alpha channel)
2020-09-22 07:15:40 -04:00
tophf
2b149f97a5
CodeMirror 5.58.0 (#1037) 2020-09-22 07:03:31 -04:00
tophf
30983db679
Scroll Anchoring L1 (ED 2020-09-18) (#1038) 2020-09-22 06:56:53 -04:00
tophf
fa1496ecb8
use tab.pendingUrl (#1040) 2020-09-22 06:54:48 -04:00
tophf
aa43507478 parserlib: consume unknown @-rules per CSS grammar 2020-09-21 11:19:53 +03:00
eight
07ba44cc2c
Change: switch to launchWebAuthFlow polyfill (#1017)
* WIP: add webextLaunchWebAuthFlow

* Change: switch to webextLaunchWebAuthFlow

* Bump dependencies

* Fix: use minimized version

* Fix: wrong call to promisifyChrome
2020-08-31 16:38:18 +08:00
narcolepticinsomniac
01cfb435f6
Remove deprecated dropbox and add sync button (#1025)
* Remove deprecated dropbox and add sync button

* re-use existing message
2020-08-24 12:27:23 -04:00
tophf
5109f9abb3
CodeMirror 5.57 (#1023)
* codemirror 5.57

* dedupe the props defined in new codemirror
2020-08-23 11:44:27 -04:00
dependabot[bot]
e0aadb752a Bump minimist from 1.2.0 to 1.2.5
Bumps [minimist](https://github.com/substack/minimist) from 1.2.0 to 1.2.5.
- [Release notes](https://github.com/substack/minimist/releases)
- [Commits](https://github.com/substack/minimist/compare/1.2.0...1.2.5)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-20 21:16:13 +03:00
dependabot[bot]
db3747d5f0 Bump lodash from 4.17.15 to 4.17.19
Bumps [lodash](https://github.com/lodash/lodash) from 4.17.15 to 4.17.19.
- [Release notes](https://github.com/lodash/lodash/releases)
- [Commits](https://github.com/lodash/lodash/compare/4.17.15...4.17.19)

Signed-off-by: dependabot[bot] <support@github.com>
2020-08-20 21:10:55 +03:00
tophf
56f1574433
fix styling of options frame in FF (#1022) 2020-08-19 15:33:04 -04:00
eight
a0172c262c
Fix: enable linter after processing metadata (#1018) 2020-08-17 21:06:52 +08:00
eight
7d93608186
Fix: show sync start error (#1016)
* Update lock file

* Fix: the first sync doesn't collect error messages
2020-08-14 22:32:24 +08:00
tophf
54b1f218e0
Enhance: promisify chrome into browser, drop promisify (#866)
* promisify `chrome` into `browser`

* comment

* comment

* comment

* Add: a naive browser polyfill

* Fix: polyfill doesn't detect content script env correctly

Co-authored-by: eight04 <eight04@gmail.com>
2020-08-14 20:16:01 +08:00
tophf
3d94c641b3
CodeMirror 5.56 + csslint update (#1014)
* CodeMirror 5.56

* csslint: Overscroll Behavior L1 (ED 2020-01-16)

* don't list rare/obsolete/future/existing css props
2020-08-13 13:58:09 -04:00
tophf
96c87fc55d fix colorpicker rgb/hsl parser with leading/trailing space 2020-08-09 15:03:20 +03:00
tophf
d18314357f
use onBoundsChanged to remember editor size/position (#1007) 2020-08-02 00:00:44 -04:00
tophf
f8402a2211
use mousewheel to change focused "input[type=number], select" (#1010)
* use mousewheel to change focused "input[type=number], select"

* revert 0f394fa8 (no longer needed)
2020-08-01 23:50:12 -04:00
tophf
0f394fa8d8 don't scroll CM when target is a scrollable input 2020-08-01 18:00:08 +03:00
tophf
7e7839bb1e
avoid adding # to the page URL when clicking dummy links (#1006) 2020-07-29 21:30:00 -04:00
tophf
c43315e697
restore Shift-Ctrl-Wheel to scroll window in multi-section mode (#1005) 2020-07-29 21:26:38 -04:00
Rob Garrison
837b119e47 1.5.13 2020-07-24 08:19:00 -05:00
Rob Garrison
282a1e7432 Update translations 2020-07-24 07:48:25 -05:00
tophf
e1807c8851 restore autosize-on-type of find input 2020-07-24 07:42:58 +03:00
tophf
7e195594b5
show all spaces in find input in firefox (#1002) 2020-07-23 17:27:29 -04:00
tophf
079b7a39f1
update the style list after DOM (#998)
regressed in 7e6edb9e
2020-07-17 15:42:21 -04:00
tophf
7e6edb9e1b
add/remove style elements on global toggle (#990) 2020-07-15 00:31:33 -04:00
tophf
787466fc40
consume leftovers when auto-completing properties (#992) 2020-07-14 16:25:19 -04:00
tophf
09f6e8d44a
prolong worker lifetime to 5 minutes (#993) 2020-07-14 16:24:00 -04:00
tophf
ca3633b896
fix column combinator detection (#994) 2020-07-14 16:21:47 -04:00
tophf
2f4658657d
increase USO style search timeout (#984) 2020-06-29 12:16:44 -04:00
tophf
429c34ca8b
csslint: Selectors L4 parts (ED 2020-04-07) (#981)
* add :where(), remove :matches()

* add "s" case-sensitivity flag

* add "||" column combinator
2020-06-26 12:47:12 -04:00
tophf
4e146d0e54
use current-line mode for csslint allow override (#977) 2020-06-25 12:08:04 -04:00
tophf
574f11b552
when sorting on updateDate use installDate as a fallback (#975) 2020-06-25 11:51:08 -04:00
tophf
60a37af0e0
add a hotkey & right-click to beautify silently (#972)
* add a hotkey & right-click to beautify silently

* fix closestVisible
2020-06-22 12:14:41 -04:00
tophf
e1ed3bf222
update CSSLint (#967)
* refactor Tokens to enable goto-symbol and find-usages

* refactor <length-percentage>

* CSS Text Module L3 (ED 2020-06-08)

* CSS Fonts Module L4 parts (ED 2020-06-11)

* CSS Scrollbars Module L1 (ED 2020-02-24)

* skip all successfully parsed parts after var()
2020-06-17 09:13:28 -04:00
silverwind
2ffad1b6bb
Add Github Actions (#958)
The tests do spew a few warnings but no errors so this should pass. I
did not bother adding macOS/Windows to the test matrix as there's
probably nothing platform-dependant in this repo, but it could be done
on request.
2020-06-11 00:48:48 -04:00
tophf
015bda764a
update deps + split devdeps/deps (#953) 2020-06-02 04:08:02 -04:00
narcolepticinsomniac
60d314b165
Improve delete confirmation autofocus visual indication (#956) 2020-06-02 02:33:07 -04:00
tophf
a1b0eb7df1
show sublime bookmarks (#951)
* remove redundant setGutterMarker optimization

* show sublime bookmarks
2020-06-01 00:54:49 -04:00
tophf
39c62e684e
throttle DOM updates in manager while importing (#950) 2020-05-31 20:14:42 -04:00
MATE
d9f5ef138c Add Korean translation 2020-05-31 22:36:40 +03:00
tophf
a7a9ee7205
use major browser version in CHROME constant (#946) 2020-05-31 01:43:56 -04:00
tophf
a8fe66550b
Fixes for csslint parserlib (#947)
* css parser: vars in @supports(--var: foo)

* css parser: @keyframes inside @supports and @media
2020-05-29 23:33:29 -04:00
tophf
379c825408
focus search field on Ctrl-F in manager (#935) 2020-05-22 23:12:54 -04:00
mcpower
367d1672c5
Add unlimitedStorage permission (#930)
IndexedDB storage may be evicted by the browser at its discretion [1].
Granting the unlimitedStorage permission ensures that IndexedDB data
will not be evicted [2], and allows the extension to store more than
5MB of data if needed [3].

[1]: https://web.dev/storage-for-the-web/#eviction
[2]: https://crbug.com/680392#c5
[3]: https://developer.chrome.com/apps/declare_permissions#unlimitedStorage
2020-05-16 15:51:50 -04:00
tophf
43f6bdf4ed
use a separate loadTimeout for the actual data transfer (#931) 2020-05-16 15:50:04 -04:00
tophf
7ab0651e4d
add ":" to prop names in css autocomplete menu (#915) 2020-05-01 01:52:18 -04:00
narcolepticinsomniac
81b6137d8a
Max version Dropbox disable and no sync pull animations (#913)
* Max version Dropbox disable and no sync pull animations

Max version Dropbox disable and no sync pull animations.

* Remove TODO

* Kill sync animations entirely, add ref
2020-04-28 10:06:57 -04:00
narcolepticinsomniac
c50b3cfddc
Fix missing CWS page icon (#907) 2020-04-23 09:43:04 -04:00
Rob Garrison
bdf32a7174 1.5.12 2020-04-22 07:26:24 -05:00
333 changed files with 57635 additions and 49799 deletions

View File

@ -1,4 +1,2 @@
vendor/ vendor/
vendor-overwrites/* vendor-overwrites/
!vendor-overwrites/colorpicker
!vendor-overwrites/csslint

View File

@ -1,13 +1,16 @@
# https://github.com/eslint/eslint/blob/master/docs/rules/README.md # https://github.com/eslint/eslint/blob/master/docs/rules/README.md
parserOptions: parserOptions:
ecmaVersion: 2015 ecmaVersion: 2017
env: env:
browser: true browser: true
es6: true es6: true
webextensions: true webextensions: true
globals:
require: readonly # in polyfill.js
rules: rules:
accessor-pairs: [2] accessor-pairs: [2]
array-bracket-spacing: [2, never] array-bracket-spacing: [2, never]
@ -19,7 +22,7 @@ rules:
brace-style: [2, 1tbs, {allowSingleLine: false}] brace-style: [2, 1tbs, {allowSingleLine: false}]
camelcase: [2, {properties: never}] camelcase: [2, {properties: never}]
class-methods-use-this: [2] class-methods-use-this: [2]
comma-dangle: [0] comma-dangle: [2, {arrays: always-multiline, objects: always-multiline}]
comma-spacing: [2, {before: false, after: true}] comma-spacing: [2, {before: false, after: true}]
comma-style: [2, last] comma-style: [2, last]
complexity: [0] complexity: [0]
@ -42,7 +45,15 @@ rules:
id-blacklist: [0] id-blacklist: [0]
id-length: [0] id-length: [0]
id-match: [0] id-match: [0]
indent-legacy: [2, 2, {VariableDeclarator: 0, SwitchCase: 1}] indent: [2, 2, {
SwitchCase: 1,
ignoreComments: true,
ignoredNodes: [
"TemplateLiteral > *",
"ConditionalExpression",
"ForStatement"
]
}]
jsx-quotes: [0] jsx-quotes: [0]
key-spacing: [0] key-spacing: [0]
keyword-spacing: [2] keyword-spacing: [2]
@ -86,7 +97,7 @@ rules:
no-empty: [2, {allowEmptyCatch: true}] no-empty: [2, {allowEmptyCatch: true}]
no-eq-null: [0] no-eq-null: [0]
no-eval: [2] no-eval: [2]
no-ex-assign: [2] no-ex-assign: [0]
no-extend-native: [2] no-extend-native: [2]
no-extra-bind: [2] no-extra-bind: [2]
no-extra-boolean-cast: [2] no-extra-boolean-cast: [2]
@ -136,6 +147,9 @@ rules:
no-proto: [2] no-proto: [2]
no-redeclare: [2] no-redeclare: [2]
no-regex-spaces: [2] no-regex-spaces: [2]
no-restricted-globals: [2, name, event]
# `name` and `event` (in Chrome) are built-in globals
# but we don't use these globals so it's most likely a mistake/typo
no-restricted-imports: [0] no-restricted-imports: [0]
no-restricted-modules: [2, domain, freelist, smalloc, sys] no-restricted-modules: [2, domain, freelist, smalloc, sys]
no-restricted-syntax: [2, WithStatement] no-restricted-syntax: [2, WithStatement]
@ -163,7 +177,7 @@ rules:
no-unreachable: [2] no-unreachable: [2]
no-unsafe-finally: [2] no-unsafe-finally: [2]
no-unsafe-negation: [2] no-unsafe-negation: [2]
no-unused-expressions: [1] no-unused-expressions: [2]
no-unused-labels: [0] no-unused-labels: [0]
no-unused-vars: [2, {args: after-used}] no-unused-vars: [2, {args: after-used}]
no-use-before-define: [2, nofunc] no-use-before-define: [2, nofunc]
@ -189,7 +203,7 @@ rules:
prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}] prefer-const: [1, {destructuring: all, ignoreReadBeforeAssign: true}]
quote-props: [0] quote-props: [0]
quotes: [1, single, avoid-escape] quotes: [1, single, avoid-escape]
radix: [2, as-needed] radix: [2, always]
require-jsdoc: [0] require-jsdoc: [0]
require-yield: [2] require-yield: [2]
semi-spacing: [2, {before: false, after: true}] semi-spacing: [2, {before: false, after: true}]
@ -220,3 +234,7 @@ overrides:
webextensions: false webextensions: false
parserOptions: parserOptions:
ecmaVersion: 2017 ecmaVersion: 2017
- files: ["**/*worker*.js"]
env:
worker: true

View File

@ -24,6 +24,9 @@ If not, then provide details describing which page the feature will effect, e.g.
## Adding translations ## Adding translations
You can help us translate the extension on [Transifex](https://www.transifex.com/github-7/Stylus). 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 ## 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. -->

14
.github/workflows/ci.yml vendored Normal file
View File

@ -0,0 +1,14 @@
name: ci
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: actions/setup-node@v2
with:
node-version: '14'
- run: npm install
- run: npm test

10
.gitignore vendored
View File

@ -1,8 +1,8 @@
.DS_Store
pull_locales_login.rb
.vscode
node_modules/
yarn.lock
*.zip *.zip
.DS_Store
.eslintcache .eslintcache
.transifexrc .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 ## Screenshots
![Style manager](https://user-images.githubusercontent.com/1310400/34453460-214eaa5c-ed67-11e7-843b-d8960b71db6e.png) Manager | Editor | Popup search | Popup config | Manager config | Options
![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) ![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)
![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)
## Help ## Help
@ -53,7 +50,7 @@ Copyright &copy; 2005-2014 [Jason Barnabe](jason.barnabe@gmail.com)
Current Stylus: 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)** **[GNU GPLv3](./LICENSE)**

View File

@ -1,19 +1,18 @@
{ {
"InaccessibleFileHint": {
"message": "Stylus لا يستطيع الوصول الى بعض انواع الملفات ( ملفات pdf و json )"
},
"addStyleLabel": { "addStyleLabel": {
"message": "كتابة نمط جديد", "message": "كتابة نمط جديد"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "إضافة نمط", "message": "إضافة نمط"
"description": "Title of the page for adding styles"
}, },
"appliesAdd": { "appliesAdd": {
"message": "إضافة", "message": "إضافة"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "ينطبق على: $applies$", "message": "ينطبق على: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,84 +20,73 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "والمزيد", "message": "و المزيد"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "عناوين URL في النطاق", "message": "عناوين URL في النطاق"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم.", "message": "استخدم عناصر تحكم 'ينطبق على' لتقييد عناوين URL التي ينطبق عليها الرمز في هذا القسم."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "ينطبق على", "message": "ينطبق على"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "عناوين URL التي تطابق regexp", "message": "عناوين URL التي تطابق regexp"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "إزالة", "message": "إزالة"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "تحديد", "message": "تحديد"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "كل شيء", "message": "كل شيء"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlOption": { "appliesUrlOption": {
"message": "عنوان URL", "message": "عنوان URL"
"description": "Option to make the style apply to the entered string as a URL"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "عناوين URL البادئة بـ", "message": "عناوين URL البادئة بـ"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "البحث عن تحديثات لكل الأنماط", "message": "البحث عن تحديثات لكل الأنماط"
"description": "Label for the button to check all styles for updates"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "البحث عن تحديث", "message": "البحث عن تحديث"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "جارٍ البحث...", "message": "جارٍ البحث..."
"description": "Text to display when checking a style for an update" },
"confirmDelete": {
"message": "حذف"
},
"confirmSave": {
"message": "حفظ"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "هل تريد بالتأكيد حذف هذا النمط؟", "message": "هل تريد بالتأكيد حذف هذا النمط؟"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "حذف", "message": "حذف"
"description": "Label for the button to delete a style"
}, },
"description": { "description": {
"message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى.", "message": "يمكنك تغيير نمط الويب باستخدام Stylus، وهي أداة لإدارة أنماط المستخدم. وتتيح Stylus لك بسهولة تثبيت المظاهر والأشكال الخارجية لكل من Google، وFacebook وYouTube وOrkut فضلاً عن الكثير جدًا من مواقع الويب الأخرى."
"description": "Extension description"
}, },
"disableStyleLabel": { "disableStyleLabel": {
"message": "تعطيل", "message": "تعطيل"
"description": "Label for the button to disable a style" },
"editDeleteText": {
"message": "حذف"
}, },
"editStyleHeading": { "editStyleHeading": {
"message": "تعديل النمط", "message": "تعديل النمط"
"description": "Title of the page for editing styles"
}, },
"editStyleLabel": { "editStyleLabel": {
"message": "تعديل", "message": "تعديل"
"description": "Label for the button to go to the edit style page"
}, },
"editStyleTitle": { "editStyleTitle": {
"message": "تعديل النمط $stylename$", "message": "تعديل النمط $stylename$",
"description": "Title of the page for editing styles",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -106,60 +94,55 @@
} }
}, },
"enableStyleLabel": { "enableStyleLabel": {
"message": "تمكين", "message": "تمكين"
"description": "Label for the button to enable a style"
}, },
"findStylesForSite": { "genericAdd": {
"message": "البحث عن المزيد من الأنماط لموقع الويب هذا", "message": "إضافة"
"description": "Text for a link that gets a list of styles for the current site" },
"genericEnabledLabel": {
"message": "ممكّن"
}, },
"helpAlt": { "helpAlt": {
"message": "مساعدة", "message": "مساعدة"
"description": "Alternate text for help buttons"
}, },
"installUpdate": { "installUpdate": {
"message": "تثبيت التحديث", "message": "تثبيت التحديث"
"description": "Label for the button to install an update for a single style"
}, },
"manageHeading": { "manageHeading": {
"message": "الأنماط المثبتة", "message": "الأنماط المثبتة"
"description": "Heading for the manage page"
}, },
"noStylesForSite": { "noStylesForSite": {
"message": "لم يتم تثبيت أي أنماط لموقع الويب هذا.", "message": "لم يتم تثبيت أي أنماط لموقع الويب هذا."
"description": "Text displayed when no styles are installed for the current site"
}, },
"openManage": { "openManage": {
"message": "إدارة الأنماط المثبتة", "message": "إدارة الأنماط المثبتة"
"description": "Link to open the manage page." },
"optionsSyncUrl": {
"message": "عنوان URL"
}, },
"sectionAdd": { "sectionAdd": {
"message": "إضافة قسم آخر", "message": "إضافة قسم آخر"
"description": "Label for the button to add a section"
}, },
"sectionCode": { "sectionCode": {
"message": "الرمز", "message": "الرمز"
"description": "Label for the code for a section"
}, },
"sectionRemove": { "sectionRemove": {
"message": "إزالة القسم", "message": "إزالة القسم"
"description": "Label for the button to remove a section" },
"sections": {
"message": "الأقسام"
}, },
"styleCancelEditLabel": { "styleCancelEditLabel": {
"message": "رجوع للإدارة", "message": "رجوع للإدارة"
"description": "Label for cancel button for style editing"
}, },
"styleChangesNotSaved": { "styleChangesNotSaved": {
"message": "لقد أجريت تغييرات على هذا النمط بدون حفظها.", "message": "لقد أجريت تغييرات على هذا النمط بدون حفظها."
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
}, },
"styleEnabledLabel": { "styleEnabledLabel": {
"message": "ممكّن", "message": "ممكّن"
"description": "Label for the enabled state of styles"
}, },
"styleInstall": { "styleInstall": {
"message": "هل تريد تثبيت '$stylename$' في Stylus؟", "message": "هل تريد تثبيت '$stylename$' في Stylus؟",
"description": "Confirmation when installing a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -167,20 +150,16 @@
} }
}, },
"styleMissingName": { "styleMissingName": {
"message": "أدخل اسمًا", "message": "أدخل اسمًا"
"description": "Error displayed when user saves without providing a name"
}, },
"styleSaveLabel": { "styleSaveLabel": {
"message": "حفظ", "message": "حفظ"
"description": "Label for save button for style editing"
}, },
"styleToMozillaFormatHelp": { "styleToMozillaFormatHelp": {
"message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org.", "message": "يمكن استخدام تنسيق موزيلا للرمز باستخدام Stylus للمتصفح فايرفوكس ويمكن إرساله إلى userstyles.org."
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
}, },
"updateCheckFailBadResponseCode": { "updateCheckFailBadResponseCode": {
"message": "أخفق التحديث - استجاب الخادم بالرمز $code$", "message": "أخفق التحديث - استجاب الخادم بالرمز $code$",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": { "placeholders": {
"code": { "code": {
"content": "$1" "content": "$1"
@ -188,15 +167,12 @@
} }
}, },
"updateCheckFailServerUnreachable": { "updateCheckFailServerUnreachable": {
"message": "أخفق التحديث - الخادم يتعذر الوصول إليه.", "message": "أخفق التحديث - الخادم يتعذر الوصول إليه."
"description": "Text that displays when an update check failed because the update server is unreachable"
}, },
"updateCheckSucceededNoUpdate": { "updateCheckSucceededNoUpdate": {
"message": "النمط محدّث.", "message": "النمط محدّث."
"description": "Text that displays when an update check completed and no update is available"
}, },
"updateCompleted": { "updateCompleted": {
"message": "اكتمل التحديث.", "message": "اكتمل التحديث."
"description": "Text that displays when an update completed"
} }
} }

View File

@ -1,19 +1,15 @@
{ {
"addStyleLabel": { "addStyleLabel": {
"message": "Писане на нов стил", "message": "Писане на нов стил"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "Добавяне на стил", "message": "Добавяне на стил"
"description": "Title of the page for adding styles"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Добавяне", "message": "Добавяне"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Приложимо за: $applies$", "message": "Приложимо за: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,317 +17,240 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "и още", "message": "и още"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "Адреси на домейна", "message": "Адреси на домейна"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела.", "message": "Използвайте 'Приложимо за', за да ограничите адресите, за които се отнася кода в отдела."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Приложимо за", "message": "Приложимо за"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "Адреси, съвпадащи с регулярен израз", "message": "Адреси, съвпадащи с регулярен израз"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Премахване", "message": "Премахване"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Уточняване", "message": "Уточняване"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Всичко", "message": "Всичко"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlOption": { "appliesUrlOption": {
"message": "Адрес", "message": "Адрес"
"description": "Option to make the style apply to the entered string as a URL"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "Адреси, започващи с", "message": "Адреси, започващи с"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"applyAllUpdates": { "applyAllUpdates": {
"message": "Прилагане на всички обновления", "message": "Прилагане на всички обновления"
"description": "Label for the button to apply all detected updates"
}, },
"backupButtons": { "backupButtons": {
"message": "Резервни копия", "message": "Резервни копия"
"description": "Heading for backup"
},
"backupMessage": {
"message": "Изберете файл или го влачете до страницата.",
"description": "Message for backup"
}, },
"bckpInstStyles": { "bckpInstStyles": {
"message": "Изнасяне на стилове", "message": "Изнасяне на стилове"
"description": ""
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "Проверка на всички стилове за обновления", "message": "Проверка на всички стилове за обновления"
"description": "Label for the button to check all styles for updates"
}, },
"checkAllUpdatesForce": { "checkAllUpdatesForce": {
"message": "Повторна проверка", "message": "Повторна проверка"
"description": "Label for the button to apply all detected updates"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Проверка за обновления", "message": "Проверка за обновления"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "Проверяване...", "message": "Проверяване..."
"description": "Text to display when checking a style for an update"
}, },
"cm_autocompleteOnTyping": { "cm_autocompleteOnTyping": {
"message": "Автоматично завършване при въвеждане", "message": "Автоматично завършване при въвеждане"
"description": "Label for the checkbox in the style editor."
}, },
"cm_indentWithTabs": { "cm_indentWithTabs": {
"message": "Подпрозорци с умен отстъп", "message": "Подпрозорци с умен отстъп"
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
}, },
"cm_keyMap": { "cm_keyMap": {
"message": "Клавиши", "message": "Клавиши"
"description": "Label for the drop-down list controlling the keymap for the style editor."
}, },
"cm_lineWrapping": { "cm_lineWrapping": {
"message": "Пренасяне", "message": "Пренасяне"
"description": "Label for the checkbox controlling word wrap option for the style editor."
}, },
"cm_matchHighlight": { "cm_matchHighlight": {
"message": "Осветяване", "message": "Осветяване"
"description": "Label for the drop-down list controlling the automatic highlighting of current word/selection occurrences in the style editor."
}, },
"cm_matchHighlightSelection": { "cm_matchHighlightSelection": {
"message": "Само избраното", "message": "Само избраното"
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of currently selected text"
}, },
"cm_matchHighlightToken": { "cm_matchHighlightToken": {
"message": "Низа под показалеца", "message": "Низа под показалеца"
"description": "Style editor's 'highglight' drop-down list option: highlight the occurrences of the word/token under cursor even if nothing is selected"
}, },
"cm_resizeGripHint": { "cm_resizeGripHint": {
"message": "Щракнете два пъти за възстановяване/увеличаване на височината", "message": "Щракнете два пъти за възстановяване/увеличаване на височината"
"description": "Tooltip for the resize grip in style editor"
}, },
"cm_smartIndent": { "cm_smartIndent": {
"message": "Умен отстъп", "message": "Умен отстъп"
"description": "Label for the checkbox controlling smart indentation option for the style editor."
}, },
"cm_tabSize": { "cm_tabSize": {
"message": "Табулация", "message": "Табулация"
"description": "Label for the text box controlling tab size option for the style editor."
}, },
"cm_theme": { "cm_theme": {
"message": "Тема", "message": "Тема"
"description": "Label for the style editor's CSS theme."
}, },
"confirmCancel": { "confirmCancel": {
"message": "Отказ", "message": "Отказ"
"description": ""
}, },
"confirmDelete": { "confirmDelete": {
"message": "Изтриване", "message": "Изтриване"
"description": ""
}, },
"confirmNo": { "confirmNo": {
"message": "Не", "message": "Не"
"description": "'No' button in a confirm dialog"
}, },
"confirmOK": { "confirmOK": {
"message": "Добре", "message": "Добре"
"description": "" },
"confirmSave": {
"message": "Запазване"
}, },
"confirmStop": { "confirmStop": {
"message": "Спиране", "message": "Спиране"
"description": "'Stop' button in a confirm dialog"
}, },
"confirmYes": { "confirmYes": {
"message": "Да", "message": "Да"
"description": "'Yes' button in a confirm dialog"
}, },
"dbError": { "dbError": {
"message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?", "message": "Възникна грешка с базата от данни. Искате ли да посетите страницата с възможни решения?"
"description": "Prompt when a DB error is encountered"
}, },
"defaultTheme": { "defaultTheme": {
"message": "по подразбиране", "message": "по подразбиране"
"description": "Default CodeMirror CSS theme option on the edit style page"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "Сигурни ли сте, че искате да изтриете стила?", "message": "Сигурни ли сте, че искате да изтриете стила?"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "Изтриване", "message": "Изтриване"
"description": "Label for the button to delete a style"
}, },
"description": { "description": {
"message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове.", "message": "Пресъздайте стила на Мрежата със Стайлус, разширението за стилове. То ви позволява лесно да инсталиране теми за много сайтове."
"description": "Extension description"
}, },
"disableAllStyles": { "disableAllStyles": {
"message": "Изключване на всички стилове", "message": "Изключване на всички стилове"
"description": "Label for the checkbox that turns all enabled styles off."
}, },
"disableStyleLabel": { "disableStyleLabel": {
"message": "Изключване", "message": "Изключване"
"description": "Label for the button to disable a style"
}, },
"dragDropMessage": { "dragDropMessage": {
"message": "Пуснете резервното копие където и да е по страницата, за да го внесете.", "message": "Пуснете резервното копие където и да е по страницата, за да го внесете."
"description": "Drag'n'drop message"
}, },
"editDeleteText": { "editDeleteText": {
"message": "Изтриване", "message": "Изтриване"
"description": "Label for the context menu item in the editor to delete selected text"
}, },
"editGotoLine": { "editGotoLine": {
"message": "Отиване на ред", "message": "Отиване на ред"
"description": "Go to line or line:column on Ctrl-G in style code editor"
}, },
"editStyleHeading": { "editStyleHeading": {
"message": "Редактиране на стила", "message": "Редактиране на стила"
"description": "Title of the page for editing styles"
}, },
"editStyleLabel": { "editStyleLabel": {
"message": "Редактиране", "message": "Редактиране"
"description": "Label for the button to go to the edit style page"
}, },
"editStyleTitle": { "editStyleTitle": {
"message": "Редактиране на стила $stylename$", "message": "Редактиране на стила $stylename$",
"description": "Title of the page for editing styles",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
} }
} }
}, },
"editorStylesButton": {
"message": "Стилове за редактора",
"description": "Find styles for the editor"
},
"enableStyleLabel": { "enableStyleLabel": {
"message": "Включване", "message": "Включване"
"description": "Label for the button to enable a style"
}, },
"exportLabel": { "exportLabel": {
"message": "Изнасяне", "message": "Изнасяне"
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
}, },
"findStylesForSite": { "genericAdd": {
"message": "Още стилове за този сайт", "message": "Добавяне"
"description": "Text for a link that gets a list of styles for the current site"
}, },
"genericDisabledLabel": { "genericDisabledLabel": {
"message": "Изключено", "message": "Изключено"
"description": "Used in various lists/options to indicate that something is disabled" },
"genericEnabledLabel": {
"message": "Включено"
}, },
"genericHistoryLabel": { "genericHistoryLabel": {
"message": "Хронология", "message": "Хронология"
"description": "Used in various places to show a history log of something"
}, },
"helpAlt": { "helpAlt": {
"message": "Помощ", "message": "Помощ"
"description": "Alternate text for help buttons"
}, },
"helpKeyMapCommand": { "helpKeyMapCommand": {
"message": "Въведете име", "message": "Въведете име"
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
}, },
"helpKeyMapHotkey": { "helpKeyMapHotkey": {
"message": "Натиснете клавиш", "message": "Натиснете клавиш"
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
}, },
"importAppendLabel": { "importAppendLabel": {
"message": "Прибавяне към стила", "message": "Прибавяне към стила"
"description": "Label for the button to import a style and append to the existing sections"
}, },
"importAppendTooltip": { "importAppendTooltip": {
"message": "Прибавяне на внесения стил към текущия", "message": "Прибавяне на внесения стил към текущия"
"description": "Tooltip for the button to import a style and append to the existing sections"
}, },
"importLabel": { "importLabel": {
"message": "Внасяне", "message": "Внасяне"
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
}, },
"importReplaceLabel": { "importReplaceLabel": {
"message": "Презаписване на стила", "message": "Презаписване на стила"
"description": "Label for the button to import and overwrite current style"
}, },
"importReplaceTooltip": { "importReplaceTooltip": {
"message": "Презаписване на съдържанието на текщия стил с това от внесения", "message": "Презаписване на съдържанието на текщия стил с това от внесения"
"description": "Label for the button to import and overwrite current style"
}, },
"importReportLegendAdded": { "importReportLegendAdded": {
"message": "добавени", "message": "добавени"
"description": "Text after the number of styles added in the report shown after importing styles"
}, },
"importReportLegendIdentical": { "importReportLegendIdentical": {
"message": "пропуснати еднакви", "message": "пропуснати еднакви"
"description": "Text after the number of styles skipped due to being identical to the already installed ones in the report shown after importing styles"
}, },
"importReportLegendInvalid": { "importReportLegendInvalid": {
"message": "пропуснати невалидни", "message": "пропуснати невалидни"
"description": "Text after the number of styles skipped due to being invalid (not a Stylus/Stylish backup file probably) in the report shown after importing styles"
}, },
"importReportLegendUpdatedBoth": { "importReportLegendUpdatedBoth": {
"message": "с обновени код и метаданни", "message": "с обновени код и метаданни"
"description": "Text after the number of styles updated entirely in the report shown after importing styles"
}, },
"importReportLegendUpdatedCode": { "importReportLegendUpdatedCode": {
"message": "с обновен код", "message": "с обновен код"
"description": "Text after the number of styles with updated code (meta info is unchanged) in the report shown after importing styles"
}, },
"importReportLegendUpdatedMeta": { "importReportLegendUpdatedMeta": {
"message": "с обновени метаданни", "message": "с обновени метаданни"
"description": "Text after the number of styles with updated meta info like name/url in the report shown after importing styles"
}, },
"importReportTitle": { "importReportTitle": {
"message": "Внасянето на стилове завърши", "message": "Внасянето на стилове завърши"
"description": "Title of the report shown after importing styles"
}, },
"importReportUnchanged": { "importReportUnchanged": {
"message": "Нищо не беше променено.", "message": "Нищо не беше променено."
"description": "Message in the report shown after importing styles"
}, },
"importReportUndone": { "importReportUndone": {
"message": "върнати стила", "message": "върнати стила"
"description": "Text after the number of styles reverted in the message box shown after undoing the import of styles"
}, },
"importReportUndoneTitle": { "importReportUndoneTitle": {
"message": "Внасянето беше отменено", "message": "Внасянето беше отменено"
"description": "Title of the message box shown after undoing the import of styles"
}, },
"installUpdate": { "installUpdate": {
"message": "Инсталиране на обновлението", "message": "Инсталиране на обновлението"
"description": "Label for the button to install an update for a single style"
}, },
"linkGetHelp": { "linkGetHelp": {
"message": "Помощ", "message": "Помощ"
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
}, },
"linkGetStyles": { "linkGetStyles": {
"message": "Вземете стилове", "message": "Вземете стилове"
"description": "Help link text on the manage page e.g. https://userstyles.org"
}, },
"linterIssues": { "linterIssues": {
"message": "Проблеми", "message": "Проблеми"
"description": "Label for the CSS linter issues block on the style edit page"
}, },
"linterIssuesHelp": { "linterIssuesHelp": {
"message": "Проблеми, намерени от $link$ при следните правила:", "message": "Проблеми, намерени от $link$ при следните правила:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
"placeholders": { "placeholders": {
"link": { "link": {
"content": "$1" "content": "$1"
@ -339,244 +258,187 @@
} }
}, },
"manageFavicons": { "manageFavicons": {
"message": "Иконки на приложимите сайтовете", "message": "Иконки на приложимите сайтовете"
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
}, },
"manageFaviconsGray": { "manageFaviconsGray": {
"message": "Сиви", "message": "Сиви"
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
},
"manageFaviconsHelp": {
"message": "Разширението използва външна услуга https://www.google.com/s2/favicons",
"description": "Label for the checkbox that toggles applies-to favicons in the new UI on manage page"
}, },
"manageFilters": { "manageFilters": {
"message": "Филтри", "message": "Филтри"
"description": "Label for filters container"
}, },
"manageHeading": { "manageHeading": {
"message": "Инсталирани стилове", "message": "Инсталирани стилове"
"description": "Heading for the manage page"
}, },
"manageMaxTargets": { "manageMaxTargets": {
"message": "Брой на видимите приложими адреси", "message": "Брой на видимите приложими адреси"
"description": "Label for the numeric input box to limit max number of applies-to targets in the new UI on manage page"
}, },
"manageNewUI": { "manageNewUI": {
"message": "Нов интерфейс за управление", "message": "Нов интерфейс за управление"
"description": "Label for the checkbox that toggles the new UI on manage page"
}, },
"manageOnlyEnabled": { "manageOnlyEnabled": {
"message": "Само включените стилове", "message": "Само включените стилове"
"description": "Checkbox to show only enabled styles"
}, },
"manageOnlyLocal": { "manageOnlyLocal": {
"message": "Само местно създадените стилове", "message": "Само местно създадените стилове"
"description": "Checkbox to show only locally created styles i.e. non-updatable"
}, },
"manageOnlyLocalTooltip": { "manageOnlyLocalTooltip": {
"message": "(стиловете, които не са инсталирани през userstyles.org)", "message": "(стиловете, които не са инсталирани през userstyles.org)"
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
}, },
"manageOnlyUpdates": { "manageOnlyUpdates": {
"message": "Само стилове с обновления или проблеми", "message": "Само стилове с обновления или проблеми"
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
}, },
"manageTitle": { "manageTitle": {
"message": "Стилове", "message": "Стилове"
"description": "Title for the manage page"
}, },
"menuShowBadge": { "menuShowBadge": {
"message": "Брой на активните стилове", "message": "Брой на активните стилове"
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
}, },
"noStylesForSite": { "noStylesForSite": {
"message": "Няма инсталирани стилове за сайта.", "message": "Няма инсталирани стилове за сайта."
"description": "Text displayed when no styles are installed for the current site"
}, },
"openManage": { "openManage": {
"message": "Управление", "message": "Управление"
"description": "Link to open the manage page." },
"openOptions": {
"message": "Настройки"
}, },
"openStylesManager": { "openStylesManager": {
"message": "Управление на стиловете", "message": "Управление на стиловете"
"description": "Label for the style maanger opener in the browser action context menu."
}, },
"optionsActions": { "optionsActions": {
"message": "Действия", "message": "Действия"
"description": ""
}, },
"optionsAdvanced": { "optionsAdvanced": {
"message": "Разширени", "message": "Разширени"
"description": ""
}, },
"optionsAdvancedContextDelete": { "optionsAdvancedContextDelete": {
"message": "Добавяне на 'Изтриване' в контекстното меню на редактора", "message": "Добавяне на 'Изтриване' в контекстното меню на редактора"
"description": ""
}, },
"optionsAdvancedExposeIframes": { "optionsAdvancedExposeIframes": {
"message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]", "message": "Разкриване на 'iframes' чрез HTML[stylus-iframe]"
"description": ""
}, },
"optionsBadgeDisabled": { "optionsBadgeDisabled": {
"message": "Цвят на фона, когато е изключено", "message": "Цвят на фона, когато е изключено"
"description": ""
}, },
"optionsBadgeNormal": { "optionsBadgeNormal": {
"message": "Цвят на фона", "message": "Цвят на фона"
"description": ""
}, },
"optionsCheck": { "optionsCheck": {
"message": "Обновяване на стиловете", "message": "Обновяване на стиловете"
"description": ""
}, },
"optionsCheckUpdate": { "optionsCheckUpdate": {
"message": "Проверка и инсталиране на наличните обновления", "message": "Проверка и инсталиране на наличните обновления"
"description": ""
}, },
"optionsCustomizeBadge": { "optionsCustomizeBadge": {
"message": "Значка на иконката на лентата", "message": "Значка на иконката на лентата"
"description": ""
}, },
"optionsCustomizeIcon": { "optionsCustomizeIcon": {
"message": "Иконка на лентата със сечива", "message": "Иконка на лентата със сечива"
"description": ""
}, },
"optionsCustomizePopup": { "optionsCustomizePopup": {
"message": "Падащ прозорец", "message": "Падащ прозорец"
"description": ""
}, },
"optionsCustomizeUpdate": { "optionsCustomizeUpdate": {
"message": "Обновления", "message": "Обновления"
"description": ""
}, },
"optionsHeading": { "optionsHeading": {
"message": "Настройки", "message": "Настройки"
"description": "Heading for options section on manage page."
}, },
"optionsIconDark": { "optionsIconDark": {
"message": "Тъмни теми", "message": "Тъмни теми"
"description": ""
}, },
"optionsIconLight": { "optionsIconLight": {
"message": "Светли теми", "message": "Светли теми"
"description": ""
}, },
"optionsOpen": { "optionsOpen": {
"message": "Отваряне", "message": "Отваряне"
"description": ""
}, },
"optionsOpenManager": { "optionsOpenManager": {
"message": "Управление на стиловете", "message": "Управление на стиловете"
"description": ""
}, },
"optionsPopupWidth": { "optionsPopupWidth": {
"message": "Ширина на падащия прозорец (в пиксели)", "message": "Ширина на падащия прозорец (в пиксели)"
"description": ""
}, },
"optionsReset": { "optionsReset": {
"message": "Зануляване на настройки на първоначалните стойности", "message": "Зануляване на настройки на първоначалните стойности"
"description": ""
}, },
"optionsResetButton": { "optionsResetButton": {
"message": "Зануляване на настройките", "message": "Зануляване на настройките"
"description": ""
}, },
"optionsSubheading": { "optionsSubheading": {
"message": "Още настройки", "message": "Още настройки"
"description": "Subheading for options section on manage page." },
"optionsSyncUrl": {
"message": "Адрес"
}, },
"optionsUpdateImportNote": { "optionsUpdateImportNote": {
"message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални.", "message": "При внасянето на резервни копия от стари версии или от Стайлиш направете ръчна проверка за обновления, за да сте сигурни, че стиловете са актуални."
"description": ""
}, },
"popupStylesFirst": { "popupStylesFirst": {
"message": "Стилове преди командите", "message": "Стилове преди командите"
"description": "Label for the checkbox controlling section order in the popup."
}, },
"prefShowBadge": { "prefShowBadge": {
"message": "Брой на активните стилове за текущия сайт", "message": "Брой на активните стилове за текущия сайт"
"description": "Label for the checkbox controlling toolbar badge text."
}, },
"replace": { "replace": {
"message": "Заместване", "message": "Заместване"
"description": "Label before the replace input field in the editor shown on Ctrl-H"
}, },
"replaceAll": { "replaceAll": {
"message": "Заместване на всички", "message": "Заместване на всички"
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
}, },
"replaceWith": { "replaceWith": {
"message": "Заместване с", "message": "Заместване с"
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
}, },
"retrieveBckp": { "retrieveBckp": {
"message": "Внасяне на стилове", "message": "Внасяне на стилове"
"description": ""
}, },
"search": { "search": {
"message": "Търсене", "message": "Търсене"
"description": "Label before the search input field in the editor shown on Ctrl-F"
}, },
"searchRegexp": { "searchRegexp": {
"message": "Използвайте синтаксиса /re/ за търсене с регулярни изрази", "message": "Използвайте синтаксиса /re/ за търсене с регулярни изрази"
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"searchStyles": {
"message": "Търсене на съдържанието",
"description": "Label for the search filter textbox on the Manage styles page"
}, },
"sectionAdd": { "sectionAdd": {
"message": "Добавяне на друг отдел", "message": "Добавяне на друг отдел"
"description": "Label for the button to add a section"
}, },
"sectionCode": { "sectionCode": {
"message": "Код", "message": "Код"
"description": "Label for the code for a section"
}, },
"sectionRemove": { "sectionRemove": {
"message": "Премахване на отдела", "message": "Премахване на отдела"
"description": "Label for the button to remove a section" },
"sections": {
"message": "Отдели"
}, },
"shortcuts": { "shortcuts": {
"message": "Клавишни комбинации", "message": "Клавишни комбинации"
"description": "Go to shortcut configuration"
}, },
"shortcutsNote": { "shortcutsNote": {
"message": "Задаване на клавишни комбинации", "message": "Задаване на клавишни комбинации"
"description": ""
}, },
"styleBadRegexp": { "styleBadRegexp": {
"message": "Регулярният израз не е правилен.", "message": "Регулярният израз не е правилен."
"description": "Validation message for a bad regexp in a style"
}, },
"styleBeautify": { "styleBeautify": {
"message": "Разкрасяване", "message": "Разкрасяване"
"description": "Label for the CSS-beautifier button on the edit style page"
}, },
"styleBeautifyIndentConditional": { "styleBeautifyIndentConditional": {
"message": "Отстъп на @media, @supports", "message": "Отстъп на @media, @supports"
"description": "CSS-beautifier option"
}, },
"styleCancelEditLabel": { "styleCancelEditLabel": {
"message": "Назад към стиловете", "message": "Назад към стиловете"
"description": "Label for cancel button for style editing"
}, },
"styleChangesNotSaved": { "styleChangesNotSaved": {
"message": "Направили сте промени по стила без да ги запазите.", "message": "Направили сте промени по стила без да ги запазите."
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
}, },
"styleEnabledLabel": { "styleEnabledLabel": {
"message": "Включено", "message": "Включено"
"description": "Label for the enabled state of styles"
}, },
"styleFromMozillaFormatPrompt": { "styleFromMozillaFormatPrompt": {
"message": "Поставете кода във формат на Мозила", "message": "Поставете кода във формат на Мозила"
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
}, },
"styleInstall": { "styleInstall": {
"message": "Да се инсталира ли '$stylename$'?", "message": "Да се инсталира ли '$stylename$'?",
"description": "Confirmation when installing a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -584,68 +446,49 @@
} }
}, },
"styleMissingName": { "styleMissingName": {
"message": "Въведете име", "message": "Въведете име"
"description": "Error displayed when user saves without providing a name"
}, },
"styleMozillaFormatHeading": { "styleMozillaFormatHeading": {
"message": "Формат на Мозила", "message": "Формат на Мозила"
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"styleNotAppliedRegexpProblemTooltip": { "styleNotAppliedRegexpProblemTooltip": {
"message": "Стилът не е приложен поради неправилно използване на регулярни изрази", "message": "Стилът не е приложен поради неправилно използване на регулярни изрази"
"description": "Tooltip in the popup for styles that were not applied at all"
}, },
"styleRegexpInvalidExplanation": { "styleRegexpInvalidExplanation": {
"message": "Има правила на регулярни изрази, които не могат да бъдат компилирани.", "message": "Има правила на регулярни изрази, които не могат да бъдат компилирани."
"description": ""
}, },
"styleRegexpPartialExplanation": { "styleRegexpPartialExplanation": {
"message": "Стилът използва частично съвпадащи регулярни изрази и нарушава <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>Спецификацията @document</a>, която изисква пълно съвпадение на адреса. Засегнатите отдели не са приложени. Стилът вероятно е създаден в Stylish-for-Chrome, което неправилно проверява правилата на 'regexp()' още от първата версия (познат дефект).", "message": "Стилът използва частично съвпадащи регулярни изрази и нарушава <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>Спецификацията @document</a>, която изисква пълно съвпадение на адреса. Засегнатите отдели не са приложени. Стилът вероятно е създаден в Stylish-for-Chrome, което неправилно проверява правилата на 'regexp()' още от първата версия (познат дефект)."
"description": ""
}, },
"styleRegexpProblemTooltip": { "styleRegexpProblemTooltip": {
"message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази", "message": "Брой на неприложените отдели поради неправилно използване на регулярни изрази"
"description": "Tooltip in the popup for styles that were applied only partially"
},
"styleRegexpTestButton": {
"message": "Тест на регулярния израз",
"description": "RegExp test button label in the editor shown when applies-to list has a regexp value"
}, },
"styleRegexpTestFull": { "styleRegexpTestFull": {
"message": "Съвпадащи подпрозорци", "message": "Съвпадащи подпрозорци"
"description": "RegExp test report: label for the fully matching expressions"
}, },
"styleRegexpTestInvalid": { "styleRegexpTestInvalid": {
"message": "Неправилните регулярни изрази са пропуснати", "message": "Неправилните регулярни изрази са пропуснати"
"description": "RegExp test report: label for the invalid expressions"
}, },
"styleRegexpTestNone": { "styleRegexpTestNone": {
"message": "Няма съвпадащи подпрозорци", "message": "Няма съвпадащи подпрозорци"
"description": "RegExp test report: label for expressions that didn't match any tabs"
}, },
"styleRegexpTestPartial": { "styleRegexpTestPartial": {
"message": "Не съвпада напълно, затова е пропуснато", "message": "Не съвпада напълно, затова е пропуснато"
"description": "RegExp test report: label for the partially matching expressions"
}, },
"styleRegexpTestTitle": { "styleRegexpTestTitle": {
"message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)", "message": "Списък със съвпадащи отворени подпрозорци (щракнете на адреса, за да се фокусира на подпрозореца)"
"description": "RegExp test report: title of the report"
}, },
"styleSaveLabel": { "styleSaveLabel": {
"message": "Запазване", "message": "Запазване"
"description": "Label for save button for style editing"
}, },
"styleToMozillaFormatHelp": { "styleToMozillaFormatHelp": {
"message": "Форматът на Мозила може да се подаде в userstyles.org и да се използва със Стайлиш (Stylish)", "message": "Форматът на Мозила може да се подаде в userstyles.org и да се използва със Стайлиш (Stylish)"
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
}, },
"styleToMozillaFormatTitle": { "styleToMozillaFormatTitle": {
"message": "Стил във формат на Мозила", "message": "Стил във формат на Мозила"
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
}, },
"styleUpdate": { "styleUpdate": {
"message": "Сигурни ли сте, че искате да обновите '$stylename$'?", "message": "Сигурни ли сте, че искате да обновите '$stylename$'?",
"description": "Confirmation when updating a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -653,44 +496,34 @@
} }
}, },
"stylusUnavailableForURL": { "stylusUnavailableForURL": {
"message": "Разширението не работи на такива страници.", "message": "Разширението не работи на такива страници."
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"stylusUnavailableForURLdetails": { "stylusUnavailableForURLdetails": {
"message": "Като предпазна мярка, четецът забранява на разширенията да влияят на вградените страници (например chrome://version, about:addons, стандартната страница от Хром 61 и други), както и на страниците на други разширения. Достъпът до магазина с добавки на всеки четец също е ограничен.", "message": "Като предпазна мярка, четецът забранява на разширенията да влияят на вградените страници (например chrome://version, about:addons, стандартната страница от Хром 61 и други), както и на страниците на други разширения. Достъпът до магазина с добавки на всеки четец също е ограничен."
"description": "Sub-note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"toggleStyle": { "toggleStyle": {
"message": "Превключване на стила", "message": "Превключване на стила"
"description": "Label for the checkbox to enable/disable a style"
}, },
"undo": { "undo": {
"message": "Отмяна", "message": "Отмяна"
"description": "Button label"
}, },
"undoGlobal": { "undoGlobal": {
"message": "Отмяна във всички отдели", "message": "Отмяна във всички отдели"
"description": "CSS-beautify global Undo button label"
}, },
"unreachableContentScript": { "unreachableContentScript": {
"message": "Няма връзка със страницата. Презаредете подпрозореца.", "message": "Няма връзка със страницата. Презаредете подпрозореца."
"description": "Note in the toolbar popup usually on file:// URLs after [re]loading Stylus"
}, },
"unreachableFileHint": { "unreachableFileHint": {
"message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions.", "message": "Разширението ще има достъп до адреси от типа file:// само ако включите съответната отметка на страницата chrome://extensions."
"description": "Note in the toolbar popup for file:// URLs"
}, },
"updateAllCheckSucceededNoUpdate": { "updateAllCheckSucceededNoUpdate": {
"message": "Няма намерени обновления.", "message": "Няма намерени обновления."
"description": "Text that displays when an update all check completed and no updates are available"
}, },
"updateAllCheckSucceededSomeEdited": { "updateAllCheckSucceededSomeEdited": {
"message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани).", "message": "Някои от стиловете не са проверени, за да не се загубят местните редакции. Обновленията могат да бъдат принудени с индивидуална проверка или с пускането на още една проверка за всички (местните промени ще бъдат презаписани)."
"description": "Text that displays when an update all check completed and no updates are available"
}, },
"updateCheckFailBadResponseCode": { "updateCheckFailBadResponseCode": {
"message": "Неуспешно обновяване: сървърът отговори с код $code$.", "message": "Неуспешно обновяване: сървърът отговори с код $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": { "placeholders": {
"code": { "code": {
"content": "$1" "content": "$1"
@ -698,47 +531,36 @@
} }
}, },
"updateCheckFailServerUnreachable": { "updateCheckFailServerUnreachable": {
"message": "Неуспешно обновяване: няма връзка със сървъра.", "message": "Неуспешно обновяване: няма връзка със сървъра."
"description": "Text that displays when an update check failed because the update server is unreachable"
}, },
"updateCheckHistory": { "updateCheckHistory": {
"message": "Хронология на проверките", "message": "Хронология на проверките"
"description": ""
}, },
"updateCheckManualUpdateForce": { "updateCheckManualUpdateForce": {
"message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)", "message": "Инсталиране на обновлението (местните редакции ще бъдат презаписани)"
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
}, },
"updateCheckManualUpdateHint": { "updateCheckManualUpdateHint": {
"message": "Принудителното обновяване ще презапише местните редакции.", "message": "Принудителното обновяване ще презапише местните редакции."
"description": "Additional text displayed when an update check skipped updating the style to avoid losing local modifications"
}, },
"updateCheckSkippedLocallyEdited": { "updateCheckSkippedLocallyEdited": {
"message": "Стилът е бил местно редактиран.", "message": "Стилът е бил местно редактиран."
"description": "Text that displays when an update check skipped updating the style to avoid losing local modifications"
}, },
"updateCheckSkippedMaybeLocallyEdited": { "updateCheckSkippedMaybeLocallyEdited": {
"message": "Стилът може да е бил местно редактиран.", "message": "Стилът може да е бил местно редактиран."
"description": "Text that displays when an update check skipped updating the style to avoid losing possible local modifications"
}, },
"updateCheckSucceededNoUpdate": { "updateCheckSucceededNoUpdate": {
"message": "Стилът е обновен.", "message": "Стилът е обновен."
"description": "Text that displays when an update check completed and no update is available"
}, },
"updateCompleted": { "updateCompleted": {
"message": "Обновяването е завършено.", "message": "Обновяването е завършено."
"description": "Text that displays when an update completed"
}, },
"updatesCurrentlyInstalled": { "updatesCurrentlyInstalled": {
"message": "Инсталирани обновления:", "message": "Инсталирани обновления:"
"description": "Text that displays when an update is installed on options page. Followed by the number of currently installed updates."
}, },
"writeStyleFor": { "writeStyleFor": {
"message": "Писане на стил за: ", "message": "Писане на стил за: "
"description": "Label for toolbar pop-up that precedes the links to write a new style"
}, },
"writeStyleForURL": { "writeStyleForURL": {
"message": "този адрес", "message": "този адрес"
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
} }
} }

View File

@ -1,19 +1,15 @@
{ {
"addStyleLabel": { "addStyleLabel": {
"message": "Напиши нов стил", "message": "Напиши нов стил"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "Добави стил", "message": "Добави стил"
"description": "Title of the page for adding styles"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Добави", "message": "Добави"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Прилага се към: $applies$", "message": "Прилага се към: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,136 +17,103 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "и още", "message": "и още"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "URLи на домейна", "message": "URLи на домейна"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция.", "message": "Използвайте \"Прилага се към\", за да ограничете адресите, за които ще работи кодът в тази секция."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Прилага се към", "message": "Прилага се към"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "Адреси, съвпадащи с regexp", "message": "Адреси, съвпадащи с regexp"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Премахни", "message": "Премахни"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Уточни", "message": "Уточни"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Всички", "message": "Всички"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "URL започващи с", "message": "URL започващи с"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"applyAllUpdates": { "applyAllUpdates": {
"message": "Приложи всички промени", "message": "Приложи всички промени"
"description": "Label for the button to apply all detected updates"
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "Провери всички стилове за обновления", "message": "Провери всички стилове за обновления"
"description": "Label for the button to check all styles for updates"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Провери за обновление", "message": "Провери за обновление"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "Проверявам...", "message": "Проверявам..."
"description": "Text to display when checking a style for an update"
}, },
"cm_indentWithTabs": { "cm_indentWithTabs": {
"message": "Използвай табулация с умно отместване", "message": "Използвай табулация с умно отместване"
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
}, },
"cm_keyMap": { "cm_keyMap": {
"message": "Клавишни комбинации", "message": "Клавишни комбинации"
"description": "Label for the drop-down list controlling the keymap for the style editor."
}, },
"cm_lineWrapping": { "cm_lineWrapping": {
"message": "Автоматично пренасяне", "message": "Автоматично пренасяне"
"description": "Label for the checkbox controlling word wrap option for the style editor."
}, },
"cm_smartIndent": { "cm_smartIndent": {
"message": "Използвай умно отместване", "message": "Използвай умно отместване"
"description": "Label for the checkbox controlling smart indentation option for the style editor."
}, },
"cm_tabSize": { "cm_tabSize": {
"message": "Размер на табулацията", "message": "Размер на табулацията"
"description": "Label for the text box controlling tab size option for the style editor."
}, },
"cm_theme": { "cm_theme": {
"message": "Тема", "message": "Тема"
"description": "Label for the style editor's CSS theme."
}, },
"confirmNo": { "confirmNo": {
"message": "Не", "message": "Не"
"description": "'No' button in a confirm dialog"
}, },
"confirmStop": { "confirmStop": {
"message": "Спри", "message": "Спри"
"description": "'Stop' button in a confirm dialog"
}, },
"confirmYes": { "confirmYes": {
"message": "Да", "message": "Да"
"description": "'Yes' button in a confirm dialog"
}, },
"dbError": { "dbError": {
"message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?", "message": "Грешка в базата данни на Stylus. Желаеш ли да посетиш уебстраницата с възможни решения?"
"description": "Prompt when a DB error is encountered"
}, },
"defaultTheme": { "defaultTheme": {
"message": "по подразбиране", "message": "по подразбиране"
"description": "Default CodeMirror CSS theme option on the edit style page"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "Наистина ли искаш да изтриеш този стил?", "message": "Наистина ли искаш да изтриеш този стил?"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "Изтрий", "message": "Изтрий"
"description": "Label for the button to delete a style"
}, },
"description": { "description": {
"message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове.", "message": "Промени уеба със Stylus, мениджър на потребителски стилове. Stylus ти позволява лесно да инсталираш теми и скинове за много популярни сайтове."
"description": "Extension description"
}, },
"disableAllStyles": { "disableAllStyles": {
"message": "Изключи всички стилове", "message": "Изключи всички стилове"
"description": "Label for the checkbox that turns all enabled styles off."
}, },
"disableStyleLabel": { "disableStyleLabel": {
"message": "Забрани", "message": "Забрани"
"description": "Label for the button to disable a style"
}, },
"editGotoLine": { "editGotoLine": {
"message": "Иди на ред (или ред:кол)", "message": "Иди на ред (или ред:кол)"
"description": "Go to line or line:column on Ctrl-G in style code editor"
}, },
"editStyleHeading": { "editStyleHeading": {
"message": "Промени стила", "message": "Промени стила"
"description": "Title of the page for editing styles"
}, },
"editStyleLabel": { "editStyleLabel": {
"message": "Редактирай", "message": "Редактирай"
"description": "Label for the button to go to the edit style page"
}, },
"editStyleTitle": { "editStyleTitle": {
"message": "Редактирай стил $stylename$", "message": "Редактирай стил $stylename$",
"description": "Title of the page for editing styles",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -158,68 +121,52 @@
} }
}, },
"enableStyleLabel": { "enableStyleLabel": {
"message": "Разреши", "message": "Разреши"
"description": "Label for the button to enable a style"
}, },
"exportLabel": { "exportLabel": {
"message": "Експорт", "message": "Експорт"
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
}, },
"helpAlt": { "helpAlt": {
"message": "Помощ", "message": "Помощ"
"description": "Alternate text for help buttons"
}, },
"helpKeyMapCommand": { "helpKeyMapCommand": {
"message": "Напиши име на команда", "message": "Напиши име на команда"
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
}, },
"helpKeyMapHotkey": { "helpKeyMapHotkey": {
"message": "Натисни клавишна комбинация", "message": "Натисни клавишна комбинация"
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
}, },
"importAppendLabel": { "importAppendLabel": {
"message": "Добави към стил", "message": "Добави към стил"
"description": "Label for the button to import a style and append to the existing sections"
}, },
"importAppendTooltip": { "importAppendTooltip": {
"message": "Добави импортирания стил към текущия", "message": "Добави импортирания стил към текущия"
"description": "Tooltip for the button to import a style and append to the existing sections"
}, },
"importLabel": { "importLabel": {
"message": "Импорт", "message": "Импорт"
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
}, },
"importReplaceLabel": { "importReplaceLabel": {
"message": "Презапиши стила", "message": "Презапиши стила"
"description": "Label for the button to import and overwrite current style"
}, },
"importReplaceTooltip": { "importReplaceTooltip": {
"message": "Презапишете съдържанието на текущия стил с импортирания", "message": "Презапишете съдържанието на текущия стил с импортирания"
"description": "Label for the button to import and overwrite current style"
}, },
"installButton": { "installButton": {
"message": "Инсталирай стил", "message": "Инсталирай стил"
"description": "Label for install button"
}, },
"installButtonInstalled": { "installButtonInstalled": {
"message": "Стилът е инсталиран", "message": "Стилът е инсталиран"
"description": "Text displayed when the style is successfully installed"
}, },
"installButtonReinstall": { "installButtonReinstall": {
"message": "Преинсталирай стила", "message": "Преинсталирай стила"
"description": "Label for reinstall button"
}, },
"installButtonUpdate": { "installButtonUpdate": {
"message": "Обнови стила", "message": "Обнови стила"
"description": "Label for update button"
}, },
"installUpdate": { "installUpdate": {
"message": "Инсталирай обновление", "message": "Инсталирай обновление"
"description": "Label for the button to install an update for a single style"
}, },
"installUpdateFrom": { "installUpdateFrom": {
"message": "В момента стилът се обновява от $url$", "message": "В момента стилът се обновява от $url$",
"description": "Label to describe where the style gets update",
"placeholders": { "placeholders": {
"url": { "url": {
"content": "$1" "content": "$1"
@ -227,28 +174,22 @@
} }
}, },
"installUpdateFromLabel": { "installUpdateFromLabel": {
"message": "Провери за обновления", "message": "Провери за обновления"
"description": "Label for the checkbox to save current URL for update check"
}, },
"license": { "license": {
"message": "Лиценз", "message": "Лиценз"
"description": "Label for the license"
}, },
"linkGetHelp": { "linkGetHelp": {
"message": "Получете помощ", "message": "Получете помощ"
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
}, },
"linkGetStyles": { "linkGetStyles": {
"message": "Вземете стилове", "message": "Вземете стилове"
"description": "Help link text on the manage page e.g. https://userstyles.org"
}, },
"linkTranslate": { "linkTranslate": {
"message": "Преведете", "message": "Преведете"
"description": "Transifex link text on the manage page"
}, },
"linterCSSLintIncompatible": { "linterCSSLintIncompatible": {
"message": "CSSLint не поддържа $preprocessorname$ preprocessor", "message": "CSSLint не поддържа $preprocessorname$ preprocessor",
"description": "The label to display when the preprocessor isn't compatible with CSSLint",
"placeholders": { "placeholders": {
"preprocessorname": { "preprocessorname": {
"content": "$1" "content": "$1"
@ -256,12 +197,10 @@
} }
}, },
"linterCSSLintSettings": { "linterCSSLintSettings": {
"message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)", "message": "(Укажете правилата: 0 = забранен; 1 = предупреждения; 2 = грешки)"
"description": "CSSLint rule config values"
}, },
"linterConfigPopupTitle": { "linterConfigPopupTitle": {
"message": "Настройте конфигурация за $linter$ правила", "message": "Настройте конфигурация за $linter$ правила",
"description": "Stylelint or CSSLint popup header",
"placeholders": { "placeholders": {
"linter": { "linter": {
"content": "$1" "content": "$1"
@ -269,20 +208,16 @@
} }
}, },
"linterConfigTooltip": { "linterConfigTooltip": {
"message": "Щракнете, за да конфигурирате този linter", "message": "Щракнете, за да конфигурирате този linter"
"description": "Icon tooltip to indicate that it opens a popup with the selected linter configuration"
}, },
"linterInvalidConfigError": { "linterInvalidConfigError": {
"message": "Не е записано заради тези неправилни настройки", "message": "Не е записано заради тези неправилни настройки"
"description": "Invalid linter config will show a message followed by a list of invalid entries"
}, },
"linterIssues": { "linterIssues": {
"message": "Проблеми", "message": "Проблеми"
"description": "Label for the CSS linter issues block on the style edit page"
}, },
"linterIssuesHelp": { "linterIssuesHelp": {
"message": "Тези проблеми бяха намерени от $link$:", "message": "Тези проблеми бяха намерени от $link$:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
"placeholders": { "placeholders": {
"link": { "link": {
"content": "$1" "content": "$1"
@ -290,71 +225,54 @@
} }
}, },
"linterJSONError": { "linterJSONError": {
"message": "Невалиден JSON формат", "message": "Невалиден JSON формат"
"description": "Setting linter config with invalid JSON"
}, },
"linterResetMessage": { "linterResetMessage": {
"message": "За да върнете погрешно нулиране, натиснете Ctrl-Z (или Cmd-Z) в текстовия прозорец", "message": "За да върнете погрешно нулиране, натиснете Ctrl-Z (или Cmd-Z) в текстовия прозорец"
"description": "Reset button tooltip to inform user on how to undo an accidental reset"
}, },
"linterRulesLink": { "linterRulesLink": {
"message": "Вижте пълния списък с правила", "message": "Вижте пълния списък с правила"
"description": "Stylelint or CSSLint rules label added immediately before a link"
}, },
"liveReloadError": { "liveReloadError": {
"message": "Получи се грешка докато наблюдавахме файла", "message": "Получи се грешка докато наблюдавахме файла"
"description": "The label of live-reload error"
}, },
"liveReloadLabel": { "liveReloadLabel": {
"message": "Преглед на живо", "message": "Преглед на живо"
"description": "The label of live-reload feature"
}, },
"manageFilters": { "manageFilters": {
"message": "Филтри", "message": "Филтри"
"description": "Label for filters container"
}, },
"manageHeading": { "manageHeading": {
"message": "Инсталирани стилове", "message": "Инсталирани стилове"
"description": "Heading for the manage page"
}, },
"manageNewStyleAsUsercss": { "manageNewStyleAsUsercss": {
"message": "като Потребителскиcss", "message": "като Потребителскиcss"
"description": "VERY SHORT label for the checkbox next to the 'Write new style' button in the style manager"
}, },
"manageNewUI": { "manageNewUI": {
"message": "Нова подредба на UI", "message": "Нова подредба на UI"
"description": "Label for the checkbox that toggles the new UI on manage page"
}, },
"manageOnlyDisabled": { "manageOnlyDisabled": {
"message": "Само забранените стилове", "message": "Само забранените стилове"
"description": "Checkbox to show only disabled styles"
}, },
"manageOnlyEnabled": { "manageOnlyEnabled": {
"message": "Само разрешените стилове", "message": "Само разрешените стилове"
"description": "Checkbox to show only enabled styles"
}, },
"manageOnlyExternal": { "manageOnlyExternal": {
"message": "Само външните стилове", "message": "Само външните стилове"
"description": "Checkbox to show only externally installed styles i.e. updatable"
}, },
"manageOnlyLocal": { "manageOnlyLocal": {
"message": "Само локалните стилове", "message": "Само локалните стилове"
"description": "Checkbox to show only locally created styles i.e. non-updatable"
}, },
"manageOnlyLocalTooltip": { "manageOnlyLocalTooltip": {
"message": "(стиловете не инсталирани чрез страницата на userstyles.org)", "message": "(стиловете не инсталирани чрез страницата на userstyles.org)"
"description": "Tooltip for the checkbox to show only locally created styles i.e. non-updatable"
}, },
"manageOnlyNonUsercss": { "manageOnlyNonUsercss": {
"message": "Само не-Потребителскитеcss стилове", "message": "Само не-Потребителскитеcss стилове"
"description": "Checkbox to show only non-Usercss (standard) styles"
}, },
"manageOnlyUpdates": { "manageOnlyUpdates": {
"message": "Само с обновления или проблеми", "message": "Само с обновления или проблеми"
"description": "Checkbox to show only styles that have updates after check-all-styles-for-updates was performed"
}, },
"manageOnlyUsercss": { "manageOnlyUsercss": {
"message": "Само Потребителскиcss стилове", "message": "Само Потребителскиcss стилове"
"description": "Checkbox to show only Usercss styles"
} }
} }

View File

@ -1 +0,0 @@
{}

File diff suppressed because it is too large Load Diff

View File

@ -1,23 +1,18 @@
{ {
"addStyleLabel": { "addStyleLabel": {
"message": "Skriv ny stil", "message": "Skriv ny stil"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "Tilføj stil", "message": "Tilføj stil"
"description": "Title of the page for adding styles"
}, },
"alphaChannel": { "alphaChannel": {
"message": "Gennemsigtighed", "message": "Gennemsigtighed"
"description": "Label of color's opacity"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Tilføj", "message": "Tilføj"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Anvendes på: $applies$", "message": "Anvendes på: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -25,107 +20,81 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "og mere", "message": "og mere"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "URL'er på domænet", "message": "URL'er på domænet"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "Brug 'Anvendt på'-styring til at begrænse hvilke URL'er koden i denne sektion anvendes på.", "message": "Brug 'Anvendt på'-styring til at begrænse hvilke URL'er koden i denne sektion anvendes på."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Anvendes på", "message": "Anvendes på"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesLineWidgetLabel": { "appliesLineWidgetLabel": {
"message": "Vis 'Anvendes på'-info", "message": "Vis 'Anvendes på'-info"
"description": "Label for the checkbox to display applies-to information in the single editor"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "URL'er der matcher regexp'en", "message": "URL'er der matcher regexp'en"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Fjern", "message": "Fjern"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesRemoveError": { "appliesRemoveError": {
"message": "Kan ikke fjerne sidste 'Anvendes på'-optegnelse", "message": "Kan ikke fjerne sidste 'Anvendes på'-optegnelse"
"description": "Error displayed when the last 'applies' is going to be removed"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Specificér", "message": "Specificér"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Alt", "message": "Alt"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "URL'er der starter med", "message": "URL'er der starter med"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"applyAllUpdates": { "applyAllUpdates": {
"message": "Anvend alle opdateringer", "message": "Anvend alle opdateringer"
"description": "Label for the button to apply all detected updates"
}, },
"author": { "author": {
"message": "Forfatter", "message": "Forfatter"
"description": "Label for the style author"
},
"backupMessage": {
"message": "Vælg en fil eller træk og slip til denne side.",
"description": "Message for backup"
}, },
"bckpInstStyles": { "bckpInstStyles": {
"message": "Eksportér stil", "message": "Eksportér stil"
"description": ""
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "Tjek alle stiler for opdateringer", "message": "Tjek alle stiler for opdateringer"
"description": "Label for the button to check all styles for updates"
}, },
"checkAllUpdatesForce": { "checkAllUpdatesForce": {
"message": "Tjek igen, jeg redigerede ikke nogen stil!", "message": "Tjek igen, jeg redigerede ikke nogen stil!"
"description": "Label for the button to apply all detected updates"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Tjek efter opdatering", "message": "Tjek efter opdatering"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "Tjekker...", "message": "Tjekker..."
"description": "Text to display when checking a style for an update"
}, },
"clickToUninstall": { "clickToUninstall": {
"message": "Klik for at afinstallere", "message": "Klik for at afinstallere"
"description": "Label for the overlay on a style thumbnail when installed via inline search in the popup"
}, },
"cm_autoCloseBrackets": { "cm_autoCloseBrackets": {
"message": "Luk automatisk paranteser og citationstegn", "message": "Luk automatisk paranteser og citationstegn"
"description": "Label for the checkbox in the style editor."
}, },
"cm_autoCloseBracketsTooltip": { "cm_autoCloseBracketsTooltip": {
"message": "Tilføj automatisk et lukket par når man åbner en af ()[]{}''\"\"", "message": "Tilføj automatisk et lukket par når man åbner en af ()[]{}''\"\""
"description": "Label for the checkbox in the style editor."
}, },
"cm_autocompleteOnTyping": { "cm_autocompleteOnTyping": {
"message": "Autoudfyld på indtastning", "message": "Autoudfyld på indtastning"
"description": "Label for the checkbox in the style editor."
}, },
"cm_colorpicker": { "cm_colorpicker": {
"message": "Farvevælgere for CSS-farver", "message": "Farvevælgere for CSS-farver"
"description": "Label for the checkbox controlling colorpicker option for the style editor."
}, },
"cm_indentWithTabs": { "cm_indentWithTabs": {
"message": "Brug tabs med smart indrykning", "message": "Brug tabs med smart indrykning"
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
}, },
"cm_keyMap": { "cm_keyMap": {
"message": "Tastegenveje", "message": "Tastegenveje"
"description": "Label for the drop-down list controlling the keymap for the style editor." },
"genericAdd": {
"message": "Tilføj"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,21 @@
{ {
"InaccessibleFileHint": {
"message": "Το Stylus δεν έχει πρόσβαση σε κάποια αρχεία (π.χ. τα αρχεία PDF και JSON)"
},
"addStyleLabel": { "addStyleLabel": {
"message": "Γράψτε νέο στυλ", "message": "Γράψτε νέο στυλ"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "Προσθήκη στυλ", "message": "Προσθήκη στυλ"
"description": "Title of the page for adding styles" },
"alphaChannel": {
"message": "Αδιαφάνεια"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Προσθήκη", "message": "Προσθήκη"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Ισχύει για: $applies$", "message": "Ισχύει για: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,112 +23,228 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "και πολλά άλλα", "message": "και πολλά άλλα"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "URL στον τομέα", "message": "URL στον τομέα"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "Χρησιμοποιήστε το \"Ισχύει για\" έλεγχοι ώστε να περιοριστουν ποιες διευθύνσεις τον κώδικα σε αυτό το τμήμα να εφαρμόζονται.", "message": "Χρησιμοποιήστε το \"Ισχύει για\" έλεγχοι ώστε να περιοριστουν ποιες διευθύνσεις τον κώδικα σε αυτό το τμήμα να εφαρμόζονται."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Ισχύει για", "message": "Ισχύει για"
"description": "Label for 'applies to' fields on the edit/add screen" },
"appliesLineWidgetWarning": {
"message": "Δε λειτουργεί με minified CSS."
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "Διευθύνσεις URL που ταιριάζουν με την κανονική έκφραση", "message": "Διευθύνσεις URL που ταιριάζουν με την κανονική έκφραση"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Αφαίρεση", "message": "Αφαίρεση"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Καθορισμός", "message": "Καθορισμός"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Τα πάντα", "message": "Τα πάντα"
"description": "Text displayed for styles that apply to all sites" },
"appliesUrlOption": {
"message": "διεύθυνση URL"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "Διευθύνσεις URL που αρχίζουν με", "message": "Διευθύνσεις URL που αρχίζουν με"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"applyAllUpdates": { "applyAllUpdates": {
"message": "Εφαρμογή όλων των ενημερώσεων", "message": "Εφαρμογή όλων των ενημερώσεων"
"description": "Label for the button to apply all detected updates" },
"author": {
"message": "Συντάκτης"
},
"backupButtons": {
"message": "Δημιουργήστε αντίγραφο ασφαλείας"
},
"bckpInstStyles": {
"message": "Εξαγωγή στυλ"
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "Έλεγχος όλων των στυλ για ενημερώσεις", "message": "Έλεγχος όλων των στυλ για ενημερώσεις"
"description": "Label for the button to check all styles for updates" },
"checkAllUpdatesForce": {
"message": "Ελέγξτε πάλι, δεν επεξεργάστηκα κανένα στυλ!"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Έλεγχος για ενημερώσεις", "message": "Έλεγχος για ενημερώσεις"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "Έλεγχος...", "message": "Έλεγχος..."
"description": "Text to display when checking a style for an update" },
"clickToUninstall": {
"message": "Πατήστε για απεγκατάσταση"
},
"cm_autoCloseBrackets": {
"message": "Αυτόματο κλείσιμο παρενθέσεων και εισαγωγικών"
},
"cm_autocompleteOnTyping": {
"message": "Αυτόματη συμπλήρωση καθώς πληκτρολογείτε"
}, },
"cm_indentWithTabs": { "cm_indentWithTabs": {
"message": "Χρήση καρτελών με έξυπνη εσοχή", "message": "Χρήση καρτελών με έξυπνη εσοχή"
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor." },
"cm_keyMap": {
"message": "Συντομεύσεις πληκτρολογίου"
}, },
"cm_lineWrapping": { "cm_lineWrapping": {
"message": "Αναδίπλωση λέξεων", "message": "Αναδίπλωση λέξεων"
"description": "Label for the checkbox controlling word wrap option for the style editor." },
"cm_matchHighlight": {
"message": "Υπογράμμιση"
},
"cm_matchHighlightSelection": {
"message": "Μόνο επιλογή"
},
"cm_resizeGripHint": {
"message": "Διπλό κλικ για μεγιστοποίηση/επαναφορά ύψους"
}, },
"cm_smartIndent": { "cm_smartIndent": {
"message": "Χρήση έξυπνης εσοχής", "message": "Χρήση έξυπνης εσοχής"
"description": "Label for the checkbox controlling smart indentation option for the style editor."
}, },
"cm_tabSize": { "cm_tabSize": {
"message": "Μέγεθος καρτέλας", "message": "Μέγεθος καρτέλας"
"description": "Label for the text box controlling tab size option for the style editor." },
"cm_theme": {
"message": "Θέμα"
},
"configOnChange": {
"message": "στην αλλαγή"
},
"configOnChangeTooltip": {
"message": "Αυτόματη αποθήκευση και εφαρμογή αλλαγών"
},
"configureStyle": {
"message": "Ρυθμίσεις"
},
"configureStyleOnHomepage": {
"message": "Ρυθμίσεις στην ιστοσελίδα"
},
"confirmCancel": {
"message": "Άκυρο"
},
"confirmClose": {
"message": "Κλείσιμο"
},
"confirmDefault": {
"message": "Χρήση προεπιλογής"
},
"confirmDelete": {
"message": "Διαγραφή"
},
"confirmDiscardChanges": {
"message": "Απόρριψη αλλαγών;"
},
"confirmNo": {
"message": "Όχι"
},
"confirmOK": {
"message": "ΟΚ"
},
"confirmSave": {
"message": "Αποθήκευση"
},
"confirmYes": {
"message": "Ναι"
},
"connectingDropbox": {
"message": "Σύνδεση με το Dropbox..."
},
"connectingDropboxNotAllowed": {
"message": "Η σύνδεση με το Dropbox είναι διαθέσιμη μόνο σε εφαρμογές εγκατεστημένες απευθείας από το κατάστημα ιστού webstore"
},
"copied": {
"message": "Αντιγράφηκε στο πρόχειρο"
},
"copy": {
"message": "Αντιγραφή στο πρόχειρο"
},
"dateAbbrDay": {
"message": "$value$μ",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrHour": {
"message": "$value$ω",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrMonth": {
"message": "$value$λ",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateAbbrYear": {
"message": "$value$χ",
"placeholders": {
"value": {
"content": "$1"
}
}
},
"dateInstalled": {
"message": "Ημερομηνία εγκατάστασης"
},
"dateUpdated": {
"message": "Ημερομηνία ενημέρωσης"
}, },
"dbError": { "dbError": {
"message": "Παρουσιάστηκε σφάλμα χρησιμοποιώντας την κομψή βάση δεδομένων. Θα θέλατε να επισκεφθείτε μια ιστοσελίδα με πιθανές λύσεις;", "message": "Παρουσιάστηκε σφάλμα χρησιμοποιώντας την κομψή βάση δεδομένων. Θα θέλατε να επισκεφθείτε μια ιστοσελίδα με πιθανές λύσεις;"
"description": "Prompt when a DB error is encountered" },
"defaultTheme": {
"message": "προεπιλογή"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;", "message": "Είστε σίγουροι ότι θέλετε να διαγράψετε αυτό το στυλ;"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "Διαγραφή", "message": "Διαγραφή"
"description": "Label for the button to delete a style"
}, },
"description": { "description": {
"message": "Επαναπροσδιορίση του διαδίκτυου με το Stylus, έναν διαχειριστή στυλ. Το Stylus σας επιτρέπει να εγκαταστήσετε εύκολα themes και skins για πολλές δημοφιλείς ιστοσελίδες.", "message": "Επαναπροσδιορίση του διαδίκτυου με το Stylus, έναν διαχειριστή στυλ. Το Stylus σας επιτρέπει να εγκαταστήσετε εύκολα themes και skins για πολλές δημοφιλείς ιστοσελίδες."
"description": "Extension description"
}, },
"disableAllStyles": { "disableAllStyles": {
"message": "Απενεργοποιηση ολων των στυλ", "message": "Απενεργοποιηση ολων των στυλ"
"description": "Label for the checkbox that turns all enabled styles off."
}, },
"disableStyleLabel": { "disableStyleLabel": {
"message": "Απενεργοποίηση", "message": "Απενεργοποίηση"
"description": "Label for the button to disable a style" },
"dragDropMessage": {
"message": "Αποθέστε το αντίγραφο ασφαλείας σας οπουδήποτε σε αυτήν τη σελίδα για εισαγωγή."
},
"dragDropUsercssTabstrip": {
"message": "Για να εγκαταστήσετε το αρχείο, αποθέστε το στη λωρίδα καρτελών (την περιοχή όπου εμφανίζονται οι τίτλοι καρτελών)."
},
"editDeleteText": {
"message": "Διαγραφή"
}, },
"editGotoLine": { "editGotoLine": {
"message": "Μετάβαση στη γραμμή (ή line:col)", "message": "Μετάβαση στη γραμμή (ή line:col)"
"description": "Go to line or line:column on Ctrl-G in style code editor"
}, },
"editStyleHeading": { "editStyleHeading": {
"message": "Επεξεργασία Στυλ", "message": "Επεξεργασία Στυλ"
"description": "Title of the page for editing styles"
}, },
"editStyleLabel": { "editStyleLabel": {
"message": "Επεξεργασία", "message": "Επεξεργασία"
"description": "Label for the button to go to the edit style page"
}, },
"editStyleTitle": { "editStyleTitle": {
"message": "Επεξεργασία του στυλ $stylename$", "message": "Επεξεργασία του στυλ $stylename$",
"description": "Title of the page for editing styles",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -134,92 +252,393 @@
} }
}, },
"enableStyleLabel": { "enableStyleLabel": {
"message": "Ενεργοποίηση", "message": "Ενεργοποίηση"
"description": "Label for the button to enable a style"
}, },
"findStylesForSite": { "excludeStyleByDomainLabel": {
"message": "Αναζήτηση περισσότερων στυλ για αυτή την ιστοσελίδα", "message": "Εξαίρεση του τρέχοντος τομέα"
"description": "Text for a link that gets a list of styles for the current site" },
"excludeStyleByUrlLabel": {
"message": "Εξαίρεση του τρέχοντος URL"
},
"exportLabel": {
"message": "Εξαγωγή"
},
"exportSavedSuccess": {
"message": "Το αρχείο αποθηκεύτηκε επιτυχώς."
},
"externalFeedback": {
"message": "Σχόλια"
},
"externalHomepage": {
"message": "Αρχική σελίδα"
},
"externalLink": {
"message": "Εξωτερική σύνδεση"
},
"externalSupport": {
"message": "Υποστήριξη"
},
"externalUsercssDocument": {
"message": "Τεκμηρίωση για Usercss"
},
"filteredStyles": {
"message": "Βλέπετε $numShown$ από 2$numTotal$ συνολικά",
"placeholders": {
"numShown": {
"content": "$1"
},
"numTotal": {
"content": "$2"
}
}
},
"filteredStylesAllHidden": {
"message": "Τα φίλτρα που εφαρμόζονται αυτήν τη στιγμή δεν ταιριάζουν με κανένα στυλ"
},
"findStyles": {
"message": "Εύρεση στυλ"
},
"genericAdd": {
"message": "Προσθήκη"
},
"genericClone": {
"message": "Δημιουργία αντιγράφου"
},
"genericDisabledLabel": {
"message": "Απενεργοποιημένο"
},
"genericEnabledLabel": {
"message": "Ενεργοποιημένο"
},
"genericError": {
"message": "Σφάλμα"
},
"genericHistoryLabel": {
"message": "Ιστορικό"
},
"genericNext": {
"message": "Επόμενο"
},
"genericPrevious": {
"message": "Προηγούμενο"
},
"genericResetLabel": {
"message": "Επαναφορά"
},
"genericSavedMessage": {
"message": "Αποθηκεύτηκε"
},
"genericTitle": {
"message": "Τίτλος"
},
"genericUnknown": {
"message": "Άγνωστο"
},
"gettingStyles": {
"message": "Λήψη όλων των στυλ..."
}, },
"helpAlt": { "helpAlt": {
"message": "Βοήθεια", "message": "Βοήθεια"
"description": "Alternate text for help buttons" },
"helpKeyMapCommand": {
"message": "Πληκτρολογήστε μια εντολή"
},
"helpKeyMapHotkey": {
"message": "Πληκτρολογήστε ένα hotkey"
},
"importLabel": {
"message": "Εισαγωγή"
},
"importReplaceLabel": {
"message": "Αντικατάσταση στυλ"
},
"importReportLegendAdded": {
"message": "προστέθηκαν"
},
"importReportLegendUpdatedCode": {
"message": "ενημερωμένος κώδικας"
},
"importReportTitle": {
"message": "Η εισαγωγή στυλ τελείωσε"
},
"importReportUnchanged": {
"message": "Τίποτα δεν άλλαξε"
},
"importReportUndoneTitle": {
"message": "Η εισαγωγή έχει αναιρεθεί"
},
"installButton": {
"message": "Εγκατάσταση στυλ"
},
"installButtonInstalled": {
"message": "Το στυλ έχει εγκατασταθεί."
},
"installButtonReinstall": {
"message": "Επανεγκατάσταση στυλ"
},
"installButtonUpdate": {
"message": "Ενημέρωση στυλ"
}, },
"installUpdate": { "installUpdate": {
"message": "Εγκατάσταση ενημέρωσης", "message": "Εγκατάσταση ενημέρωσης"
"description": "Label for the button to install an update for a single style" },
"installUpdateFromLabel": {
"message": "Έλεγχος για ενημερώσεις"
},
"license": {
"message": "Άδεια χρήσης"
},
"linkGetHelp": {
"message": "Βοήθεια"
},
"linkGetStyles": {
"message": "Λήψη στυλ"
},
"linkTranslate": {
"message": "Μετάφραση"
},
"linterConfigTooltip": {
"message": "Πατήστε εδώ για να ρυθμίσετε το linter"
},
"linterIssues": {
"message": "Ζητήματα"
},
"linterJSONError": {
"message": "Μη έγκυρη μορφή JSON"
},
"linterResetMessage": {
"message": "Για αναίρεση μιας κατά λάθος επαναφοράς, πατήστε Ctrl-Z (ή Cmd-Z) στο πλαίσιο κειμένου"
}, },
"manageFilters": { "manageFilters": {
"message": "Φίλτρα", "message": "Φίλτρα"
"description": "Label for filters container"
}, },
"manageHeading": { "manageHeading": {
"message": "Εγκατεστημένα Στυλ", "message": "Εγκατεστημένα Στυλ"
"description": "Heading for the manage page" },
"manageNewUI": {
"message": "Νέα διαχείριση διάταξης UI"
},
"manageOnlyDisabled": {
"message": "Μόνο απενεργοποιημένα στυλ"
}, },
"manageOnlyEnabled": { "manageOnlyEnabled": {
"message": "Μόνο ενεργοποιημένα στυλ", "message": "Μόνο ενεργοποιημένα στυλ"
"description": "Checkbox to show only enabled styles" },
"manageOnlyExternal": {
"message": "Μόνο στυλ από άλλες ιστοσελίδες"
},
"manageOnlyLocal": {
"message": "Μόνο στυλ δημιουργημένα τοπικά"
}, },
"manageTitle": { "manageTitle": {
"message": "Κομψή", "message": "Κομψή"
"description": "Title for the manage page"
}, },
"menuShowBadge": { "menuShowBadge": {
"message": "Εμφάνιση ενεργους καταμέτρησης στυλ", "message": "Εμφάνιση ενεργους καταμέτρησης στυλ"
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text." },
"noFileToImport": {
"message": "Για να εισάγετε τα στυλ σας, πρέπει πρώτα να τα εξάγετε."
}, },
"noStylesForSite": { "noStylesForSite": {
"message": "Δεν υπάρχουν εγκατεστημένα στυλ για αυτή την ιστοσελίδα.", "message": "Δεν υπάρχουν εγκατεστημένα στυλ για αυτή την ιστοσελίδα."
"description": "Text displayed when no styles are installed for the current site"
}, },
"openManage": { "openManage": {
"message": "Διαχείριση εγκατεστημένων στυλ", "message": "Διαχείριση εγκατεστημένων στυλ"
"description": "Link to open the manage page." },
"openOptions": {
"message": "Επιλογές"
},
"openStylesManager": {
"message": "Άνοιγμα διαχείρισης στυλ"
},
"optionsActions": {
"message": "Ενέργειες"
},
"optionsAdvanced": {
"message": "Για προχωρημένους"
},
"optionsAdvancedContextDelete": {
"message": "Προσθήκη του 'Delete' στο μενού περιβάλλοντος του προγράμματος επεξεργασίας"
},
"optionsBadgeDisabled": {
"message": "Χρώμα φόντου όταν είναι απενεργοποιημένο"
},
"optionsBadgeNormal": {
"message": "Χρώμα υποβάθρου"
},
"optionsCheck": {
"message": "Ενημέρωση στυλ"
},
"optionsCheckUpdate": {
"message": "Έλεγχος και εγκατάσταση διαθέσιμων ενημερώσεων"
},
"optionsCustomizeBadge": {
"message": "Σήμα στο εικονίδιο της γραμμής εργαλείων"
},
"optionsCustomizePopup": {
"message": "Αναδυόμενο παράθυρο"
},
"optionsCustomizeUpdate": {
"message": "Ενημερώσεις"
}, },
"optionsHeading": { "optionsHeading": {
"message": "Επιλογές", "message": "Επιλογές"
"description": "Heading for options section on manage page." },
"optionsIconDark": {
"message": "Σκούρο θέμα φυλλομετρητή"
},
"optionsOpen": {
"message": "Άνοιγμα"
},
"optionsOpenManager": {
"message": "Διαχείριση στυλ"
},
"optionsPopupWidth": {
"message": "Πλάτος αναδυόμενου παραθύρου (σε pixels)"
},
"optionsReset": {
"message": "Επαναφορά ρυθμίσεων στις προεπιλεγμένες"
},
"optionsResetButton": {
"message": "Επαναφορά επιλογών"
},
"optionsSubheading": {
"message": "Περισσότερες επιλογές"
},
"optionsSyncConnect": {
"message": "Σύνδεση"
},
"optionsSyncDisconnect": {
"message": "Αποσύνδεση"
},
"optionsSyncStatusConnected": {
"message": "Συνδεδεμένο"
},
"optionsSyncStatusConnecting": {
"message": "Σύνδεση..."
},
"optionsSyncStatusDisconnected": {
"message": "Αποσυνδέθηκε"
},
"optionsSyncStatusDisconnecting": {
"message": "Αποσύνδεση..."
},
"optionsSyncStatusSyncing": {
"message": "Συγχρονισμός ..."
},
"optionsSyncSyncNow": {
"message": "Συγχρονισμός τώρα"
},
"optionsSyncUrl": {
"message": "διεύθυνση URL"
},
"optionsUpdateInterval": {
"message": "Διάστημα αυτόματης ενημέρωσης των στυλ σε ώρες (0 για απενεργοποίηση)"
},
"paginationNext": {
"message": "Επόμενη σελίδα"
},
"paginationPrevious": {
"message": "Προηγούμενη σελίδα"
},
"popupBordersTooltip": {
"message": "Χρήσιμο για σκούρα θέματα στο καινούριο Chrome, καθώς δε βάφει πλέον τα ακριανά περιθώρια."
},
"popupOpenEditInPopup": {
"message": "Χρήση ενός απλού παραθύρου (χωρίς omnibox)"
},
"popupOpenEditInWindow": {
"message": "Άνοιγμα επεξαργαστή σε νέο παράθυρο"
}, },
"popupStylesFirst": { "popupStylesFirst": {
"message": "Στυλ λίστας πριν των εντολών στο μενού του κουμπιού γραμμής εργαλείων", "message": "Στυλ λίστας πριν των εντολών στο μενού του κουμπιού γραμμής εργαλείων"
"description": "Label for the checkbox controlling section order in the popup."
}, },
"prefShowBadge": { "prefShowBadge": {
"message": "Εμφάνιση αριθμού των στυλ που δραστηριοποιούνται για την τρέχουσα τοποθεσία στην μπάρα εργαλείων", "message": "Εμφάνιση αριθμού των στυλ που δραστηριοποιούνται για την τρέχουσα τοποθεσία στην μπάρα εργαλείων"
"description": "Label for the checkbox controlling toolbar badge text." },
"readingStyles": {
"message": "Ανάγνωση στυλ..."
},
"replace": {
"message": "Αντικατάσταση"
},
"replaceAll": {
"message": "Αντικατάσταση όλων"
},
"replaceWith": {
"message": "Αντικατάσταση με"
},
"retrieveBckp": {
"message": "Εισαγωγή στυλ"
},
"retrieveDropboxSync": {
"message": "Εισαγωγή από το Dropbox"
},
"search": {
"message": "Αναζήτηση"
},
"searchGlobalStyles": {
"message": "Επίσης, αναζητήστε καθολικά στυλ"
},
"searchRegexp": {
"message": "Χρησιμοποιήστε τη σύνταξη /re/ για αναζήτηση με regexp."
},
"searchResultInstallCount": {
"message": "Συνολικός αριθμός εγκαταστάσεων"
},
"searchResultUpdated": {
"message": "Ενημερωμένο"
},
"searchResultWeeklyCount": {
"message": "Εβδομαδιαίος αριθμός εγκαταστάσεων"
},
"searchStylesName": {
"message": "Όνομα"
}, },
"sectionAdd": { "sectionAdd": {
"message": "Προσθήκη ένος άλλου τμήματος", "message": "Προσθήκη ένος άλλου τμήματος"
"description": "Label for the button to add a section"
}, },
"sectionCode": { "sectionCode": {
"message": "Κώδικας", "message": "Κώδικας"
"description": "Label for the code for a section"
}, },
"sectionRemove": { "sectionRemove": {
"message": "Αφαίρεση ενότητας", "message": "Αφαίρεση ενότητας"
"description": "Label for the button to remove a section" },
"sections": {
"message": "Ενότητες"
},
"shortcuts": {
"message": "Συντομεύσεις"
},
"sortDateNewestFirst": {
"message": "πιο πρόσφατα πρώτα"
},
"sortDateOldestFirst": {
"message": "πιο παλιά πρώτα"
}, },
"styleBadRegexp": { "styleBadRegexp": {
"message": "Το Regexp δεν είναι έγκυρο.", "message": "Το Regexp δεν είναι έγκυρο."
"description": "Validation message for a bad regexp in a style" },
"styleBeautify": {
"message": "Ωραιοποίηση"
},
"styleBeautifyIndentConditional": {
"message": "Διόρθωση εσοχής για @media και @supports"
},
"styleBeautifyPreserveNewlines": {
"message": "Διατήρηση νέων γραμμών (newlines)"
}, },
"styleCancelEditLabel": { "styleCancelEditLabel": {
"message": "Πίσω στη διαχείριση", "message": "Πίσω στη διαχείριση"
"description": "Label for cancel button for style editing"
}, },
"styleChangesNotSaved": { "styleChangesNotSaved": {
"message": "Έχετε κάνει αλλαγές σε αυτό το ύφος χωρίς αποθήκευση.", "message": "Έχετε κάνει αλλαγές σε αυτό το ύφος χωρίς αποθήκευση."
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
}, },
"styleEnabledLabel": { "styleEnabledLabel": {
"message": "Ενεργοποιημένη", "message": "Ενεργοποιημένη"
"description": "Label for the enabled state of styles"
}, },
"styleInstall": { "styleInstall": {
"message": "Εγκατάσταση του '$stylename$' στο Stylus;", "message": "Εγκατάσταση του '$stylename$' στο Stylus;",
"description": "Confirmation when installing a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -227,20 +646,19 @@
} }
}, },
"styleMissingName": { "styleMissingName": {
"message": "Εισάγετε ένα όνομα", "message": "Εισάγετε ένα όνομα"
"description": "Error displayed when user saves without providing a name" },
"styleRegexpTestNone": {
"message": "Δε βρέθηκαν καρτέλες που αντιστοιχούν."
}, },
"styleSaveLabel": { "styleSaveLabel": {
"message": "Αποθήκευση", "message": "Αποθήκευση"
"description": "Label for save button for style editing"
}, },
"styleToMozillaFormatHelp": { "styleToMozillaFormatHelp": {
"message": "Η μορφή του Mozilla κώδικα μπορεί να χρησιμοποιηθεί με το Stylish για το Firefox και μπορεί να υποβληθεί στο userstyles.org.", "message": "Η μορφή του Mozilla κώδικα μπορεί να χρησιμοποιηθεί με το Stylish για το Firefox και μπορεί να υποβληθεί στο userstyles.org."
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
}, },
"styleUpdate": { "styleUpdate": {
"message": "Είστε σίγουροι ότι θέλετε να ενημερώσετε το '$stylename$';", "message": "Είστε σίγουροι ότι θέλετε να ενημερώσετε το '$stylename$';",
"description": "Confirmation when updating a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -248,16 +666,40 @@
} }
}, },
"stylusUnavailableForURL": { "stylusUnavailableForURL": {
"message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή.", "message": "To Stylus δεν λειτουργεί σε σελίδες όπως αυτή."
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect" },
"stylusUnavailableForURLdetails": {
"message": "Ως μέτρο ασφαλείας, ο φυλλομετρητής απαγορεύει στα πρόσθετα να επέμβουν στις built-in σελίδες του (όπως π.χ. chrome://version, η σελίδα νέας καρτέλας από το Chrome 61 και μετά, about:addons, κλπ.), καθώς και τις σελίδες άλλωων προσθέτων. Επιπλέον, κάθε φυλλομετρητής περιορίζει την πρόσβαση στο κατάστημα προσθέτων (όπως το Chrome Web Store ή το AMO)."
},
"syncDropboxStyles": {
"message": "Εξαγωγή από το Dropbox"
},
"syncError": {
"message": "Ο συγχρονισμός απέτυχε"
},
"toggleStyle": {
"message": "Αλλαγή στυλ"
},
"undo": {
"message": "Αναίρεση"
},
"undoGlobal": {
"message": "Αναίρεση όλων των ενεργειών"
},
"unreachableFileHint": {
"message": "Το Stylus έχει πρόσβαση στις file:// διευθύνσεις URL μόνο αν έχετε επιλέξει το αντίστοιχο πλαίσιο ελέγχου για το πρόσθετο Stylus στη σελίδα chrome://extensions."
},
"unzipStyles": {
"message": "Αποσυμπίεση στυλ..."
}, },
"updateAllCheckSucceededNoUpdate": { "updateAllCheckSucceededNoUpdate": {
"message": "Όλα τα στυλ είναι ενημερωμένα.", "message": "Όλα τα στυλ είναι ενημερωμένα."
"description": "Text that displays when an update all check completed and no updates are available" },
"updateAllCheckSucceededSomeEdited": {
"message": "Δεν έχει γίνει έλεγχος ενημερώσεων για κάποια στυλ, για να αποφευχθεί η πιθανότητα απώλειας τοπικών επεξεργασιών. Οι ενημερώσεις μπορούν να εξαναγκαστούν ελέγχοντας το κάθε στυλ ξεχωριστά ή ελέγχοντας πάλι όλα τα στυλ (τοπικές επεξεργασίες θα αντικατασταθούν)"
}, },
"updateCheckFailBadResponseCode": { "updateCheckFailBadResponseCode": {
"message": "Αποτυχία ενημέρωσης: ο διακομιστής ανταποκρίθηκε με κωδικό $code$.", "message": "Αποτυχία ενημέρωσης: ο διακομιστής ανταποκρίθηκε με κωδικό $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": { "placeholders": {
"code": { "code": {
"content": "$1" "content": "$1"
@ -265,23 +707,39 @@
} }
}, },
"updateCheckFailServerUnreachable": { "updateCheckFailServerUnreachable": {
"message": "Αποτυχία ενημέρωσης: απρόσιτος διακομιστής.", "message": "Αποτυχία ενημέρωσης: απρόσιτος διακομιστής."
"description": "Text that displays when an update check failed because the update server is unreachable" },
"updateCheckSkippedLocallyEdited": {
"message": "Το στυλ επεξεργάστηκε τοπικά στον υπολογιστή σας."
},
"updateCheckSkippedMaybeLocallyEdited": {
"message": "Το στυλ αυτό μπορεί να έχει επεξεργαστεί τοπικά στον υπολογιστή σας."
}, },
"updateCheckSucceededNoUpdate": { "updateCheckSucceededNoUpdate": {
"message": "Το στυλ είναι ενημερωμένο.", "message": "Το στυλ είναι ενημερωμένο."
"description": "Text that displays when an update check completed and no update is available"
}, },
"updateCompleted": { "updateCompleted": {
"message": "Η ενημέρωση ολοκληρώθηκε.", "message": "Η ενημέρωση ολοκληρώθηκε."
"description": "Text that displays when an update completed" },
"updatesCurrentlyInstalled": {
"message": "Ενημερώσεις που εγκαταστάθηκαν"
},
"uploadingFile": {
"message": "Μεταφόρτωση αρχείου..."
},
"usercssEditorNamePlaceholder": {
"message": "Καθορίστε το @name στον κώδικα"
},
"versionInvalidOlder": {
"message": "Η έκδοση αυτή είναι παλαιότερη από αυτήν που είναι ήδη εγκατεστημένη."
}, },
"writeStyleFor": { "writeStyleFor": {
"message": "Γράψτε νέο στυλ για:", "message": "Γράψτε νέο στυλ για:"
"description": "Label for toolbar pop-up that precedes the links to write a new style"
}, },
"writeStyleForURL": { "writeStyleForURL": {
"message": "αυτή την διεύθυνση URL", "message": "αυτή την διεύθυνση URL"
"description": "Text for link in toolbar pop-up to write a new style for the current URL" },
"zipStyles": {
"message": "Συμπίεση στυλ..."
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,67 +1,51 @@
{ {
"appliesRemoveError": { "appliesRemoveError": {
"message": "Cannot remove last 'applies to' entry", "message": "Cannot remove last 'applies to' entry"
"description": "Error displayed when the last 'applies' is going to be removed"
}, },
"checkAllUpdatesForce": { "checkAllUpdatesForce": {
"message": "Check again—I didn't edit any styles!", "message": "Check again—I didn't edit any styles!"
"description": "Label for the button to apply all detected updates"
}, },
"cm_autoCloseBrackets": { "cm_autoCloseBrackets": {
"message": "Auto-close brackets and quotes", "message": "Auto-close brackets and quotes"
"description": "Label for the checkbox in the style editor."
}, },
"cm_colorpicker": { "cm_colorpicker": {
"message": "Colour pickers for CSS colours", "message": "Colour pickers for CSS colours"
"description": "Label for the checkbox controlling colorpicker option for the style editor."
}, },
"cm_resizeGripHint": { "cm_resizeGripHint": {
"message": "Double-click to maximise/restore the height", "message": "Double-click to maximise/restore the height"
"description": "Tooltip for the resize grip in style editor"
}, },
"colorpickerTooltip": { "colorpickerTooltip": {
"message": "Open colour picker", "message": "Open colour picker"
"description": "Tooltip for the colored squares shown before CSS colors in the style editor."
}, },
"description": { "description": {
"message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites.", "message": "Redesign the web with Stylus, a user-style manager. Stylus allows you to easily install themes and skins for many popular sites."
"description": "Extension description"
}, },
"editGotoLine": { "editGotoLine": {
"message": "Go to line (or line:col)", "message": "Go to line (or line:col)"
"description": "Go to line or line:column on Ctrl-G in style code editor"
}, },
"editStyleHeading": { "editStyleHeading": {
"message": "Edit style", "message": "Edit style"
"description": "Title of the page for editing styles"
}, },
"license": { "license": {
"message": "Licence", "message": "Licence"
"description": "Label for the license"
}, },
"manageFaviconsGray": { "manageFaviconsGray": {
"message": "Greyed out", "message": "Greyed out"
"description": "Label for the checkbox that toggles grayed out mode of applies-to favicons in the new UI on manage page"
}, },
"optionsBadgeDisabled": { "optionsBadgeDisabled": {
"message": "Background colour when disabled", "message": "Background colour when disabled"
"description": ""
}, },
"optionsBadgeNormal": { "optionsBadgeNormal": {
"message": "Background colour", "message": "Background colour"
"description": ""
}, },
"optionsUpdateImportNote": { "optionsUpdateImportNote": {
"message": "When importing style backups from an old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated.", "message": "When importing style backups from an old version or from Stylish, do a one-time check for updates manually in the styles manager to ensure all styles are updated."
"description": ""
}, },
"optionsUpdateInterval": { "optionsUpdateInterval": {
"message": "Userstyle auto-update interval in hours (specify 0 to disable)", "message": "Userstyle auto-update interval in hours (specify 0 to disable)"
"description": ""
}, },
"styleInstallFailed": { "styleInstallFailed": {
"message": "Failed to install userstyle\n$error$", "message": "Failed to install userstyle\n$error$",
"description": "Warning when installation failed",
"placeholders": { "placeholders": {
"error": { "error": {
"content": "$1" "content": "$1"
@ -69,15 +53,12 @@
} }
}, },
"styleRegexpPartialExplanation": { "styleRegexpPartialExplanation": {
"message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome, which incorrectly checks 'regexp()' rules since the very first version (known bug).", "message": "This style uses partially matching regexps in violation of <a href='https://developer.mozilla.org/docs/Web/CSS/@document'>CSS4 @document specification</a> which requires a full URL match. The affected CSS sections were not applied to the page. This style was probably created in Stylish-for-Chrome, which incorrectly checks 'regexp()' rules since the very first version (known bug)."
"description": ""
}, },
"styleUpdateDiscardChanges": { "styleUpdateDiscardChanges": {
"message": "The style has been changed outside the editor. Would you like to reload the style?", "message": "The style has been changed outside the editor. Would you like to reload the style?"
"description": "Confirmation to update the style in the editor"
}, },
"usercssConfigIncomplete": { "usercssConfigIncomplete": {
"message": "The style was updated or deleted after the configuration dialogue was shown. These variables were not saved to avoid corrupting the style's metadata:", "message": "The style was updated or deleted after the configuration dialogue was shown. These variables were not saved to avoid corrupting the style's metadata:"
"description": ""
} }
} }

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,19 +1,15 @@
{ {
"addStyleLabel": { "addStyleLabel": {
"message": "Uusi Tyyli", "message": "Uusi Tyyli"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "Lisää Tyyli", "message": "Lisää Tyyli"
"description": "Title of the page for adding styles"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Lisää", "message": "Lisää"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Kooskee: $applies$", "message": "Kooskee: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,80 +17,70 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "ja lisää", "message": "ja lisää"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "URL ositteita domainilla", "message": "URL ositteita domainilla"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee.", "message": "Käytä 'Koskee' kontrolleja rajoittaaksesi mitä URL osoitteisiin tämä osio koodista koskee."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Koskee", "message": "Koskee"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "URL ositteet jotka vastaavat regexpiä", "message": "URL ositteet jotka vastaavat regexpiä"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Poista", "message": "Poista"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Tarkenna", "message": "Tarkenna"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Kaikki", "message": "Kaikki"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "URL osoitteet jotka alkavat", "message": "URL osoitteet jotka alkavat"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "Tarkista kaikki tyylit päivityksien varalta", "message": "Tarkista kaikki tyylit päivityksien varalta"
"description": "Label for the button to check all styles for updates"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Hae päivityksiä", "message": "Hae päivityksiä"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "Tarkistetaan...", "message": "Tarkistetaan..."
"description": "Text to display when checking a style for an update" },
"confirmDelete": {
"message": "Poista"
},
"confirmSave": {
"message": "Tallenna"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "Oletko varma että haluat poistaa tämän tyylin?", "message": "Oletko varma että haluat poistaa tämän tyylin?"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "Poista", "message": "Poista"
"description": "Label for the button to delete a style"
}, },
"description": { "description": {
"message": "Uudelleen stailaa netti Stylusillä, käyttäjän tyyli hallintapaneelilla. Stylus antaa sinun helposti asentaa teemoja ja skinejä palvelluille kuten Google, Facebook, YouTube, Orkut, ja monelle, monelle muulle sivustolle.", "message": "Uudelleen stailaa netti Stylusillä, käyttäjän tyyli hallintapaneelilla. Stylus antaa sinun helposti asentaa teemoja ja skinejä palvelluille kuten Google, Facebook, YouTube, Orkut, ja monelle, monelle muulle sivustolle."
"description": "Extension description"
}, },
"disableStyleLabel": { "disableStyleLabel": {
"message": "Poista Käytöstä", "message": "Poista Käytöstä"
"description": "Label for the button to disable a style" },
"editDeleteText": {
"message": "Poista"
}, },
"editStyleHeading": { "editStyleHeading": {
"message": "Muokkaa Tyyliä", "message": "Muokkaa Tyyliä"
"description": "Title of the page for editing styles"
}, },
"editStyleLabel": { "editStyleLabel": {
"message": "Muokkaa", "message": "Muokkaa"
"description": "Label for the button to go to the edit style page"
}, },
"editStyleTitle": { "editStyleTitle": {
"message": "Muokkaa Tyyliä $stylename$", "message": "Muokkaa Tyyliä $stylename$",
"description": "Title of the page for editing styles",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -102,76 +88,64 @@
} }
}, },
"enableStyleLabel": { "enableStyleLabel": {
"message": "Aktivoi", "message": "Aktivoi"
"description": "Label for the button to enable a style"
}, },
"findStylesForSite": { "genericAdd": {
"message": "Hae lisää tyylejä tälle sivustolle", "message": "Lisää"
"description": "Text for a link that gets a list of styles for the current site" },
"genericEnabledLabel": {
"message": "Aktivoitu"
}, },
"helpAlt": { "helpAlt": {
"message": "Apu", "message": "Apu"
"description": "Alternate text for help buttons"
}, },
"installUpdate": { "installUpdate": {
"message": "Asenna päivitys", "message": "Asenna päivitys"
"description": "Label for the button to install an update for a single style"
}, },
"manageHeading": { "manageHeading": {
"message": "Asennetut Tyylit", "message": "Asennetut Tyylit"
"description": "Heading for the manage page"
}, },
"manageTitle": { "manageTitle": {
"message": "Tyylikäs", "message": "Tyylikäs"
"description": "Title for the manage page"
}, },
"noStylesForSite": { "noStylesForSite": {
"message": "Ei asennettuja tyylejä tällä sivustolla.", "message": "Ei asennettuja tyylejä tällä sivustolla."
"description": "Text displayed when no styles are installed for the current site"
}, },
"openManage": { "openManage": {
"message": "Hallitse asennettuja tyylejä", "message": "Hallitse asennettuja tyylejä"
"description": "Link to open the manage page."
}, },
"popupStylesFirst": { "popupStylesFirst": {
"message": "List styles before commands in the toolbar button menu", "message": "List styles before commands in the toolbar button menu"
"description": "Label for the checkbox controlling section order in the popup."
}, },
"prefShowBadge": { "prefShowBadge": {
"message": "Show number of styles active for the current site on the toolbar button", "message": "Show number of styles active for the current site on the toolbar button"
"description": "Label for the checkbox controlling toolbar badge text."
}, },
"sectionAdd": { "sectionAdd": {
"message": "Lisää uusi osio", "message": "Lisää uusi osio"
"description": "Label for the button to add a section"
}, },
"sectionCode": { "sectionCode": {
"message": "Koodi", "message": "Koodi"
"description": "Label for the code for a section"
}, },
"sectionRemove": { "sectionRemove": {
"message": "Poista osio", "message": "Poista osio"
"description": "Label for the button to remove a section" },
"sections": {
"message": "Osiot"
}, },
"styleBadRegexp": { "styleBadRegexp": {
"message": "Regexp ei kelpaa.", "message": "Regexp ei kelpaa."
"description": "Validation message for a bad regexp in a style"
}, },
"styleCancelEditLabel": { "styleCancelEditLabel": {
"message": "Takaisin hallintapaneeliin", "message": "Takaisin hallintapaneeliin"
"description": "Label for cancel button for style editing"
}, },
"styleChangesNotSaved": { "styleChangesNotSaved": {
"message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta.", "message": "Olet tehnyt muutoksia tähän tyyliin tallentamatta."
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
}, },
"styleEnabledLabel": { "styleEnabledLabel": {
"message": "Aktivoitu", "message": "Aktivoitu"
"description": "Label for the enabled state of styles"
}, },
"styleInstall": { "styleInstall": {
"message": "Asennetaanko '$stylename$' Stylusiin?", "message": "Asennetaanko '$stylename$' Stylusiin?",
"description": "Confirmation when installing a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -179,24 +153,19 @@
} }
}, },
"styleMissingName": { "styleMissingName": {
"message": "Syötä nimi", "message": "Syötä nimi"
"description": "Error displayed when user saves without providing a name"
}, },
"styleSaveLabel": { "styleSaveLabel": {
"message": "Tallenna", "message": "Tallenna"
"description": "Label for save button for style editing"
}, },
"styleToMozillaFormatHelp": { "styleToMozillaFormatHelp": {
"message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin.", "message": "Mozilla formaattia koodista voidaan käyttää Stylish Firefoxille ohjelmassa ja voidaan lähettää userstyles.orgiin."
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
}, },
"updateAllCheckSucceededNoUpdate": { "updateAllCheckSucceededNoUpdate": {
"message": "All styles are up to date.", "message": "All styles are up to date."
"description": "Text that displays when an update all check completed and no updates are available"
}, },
"updateCheckFailBadResponseCode": { "updateCheckFailBadResponseCode": {
"message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.", "message": "Päivitys epäonnistui: palvelin vastasi koodilla $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": { "placeholders": {
"code": { "code": {
"content": "$1" "content": "$1"
@ -204,15 +173,12 @@
} }
}, },
"updateCheckFailServerUnreachable": { "updateCheckFailServerUnreachable": {
"message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen.", "message": "Päivitys epäonnistui: ei voitu yhdistää palvelimeen."
"description": "Text that displays when an update check failed because the update server is unreachable"
}, },
"updateCheckSucceededNoUpdate": { "updateCheckSucceededNoUpdate": {
"message": "Tyyli on ajan tasalla.", "message": "Tyyli on ajan tasalla."
"description": "Text that displays when an update check completed and no update is available"
}, },
"updateCompleted": { "updateCompleted": {
"message": "Päivitys suoritettu.", "message": "Päivitys suoritettu."
"description": "Text that displays when an update completed"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,19 +1,15 @@
{ {
"addStyleLabel": { "addStyleLabel": {
"message": "Nije styl skriuwe", "message": "Nije styl skriuwe"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "Styl tafoegje", "message": "Styl tafoegje"
"description": "Title of the page for adding styles"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Tafoegje", "message": "Tafoegje"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Fan tapassing op: $applies$", "message": "Fan tapassing op: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,107 +17,81 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "en mear", "message": "en mear"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "URLs op it domein", "message": "URLs op it domein"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "Brûk de Fan tapassing op-funksjes om de URLs foar de koade yn dizze seksje te beheinen.", "message": "Brûk de Fan tapassing op-funksjes om de URLs foar de koade yn dizze seksje te beheinen."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Fan tapassing op", "message": "Fan tapassing op"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "URLs oerienkommend mei de regexp", "message": "URLs oerienkommend mei de regexp"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Fuortsmite", "message": "Fuortsmite"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Spesifisearje", "message": "Spesifisearje"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Alles", "message": "Alles"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "URLs begjinnend mei", "message": "URLs begjinnend mei"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"applyAllUpdates": { "applyAllUpdates": {
"message": "Alle fernijingen tapasse", "message": "Alle fernijingen tapasse"
"description": "Label for the button to apply all detected updates"
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "Alle stilen kontrolearje op fernijingen", "message": "Alle stilen kontrolearje op fernijingen"
"description": "Label for the button to check all styles for updates"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Kontrolearje op fernijing", "message": "Kontrolearje op fernijing"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "Kontrolearje...", "message": "Kontrolearje..."
"description": "Text to display when checking a style for an update"
}, },
"cm_indentWithTabs": { "cm_indentWithTabs": {
"message": "Ljepblêden mei tûke ynspringing brûke", "message": "Ljepblêden mei tûke ynspringing brûke"
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
}, },
"cm_keyMap": { "cm_keyMap": {
"message": "Toetseboerdyndieling", "message": "Toetseboerdyndieling"
"description": "Label for the drop-down list controlling the keymap for the style editor."
}, },
"cm_lineWrapping": { "cm_lineWrapping": {
"message": "Teksttebekrin", "message": "Teksttebekrin"
"description": "Label for the checkbox controlling word wrap option for the style editor."
}, },
"cm_smartIndent": { "cm_smartIndent": {
"message": "Tûke ynspringing brûke", "message": "Tûke ynspringing brûke"
"description": "Label for the checkbox controlling smart indentation option for the style editor."
}, },
"cm_tabSize": { "cm_tabSize": {
"message": "Ljepblêdgrutte", "message": "Ljepblêdgrutte"
"description": "Label for the text box controlling tab size option for the style editor."
}, },
"cm_theme": { "cm_theme": {
"message": "Tema", "message": "Tema"
"description": "Label for the style editor's CSS theme."
}, },
"confirmNo": { "confirmNo": {
"message": "Nee", "message": "Nee"
"description": "'No' button in a confirm dialog"
}, },
"confirmStop": { "confirmStop": {
"message": "Stoppe", "message": "Stoppe"
"description": "'Stop' button in a confirm dialog"
}, },
"confirmYes": { "confirmYes": {
"message": "Ja", "message": "Ja"
"description": "'Yes' button in a confirm dialog"
}, },
"dbError": { "dbError": {
"message": "Der is in flater bard by it brûken fan de Stylus-database. Wolle jo in webside mei mooglike oplossingen besykje?", "message": "Der is in flater bard by it brûken fan de Stylus-database. Wolle jo in webside mei mooglike oplossingen besykje?"
"description": "Prompt when a DB error is encountered"
}, },
"defaultTheme": { "defaultTheme": {
"message": "standert", "message": "standert"
"description": "Default CodeMirror CSS theme option on the edit style page"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "Binne jo wis dat jo dizze styl fuortsmite wolle?", "message": "Binne jo wis dat jo dizze styl fuortsmite wolle?"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "Fuortsmite", "message": "Fuortsmite"
"description": "Label for the button to delete a style"
} }
} }

View File

@ -1,19 +1,15 @@
{ {
"addStyleTitle": { "addStyleTitle": {
"message": "Engadir Estilo", "message": "Engadir Estilo"
"description": "Title of the page for adding styles"
}, },
"alphaChannel": { "alphaChannel": {
"message": "Opacidade", "message": "Opacidade"
"description": "Label of color's opacity"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Engadir", "message": "Engadir"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Aplica a: $applies$", "message": "Aplica a: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,47 +17,36 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "e mais", "message": "e mais"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "URLs no dominio", "message": "URLs no dominio"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Aplica para", "message": "Aplica para"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesLineWidgetWarning": { "appliesLineWidgetWarning": {
"message": "Non funciona con CSS minificado", "message": "Non funciona con CSS minificado"
"description": "A warning that applies-to information won't show properly with minified CSS"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "URLs que concorden co regexp", "message": "URLs que concorden co regexp"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Suprimir", "message": "Suprimir"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Especificar", "message": "Especificar"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Todo", "message": "Todo"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "URLs que comecen por", "message": "URLs que comecen por"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"applyAllUpdates": { "applyAllUpdates": {
"message": "Aplicar tódalas actualizacións", "message": "Aplicar tódalas actualizacións"
"description": "Label for the button to apply all detected updates"
}, },
"author": { "author": {
"message": "Autor", "message": "Autor"
"description": "Label for the style author"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1 +0,0 @@
{}

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

View File

@ -1,19 +1,15 @@
{ {
"addStyleLabel": { "addStyleLabel": {
"message": "Упиши нови стил", "message": "Упиши нови стил"
"description": "Label for the button to go to the add style page"
}, },
"addStyleTitle": { "addStyleTitle": {
"message": "Додај стил", "message": "Додај стил"
"description": "Title of the page for adding styles"
}, },
"appliesAdd": { "appliesAdd": {
"message": "Додај", "message": "Додај"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "Примењује се на: $applies$", "message": "Примењује се на: $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -21,140 +17,115 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "и још", "message": "и још"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesDomainOption": { "appliesDomainOption": {
"message": "УРЛ адресе на домену", "message": "УРЛ адресе на домену"
"description": "Option to make the style apply to the entered string as a domain"
}, },
"appliesHelp": { "appliesHelp": {
"message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује.", "message": "Употреба 'Примењује се на' одређује опсег УРЛ адреса на које се код у овом одељку примењује."
"description": "Help text for 'applies to' section"
}, },
"appliesLabel": { "appliesLabel": {
"message": "Примењује се на", "message": "Примењује се на"
"description": "Label for 'applies to' fields on the edit/add screen"
}, },
"appliesRegexpOption": { "appliesRegexpOption": {
"message": "УРЛ адресе које одговарају регуларном изразу", "message": "УРЛ адресе које одговарају регуларном изразу"
"description": "Option to make the style apply to the entered string as a regular expression"
}, },
"appliesRemove": { "appliesRemove": {
"message": "Уклони", "message": "Уклони"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesSpecify": { "appliesSpecify": {
"message": "Детаљније", "message": "Детаљније"
"description": "Label for the button to make a style apply only to specific sites"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "Све", "message": "Све"
"description": "Text displayed for styles that apply to all sites"
}, },
"appliesUrlOption": { "appliesUrlOption": {
"message": "УРЛ", "message": "УРЛ"
"description": "Option to make the style apply to the entered string as a URL"
}, },
"appliesUrlPrefixOption": { "appliesUrlPrefixOption": {
"message": "УРЛ адресе које почињу са", "message": "УРЛ адресе које почињу са"
"description": "Option to make the style apply to the entered string as a URL prefix"
}, },
"applyAllUpdates": { "applyAllUpdates": {
"message": "Примени сва ажурирања", "message": "Примени сва ажурирања"
"description": "Label for the button to apply all detected updates"
}, },
"checkAllUpdates": { "checkAllUpdates": {
"message": "Проверите ажурирања за све стилове", "message": "Проверите ажурирања за све стилове"
"description": "Label for the button to check all styles for updates"
}, },
"checkForUpdate": { "checkForUpdate": {
"message": "Проверите ажурирање", "message": "Проверите ажурирање"
"description": "Label for the button to check a single style for an update"
}, },
"checkingForUpdate": { "checkingForUpdate": {
"message": "Проверавање...", "message": "Проверавање..."
"description": "Text to display when checking a style for an update"
}, },
"cm_indentWithTabs": { "cm_indentWithTabs": {
"message": "Користи картице са паметним увлачењем редова", "message": "Користи картице са паметним увлачењем редова"
"description": "Label for the checkbox controlling tabs with smart indentation option for the style editor."
}, },
"cm_keyMap": { "cm_keyMap": {
"message": "Мапа тастера", "message": "Мапа тастера"
"description": "Label for the drop-down list controlling the keymap for the style editor."
}, },
"cm_lineWrapping": { "cm_lineWrapping": {
"message": "Преламање текста", "message": "Преламање текста"
"description": "Label for the checkbox controlling word wrap option for the style editor."
}, },
"cm_smartIndent": { "cm_smartIndent": {
"message": "Користи паметно увлачење редова", "message": "Користи паметно увлачење редова"
"description": "Label for the checkbox controlling smart indentation option for the style editor."
}, },
"cm_tabSize": { "cm_tabSize": {
"message": "Величина картице", "message": "Величина картице"
"description": "Label for the text box controlling tab size option for the style editor."
}, },
"cm_theme": { "cm_theme": {
"message": "Тема", "message": "Тема"
"description": "Label for the style editor's CSS theme." },
"confirmDelete": {
"message": "Избриши"
}, },
"confirmNo": { "confirmNo": {
"message": "Не", "message": "Не"
"description": "'No' button in a confirm dialog" },
"confirmSave": {
"message": "Сачувај"
}, },
"confirmStop": { "confirmStop": {
"message": "Заустави", "message": "Заустави"
"description": "'Stop' button in a confirm dialog"
}, },
"confirmYes": { "confirmYes": {
"message": "Да", "message": "Да"
"description": "'Yes' button in a confirm dialog"
}, },
"dbError": { "dbError": {
"message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?", "message": "Дошло је до грешке користећи Stylus базу података. Да ли желите да посетите веб страницу са могућим решењима?"
"description": "Prompt when a DB error is encountered"
}, },
"defaultTheme": { "defaultTheme": {
"message": "подразумевано", "message": "подразумевано"
"description": "Default CodeMirror CSS theme option on the edit style page"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "Да ли сте сигурни да желите да избришете овај стил?", "message": "Да ли сте сигурни да желите да избришете овај стил?"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "Избриши", "message": "Избриши"
"description": "Label for the button to delete a style"
}, },
"description": { "description": {
"message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове.", "message": "Измените стил интернет мреже управљачем корисничких стилова. Stylus вам омогућава да лако инсталирате теме и скинове за многе популарне сајтове."
"description": "Extension description"
}, },
"disableAllStyles": { "disableAllStyles": {
"message": "Искључи све стилове", "message": "Искључи све стилове"
"description": "Label for the checkbox that turns all enabled styles off."
}, },
"disableStyleLabel": { "disableStyleLabel": {
"message": "Онемогући", "message": "Онемогући"
"description": "Label for the button to disable a style" },
"editDeleteText": {
"message": "Избриши"
}, },
"editGotoLine": { "editGotoLine": {
"message": "Иди на ред (или line:col)", "message": "Иди на ред (или line:col)"
"description": "Go to line or line:column on Ctrl-G in style code editor"
}, },
"editStyleHeading": { "editStyleHeading": {
"message": "Уреди стил", "message": "Уреди стил"
"description": "Title of the page for editing styles"
}, },
"editStyleLabel": { "editStyleLabel": {
"message": "Уреди", "message": "Уреди"
"description": "Label for the button to go to the edit style page"
}, },
"editStyleTitle": { "editStyleTitle": {
"message": "Уреди стил $stylename$", "message": "Уреди стил $stylename$",
"description": "Title of the page for editing styles",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -162,68 +133,55 @@
} }
}, },
"enableStyleLabel": { "enableStyleLabel": {
"message": "Омогући", "message": "Омогући"
"description": "Label for the button to enable a style"
}, },
"exportLabel": { "exportLabel": {
"message": "Извези", "message": "Извези"
"description": "Label for the button to export a style ('edit' page) or all styles ('manage' page)"
}, },
"findStylesForSite": { "genericAdd": {
"message": "Пронађи још стилова за овај сајт", "message": "Додај"
"description": "Text for a link that gets a list of styles for the current site" },
"genericEnabledLabel": {
"message": "Омогућено"
}, },
"helpAlt": { "helpAlt": {
"message": "Помоћ", "message": "Помоћ"
"description": "Alternate text for help buttons"
}, },
"helpKeyMapCommand": { "helpKeyMapCommand": {
"message": "Укуцај име команде", "message": "Укуцај име команде"
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
}, },
"helpKeyMapHotkey": { "helpKeyMapHotkey": {
"message": "Притисни пречицу", "message": "Притисни пречицу"
"description": "Placeholder text of inputbox in keymap help popup on the edit style page. Must be very short"
}, },
"importAppendLabel": { "importAppendLabel": {
"message": "Додај стилу", "message": "Додај стилу"
"description": "Label for the button to import a style and append to the existing sections"
}, },
"importAppendTooltip": { "importAppendTooltip": {
"message": "Додај увезени стил тренутном стилу", "message": "Додај увезени стил тренутном стилу"
"description": "Tooltip for the button to import a style and append to the existing sections"
}, },
"importLabel": { "importLabel": {
"message": "Увези", "message": "Увези"
"description": "Label for the button to import a style ('edit' page) or all styles ('manage' page)"
}, },
"importReplaceLabel": { "importReplaceLabel": {
"message": "Упиши преко стила", "message": "Упиши преко стила"
"description": "Label for the button to import and overwrite current style"
}, },
"importReplaceTooltip": { "importReplaceTooltip": {
"message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил", "message": "Одбаци садржај тренутног стила и упиши преко њега увезени стил"
"description": "Label for the button to import and overwrite current style"
}, },
"installUpdate": { "installUpdate": {
"message": "Инсталирај ажурирање", "message": "Инсталирај ажурирање"
"description": "Label for the button to install an update for a single style"
}, },
"linkGetHelp": { "linkGetHelp": {
"message": "Помоћ", "message": "Помоћ"
"description": "Homepage link text on the manage page e.g. https://add0n.com/stylus.html#features with chat/FAQ/intro/info"
}, },
"linkGetStyles": { "linkGetStyles": {
"message": "Преузмите стилове", "message": "Преузмите стилове"
"description": "Help link text on the manage page e.g. https://userstyles.org"
}, },
"linterIssues": { "linterIssues": {
"message": "Проблеми", "message": "Проблеми"
"description": "Label for the CSS linter issues block on the style edit page"
}, },
"linterIssuesHelp": { "linterIssuesHelp": {
"message": "Проблем пронађен од стране $link$:", "message": "Проблем пронађен од стране $link$:",
"description": "Help popup message for the selected CSS linter issues block on the style edit page",
"placeholders": { "placeholders": {
"link": { "link": {
"content": "$1" "content": "$1"
@ -231,104 +189,85 @@
} }
}, },
"manageFilters": { "manageFilters": {
"message": "Филтери", "message": "Филтери"
"description": "Label for filters container"
}, },
"manageHeading": { "manageHeading": {
"message": "Инсталирани стилови", "message": "Инсталирани стилови"
"description": "Heading for the manage page"
}, },
"manageOnlyEnabled": { "manageOnlyEnabled": {
"message": "Само омогућени стилови", "message": "Само омогућени стилови"
"description": "Checkbox to show only enabled styles"
}, },
"menuShowBadge": { "menuShowBadge": {
"message": "Прикажи број активних стилова", "message": "Прикажи број активних стилова"
"description": "Label (must be very short) for the checkbox in the toolbar button context menu controlling toolbar badge text."
}, },
"noStylesForSite": { "noStylesForSite": {
"message": "Нема инсталираних стилова за овај сајт.", "message": "Нема инсталираних стилова за овај сајт."
"description": "Text displayed when no styles are installed for the current site"
}, },
"openManage": { "openManage": {
"message": "Управљај инсталираним стиловима", "message": "Управљај инсталираним стиловима"
"description": "Link to open the manage page." },
"openOptions": {
"message": "Опције"
}, },
"optionsHeading": { "optionsHeading": {
"message": "Опције", "message": "Опције"
"description": "Heading for options section on manage page." },
"optionsSyncUrl": {
"message": "УРЛ"
}, },
"popupStylesFirst": { "popupStylesFirst": {
"message": "Излистај стилове пре команди у менију дугмета на алатној траци", "message": "Излистај стилове пре команди у менију дугмета на алатној траци"
"description": "Label for the checkbox controlling section order in the popup."
}, },
"prefShowBadge": { "prefShowBadge": {
"message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци", "message": "Прикажи број активних стилова за тренутни сајт на дугмету на алатној траци"
"description": "Label for the checkbox controlling toolbar badge text."
}, },
"replace": { "replace": {
"message": "Замени", "message": "Замени"
"description": "Label before the replace input field in the editor shown on Ctrl-H"
}, },
"replaceAll": { "replaceAll": {
"message": "Замени све", "message": "Замени све"
"description": "Label before the replace input field in the editor shown on 'replaceAll' hotkey"
}, },
"replaceWith": { "replaceWith": {
"message": "Замени са", "message": "Замени са"
"description": "Label before the replace-with input field in the editor shown on Ctrl-H etc."
}, },
"search": { "search": {
"message": "Претражи", "message": "Претражи"
"description": "Label before the search input field in the editor shown on Ctrl-F"
}, },
"searchRegexp": { "searchRegexp": {
"message": "Користи /re/ синтаксу за претрагу регуларним изразом", "message": "Користи /re/ синтаксу за претрагу регуларним изразом"
"description": "Label after the search input field in the editor shown on Ctrl-F"
},
"searchStyles": {
"message": "Претражи садржај",
"description": "Label for the search filter textbox on the Manage styles page"
}, },
"sectionAdd": { "sectionAdd": {
"message": "Додај нови одељак", "message": "Додај нови одељак"
"description": "Label for the button to add a section"
}, },
"sectionCode": { "sectionCode": {
"message": "Код", "message": "Код"
"description": "Label for the code for a section"
}, },
"sectionRemove": { "sectionRemove": {
"message": "Уклони одељак", "message": "Уклони одељак"
"description": "Label for the button to remove a section" },
"sections": {
"message": "Одељци"
}, },
"styleBadRegexp": { "styleBadRegexp": {
"message": "Регуларни израз је неисправан.", "message": "Регуларни израз је неисправан."
"description": "Validation message for a bad regexp in a style"
}, },
"styleBeautify": { "styleBeautify": {
"message": "Улепшај", "message": "Улепшај"
"description": "Label for the CSS-beautifier button on the edit style page"
}, },
"styleCancelEditLabel": { "styleCancelEditLabel": {
"message": "Назад на управљање", "message": "Назад на управљање"
"description": "Label for cancel button for style editing"
}, },
"styleChangesNotSaved": { "styleChangesNotSaved": {
"message": "Направили сте измене овог стила које нисте сачували.", "message": "Направили сте измене овог стила које нисте сачували."
"description": "Text for the prompt when changes are made to a style and the user tries to leave without saving"
}, },
"styleEnabledLabel": { "styleEnabledLabel": {
"message": "Омогућено", "message": "Омогућено"
"description": "Label for the enabled state of styles"
}, },
"styleFromMozillaFormatPrompt": { "styleFromMozillaFormatPrompt": {
"message": "Налепи код у Mozilla формату", "message": "Налепи код у Mozilla формату"
"description": "Prompt in the dialog displayed after clicking 'Import from Mozilla format' button"
}, },
"styleInstall": { "styleInstall": {
"message": "Инсталирати '$stylename$' у Stylus?", "message": "Инсталирати '$stylename$' у Stylus?",
"description": "Confirmation when installing a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -336,28 +275,22 @@
} }
}, },
"styleMissingName": { "styleMissingName": {
"message": "Унесите назив", "message": "Унесите назив"
"description": "Error displayed when user saves without providing a name"
}, },
"styleMozillaFormatHeading": { "styleMozillaFormatHeading": {
"message": "Mozilla формат", "message": "Mozilla формат"
"description": "Heading for the section with buttons to import/export Mozilla format of the style"
}, },
"styleSaveLabel": { "styleSaveLabel": {
"message": "Сачувај", "message": "Сачувај"
"description": "Label for save button for style editing"
}, },
"styleToMozillaFormatHelp": { "styleToMozillaFormatHelp": {
"message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org.", "message": "Mozilla формат кода се може користити у Stylish за Firefox и може се послати на userstyles.org."
"description": "Help info for the Mozilla format header section that converts the code to/from Mozilla format"
}, },
"styleToMozillaFormatTitle": { "styleToMozillaFormatTitle": {
"message": "Стил у Mozilla формату", "message": "Стил у Mozilla формату"
"description": "Title of the popup with the style code in Mozilla format, shown after pressing the Export button on Edit style page"
}, },
"styleUpdate": { "styleUpdate": {
"message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?", "message": "Да ли сте сигурни да желите да ажурирате '$stylename$'?",
"description": "Confirmation when updating a style",
"placeholders": { "placeholders": {
"stylename": { "stylename": {
"content": "$1" "content": "$1"
@ -365,24 +298,19 @@
} }
}, },
"stylusUnavailableForURL": { "stylusUnavailableForURL": {
"message": "Stylus не ради на страницама као што је ова.", "message": "Stylus не ради на страницама као што је ова."
"description": "Note in the toolbar pop-up when on a URL Stylus can't affect"
}, },
"undo": { "undo": {
"message": "Опозови", "message": "Опозови"
"description": "Button label"
}, },
"undoGlobal": { "undoGlobal": {
"message": "Опозови (свеобухватно)", "message": "Опозови (свеобухватно)"
"description": "CSS-beautify global Undo button label"
}, },
"updateAllCheckSucceededNoUpdate": { "updateAllCheckSucceededNoUpdate": {
"message": "Сви стилови су ажурирани.", "message": "Сви стилови су ажурирани."
"description": "Text that displays when an update all check completed and no updates are available"
}, },
"updateCheckFailBadResponseCode": { "updateCheckFailBadResponseCode": {
"message": "Ажурирање није успело: сервер је одговорио кодом $code$.", "message": "Ажурирање није успело: сервер је одговорио кодом $code$.",
"description": "Text that displays when an update check failed because the response code indicates an error",
"placeholders": { "placeholders": {
"code": { "code": {
"content": "$1" "content": "$1"
@ -390,23 +318,18 @@
} }
}, },
"updateCheckFailServerUnreachable": { "updateCheckFailServerUnreachable": {
"message": "Ажурирање није успело: сервер није доступан.", "message": "Ажурирање није успело: сервер није доступан."
"description": "Text that displays when an update check failed because the update server is unreachable"
}, },
"updateCheckSucceededNoUpdate": { "updateCheckSucceededNoUpdate": {
"message": "Стил је ажуриран.", "message": "Стил је ажуриран."
"description": "Text that displays when an update check completed and no update is available"
}, },
"updateCompleted": { "updateCompleted": {
"message": "Ажурирање је комплетирано.", "message": "Ажурирање је комплетирано."
"description": "Text that displays when an update completed"
}, },
"writeStyleFor": { "writeStyleFor": {
"message": "Упиши стил за:", "message": "Упиши стил за:"
"description": "Label for toolbar pop-up that precedes the links to write a new style"
}, },
"writeStyleForURL": { "writeStyleForURL": {
"message": "ову УРЛ адресу", "message": "ову УРЛ адресу"
"description": "Text for link in toolbar pop-up to write a new style for the current URL"
} }
} }

File diff suppressed because it is too large Load Diff

View File

@ -1,15 +1,12 @@
{ {
"addStyleLabel": { "addStyleLabel": {
"message": "క్రొత్త స్టైల్ వ్రాయండి", "message": "క్రొత్త స్టైల్ వ్రాయండి"
"description": "Label for the button to go to the add style page"
}, },
"appliesAdd": { "appliesAdd": {
"message": "చేర్చు", "message": "చేర్చు"
"description": "Label for the button to add an 'applies' entry"
}, },
"appliesDisplay": { "appliesDisplay": {
"message": "వేటికి వర్తిస్తుంది; $applies$", "message": "వేటికి వర్తిస్తుంది; $applies$",
"description": "Text on the manage screen to describe what the style applies to",
"placeholders": { "placeholders": {
"applies": { "applies": {
"content": "$1" "content": "$1"
@ -17,51 +14,54 @@
} }
}, },
"appliesDisplayTruncatedSuffix": { "appliesDisplayTruncatedSuffix": {
"message": "ఇంకా మరిన్ని", "message": "ఇంకా మరిన్ని"
"description": "Text added to appliesDisplay when there are more sites for the style than are displayed"
}, },
"appliesRemove": { "appliesRemove": {
"message": "తొలగించు", "message": "తొలగించు"
"description": "Label for the button to remove an 'applies' entry"
}, },
"appliesToEverything": { "appliesToEverything": {
"message": "అన్నిటికీ", "message": "అన్నిటికీ"
"description": "Text displayed for styles that apply to all sites" },
"confirmDelete": {
"message": "తొలగించు"
},
"confirmSave": {
"message": "భద్రపరచు"
}, },
"deleteStyleConfirm": { "deleteStyleConfirm": {
"message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?", "message": "మీరు నజంగానే ఈ శైలిని తొలగించాలనుకుంటున్నారా?"
"description": "Confirmation before deleting a style"
}, },
"deleteStyleLabel": { "deleteStyleLabel": {
"message": "తొలగించు", "message": "తొలగించు"
"description": "Label for the button to delete a style"
}, },
"disableStyleLabel": { "disableStyleLabel": {
"message": "అచేతనించు", "message": "అచేతనించు"
"description": "Label for the button to disable a style" },
"editDeleteText": {
"message": "తొలగించు"
}, },
"editStyleLabel": { "editStyleLabel": {
"message": "మార్చు", "message": "మార్చు"
"description": "Label for the button to go to the edit style page"
}, },
"enableStyleLabel": { "enableStyleLabel": {
"message": "చేతనించు", "message": "చేతనించు"
"description": "Label for the button to enable a style" },
"genericAdd": {
"message": "చేర్చు"
}, },
"helpAlt": { "helpAlt": {
"message": "సహాయం", "message": "సహాయం"
"description": "Alternate text for help buttons"
}, },
"manageHeading": { "manageHeading": {
"message": "స్థాపిత శైలులు", "message": "స్థాపిత శైలులు"
"description": "Heading for the manage page"
}, },
"manageTitle": { "manageTitle": {
"message": "స్టైలిష్", "message": "స్టైలిష్"
"description": "Title for the manage page" },
"sections": {
"message": "విభాగాలు"
}, },
"styleSaveLabel": { "styleSaveLabel": {
"message": "భద్రపరచు", "message": "భద్రపరచు"
"description": "Label for save button for style editing"
} }
} }

File diff suppressed because it is too large Load Diff

285
_locales/uk/messages.json Normal file
View File

@ -0,0 +1,285 @@
{
"InaccessibleFileHint": {
"message": "Stylus не може отримати доступ до деяких типів файлів (наприклад, файли PDF і JSON)."
},
"addStyleLabel": {
"message": "Написати новий стиль"
},
"addStyleTitle": {
"message": "Додати стиль"
},
"alphaChannel": {
"message": "Прозорість"
},
"appliesAdd": {
"message": "Додати"
},
"appliesDisplay": {
"message": "Застосувати до $applies$",
"placeholders": {
"applies": {
"content": "$1"
}
}
},
"appliesDisplayTruncatedSuffix": {
"message": "та інші"
},
"appliesDomainOption": {
"message": "URL в домені"
},
"appliesHelp": {
"message": "Щоб вказати, до яких URL відноситься код в цьому розділі, скористайтеся параметром \"Застосувати до\"."
},
"appliesLabel": {
"message": "Застосувати до"
},
"appliesLineWidgetLabel": {
"message": "Показати цільові сайти секцій"
},
"appliesRemove": {
"message": "Видалити"
},
"appliesRemoveError": {
"message": "Не можна видалити останній елемент"
},
"appliesSpecify": {
"message": "Вказати"
},
"appliesToEverything": {
"message": "Все"
},
"appliesUrlPrefixOption": {
"message": "URL, що починаються з "
},
"applyAllUpdates": {
"message": "Застосувати всі оновлення"
},
"author": {
"message": "Автор"
},
"backupButtons": {
"message": "Резервне копіювання"
},
"bckpInstStyles": {
"message": "Експорт стилів"
},
"checkingForUpdate": {
"message": "Перевірка ... "
},
"clickToUninstall": {
"message": "Натисніть, щоб видалити "
},
"cm_selectByTokensTooltip": {
"message": "Приклади токенов: .foo-бар-2 #aabbcc 0,32 !important\nЯкщо вимкнено: вибираються слова, розділені розділовими знаками."
},
"confirmCancel": {
"message": "Скасувати "
},
"confirmDelete": {
"message": "Видалити "
},
"confirmNo": {
"message": "Ні"
},
"confirmSave": {
"message": "Зберегти"
},
"confirmStop": {
"message": "Зупинити"
},
"confirmYes": {
"message": "Так"
},
"deleteStyleLabel": {
"message": "Видалити"
},
"disableStyleLabel": {
"message": "Вимкнути"
},
"editDeleteText": {
"message": "Видалити"
},
"findStyles": {
"message": "Знайти стилі"
},
"genericAdd": {
"message": "Додати"
},
"genericDescription": {
"message": "Опис"
},
"genericDisabledLabel": {
"message": "Відключено"
},
"genericEnabledLabel": {
"message": "Увімкнено"
},
"genericError": {
"message": "Помилка"
},
"genericHistoryLabel": {
"message": "Історія"
},
"genericNext": {
"message": "Наступний "
},
"genericPrevious": {
"message": "Попередній"
},
"genericResetLabel": {
"message": "Скинути"
},
"genericSavedMessage": {
"message": "Збережено"
},
"genericTitle": {
"message": "Ім'я"
},
"genericUnknown": {
"message": "Невідомо"
},
"gettingStyles": {
"message": "Отримання всіх стилів ..."
},
"helpAlt": {
"message": "Довідка"
},
"helpKeyMapCommand": {
"message": "Введіть ім'я команди"
},
"helpKeyMapHotkey": {
"message": "Натисніть клавішу"
},
"hostDisabled": {
"message": "Цей хост вимкнено через помилку в поточній версії браузера, що використовується"
},
"importLabel": {
"message": "Імпорт"
},
"importReplaceLabel": {
"message": "Замінити стиль"
},
"importReportLegendIdentical": {
"message": "ідентичні пропущені"
},
"importReportLegendInvalid": {
"message": "недісних пропущено"
},
"importReportTitle": {
"message": "Імпорт стилів закінчено"
},
"installUpdate": {
"message": "Встановити оновлення"
},
"manageFavicons": {
"message": "Піктограми для цільових сайтів"
},
"manageFilters": {
"message": "Фільтри"
},
"manageHeading": {
"message": "Встановити Styles"
},
"manageNewUI": {
"message": "Новий макет інтерфейсу управління користувача"
},
"meta_unknownJSONLiteral": {
"message": "Невірний JSON: $literal$не є дійсним літералом JSON",
"placeholders": {
"literal": {
"content": "$1"
}
}
},
"openManage": {
"message": "Керування"
},
"openOptions": {
"message": "Налаштування"
},
"optionsHeading": {
"message": "Налаштування"
},
"optionsReset": {
"message": "Скидання налаштувань до значень за замовчуванням"
},
"optionsResetButton": {
"message": "Скинути параметри"
},
"optionsSubheading": {
"message": "Додатково"
},
"optionsSyncLogin": {
"message": "Логін"
},
"optionsSyncStatusRelogin": {
"message": "Сеанс закінчився, будь ласка, увійдіть ще раз."
},
"paginationNext": {
"message": "Наступна сторінка"
},
"paginationPrevious": {
"message": "Попередня сторінка"
},
"replace": {
"message": "Замінити"
},
"replaceAll": {
"message": "Замінити все"
},
"retrieveBckp": {
"message": "Імпорт стилів"
},
"search": {
"message": "Пошук"
},
"searchStylesAll": {
"message": "Усі"
},
"searchStylesCode": {
"message": "CSS код"
},
"searchStylesHelp": {
"message": "</> або <Ctrl-F>клавіша фокусує поле пошуку.\nРежим за замовчуванням — це пошук у звичайному тексті для всіх термінів, розділених пробілами, у будь-якому порядку.\nТочні слова: оберніть запит у подвійні лапки, напр. <.header ~ div\">\nРегулярні вирази: включають косі риски та прапорці, напр.</body.*?\\ba\\b/i>\n«By URL» в селекторі області: знаходить стилі, які застосовуються до повністю вказаної URL-адреси, напр. https://www.example.org/\n\"Metadata\" в селектрі області: шукає в іменах, \"applies to\" специфікаторів, URL-адреси встановлення, URL-адресі оновлення та всьому блоку метаданих для стилів CSS користувача."
},
"searchStylesName": {
"message": "Назва"
},
"sectionCode": {
"message": "Код"
},
"sections": {
"message": "Розділи"
},
"styleBeautify": {
"message": "Облагородити"
},
"styleCancelEditLabel": {
"message": "Повернутися до керування"
},
"styleEnabledLabel": {
"message": "Увімкнено"
},
"stylePreferSchemeLabel": {
"message": "Темна/Світла тема"
},
"styleSaveLabel": {
"message": "Зберегти"
},
"syncErrorRelogin": {
"message": "Помилка синхронізації. Ви вийшли із системи. \nСпробуйте повторно увійти до системи в налаштуваннях Stylus."
},
"toggleStyle": {
"message": "Включити/виключити стиль"
},
"writeStyleFor": {
"message": "Створити стиль для:"
},
"writeStyleForURL": {
"message": "цей URL"
},
"zipStyles": {
"message": "Запаковування стилів ... "
}
}

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

@ -1,169 +1,26 @@
/* global workerUtil importScripts parseMozFormat metaParser styleCodeEmpty colorConverter */ /* global createWorkerApi */// worker-util.js
'use strict'; 'use strict';
importScripts('/js/worker-util.js'); /** @namespace BackgroundWorker */
const {loadScript, createAPI} = workerUtil; createWorkerApi({
createAPI({ async compileUsercss(...args) {
parseMozFormat(arg) { require(['/js/usercss-compiler']); /* global compileUsercss */
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js'); return compileUsercss(...args);
return parseMozFormat(arg);
},
compileUsercss,
parseUsercssMeta(text, indexOffset = 0) {
loadScript(
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js'
);
return metaParser.parse(text, indexOffset);
}, },
nullifyInvalidVars(vars) { nullifyInvalidVars(vars) {
loadScript( require(['/js/meta-parser']); /* global metaParser */
'/js/polyfill.js',
'/vendor/usercss-meta/usercss-meta.min.js',
'/vendor-overwrites/colorpicker/colorconverter.js',
'/js/meta-parser.js'
);
return metaParser.nullifyInvalidVars(vars); return metaParser.nullifyInvalidVars(vars);
} },
parseMozFormat(...args) {
require(['/js/moz-parser']); /* global extractSections */
return extractSections(...args);
},
parseUsercssMeta(text) {
require(['/js/meta-parser']);
return metaParser.parse(text);
},
}); });
function compileUsercss(preprocessor, code, vars) {
loadScript('/vendor-overwrites/csslint/parserlib.js', '/js/moz-parser.js');
const builder = getUsercssCompiler(preprocessor);
vars = simpleVars(vars);
return Promise.resolve(builder.preprocess ? builder.preprocess(code, vars) : code)
.then(code => parseMozFormat({code}))
.then(({sections, errors}) => {
if (builder.postprocess) {
builder.postprocess(sections, vars);
}
return {sections, errors};
});
function simpleVars(vars) {
if (!vars) {
return {};
}
// simplify vars by merging `va.default` to `va.value`, so BUILDER don't
// need to test each va's default value.
return Object.keys(vars).reduce((output, key) => {
const va = vars[key];
output[key] = Object.assign({}, va, {
value: va.value === null || va.value === undefined ?
getVarValue(va, 'default') : getVarValue(va, 'value')
});
return output;
}, {});
}
function getVarValue(va, prop) {
if (va.type === 'select' || va.type === 'dropdown' || va.type === 'image') {
// TODO: handle customized image
return va.options.find(o => o.name === va[prop]).value;
}
if ((va.type === 'number' || va.type === 'range') && va.units) {
return va[prop] + va.units;
}
return va[prop];
}
}
function getUsercssCompiler(preprocessor) {
const BUILDER = {
default: {
postprocess(sections, vars) {
loadScript('/js/sections-util.js');
let varDef = Object.keys(vars).map(k => ` --${k}: ${vars[k].value};\n`).join('');
if (!varDef) return;
varDef = ':root {\n' + varDef + '}\n';
for (const section of sections) {
if (!styleCodeEmpty(section.code)) {
section.code = varDef + section.code;
}
}
}
},
stylus: {
preprocess(source, vars) {
loadScript('/vendor/stylus-lang-bundle/stylus.min.js');
return new Promise((resolve, reject) => {
const varDef = Object.keys(vars).map(key => `${key} = ${vars[key].value};\n`).join('');
if (!Error.captureStackTrace) Error.captureStackTrace = () => {};
self.stylus(varDef + source).render((err, output) => {
if (err) {
reject(err);
} else {
resolve(output);
}
});
});
}
},
less: {
preprocess(source, vars) {
if (!self.less) {
self.less = {
logLevel: 0,
useFileCache: false,
};
}
loadScript('/vendor/less-bundle/less.min.js');
const varDefs = Object.keys(vars).map(key => `@${key}:${vars[key].value};\n`).join('');
return self.less.render(varDefs + source)
.then(({css}) => css);
}
},
uso: {
preprocess(source, vars) {
loadScript('/vendor-overwrites/colorpicker/colorconverter.js');
const pool = new Map();
return Promise.resolve(doReplace(source));
function getValue(name, rgb) {
if (!vars.hasOwnProperty(name)) {
if (name.endsWith('-rgb')) {
return getValue(name.slice(0, -4), true);
}
return null;
}
if (rgb) {
if (vars[name].type === 'color') {
const color = colorConverter.parse(vars[name].value);
if (!color) return null;
const {r, g, b} = color;
return `${r}, ${g}, ${b}`;
}
return null;
}
if (vars[name].type === 'dropdown' || vars[name].type === 'select') {
// prevent infinite recursion
pool.set(name, '');
return doReplace(vars[name].value);
}
return vars[name].value;
}
function doReplace(text) {
return text.replace(/\/\*\[\[([\w-]+)\]\]\*\//g, (match, name) => {
if (!pool.has(name)) {
const value = getValue(name);
pool.set(name, value === null ? match : value);
}
return pool.get(name);
});
}
}
}
};
if (preprocessor) {
if (!BUILDER[preprocessor]) {
throw new Error('unknwon preprocessor');
}
return BUILDER[preprocessor];
}
return BUILDER.default;
}

View File

@ -1,76 +1,127 @@
/* global download prefs openURL FIREFOX CHROME /* global API msg */// msg.js
URLS ignoreChromeError usercssHelper /* global addAPI bgReady */// common.js
styleManager msg navigatorUtil workerUtil contentScripts sync /* global createWorker */// worker-util.js
findExistingTab createTab activateTab isTabReplaceable getActiveTab /* global prefs */
tabManager */ /* global styleMan */
/* global syncMan */
/* global updateMan */
/* global usercssMan */
/* global usoApi */
/* global uswApi */
/* global FIREFOX UA activateTab openURL */ // toolbox.js
/* global colorScheme */ // color-scheme.js
'use strict'; 'use strict';
// eslint-disable-next-line no-var //#region API
var backgroundWorker = workerUtil.createWorker({
url: '/background/background-worker.js'
});
// eslint-disable-next-line no-var addAPI(/** @namespace API */ {
var browserCommands, contextMenus;
// ************************************************************************* /** Temporary storage for data needed elsewhere e.g. in a content script */
// browser commands data: ((data = {}) => ({
browserCommands = { del: key => delete data[key],
openManage, get: key => data[key],
openOptions: () => openManage({options: true}), has: key => key in data,
styleDisableAll(info) { pop: key => {
prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll')); const val = data[key];
}, delete data[key];
reload: () => chrome.runtime.reload(), return val;
}; },
set: (key, val) => {
data[key] = val;
},
}))(),
window.API_METHODS = Object.assign(window.API_METHODS || {}, { styles: styleMan,
deleteStyle: styleManager.deleteStyle, sync: syncMan,
editSave: styleManager.editSave, updater: updateMan,
findStyle: styleManager.findStyle, usercss: usercssMan,
getAllStyles: styleManager.getAllStyles, // used by importer uso: usoApi,
getSectionsByUrl: styleManager.getSectionsByUrl, usw: uswApi,
getStyle: styleManager.get, colorScheme,
getStylesByUrl: styleManager.getStylesByUrl, /** @type {BackgroundWorker} */
importStyle: styleManager.importStyle, worker: createWorker({url: '/background/background-worker'}),
importManyStyles: styleManager.importMany,
installStyle: styleManager.installStyle,
styleExists: styleManager.styleExists,
toggleStyle: styleManager.toggleStyle,
addInclusion: styleManager.addInclusion,
removeInclusion: styleManager.removeInclusion,
addExclusion: styleManager.addExclusion,
removeExclusion: styleManager.removeExclusion,
/** @returns {string} */
getTabUrlPrefix() { getTabUrlPrefix() {
const {url} = this.sender.tab; return this.sender.tab.url.match(/^([\w-]+:\/+[^/#]+)/)[1];
if (url.startsWith(URLS.ownOrigin)) { },
return 'stylus';
/**
* Opens the editor or activates an existing tab
* @param {{
id?: number
domain?: string
'url-prefix'?: string
}} params
* @returns {Promise<chrome.tabs.Tab>}
*/
async openEditor(params) {
const u = new URL(chrome.runtime.getURL('edit.html'));
u.search = new URLSearchParams(params);
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;
}
} }
return url.match(/^([\w-]+:\/+[^/#]+)/)[1]; const tab = await openURL({
url: `${u}`,
currentWindow: null,
newWindow: wnd && Object.assign(wndBase, !ffBug && wndPos),
});
if (ffBug) await browser.windows.update(tab.windowId, wndPos);
return tab;
}, },
download(msg) { /** @returns {Promise<chrome.tabs.Tab>} */
delete msg.method; async openManage({options = false, search, searchMode} = {}) {
return download(msg.url, msg); 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)});
}
return activateTab(tab); // activateTab unminimizes the window
}, },
parseCss({code}) {
return backgroundWorker.parseMozFormat({code});
},
getPrefs: prefs.getAll,
openEditor, /**
* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent
/* Same as openURL, the only extra prop in `opts` is `message` - it'll be sent when the tab is ready, * when the tab is ready, which is needed in the popup, otherwise another
which is needed in the popup, otherwise another extension could force the tab to open in foreground * extension could force the tab to open in foreground thus auto-closing the
thus auto-closing the popup (in Chrome at least) and preventing the sendMessage code from running */ * popup (in Chrome at least) and preventing the sendMessage code from running
openURL(opts) { * @returns {Promise<chrome.tabs.Tab>}
const {message} = opts; */
return openURL(opts) // will pass the resolved value untouched when `message` is absent or falsy async openURL(opts) {
.then(message && (tab => tab.status === 'complete' ? tab : onTabReady(tab))) const tab = await openURL(opts);
.then(message && (tab => msg.sendTab(tab.id, opts.message))); if (opts.message) {
await onTabReady(tab);
await msg.sendTab(tab.id, opts.message);
}
return tab;
function onTabReady(tab) { function onTabReady(tab) {
return new Promise((resolve, reject) => return new Promise((resolve, reject) =>
setTimeout(function ping(numTries = 10, delay = 100) { setTimeout(function ping(numTries = 10, delay = 100) {
@ -84,259 +135,79 @@ window.API_METHODS = Object.assign(window.API_METHODS || {}, {
} }
}, },
optionsCustomizeHotkeys() { prefs: {
return browserCommands.openOptions() getValues: () => prefs.__values, // will be deepCopy'd by apiHandler
.then(() => new Promise(resolve => setTimeout(resolve, 500))) set: prefs.set,
.then(() => msg.broadcastExtension({method: 'optionsCustomizeHotkeys'}));
}, },
syncStart: sync.start,
syncStop: sync.stop,
syncNow: sync.syncNow,
getSyncStatus: sync.getStatus,
syncLogin: sync.login,
openManage
}); });
// ************************************************************************* //#endregion
// register all listeners //#region Events
msg.on(onRuntimeMessage);
// tell apply.js to refresh styles for non-committed navigation const browserCommands = {
navigatorUtil.onUrlChange(({tabId, frameId}, type) => { openManage: () => API.openManage(),
if (type !== 'committed') { openOptions: () => API.openManage({options: true}),
msg.sendTab(tabId, {method: 'urlChanged'}, {frameId}) reload: () => chrome.runtime.reload(),
.catch(msg.ignoreError); styleDisableAll(info) {
} prefs.set('disableAll', info ? info.checked : !prefs.get('disableAll'));
});
tabManager.onUpdate(({tabId, url, oldUrl = ''}) => {
if (usercssHelper.testUrl(url) && !oldUrl.startsWith(URLS.installUsercss)) {
usercssHelper.testContents(tabId, url).then(data => {
if (data.code) usercssHelper.openInstallerPage(tabId, url, data);
});
}
});
if (FIREFOX) {
// FF misses some about:blank iframes so we inject our content script explicitly
navigatorUtil.onDOMContentLoaded(webNavIframeHelperFF, {
url: [
{urlEquals: 'about:blank'},
]
});
}
if (chrome.contextMenus) {
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
}
if (chrome.commands) {
// Not available in Firefox - https://bugzilla.mozilla.org/show_bug.cgi?id=1240350
chrome.commands.onCommand.addListener(command => browserCommands[command]());
}
// *************************************************************************
chrome.runtime.onInstalled.addListener(({reason}) => {
// save install type: "admin", "development", "normal", "sideload" or "other"
// "normal" = addon installed from webstore
chrome.management.getSelf(info => {
localStorage.installType = info.installType;
if (reason === 'install' && info.installType === 'development' && chrome.contextMenus) {
createContextMenus(['reload']);
}
});
if (reason !== 'update') return;
// translations may change
localStorage.L10N = JSON.stringify({
browserUIlanguage: chrome.i18n.getUILanguage(),
});
// themes may change
delete localStorage.codeMirrorThemes;
});
// *************************************************************************
// context menus
contextMenus = {
'show-badge': {
title: 'menuShowBadge',
click: info => prefs.set(info.menuItemId, info.checked),
}, },
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'openStylesManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
'reload': {
presentIf: () => localStorage.installType === 'development',
title: 'reload',
click: browserCommands.reload,
},
'editor.contextDelete': {
presentIf: () => !FIREFOX && prefs.get('editor.contextDelete'),
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + 'edit*'],
click: (info, tab) => {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension');
},
}
}; };
function createContextMenus(ids) { if (chrome.commands) {
for (const id of ids) { chrome.commands.onCommand.addListener(id => browserCommands[id]());
let item = contextMenus[id]; }
if (item.presentIf && !item.presentIf()) {
continue; chrome.runtime.onInstalled.addListener(({reason, previousVersion}) => {
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) {}
} }
item = Object.assign({id}, item);
delete item.presentIf;
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'];
}
delete item.click;
chrome.contextMenus.create(item, ignoreChromeError);
} }
} });
if (chrome.contextMenus) { msg.on((msg, sender) => {
// circumvent the bug with disabling check marks in Chrome 62-64 if (msg.method === 'invokeAPI') {
const toggleCheckmark = CHROME >= 3172 && CHROME <= 3288 ? let res = msg.path.reduce((res, name) => res && res[name], API);
(id => chrome.contextMenus.remove(id, () => createContextMenus([id]) + ignoreChromeError())) : if (!res) throw new Error(`Unknown API.${msg.path.join('.')}`);
((id, checked) => chrome.contextMenus.update(id, {checked}, ignoreChromeError)); res = res.apply({msg, sender}, msg.args);
return res === undefined ? null : res;
const togglePresence = (id, checked) => {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
};
const keys = Object.keys(contextMenus);
prefs.subscribe(keys.filter(id => typeof prefs.defaults[id] === 'boolean'), toggleCheckmark);
prefs.subscribe(keys.filter(id => contextMenus[id].presentIf), togglePresence);
createContextMenus(keys);
}
// reinject content scripts when the extension is reloaded/updated. Firefox
// would handle this automatically.
if (!FIREFOX) {
setTimeout(contentScripts.injectToAllTabs, 0);
}
// register hotkeys
if (FIREFOX && browser.commands && browser.commands.update) {
const hotkeyPrefs = Object.keys(prefs.defaults).filter(k => k.startsWith('hotkey.'));
prefs.subscribe(hotkeyPrefs, (name, value) => {
try {
name = name.split('.')[1];
if (value.trim()) {
browser.commands.update({name, shortcut: value});
} else {
browser.commands.reset(name);
}
} catch (e) {}
});
}
msg.broadcastTab({method: 'backgroundReady'});
function webNavIframeHelperFF({tabId, frameId}) {
if (!frameId) return;
msg.sendTab(tabId, {method: 'ping'}, {frameId})
.catch(() => false)
.then(pong => {
if (pong) return;
// insert apply.js to iframe
const files = chrome.runtime.getManifest().content_scripts[0].js;
for (const file of files) {
chrome.tabs.executeScript(tabId, {
frameId,
file,
matchAboutBlank: true,
}, ignoreChromeError);
}
});
}
function onRuntimeMessage(msg, sender) {
if (msg.method !== 'invokeAPI') {
return;
} }
const fn = window.API_METHODS[msg.name]; });
if (!fn) {
throw new Error(`unknown API: ${msg.name}`);
}
const context = {msg, sender};
return fn.apply(context, msg.args);
}
function openEditor(params) { //#endregion
/* Open the editor. Activate if it is already opened
params: { Promise.all([
id?: Number, browser.extension.isAllowedFileSchemeAccess()
domain?: String, .then(res => API.data.set('hasFileAccess', res)),
'url-prefix'?: String bgReady.styles,
} /* These are loaded conditionally.
*/ Each item uses `require` individually so IDE can jump to the source and track usage. */
const searchParams = new URLSearchParams(); FIREFOX &&
for (const key in params) { require(['/background/style-via-api']),
searchParams.set(key, params[key]); FIREFOX && ((browser.commands || {}).update) &&
} require(['/background/browser-cmd-hotkeys']),
const search = searchParams.toString(); !FIREFOX &&
return openURL({ require(['/background/content-scripts']),
url: 'edit.html' + (search && `?${search}`), chrome.contextMenus &&
newWindow: prefs.get('openEditInWindow'), require(['/background/context-menus']),
windowPosition: prefs.get('windowPosition'), ]).then(() => {
currentWindow: null bgReady._resolveAll();
}); msg.ready = true;
} msg.broadcast({method: 'backgroundReady'});
});
function openManage({options = false, search} = {}) {
let url = chrome.runtime.getURL('manage.html');
if (search) {
url += `?search=${encodeURIComponent(search)}`;
}
if (options) {
url += '#stylus-options';
}
return findExistingTab({
url,
currentWindow: null,
ignoreHash: true,
ignoreSearch: true
})
.then(tab => {
if (tab) {
return Promise.all([
activateTab(tab),
tab.url !== url && msg.sendTab(tab.id, {method: 'pushState', url})
.catch(console.error)
]);
}
return getActiveTab().then(tab => {
if (isTabReplaceable(tab, url)) {
return activateTab(tab, {url});
}
return createTab({url});
});
});
}

View File

@ -0,0 +1,22 @@
/* global prefs */
'use strict';
/*
Registers hotkeys in FF
*/
(() => {
const hotkeyPrefs = prefs.knownKeys.filter(k => k.startsWith('hotkey.'));
prefs.subscribe(hotkeyPrefs, updateHotkey, {runNow: true});
async function updateHotkey(name, value) {
try {
name = name.split('.')[1];
if (value.trim()) {
await browser.commands.update({name, shortcut: value});
} else {
await browser.commands.reset(name);
}
} catch (e) {}
}
})();

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);
}
}
}
})();

92
background/common.js Normal file
View File

@ -0,0 +1,92 @@
/* global API */// msg.js
'use strict';
/**
* Common stuff that's loaded first so it's immediately available to all background scripts
*/
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];
if (old && Object.prototype.toString.call(old) === '[object Object]') {
Object.assign(old, val);
} else {
API[key] = val;
}
}
}
/* 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,15 +1,21 @@
/* global msg queryTabs ignoreChromeError URLS */ /* global bgReady */// common.js
/* exported contentScripts */ /* global msg */
/* global URLS ignoreChromeError */// toolbox.js
'use strict'; 'use strict';
const contentScripts = (() => { /*
Reinject content scripts when the extension is reloaded/updated.
Not used in Firefox as it reinjects automatically.
*/
bgReady.all.then(() => {
const NTP = 'chrome://newtab/'; const NTP = 'chrome://newtab/';
const ALL_URLS = '<all_urls>'; const ALL_URLS = '<all_urls>';
const SCRIPTS = chrome.runtime.getManifest().content_scripts; const SCRIPTS = chrome.runtime.getManifest().content_scripts;
// expand * as .*? // expand * as .*?
const wildcardAsRegExp = (s, flags) => new RegExp( const wildcardAsRegExp = (s, flags) => new RegExp(
s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&') s.replace(/[{}()[\]/\\.+?^$:=!|]/g, '\\$&')
.replace(/\*/g, '.*?'), flags); .replace(/\*/g, '.*?'), flags);
for (const cs of SCRIPTS) { for (const cs of SCRIPTS) {
cs.matches = cs.matches.map(m => ( cs.matches = cs.matches.map(m => (
m === ALL_URLS ? m : wildcardAsRegExp(m) m === ALL_URLS ? m : wildcardAsRegExp(m)
@ -17,7 +23,8 @@ const contentScripts = (() => {
} }
const busyTabs = new Set(); const busyTabs = new Set();
let busyTabsTimer; let busyTabsTimer;
return {injectToTab, injectToAllTabs};
setTimeout(injectToAllTabs);
function injectToTab({url, tabId, frameId = null}) { function injectToTab({url, tabId, frameId = null}) {
for (const script of SCRIPTS) { for (const script of SCRIPTS) {
@ -42,7 +49,7 @@ const contentScripts = (() => {
const options = { const options = {
runAt: script.run_at, runAt: script.run_at,
allFrames: script.all_frames, allFrames: script.all_frames,
matchAboutBlank: script.match_about_blank matchAboutBlank: script.match_about_blank,
}; };
if (frameId !== null) { if (frameId !== null) {
options.allFrames = false; options.allFrames = false;
@ -55,17 +62,17 @@ const contentScripts = (() => {
} }
function injectToAllTabs() { function injectToAllTabs() {
return queryTabs({}).then(tabs => { return browser.tabs.query({}).then(tabs => {
for (const tab of tabs) { for (const tab of tabs) {
// skip unloaded/discarded/chrome tabs // skip unloaded/discarded/chrome tabs
if (!tab.width || tab.discarded || !URLS.supported(tab.url)) continue; if (!tab.width || tab.discarded || !URLS.supported(tab.pendingUrl || tab.url)) continue;
// our content scripts may still be pending injection at browser start so it's too early to ping them // our content scripts may still be pending injection at browser start so it's too early to ping them
if (tab.status === 'loading') { if (tab.status === 'loading') {
trackBusyTab(tab.id, true); trackBusyTab(tab.id, true);
} else { } else {
injectToTab({ injectToTab({
url: tab.url, url: tab.pendingUrl || tab.url,
tabId: tab.id tabId: tab.id,
}); });
} }
} }
@ -107,4 +114,4 @@ const contentScripts = (() => {
function onBusyTabRemoved(tabId) { function onBusyTabRemoved(tabId) {
trackBusyTab(tabId, false); trackBusyTab(tabId, false);
} }
})(); });

View File

@ -0,0 +1,87 @@
/* global browserCommands */// background.js
/* global msg */
/* global prefs */
/* global CHROME URLS ignoreChromeError */// toolbox.js
'use strict';
chrome.management.getSelf(ext => {
const contextMenus = Object.assign({
'show-badge': {
title: 'menuShowBadge',
click: togglePref,
},
'disableAll': {
title: 'disableAllStyles',
click: browserCommands.styleDisableAll,
},
'open-manager': {
title: 'optionsOpenManager',
click: browserCommands.openManage,
},
'open-options': {
title: 'openOptions',
click: browserCommands.openOptions,
},
}, ext.installType === 'development' && {
'reload': {
title: 'reload',
click: browserCommands.reload,
},
}, CHROME && {
'editor.contextDelete': {
title: 'editDeleteText',
type: 'normal',
contexts: ['editable'],
documentUrlPatterns: [URLS.ownOrigin + '*'],
click: (info, tab) => {
msg.sendTab(tab.id, {method: 'editDeleteText'}, undefined, 'extension')
.catch(msg.ignoreError);
},
},
});
createContextMenus(Object.keys(contextMenus));
chrome.contextMenus.onClicked.addListener((info, tab) =>
contextMenus[info.menuItemId].click(info, tab));
function createContextMenus(ids) {
for (const id of ids) {
const item = Object.assign({id, contexts: ['browser_action']}, contextMenus[id]);
item.title = chrome.i18n.getMessage(item.title);
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);
}
}
function toggleCheckmark(id, checked) {
chrome.contextMenus.update(id, {checked}, ignoreChromeError);
}
/** Circumvents the bug with disabling check marks in Chrome 62-64 */
async function toggleCheckmarkBugged(id) {
await browser.contextMenus.remove(id).catch(ignoreChromeError);
createContextMenus([id]);
}
/** @param {chrome.contextMenus.OnClickData} info */
function togglePref(info) {
prefs.set(info.menuItemId, info.checked);
}
function togglePresence(id, checked) {
if (checked) {
createContextMenus([id]);
} else {
chrome.contextMenus.remove(id, ignoreChromeError);
}
}
});

View File

@ -1,84 +1,64 @@
/* global promisify */ /* global chromeLocal */// storage-util.js
/* exported createChromeStorageDB */
'use strict'; 'use strict';
function createChromeStorageDB() { /* exported createChromeStorageDB */
const get = promisify(chrome.storage.local.get.bind(chrome.storage.local)); function createChromeStorageDB(PREFIX) {
const set = promisify(chrome.storage.local.set.bind(chrome.storage.local));
const remove = promisify(chrome.storage.local.remove.bind(chrome.storage.local));
let INC; let INC;
const isMain = !PREFIX;
if (!PREFIX) PREFIX = 'style-';
const PREFIX = 'style-'; return {
const METHODS = {
// FIXME: we don't use this method at all. Should we remove this? delete(id) {
get: id => get(PREFIX + id) return chromeLocal.remove(PREFIX + id);
.then(result => result[PREFIX + id]), },
put: obj => Promise.resolve()
.then(() => { get(id) {
if (!obj.id) { return chromeLocal.getValue(PREFIX + id);
return prepareInc() },
.then(() => {
// FIXME: should we clone the object? async getAll() {
obj.id = INC++; const all = await chromeLocal.get();
}); if (!INC) prepareInc(all);
return Object.entries(all)
.map(([key, val]) => key.startsWith(PREFIX) &&
(!isMain || Number(key.slice(PREFIX.length))) &&
val)
.filter(Boolean);
},
async put(item) {
if (!item.id) {
if (!INC) await prepareInc();
item.id = INC++;
}
await chromeLocal.setValue(PREFIX + item.id, item);
return item.id;
},
async putMany(items) {
const data = {};
for (const item of items) {
if (!item.id) {
if (!INC) await prepareInc();
item.id = INC++;
} }
}) data[PREFIX + item.id] = item;
.then(() => set({[PREFIX + obj.id]: obj})) }
.then(() => obj.id), await chromeLocal.set(data);
putMany: items => prepareInc() return items.map(_ => _.id);
.then(() => { },
for (const item of items) {
if (!item.id) {
item.id = INC++;
}
}
return set(items.reduce((obj, curr) => {
obj[PREFIX + curr.id] = curr;
return obj;
}, {}));
})
.then(() => items.map(i => i.id)),
delete: id => remove(PREFIX + id),
getAll: () => get(null)
.then(result => {
const output = [];
for (const key in result) {
if (key.startsWith(PREFIX) && Number(key.slice(PREFIX.length))) {
output.push(result[key]);
}
}
return output;
})
}; };
return {exec}; async function prepareInc(data) {
INC = 1;
function exec(method, ...args) { for (const key in data || await chromeLocal.get()) {
if (METHODS[method]) { if (key.startsWith(PREFIX)) {
return METHODS[method](...args) const id = Number(key.slice(PREFIX.length));
.then(result => { if (id >= INC) {
if (method === 'putMany' && result.map) { INC = id + 1;
return result.map(r => ({target: {result: r}}));
}
return {target: {result}};
});
}
return Promise.reject(new Error(`unknown DB method ${method}`));
}
function prepareInc() {
if (INC) return Promise.resolve();
return get(null).then(result => {
INC = 1;
for (const key in result) {
if (key.startsWith(PREFIX)) {
const id = Number(key.slice(PREFIX.length));
if (id >= INC) {
INC = id + 1;
}
} }
} }
}); }
} }
} }

View File

@ -1,156 +1,150 @@
/* global chromeLocal ignoreChromeError workerUtil createChromeStorageDB */ /* global addAPI */// common.js
/* exported db */ /* global chromeLocal */// storage-util.js
/* /* global cloneError */// worker-util.js
Initialize a database. There are some problems using IndexedDB in Firefox: /* global deepCopy */// toolbox.js
https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/ /* global prefs */
Some of them are fixed in FF59:
https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
*/
'use strict'; 'use strict';
/*
Initialize a database. There are some problems using IndexedDB in Firefox:
https://www.reddit.com/r/firefox/comments/74wttb/note_to_firefox_webextension_developers_who_use/
Some of them are fixed in FF59:
https://www.reddit.com/r/firefox/comments/7ijuaq/firefox_59_webextensions_can_use_indexeddb_when/
*/
/* exported db */
const db = (() => { const db = (() => {
let exec; let exec = async (...args) => (
const preparing = prepare(); exec = await tryUsingIndexedDB().catch(useChromeStorage)
)(...args);
const DB = 'stylish';
const FALLBACK = 'dbInChromeStorage';
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),
};
/**
* @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 { return {
exec: (...args) => styles: getProxy(DB),
preparing.then(() => exec(...args))
}; };
function prepare() { async function cachedExec(dbName, cmd, a, b) {
return withPromise(shouldUseIndexedDB).then( const hub = cache[dbName] || (cache[dbName] = {});
ok => { const res = cmd === 'get' && a in hub ? hub[a] : await exec(...arguments);
if (ok) { if (cmd === 'get') {
useIndexedDB(); hub[a] = deepCopy(res);
} else { } else if (cmd === 'put') {
useChromeStorage(); hub[ID_AS_KEY[dbName] ? a.id : b] = deepCopy(a);
} } else if (cmd === 'delete') {
}, delete hub[a];
err => { }
useChromeStorage(err); return res;
}
);
} }
function shouldUseIndexedDB() { async function tryUsingIndexedDB() {
// we use chrome.storage.local fallback if IndexedDB doesn't save data, // we use chrome.storage.local fallback if IndexedDB doesn't save data,
// which, once detected on the first run, is remembered in chrome.storage.local // which, once detected on the first run, is remembered in chrome.storage.local
// for reliablility and in localStorage for fast synchronous access // note that accessing indexedDB may throw, https://github.com/openstyles/stylus/issues/615
// (FF may block localStorage depending on its privacy options)
// note that it may throw when accessing the variable
// https://github.com/openstyles/stylus/issues/615
if (typeof indexedDB === 'undefined') { if (typeof indexedDB === 'undefined') {
throw new Error('indexedDB is undefined'); throw new Error('indexedDB is undefined');
} }
// test localStorage switch (await chromeLocal.getValue(FALLBACK)) {
const fallbackSet = localStorage.dbInChromeStorage; case true: throw null;
if (fallbackSet === 'true') { case false: break;
return false; default: await testDB();
} }
if (fallbackSet === 'false') { chromeLocal.setValue(FALLBACK, false);
return true; return dbExecIndexedDB;
}
// test storage.local
return chromeLocal.get('dbInChromeStorage')
.then(data => {
if (data && data.dbInChromeStorage) {
return false;
}
return testDBSize()
.then(ok => ok || testDBMutation());
});
} }
function withPromise(fn) { async function testDB() {
try { const id = `${performance.now()}.${Math.random()}.${Date.now()}`;
return Promise.resolve(fn()); await dbExecIndexedDB(DB, 'put', {id});
} catch (err) { const e = await dbExecIndexedDB(DB, 'get', id);
return Promise.reject(err); await dbExecIndexedDB(DB, 'delete', e.id); // throws if `e` or id is null
}
} }
function testDBSize() { async function useChromeStorage(err) {
return dbExecIndexedDB('getAllKeys', IDBKeyRange.lowerBound(1), 1) chromeLocal.setValue(FALLBACK, true);
.then(event => (
event.target.result &&
event.target.result.length &&
event.target.result[0]
));
}
function testDBMutation() {
return dbExecIndexedDB('put', {id: -1})
.then(() => dbExecIndexedDB('get', -1))
.then(event => {
if (!event.target.result) {
throw new Error('failed to get previously put item');
}
if (event.target.result.id !== -1) {
throw new Error('item id is wrong');
}
return dbExecIndexedDB('delete', -1);
})
.then(() => true);
}
function useChromeStorage(err) {
exec = createChromeStorageDB().exec;
chromeLocal.set({dbInChromeStorage: true}, ignoreChromeError);
if (err) { if (err) {
chromeLocal.setValue('dbInChromeStorageReason', workerUtil.cloneError(err)); chromeLocal.setValue(FALLBACK + 'Reason', cloneError(err));
console.warn('Failed to access indexedDB. Switched to storage API.', err); console.warn('Failed to access indexedDB. Switched to storage API.', err);
} }
localStorage.dbInChromeStorage = 'true'; await require(['/background/db-chrome-storage']); /* global createChromeStorageDB */
const BASES = {};
return (dbName, method, ...args) => (
BASES[dbName] || (
BASES[dbName] = createChromeStorageDB(dbName !== DB && `${dbName}-`)
)
)[method](...args);
} }
function useIndexedDB() { async function dbExecIndexedDB(dbName, method, ...args) {
exec = dbExecIndexedDB; const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
chromeLocal.set({dbInChromeStorage: false}, ignoreChromeError); const storeName = getStoreName(dbName);
localStorage.dbInChromeStorage = 'false'; const store = (await open(dbName)).transaction([storeName], mode).objectStore(storeName);
const fn = method === 'putMany' ? putMany : storeRequest;
return fn(store, method, ...args);
} }
function dbExecIndexedDB(method, ...args) { function storeRequest(store, method, ...args) {
return open().then(database => { return new Promise((resolve, reject) => {
if (!method) { /** @type {IDBRequest} */
return database; const request = store[method](...args);
} request.onsuccess = () => resolve(request.result);
if (method === 'putMany') { request.onerror = reject;
return putMany(database, ...args);
}
const mode = method.startsWith('get') ? 'readonly' : 'readwrite';
const transaction = database.transaction(['styles'], mode);
const store = transaction.objectStore('styles');
return storeRequest(store, method, ...args);
}); });
}
function storeRequest(store, method, ...args) { function putMany(store, _method, items) {
return new Promise((resolve, reject) => { return Promise.all(items.map(item => storeRequest(store, 'put', item)));
const request = store[method](...args); }
request.onsuccess = resolve;
request.onerror = reject;
});
}
function open() { function open(name) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const request = indexedDB.open('stylish', 2); const request = indexedDB.open(name, 2);
request.onsuccess = () => resolve(request.result); request.onsuccess = e => resolve(create(e));
request.onerror = reject; request.onerror = reject;
request.onupgradeneeded = event => { request.onupgradeneeded = create;
if (event.oldVersion === 0) { });
event.target.result.createObjectStore('styles', { }
keyPath: 'id',
autoIncrement: true,
});
}
};
});
}
function putMany(database, items) { function create(event) {
const transaction = database.transaction(['styles'], 'readwrite'); /** @type IDBDatabase */
const store = transaction.objectStore('styles'); const idb = event.target.result;
return Promise.all(items.map(item => storeRequest(store, 'put', item))); 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,85 +1,123 @@
/* global prefs debounce iconUtil FIREFOX CHROME VIVALDI tabManager navigatorUtil API_METHODS */ /* global API */// msg.js
/* exported iconManager */ /* global addAPI bgReady */// common.js
/* global colorScheme */
/* global prefs */
/* global tabMan */
/* global CHROME FIREFOX UA debounce ignoreChromeError */// toolbox.js
'use strict'; 'use strict';
const iconManager = (() => { /* exported iconMan */
const ICON_SIZES = FIREFOX || CHROME >= 2883 && !VIVALDI ? [16, 32] : [19, 38]; const iconMan = (() => {
const ICON_SIZES = FIREFOX || CHROME && !UA.vivaldi ? [16, 32] : [19, 38];
const staleBadges = new Set(); 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 = FIREFOX_ANDROID ? false : loadImage(`/images/icon/${ICON_SIZES[0]}.png`)
.then(({data}) => (hasCanvas = data.some(b => b !== 255)));
prefs.subscribe([ addAPI(/** @namespace API */ {
'disableAll', /**
'badgeDisabled', * @param {(number|string)[]} styleIds
'badgeNormal', * @param {boolean} [lazyBadge=false] preventing flicker during page load
], () => debounce(refreshIconBadgeColor)); */
prefs.subscribe([
'show-badge'
], () => debounce(refreshAllIconsBadgeText));
prefs.subscribe([
'disableAll',
'iconset',
], () => debounce(refreshAllIcons));
prefs.initializing.then(() => {
refreshIconBadgeColor();
refreshAllIconsBadgeText();
refreshAllIcons();
});
Object.assign(API_METHODS, {
/** @param {(number|string)[]} styleIds
* @param {boolean} [lazyBadge=false] preventing flicker during page load */
updateIconBadge(styleIds, {lazyBadge} = {}) { updateIconBadge(styleIds, {lazyBadge} = {}) {
// FIXME: in some cases, we only have to redraw the badge. is it worth a optimization? // FIXME: in some cases, we only have to redraw the badge. is it worth a optimization?
const {frameId, tab: {id: tabId}} = this.sender; const {frameId, tab: {id: tabId}} = this.sender;
const value = styleIds.length ? styleIds.map(Number) : undefined; const value = styleIds.length ? styleIds.map(Number) : undefined;
tabManager.set(tabId, 'styleIds', frameId, value); tabMan.set(tabId, 'styleIds', frameId, value);
debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0); debounce(refreshStaleBadges, frameId && lazyBadge ? 250 : 0);
staleBadges.add(tabId); staleBadges.add(tabId);
if (!frameId) refreshIcon(tabId, true); if (!frameId) refreshIcon(tabId, true);
}, },
}); });
navigatorUtil.onCommitted(({tabId, frameId}) => { chrome.webNavigation.onCommitted.addListener(({tabId, frameId}) => {
if (!frameId) tabManager.set(tabId, 'styleIds', undefined); if (!frameId) tabMan.set(tabId, 'styleIds', undefined);
}); });
chrome.runtime.onConnect.addListener(port => { chrome.runtime.onConnect.addListener(port => {
if (port.name === 'iframe') { if (port.name === 'iframe') {
port.onDisconnect.addListener(onPortDisconnected); port.onDisconnect.addListener(onPortDisconnected);
} }
}); });
colorScheme.onChange(val => {
isDark = val;
if (prefs.get('iconset') === -1) {
debounce(refreshAllIcons);
}
});
bgReady.all.then(() => {
prefs.subscribe([
'disableAll',
'badgeDisabled',
'badgeNormal',
], () => debounce(refreshIconBadgeColor), {runNow: true});
prefs.subscribe([
'show-badge',
], () => debounce(refreshAllIconsBadgeText), {runNow: true});
prefs.subscribe([
'disableAll',
'iconset',
], () => debounce(refreshAllIcons), {runNow: true});
});
return {
/** Calling with no params clears the override */
overrideBadge({text = '', color = '', title = ''} = {}) {
if (badgeOvr.text === text) {
return;
}
badgeOvr.text = text;
badgeOvr.color = color;
refreshIconBadgeColor();
setBadgeText({text});
for (const tabId of tabMan.list()) {
if (text) {
setBadgeText({tabId, text});
} else {
refreshIconBadgeText(tabId);
}
}
chrome.browserAction.setTitle({
title: title && chrome.i18n.getMessage(title) || title || '',
});
},
};
function onPortDisconnected({sender}) { function onPortDisconnected({sender}) {
if (tabManager.get(sender.tab.id, 'styleIds')) { if (tabMan.get(sender.tab.id, 'styleIds')) {
API_METHODS.updateIconBadge.call({sender}, [], {lazyBadge: true}); API.updateIconBadge.call({sender}, [], {lazyBadge: true});
} }
} }
function refreshIconBadgeText(tabId) { function refreshIconBadgeText(tabId) {
if (badgeOvr.text) return;
const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : ''; const text = prefs.get('show-badge') ? `${getStyleCount(tabId)}` : '';
iconUtil.setBadgeText({tabId, text}); setBadgeText({tabId, text});
} }
function getIconName(hasStyles = false) { 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' : ''; const postfix = prefs.get('disableAll') ? 'x' : !hasStyles ? 'w' : '';
return `${iconset}$SIZE$${postfix}`; return `${prefix}$SIZE$${postfix}`;
} }
function refreshIcon(tabId, force = false) { function refreshIcon(tabId, force = false) {
const oldIcon = tabManager.get(tabId, 'icon'); const oldIcon = tabMan.get(tabId, 'icon');
const newIcon = getIconName(tabManager.get(tabId, 'styleIds', 0)); const newIcon = getIconName(tabMan.get(tabId, 'styleIds', 0));
// (changing the icon only for the main page, frameId = 0) // (changing the icon only for the main page, frameId = 0)
if (!force && oldIcon === newIcon) { if (!force && oldIcon === newIcon) {
return; return;
} }
tabManager.set(tabId, 'icon', newIcon); tabMan.set(tabId, 'icon', newIcon);
iconUtil.setIcon({ setIcon({
path: getIconPath(newIcon), path: getIconPath(newIcon),
tabId tabId,
}); });
} }
@ -96,33 +134,55 @@ const iconManager = (() => {
/** @return {number | ''} */ /** @return {number | ''} */
function getStyleCount(tabId) { function getStyleCount(tabId) {
const allIds = new Set(); const allIds = new Set();
const data = tabManager.get(tabId, 'styleIds') || {}; const data = tabMan.get(tabId, 'styleIds') || {};
Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id))); Object.values(data).forEach(frameIds => frameIds.forEach(id => allIds.add(id)));
return allIds.size || ''; return allIds.size || '';
} }
// Caches imageData for icon paths
async function loadImage(url) {
const {OffscreenCanvas} = !FIREFOX && self.createImageBitmap && self || {};
const img = OffscreenCanvas
? await createImageBitmap(await (await fetch(url)).blob())
: await new Promise((resolve, reject) =>
Object.assign(new Image(), {
src: url,
onload: e => resolve(e.target),
onerror: reject,
}));
const {width: w, height: h} = img;
const canvas = OffscreenCanvas
? new OffscreenCanvas(w, h)
: Object.assign(document.createElement('canvas'), {width: w, height: h});
const ctx = canvas.getContext('2d');
ctx.drawImage(img, 0, 0, w, h);
const result = ctx.getImageData(0, 0, w, h);
imageDataCache.set(url, result);
return result;
}
function refreshGlobalIcon() { function refreshGlobalIcon() {
iconUtil.setIcon({ setIcon({
path: getIconPath(getIconName()) path: getIconPath(getIconName()),
}); });
} }
function refreshIconBadgeColor() { function refreshIconBadgeColor() {
const color = prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'); setBadgeBackgroundColor({
iconUtil.setBadgeBackgroundColor({ color: badgeOvr.color ||
color prefs.get(prefs.get('disableAll') ? 'badgeDisabled' : 'badgeNormal'),
}); });
} }
function refreshAllIcons() { function refreshAllIcons() {
for (const tabId of tabManager.list()) { for (const tabId of tabMan.list()) {
refreshIcon(tabId); refreshIcon(tabId);
} }
refreshGlobalIcon(); refreshGlobalIcon();
} }
function refreshAllIconsBadgeText() { function refreshAllIconsBadgeText() {
for (const tabId of tabManager.list()) { for (const tabId of tabMan.list()) {
refreshIconBadgeText(tabId); refreshIconBadgeText(tabId);
} }
} }
@ -133,4 +193,40 @@ const iconManager = (() => {
} }
staleBadges.clear(); staleBadges.clear();
} }
function safeCall(method, data) {
const {browserAction = {}} = chrome;
const fn = browserAction[method];
if (fn) {
try {
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
fn.call(browserAction, data, ignoreChromeError);
} catch (e) {
// FIXME: skip pre-rendered tabs?
fn.call(browserAction, data);
}
}
}
/** @param {chrome.browserAction.TabIconDetails} data */
async function setIcon(data) {
if (hasCanvas === true || await hasCanvas) {
data.imageData = {};
for (const [key, url] of Object.entries(data.path)) {
data.imageData[key] = imageDataCache.get(url) || await loadImage(url);
}
delete data.path;
}
safeCall('setIcon', data);
}
/** @param {chrome.browserAction.BadgeTextDetails} data */
function setBadgeText(data) {
safeCall('setBadgeText', data);
}
/** @param {chrome.browserAction.BadgeBackgroundColorDetails} data */
function setBadgeBackgroundColor(data) {
safeCall('setBadgeBackgroundColor', data);
}
})(); })();

View File

@ -1,91 +0,0 @@
/* global ignoreChromeError */
/* exported iconUtil */
'use strict';
const iconUtil = (() => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
// https://github.com/openstyles/stylus/issues/335
let noCanvas;
const imageDataCache = new Map();
// test if canvas is usable
const canvasReady = loadImage('/images/icon/16.png')
.then(imageData => {
noCanvas = imageData.data.every(b => b === 255);
});
return extendNative({
/*
Cache imageData for paths
*/
setIcon,
setBadgeText
});
function loadImage(url) {
let result = imageDataCache.get(url);
if (!result) {
result = new Promise((resolve, reject) => {
const img = new Image();
img.src = url;
img.onload = () => {
const w = canvas.width = img.width;
const h = canvas.height = img.height;
ctx.clearRect(0, 0, w, h);
ctx.drawImage(img, 0, 0, w, h);
resolve(ctx.getImageData(0, 0, w, h));
};
img.onerror = reject;
});
imageDataCache.set(url, result);
}
return result;
}
function setIcon(data) {
canvasReady.then(() => {
if (noCanvas) {
chrome.browserAction.setIcon(data, ignoreChromeError);
return;
}
const pending = [];
data.imageData = {};
for (const [key, url] of Object.entries(data.path)) {
pending.push(loadImage(url)
.then(imageData => {
data.imageData[key] = imageData;
}));
}
Promise.all(pending).then(() => {
delete data.path;
chrome.browserAction.setIcon(data, ignoreChromeError);
});
});
}
function setBadgeText(data) {
try {
// Chrome supports the callback since 67.0.3381.0, see https://crbug.com/451320
chrome.browserAction.setBadgeText(data, ignoreChromeError);
} catch (e) {
// FIXME: skip pre-rendered tabs?
chrome.browserAction.setBadgeText(data);
}
}
function extendNative(target) {
return new Proxy(target, {
get: (target, prop) => {
// FIXME: do we really need this?
if (!chrome.browserAction ||
!['setIcon', 'setBadgeBackgroundColor', 'setBadgeText'].every(name => chrome.browserAction[name])) {
return () => {};
}
if (target[prop]) {
return target[prop];
}
return chrome.browserAction[prop].bind(chrome.browserAction);
}
});
}
})();

View File

@ -0,0 +1,103 @@
/* global CHROME FIREFOX URLS deepEqual ignoreChromeError */// toolbox.js
/* global bgReady */// common.js
/* global msg */
'use strict';
/* exported navMan */
const navMan = (() => {
const listeners = new Set();
let prevData = {};
chrome.webNavigation.onCommitted.addListener(onNavigation.bind('committed'));
chrome.webNavigation.onHistoryStateUpdated.addListener(onFakeNavigation.bind('history'));
chrome.webNavigation.onReferenceFragmentUpdated.addListener(onFakeNavigation.bind('hash'));
return {
/** @param {function(data: Object, type: ('committed'|'history'|'hash'))} fn */
onUrlChange(fn) {
listeners.add(fn);
},
};
/** @this {string} type */
async function onNavigation(data) {
if (CHROME && data.timeStamp === prevData.timeStamp && deepEqual(data, prevData)) {
return; // Chrome bug: listener is called twice with identical data
}
prevData = data;
if (CHROME &&
URLS.chromeProtectsNTP &&
data.url.startsWith('https://www.google.') &&
data.url.includes('/_/chrome/newtab?')) {
// Modern Chrome switched to WebUI NTP so this is obsolete, but there may be exceptions
// TODO: investigate, and maybe use a separate listener for CHROME <= ver
const tab = await browser.tabs.get(data.tabId);
const url = tab.pendingUrl || tab.url;
if (url === 'chrome://newtab/') {
data.url = url;
}
}
listeners.forEach(fn => fn(data, this));
}
/** @this {string} type */
function onFakeNavigation(data) {
const {url, frameId} = data;
onNavigation.call(this, data);
msg.sendTab(data.tabId, {method: 'urlChanged', url}, {frameId})
.catch(msg.ignoreError);
}
})();
bgReady.all.then(() => {
/*
* Expose style version on greasyfork/sleazyfork 1) info page and 2) code page
* Not using manifest.json as adding a content script disables the extension on update.
*/
const urlMatches = '/scripts/\\d+[^/]*(/code)?([?#].*)?$';
chrome.webNavigation.onCommitted.addListener(({tabId}) => {
chrome.tabs.executeScript(tabId, {
file: '/content/install-hook-greasyfork.js',
runAt: 'document_start',
});
}, {
url: [
{hostEquals: 'greasyfork.org', urlMatches},
{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
*/
if (FIREFOX) {
chrome.webNavigation.onDOMContentLoaded.addListener(async ({tabId, frameId}) => {
if (frameId &&
!await msg.sendTab(tabId, {method: 'ping'}, {frameId}).catch(ignoreChromeError)) {
for (const file of chrome.runtime.getManifest().content_scripts[0].js) {
chrome.tabs.executeScript(tabId, {
frameId,
file,
matchAboutBlank: true,
}, ignoreChromeError);
}
}
}, {
url: [{urlEquals: 'about:blank'}],
});
}
});

View File

@ -1,75 +0,0 @@
/* global promisify CHROME URLS */
/* exported navigatorUtil */
'use strict';
const navigatorUtil = (() => {
const handler = {
urlChange: null
};
const tabGet = promisify(chrome.tabs.get.bind(chrome.tabs));
return extendNative({onUrlChange});
function onUrlChange(fn) {
initUrlChange();
handler.urlChange.push(fn);
}
function initUrlChange() {
if (handler.urlChange) {
return;
}
handler.urlChange = [];
chrome.webNavigation.onCommitted.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'committed'))
.catch(console.error)
);
chrome.webNavigation.onHistoryStateUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'historyStateUpdated'))
.catch(console.error)
);
chrome.webNavigation.onReferenceFragmentUpdated.addListener(data =>
fixNTPUrl(data)
.then(() => executeCallbacks(handler.urlChange, data, 'referenceFragmentUpdated'))
.catch(console.error)
);
}
function fixNTPUrl(data) {
if (
!CHROME ||
!URLS.chromeProtectsNTP ||
!data.url.startsWith('https://www.google.') ||
!data.url.includes('/_/chrome/newtab?')
) {
return Promise.resolve();
}
return tabGet(data.tabId)
.then(tab => {
if (tab.url === 'chrome://newtab/') {
data.url = tab.url;
}
});
}
function executeCallbacks(callbacks, data, type) {
for (const cb of callbacks) {
cb(data, type);
}
}
function extendNative(target) {
return new Proxy(target, {
get: (target, prop) => {
if (target[prop]) {
return target[prop];
}
return chrome.webNavigation[prop].addListener.bind(chrome.webNavigation[prop]);
}
});
}
})();

View File

@ -1,102 +0,0 @@
'use strict';
(() => {
// 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 = ({id}, queryString) => {
const query = gql(queryString);
return fetch(api, {
method: 'POST',
headers: new Headers({
'Content-Type': 'application/json'
}),
body: query({
id
})
})
.then(res => res.json());
};
window.API_METHODS = Object.assign(window.API_METHODS || {}, {
/**
* 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,104 +0,0 @@
/* global API_METHODS styleManager tryRegExp debounce */
'use strict';
(() => {
// toLocaleLowerCase cache, autocleared after 1 minute
const cache = new Map();
// top-level style properties to be searched
const PARTS = {
name: searchText,
url: searchText,
sourceCode: searchText,
sections: searchSections,
};
/**
* @param params
* @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
* @param {number[]} [params.ids] - if not specified, all styles are searched
* @returns {number[]} - array of matched styles ids
*/
API_METHODS.searchDB = ({query, ids}) => {
let rx, words, icase, matchUrl;
query = query.trim();
if (/^url:/i.test(query)) {
matchUrl = query.slice(query.indexOf(':') + 1).trim();
if (matchUrl) {
return styleManager.getStylesByUrl(matchUrl)
.then(results => results.map(r => r.data.id));
}
}
if (query.startsWith('/') && /^\/(.+?)\/([gimsuy]*)$/.test(query)) {
rx = tryRegExp(RegExp.$1, RegExp.$2);
}
if (!rx) {
words = query
.split(/(".*?")|\s+/)
.filter(Boolean)
.map(w => w.startsWith('"') && w.endsWith('"')
? w.slice(1, -1)
: w)
.filter(w => w.length > 1);
words = words.length ? words : [query];
icase = words.some(w => w === lower(w));
}
return styleManager.getAllStyles().then(styles => {
if (ids) {
const idSet = new Set(ids);
styles = styles.filter(s => idSet.has(s.id));
}
const results = [];
for (const style of styles) {
const id = style.id;
if (!query || words && !words.length) {
results.push(id);
continue;
}
for (const part in PARTS) {
const text = style[part];
if (text && PARTS[part](text, rx, words, icase)) {
results.push(id);
break;
}
}
}
if (cache.size) debounce(clearCache, 60e3);
return results;
});
};
function searchText(text, rx, words, icase) {
if (rx) return rx.test(text);
for (let pass = 1; pass <= (icase ? 2 : 1); pass++) {
if (words.every(w => text.includes(w))) return true;
text = lower(text);
}
}
function searchSections(sections, rx, words, icase) {
for (const section of sections) {
for (const prop in section) {
const value = section[prop];
if (typeof value === 'string') {
if (searchText(value, rx, words, icase)) return true;
} else if (Array.isArray(value)) {
if (value.some(str => searchText(str, rx, words, icase))) return true;
}
}
}
}
function lower(text) {
let result = cache.get(text);
if (result) return result;
result = text.toLocaleLowerCase();
cache.set(text, result);
return result;
}
function clearCache() {
cache.clear();
}
})();

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,108 @@
/* global API */// msg.js
/* global RX_META debounce stringAsRegExp tryRegExp */// toolbox.js
/* global addAPI */// common.js
'use strict';
(() => {
// toLocaleLowerCase cache, autocleared after 1 minute
const cache = new Map();
const METAKEYS = ['customName', 'name', 'url', 'installationUrl', 'updateUrl'];
const extractMeta = style =>
style.usercssData
? (style.sourceCode.match(RX_META) || [''])[0]
: null;
const stripMeta = style =>
style.usercssData
? style.sourceCode.replace(RX_META, '')
: null;
const MODES = Object.assign(Object.create(null), {
code: (style, test) =>
style.usercssData
? test(stripMeta(style))
: searchSections(style, test, 'code'),
meta: (style, test, part) =>
METAKEYS.some(key => test(style[key])) ||
test(part === 'all' ? style.sourceCode : extractMeta(style)) ||
searchSections(style, test, 'funcs'),
name: (style, test) =>
test(style.customName) ||
test(style.name),
all: (style, test) =>
MODES.meta(style, test, 'all') ||
!style.usercssData && MODES.code(style, test),
});
addAPI(/** @namespace API */ {
styles: {
/**
* @param params
* @param {string} params.query - 1. url:someurl 2. text (may contain quoted parts like "qUot Ed")
* @param {'name'|'meta'|'code'|'all'|'url'} [params.mode=all]
* @param {number[]} [params.ids] - if not specified, all styles are searched
* @returns {number[]} - array of matched styles ids
*/
async searchDB({query, mode = 'all', ids}) {
let res = [];
if (mode === 'url' && query) {
res = (await API.styles.getByUrl(query)).map(r => r.style.id);
} else if (mode in MODES) {
const modeHandler = MODES[mode];
const m = /^\/(.+?)\/([gimsuy]*)$/.exec(query);
const rx = m && tryRegExp(m[1], m[2]);
const test = rx ? rx.test.bind(rx) : createTester(query);
res = (await API.styles.getAll())
.filter(style =>
(!ids || ids.includes(style.id)) &&
(!query || modeHandler(style, test)))
.map(style => style.id);
if (cache.size) debounce(clearCache, 60e3);
}
return res;
},
},
});
function createTester(query) {
const flags = `u${lower(query) === query ? 'i' : ''}`;
const words = query
.split(/(".*?")|\s+/)
.filter(Boolean)
.map(w => w.startsWith('"') && w.endsWith('"')
? w.slice(1, -1)
: w)
.filter(w => w.length > 1);
const rxs = (words.length ? words : [query])
.map(w => stringAsRegExp(w, flags));
return text => rxs.every(rx => rx.test(text));
}
function searchSections({sections}, test, part) {
const inCode = part === 'code' || part === 'all';
const inFuncs = part === 'funcs' || part === 'all';
for (const section of sections) {
for (const prop in section) {
const value = section[prop];
if (inCode && prop === 'code' && test(value) ||
inFuncs && Array.isArray(value) && value.some(str => test(str))) {
return true;
}
}
}
}
function lower(text) {
let result = cache.get(text);
if (!result) cache.set(text, result = text.toLocaleLowerCase());
return result;
}
function clearCache() {
cache.clear();
}
})();

View File

@ -1,7 +1,14 @@
/* global API_METHODS styleManager CHROME prefs */ /* global API */// msg.js
/* global addAPI */// common.js
/* global isEmptyObj */// toolbox.js
/* global prefs */
'use strict'; 'use strict';
API_METHODS.styleViaAPI = !CHROME && (() => { /**
* Uses chrome.tabs.insertCSS
*/
(() => {
const ACTIONS = { const ACTIONS = {
styleApply, styleApply,
styleDeleted, styleDeleted,
@ -11,25 +18,25 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
prefChanged, prefChanged,
updateCount, updateCount,
}; };
const NOP = Promise.resolve(new Error('NOP')); const NOP = new Error('NOP');
const onError = () => {}; const onError = () => {};
/* <tabId>: Object /* <tabId>: Object
<frameId>: Object <frameId>: Object
url: String, non-enumerable url: String, non-enumerable
<styleId>: Array of strings <styleId>: Array of strings
section code */ section code */
const cache = new Map(); const cache = new Map();
let observingTabs = false; let observingTabs = false;
return function (request) { addAPI(/** @namespace API */ {
const action = ACTIONS[request.method]; async styleViaAPI(request) {
return !action ? NOP : try {
action(request, this.sender) const fn = ACTIONS[request.method];
.catch(onError) return fn ? fn(request, this.sender) : NOP;
.then(maybeToggleObserver); } catch (e) {}
}; maybeToggleObserver();
},
});
function updateCount(request, sender) { function updateCount(request, sender) {
const {tab, frameId} = sender; const {tab, frameId} = sender;
@ -37,7 +44,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
throw new Error('we do not count styles for frames'); throw new Error('we do not count styles for frames');
} }
const {frameStyles} = getCachedData(tab.id, frameId); const {frameStyles} = getCachedData(tab.id, frameId);
API_METHODS.updateIconBadge.call({sender}, Object.keys(frameStyles)); API.updateIconBadge.call({sender}, Object.keys(frameStyles));
} }
function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) { function styleApply({id = null, ignoreUrlCheck = false}, {tab, frameId, url}) {
@ -48,7 +55,8 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
if (id === null && !ignoreUrlCheck && frameStyles.url === url) { if (id === null && !ignoreUrlCheck && frameStyles.url === url) {
return NOP; return NOP;
} }
return styleManager.getSectionsByUrl(url, id).then(sections => { return API.styles.getSectionsByUrl(url, id).then(sections => {
delete sections.cfg;
const tasks = []; const tasks = [];
for (const section of Object.values(sections)) { for (const section of Object.values(sections)) {
const styleId = section.id; const styleId = section.id;
@ -125,7 +133,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
} }
const {tab, frameId} = sender; const {tab, frameId} = sender;
const {tabFrames, frameStyles} = getCachedData(tab.id, frameId); const {tabFrames, frameStyles} = getCachedData(tab.id, frameId);
if (isEmpty(frameStyles)) { if (isEmptyObj(frameStyles)) {
return NOP; return NOP;
} }
removeFrameIfEmpty(tab.id, frameId, tabFrames, {}); removeFrameIfEmpty(tab.id, frameId, tabFrames, {});
@ -162,7 +170,7 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
const tabFrames = cache.get(tabId); const tabFrames = cache.get(tabId);
if (tabFrames && frameId in tabFrames) { if (tabFrames && frameId in tabFrames) {
delete tabFrames[frameId]; delete tabFrames[frameId];
if (isEmpty(tabFrames)) { if (isEmptyObj(tabFrames)) {
onTabRemoved(tabId); onTabRemoved(tabId);
} }
} }
@ -178,9 +186,9 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
} }
function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) { function removeFrameIfEmpty(tabId, frameId, tabFrames, frameStyles) {
if (isEmpty(frameStyles)) { if (isEmptyObj(frameStyles)) {
delete tabFrames[frameId]; delete tabFrames[frameId];
if (isEmpty(tabFrames)) { if (isEmptyObj(tabFrames)) {
cache.delete(tabId); cache.delete(tabId);
} }
return true; return true;
@ -223,11 +231,4 @@ API_METHODS.styleViaAPI = !CHROME && (() => {
return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true}) return browser.tabs.removeCSS(tabId, {frameId, code, matchAboutBlank: true})
.catch(onError); .catch(onError);
} }
function isEmpty(obj) {
for (const k in obj) {
return false;
}
return true;
}
})(); })();

View File

@ -0,0 +1,166 @@
/* global API */// msg.js
/* global CHROME URLS ignoreChromeError */// toolbox.js
/* global prefs */
'use strict';
(() => {
const idCSP = 'patchCsp';
const idOFF = 'disableAll';
const idXHR = 'styleViaXhr';
const rxHOST = /^('none'|(https?:\/\/)?[^']+?[^:'])$/; // strips CSP sources covered by *
const blobUrlPrefix = 'blob:' + chrome.runtime.getURL('/');
/** @type {Object<string,StylesToPass>} */
const stylesToPass = {};
const state = {};
const injectedCode = CHROME && `${data => {
if (self.INJECTED !== 1) { // storing data only if apply.js hasn't run yet
window[Symbol.for('styles')] = data;
}
}}`;
toggle();
prefs.subscribe([idXHR, idOFF, idCSP], toggle);
function toggle() {
const off = prefs.get(idOFF);
const csp = prefs.get(idCSP) && !off;
const xhr = prefs.get(idXHR) && !off;
if (xhr === state.xhr && csp === state.csp && off === state.off) {
return;
}
const reqFilter = {
urls: ['*://*/*'],
types: ['main_frame', 'sub_frame'],
};
chrome.webNavigation.onCommitted.removeListener(injectData);
chrome.webRequest.onBeforeRequest.removeListener(prepareStyles);
chrome.webRequest.onHeadersReceived.removeListener(modifyHeaders);
if (xhr || csp) {
// We unregistered it above so that the optional EXTRA_HEADERS is properly re-registered
chrome.webRequest.onHeadersReceived.addListener(modifyHeaders, reqFilter, [
'blocking',
'responseHeaders',
xhr && chrome.webRequest.OnHeadersReceivedOptions.EXTRA_HEADERS,
].filter(Boolean));
}
if (CHROME ? !off : xhr || csp) {
chrome.webRequest.onBeforeRequest.addListener(prepareStyles, reqFilter);
}
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;
}
/** @param {chrome.webRequest.WebRequestBodyDetails} req */
async function prepareStyles(req) {
const sections = await API.styles.getSectionsByUrl(req.url);
stylesToPass[req2key(req)] = /** @namespace StylesToPass */ {
blobId: '',
str: JSON.stringify(sections),
timer: setTimeout(cleanUp, 600e3, req),
};
}
function injectData(req) {
const data = stylesToPass[req2key(req)];
if (data && !data.injected) {
data.injected = true;
chrome.tabs.executeScript(req.tabId, {
frameId: req.frameId,
runAt: 'document_start',
code: `(${injectedCode})(${data.str})`,
}, ignoreChromeError);
if (!state.xhr) cleanUp(req);
}
}
/** @param {chrome.webRequest.WebResponseHeadersDetails} req */
function modifyHeaders(req) {
const {responseHeaders} = req;
const data = stylesToPass[req2key(req)];
if (!data || data.str === '{}') {
cleanUp(req);
return;
}
if (state.xhr) {
data.blobId = URL.createObjectURL(new Blob([data.str])).slice(blobUrlPrefix.length);
responseHeaders.push({
name: 'Set-Cookie',
value: `${chrome.runtime.id}=${data.blobId}`,
});
}
const csp = state.csp &&
responseHeaders.find(h => h.name.toLowerCase() === 'content-security-policy');
if (csp) {
patchCsp(csp);
}
if (state.xhr || csp) {
return {responseHeaders};
}
}
/** @param {chrome.webRequest.HttpHeader} csp */
function patchCsp(csp) {
const src = {};
for (let p of csp.value.split(';')) {
p = p.trim().split(/\s+/);
src[p[0]] = p.slice(1);
}
// Allow style assets
patchCspSrc(src, 'img-src', 'data:', '*');
patchCspSrc(src, 'font-src', 'data:', '*');
// 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');
}
csp.value = Object.entries(src).map(([k, v]) =>
`${k}${v.length ? ' ' : ''}${v.join(' ')}`).join('; ');
}
function patchCspSrc(src, name, ...values) {
let def = src['default-src'];
let list = src[name];
if (def || list) {
if (!def) def = [];
if (!list) list = [...def];
if (values.includes('*')) list = src[name] = list.filter(v => !rxHOST.test(v));
list.push(...values.filter(v => !list.includes(v) && !def.includes(v)));
if (!list.length) delete src[name];
}
}
function cleanUp(req) {
const key = req2key(req);
const data = stylesToPass[key];
if (data) {
delete stylesToPass[key];
clearTimeout(data.timer);
if (data.blobId) {
URL.revokeObjectURL(blobUrlPrefix + data.blobId);
}
}
}
/** @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;
}
})();

307
background/sync-manager.js Normal file
View File

@ -0,0 +1,307 @@
/* global API msg */// msg.js
/* global bgReady uuidIndex */// common.js
/* global chromeLocal chromeSync */// storage-util.js
/* global db */
/* global iconMan */
/* global prefs */
/* global styleUtil */
/* global tokenMan */
'use strict';
const syncMan = (() => {
//#region Init
const SYNC_DELAY = 1; // minutes
const SYNC_INTERVAL = 30; // minutes
const STATES = Object.freeze({
connected: 'connected',
connecting: 'connecting',
disconnected: 'disconnected',
disconnecting: 'disconnecting',
});
const STORAGE_KEY = 'sync/state/';
const NO_LOGIN = ['webdav'];
const status = /** @namespace SyncManager.Status */ {
STATES,
state: STATES.disconnected,
syncing: false,
progress: null,
currentDriveName: null,
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 = bgReady.styles.then(() => {
ready = true;
prefs.subscribe('sync.enabled',
(_, val) => val === 'none'
? syncMan.stop()
: syncMan.start(val, true),
{runNow: true});
});
chrome.alarms.onAlarm.addListener(async ({name}) => {
if (name === 'syncNow') {
await syncMan.syncNow();
}
});
//#endregion
//#region Exports
return {
async delete(...args) {
if (ready.then) await ready;
if (!currentDrive) return;
schedule();
return ctrl.delete(...args);
},
/** @returns {Promise<SyncManager.Status>} */
async getStatus() {
return status;
},
async login(name) {
if (ready.then) await ready;
if (!name) name = prefs.get('sync.enabled');
await tokenMan.revokeToken(name);
try {
await tokenMan.getToken(name, true);
status.login = true;
} catch (err) {
status.login = false;
throw err;
} finally {
emitStatusChange();
}
},
async putDoc({_id, _rev}) {
if (ready.then) await ready;
if (!currentDrive) return;
schedule();
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) {
if (ready.then) await ready;
if (!ctrl) await initController();
if (currentDrive) return;
currentDrive = await getDrive(name);
ctrl.use(currentDrive);
status.state = STATES.connecting;
status.currentDriveName = currentDrive.name;
emitStatusChange();
if (fromPref || NO_LOGIN.includes(currentDrive.name)) {
status.login = true;
} else {
try {
await syncMan.login(name);
} catch (err) {
console.error(err);
status.errorMessage = err.message;
lastError = err;
emitStatusChange();
return syncMan.stop();
}
}
await ctrl.init();
await syncMan.syncNow(name);
prefs.set('sync.enabled', name);
status.state = STATES.connected;
schedule(SYNC_INTERVAL);
emitStatusChange();
},
async stop() {
if (ready.then) await ready;
if (!currentDrive) return;
chrome.alarms.clear('syncNow');
status.state = STATES.disconnecting;
emitStatusChange();
try {
await ctrl.uninit();
await tokenMan.revokeToken(currentDrive.name);
await chromeLocal.remove(STORAGE_KEY + currentDrive.name);
} catch (e) {}
currentDrive = null;
prefs.set('sync.enabled', 'none');
status.state = STATES.disconnected;
status.currentDriveName = null;
status.login = false;
emitStatusChange();
},
async syncNow() {
if (ready.then) await ready;
if (!currentDrive || !status.login) {
console.warn('cannot sync when disconnected');
return;
}
try {
await ctrl.syncNow();
status.errorMessage = null;
lastError = null;
} catch (err) {
err.message = translateErrorMessage(err);
status.errorMessage = err.message;
lastError = err;
if (isGrantError(err)) {
status.login = false;
}
}
emitStatusChange();
},
};
//#endregion
//#region Utils
async function initController() {
await require(['/vendor/db-to-cloud/db-to-cloud']); /* global dbToCloud */
ctrl = dbToCloud.dbToCloud({
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'});
}
},
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 Object.values(uuidIndex.custom).concat(await API.styles.getAll())) {
ctrl.put(i._id, i._rev);
}
},
onProgress(e) {
if (e.phase === 'start') {
status.syncing = true;
} else if (e.phase === 'end') {
status.syncing = false;
status.progress = null;
} else {
status.progress = e;
}
emitStatusChange();
},
compareRevision,
getState(drive) {
return chromeLocal.getValue(STORAGE_KEY + drive.name);
},
setState(drive, state) {
return chromeLocal.setValue(STORAGE_KEY + drive.name, state);
},
retryMaxAttempts: 10,
retryExp: 1.2,
retryDelay: 6,
});
}
function emitStatusChange() {
msg.broadcastExtension({method: 'syncStatusUpdate', status});
iconMan.overrideBadge(getErrorBadge());
}
function isNetworkError(err) {
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;
}
function getErrorBadge() {
if (status.state === STATES.connected &&
(!status.login || lastError && !isNetworkError(lastError))) {
return {
text: 'x',
color: '#F00',
title: !status.login ? 'syncErrorRelogin' : `${
chrome.i18n.getMessage('syncError')
}\n---------------------\n${
// splitting to limit each line length
lastError.message.replace(/.{60,}?\s(?=.{30,})/g, '$&\n')
}`,
};
}
}
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, // 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,238 +0,0 @@
/* global dbToCloud styleManager chromeLocal prefs tokenManager msg */
/* exported sync */
'use strict';
const sync = (() => {
const SYNC_DELAY = 1; // minutes
const SYNC_INTERVAL = 30; // minutes
const status = {
state: 'disconnected',
syncing: false,
progress: null,
currentDriveName: null,
errorMessage: null,
login: false
};
let currentDrive;
const ctrl = dbToCloud.dbToCloud({
onGet(id) {
return styleManager.getByUUID(id);
},
onPut(doc) {
return styleManager.putByUUID(doc);
},
onDelete(id, rev) {
return styleManager.deleteByUUID(id, rev);
},
onFirstSync() {
return styleManager.getAllStyles()
.then(styles => {
styles.forEach(i => ctrl.put(i._id, i._rev));
});
},
onProgress,
compareRevision(a, b) {
return styleManager.compareRevision(a, b);
},
getState(drive) {
const key = `sync/state/${drive.name}`;
return chromeLocal.get(key)
.then(obj => obj[key]);
},
setState(drive, state) {
const key = `sync/state/${drive.name}`;
return chromeLocal.set({
[key]: state
});
}
});
const initializing = prefs.initializing.then(() => {
prefs.subscribe(['sync.enabled'], onPrefChange);
onPrefChange(null, prefs.get('sync.enabled'));
});
chrome.alarms.onAlarm.addListener(info => {
if (info.name === 'syncNow') {
syncNow().catch(console.error);
}
});
return Object.assign({
getStatus: () => status
}, ensurePrepared({
start,
stop,
put: (...args) => {
if (!currentDrive) return;
schedule();
return ctrl.put(...args);
},
delete: (...args) => {
if (!currentDrive) return;
schedule();
return ctrl.delete(...args);
},
syncNow,
login
}));
function ensurePrepared(obj) {
return Object.entries(obj).reduce((o, [key, fn]) => {
o[key] = (...args) =>
initializing.then(() => fn(...args));
return o;
}, {});
}
function onProgress(e) {
if (e.phase === 'start') {
status.syncing = true;
} else if (e.phase === 'end') {
status.syncing = false;
status.progress = null;
} else {
status.progress = e;
}
emitStatusChange();
}
function schedule(delay = SYNC_DELAY) {
chrome.alarms.create('syncNow', {
delayInMinutes: delay,
periodInMinutes: SYNC_INTERVAL
});
}
function onPrefChange(key, value) {
if (value === 'none') {
stop().catch(console.error);
} else {
start(value, true).catch(console.error);
}
}
function withFinally(p, cleanup) {
return p.then(
result => {
cleanup(undefined, result);
return result;
},
err => {
cleanup(err);
throw err;
}
);
}
function syncNow() {
if (!currentDrive) {
return Promise.reject(new Error('cannot sync when disconnected'));
}
return withFinally(
(ctrl.isInit() ? ctrl.syncNow() : ctrl.start())
.catch(handle401Error),
err => {
status.errorMessage = err ? err.message : null;
emitStatusChange();
}
);
}
function handle401Error(err) {
if (err.code === 401) {
return tokenManager.revokeToken(currentDrive.name)
.catch(console.error)
.then(() => {
status.login = false;
emitStatusChange();
throw err;
});
}
if (/User interaction required|Requires user interaction/i.test(err.message)) {
status.login = false;
emitStatusChange();
}
throw err;
}
function emitStatusChange() {
msg.broadcastExtension({method: 'syncStatusUpdate', status});
}
function login(name = prefs.get('sync.enabled')) {
return tokenManager.getToken(name, true)
.catch(err => {
if (/Authorization page could not be loaded/i.test(err.message)) {
// FIXME: Chrome always fails at the first login so we try again
return tokenManager.getToken(name);
}
throw err;
})
.then(() => {
status.login = true;
emitStatusChange();
});
}
function start(name, fromPref = false) {
if (currentDrive) {
return Promise.resolve();
}
currentDrive = getDrive(name);
ctrl.use(currentDrive);
status.state = 'connecting';
status.currentDriveName = currentDrive.name;
status.login = true;
emitStatusChange();
return withFinally(
(fromPref ? Promise.resolve() : login(name))
.catch(handle401Error)
.then(() => syncNow()),
err => {
// FIXME: should we move this logic to options.js?
if (err && !fromPref) {
console.error(err);
return stop();
}
prefs.set('sync.enabled', name);
schedule(SYNC_INTERVAL);
status.state = 'connected';
emitStatusChange();
}
);
}
function getDrive(name) {
if (name === 'dropbox' || name === 'google' || name === 'onedrive') {
return dbToCloud.drive[name]({
getAccessToken: () => tokenManager.getToken(name)
});
}
throw new Error(`unknown cloud name: ${name}`);
}
function stop() {
if (!currentDrive) {
return Promise.resolve();
}
chrome.alarms.clear('syncNow');
status.state = 'disconnecting';
emitStatusChange();
return withFinally(
ctrl.stop()
.then(() => tokenManager.revokeToken(currentDrive.name))
.then(() => chromeLocal.remove(`sync/state/${currentDrive.name}`)),
() => {
currentDrive = null;
prefs.set('sync.enabled', 'none');
status.state = 'disconnected';
status.currentDriveName = null;
status.login = false;
emitStatusChange();
}
);
}
})();

View File

@ -1,32 +1,37 @@
/* global navigatorUtil */ /* global bgReady */// common.js
/* exported tabManager */ /* global navMan */
'use strict'; 'use strict';
const tabManager = (() => { const tabMan = (() => {
const listeners = []; const listeners = new Set();
const cache = new Map(); const cache = new Map();
chrome.tabs.onRemoved.addListener(tabId => cache.delete(tabId)); chrome.tabs.onRemoved.addListener(tabId => cache.delete(tabId));
chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed)); chrome.tabs.onReplaced.addListener((added, removed) => cache.delete(removed));
navigatorUtil.onUrlChange(({tabId, frameId, url}) => {
if (frameId) return; bgReady.all.then(() => {
const oldUrl = tabManager.get(tabId, 'url'); navMan.onUrlChange(({tabId, frameId, url}) => {
tabManager.set(tabId, 'url', url); const oldUrl = !frameId && tabMan.get(tabId, 'url', frameId);
for (const fn of listeners) { tabMan.set(tabId, 'url', frameId, url);
try { if (frameId) return;
fn({tabId, url, oldUrl}); for (const fn of listeners) {
} catch (err) { try {
console.error(err); fn({tabId, url, oldUrl});
} catch (err) {
console.error(err);
}
} }
} });
}); });
return { return {
onUpdate(fn) { onUpdate(fn) {
listeners.push(fn); listeners.add(fn);
}, },
get(tabId, ...keys) { get(tabId, ...keys) {
return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId)); return keys.reduce((meta, key) => meta && meta[key], cache.get(tabId));
}, },
/** /**
* number of keys is arbitrary, last arg is value, `undefined` will delete the last key from meta * number of keys is arbitrary, last arg is value, `undefined` will delete the last key from meta
* (tabId, 'foo', 123) will set tabId's meta to {foo: 123}, * (tabId, 'foo', 123) will set tabId's meta to {foo: 123},
@ -47,8 +52,11 @@ const tabManager = (() => {
meta[lastKey] = value; meta[lastKey] = value;
} }
}, },
/** @returns {IterableIterator<number>} */
list() { list() {
return cache.keys(); return cache.keys();
}, },
}; };
})(); })();

View File

@ -1,9 +1,9 @@
/* global chromeLocal promisify FIREFOX */ /* global FIREFOX getActiveTab waitForTabUrl URLS */// toolbox.js
/* exported tokenManager */ /* global chromeLocal */// storage-util.js
'use strict'; 'use strict';
const tokenManager = (() => { /* exported tokenMan */
const launchWebAuthFlow = promisify(chrome.identity.launchWebAuthFlow.bind(chrome.identity)); const tokenMan = (() => {
const AUTH = { const AUTH = {
dropbox: { dropbox: {
flow: 'token', flow: 'token',
@ -14,9 +14,9 @@ const tokenManager = (() => {
fetch('https://api.dropboxapi.com/2/auth/token/revoke', { fetch('https://api.dropboxapi.com/2/auth/token/revoke', {
method: 'POST', method: 'POST',
headers: { headers: {
'Authorization': `Bearer ${token}` 'Authorization': `Bearer ${token}`,
} },
}) }),
}, },
google: { google: {
flow: 'code', flow: 'code',
@ -28,14 +28,15 @@ const tokenManager = (() => {
// tokens for multiple machines. // tokens for multiple machines.
// https://stackoverflow.com/q/18519185 // https://stackoverflow.com/q/18519185
access_type: 'offline', access_type: 'offline',
prompt: 'consent' prompt: 'consent',
}, },
tokenURL: 'https://oauth2.googleapis.com/token', tokenURL: 'https://oauth2.googleapis.com/token',
scopes: ['https://www.googleapis.com/auth/drive.appdata'], scopes: ['https://www.googleapis.com/auth/drive.appdata'],
revoke: token => { // FIXME: https://github.com/openstyles/stylus/issues/1248
const params = {token}; // revoke: token => {
return postQuery(`https://accounts.google.com/o/oauth2/revoke?${stringifyQuery(params)}`); // const params = {token};
} // return postQuery(`https://accounts.google.com/o/oauth2/revoke?${new URLSearchParams(params)}`);
// },
}, },
onedrive: { onedrive: {
flow: 'code', flow: 'code',
@ -43,113 +44,115 @@ const tokenManager = (() => {
clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w', clientSecret: '9Pj=TpsrStq8K@1BiwB9PIWLppM:@s=w',
authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize', authURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/authorize',
tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token', tokenURL: 'https://login.microsoftonline.com/common/oauth2/v2.0/token',
redirect_uri: FIREFOX ? scopes: ['Files.ReadWrite.AppFolder', 'offline_access'],
'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/' : },
'https://' + location.hostname + '.chromiumapp.org/', userstylesworld: {
scopes: ['Files.ReadWrite.AppFolder', 'offline_access'] 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 NETWORK_LATENCY = 30; // seconds
const DEFAULT_REDIRECT_URI = 'https://clngdbkpkpeebahjckkjfobafhncgmne.chromiumapp.org/';
return {getToken, revokeToken, getClientId, buildKeys}; let alwaysUseTab = !chrome.windows || (FIREFOX ? false : null);
function getClientId(name) { class TokenError extends Error {
return AUTH[name].clientId; constructor(provider, message) {
super(`[${provider}] ${message}`);
this.name = 'TokenError';
this.provider = provider;
if (Error.captureStackTrace) {
Error.captureStackTrace(this, TokenError);
}
}
} }
function buildKeys(name) { return {
const k = {
TOKEN: `secure/token/${name}/token`,
EXPIRE: `secure/token/${name}/expire`,
REFRESH: `secure/token/${name}/refresh`,
};
k.LIST = Object.values(k);
return k;
}
function getToken(name, interactive) { buildKeys(name, hooks) {
const k = buildKeys(name); const prefix = `secure/token/${hooks ? hooks.keyName(name) : name}/`;
return chromeLocal.get(k.LIST) const k = {
.then(obj => { TOKEN: `${prefix}token`,
if (!obj[k.TOKEN]) { EXPIRE: `${prefix}expire`,
return authUser(name, k, interactive); REFRESH: `${prefix}refresh`,
} };
k.LIST = Object.values(k);
return k;
},
getClientId(name) {
return AUTH[name].clientId;
},
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]) { if (!obj[k.EXPIRE] || Date.now() < obj[k.EXPIRE]) {
return obj[k.TOKEN]; return obj[k.TOKEN];
} }
if (obj[k.REFRESH]) { if (obj[k.REFRESH]) {
return refreshToken(name, k, obj) return refreshToken(name, k, obj);
.catch(err => {
if (err.code === 401) {
return authUser(name, k, interactive);
}
throw err;
});
} }
return authUser(name, k, interactive);
});
}
function revokeToken(name) {
const provider = AUTH[name];
const k = buildKeys(name);
return revoke()
.then(() => chromeLocal.remove(k.LIST));
function revoke() {
if (!provider.revoke) {
return Promise.resolve();
} }
return chromeLocal.get(k.TOKEN) if (!interactive) {
.then(obj => { throw new TokenError(name, 'Token is missing');
if (obj[k.TOKEN]) { }
return provider.revoke(obj[k.TOKEN]); return authUser(k, name, interactive, hooks);
} },
})
.catch(console.error);
}
}
function refreshToken(name, k, obj) { async revokeToken(name, hooks) {
const provider = AUTH[name];
const k = tokenMan.buildKeys(name, hooks);
if (provider.revoke) {
try {
const token = await chromeLocal.getValue(k.TOKEN);
if (token) await provider.revoke(token);
} catch (e) {
console.error(e);
}
}
await chromeLocal.remove(k.LIST);
},
};
async function refreshToken(name, k, obj) {
if (!obj[k.REFRESH]) { if (!obj[k.REFRESH]) {
return Promise.reject(new Error('no refresh token')); throw new TokenError(name, 'No refresh token');
} }
const provider = AUTH[name]; const provider = AUTH[name];
const body = { const body = {
client_id: provider.clientId, client_id: provider.clientId,
refresh_token: obj[k.REFRESH], refresh_token: obj[k.REFRESH],
grant_type: 'refresh_token', grant_type: 'refresh_token',
scope: provider.scopes.join(' ') scope: provider.scopes.join(' '),
}; };
if (provider.clientSecret) { if (provider.clientSecret) {
body.client_secret = provider.clientSecret; body.client_secret = provider.clientSecret;
} }
return postQuery(provider.tokenURL, body) const result = await postQuery(provider.tokenURL, body);
.then(result => { if (!result.refresh_token) {
if (!result.refresh_token) { // reuse old refresh token
// reuse old refresh token result.refresh_token = obj[k.REFRESH];
result.refresh_token = obj[k.REFRESH];
}
return handleTokenResult(result, k);
});
}
function stringifyQuery(obj) {
const search = new URLSearchParams();
for (const key of Object.keys(obj)) {
search.set(key, obj[key]);
} }
return search.toString(); return handleTokenResult(result, k);
} }
function authUser(name, k, interactive = false) { 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 provider = AUTH[name];
const state = Math.random().toFixed(8).slice(2); const state = Math.random().toFixed(8).slice(2);
const query = { const query = {
response_type: provider.flow, response_type: provider.flow,
client_id: provider.clientId, client_id: provider.clientId,
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL(), redirect_uri: provider.redirect_uri || DEFAULT_REDIRECT_URI,
state state,
}; };
if (provider.scopes) { if (provider.scopes) {
query.scope = provider.scopes.join(' '); query.scope = provider.scopes.join(' ');
@ -157,72 +160,111 @@ const tokenManager = (() => {
if (provider.authQuery) { if (provider.authQuery) {
Object.assign(query, provider.authQuery); Object.assign(query, provider.authQuery);
} }
const url = `${provider.authURL}?${stringifyQuery(query)}`; if (alwaysUseTab == null) {
return launchWebAuthFlow({ 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, url,
interactive alwaysUseTab,
}) interactive,
.then(url => { redirect_uri: query.redirect_uri,
const params = new URLSearchParams( windowOptions: wnd && Object.assign({
provider.flow === 'token' ? state: 'normal',
new URL(url).hash.slice(1) : width,
new URL(url).search.slice(1) height,
); }, wnd.state !== 'minimized' && {
if (params.get('state') !== state) { // Center the popup to the current window
throw new Error(`unexpected state: ${params.get('state')}, expected: ${state}`); top: Math.ceil(wnd.top + (wnd.height - width) / 2),
} left: Math.ceil(wnd.left + (wnd.width - width) / 2),
if (provider.flow === 'token') { }),
const obj = {}; });
for (const [key, value] of params.entries()) { const params = new URLSearchParams(
obj[key] = value; provider.flow === 'token' ?
} new URL(finalUrl).hash.slice(1) :
return obj; new URL(finalUrl).search.slice(1)
} );
const code = params.get('code'); if (params.get('state') !== state) {
const body = { throw new TokenError(name, `Unexpected state: ${params.get('state')}, expected: ${state}`);
code, }
grant_type: 'authorization_code', let result;
client_id: provider.clientId, if (provider.flow === 'token') {
redirect_uri: provider.redirect_uri || chrome.identity.getRedirectURL() const obj = {};
}; for (const [key, value] of params) {
if (provider.clientSecret) { obj[key] = value;
body.client_secret = provider.clientSecret; }
} result = obj;
return postQuery(provider.tokenURL, body); } else {
}) const code = params.get('code');
.then(result => handleTokenResult(result, k)); const body = {
code,
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, keys);
} }
function handleTokenResult(result, k) { async function handleTokenResult(result, k) {
return chromeLocal.set({ await chromeLocal.set({
[k.TOKEN]: result.access_token, [k.TOKEN]: result.access_token,
[k.EXPIRE]: result.expires_in ? Date.now() + (Number(result.expires_in) - NETWORK_LATENCY) * 1000 : undefined, [k.EXPIRE]: result.expires_in
[k.REFRESH]: result.refresh_token ? Date.now() + (result.expires_in - NETWORK_LATENCY) * 1000
}) : undefined,
.then(() => result.access_token); [k.REFRESH]: result.refresh_token,
});
return result.access_token;
} }
function postQuery(url, body) { async function postQuery(url, body) {
const options = { const options = {
method: 'POST', method: 'POST',
headers: { headers: {
'Content-Type': 'application/x-www-form-urlencoded' 'Content-Type': 'application/x-www-form-urlencoded',
} },
body: body ? new URLSearchParams(body) : null,
}; };
if (body) { const r = await fetch(url, options);
options.body = stringifyQuery(body); if (r.ok) {
return r.json();
} }
return fetch(url, options) const text = await r.text();
.then(r => { const err = new Error(`Failed to fetch (${r.status}): ${text}`);
if (r.ok) { err.code = r.status;
return r.json(); throw err;
} }
return r.text()
.then(body => { async function detectVivaldiWebRequestBug() {
const err = new Error(`failed to fetch (${r.status}): ${body}`); // Workaround for https://github.com/openstyles/stylus/issues/1182
err.code = r.status; // Note that modern Vivaldi isn't exposed in `navigator.userAgent` but it adds `extData` to tabs
throw err; const anyTab = await getActiveTab() || (await browser.tabs.query({}))[0];
}); if (anyTab && !(anyTab.extData || anyTab.vivExtData)) {
}); return false;
}
let bugged = true;
const TEST_URL = chrome.runtime.getURL('manifest.json');
const check = ({url}) => {
bugged = url !== TEST_URL;
};
chrome.webRequest.onBeforeRequest.addListener(check, {urls: [TEST_URL], types: ['main_frame']});
const {tabs: [tab]} = await browser.windows.create({
type: 'popup',
state: 'minimized',
url: TEST_URL,
});
await waitForTabUrl(tab);
chrome.windows.remove(tab.windowId);
chrome.webRequest.onBeforeRequest.removeListener(check);
return bugged;
} }
})(); })();

View File

@ -0,0 +1,348 @@
/* global API */// msg.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';
/* exported updateMan */
const updateMan = (() => {
const STATES = /** @namespace UpdaterStates */ {
UPDATED: 'updated',
SKIPPED: 'skipped',
UNREACHABLE: 'server unreachable',
// details for SKIPPED status
EDITED: 'locally edited',
MAYBE_EDITED: 'may be locally edited',
SAME_MD5: 'up-to-date: MD5 is unchanged',
SAME_CODE: 'up-to-date: code sections are unchanged',
SAME_VERSION: 'up-to-date: version is unchanged',
ERROR_MD5: 'error: MD5 is invalid',
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})/,
/(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;
const RETRY_ERRORS = [
503, // service unavailable
429, // too many requests
];
let usoReferers = 0;
let lastUpdateTime;
let checkingAll = false;
let logQueue = [];
let logLastWriteTime = 0;
chromeLocal.getValue('lastUpdateTime').then(val => {
lastUpdateTime = val || Date.now();
prefs.subscribe('updateInterval', schedule, {runNow: true});
chrome.alarms.onAlarm.addListener(onAlarm);
});
return {
checkAllStyles,
checkStyle,
getStates: () => STATES,
};
async function checkAllStyles({
save = true,
ignoreDigest,
observe,
} = {}) {
resetInterval();
checkingAll = true;
const port = observe && chrome.runtime.connect({name: 'updater'});
const styles = (await API.styles.getAll())
.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`);
await Promise.all(
styles.map(style =>
checkStyle({style, port, save, ignoreDigest})));
if (port) port.postMessage({done: true});
if (port) port.disconnect();
log('');
checkingAll = false;
}
/**
* @param {{
id?: number,
style?: StyleObj,
port?: chrome.runtime.Port,
save?: boolean,
ignoreDigest?: boolean,
}} opts
* @returns {{
style: StyleObj,
updated?: boolean,
error?: any,
STATES: UpdaterStates,
}}
Original style digests are calculated in these cases:
* style is installed or updated from server
* non-usercss style is checked for an update and styleSectionsEqual considers it unchanged
Update check proceeds in these cases:
* style has the original digest and it's equal to the current digest
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
* [ignoreDigest: none/false] style doesn't yet have the original digest
so we compare the code to the server code and if it's the same we save the digest,
otherwise we skip the style and report MAYBE_EDITED status
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
async function checkStyle(opts) {
let {id} = opts;
const {
style = await API.styles.get(id),
ignoreDigest,
port,
save,
} = opts;
if (!id) id = style.id;
const {md5Url} = style;
let {usercssData: ucd, updateUrl} = style;
let res, state;
try {
await checkIfEdited();
res = {
style: await (ucd && !md5Url ? updateUsercss : updateUSO)().then(maybeSave),
updated: true,
};
state = STATES.UPDATED;
} catch (err) {
const error = err === 0 && STATES.UNREACHABLE ||
err && err.message ||
err;
res = {error, style, STATES};
state = `${STATES.SKIPPED} (${Array.isArray(err) ? err[0].message : error})`;
}
log(`${state} #${id} ${style.customName || style.name}`);
if (port) port.postMessage(res);
return res;
async function checkIfEdited() {
if (!ignoreDigest &&
style.originalDigest &&
style.originalDigest !== await calcStyleDigest(style)) {
return Promise.reject(STATES.EDITED);
}
}
async function updateUSO() {
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);
}
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 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;
}
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(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
: !URLS.isLocalhost(updateUrl) && STATES.SAME_VERSION;
}
if (delta < 0) {
// downgrade is always invalid
err = STATES.ERROR_VERSION;
}
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.styles.put(style);
}
return err
? Promise.reject(err)
: API.usercss.buildCode(json);
}
async function maybeSave(json) {
json.id = id;
// keep current state
delete json.customName;
delete json.enabled;
const newStyle = Object.assign({}, style, json);
newStyle.updateDate = getDateFromVer(newStyle) || Date.now();
// update digest even if save === false as there might be just a space added etc.
if (!ucd && styleSectionsEqual(json, style)) {
style.originalDigest = (await API.styles.install(newStyle)).originalDigest;
return Promise.reject(STATES.SAME_CODE);
}
if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.MAYBE_EDITED);
}
return !save ? newStyle :
(ucd ? API.usercss.install : API.styles.install)(newStyle);
}
async function tryDownload(url, params) {
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) ||
retryDelay > MIN_INTERVAL_MS) {
return Promise.reject(code);
}
}
retryDelay *= 1.25;
await new Promise(resolve => setTimeout(resolve, retryDelay));
}
}
async function downloadEtag() {
const opts = Object.assign({method: 'head'}, RH_ETAG);
const req = await tryDownload(style.updateUrl, opts);
return req.headers.etag;
}
function getDateFromVer(style) {
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();
}
}
/** 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);
}
}
function schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
if (interval > 0) {
const elapsed = Math.max(0, Date.now() - lastUpdateTime);
chrome.alarms.create(ALARM_NAME, {
when: Date.now() + Math.max(MIN_INTERVAL_MS, interval - elapsed),
});
} else {
chrome.alarms.clear(ALARM_NAME, ignoreChromeError);
}
}
function onAlarm({name}) {
if (name === ALARM_NAME) checkAllStyles();
}
function resetInterval() {
chromeLocal.setValue('lastUpdateTime', lastUpdateTime = Date.now());
schedule();
}
function log(text) {
logQueue.push({text, time: new Date().toLocaleString()});
debounce(flushQueue, text && checkingAll ? 1000 : 0);
}
async function flushQueue(lines) {
if (!lines) {
flushQueue(await chromeLocal.getValue('updateLog') || []);
return;
}
const time = Date.now() - logLastWriteTime > 11e3 ?
logQueue[0].time + ' ' :
'';
if (logQueue[0] && !logQueue[0].text) {
logQueue.shift();
if (lines[lines.length - 1]) lines.push('');
}
lines.splice(0, lines.length - 1000);
lines.push(time + (logQueue[0] && logQueue[0].text || ''));
lines.push(...logQueue.slice(1).map(item => item.text));
chromeLocal.setValue('updateLog', lines);
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

@ -1,283 +0,0 @@
/* global styleSectionsEqual prefs download tryJSONparse ignoreChromeError
calcStyleDigest getStyleWithNoCode debounce chromeLocal
usercss semverCompare styleJSONseemsValid
API_METHODS styleManager */
'use strict';
(() => {
const STATES = {
UPDATED: 'updated',
SKIPPED: 'skipped',
// details for SKIPPED status
EDITED: 'locally edited',
MAYBE_EDITED: 'may be locally edited',
SAME_MD5: 'up-to-date: MD5 is unchanged',
SAME_CODE: 'up-to-date: code sections are unchanged',
SAME_VERSION: 'up-to-date: version is unchanged',
ERROR_MD5: 'error: MD5 is invalid',
ERROR_JSON: 'error: JSON is invalid',
ERROR_VERSION: 'error: version is older than installed style',
};
const ALARM_NAME = 'scheduledUpdate';
const MIN_INTERVAL_MS = 60e3;
let lastUpdateTime = parseInt(localStorage.lastUpdateTime) || Date.now();
let checkingAll = false;
let logQueue = [];
let logLastWriteTime = 0;
const retrying = new Set();
API_METHODS.updateCheckAll = checkAllStyles;
API_METHODS.updateCheck = checkStyle;
API_METHODS.getUpdaterStates = () => STATES;
prefs.subscribe(['updateInterval'], schedule);
schedule();
chrome.alarms.onAlarm.addListener(onAlarm);
return {checkAllStyles, checkStyle, STATES};
function checkAllStyles({
save = true,
ignoreDigest,
observe,
} = {}) {
resetInterval();
checkingAll = true;
retrying.clear();
const port = observe && chrome.runtime.connect({name: 'updater'});
return styleManager.getAllStyles().then(styles => {
styles = styles.filter(style => style.updateUrl);
if (port) port.postMessage({count: styles.length});
log('');
log(`${save ? 'Scheduled' : 'Manual'} update check for ${styles.length} styles`);
return Promise.all(
styles.map(style =>
checkStyle({style, port, save, ignoreDigest})));
}).then(() => {
if (port) port.postMessage({done: true});
if (port) port.disconnect();
log('');
checkingAll = false;
retrying.clear();
});
}
function checkStyle({
id,
style,
port,
save = true,
ignoreDigest,
}) {
/*
Original style digests are calculated in these cases:
* style is installed or updated from server
* style is checked for an update and its code is equal to the server code
Update check proceeds in these cases:
* style has the original digest and it's equal to the current digest
* [ignoreDigest: true] style doesn't yet have the original digest but we ignore it
* [ignoreDigest: none/false] style doesn't yet have the original digest
so we compare the code to the server code and if it's the same we save the digest,
otherwise we skip the style and report MAYBE_EDITED status
'ignoreDigest' option is set on the second manual individual update check on the manage page.
*/
return fetchStyle()
.then(() => {
if (!ignoreDigest) {
return calcStyleDigest(style)
.then(checkIfEdited);
}
})
.then(() => {
if (style.usercssData) {
return maybeUpdateUsercss();
}
return maybeUpdateUSO();
})
.then(maybeSave)
.then(reportSuccess)
.catch(reportFailure);
function fetchStyle() {
if (style) {
return Promise.resolve();
}
return styleManager.get(id)
.then(style_ => {
style = style_;
});
}
function reportSuccess(saved) {
log(STATES.UPDATED + ` #${style.id} ${style.name}`);
const info = {updated: true, style: saved};
if (port) port.postMessage(info);
return info;
}
function reportFailure(error) {
if ((
error === 503 || // Service Unavailable
error === 429 // Too Many Requests
) && !retrying.has(id)) {
retrying.add(id);
return new Promise(resolve => {
setTimeout(() => {
resolve(checkStyle({id, style, port, save, ignoreDigest}));
}, 1000);
});
}
error = error === 0 ? 'server unreachable' : error;
// UserCSS metadata error returns an object; e.g. "Invalid @var color..."
if (typeof error === 'object' && error.message) {
error = error.message;
}
log(STATES.SKIPPED + ` (${error}) #${style.id} ${style.name}`);
const info = {error, STATES, style: getStyleWithNoCode(style)};
if (port) port.postMessage(info);
return info;
}
function checkIfEdited(digest) {
if (style.originalDigest && style.originalDigest !== digest) {
return Promise.reject(STATES.EDITED);
}
}
function maybeUpdateUSO() {
return download(style.md5Url).then(md5 => {
if (!md5 || md5.length !== 32) {
return Promise.reject(STATES.ERROR_MD5);
}
if (md5 === style.originalMd5 && style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.SAME_MD5);
}
// USO can't handle POST requests for style json
return download(style.updateUrl, {body: null})
.then(text => {
const style = tryJSONparse(text);
if (style) {
// USO may not provide a correctly updated originalMd5 (#555)
style.originalMd5 = md5;
}
return style;
});
});
}
function maybeUpdateUsercss() {
// TODO: when sourceCode is > 100kB use http range request(s) for version check
return download(style.updateUrl).then(text =>
usercss.buildMeta(text).then(json => {
const {usercssData: {version}} = style;
const {usercssData: {version: newVersion}} = json;
switch (Math.sign(semverCompare(version, newVersion))) {
case 0:
// re-install is invalid in a soft upgrade
if (!ignoreDigest) {
const sameCode = text === style.sourceCode;
return Promise.reject(sameCode ? STATES.SAME_CODE : STATES.SAME_VERSION);
}
break;
case 1:
// downgrade is always invalid
return Promise.reject(STATES.ERROR_VERSION);
}
return usercss.buildCode(json);
})
);
}
function maybeSave(json = {}) {
// usercss is already validated while building
if (!json.usercssData && !styleJSONseemsValid(json)) {
return Promise.reject(STATES.ERROR_JSON);
}
json.id = style.id;
json.updateDate = Date.now();
// keep current state
delete json.enabled;
// keep local name customizations
if (style.originalName !== style.name && style.name !== json.name) {
delete json.name;
} else {
json.originalName = json.name;
}
const newStyle = Object.assign({}, style, json);
if (styleSectionsEqual(json, style, {checkSource: true})) {
// update digest even if save === false as there might be just a space added etc.
return styleManager.installStyle(newStyle)
.then(saved => {
style.originalDigest = saved.originalDigest;
return Promise.reject(STATES.SAME_CODE);
});
}
if (!style.originalDigest && !ignoreDigest) {
return Promise.reject(STATES.MAYBE_EDITED);
}
return save ?
API_METHODS[json.usercssData ? 'installUsercss' : 'installStyle'](newStyle) :
newStyle;
}
}
function schedule() {
const interval = prefs.get('updateInterval') * 60 * 60 * 1000;
if (interval > 0) {
const elapsed = Math.max(0, Date.now() - lastUpdateTime);
chrome.alarms.create(ALARM_NAME, {
when: Date.now() + Math.max(MIN_INTERVAL_MS, interval - elapsed),
});
} else {
chrome.alarms.clear(ALARM_NAME, ignoreChromeError);
}
}
function onAlarm({name}) {
if (name === ALARM_NAME) checkAllStyles();
}
function resetInterval() {
localStorage.lastUpdateTime = lastUpdateTime = Date.now();
schedule();
}
function log(text) {
logQueue.push({text, time: new Date().toLocaleString()});
debounce(flushQueue, text && checkingAll ? 1000 : 0);
}
function flushQueue(lines) {
if (!lines) {
chromeLocal.getValue('updateLog', []).then(flushQueue);
return;
}
const time = Date.now() - logLastWriteTime > 11e3 ?
logQueue[0].time + ' ' :
'';
if (logQueue[0] && !logQueue[0].text) {
logQueue.shift();
if (lines[lines.length - 1]) lines.push('');
}
lines.splice(0, lines.length - 1000);
lines.push(time + (logQueue[0] && logQueue[0].text || ''));
lines.push(...logQueue.slice(1).map(item => item.text));
chromeLocal.setValue('updateLog', lines);
logLastWriteTime = Date.now();
logQueue = [];
}
})();

View File

@ -1,183 +0,0 @@
/* global API_METHODS usercss styleManager deepCopy openURL download URLS getTab */
/* exports usercssHelper */
'use strict';
// eslint-disable-next-line no-unused-vars
const usercssHelper = (() => {
const installCodeCache = {};
const clearInstallCode = url => delete installCodeCache[url];
const isResponseText = r => /^text\/(css|plain)(;.*?)?$/i.test(r.headers.get('content-type'));
// in Firefox we have to use a content script to read file://
const fileLoader = !chrome.app && // not relying on navigator.ua which can be spoofed
(tabId => browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}).then(r => r[0]));
API_METHODS.installUsercss = installUsercss;
API_METHODS.editSaveUsercss = editSaveUsercss;
API_METHODS.configUsercssVars = configUsercssVars;
API_METHODS.buildUsercss = build;
API_METHODS.findUsercss = find;
API_METHODS.getUsercssInstallCode = url => {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);
clearTimeout(timer);
return code;
};
return {
testUrl(url) {
return url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]);
},
/** @return {Promise<{ code:string, inTab:boolean } | false>} */
testContents(tabId, url) {
const isFile = url.startsWith('file:');
const inTab = isFile && Boolean(fileLoader);
return Promise.resolve(isFile || fetch(url, {method: 'HEAD'}).then(isResponseText))
.then(ok => ok && (inTab ? fileLoader(tabId) : download(url)))
.then(code => /==userstyle==/i.test(code) && {code, inTab});
},
openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = `${URLS.installUsercss}?updateUrl=${encodeURIComponent(url)}`;
if (inTab) {
getTab(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});
}
},
};
function buildMeta(style) {
if (style.usercssData) {
return Promise.resolve(style);
}
// allow sourceCode to be normalized
const {sourceCode} = style;
delete style.sourceCode;
return usercss.buildMeta(sourceCode)
.then(newStyle => Object.assign(newStyle, style));
}
function assignVars(style) {
return find(style)
.then(dup => {
if (dup) {
style.id = dup.id;
// preserve style.vars during update
return usercss.assignVars(style, dup)
.then(() => style);
}
return style;
});
}
/**
* Parse the source, find the duplication, and build sections with variables
* @param _
* @param {String} _.sourceCode
* @param {Boolean=} _.checkDup
* @param {Boolean=} _.metaOnly
* @param {Object} _.vars
* @param {Boolean=} _.assignVars
* @returns {Promise<{style, dup:Boolean?}>}
*/
function build({
styleId,
sourceCode,
checkDup,
metaOnly,
vars,
assignVars = false,
}) {
return usercss.buildMeta(sourceCode)
.then(style => {
const findDup = checkDup || assignVars ?
find(styleId ? {id: styleId} : style) : Promise.resolve();
return Promise.all([
metaOnly ? style : doBuild(style, findDup),
findDup
]);
})
.then(([style, dup]) => ({style, dup}));
function doBuild(style, findDup) {
if (vars || assignVars) {
const getOld = vars ? Promise.resolve({usercssData: {vars}}) : findDup;
return getOld
.then(oldStyle => usercss.assignVars(style, oldStyle))
.then(() => usercss.buildCode(style));
}
return usercss.buildCode(style);
}
}
// Build the style within aditional properties then inherit variable values
// from the old style.
function parse(style) {
return buildMeta(style)
.then(buildMeta)
.then(assignVars)
.then(usercss.buildCode);
}
// FIXME: simplify this to `installUsercss(sourceCode)`?
function installUsercss(style) {
return parse(style)
.then(styleManager.installStyle);
}
// FIXME: simplify this to `editSaveUsercss({sourceCode, exclusions})`?
function editSaveUsercss(style) {
return parse(style)
.then(styleManager.editSave);
}
function configUsercssVars(id, vars) {
return styleManager.get(id)
.then(style => {
const newStyle = deepCopy(style);
newStyle.usercssData.vars = vars;
return usercss.buildCode(newStyle);
})
.then(style => styleManager.installStyle(style, 'config'))
.then(style => style.usercssData.vars);
}
/**
* @param {Style|{name:string, namespace:string}} styleOrData
* @returns {Style}
*/
function find(styleOrData) {
if (styleOrData.id) {
return styleManager.get(styleOrData.id);
}
const {name, namespace} = styleOrData.usercssData || styleOrData;
return styleManager.getAllStyles().then(styleList => {
for (const dup of styleList) {
const data = dup.usercssData;
if (!data) continue;
if (data.name === name &&
data.namespace === namespace) {
return dup;
}
}
});
}
})();

View File

@ -0,0 +1,141 @@
/* global RX_META URLS download openURL */// toolbox.js
/* global addAPI bgReady */// common.js
/* global tabMan */// msg.js
'use strict';
bgReady.all.then(() => {
const installCodeCache = {};
addAPI(/** @namespace API */ {
usercss: {
getInstallCode(url) {
// when the installer tab is reloaded after the cache is expired, this will throw intentionally
const {code, timer} = installCodeCache[url];
clearInstallCode(url);
clearTimeout(timer);
return code;
},
},
});
// `glob`: pathname match pattern for webRequest
// `rx`: pathname regex to verify the URL really looks like a raw usercss
const maybeDistro = {
// https://github.com/StylishThemes/GitHub-Dark/raw/master/github-dark.user.css
'github.com': {
glob: '/*/raw/*',
rx: /^\/[^/]+\/[^/]+\/raw\/[^/]+\/[^/]+?\.user\.(css|styl)$/,
},
// https://raw.githubusercontent.com/StylishThemes/GitHub-Dark/master/github-dark.user.css
'raw.githubusercontent.com': {
glob: '/*',
rx: /^(\/[^/]+?){4}\.user\.(css|styl)$/,
},
};
chrome.webRequest.onBeforeSendHeaders.addListener(maybeInstallFromDistro, {
urls: [
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))),
],
types: ['main_frame'],
}, ['blocking']);
chrome.webRequest.onHeadersReceived.addListener(rememberContentType, {
urls: makeUsercssGlobs('*', '/*'),
types: ['main_frame'],
}, ['responseHeaders']);
tabMan.onUpdate(maybeInstall);
function clearInstallCode(url) {
return delete installCodeCache[url];
}
/** Sites may be using custom types like text/stylus so this coarse filter only excludes html */
function isContentTypeText(type) {
return /^text\/(?!html)/i.test(type);
}
// in Firefox we have to use a content script to read file://
async function loadFromFile(tabId) {
return (await browser.tabs.executeScript(tabId, {file: '/content/install-hook-usercss.js'}))[0];
}
async function loadFromUrl(tabId, url) {
return (
url.startsWith('file:') ||
tabMan.get(tabId, isContentTypeText.name) ||
isContentTypeText((await fetch(url, {method: 'HEAD'})).headers.get('content-type'))
) && 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(',');
}
async function maybeInstall({tabId, url, oldUrl = ''}) {
if (url.includes('.user.') &&
/^(https?|file|ftps?):/.test(url) &&
/\.user\.(css|styl)$/.test(url.split(/[#?]/, 1)[0]) &&
!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)) {
await openInstallerPage(tabId, url, {code, inTab});
}
}
}
/** Faster installation on known distribution sites to avoid flicker of css text */
function maybeInstallFromDistro({tabId, url}) {
const u = new URL(url);
const m = maybeDistro[u.hostname];
if (!m || m.rx.test(u.pathname)) {
openInstallerPage(tabId, url, {});
// Silently suppress navigation.
// Don't redirect to the install URL as it'll flash the text!
return {cancel: true};
}
}
async function openInstallerPage(tabId, url, {code, inTab} = {}) {
const newUrl = makeInstallerUrl(url);
if (inTab) {
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;
}
}
/** Remember Content-Type to avoid wasting time to re-fetch in loadFromUrl **/
function rememberContentType({tabId, responseHeaders}) {
const h = responseHeaders.find(h => h.name.toLowerCase() === 'content-type');
tabMan.set(tabId, isContentTypeText.name, h && isContentTypeText(h.value) || undefined);
}
});

View File

@ -0,0 +1,163 @@
/* global API */// msg.js
/* global RX_META deepCopy download */// toolbox.js
'use strict';
const usercssMan = {
GLOBAL_META: Object.entries({
author: null,
description: null,
homepageURL: 'url',
updateURL: 'updateUrl',
name: null,
}),
/** `src` is a style or vars */
async assignVars(style, src) {
const meta = style.usercssData;
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)) {
const old = oldVars[key] && oldVars[key].value;
if (old) v.value = old;
}
meta.vars = await API.worker.nullifyInvalidVars(vars);
}
},
async build({
styleId,
sourceCode,
vars,
checkDup,
metaOnly,
assignVars,
initialUrl,
}) {
// downloading here while install-usercss page is loading to avoid the wait
if (initialUrl) sourceCode = await download(initialUrl);
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 || dup);
}
await usercssMan.buildCode(style);
log = style.log; // extracting the non-enumerable prop, otherwise it won't survive messaging
}
return {style, dup, log};
},
async buildCode(style) {
const {sourceCode: code, usercssData: {vars, preprocessor}} = style;
const match = code.match(RX_META);
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, 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;
},
async buildMeta(style) {
if (style.usercssData) {
return style;
}
// remember normalized sourceCode
let code = style.sourceCode = style.sourceCode.replace(/\r\n?/g, '\n');
style = Object.assign({
enabled: true,
sections: [],
}, style);
const match = code.match(RX_META);
if (!match) {
return Promise.reject(new Error('Could not find metadata.'));
}
try {
code = blankOut(code, 0, match.index) + match[0];
const {metadata} = await API.worker.parseUsercssMeta(code);
style.usercssData = metadata;
// https://github.com/openstyles/stylus/issues/560#issuecomment-440561196
for (const [key, globalKey] of usercssMan.GLOBAL_META) {
const val = metadata[key];
if (val !== undefined) {
style[globalKey || key] = val;
}
}
return style;
} catch (err) {
if (err.code) {
const args = err.code === 'missingMandatory' || err.code === 'missingChar'
? err.args.map(e => e.length === 1 ? JSON.stringify(e) : e).join(', ')
: err.args;
const msg = chrome.i18n.getMessage(`meta_${(err.code)}`, args);
if (msg) err.message = msg;
}
return Promise.reject(err);
}
},
async configVars(id, vars) {
const style = deepCopy(await API.styles.get(id));
style.usercssData.vars = vars;
await usercssMan.buildCode(style);
return (await API.styles.install(style, 'config'))
.usercssData.vars;
},
async editSave(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) {
if (styleOrData.id) {
return API.styles.get(styleOrData.id);
}
const {name, namespace} = styleOrData.usercssData || styleOrData;
for (const dup of await API.styles.getAll()) {
const data = dup.usercssData;
if (data &&
data.name === name &&
data.namespace === namespace) {
return dup;
}
}
},
async install(style, opts) {
return API.styles.install(await usercssMan.parse(style, opts));
},
async parse(style, {dup, vars} = {}) {
style = await usercssMan.buildMeta(style);
// preserve style.vars during update
if (dup || (dup = await usercssMan.find(style))) {
style.id = dup.id;
}
if (vars || (vars = dup)) {
await usercssMan.assignVars(style, vars);
}
return usercssMan.buildCode(style);
},
};
/** Replaces everything with spaces to keep the original length,
* but preserves the line breaks to keep the original line/col relation */
function blankOut(str, start = 0, end = str.length) {
return str.slice(start, end).replace(/[^\r\n]/g, ' ');
}

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

@ -1,33 +1,34 @@
/* global msg API prefs createStyleInjector */ /* global API msg */// msg.js
/* global StyleInjector */
/* global prefs */
'use strict'; 'use strict';
// Chrome reruns content script when documentElement is replaced. (() => {
// Note, we're checking against a literal `1`, not just `if (truthy)`, if (window.INJECTED === 1) return;
// because <html id="INJECTED"> is exposed per HTML spec as a global variable and `window.INJECTED`. window.INJECTED = 1;
// eslint-disable-next-line no-unused-expressions /** true -> when the page styles are received,
self.INJECTED !== 1 && (() => { * false -> when disableAll mode is on at start, the styles won't be sent
self.INJECTED = 1; * so while disableAll lasts we can ignore messages about style updates because
* the tab will explicitly ask for all styles in bulk when disableAll mode ends */
let IS_TAB = !chrome.tabs || location.pathname !== '/popup.html'; let hasStyles = false;
const IS_FRAME = window !== parent; let isDisabled = false;
const STYLE_VIA_API = !chrome.app && document instanceof XMLDocument; let isTab = !chrome.tabs || location.pathname !== '/popup.html';
const styleInjector = createStyleInjector({ const order = {main: [], prio: []};
compare: (a, b) => a.id - b.id, 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) => calcOrder(a) - calcOrder(b),
onUpdate: onInjectorUpdate, onUpdate: onInjectorUpdate,
}); });
const initializing = init(); // dynamic iframes don't have a URL yet so we'll use their parent's URL (hash isn't inherited)
/** @type chrome.runtime.Port */ let matchUrl = isFrameAboutBlank && tryCatch(() => parent.location.href.split('#')[0]) ||
let port; location.href;
let lazyBadge = IS_FRAME;
// the popup needs a check as it's not a tab but can be opened in a tab manually for whatever reason
if (!IS_TAB) {
chrome.tabs.getCurrent(tab => {
IS_TAB = Boolean(tab);
if (tab && styleInjector.list.length) updateCount();
});
}
// save it now because chrome.runtime will be unavailable in the orphaned script // save it now because chrome.runtime will be unavailable in the orphaned script
const orphanEventId = chrome.runtime.id; const orphanEventId = chrome.runtime.id;
@ -35,101 +36,158 @@ self.INJECTED !== 1 && (() => {
// firefox doesn't orphanize content scripts so the old elements stay // firefox doesn't orphanize content scripts so the old elements stay
if (!chrome.app) styleInjector.clearOrphans(); if (!chrome.app) styleInjector.clearOrphans();
/** @type chrome.runtime.Port */
let port;
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
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) {
chrome.tabs.getCurrent(tab => {
isTab = Boolean(tab);
if (tab && styleInjector.list.length) updateCount();
});
}
msg.onTab(applyOnMessage); msg.onTab(applyOnMessage);
window.addEventListener('pageshow', e => {
if (e.isTrusted && e.persisted) { // bfcache
updateCount();
}
});
if (!chrome.tabs) { if (!chrome.tabs) {
window.dispatchEvent(new CustomEvent(orphanEventId)); window.dispatchEvent(new CustomEvent(orphanEventId));
window.addEventListener(orphanEventId, orphanCheck, true); window.addEventListener(orphanEventId, orphanCheck, true);
} }
let parentDomain;
prefs.subscribe(['disableAll'], (key, value) => doDisableAll(value));
if (IS_FRAME) {
prefs.subscribe(['exposeIframes'], updateExposeIframes);
}
function onInjectorUpdate() { function onInjectorUpdate() {
if (!isOrphaned) { if (!isOrphaned) {
updateCount(); updateCount();
updateExposeIframes(); const onOff = prefs[styleInjector.list.length ? 'subscribe' : 'unsubscribe'];
onOff('disableAll', updateDisableAll);
if (isFrame) {
updateExposeIframes();
onOff('exposeIframes', updateExposeIframes);
}
} }
} }
function init() { async function init() {
return STYLE_VIA_API ? if (isUnstylable) {
API.styleViaAPI({method: 'styleApply'}) : await API.styleViaAPI({method: 'styleApply'});
API.getSectionsByUrl(getMatchUrl()).then(styleInjector.apply); } else {
const SYM_ID = 'styles';
const SYM = Symbol.for(SYM_ID);
const parentStyles = isFrameAboutBlank &&
tryCatch(() => parent[parent.Symbol.for(SYM_ID)]);
const styles =
window[SYM] ||
parentStyles && await new Promise(onFrameElementInView) && parentStyles ||
!isFrameAboutBlank && chrome.app && !chrome.tabs && tryCatch(getStylesViaXhr) ||
await API.styles.getSectionsByUrl(matchUrl, null, true);
if (styles.cfg) {
isDisabled = styles.cfg.disableAll;
Object.assign(order, styles.cfg.order);
}
hasStyles = !isDisabled;
if (hasStyles) {
window[SYM] = styles;
await styleInjector.apply(styles);
} else {
delete window[SYM];
prefs.subscribe('disableAll', updateDisableAll);
}
styleInjector.toggle(hasStyles);
}
} }
function getMatchUrl() { /** Must be executed inside try/catch */
let matchUrl = location.href; function getStylesViaXhr() {
if (!matchUrl.match(/^(http|file|chrome|ftp)/)) { const blobId = document.cookie.split(chrome.runtime.id + '=')[1].split(';')[0];
// dynamic about: and javascript: iframes don't have an URL yet const url = 'blob:' + chrome.runtime.getURL(blobId);
// so we'll try the parent frame which is guaranteed to have a real URL document.cookie = `${chrome.runtime.id}=1; max-age=0`; // remove our cookie
try { const xhr = new XMLHttpRequest();
if (IS_FRAME) { xhr.open('GET', url, false); // synchronous
matchUrl = parent.location.href; xhr.send();
} URL.revokeObjectURL(url);
} catch (e) {} return JSON.parse(xhr.response);
}
return matchUrl;
} }
function applyOnMessage(request) { function applyOnMessage(request) {
if (STYLE_VIA_API) { const {method} = request;
if (request.method === 'urlChanged') { if (isUnstylable) {
if (method === 'urlChanged') {
request.method = 'styleReplaceAll'; request.method = 'styleReplaceAll';
} }
if (/^(style|updateCount)/.test(request.method)) { if (/^(style|updateCount)/.test(method)) {
API.styleViaAPI(request); API.styleViaAPI(request);
return; return;
} }
} }
switch (request.method) { const {style} = request;
switch (method) {
case 'ping': case 'ping':
return true; return true;
case 'styleDeleted': case 'styleDeleted':
styleInjector.remove(request.style.id); styleInjector.remove(style.id);
break; break;
case 'styleUpdated': case 'styleUpdated':
if (request.style.enabled) { if (!hasStyles && isDisabled) break;
API.getSectionsByUrl(getMatchUrl(), request.style.id) if (style.enabled) {
.then(sections => { API.styles.getSectionsByUrl(matchUrl, style.id).then(sections =>
if (!sections[request.style.id]) { sections[style.id]
styleInjector.remove(request.style.id); ? styleInjector.apply(sections)
} else { : styleInjector.remove(style.id));
styleInjector.apply(sections);
}
});
} else { } else {
styleInjector.remove(request.style.id); styleInjector.remove(style.id);
} }
break; break;
case 'styleAdded': case 'styleAdded':
if (request.style.enabled) { if (!hasStyles && isDisabled) break;
API.getSectionsByUrl(getMatchUrl(), request.style.id) if (style.enabled) {
API.styles.getSectionsByUrl(matchUrl, style.id)
.then(styleInjector.apply); .then(styleInjector.apply);
} }
break; break;
case 'urlChanged': case 'styleSort':
API.getSectionsByUrl(getMatchUrl()) Object.assign(order, request.order);
.then(styleInjector.replace); styleInjector.sort();
break; break;
case 'backgroundReady': case 'urlChanged':
initializing if (!hasStyles && isDisabled || matchUrl === request.url) break;
.catch(err => { matchUrl = request.url;
if (msg.RX_NO_RECEIVER.test(err.message)) { API.styles.getSectionsByUrl(matchUrl).then(sections => {
return init(); hasStyles = true;
} styleInjector.replace(sections);
}) });
.catch(console.error);
break; break;
case 'updateCount': case 'updateCount':
@ -138,36 +196,35 @@ self.INJECTED !== 1 && (() => {
} }
} }
function doDisableAll(disableAll) { function updateDisableAll(key, disableAll) {
if (STYLE_VIA_API) { isDisabled = disableAll;
if (isUnstylable) {
API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}}); API.styleViaAPI({method: 'prefChanged', prefs: {disableAll}});
} else if (!hasStyles && !disableAll) {
init();
} else { } else {
styleInjector.toggle(!disableAll); styleInjector.toggle(!disableAll);
} }
} }
function fetchParentDomain() { async function updateExposeIframes(key, value = prefs.get('exposeIframes')) {
return parentDomain ? const attr = 'stylus-iframe';
Promise.resolve() : const el = document.documentElement;
API.getTabUrlPrefix() if (!el) return; // got no styles so styleInjector didn't wait for <html>
.then(newDomain => { if (!value || !styleInjector.list.length) {
parentDomain = newDomain; el.removeAttribute(attr);
});
}
function updateExposeIframes() {
if (!prefs.get('exposeIframes') || window === parent || !styleInjector.list.length) {
document.documentElement.removeAttribute('stylus-iframe');
} else { } else {
fetchParentDomain().then(() => { if (!parentDomain) parentDomain = await API.getTabUrlPrefix();
document.documentElement.setAttribute('stylus-iframe', parentDomain); // Check first to avoid triggering DOM mutation
}); if (el.getAttribute(attr) !== parentDomain) {
el.setAttribute(attr, parentDomain);
}
} }
} }
function updateCount() { function updateCount() {
if (!IS_TAB) return; if (!isTab) return;
if (IS_FRAME) { if (isFrame) {
if (!port && styleInjector.list.length) { if (!port && styleInjector.list.length) {
port = chrome.runtime.connect({name: 'iframe'}); port = chrome.runtime.connect({name: 'iframe'});
} else if (port && !styleInjector.list.length) { } else if (port && !styleInjector.list.length) {
@ -175,23 +232,40 @@ self.INJECTED !== 1 && (() => {
} }
if (lazyBadge && performance.now() > 1000) lazyBadge = false; if (lazyBadge && performance.now() > 1000) lazyBadge = false;
} }
(STYLE_VIA_API ? (isUnstylable ?
API.styleViaAPI({method: 'updateCount'}) : API.styleViaAPI({method: 'updateCount'}) :
API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge}) API.updateIconBadge(styleInjector.list.map(style => style.id), {lazyBadge})
).catch(msg.ignoreError); ).catch(msg.ignoreError);
} }
function orphanCheck() { 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 { try {
if (chrome.i18n.getUILanguage()) return; return func(...args);
} catch (e) {} } catch (e) {}
}
function orphanCheck() {
if (chrome.runtime.id) return;
// In Chrome content script is orphaned on an extension update/reload // In Chrome content script is orphaned on an extension update/reload
// so we need to detach event listeners // so we need to detach event listeners
window.removeEventListener(orphanEventId, orphanCheck, true); window.removeEventListener(orphanEventId, orphanCheck, true);
mqDark.onchange = null;
isOrphaned = true; isOrphaned = true;
styleInjector.clear(); setTimeout(styleInjector.clear, 1000); // avoiding FOUC
try { tryCatch(msg.off, applyOnMessage);
msg.off(applyOnMessage);
} catch (e) {}
} }
})(); })();

View File

@ -0,0 +1,21 @@
/* global API */// msg.js
'use strict';
// onCommitted may fire twice
// Note, we're checking against a literal `1`, not just `if (truthy)`,
// because <html id="INJECTED"> is exposed per HTML spec as a global variable and `window.INJECTED`.
if (window.INJECTED_GREASYFORK !== 1) {
window.INJECTED_GREASYFORK = 1;
addEventListener('message', async function onMessage(e) {
if (e.origin === location.origin &&
e.data &&
e.data.name &&
e.data.type === 'style-version-query') {
removeEventListener('message', onMessage);
const style = await API.usercss.find(e.data) || {};
const {version} = style.usercssData || {};
postMessage({type: 'style-version', version}, '*');
}
});
}

View File

@ -1,174 +0,0 @@
/* global API */
'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.findUsercss({
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 = () => {
// 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();
}
};
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.installUsercss({
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,22 +1,26 @@
'use strict'; 'use strict';
// preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case // preventing reregistration if reinjected by tabs.executeScript for whatever reason, just in case
if (typeof self.oldCode !== 'string') { if (typeof window.oldCode !== 'string') {
self.oldCode = (document.querySelector('body > pre') || document.body).textContent; window.oldCode = (document.querySelector('body > pre') || document.body).textContent;
chrome.runtime.onConnect.addListener(port => { chrome.runtime.onConnect.addListener(port => {
if (port.name !== 'downloadSelf') return; if (port.name !== 'downloadSelf') return;
port.onMessage.addListener(({id, timer}) => { port.onMessage.addListener(async ({id, force}) => {
fetch(location.href, {mode: 'same-origin'}) const msg = {id};
.then(r => r.text()) try {
.then(code => ({id, code: timer && code === self.oldCode ? null : code})) const code = await (await fetch(location.href, {mode: 'same-origin'})).text();
.catch(error => ({id, error: error.message || `${error}`})) if (code !== window.oldCode || force) {
.then(msg => { msg.code = window.oldCode = code;
port.postMessage(msg); }
if (msg.code != null) self.oldCode = msg.code; } catch (error) {
}); msg.error = error.message || `${error}`;
}
port.postMessage(msg);
}); });
// FF keeps content scripts connected on navigation https://github.com/openstyles/stylus/issues/864
addEventListener('pagehide', () => port.disconnect(), {once: true});
}); });
} }
// passing the result to tabs.executeScript // passing the result to tabs.executeScript
self.oldCode; // eslint-disable-line no-unused-expressions window.oldCode; // eslint-disable-line no-unused-expressions

View File

@ -1,497 +1,324 @@
/* global cloneInto msg API */ /* global API */// msg.js
'use strict'; 'use strict';
(() => { // eslint-disable-next-line no-unused-expressions
window.dispatchEvent(new CustomEvent(chrome.runtime.id + '-install')); /^\/styles\/(\d+)(\/([^/]*))?([?#].*)?$/.test(location.pathname) && (async () => {
window.addEventListener(chrome.runtime.id + '-install', orphanCheck, true); if (window.INJECTED_USO === 1) return;
window.INJECTED_USO = 1;
document.addEventListener('stylishInstallChrome', onClick); const usoId = RegExp.$1;
document.addEventListener('stylishUpdateChrome', onClick); 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);
msg.on(onMessage); const mo = new MutationObserver(onMutation);
const observeColors = isOn =>
isOn ? mo.observe(document.body, {subtree: true, attributes: true, attributeFilter: ['value']})
: mo.disconnect();
onDOMready().then(() => { let style, dup, md5, pageData, badKeys;
window.postMessage({
direction: 'from-content-script',
message: 'StylishInstalled',
}, '*');
});
let gotBody = false; runInPage(inPageContext, pageEventId, contentEventId, usoId, apiUrl);
let currentMd5; addEventListener(orphanEventId, orphanCheck, true);
new MutationObserver(observeDOM).observe(document.documentElement, { addEventListener('click', onClick, true);
childList: true, togglePageListener(true);
subtree: true,
});
observeDOM();
function observeDOM() { [md5, dup] = await Promise.all([
if (!gotBody) { fetch(md5Url).then(r => r.text()),
if (!document.body) return; API.styles.find({md5Url}, {installationUrl: `https://uso.kkx.one/style/${usoId}`})
gotBody = true; .then(sendVarsToPage),
// TODO: remove the following statement when USO pagination title is fixed document.body || new Promise(resolve => addEventListener('load', resolve, {once: true})),
document.title = document.title.replace(/^(\d+)&\w+=/, '#$1: '); ]);
const md5Url = getMeta('stylish-md5-url') || location.href;
Promise.all([ if (!dup) {
API.findStyle({md5Url}), sendStylishEvent('styleCanBeInstalledChrome');
getResource(md5Url) } 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
.then(checkUpdatability); sendStylishEvent('styleCanBeUpdatedChrome');
} } else {
if (document.getElementById('install_button')) { sendStylishEvent('styleAlreadyInstalledChrome');
onDOMready().then(() => {
requestAnimationFrame(() => {
sendEvent(sendEvent.lastEvent);
});
});
}
} }
function onMessage(msg) { async function onClick(e) {
switch (msg.method) { for (const [sel, fn] of CLICK) {
case 'ping': const el = e.target.closest(sel);
// orphaned content script check if (!el) continue;
return true; try {
case 'openSettings': el.disabled = true;
openSettings(); await fn(e);
return true; } catch (e) {
} alert(chrome.i18n.getMessage('styleInstallFailed', e.message || e));
} } finally {
el.disabled = false;
/* 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('stylusFixBuggyUSOsettings', {
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);
} else {
detail = {detail};
}
onDOMready().then(() => {
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.findStyle({
md5Url: getMeta('stylish-md5-url') || location.href
}, true)
.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);
});
}
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);
return getStyleJson().then(json => {
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
return API.installStyle(Object.assign(json, addProps, {originalMd5: currentMd5}))
.then(style => {
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);')));
}
}
} }
} }
} }
function onCustomize() {
function getMeta(name) { const ss = $('#style-settings');
const e = document.querySelector(`link[rel="${name}"]`); const willShow = !ss || !ss.offsetHeight;
return e ? e.getAttribute('href') : null; observeColors(willShow);
toggleListener(willShow, 'change', onChange);
} }
async function onInstall(e) {
function getResource(url, options) { const {id} = dup;
if (url.startsWith('#')) { e.stopPropagation();
return Promise.resolve(document.getElementById(url.slice(1)).textContent); if (!style) await buildStyle();
} style = dup = await API.usercss.install(style, {
return API.download(Object.assign({ dup: {id},
url, vars: getPageVars(),
timeout: 60e3, });
// USO can't handle POST requests for style json sendStylishEvent('styleInstalledChrome');
body: null, API.uso.pingback(id);
}, options))
.catch(error => {
alert('Error' + (error ? '\n' + error : ''));
throw error;
});
} }
// USO providing md5Url as "https://update.update.userstyles.org/#####.md5" function onUninstall() {
// instead of "https://update.userstyles.org/#####.md5" const {id} = dup;
function tryFixMd5(style) { dup = style = false;
if (style && style.md5Url && style.md5Url.includes('update.update')) { observeColors(false);
style.md5Url = style.md5Url.replace('update.update', 'update'); removeEventListener('change', onChange);
} return API.styles.delete(id);
return style;
} }
function getStyleJson() { function onChange({target: el}) {
return getResource(getStyleURL(), {responseType: 'json'}) if (dup && el.matches('[name^="ik-"], [type=file]')) {
.then(style => { API.usercss.configVars(dup.id, getPageVars());
if (!style || !Array.isArray(style.sections) || style.sections.length) { }
return style;
}
const codeElement = document.getElementById('stylish-code');
if (codeElement && !codeElement.textContent.trim()) {
return style;
}
return getResource(getMeta('stylish-update-url'))
.then(code => API.parseCss({code}))
.then(result => {
style.sections = result.sections;
return style;
});
})
.then(tryFixMd5)
.catch(() => null);
} }
function onMutation(mutations) {
function styleSectionsEqual({sections: a}, {sections: b}) { for (const {target: el} of mutations) {
if (!a || !b) { if (el.style.display === 'none' &&
return undefined; /^ik-/.test(el.name) &&
} /^#[\da-f]{6}$/.test(el.value)) {
if (a.length !== b.length) { onChange({target: el});
return false;
}
// order of sections should be identical to account for the case of multiple
// sections matching the same URL because the order of rules is part of cascading
return a.every((sectionA, index) => propertiesEqual(sectionA, b[index]));
function propertiesEqual(secA, secB) {
for (const name of ['urlPrefixes', 'urls', 'domains', 'regexps']) {
if (!equalOrEmpty(secA[name], secB[name], 'every', arrayMirrors)) {
return false;
}
} }
return equalOrEmpty(secA.code, secB.code, 'substr', (a, b) => a === b);
}
function equalOrEmpty(a, b, telltale, comparator) {
const typeA = a && typeof a[telltale] === 'function';
const typeB = b && typeof b[telltale] === 'function';
return (
(a === null || a === undefined || (typeA && !a.length)) &&
(b === null || b === undefined || (typeB && !b.length))
) || typeA && typeB && a.length === b.length && comparator(a, b);
}
function arrayMirrors(array1, array2) {
return (
array1.every(el => array2.includes(el)) &&
array2.every(el => array1.includes(el))
);
} }
} }
function onPageEvent(e) {
pageData = e.detail;
togglePageListener(false);
}
function onDOMready() { async function buildStyle() {
if (document.readyState !== 'loading') { if (!pageData) pageData = await (await fetch(apiUrl)).json();
return Promise.resolve(); ({style, badKeys} = await API.uso.toUsercss(pageData, {varsUrl: dup.updateUrl}));
} Object.assign(style, {
return new Promise(resolve => { md5Url,
document.addEventListener('DOMContentLoaded', function _() { id: dup.id,
document.removeEventListener('DOMContentLoaded', _); originalMd5: md5,
resolve(); updateUrl: apiUrl,
});
}); });
} }
function getPageVars() {
function openSettings(countdown = 10e3) { const {vars} = (style || dup).usercssData;
const button = document.querySelector('.customize_button'); for (const el of document.querySelectorAll('[name^="ik-"]')) {
if (button) { const name = el.name.slice(3); // dropping "ik-"
button.dispatchEvent(new MouseEvent('click', {bubbles: true})); const ik = (badKeys || {})[name] || name;
setTimeout(function pollArea(countdown = 2000) { const v = vars[ik] || false;
const area = document.getElementById('advancedsettings_area'); const isImage = el.type === 'radio';
if (area || countdown < 0) { if (v && (!isImage || el.checked)) {
(area || button).scrollIntoView({behavior: 'smooth', block: area ? 'end' : 'center'}); 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 { } 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() { function orphanCheck() {
// TODO: switch to install-hook-usercss.js impl, and remove explicit orphanCheck() calls if (chrome.runtime.id) return true;
if (chrome.i18n && chrome.i18n.getUILanguage()) { removeEventListener(orphanEventId, orphanCheck, true);
return true; removeEventListener('click', onClick, true);
} removeEventListener('change', onChange);
// In Chrome content script is orphaned on an extension update/reload sendPageEvent('quit');
// so we need to detach event listeners observeColors(false);
window.removeEventListener(chrome.runtime.id + '-install', orphanCheck, true); togglePageListener(false);
document.removeEventListener('stylishInstallChrome', onClick);
document.removeEventListener('stylishUpdateChrome', onClick);
try {
msg.off(onMessage);
} catch (e) {}
} }
})(); })();
// run in page context function inPageContext(eventId, eventIdHost, styleId, apiUrl) {
document.documentElement.appendChild(document.createElement('script')).text = '(' + ( let done, orphaned, vars;
() => { // `chrome` may be empty if no extensions use externally_connectable but USO needs it
document.currentScript.remove(); if (!window.chrome) window.chrome = {runtime: {sendMessage: () => {}}};
const EXT_ID = 'fjnbnpbmkenffdnngjfgmeleoegfcffe';
// spoof Stylish extension presence in Chrome const {defineProperty} = Object;
if (window.chrome && chrome.app) { const {dispatchEvent, CustomEvent, removeEventListener} = window;
const realImage = window.Image; const apply = Map.call.bind(Map.apply);
window.Image = function Image(...args) { const OVR = [
return new Proxy(new realImage(...args), { [chrome.runtime, 'sendMessage', (fn, me, args) => {
get(obj, key) { const [id, /*msg*/, opts, cb = opts] = args;
return obj[key]; if (id !== EXT_ID) return apply(fn, me, args);
}, if (typeof cb !== 'function') return Promise.resolve(true);
set(obj, key, value) { cb(true);
if (key === 'src' && /^chrome-extension:/i.test(value)) { }],
setTimeout(() => typeof obj.onload === 'function' && obj.onload()); [Response.prototype, 'json', async (fn, me, args) => {
} else { const res = await apply(fn, me, args);
obj[key] = value; try {
} if (!done && me.url === apiUrl) {
return true; done = true;
}, send(res);
}); setVars(res);
}; }
} } catch (e) {}
return res;
// USO bug workaround: use the actual style settings in API response }],
let settings; [window, 'fetch', (fn, me, args) =>
const originalResponseJson = Response.prototype.json; args[0] === `chrome-extension://${EXT_ID}/index.html`
document.addEventListener('stylusFixBuggyUSOsettings', function _({detail}) { ? Promise.resolve(new Response('<!doctype html><html lang="en"></html>'))
document.removeEventListener('stylusFixBuggyUSOsettings', _); : apply(fn, me, args),
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425) ],
settings = /\?/.test(detail) && new URLSearchParams(new URL(detail).search.replace(/^\?/, '')); ];
if (!settings) { OVR.forEach(([obj, name, caller], i) => {
Response.prototype.json = originalResponseJson; 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);
},
}); });
Response.prototype.json = function (...args) { defineProperty(obj, name, {value: ovr});
return originalResponseJson.call(this, ...args).then(json => { OVR[i] = [obj, name, ovr, orig]; // same args as restore()
if (!settings || typeof ((json || {}).style_settings || {}).every !== 'function') { });
return json; /* 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). */
Response.prototype.json = originalResponseJson; window.isInstalled = true;
const images = new Map(); addEventListener(eventId, onCommand, true);
for (const jsonSetting of json.style_settings) { function onCommand(e) {
let value = settings.get('ik-' + jsonSetting.install_key); if (e.detail === 'quit') {
if (!value removeEventListener(eventId, onCommand, true);
|| !jsonSetting.style_setting_options OVR.forEach(restore);
|| !jsonSetting.style_setting_options[0]) { done = orphaned = true;
continue; } else if (/^vars:/.test(e.detail)) {
} vars = JSON.parse(e.detail.slice(5));
if (value.startsWith('ik-')) { } else if (e.relatedTarget) {
value = value.replace(/^ik-/, ''); send(e.relatedTarget.uploadedData);
const defaultItem = jsonSetting.style_setting_options.find(item => item.default); }
if (!defaultItem || defaultItem.install_key !== value) {
if (defaultItem) {
defaultItem.default = false;
}
jsonSetting.style_setting_options.some(item => {
if (item.install_key === value) {
item.default = true;
return true;
}
});
}
} else if (jsonSetting.setting_type === 'image') {
jsonSetting.style_setting_options.some(item => {
if (item.default) {
item.default = false;
return true;
}
});
images.set(jsonSetting.install_key, value);
} else {
const item = jsonSetting.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
if (images.size) {
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) {
return;
}
observer.disconnect();
for (const [name, url] of images.entries()) {
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) {
elUrl.value = url;
}
}
}).observe(document, {childList: true, subtree: true});
}
return json;
});
};
} }
) + `)('${chrome.runtime.getURL('').slice(0, -1)}')`; function restore(obj, name, ovr, orig) { // same order as OVR after patching
if (obj[name] === ovr) {
// TODO: remove the following statement when USO pagination is fixed defineProperty(obj, name, {value: orig});
if (location.search.includes('category=')) { }
document.addEventListener('DOMContentLoaded', function _() { }
document.removeEventListener('DOMContentLoaded', _); function send(data) {
new MutationObserver((_, observer) => { dispatchEvent(new CustomEvent(eventIdHost, {__proto: null, detail: data}));
if (!document.getElementById('pagination')) { }
return; 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);
}
} }
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;
}
}
}
} else {
const item = ss.style_setting_options[0];
if (item.value !== value && item.install_key === 'placeholder') {
item.value = value;
}
}
}
if (!images.size) return;
new MutationObserver((_, observer) => {
if (!document.getElementById('style-settings')) return;
observer.disconnect(); observer.disconnect();
const category = '&' + location.search.match(/category=[^&]+/)[0]; for (const [name, {url, isListed}] of images) {
const links = document.querySelectorAll('#pagination a[href*="page="]:not([href*="category="])'); const elRadio = document.querySelector(`input[name="ik-${name}"][value="user-url"]`);
for (let i = 0; i < links.length; i++) { const elUrl = elRadio && document.getElementById(elRadio.id.replace('url-choice', 'user-url'));
links[i].href += category; if (elUrl) {
elRadio.checked = !isListed;
elUrl.value = url;
}
} }
}).observe(document, {childList: true, subtree: true}); }).observe(document, {childList: true, subtree: true});
}); }
}
if (/^https?:\/\/userstyles\.org\/styles\/\d{3,}/.test(location.href)) {
new MutationObserver((_, observer) => {
const cssButton = document.getElementsByClassName('css_button');
if (cssButton.length) {
// Click on the "Show CSS Code" button to workaround the JS error
cssButton[0].click();
cssButton[0].click();
observer.disconnect();
}
}).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

@ -1,6 +1,7 @@
'use strict'; 'use strict';
self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({ /** @type {function(opts):StyleInjector} */
window.StyleInjector = window.INJECTED === 1 ? window.StyleInjector : ({
compare, compare,
onUpdate = () => {}, onUpdate = () => {},
}) => { }) => {
@ -8,105 +9,107 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
const PATCH_ID = 'transition-patch'; const PATCH_ID = 'transition-patch';
// styles are out of order if any of these elements is injected between them // 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 ORDERED_TAGS = new Set(['head', 'body', 'frameset', 'style', 'link']);
const IS_OWN_PAGE = Boolean(chrome.tabs); const docRewriteObserver = RewriteObserver(sort);
// detect Chrome 65 via a feature it added since browser version can be spoofed const docRootObserver = RootObserver(sortIfNeeded);
const isChromePre65 = chrome.app && typeof Worklet !== 'function'; const toSafeChar = c => String.fromCharCode(0xFF00 + c.charCodeAt(0) - 0x20);
const docRewriteObserver = RewriteObserver(_sort);
const docRootObserver = RootObserver(_sortIfNeeded);
const list = []; const list = [];
const table = new Map(); const table = new Map();
let isEnabled = true; 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 // will store the original method refs because the page can override them
let creationDoc, createElement, createElementNS; let creationDoc, createElement, createElementNS;
return {
apply, return /** @namespace StyleInjector */ {
clear,
clearOrphans,
remove,
replace,
toggle,
list, list,
async apply(styleMap) {
const styles = styleMapToArray(styleMap);
const value = !styles.length
? []
: await docRootObserver.evade(() => {
if (!isTransitionPatched && isEnabled) {
applyTransitionPatch(styles);
}
return styles.map(addUpdate);
});
emitUpdate();
return value;
},
clear() {
addRemoveElements(false);
list.length = 0;
table.clear();
emitUpdate();
},
clearOrphans() {
for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) {
const id = el.id.slice(PREFIX.length);
if (/^\d+$/.test(id) || id === PATCH_ID) {
el.remove();
}
}
},
remove(id) {
remove(id);
emitUpdate();
},
replace(styleMap) {
const styles = styleMapToArray(styleMap);
const added = new Set(styles.map(s => s.id));
const removed = [];
for (const style of list) {
if (!added.has(style.id)) {
removed.push(style.id);
}
}
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);
},
sort: sort,
}; };
function apply(styleMap) { function add(style) {
const styles = _styleMapToArray(styleMap); const el = style.el = createStyle(style);
return !styles.length ? const i = list.findIndex(item => compare(item, style) > 0);
Promise.resolve([]) : table.set(style.id, style);
docRootObserver.evade(() => { if (isEnabled) {
if (!isTransitionPatched) _applyTransitionPatch(styles); document.documentElement.insertBefore(el, i < 0 ? null : list[i].el);
const els = styles.map(_apply);
_emitUpdate();
return els;
});
}
function clear() {
for (const style of list) {
style.el.remove();
} }
list.length = 0; list.splice(i < 0 ? list.length : i, 0, style);
table.clear(); return el;
_emitUpdate();
} }
function clearOrphans() { function addRemoveElements(add) {
for (const el of document.querySelectorAll(`style[id^="${PREFIX}"].stylus`)) { for (const {el} of list) {
const id = el.id.slice(PREFIX.length); if (add) {
if (/^\d+$/.test(id) || id === PATCH_ID) { document.documentElement.appendChild(el);
} else {
el.remove(); el.remove();
} }
} }
} }
function remove(id) { function addUpdate(style) {
_remove(id); return table.has(style.id) ? update(style) : add(style);
_emitUpdate();
} }
function replace(styleMap) { function applyTransitionPatch(styles) {
const styles = _styleMapToArray(styleMap);
const added = new Set(styles.map(s => s.id));
const removed = [];
for (const style of list) {
if (!added.has(style.id)) {
removed.push(style.id);
}
}
styles.forEach(_apply);
removed.forEach(_remove);
_emitUpdate();
}
function toggle(_enabled) {
if (isEnabled === _enabled) return;
isEnabled = _enabled;
for (const style of list) {
style.el.disabled = !isEnabled;
}
}
function _add(style) {
const el = style.el = _createStyle(style.id, style.code);
table.set(style.id, style);
const nextIndex = list.findIndex(i => compare(i, style) > 0);
if (nextIndex < 0) {
document.documentElement.appendChild(el);
list.push(style);
} else {
document.documentElement.insertBefore(el, list[nextIndex].el);
list.splice(nextIndex, 0, style);
}
// moving an element resets its 'disabled' state
el.disabled = !isEnabled;
return el;
}
function _apply(style) {
return table.has(style.id) ? _update(style) : _add(style);
}
function _applyTransitionPatch(styles) {
isTransitionPatched = true; isTransitionPatched = true;
// CSS transition bug workaround: since we insert styles asynchronously, // CSS transition bug workaround: since we insert styles asynchronously,
// the browsers, especially Firefox, may apply all transitions on page load // the browsers, especially Firefox, may apply all transitions on page load
@ -115,19 +118,20 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
!styles.some(s => s.code.includes('transition'))) { !styles.some(s => s.code.includes('transition'))) {
return; return;
} }
const el = _createStyle(PATCH_ID, ` const el = createStyle({id: PATCH_ID, code: `
:root:not(#\\0):not(#\\0) * { :root:not(#\\0):not(#\\0) * {
transition: none !important; transition: none !important;
} }
`); `});
document.documentElement.appendChild(el); document.documentElement.appendChild(el);
// wait for the next paint to complete // wait for the next paint to complete
// note: requestAnimationFrame won't fire in inactive tabs // note: requestAnimationFrame won't fire in inactive tabs
requestAnimationFrame(() => setTimeout(() => el.remove())); requestAnimationFrame(() => setTimeout(() => el.remove()));
} }
function _createStyle(id, code = '') { function createStyle(style = {}) {
if (!creationDoc) _initCreationDoc(); const {id} = style;
if (!creationDoc) initCreationDoc();
let el; let el;
if (document.documentElement instanceof SVGSVGElement) { if (document.documentElement instanceof SVGSVGElement) {
// SVG document style // SVG document style
@ -147,18 +151,27 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
el.type = 'text/css'; el.type = 'text/css';
// SVG className is not a string, but an instance of SVGAnimatedString // SVG className is not a string, but an instance of SVGAnimatedString
el.classList.add('stylus'); el.classList.add('stylus');
el.textContent = code; setTextAndName(el, style);
return el; return el;
} }
function _emitUpdate() { function setTextAndName(el, {id, code = '', name}) {
if (!IS_OWN_PAGE && list.length) { if (exposeStyleName && name) {
docRewriteObserver.start(); el.dataset.name = name;
docRootObserver.start(); name = encodeURIComponent(name.replace(/[?#/']/g, toSafeChar));
} else { code += `\n/*# sourceURL=${chrome.runtime.getURL(name)}.user.css#${id} */`;
docRewriteObserver.stop();
docRootObserver.stop();
} }
el.textContent = code;
}
function toggleObservers(shouldStart) {
const onOff = shouldStart && isEnabled ? 'start' : 'stop';
docRewriteObserver[onOff]();
docRootObserver[onOff]();
}
function emitUpdate() {
toggleObservers(list.length);
onUpdate(); onUpdate();
} }
@ -169,11 +182,11 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
and since userAgent.navigator can be spoofed via about:config or devtools, and since userAgent.navigator can be spoofed via about:config or devtools,
we're checking for getPreventDefault that was removed in FF59 we're checking for getPreventDefault that was removed in FF59
*/ */
function _initCreationDoc() { function initCreationDoc() {
creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject; creationDoc = !Event.prototype.getPreventDefault && document.wrappedJSObject;
if (creationDoc) { if (creationDoc) {
({createElement, createElementNS} = creationDoc); ({createElement, createElementNS} = creationDoc);
const el = document.documentElement.appendChild(_createStyle()); const el = document.documentElement.appendChild(createStyle());
const isApplied = el.sheet; const isApplied = el.sheet;
el.remove(); el.remove();
if (isApplied) return; if (isApplied) return;
@ -182,7 +195,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
({createElement, createElementNS} = document); ({createElement, createElementNS} = document);
} }
function _remove(id) { function remove(id) {
const style = table.get(id); const style = table.get(id);
if (!style) return; if (!style) return;
table.delete(id); table.delete(id);
@ -190,18 +203,14 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
style.el.remove(); style.el.remove();
} }
function _sort() { function sort() {
docRootObserver.evade(() => { docRootObserver.evade(() => {
list.sort(compare); list.sort(compare);
for (const style of list) { addRemoveElements(true);
// moving an element resets its 'disabled' state
document.documentElement.appendChild(style.el);
style.el.disabled = !isEnabled;
}
}); });
} }
function _sortIfNeeded() { function sortIfNeeded() {
let needsSort; let needsSort;
let el = list.length && list[0].el; let el = list.length && list[0].el;
if (!el) { if (!el) {
@ -222,32 +231,30 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
// some styles are not injected to the document // some styles are not injected to the document
if (i < list.length) needsSort = true; if (i < list.length) needsSort = true;
} }
if (needsSort) _sort(); if (needsSort) sort();
return needsSort; return needsSort;
} }
function _styleMapToArray(styleMap) { function styleMapToArray(styleMap) {
return Object.values(styleMap).map(s => ({ if (styleMap.cfg) {
id: s.id, ({exposeStyleName} = styleMap.cfg);
code: s.code.join(''), 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); const style = table.get(id);
if (style.code === code) return; if (style.code !== code ||
style.code = code; style.name !== newStyle.name && exposeStyleName) {
// workaround for Chrome devtools bug fixed in v65 style.code = code;
if (isChromePre65) { setTextAndName(style.el, newStyle);
const oldEl = style.el;
style.el = _createStyle(id, code);
oldEl.parentNode.insertBefore(style.el, oldEl.nextSibling);
oldEl.remove();
} else {
style.el.textContent = code;
} }
// https://github.com/openstyles/stylus/issues/693
style.el.disabled = !isEnabled;
} }
function RewriteObserver(onChange) { function RewriteObserver(onChange) {
@ -255,14 +262,14 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
let root; let root;
let observing = false; let observing = false;
let timer; let timer;
const observer = new MutationObserver(_check); const observer = new MutationObserver(check);
return {start, stop}; return {start, stop};
function start() { function start() {
if (observing) return; if (observing) return;
// detect dynamic iframes rewritten after creation by the embedder i.e. externally // detect dynamic iframes rewritten after creation by the embedder i.e. externally
root = document.documentElement; root = document.documentElement;
timer = setTimeout(_check); timer = setTimeout(check);
observer.observe(document, {childList: true}); observer.observe(document, {childList: true});
observing = true; observing = true;
} }
@ -274,7 +281,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
observing = false; observing = false;
} }
function _check() { function check() {
if (root !== document.documentElement) { if (root !== document.documentElement) {
root = document.documentElement; root = document.documentElement;
onChange(); onChange();
@ -304,7 +311,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
function evade(fn) { function evade(fn) {
const restore = observing && start; const restore = observing && start;
stop(); stop();
return new Promise(resolve => _run(fn, resolve, _waitForRoot)) return new Promise(resolve => run(fn, resolve, waitForRoot))
.then(restore); .then(restore);
} }
@ -322,7 +329,7 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
observing = false; observing = false;
} }
function _run(fn, resolve, wait) { function run(fn, resolve, wait) {
if (document.documentElement) { if (document.documentElement) {
resolve(fn()); resolve(fn());
return true; return true;
@ -330,8 +337,8 @@ self.createStyleInjector = self.INJECTED === 1 ? self.createStyleInjector : ({
if (wait) wait(fn, resolve); if (wait) wait(fn, resolve);
} }
function _waitForRoot(...args) { function waitForRoot(...args) {
new MutationObserver((_, observer) => _run(...args) && observer.disconnect()) new MutationObserver((_, observer) => run(...args) && observer.disconnect())
.observe(document, {childList: true}); .observe(document, {childList: true});
} }
} }

522
edit.html
View File

@ -1,135 +1,86 @@
<!DOCTYPE html>
<html id="stylus"> <html id="stylus">
<head> <head>
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link href="global.css" rel="stylesheet"> <link href="global.css" rel="stylesheet">
<link href="edit/edit.css" rel="stylesheet"> <link href="global-dark.css" rel="stylesheet">
<link rel="stylesheet" href="msgbox/msgbox.css"> <style id="cm-theme"></style>
<style id="firefox-transitions-bug-suppressor"> <script src="js/polyfill.js"></script>
/* restrict to FF */ <script src="js/toolbox.js"></script>
@supports (-moz-appearance:none) { <script src="js/msg.js"></script>
/* increased specificity to override sane selectors in user styles */ <script src="js/prefs.js"></script>
html#stylus.firefox #stylus-edit #header *, <script src="js/dom.js"></script>
html#stylus.firefox #stylus-edit #sections * { <script src="js/localization.js"></script>
transition: none !important; <script src="content/style-injector.js"></script>
} <script src="content/apply.js"></script>
}
</style> <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>
<link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
<script src="vendor/codemirror/lib/codemirror.js"></script> <script src="vendor/codemirror/lib/codemirror.js"></script>
<script src="vendor/codemirror/mode/css/css.js"></script> <script src="vendor/codemirror/mode/css/css.js"></script>
<script src="vendor/codemirror/mode/stylus/stylus.js"></script>
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
<script src="vendor/codemirror/addon/dialog/dialog.js"></script> <script src="vendor/codemirror/addon/dialog/dialog.js"></script>
<script src="vendor/codemirror/addon/edit/closebrackets.js"></script> <script src="vendor/codemirror/addon/edit/closebrackets.js"></script>
<link href="vendor/codemirror/addon/search/matchesonscrollbar.css" rel="stylesheet">
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script> <script src="vendor/codemirror/addon/scroll/annotatescrollbar.js"></script>
<script src="vendor/codemirror/addon/search/searchcursor.js"></script> <script src="vendor/codemirror/addon/search/searchcursor.js"></script>
<script src="vendor/codemirror/addon/search/matchesonscrollbar.js"></script>
<script src="vendor/codemirror/addon/comment/comment.js"></script> <script src="vendor/codemirror/addon/comment/comment.js"></script>
<script src="vendor/codemirror/addon/selection/active-line.js"></script> <script src="vendor/codemirror/addon/selection/active-line.js"></script>
<script src="vendor/codemirror/addon/edit/matchbrackets.js"></script> <script src="vendor/codemirror/addon/edit/matchbrackets.js"></script>
<link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet" />
<script src="vendor/codemirror/addon/fold/foldcode.js"></script> <script src="vendor/codemirror/addon/fold/foldcode.js"></script>
<script src="vendor/codemirror/addon/fold/foldgutter.js"></script> <script src="vendor/codemirror/addon/fold/foldgutter.js"></script>
<script src="vendor/codemirror/addon/fold/brace-fold.js"></script> <script src="vendor/codemirror/addon/fold/brace-fold.js"></script>
<script src="vendor/codemirror/addon/fold/indent-fold.js"></script> <script src="vendor/codemirror/addon/fold/indent-fold.js"></script>
<script src="vendor/codemirror/addon/fold/comment-fold.js"></script> <script src="vendor/codemirror/addon/fold/comment-fold.js"></script>
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet" />
<script src="vendor/codemirror/addon/lint/lint.js"></script> <script src="vendor/codemirror/addon/lint/lint.js"></script>
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet" />
<script src="vendor/codemirror/addon/hint/show-hint.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/addon/hint/css-hint.js"></script>
<script src="vendor/codemirror/keymap/sublime.js"></script>
<script src="vendor/codemirror/keymap/emacs.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/codemirror/keymap/vim.js"></script>
<link href="vendor-overwrites/colorpicker/colorpicker.css" rel="stylesheet">
<script src="vendor-overwrites/colorpicker/colorconverter.js"></script>
<script src="vendor-overwrites/colorpicker/colorpicker.js"></script>
<script src="vendor-overwrites/colorpicker/colorview.js"></script>
<script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script> <script src="vendor-overwrites/codemirror-addon/match-highlighter.js"></script>
<script src="js/polyfill.js"></script> <script src="js/color/color-converter.js"></script>
<script src="js/promisify.js"></script> <script src="js/color/color-mimicry.js"></script>
<script src="js/dom.js"></script> <script src="js/color/color-picker.js"></script>
<script src="js/messaging.js"></script> <script src="js/color/color-view.js"></script>
<script src="js/prefs.js"></script>
<script src="js/localization.js"></script>
<script src="js/script-loader.js"></script>
<script src="js/storage-util.js"></script>
<script src="js/msg.js"></script>
<script src="js/worker-util.js"></script> <script src="js/worker-util.js"></script>
<script src="content/style-injector.js"></script>
<script src="content/apply.js"></script>
<link href="edit/global-search.css" rel="stylesheet">
<script src="edit/global-search.js"></script>
<link href="edit/codemirror-default.css" rel="stylesheet">
<script src="edit/codemirror-default.js"></script>
<script src="edit/util.js"></script> <script src="edit/util.js"></script>
<script src="edit/regexp-tester.js"></script> <script src="edit/codemirror-default.js"></script>
<script src="edit/live-preview.js"></script>
<script src="edit/applies-to-line-widget.js"></script>
<script src="edit/reroute-hotkeys.js"></script>
<script src="edit/codemirror-factory.js"></script> <script src="edit/codemirror-factory.js"></script>
<script src="edit/colorpicker-helper.js"></script> <script src="edit/moz-section-finder.js"></script>
<script src="edit/moz-section-widget.js"></script>
<script src="edit/linter-manager.js"></script>
<script src="edit/beautify.js"></script> <script src="edit/beautify.js"></script>
<script src="edit/show-keymap-help.js"></script>
<script src="edit/refresh-on-view.js"></script>
<script src="edit/codemirror-themes.js"></script>
<script src="edit/source-editor.js"></script> <script src="edit/source-editor.js"></script>
<script src="edit/sections-editor-section.js"></script> <script src="edit/sections-editor-section.js"></script>
<script src="edit/sections-editor.js"></script> <script src="edit/sections-editor.js"></script>
<script src="edit/usw-integration.js"></script>
<script src="edit/edit.js"></script>
<script src="msgbox/msgbox.js" async></script>
<script src="edit/linter.js"></script>
<script src="edit/linter-defaults.js"></script>
<script src="edit/linter-engines.js"></script>
<script src="edit/linter-meta.js"></script>
<script src="edit/linter-help-dialog.js"></script>
<script src="edit/linter-report.js"></script>
<script src="edit/linter-config-dialog.js"></script>
<link id="cm-theme" rel="stylesheet">
<template data-id="appliesTo"> <template data-id="appliesTo">
<li class="applies-to-item"> <li class="applies-to-item">
<div class="select-resizer"> <div class="select-resizer">
<select name="applies-type" class="applies-type style-contributor"> <select name="applies-type" class="applies-type style-contributor">
<option value="url" i18n-text="appliesUrlOption"></option> <option value="url" i18n="appliesUrlOption"></option>
<option value="url-prefix" i18n-text="appliesUrlPrefixOption"></option> <option value="url-prefix" i18n="appliesUrlPrefixOption"></option>
<option value="domain" i18n-text="appliesDomainOption"></option> <option value="domain" i18n="appliesDomainOption"></option>
<option value="regexp" i18n-text="appliesRegexpOption"></option> <option value="regexp" i18n="appliesRegexpOption"></option>
</select> </select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg> <svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div> </div>
<div class="applies-value-wrapper"> <div class="applies-value-wrapper">
<input name="applies-value" class="applies-value style-contributor" spellcheck="false"> <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> <svg class="svg-icon remove"><use xlink:href="#svg-icon-minus"/></svg>
</a> </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> <svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
</a> </a>
</div> </div>
@ -137,8 +88,8 @@
</template> </template>
<template data-id="appliesToEverything"> <template data-id="appliesToEverything">
<li class="applies-to-everything" i18n-text="appliesToEverything"> <li class="applies-to-everything" i18n="appliesToEverything">
<a class="add-applies-to" i18n-text="appliesAdd" i18n-title="appliesAdd" href="#"> <a class="add-applies-to" i18n="appliesAdd, title:appliesAdd" tabindex="0">
<svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg> <svg class="svg-icon add"><use xlink:href="#svg-icon-plus"/></svg>
</a> </a>
</li> </li>
@ -148,25 +99,25 @@
<div class="section"> <div class="section">
<!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section --> <!-- not using DIV to make our CSS work for #sections > div:only-of-type .remove-section -->
<p class="deleted-section"> <p class="deleted-section">
<button class="restore-section" i18n-text="sectionRestore"></button> <button class="restore-section" i18n="sectionRestore"></button>
</p> </p>
<label i18n-text="sectionCode" class="code-label"></label> <label i18n="sectionCode" class="code-label"></label>
<div class="applies-to"> <div class="applies-to">
<label i18n-text="appliesLabel"> <label i18n="appliesLabel, title:appliesHelp" data-cmd="note">
<a href="#" class="svg-inline-wrapper applies-to-help" tabindex="0"> <a class="svg-inline-wrapper applies-to-help" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a> </a>
</label> </label>
<ul class="applies-to-list"></ul> <ul class="applies-to-list"></ul>
</div> </div>
<div class="edit-actions"> <div class="edit-actions">
<button class="remove-section" i18n-text="sectionRemove"></button> <button class="remove-section" i18n="sectionRemove"></button>
<button class="add-section" i18n-long-text="sectionAdd" i18n-short-text="genericAdd"></button> <button class="add-section" i18n="long-text:sectionAdd, short-text:genericAdd"></button>
<button class="clone-section" i18n-text="genericClone"></button> <button class="clone-section" i18n="genericClone"></button>
<button class="move-section-up"></button> <button class="move-section-up"></button>
<button class="move-section-down"></button> <button class="move-section-down"></button>
<button class="beautify-section" i18n-text="styleBeautify"></button> <button class="beautify-section" i18n="styleBeautify"></button>
<button class="test-regexp" i18n-text="styleRegexpTestButton"></button> <button class="test-regexp" i18n="genericTest"></button>
</div> </div>
</div> </div>
</template> </template>
@ -176,27 +127,27 @@
<div data-type="main"> <div data-type="main">
<div data-type="content"></div> <div data-type="content"></div>
<div data-type="actions"> <div data-type="actions">
<a data-action="case" i18n-title="searchCaseSensitive" href="#" tabindex="0">Aa</a> <a data-action="case" i18n="title:searchCaseSensitive" tabindex="0">Aa</a>
<a data-action="prev" i18n-title="genericPrevious" href="#" data-hotkey-tooltip="findPrev" tabindex="0"> <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> <svg class="svg-icon" style="transform: rotate(180deg)"><use xlink:href="#svg-icon-v"/></svg>
</a> </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> <svg class="svg-icon"><use xlink:href="#svg-icon-v"/></svg>
</a> </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> <svg class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
</a> </a>
</div> </div>
</div> </div>
<div data-type="status"> <div data-type="status">
<div class="CodeMirror-search-hint" i18n-text="searchRegexp"></div> <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>
</div> </div>
</template> </template>
<template data-id="clearSearch"> <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> <svg data-action="clear" class="svg-icon"><use xlink:href="#svg-icon-close"></use></svg>
</div> </div>
</template> </template>
@ -205,7 +156,7 @@
<div data-type="content"> <div data-type="content">
<div data-type="input-wrapper"> <div data-type="input-wrapper">
<textarea class="CodeMirror-search-field" rows="1" spellcheck="false" required <textarea class="CodeMirror-search-field" rows="1" spellcheck="false" required
i18n-placeholder="search"></textarea> i18n="placeholder:search"></textarea>
</div> </div>
</div> </div>
</template> </template>
@ -214,36 +165,36 @@
<div data-type="content"> <div data-type="content">
<div data-type="input-wrapper"> <div data-type="input-wrapper">
<textarea data-type="replace-from" <textarea data-type="replace-from"
i18n-placeholder="replace" i18n="placeholder:replace"
class="CodeMirror-search-field" rows="1" required class="CodeMirror-search-field" rows="1" required
spellcheck="false"></textarea> spellcheck="false"></textarea>
</div> </div>
<div data-type="input-wrapper"> <div data-type="input-wrapper">
<textarea data-type="replace-to" <textarea data-type="replace-to"
i18n-placeholder="replaceWith" i18n="placeholder:replaceWith"
class="CodeMirror-search-field" rows="1" required class="CodeMirror-search-field" rows="1" required
spellcheck="false"></textarea> spellcheck="false"></textarea>
</div> </div>
<button data-action="replace" i18n-text="replace" disabled></button> <button data-action="replace" i18n="replace" disabled></button>
<button data-action="replaceAll" i18n-text="replaceAll" disabled></button> <button data-action="replaceAll" i18n="replaceAll" disabled></button>
<button data-action="undo" i18n-text="undo" disabled></button> <button data-action="undo" i18n="undo" disabled></button>
<!-- <!--
Using a separate set of buttons because Using a separate set of buttons because
1. FF can display tooltips only when specified on the <button>, ignores the nested <title> in <svg> 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 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"> <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"/> <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> </svg>
</button> </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"> <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,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 "/> <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> </svg>
</button> </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"> <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"/> <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> </svg>
@ -252,7 +203,7 @@
</template> </template>
<template data-id="jumpToLine"> <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>
<template data-id="regexpTestPartial"> <template data-id="regexpTestPartial">
@ -260,15 +211,15 @@
</template> </template>
<template data-id="resizeGrip"> <template data-id="resizeGrip">
<div class="resize-grip" i18n-title="cm_resizeGripHint"></div> <div class="resize-grip" i18n="title:cm_resizeGripHint"></div>
</template> </template>
<template data-id="keymapHelp"> <template data-id="keymapHelp">
<table class="keymap-list"> <table class="keymap-list">
<thead> <thead>
<tr> <tr>
<th><input i18n-placeholder="helpKeyMapHotkey" type="search" class="can-close-on-esc"></th> <th><input i18n="placeholder:helpKeyMapHotkey" type="search"></th>
<th><input i18n-placeholder="helpKeyMapCommand" type="search" class="can-close-on-esc" spellcheck="false"></th> <th><input i18n="placeholder:helpKeyMapCommand" type="search"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@ -279,185 +230,225 @@
</tbody> </tbody>
</table> </table>
</template> </template>
</head>
<body id="stylus-edit"> <link href="vendor/codemirror/lib/codemirror.css" rel="stylesheet">
<link href="vendor/codemirror/addon/dialog/dialog.css" rel="stylesheet">
<link href="vendor/codemirror/addon/fold/foldgutter.css" rel="stylesheet">
<link href="vendor/codemirror/addon/hint/show-hint.css" rel="stylesheet">
<link href="vendor/codemirror/addon/lint/lint.css" rel="stylesheet">
<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">
<template data-id="body"> <!-- https://crbug.com/1288447 -->
<div id="header"> <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"> <section id="basic-info">
<div id="basic-info-name"> <div id="basic-info-name">
<input id="name" class="style-contributor" spellcheck="false" required> <input id="name" class="style-contributor" spellcheck="false">
<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 "/>
</svg>
</a>
<a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a> <a id="url" target="_blank"><svg class="svg-icon"><use xlink:href="#svg-icon-external-link"/></svg></a>
</div> </div>
<div id="basic-info-enabled"> <div id="basic-info-enabled">
<label id="enabled-label" <label id="enabled-label"
i18n-text="styleEnabledLabel" i18n="styleEnabledLabel, title:toggleStyle"
i18n-title="toggleStyle"
data-hotkey-tooltip="toggleStyle"> data-hotkey-tooltip="toggleStyle">
<input type="checkbox" id="enabled" class="style-contributor"> <input type="checkbox" id="enabled" class="style-contributor">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label> </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"> <input type="checkbox" id="editor.livePreview">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label> </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> </div>
</section> </section>
<section id="actions"> <section id="actions">
<div> <div class="buttons">
<button id="save-button" i18n-text="styleSaveLabel" data-hotkey-tooltip="save"></button> <div class="split-btn">
<button id="beautify" i18n-text="styleBeautify"></button> <button id="save-button" i18n="styleSaveLabel" data-hotkey-tooltip="save" disabled></button
<a href="manage.html" tabindex="-1"><button id="cancel-button" i18n-text="styleCancelEditLabel"></button></a> ><button class="split-btn-pedal usercss-only" i18n="menu-tpl:saveAsTemplate"></button>
</div>
<div id="mozilla-format-container">
<h2 id="mozilla-format-heading" i18n-text="styleMozillaFormatHeading">
<a id="to-mozilla-help" class="svg-inline-wrapper" href="#" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</h2>
<div id="mozilla-format-buttons">
<button id="from-mozilla" i18n-text="importLabel"></button>
<button id="to-mozilla" i18n-text="exportLabel"></button>
</div> </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="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> </div>
</section> </section>
<details id="options" data-pref="editor.options.expanded"> <div id="details-wrapper">
<summary><h2 id="options-heading" i18n-text="optionsHeading"></h2></summary> <details id="options" data-pref="editor.options.expanded" class="ignore-pref-if-compact">
<div id="options-wrapper"> <summary><h2 id="options-heading" i18n="editorSettings"></h2></summary>
<div class="options-column"> <div id="options-wrapper">
<div class="option"> <div class="options-column">
<label id="lineWrapping-label" i18n-text="cm_lineWrapping"> <div class="option">
<input id="editor.lineWrapping" type="checkbox"> <label id="lineWrapping-label" i18n="cm_lineWrapping">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg> <input id="editor.lineWrapping" type="checkbox">
</label> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</div> </label>
<div class="option">
<label id="smartIndent-label" i18n-text="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">
<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">
<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">
<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">
<input id="editor.selectByTokens" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
<div class="option">
<label i18n-text="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">
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg>
</a>
</div>
<div class="option usercss-only">
<label i18n-text="appliesLineWidgetLabel" i18n-title="appliesLineWidgetWarning">
<input id="editor.appliesToLineWidget" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div>
</div>
<div class="options-column">
<div class="option aligned">
<label id="tabSize-label" for="editor.tabSize" i18n-text="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>
<div class="select-resizer">
<select id="editor.keyMap"></select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div> </div>
<a id="keyMap-help" href="#" class="svg-inline-wrapper" tabindex="0"> <div class="option">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <label id="smartIndent-label" i18n="cm_smartIndent">
</a> <input id="editor.smartIndent" type="checkbox">
</div> <svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
<div class="option aligned"> </label>
<label id="theme-label" for="editor.theme" i18n-text="cm_theme"></label> </div>
<div class="select-resizer"> <div class="option">
<select id="editor.theme"></select> <label id="indentWithTabs-label" i18n="cm_indentWithTabs">
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg> <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="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="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="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="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" 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="appliesLineWidgetLabel, title:appliesLineWidgetWarning">
<input id="editor.appliesToLineWidget" type="checkbox">
<svg class="svg-icon checked"><use xlink:href="#svg-icon-checked"/></svg>
</label>
</div> </div>
</div> </div>
<div class="option aligned"> <div class="options-column">
<label id="highlight-label" for="editor.matchHighlight" i18n-text="cm_matchHighlight"></label> <div class="option aligned">
<div class="select-resizer"> <label id="tabSize-label" for="editor.tabSize" i18n="cm_tabSize"></label>
<select id="editor.matchHighlight"> <input id="editor.tabSize" type="number" min="0">
<option i18n-text="cm_matchHighlightToken" value="token">
<option i18n-text="cm_matchHighlightSelection" value="selection">
<option i18n-text="genericDisabledLabel" value="">
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div> </div>
</div> <div class="option aligned">
<div class="option aligned"> <label id="keyMap-label" for="editor.keyMap" i18n="cm_keyMap"></label>
<label id="linter-label" for="editor.linter" i18n-text="cm_linter"></label>
<div class="select-resizer"> <div class="select-resizer">
<select id="editor.linter"> <select id="editor.keyMap"></select>
<option value="csslint" selected>CSSLint</option> <svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
<option value="stylelint">Stylelint</option> </div>
<option value="" i18n-text="genericDisabledLabel"></option> <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="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="cm_matchHighlight"></label>
<div class="select-resizer">
<select id="editor.matchHighlight">
<option i18n="cm_matchHighlightToken" value="token">
<option i18n="cm_matchHighlightSelection" value="selection">
<option i18n="genericDisabledLabel" value="">
</select> </select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg> <svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div> </div>
<a id="linter-settings" href="#" class="svg-inline-wrapper" i18n-title="linterConfigTooltip" tabindex="0"> </div>
<svg class="svg-icon settings"><use xlink:href="#svg-icon-settings"/></svg> <div class="option aligned">
</a> <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="genericDisabledLabel"></option>
</select>
<svg class="svg-icon select-arrow"><use xlink:href="#svg-icon-select-arrow"/></svg>
</div>
<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>
</div> </div>
</div> </details>
</details> <details id="publish" data-pref="editor.publish.expanded" class="ignore-pref-if-compact">
<details id="lint" class="hidden-unless-compact" data-pref="editor.lint.expanded"> <summary><h2 i18n="publish"></h2></summary>
<summary> <div>
<h2 i18n-text="linterIssues">: <span id="issue-count"></span> <a id="usw-url" href="https://userstyles.world" target="_blank">&nbsp;</a>
<a id="lint-help" href="#" class="svg-inline-wrapper intercepts-click" tabindex="0"> <div id="usw-link-info">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg> <dl><dt i18n="styleName"></dt><dd data-usw="name"></dd></dl>
</a> <dl><dt i18n="genericDescription"></dt><dd data-usw="description"></dd></dl>
</h2> </div>
</summary> <div>
<div class="lint-scroll-container"> <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="sections"></h2></summary>
<ol id="toc"></ol>
</details>
<details id="lint" data-pref="editor.lint.expanded" class="ignore-pref-if-compact" hidden>
<summary>
<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-report-container"></div> <div class="lint-report-container"></div>
</div> </details>
</details> </div>
<div id="header-resizer" i18n="title:headerResizerHint"></div>
<div id="footer" class="hidden"> <div id="footer" class="hidden">
<a href="https://github.com/openstyles/stylus/wiki/Usercss" <a href="https://github.com/openstyles/stylus/wiki/Usercss"
i18n-text="externalUsercssDocument" i18n="externalUsercssDocument"
target="_blank"></a> target="_blank"></a>
</div> </div>
</div> </div>
<section id="sections"> <section id="sections"></section>
<!--
It seems that we don't use these anymore
https://github.com/openstyles/stylus/blob/5cbe8a8d780a6eb9fce11d5846e92bf244c3a3f3/edit/sections.js#L18
-->
<!-- <h2><span id="sections-heading" i18n-text="styleSectionsTitle"></span>
<a id="sections-help" href="#" class="svg-inline-wrapper" tabindex="0">
<svg class="svg-icon info"><use xlink:href="#svg-icon-help"/></svg>
</a>
</h2> -->
</section>
<div id="help-popup"> <div id="help-popup">
<div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg> <div class="title"></div><svg id="sections-help" class="svg-icon dismiss"><use xlink:href="#svg-icon-close"/></svg>
<div class="contents"></div> <div class="contents"></div>
@ -469,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> <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>
<symbol id="svg-icon-help" viewBox="0 0 14 16" i18n-alt="helpAlt"> <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> <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>
<symbol id="svg-icon-close" viewBox="0 0 12 16"> <symbol id="svg-icon-close" viewBox="0 0 12 16">
@ -481,8 +474,8 @@
<path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/> <path d="M8,11.5L2.8,6.3l1.5-1.5L8,8.6l3.7-3.7l1.5,1.5L8,11.5z"/>
</symbol> </symbol>
<symbol id="svg-icon-settings" viewBox="0 0 16 16"> <symbol id="svg-icon-config" viewBox="0 0 16 16">
<path d="M8,0C7.6,0,7.3,0,6.9,0.1v2.2C6.1,2.5,5.4,2.8,4.8,3.2L3.2,1.6c-0.6,0.4-1.1,1-1.6,1.6l1.6,1.6C2.8,5.4,2.5,6.1,2.3,6.9H0.1C0,7.3,0,7.6,0,8c0,0.4,0,0.7,0.1,1.1h2.2c0.1,0.8,0.4,1.5,0.9,2.1l-1.6,1.6c0.4,0.6,1,1.1,1.6,1.6l1.6-1.6c0.6,0.4,1.4,0.7,2.1,0.9v2.2C7.3,16,7.6,16,8,16c0.4,0,0.7,0,1.1-0.1v-2.2c0.8-0.1,1.5-0.4,2.1-0.9l1.6,1.6c0.6-0.4,1.1-1,1.6-1.6l-1.6-1.6c0.4-0.6,0.7-1.4,0.9-2.1h2.2C16,8.7,16,8.4,16,8c0-0.4,0-0.7-0.1-1.1h-2.2c-0.1-0.8-0.4-1.5-0.9-2.1l1.6-1.6c-0.4-0.6-1-1.1-1.6-1.6l-1.6,1.6c-0.6-0.4-1.4-0.7-2.1-0.9V0.1C8.7,0,8.4,0,8,0z M8,4.3c2.1,0,3.7,1.7,3.7,3.7c0,0,0,0,0,0c0,2.1-1.7,3.7-3.7,3.7c0,0,0,0,0,0c-2.1,0-3.7-1.7-3.7-3.7c0,0,0,0,0,0C4.3,5.9,5.9,4.3,8,4.3C8,4.3,8,4.3,8,4.3z"/> <path d="M13.3,12.8l1.5-2.6l-2.2-1.5c0-0.2,0.1-0.5,0.1-0.7c0-0.2,0-0.5-0.1-0.7l2.2-1.5l-1.5-2.6l-2.4,1.2 c-0.4-0.3-0.8-0.5-1.2-0.7L9.5,1h-3L6.3,3.7C5.9,3.8,5.5,4.1,5.1,4.4L2.7,3.2L1.2,5.8l2.2,1.5c0,0.2-0.1,0.5-0.1,0.7 c0,0.2,0,0.5,0.1,0.7l-2.2,1.5l1.5,2.6l2.4-1.2c0.4,0.3,0.8,0.5,1.2,0.7L6.5,15h3l0.2-2.7c0.4-0.2,0.8-0.4,1.2-0.7L13.3,12.8z M8,10.3c-1.3,0-2.3-1-2.3-2.3c0-1.3,1-2.3,2.3-2.3c1.3,0,2.3,1,2.3,2.3C10.3,9.3,9.3,10.3,8,10.3z"/>
</symbol> </symbol>
<symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792"> <symbol id="svg-icon-select-arrow" viewBox="0 0 1792 1792">
@ -494,14 +487,21 @@
</symbol> </symbol>
<symbol id="svg-icon-plus" viewBox="0 0 8 8"> <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>
<symbol id="svg-icon-minus" viewBox="0 0 8 8"> <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> </symbol>
</svg> </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> </body>
</html> </html>

View File

@ -1,590 +0,0 @@
/* global regExpTester debounce messageBox CodeMirror template colorMimicry msg
$ $create t prefs tryCatch deepEqual */
/* exported createAppliesToLineWidget */
'use strict';
function createAppliesToLineWidget(cm) {
const THROTTLE_DELAY = 400;
const RX_SPACE = /(?:\s+|\/\*)+/y;
let TPL, EVENTS, CLICK_ROUTE;
let widgets = [];
let fromLine, toLine, actualStyle;
let initialized = false;
return {toggle};
function toggle(newState = !initialized) {
newState = Boolean(newState);
if (newState !== initialized) {
if (newState) {
init();
} else {
uninit();
}
}
}
function init() {
initialized = true;
TPL = {
container:
$create('div.applies-to', [
$create('label', t('appliesLabel')),
$create('ul.applies-to-list'),
]),
listItem: template.appliesTo.cloneNode(true),
appliesToEverything:
$create('li.applies-to-everything', t('appliesToEverything')),
};
$('.applies-value', TPL.listItem).insertAdjacentElement('afterend',
$create('button.test-regexp', t('styleRegexpTestButton')));
CLICK_ROUTE = {
'.test-regexp': showRegExpTester,
'.remove-applies-to': (item, apply, event) => {
event.preventDefault();
const applies = item.closest('.applies-to').__applies;
const i = applies.indexOf(apply);
let repl;
let from;
let to;
if (applies.length < 2) {
messageBox({
contents: t('appliesRemoveError'),
buttons: [t('confirmClose')]
});
return;
}
if (i === 0) {
from = apply.mark.find().from;
to = applies[i + 1].mark.find().from;
repl = '';
} else if (i === applies.length - 1) {
from = applies[i - 1].mark.find().to;
to = apply.mark.find().to;
repl = '';
} else {
from = applies[i - 1].mark.find().to;
to = applies[i + 1].mark.find().from;
repl = ', ';
}
cm.replaceRange(repl, from, to, 'appliesTo');
clearApply(apply);
item.remove();
applies.splice(i, 1);
},
'.add-applies-to': (item, apply, event) => {
event.preventDefault();
const applies = item.closest('.applies-to').__applies;
const i = applies.indexOf(apply);
const pos = apply.mark.find().to;
const text = `, ${apply.type.text}("")`;
cm.replaceRange(text, pos, pos, 'appliesTo');
const newApply = createApply(
cm.indexFromPos(pos) + 2,
apply.type.text,
'',
true
);
setupApplyMarkers(newApply);
applies.splice(i + 1, 0, newApply);
item.insertAdjacentElement('afterend', buildChildren(applies, newApply));
},
};
EVENTS = {
onchange({target}) {
const typeElement = target.closest('.applies-type');
if (typeElement) {
const item = target.closest('.applies-to-item');
const apply = item.__apply;
changeItem(item, apply, 'type', typeElement.value);
item.dataset.type = apply.type.text;
} else {
return EVENTS.oninput.apply(this, arguments);
}
},
oninput({target}) {
if (target.matches('.applies-value')) {
const item = target.closest('.applies-to-item');
const apply = item.__apply;
changeItem(item, apply, 'value', target.value);
}
},
onclick(event) {
const {target} = event;
for (const selector in CLICK_ROUTE) {
const routed = target.closest(selector);
if (routed) {
const item = routed.closest('.applies-to-item');
CLICK_ROUTE[selector].call(routed, item, item.__apply, event);
return;
}
}
}
};
actualStyle = $create('style');
fromLine = 0;
toLine = cm.doc.size;
cm.on('change', onChange);
cm.on('optionChange', onOptionChange);
msg.onExtension(onRuntimeMessage);
requestAnimationFrame(updateWidgetStyle);
update();
}
function uninit() {
initialized = false;
widgets.forEach(clearWidget);
widgets.length = 0;
cm.off('change', onChange);
cm.off('optionChange', onOptionChange);
msg.off(onRuntimeMessage);
actualStyle.remove();
}
function onChange(cm, event) {
const {from, to, origin} = event;
if (origin === 'appliesTo') {
return;
}
const lastChanged = CodeMirror.changeEnd(event).line;
fromLine = Math.min(fromLine === null ? from.line : fromLine, from.line);
toLine = Math.max(toLine === null ? lastChanged : toLine, to.line);
if (origin === 'setValue') {
update();
} else {
debounce(update, THROTTLE_DELAY);
}
}
function onOptionChange(cm, option) {
if (option === 'theme') {
updateWidgetStyle();
}
}
function onRuntimeMessage(msg) {
if (msg.reason === 'editPreview' && !$(`#stylus-${msg.style.id}`)) {
// no style element with this id means the style doesn't apply to the editor URL
return;
}
if (msg.style || msg.styles ||
msg.prefs && 'disableAll' in msg.prefs ||
msg.method === 'styleDeleted') {
requestAnimationFrame(updateWidgetStyle);
}
}
function update() {
const changed = {fromLine, toLine};
fromLine = Math.max(fromLine || 0, cm.display.viewFrom);
toLine = Math.min(toLine === null ? cm.doc.size : toLine, cm.display.viewTo || toLine);
const visible = {fromLine, toLine};
const {curOp} = cm;
if (fromLine >= cm.display.viewFrom && toLine <= (cm.display.viewTo || toLine)) {
if (!curOp) cm.startOperation();
doUpdate();
if (!curOp) cm.endOperation();
}
if (changed.fromLine !== visible.fromLine || changed.toLine !== visible.toLine) {
setTimeout(updateInvisible, 0, changed, visible);
}
}
function updateInvisible(changed, visible) {
let inOp = false;
if (changed.fromLine < visible.fromLine) {
fromLine = Math.min(fromLine, changed.fromLine);
toLine = Math.min(changed.toLine, visible.fromLine);
inOp = true;
cm.startOperation();
doUpdate();
}
if (changed.toLine > visible.toLine) {
fromLine = Math.max(fromLine, changed.toLine);
toLine = Math.max(changed.toLine, visible.toLine);
if (!inOp) {
inOp = true;
cm.startOperation();
}
doUpdate();
}
if (inOp) {
cm.endOperation();
}
}
function updateWidgetStyle() {
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 = {
wrapper: colorMimicry.get(cm.display.wrapper),
gutter: colorMimicry.get(cm.display.gutters, {
bg: 'backgroundColor',
border: 'borderRightColor',
}),
line: colorMimicry.get('.CodeMirror-linenumber', null, cm.display.lineDiv),
comment: colorMimicry.get('span.cm-comment', null, cm.display.lineDiv),
};
const hasBorder =
color.gutter.style.borderRightWidth !== '0px' &&
!/transparent|\b0\)/g.test(color.gutter.style.borderRightColor);
const diff = {
wrapper: Math.abs(color.gutter.bgLuma - color.wrapper.foreLuma),
border: hasBorder ? Math.abs(color.gutter.bgLuma - color.gutter.borderLuma) : 0,
line: Math.abs(color.gutter.bgLuma - color.line.foreLuma),
};
const preferLine = diff.line > diff.wrapper || diff.line > MIN_LUMA_DIFF;
const fore = preferLine ? color.line.fore : color.wrapper.fore;
const border = fore.replace(/[\d.]+(?=\))/, MIN_LUMA_DIFF / 2);
const borderStyleForced = `1px ${hasBorder ? color.gutter.style.borderRightStyle : 'solid'} ${border}`;
actualStyle.textContent = `
.applies-to {
background-color: ${color.gutter.bg};
border-top: ${borderStyleForced};
border-bottom: ${borderStyleForced};
}
.applies-to label {
color: ${fore};
}
.applies-to input,
.applies-to button,
.applies-to select {
background: rgba(255, 255, 255, ${
Math.max(MIN_LUMA, Math.pow(Math.max(0, color.gutter.bgLuma - MIN_LUMA * 2), 2)).toFixed(2)
});
border: ${borderStyleForced};
transition: none;
color: ${fore};
}
.applies-to .svg-icon.select-arrow {
fill: ${fore};
transition: none;
}
`;
document.documentElement.appendChild(actualStyle);
}
function doUpdate() {
// find which widgets needs to be update
// some widgets (lines) might be deleted
widgets = widgets.filter(w => w.line.lineNo() !== null);
let i = widgets.findIndex(w => w.line.lineNo() > fromLine) - 1;
let j = widgets.findIndex(w => w.line.lineNo() > toLine);
if (i === -2) {
i = widgets.length - 1;
}
if (j < 0) {
j = widgets.length;
}
// decide search range
const fromPos = {line: widgets[i] ? widgets[i].line.lineNo() : 0, ch: 0};
const toPos = {line: widgets[j] ? widgets[j].line.lineNo() : toLine + 1, ch: 0};
// calc index->pos lookup table
let index = 0;
const lineIndexes = [0];
cm.doc.iter(0, toPos.line + 1, ({text}) => {
lineIndexes.push((index += text.length + 1));
});
// splice
i = Math.max(0, i);
widgets.splice(i, 0, ...createWidgets(fromPos, toPos, widgets.splice(i, j - i), lineIndexes));
fromLine = null;
toLine = null;
}
function *createWidgets(start, end, removed, lineIndexes) {
let i = 0;
let itemHeight;
for (const section of findAppliesTo(start, end, lineIndexes)) {
let removedWidget = removed[i];
while (removedWidget && removedWidget.line.lineNo() < section.pos.line) {
clearWidget(removed[i]);
removedWidget = removed[++i];
}
if (removedWidget && deepEqual(removedWidget.node.__applies, section.applies, ['mark'])) {
yield removedWidget;
i++;
continue;
}
for (const a of section.applies) {
setupApplyMarkers(a, lineIndexes);
}
if (removedWidget && removedWidget.line.lineNo() === section.pos.line) {
// reuse old widget
removedWidget.section.applies.forEach(apply => {
apply.type.mark.clear();
apply.value.mark.clear();
});
removedWidget.section = section;
const newNode = buildElement(section);
const removedNode = removedWidget.node;
if (removedNode.parentNode) {
removedNode.parentNode.replaceChild(newNode, removedNode);
}
removedWidget.node = newNode;
removedWidget.changed();
yield removedWidget;
i++;
continue;
}
// new widget
const widget = cm.addLineWidget(section.pos.line, buildElement(section), {
coverGutter: true,
noHScroll: true,
above: true,
height: itemHeight ? section.applies.length * itemHeight : undefined,
});
widget.section = section;
itemHeight = itemHeight || widget.node.offsetHeight / (section.applies.length || 1);
yield widget;
}
removed.slice(i).forEach(clearWidget);
}
function clearWidget(widget) {
widget.clear();
widget.section.applies.forEach(clearApply);
}
function clearApply(apply) {
apply.type.mark.clear();
apply.value.mark.clear();
apply.mark.clear();
}
function setupApplyMarkers(apply, lineIndexes) {
apply.type.mark = cm.markText(
posFromIndex(cm, apply.type.start, lineIndexes),
posFromIndex(cm, apply.type.end, lineIndexes),
{clearWhenEmpty: false}
);
apply.value.mark = cm.markText(
posFromIndex(cm, apply.value.start, lineIndexes),
posFromIndex(cm, apply.value.end, lineIndexes),
{clearWhenEmpty: false}
);
apply.mark = cm.markText(
posFromIndex(cm, apply.start, lineIndexes),
posFromIndex(cm, apply.end, lineIndexes),
{clearWhenEmpty: false}
);
}
function posFromIndex(cm, index, lineIndexes) {
if (!lineIndexes) {
return cm.posFromIndex(index);
}
let line = lineIndexes.prev || 0;
const prev = lineIndexes[line];
const next = lineIndexes[line + 1];
if (prev <= index && index < next) {
return {line, ch: index - prev};
}
let a = index < prev ? 0 : line;
let b = index < next ? line + 1 : lineIndexes.length - 1;
while (a < b - 1) {
const mid = (a + b) >> 1;
if (lineIndexes[mid] < index) {
a = mid;
} else {
b = mid;
}
}
line = lineIndexes[b] > index ? a : b;
Object.defineProperty(lineIndexes, 'prev', {value: line, configurable: true});
return {line, ch: index - lineIndexes[line]};
}
function buildElement({applies}) {
const container = TPL.container.cloneNode(true);
const list = $('.applies-to-list', container);
for (const apply of applies) {
list.appendChild(buildChildren(applies, apply));
}
if (!list.children[0]) {
list.appendChild(TPL.appliesToEverything.cloneNode(true));
}
return Object.assign(container, EVENTS, {__applies: applies});
}
function buildChildren(applies, apply) {
const el = TPL.listItem.cloneNode(true);
el.dataset.type = apply.type.text;
el.__apply = apply;
$('.applies-type', el).value = apply.type.text;
$('.applies-value', el).value = apply.value.text;
return el;
}
function changeItem(itemElement, apply, part, newText) {
if (!apply) {
return;
}
part = apply[part];
const range = part.mark.find();
part.mark.clear();
newText = unescapeDoubleslash(newText).replace(/\\/g, '\\\\');
cm.replaceRange(newText, range.from, range.to, 'appliesTo');
part.mark = cm.markText(
range.from,
cm.findPosH(range.from, newText.length, 'char'),
{clearWhenEmpty: false}
);
part.text = newText;
if (part === apply.type) {
const range = apply.mark.find();
apply.mark.clear();
apply.mark = cm.markText(
part.mark.find().from,
range.to,
{clearWhenEmpty: false}
);
}
if (apply.type.text === 'regexp' && apply.value.text.trim()) {
showRegExpTester(itemElement);
}
}
function createApply(pos, typeText, valueText, isQuoted = false) {
typeText = typeText.toLowerCase();
const start = pos;
const typeStart = start;
const typeEnd = typeStart + typeText.length;
const valueStart = typeEnd + 1 + Number(isQuoted);
const valueEnd = valueStart + valueText.length;
const end = valueEnd + Number(isQuoted) + 1;
return {
start,
type: {
text: typeText,
start: typeStart,
end: typeEnd,
},
value: {
text: unescapeDoubleslash(valueText),
start: valueStart,
end: valueEnd,
},
end
};
}
function *findAppliesTo(posStart, posEnd, lineIndexes) {
const funcRe = /^(url|url-prefix|domain|regexp)$/i;
let pos;
const eatToken = sticky => {
if (!sticky) skipSpace(pos, posEnd);
pos.ch++;
const token = cm.getTokenAt(pos, true);
pos.ch = token.end;
return CodeMirror.cmpPos(pos, posEnd) <= 0 ? token : {};
};
const docCur = cm.getSearchCursor('@-moz-document', posStart);
while (docCur.findNext() &&
CodeMirror.cmpPos(docCur.pos.to, posEnd) <= 0) {
// CM can be nitpicky at token boundary so we'll check the next character
const safePos = {line: docCur.pos.from.line, ch: docCur.pos.from.ch + 1};
if (/\b(string|comment)\b/.test(cm.getTokenTypeAt(safePos))) continue;
const applies = [];
pos = docCur.pos.to;
do {
skipSpace(pos, posEnd);
const funcIndex = lineIndexes[pos.line] + pos.ch;
const func = eatToken().string;
// no space allowed before the opening parenthesis
if (!funcRe.test(func) || eatToken(true).string !== '(') break;
const url = eatToken();
if (url.type !== 'string' || eatToken().string !== ')') break;
const unquotedUrl = unquote(url.string);
const apply = createApply(
funcIndex,
func,
unquotedUrl,
unquotedUrl !== url.string
);
applies.push(apply);
} while (eatToken().string === ',');
yield {
pos: docCur.pos.from,
applies
};
}
}
function skipSpace(pos, posEnd) {
let {ch, line} = pos;
let lookForEnd;
line--;
cm.doc.iter(pos.line, posEnd.line + 1, ({text}) => {
line++;
while (true) {
if (lookForEnd) {
ch = text.indexOf('*/', ch) + 1;
if (!ch) {
return;
}
ch++;
lookForEnd = false;
}
// EOL is a whitespace so we'll check the next line
if (ch >= text.length) {
ch = 0;
return;
}
RX_SPACE.lastIndex = ch;
const m = RX_SPACE.exec(text);
if (!m) {
return true;
}
ch += m[0].length;
lookForEnd = m[0].includes('/*');
if (ch < text.length && !lookForEnd) {
return true;
}
}
});
pos.line = line;
pos.ch = ch;
}
function unquote(s) {
const first = s.charAt(0);
return (first === '"' || first === "'") && s.endsWith(first) ? s.slice(1, -1) : s;
}
function unescapeDoubleslash(s) {
const hasSingleEscapes = /([^\\]|^)\\([^\\]|$)/.test(s);
return hasSingleEscapes ? s : s.replace(/\\\\/g, '\\');
}
function showRegExpTester(item) {
regExpTester.toggle(true);
regExpTester.update(
item.closest('.applies-to').__applies
.filter(a => a.type.text === 'regexp')
.map(a => unescapeDoubleslash(a.value.text)));
}
}

274
edit/autocomplete.js Normal file
View File

@ -0,0 +1,274 @@
/* global CodeMirror */
/* global cmFactory */
/* global debounce */// toolbox.js
/* global editor */
/* global linterMan */
/* global prefs */
'use strict';
/* Registers 'hint' helper and 'autocompleteOnTyping' option in CodeMirror */
(() => {
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'];
const docFuncs = addSuffix(cssMime.documentTypes, '(');
const {tokenHooks} = cssMime;
const originalCommentHook = tokenHooks['/'];
const originalHelper = CodeMirror.hint.css || (() => {});
let cssMedia, cssProps, cssValues;
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;
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') {
return originalHelper(cm);
}
// not using getTokenAt until the need is unavoidable because it reparses text
// and runs a whole lot of complex calc inside which is slow on long lines
// especially if autocomplete is auto-shown on each keystroke
let prev, end, state;
let i = index;
while (
(prev == null || `${styles[i - 1]}`.startsWith(type)) &&
(prev = i > 2 ? styles[i - 2] : 0) &&
isSameToken(text, style, prev)
) i -= 2;
i = index;
while (
(end == null || `${styles[i + 1]}`.startsWith(type)) &&
(end = styles[i]) &&
isSameToken(text, style, end)
) i += 2;
const getTokenState = () => state || (state = cm.getTokenAt(pos, true).state.state);
const str = text.slice(prev, end);
const left = text.slice(prev, ch).trim();
let leftLC = left.toLowerCase();
let list;
switch (leftLC[0]) {
case '!':
list = '!important'.startsWith(leftLC) ? ['!important'] : [];
break;
case '@':
list = [
'@-moz-document',
'@charset',
'@font-face',
'@import',
'@keyframes',
'@media',
'@namespace',
'@page',
'@supports',
'@viewport',
];
if (isLessLang) list = findAllCssVars(cm, left, '\\s*:').concat(list);
break;
case '#': // prevents autocomplete for #hex colors
break;
case '-': // --variable
case '(': // var(
list = str.startsWith('--') || testAt(rxVAR, ch - 5, text)
? findAllCssVars(cm, left)
: [];
if (str.startsWith('(')) {
prev++;
leftLC = left.slice(1);
} else {
leftLC = left;
}
break;
case '/': // USO vars
if (str.startsWith('/*[[') && str.endsWith(']]*/')) {
prev += 4;
end -= 4;
end -= text.slice(end - 4, end) === '-rgb' ? 4 : 0;
list = Object.keys((editor.style.usercssData || {}).vars || {}).sort();
leftLC = left.slice(4);
}
break;
case 'u': // url(), url-prefix()
case 'd': // domain()
case 'r': // regexp()
if (/^(variable|tag|error)/.test(type) &&
docFuncs.some(s => s.startsWith(leftLC)) &&
/^(top|documentTypes|atBlock)/.test(getTokenState())) {
end++;
list = docFuncs;
break;
}
// 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 (!list &&
/^(prop(erty|\?)|atom|error|tag)/.test(type) &&
/^(block|atBlock_parens|maybeprop)/.test(getTokenState())) {
if (!cssProps) await initCssProps();
if (type === 'prop?') {
prev += leftLC.length;
leftLC = '';
}
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;
}
if (!list) {
return isStylusLang
? CodeMirror.hint.fromList(cm, {words: CodeMirror.hintWords.stylus})
: originalHelper(cm);
}
}
return {
list: (list || []).filter(s => s.startsWith(leftLC)),
from: {line, ch: prev + str.match(/^\s*/)[0].length},
to: {line, ch: end},
};
}
async function initCssProps() {
cssValues = await linterMan.worker.getCssPropsValues();
cssProps = addSuffix(cssValues.all);
cssMedia = [].concat(...Object.entries(cssMime).map(getMediaKeys).filter(Boolean)).sort();
}
function addSuffix(obj, suffix = ': ') {
// Sorting first, otherwise "foo-bar:" would precede "foo:"
return Object.keys(obj).sort().map(k => k + suffix);
}
function getMediaKeys([k, v]) {
return k === 'mediaFeatures' && addSuffix(v) ||
k.startsWith('media') && Object.keys(v);
}
/** makes sure we don't process a different adjacent comment */
function isSameToken(text, style, i) {
return !style || text[i] !== '/' && text[i + 1] !== '*' ||
!style.startsWith(USO_VALID_VAR) && !style.startsWith(USO_INVALID_VAR);
}
function findAllCssVars(cm, leftPart, rightPart = '') {
// simplified regex without CSS escapes
const [, prefixed, named] = leftPart.match(/^(--|@)?(\S)?/);
const rx = new RegExp(
'(?:^|[\\s/;{])(' +
(prefixed ? leftPart : '--') +
(named ? '' : '[a-zA-Z_\u0080-\uFFFF]') +
'[-0-9a-zA-Z_\u0080-\uFFFF]*)' +
rightPart,
'g');
const list = new Set();
cm.eachLine(({text}) => {
for (let m; (m = rx.exec(text));) {
list.add(m[1]);
}
});
return [...list].sort();
}
function tokenizeUsoVariables(stream) {
const token = originalCommentHook.apply(this, arguments);
if (token[1] === 'comment') {
const {string, start, pos} = stream;
if (testAt(/\/\*\[\[/y, start, string) &&
testAt(/]]\*\//y, pos - 4, string)) {
const vars = (editor.style.usercssData || {}).vars;
token[0] =
vars && vars.hasOwnProperty(string.slice(start + 4, pos - 4).replace(/-rgb$/, ''))
? USO_VALID_VAR
: USO_INVALID_VAR;
}
}
return token;
}
function execAt(rx, index, text) {
rx.lastIndex = index;
return rx.exec(text);
}
function testAt(rx, index, text) {
rx.lastIndex = Math.max(0, index);
return rx.test(text);
}
function autocompleteOnTyping(cm, [info], debounced) {
const lastLine = info.text[info.text.length - 1];
if (cm.state.completionActive ||
info.origin && !info.origin.includes('input') ||
!lastLine) {
return;
}
if (cm.state.autocompletePicked) {
cm.state.autocompletePicked = false;
return;
}
if (!debounced) {
debounce(autocompleteOnTyping, 100, cm, [info], true);
return;
}
if (lastLine.match(/[-a-z!]+$/i)) {
cm.state.autocompletePicked = false;
cm.options.hintOptions.completeSingle = false;
cm.execCommand('autocomplete');
setTimeout(() => {
cm.options.hintOptions.completeSingle = true;
});
}
}
function autocompletePicked(cm) {
cm.state.autocompletePicked = true;
}
})();

427
edit/base.js Normal file
View File

@ -0,0 +1,427 @@
/* 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 getOwnTab sessionStore tryJSONparse tryURL */// toolbox.js
'use strict';
/**
* @type Editor
* @namespace Editor
*/
const editor = {
style: null,
dirty: DirtyReporter(),
isUsercss: false,
isWindowed: false,
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 = `${
isDirty ? '* ' : ''
}${
customName || name || t('styleMissingName')
} - Stylus`; // the suffix enables external utilities to process our windows e.g. pin on top
},
};
//#region pre-init
(() => {
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);
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') ||
tryURL(params.get('url-prefix')).hostname ||
'',
enabled: true,
sections: [
MozDocMapper.toSection([...params], {code: ''}),
],
};
// switching the mode here to show the correct page ASAP, usually before DOMContentLoaded
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);
$.rootCL.add(isUC ? 'usercss' : 'sectioned');
sessionStore.justEditedStyleId = id || '';
// no such style so let's clear the invalid URL parameters
if (!id) history.replaceState({}, '', location.pathname);
}
})();
//#endregion
//#region init header
/* exported EditorHeader */
function EditorHeader() {
initBeautifyButton($('#beautify'));
initKeymapElement();
initNameArea();
initThemeElement();
setupLivePrefs();
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];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
function initNameArea() {
const nameEl = $('#name');
const resetEl = $('#reset-name');
const isCustomName = editor.style.updateUrl || editor.isUsercss;
editor.nameTarget = isCustomName ? 'customName' : 'name';
nameEl.placeholder = t(editor.isUsercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
nameEl.title = isCustomName ? t('customNameHint') : '';
nameEl.on('input', () => {
editor.updateName(true);
resetEl.hidden = !editor.style.customName;
});
resetEl.hidden = !editor.style.customName;
resetEl.onclick = () => {
editor.style.customName = null; // to delete it from db
setInputValue(nameEl, editor.style.name);
resetEl.hidden = true;
};
const enabledEl = $('#enabled');
enabledEl.onchange = () => editor.updateEnabledness(enabledEl.checked);
}
function initThemeElement() {
$('#editor.theme').append(...[
$create('option', {value: 'default'}, t('defaultTheme')),
...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'));
}
function initKeymapElement() {
// move 'pc' or 'mac' prefix to the end of the displayed label
const maps = Object.keys(CodeMirror.keyMap)
.map(name => ({
value: name,
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
}))
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
const fragment = document.createDocumentFragment();
let bin = fragment;
let groupName;
// group suffixed maps in <optgroup>
maps.forEach(({value, name}, i) => {
groupName = !name.includes('-') ? name : groupName;
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
if (groupWithNext) {
if (bin === fragment) {
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
}
}
const el = bin.appendChild($create('option', {value}, name));
if (value === prefs.defaults['editor.keyMap']) {
el.dataset.default = '';
el.title = t('defaultTheme');
}
if (!groupWithNext) bin = fragment;
});
const selector = $('#editor.keyMap');
selector.textContent = '';
selector.appendChild(fragment);
selector.value = prefs.get('editor.keyMap');
}
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) {
const extraKeys = CodeMirror.defaults.extraKeys;
for (const el of $$('[data-hotkey-tooltip]')) {
if (el._hotkeyTooltipKeyMap !== mapName) {
el._hotkeyTooltipKeyMap = mapName;
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
const cmd = el.dataset.hotkeyTooltip;
const key = cmd[0] === '=' ? cmd.slice(1) :
findKeyForCommand(cmd, mapName) ||
extraKeys && findKeyForCommand(cmd, extraKeys);
const newTitle = title + (title && key ? '\n' : '') + (key || '');
if (el.title !== newTitle) el.title = newTitle;
}
}
}
}
//#endregion
//#region init windowed mode
(() => {
let ownTabId;
if (chrome.windows) {
initWindowedMode();
const pos = tryJSONparse(sessionStore.windowPos);
delete sessionStore.windowPos;
// resize the window on 'undo close'
if (pos && pos.left != null) {
chrome.windows.update(chrome.windows.WINDOW_ID_CURRENT, pos);
}
}
getOwnTab().then(tab => {
ownTabId = tab.id;
if (sessionStore['manageStylesHistory' + ownTabId] === location.href) {
editor.cancel = () => history.back();
}
});
async function initWindowedMode() {
chrome.tabs.onAttached.addListener(onTabAttached);
// 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 &&
await prefs.ready && prefs.get('openEditInWindow') &&
(await browser.windows.getAll()).length > 1 &&
(await browser.tabs.query({currentWindow: true})).length === 1
);
}
async function onTabAttached(tabId, info) {
if (tabId !== ownTabId) {
return;
}
if (info.newPosition !== 0) {
prefs.set('openEditInWindow', false);
return;
}
const win = await browser.windows.get(info.newWindowId, {populate: true});
// If there's only one tab in this window, it's been dragged to new window
const openEditInWindow = win.tabs.length === 1;
// FF-only because Chrome retardedly resets the size during dragging
if (openEditInWindow && FIREFOX) {
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
}
prefs.set('openEditInWindow', openEditInWindow);
}
})();
//#endregion
//#region internals
/** @returns DirtyReporter */
function DirtyReporter() {
const data = new Map();
const listeners = new Set();
const dataListeners = new Set();
const notifyChange = wasDirty => {
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 */
return {
add(obj, value) {
const wasDirty = data.size > 0;
const saved = data.get(obj);
if (!saved) {
data.set(obj, {type: 'add', newValue: value});
} else if (saved.type === 'remove') {
if (saved.savedValue === value) {
data.delete(obj);
} else {
saved.newValue = value;
saved.type = 'modify';
}
} else {
return;
}
notifyChange(wasDirty);
},
clear(...objs) {
if (data.size && (
objs.length
? objs.map(data.delete, data).includes(true)
: (data.clear(), true)
)) {
notifyChange(true);
}
},
has(key) {
return data.has(key);
},
isDirty() {
return data.size > 0;
},
modify(obj, oldValue, newValue) {
const wasDirty = data.size > 0;
const saved = data.get(obj);
if (!saved) {
if (oldValue !== newValue) {
data.set(obj, {type: 'modify', savedValue: oldValue, newValue});
} else {
return;
}
} else if (saved.type === 'modify') {
if (saved.savedValue === newValue) {
data.delete(obj);
} else {
saved.newValue = newValue;
}
} 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);
if (!saved) {
data.set(obj, {type: 'remove', savedValue: value});
} else if (saved.type === 'add') {
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

@ -1,135 +1,174 @@
/* global loadScript css_beautify showHelp prefs t $ $create */ /* global $ $create moveFocus */// dom.js
/* exported beautify */ /* global CodeMirror */
/* global createHotkeyInput helpPopup */// util.js
/* global editor */
/* global prefs */
/* global t */// localization.js
'use strict'; 'use strict';
function beautify(scope) { CodeMirror.commands.beautify = cm => {
loadScript('/vendor-overwrites/beautify/beautify-css-mod.js') // using per-section mode when code editor or applies-to block is focused
.then(() => { const isPerSection = cm.display.wrapper.parentElement.contains(document.activeElement);
if (!window.css_beautify && window.exports) { beautify(isPerSection ? [cm] : editor.getEditors(), false);
window.css_beautify = window.exports.css_beautify; };
}
})
.then(doBeautify);
function doBeautify() { prefs.subscribe('editor.beautify.hotkey', (key, value) => {
const tabs = prefs.get('editor.indentWithTabs'); const {extraKeys} = CodeMirror.defaults;
const options = Object.assign({}, prefs.get('editor.beautify')); for (const [key, cmd] of Object.entries(extraKeys)) {
for (const k of Object.keys(prefs.defaults['editor.beautify'])) { if (cmd === 'beautify') {
if (!(k in options)) options[k] = prefs.defaults['editor.beautify'][k]; delete extraKeys[key];
break;
} }
options.indent_size = tabs ? 1 : prefs.get('editor.tabSize'); }
options.indent_char = tabs ? '\t' : ' '; if (value) {
extraKeys[value] = 'beautify';
}
}, {runNow: true});
showHelp(t('styleBeautify'), /**
$create([ * @name beautify
$create('.beautify-options', [ * @param {CodeMirror[]} scope
$createOption('.selector1,', 'selector_separator_newline'), * @param {boolean} [ui=true]
$createOption('.selector2', 'newline_before_open_brace'), */
$createOption('{', 'newline_after_open_brace'), async function beautify(scope, ui = true) {
$createOption('border: none;', 'newline_between_properties', true), await require(['/vendor-overwrites/beautify/beautify-css-mod']); /* global css_beautify */
$createOption('display: block;', 'newline_before_close_brace', true), const tabs = prefs.get('editor.indentWithTabs');
$createOption('}', 'newline_between_rules'), const options = Object.assign(prefs.defaults['editor.beautify'], prefs.get('editor.beautify'));
$createLabeledCheckbox('preserve_newlines', 'styleBeautifyPreserveNewlines'), options.indent_size = tabs ? 1 : prefs.get('editor.tabSize');
$createLabeledCheckbox('indent_conditional', 'styleBeautifyIndentConditional'), options.indent_char = tabs ? '\t' : ' ';
]), if (ui) {
$create('.buttons', [ createBeautifyUI(scope, options);
$create('button', { }
attributes: {role: 'close'}, for (const cm of scope) {
// showHelp.close will be defined after showHelp() is invoked setTimeout(beautifyEditor, 0, cm, options, ui);
onclick: () => showHelp.close(), }
}, t('confirmClose')), }
$create('button', {
attributes: {role: 'undo'},
onclick() {
let undoable = false;
for (const cm of scope) {
const data = cm.beautifyChange;
if (!data || !data[cm.changeGeneration()]) continue;
delete data[cm.changeGeneration()];
const {scrollX, scrollY} = window;
cm.undo();
cm.scrollIntoView(cm.getCursor());
window.scrollTo(scrollX, scrollY);
undoable |= data[cm.changeGeneration()];
}
this.disabled = !undoable;
},
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
]),
]));
$('#help-popup').className = 'wide'; function beautifyEditor(cm, options, ui) {
const pos = options.translate_positions =
scope.forEach(cm => { [].concat.apply([], cm.doc.sel.ranges.map(r =>
setTimeout(() => { [Object.assign({}, r.anchor), Object.assign({}, r.head)]));
const pos = options.translate_positions = const text = cm.getValue();
[].concat.apply([], cm.doc.sel.ranges.map(r => const newText = css_beautify(text, options);
[Object.assign({}, r.anchor), Object.assign({}, r.head)])); if (newText !== text) {
const text = cm.getValue(); if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
const newText = css_beautify(text, options); // clear the list if last change wasn't a css-beautify
if (newText !== text) { cm.beautifyChange = {};
if (!cm.beautifyChange || !cm.beautifyChange[cm.changeGeneration()]) {
// clear the list if last change wasn't a css-beautify
cm.beautifyChange = {};
}
cm.setValue(newText);
const selections = [];
for (let i = 0; i < pos.length; i += 2) {
selections.push({anchor: pos[i], head: pos[i + 1]});
}
const {scrollX, scrollY} = window;
cm.setSelections(selections);
window.scrollTo(scrollX, scrollY);
cm.beautifyChange[cm.changeGeneration()] = true;
$('#help-popup button[role="close"]').disabled = false;
}
});
});
$('.beautify-options').onchange = ({target}) => {
const value = target.type === 'checkbox' ? target.checked : target.selectedIndex > 0;
prefs.set('editor.beautify', Object.assign(options, {[target.dataset.option]: value}));
if (target.parentNode.hasAttribute('newline')) {
target.parentNode.setAttribute('newline', value.toString());
}
doBeautify();
};
function $createOption(label, optionName, indent) {
const value = options[optionName];
return (
$create('div', {attributes: {newline: value}}, [
$create('span', indent ? {attributes: {indent: ''}} : {}, label),
$create('div.select-resizer', [
$create('select', {dataset: {option: optionName}}, [
$create('option', {selected: !value}, '\xA0'),
$create('option', {selected: value}, '\\n'),
]),
$create('SVG:svg.svg-icon.select-arrow', {viewBox: '0 0 1792 1792'}, [
$create('SVG:path', {
'fill-rule': 'evenodd',
'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z'
}),
]),
]),
])
);
} }
cm.setValue(newText);
function $createLabeledCheckbox(optionName, i18nKey) { const selections = [];
return ( for (let i = 0; i < pos.length; i += 2) {
$create('label', {style: 'display: block; clear: both;'}, [ selections.push({anchor: pos[i], head: pos[i + 1]});
$create('input', { }
type: 'checkbox', const {scrollX, scrollY} = window;
dataset: {option: optionName}, cm.setSelections(selections);
checked: options[optionName] !== false window.scrollTo(scrollX, scrollY);
}), cm.beautifyChange[cm.changeGeneration()] = true;
$create('SVG:svg.svg-icon.checked', if (ui) {
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})), $('button[role="close"]', helpPopup.div).disabled = false;
t(i18nKey),
])
);
} }
} }
} }
function createBeautifyUI(scope, options) {
helpPopup.show(t('styleBeautify'),
$create([
$create('.beautify-options', [
$createOption('.selector1,', 'selector_separator_newline'),
$createOption('.selector2', 'newline_before_open_brace'),
$createOption('{', 'newline_after_open_brace'),
$createOption('border: none;', 'newline_between_properties', true),
$createOption('display: block;', 'newline_before_close_brace', true),
$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', {
buttons: false,
onDone: () => moveFocus(helpPopup.div, 0),
}),
]),
$create('.buttons', [
$create('button', {
attributes: {role: 'close'},
onclick: helpPopup.close,
}, t('confirmClose')),
$create('button', {
attributes: {role: 'undo'},
onclick() {
let undoable = false;
for (const cm of scope) {
const data = cm.beautifyChange;
if (!data || !data[cm.changeGeneration()]) continue;
delete data[cm.changeGeneration()];
const {scrollX, scrollY} = window;
cm.undo();
cm.scrollIntoView(cm.getCursor());
window.scrollTo(scrollX, scrollY);
undoable |= data[cm.changeGeneration()];
}
this.disabled = !undoable;
},
}, t(scope.length === 1 ? 'undo' : 'undoGlobal')),
]),
]),
{
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}));
beautify(scope, false);
};
function $createOption(label, optionName, indent) {
const value = options[optionName];
return (
$create('div', {attributes: {newline: value}}, [
$create('span', indent ? {attributes: {indent: ''}} : {}, label),
$create('div.select-resizer', [
$create('select', {dataset: {option: optionName}}, [
$create('option', {selected: !value}, '\xA0'),
$create('option', {selected: value}, '\\n'),
]),
$create('SVG:svg.svg-icon.select-arrow', {viewBox: '0 0 1792 1792'}, [
$create('SVG:path', {
'fill-rule': 'evenodd',
'd': 'M1408 704q0 26-19 45l-448 448q-19 19-45 ' +
'19t-45-19l-448-448q-19-19-19-45t19-45 45-19h896q26 0 45 19t19 45z',
}),
]),
]),
])
);
}
function $createLabeledCheckbox(optionName, i18nKey, text) {
return (
$create('label', {style: 'display: block; clear: both;'}, [
$create('input', {
type: 'checkbox',
dataset: {option: optionName},
checked: options[optionName] !== false,
}),
$create('SVG:svg.svg-icon.checked',
$create('SVG:use', {'xlink:href': '#svg-icon-checked'})),
i18nKey ? t(i18nKey) : text,
])
);
}
}
/* exported initBeautifyButton */
function initBeautifyButton(btn, scope) {
btn.onclick = btn.oncontextmenu = e => {
e.preventDefault();
beautify(scope || editor.getEditors(), e.type === 'click');
};
}

View File

@ -1,29 +1,33 @@
/* Built-in CodeMirror and addon customization */
.CodeMirror-hints { .CodeMirror-hints {
z-index: 999; z-index: 999;
} }
.CodeMirror-hint:hover { .CodeMirror-hint:hover {
color: white; color: var(--bg);
background: #08f; background: #08f;
} }
.CodeMirror { .CodeMirror {
border: solid #CCC 1px; border: solid var(--c80) 1px;
transition: box-shadow .1s;
} }
.CodeMirror-lint-mark-warning { .CodeMirror {
background: none; 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 */
outline: none !important;
} }
.CodeMirror-dialog { .CodeMirror-dialog {
-webkit-animation: highlight 3s cubic-bezier(.18, .02, 0, .94); animation: highlight 3s cubic-bezier(.18, .02, 0, .94);
}
.CodeMirror-focused {
outline: -webkit-focus-ring-color auto 5px;
outline-offset: -2px;
}
@supports (-moz-appearance:none) {
/* restrict to FF */
.CodeMirror-focused {
outline: #7dadd9 auto 1px;
outline-offset: -1px;
}
} }
.CodeMirror-search-field { .CodeMirror-search-field {
width: 10em; width: 10em;
@ -32,12 +36,8 @@
width: 5em; width: 5em;
} }
.CodeMirror-search-hint { .CodeMirror-search-hint {
color: #888; color: var(--c50);
} }
.cm-uso-variable {
font-weight: bold;
}
.CodeMirror-activeline .applies-to:before { .CodeMirror-activeline .applies-to:before {
background-color: hsla(214, 100%, 90%, 0.15); background-color: hsla(214, 100%, 90%, 0.15);
content: ""; content: "";
@ -48,11 +48,9 @@
position: absolute; position: absolute;
pointer-events: none; pointer-events: none;
} }
.CodeMirror-activeline .applies-to ul { .CodeMirror-activeline .applies-to ul {
z-index: 2; z-index: 2;
} }
.CodeMirror-foldgutter-open::after, .CodeMirror-foldgutter-open::after,
.CodeMirror-foldgutter-folded::after { .CodeMirror-foldgutter-folded::after {
top: 5px; top: 5px;
@ -64,15 +62,87 @@
opacity: .5; opacity: .5;
left: 1px; left: 1px;
} }
.CodeMirror-foldgutter-open::after { .CodeMirror-foldgutter-open::after {
border-width: 5px 3px 0 3px; border-width: 5px 3px 0 3px;
border-color: currentColor transparent transparent transparent; border-color: currentColor transparent transparent transparent;
} }
.CodeMirror-foldgutter-folded::after { .CodeMirror-foldgutter-folded::after {
margin-top: -2px; margin-top: -2px;
margin-left: 1px; margin-left: 1px;
border-width: 4px 0 4px 5px; border-width: 4px 0 4px 5px;
border-color: transparent transparent transparent currentColor; border-color: transparent transparent transparent currentColor;
} }
.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 */
.cm-uso-variable {
font-weight: bold;
}
.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,8 +1,12 @@
/* global CodeMirror prefs loadScript editor $ template */ /* global $ */// dom.js
/* global CodeMirror */
/* global UA */// toolbox.js
/* global editor */
/* global prefs */
/* global t */// localization.js
'use strict'; 'use strict';
(function () { (() => {
// CodeMirror miserably fails on keyMap='' so let's ensure it's not // CodeMirror miserably fails on keyMap='' so let's ensure it's not
if (!prefs.get('editor.keyMap')) { if (!prefs.get('editor.keyMap')) {
prefs.reset('editor.keyMap'); prefs.reset('editor.keyMap');
@ -22,11 +26,11 @@
matchBrackets: true, matchBrackets: true,
hintOptions: {}, hintOptions: {},
lintReportDelay: prefs.get('editor.lintReportDelay'), lintReportDelay: prefs.get('editor.lintReportDelay'),
styleActiveLine: true, styleActiveLine: {nonEmpty: true},
theme: prefs.get('editor.theme'), theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap'), keyMap: prefs.get('editor.keyMap'),
extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, { extraKeys: Object.assign(CodeMirror.defaults.extraKeys || {}, {
// independent of current keyMap // independent of current keyMap; some are implemented only for the edit page
'Alt-Enter': 'toggleStyle', 'Alt-Enter': 'toggleStyle',
'Alt-PageDown': 'nextEditor', 'Alt-PageDown': 'nextEditor',
'Alt-PageUp': 'prevEditor', 'Alt-PageUp': 'prevEditor',
@ -37,387 +41,108 @@
Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options')); Object.assign(CodeMirror.defaults, defaults, prefs.get('editor.options'));
// 'basic' keymap only has basic keys by design, so we skip it // Adding hotkeys to some keymaps except 'basic' which is primitive by design
{
const extraKeysCommands = {}; const KM = CodeMirror.keyMap;
Object.keys(CodeMirror.defaults.extraKeys).forEach(key => { const extras = Object.values(CodeMirror.defaults.extraKeys);
extraKeysCommands[CodeMirror.defaults.extraKeys[key]] = true; if (!extras.includes('jumpToLine')) {
}); KM.sublime['Ctrl-G'] = 'jumpToLine';
if (!extraKeysCommands.jumpToLine) { KM.emacsy['Ctrl-G'] = 'jumpToLine';
CodeMirror.keyMap.sublime['Ctrl-G'] = 'jumpToLine'; KM.pcDefault['Ctrl-J'] = 'jumpToLine';
CodeMirror.keyMap.emacsy['Ctrl-G'] = 'jumpToLine'; KM.macDefault['Cmd-J'] = 'jumpToLine';
CodeMirror.keyMap.pcDefault['Ctrl-J'] = 'jumpToLine';
CodeMirror.keyMap.macDefault['Cmd-J'] = 'jumpToLine';
}
if (!extraKeysCommands.autocomplete) {
// will be used by 'sublime' on PC via fallthrough
CodeMirror.keyMap.pcDefault['Ctrl-Space'] = 'autocomplete';
// OSX uses Ctrl-Space and Cmd-Space for something else
CodeMirror.keyMap.macDefault['Alt-Space'] = 'autocomplete';
// copied from 'emacs' keymap
CodeMirror.keyMap.emacsy['Alt-/'] = 'autocomplete';
// 'vim' and 'emacs' define their own autocomplete hotkeys
}
if (!extraKeysCommands.blockComment) {
CodeMirror.keyMap.sublime['Shift-Ctrl-/'] = 'commentSelection';
}
if (navigator.appVersion.includes('Windows')) {
// 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
if (!extraKeysCommands.findNext) {
CodeMirror.keyMap.pcDefault['F3'] = 'findNext';
} }
if (!extraKeysCommands.findPrev) { if (!extras.includes('autocomplete')) {
CodeMirror.keyMap.pcDefault['Shift-F3'] = 'findPrev'; // will be used by 'sublime' on PC via fallthrough
KM.pcDefault['Ctrl-Space'] = 'autocomplete';
// OSX uses Ctrl-Space and Cmd-Space for something else
KM.macDefault['Alt-Space'] = 'autocomplete';
// copied from 'emacs' keymap
KM.emacsy['Alt-/'] = 'autocomplete';
// 'vim' and 'emacs' define their own autocomplete hotkeys
} }
if (!extraKeysCommands.replace) { if (!extras.includes('blockComment')) {
CodeMirror.keyMap.pcDefault['Ctrl-R'] = 'replace'; KM.sublime['Shift-Ctrl-/'] = 'commentSelection';
} }
if (UA.windows) {
// try to remap non-interceptable Ctrl-(Shift-)N/T/W hotkeys // 'pcDefault' keymap on Windows should have F3/Shift-F3/Ctrl-R
['N', 'T', 'W'].forEach(char => { if (!extras.includes('findNext')) KM.pcDefault['F3'] = 'findNext';
[ if (!extras.includes('findPrev')) KM.pcDefault['Shift-F3'] = 'findPrev';
{from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']}, if (!extras.includes('replace')) KM.pcDefault['Ctrl-R'] = 'replace';
// Note: modifier order in CodeMirror is S-C-A // try to remap non-interceptable (Shift-)Ctrl-N/T/W hotkeys
{from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']} // Note: modifier order in CodeMirror is S-C-A
].forEach(remap => { for (const char of ['N', 'T', 'W']) {
const oldKey = remap.from + char; for (const remap of [
Object.keys(CodeMirror.keyMap).forEach(keyMapName => { {from: 'Ctrl-', to: ['Alt-', 'Ctrl-Alt-']},
const keyMap = CodeMirror.keyMap[keyMapName]; {from: 'Shift-Ctrl-', to: ['Ctrl-Alt-', 'Shift-Ctrl-Alt-']},
const command = keyMap[oldKey]; ]) {
if (!command) { const oldKey = remap.from + char;
return; for (const km of Object.values(KM)) {
} const command = km[oldKey];
remap.to.some(newMod => { if (!command) continue;
const newKey = newMod + char; for (const newMod of remap.to) {
if (!(newKey in keyMap)) { const newKey = newMod + char;
delete keyMap[oldKey]; if (newKey in km) continue;
keyMap[newKey] = command; km[newKey] = command;
return true; delete km[oldKey];
break;
} }
}); }
}); }
}); }
}); }
} }
Object.assign(CodeMirror.mimeModes['text/css'].propertyKeywords, { Object.assign(CodeMirror.prototype, {
// CSS Backgrounds and Borders Module L4 /**
'background-position-x': true, * @param {'less' | 'stylus' | ?} [pp] - any value besides `less` or `stylus` sets `css` mode
'background-position-y': true, * @param {boolean} [force]
*/
// CSS Logical Properties and Values L1 setPreprocessor(pp, force) {
'block-size': true, const name = pp === 'less' ? 'text/x-less' : pp === 'stylus' ? pp : 'css';
'border-block-color': true, const m = this.doc.mode;
'border-block-end': true, if (force || (m.helperType ? m.helperType !== pp : m.name !== name)) {
'border-block-end-color': true, this.setOption('mode', name);
'border-block-end-style': true, this.doc.mode.lineComment = ''; // stylelint chokes on line comments a lot
'border-block-end-width': true, }
'border-block-start': true, },
'border-block-start-color': true, /** Superfast GC-friendly check that runs until the first non-space line */
'border-block-start-style': true, isBlank() {
'border-block-start-width': true, let filled;
'border-block-style': true, this.eachLine(({text}) => (filled = text && /\S/.test(text)));
'border-block-width': true, return !filled;
'border-inline-color': true, },
'border-inline-end': true, /**
'border-inline-end-color': true, * Sets cursor and centers it in view if `pos` was out of view
'border-inline-end-style': true, * @param {CodeMirror.Pos} pos
'border-inline-end-width': true, * @param {CodeMirror.Pos} [end] - will set a selection from `pos` to `end`
'border-inline-start': true, */
'border-inline-start-color': true, jumpToPos(pos, end = pos) {
'border-inline-start-style': true, const {curOp} = this;
'border-inline-start-width': true, if (!curOp) this.startOperation();
'border-inline-style': true, const y = this.cursorCoords(pos, 'window').top;
'border-inline-width': true, const rect = this.display.wrapper.getBoundingClientRect();
'inline-size': true, // case 1) outside of CM viewport or too close to edge so tell CM to render a new viewport
'inset': true, if (y < rect.top + 50 || y > rect.bottom - 100) {
'inset-block': true, this.scrollIntoView(pos, rect.height / 2);
'inset-block-end': true, // case 2) inside CM viewport but outside of window viewport so just scroll the window
'inset-block-start': true, } else if (y < 0 || y > innerHeight) {
'inset-inline': true, editor.scrollToEditor(this);
'inset-inline-end': true, }
'inset-inline-start': true, // Using prototype since our bookmark patch sets cm.setSelection to jumpToPos
'margin-block': true, CodeMirror.prototype.setSelection.call(this, pos, end);
'margin-block-end': true, if (!curOp) this.endOperation();
'margin-block-start': true,
'margin-inline': true,
'margin-inline-end': true,
'margin-inline-start': true,
'max-block-size': true,
'max-inline-size': true,
'min-block-size': true,
'min-inline-size': true,
'padding-block': true,
'padding-block-end': true,
'padding-block-start': true,
'padding-inline': true,
'padding-inline-end': true,
'padding-inline-start': true,
'text-align-all': true,
'contain': true,
'mask-image': true,
'mix-blend-mode': true,
'rotate': true,
'isolation': true,
'zoom': true,
// https://www.w3.org/TR/css-round-display-1/
'border-boundary': true,
'shape': true,
'shape-inside': true,
'viewport-fit': true,
// nonstandard https://compat.spec.whatwg.org/
'box-reflect': true,
'text-fill-color': true,
'text-stroke': true,
'text-stroke-color': true,
'text-stroke-width': true,
// end
});
Object.assign(CodeMirror.mimeModes['text/css'].valueKeywords, {
'isolate': true,
'rect': true,
'recto': true,
'verso': true,
});
Object.assign(CodeMirror.mimeModes['text/css'].colorKeywords, {
'darkgrey': true,
'darkslategrey': true,
'dimgrey': true,
'grey': true,
'lightgrey': true,
'lightslategrey': true,
'slategrey': true,
});
const MODE = {
less: {
family: 'css',
value: 'text/x-less',
isActive: cm =>
cm.doc.mode &&
cm.doc.mode.name === 'css' &&
cm.doc.mode.helperType === 'less',
}, },
stylus: 'stylus',
uso: 'css'
};
CodeMirror.defineExtension('setPreprocessor', function (preprocessor, force = false) {
const mode = MODE[preprocessor] || 'css';
const isActive = mode.isActive || (
cm => cm.doc.mode === mode ||
cm.doc.mode && (cm.doc.mode.name + (cm.doc.mode.helperType || '') === mode)
);
if (!force && isActive(this)) {
return Promise.resolve();
}
if ((mode.family || mode) === 'css') {
// css.js is always loaded via html
this.setOption('mode', mode.value || mode);
return Promise.resolve();
}
return loadScript(`/vendor/codemirror/mode/${mode}/${mode}.js`).then(() => {
this.setOption('mode', mode);
});
}); });
CodeMirror.defineExtension('isBlank', function () {
// superfast checking as it runs only until the first non-blank line
let isBlank = true;
this.doc.eachLine(line => {
if (line.text && line.text.trim()) {
isBlank = false;
return true;
}
});
return isBlank;
});
// editor commands
for (const name of ['save', 'toggleStyle', 'nextEditor', 'prevEditor']) {
CodeMirror.commands[name] = (...args) => editor[name](...args);
}
// speedup: reuse the old folding marks
// TODO: remove when https://github.com/codemirror/CodeMirror/pull/6010 is shipped in /vendor
const {setGutterMarker} = CodeMirror.prototype;
CodeMirror.prototype.setGutterMarker = function (line, gutterID, value) {
const o = this.state.foldGutter.options;
if (typeof o.indicatorOpen === 'string' ||
typeof o.indicatorFolded === 'string') {
const old = line.gutterMarkers && line.gutterMarkers[gutterID];
// old className can contain other names set by CodeMirror so we'll use classList
if (old && value && old.classList.contains(value.className) ||
!old && !value) {
return line;
}
}
return setGutterMarker.apply(this, arguments);
};
// CodeMirror convenience commands
Object.assign(CodeMirror.commands, { Object.assign(CodeMirror.commands, {
toggleEditorFocus, jumpToLine(cm) {
jumpToLine, const cur = cm.getCursor();
commentSelection, const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
if (oldDialog) cm.focus(); // close the currently opened minidialog
cm.openDialog(t.template.jumpToLine.cloneNode(true), str => {
const [line, ch] = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$|$/);
if (line) cm.setCursor(line - 1, ch ? ch - 1 : cur.ch);
}, {value: cur.line + 1});
},
}); });
function jumpToLine(cm) {
const cur = cm.getCursor();
const oldDialog = $('.CodeMirror-dialog', cm.display.wrapper);
if (oldDialog) {
// close the currently opened minidialog
cm.focus();
}
// make sure to focus the input in newly opened minidialog
// setTimeout(() => {
// $('.CodeMirror-dialog', section).focus();
// });
cm.openDialog(template.jumpToLine.cloneNode(true), str => {
const m = str.match(/^\s*(\d+)(?:\s*:\s*(\d+))?\s*$/);
if (m) {
cm.setCursor(m[1] - 1, m[2] ? m[2] - 1 : cur.ch);
}
}, {value: cur.line + 1});
}
function commentSelection(cm) {
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
}
function toggleEditorFocus(cm) {
if (!cm) return;
if (cm.hasFocus()) {
setTimeout(() => cm.display.input.blur());
} else {
cm.focus();
}
}
})();
// eslint-disable-next-line no-unused-expressions
CodeMirror.hint && (() => {
const USO_VAR = 'uso-variable';
const USO_VALID_VAR = 'variable-3 ' + USO_VAR;
const USO_INVALID_VAR = 'error ' + USO_VAR;
const RX_IMPORTANT = /(i(m(p(o(r(t(a(nt?)?)?)?)?)?)?)?)?(?=\b|\W|$)/iy;
const RX_VAR_KEYWORD = /(^|[^-\w\u0080-\uFFFF])var\(/iy;
const RX_END_OF_VAR = /[\s,)]|$/g;
const originalHelper = CodeMirror.hint.css || (() => {});
const helper = cm => {
const pos = cm.getCursor();
const {line, ch} = pos;
const {styles, text} = cm.getLineHandle(line);
if (!styles) return originalHelper(cm);
const {style, index} = cm.getStyleAtPos({styles, pos: ch}) || {};
if (style && (style.startsWith('comment') || style.startsWith('string'))) {
return originalHelper(cm);
}
// !important
if (text[ch - 1] === '!' && /i|\W|^$/i.test(text[ch] || '')) {
RX_IMPORTANT.lastIndex = ch;
return {
list: ['important'],
from: pos,
to: {line, ch: ch + RX_IMPORTANT.exec(text)[0].length},
};
}
let prev = index > 2 ? styles[index - 2] : 0;
let end = styles[index];
// #hex colors
if (text[prev] === '#') {
return {list: [], from: pos, to: pos};
}
// adjust cursor position for /*[[ and ]]*/
const adjust = text[prev] === '/' ? 4 : 0;
prev += adjust;
end -= adjust;
const leftPart = text.slice(prev, ch);
// --css-variables
const startsWithDoubleDash = text[prev] === '-' && text[prev + 1] === '-';
if (startsWithDoubleDash ||
leftPart === '(' && testAt(RX_VAR_KEYWORD, Math.max(0, prev - 4), text)) {
// simplified regex without CSS escapes
const RX_CSS_VAR = new RegExp(
'(?:^|[\\s/;{])(' +
(leftPart.startsWith('--') ? leftPart : '--') +
(leftPart.length <= 2 ? '[a-zA-Z_\u0080-\uFFFF]' : '') +
'[-0-9a-zA-Z_\u0080-\uFFFF]*)',
'gm');
const cursor = cm.getSearchCursor(RX_CSS_VAR, null, {caseFold: false, multiline: false});
const list = new Set();
while (cursor.findNext()) {
list.add(cursor.pos.match[1]);
}
if (!startsWithDoubleDash) {
prev++;
}
RX_END_OF_VAR.lastIndex = prev;
end = RX_END_OF_VAR.exec(text).index;
return {
list: [...list.keys()].sort(),
from: {line, ch: prev},
to: {line, ch: end},
};
}
if (!editor || !style || !style.includes(USO_VAR)) {
return originalHelper(cm);
}
// USO vars in usercss mode editor
const vars = editor.getStyle().usercssData.vars;
const list = vars ?
Object.keys(vars).filter(name => name.startsWith(leftPart)) : [];
return {
list,
from: {line, ch: prev},
to: {line, ch: end},
};
};
CodeMirror.registerHelper('hint', 'css', helper);
CodeMirror.registerHelper('hint', 'stylus', helper);
const hooks = CodeMirror.mimeModes['text/css'].tokenHooks;
const originalCommentHook = hooks['/'];
hooks['/'] = tokenizeUsoVariables;
function tokenizeUsoVariables(stream) {
const token = originalCommentHook.apply(this, arguments);
if (token[1] !== 'comment') {
return token;
}
const {string, start, pos} = stream;
// /*[[install-key]]*/
// 01234 43210
if (string[start + 2] === '[' &&
string[start + 3] === '[' &&
string[pos - 3] === ']' &&
string[pos - 4] === ']') {
const vars = typeof editor !== 'undefined' && (editor.getStyle().usercssData || {}).vars;
const name = vars && string.slice(start + 4, pos - 4);
if (vars && Object.hasOwnProperty.call(vars, name.endsWith('-rgb') ? name.slice(0, -4) : name)) {
token[0] = USO_VALID_VAR;
} else {
token[0] = USO_INVALID_VAR;
}
}
return token;
}
function testAt(rx, index, text) {
if (!rx) return false;
rx.lastIndex = index;
return rx.test(text);
}
})(); })();

View File

@ -1,86 +1,181 @@
/* global CodeMirror loadScript rerouteHotkeys prefs $ debounce $create */ /* global CodeMirror */
/* exported cmFactory */ /* global editor */
/* global prefs */
/* global rerouteHotkeys */// util.js
'use strict'; 'use strict';
/* /*
All cm instances created by this module are collected so we can broadcast prefs All cm instances created by this module are collected so we can broadcast prefs
settings to them. You should `cmFactory.destroy(cm)` to unregister the listener settings to them. You should `cmFactory.destroy(cm)` to unregister the listener
when the instance is not used anymore. when the instance is not used anymore.
*/ */
const cmFactory = (() => {
const editors = new Set();
// used by `indentWithTabs` option
const INSERT_TAB_COMMAND = CodeMirror.commands.insertTab;
const INSERT_SOFT_TAB_COMMAND = CodeMirror.commands.insertSoftTab;
CodeMirror.defineOption('tabSize', prefs.get('editor.tabSize'), (cm, value) => { (() => {
cm.setOption('indentUnit', Number(value)); //#region Factory
});
CodeMirror.defineOption('indentWithTabs', prefs.get('editor.indentWithTabs'), (cm, value) => { const cms = new Set();
CodeMirror.commands.insertTab = value ? let lazyOpt;
INSERT_TAB_COMMAND :
INSERT_SOFT_TAB_COMMAND;
});
CodeMirror.defineOption('autocompleteOnTyping', prefs.get('editor.autocompleteOnTyping'), (cm, value) => { const cmFactory = window.cmFactory = {
const onOff = value ? 'on' : 'off';
cm[onOff]('changes', autocompleteOnTyping);
cm[onOff]('pick', autocompletePicked);
});
CodeMirror.defineOption('matchHighlight', prefs.get('editor.matchHighlight'), (cm, value) => { create(place, options) {
if (value === 'token') { const cm = CodeMirror(place, options);
cm.setOption('highlightSelectionMatches', { cm.lastActive = 0;
showToken: /[#.\-\w]/, cms.add(cm);
annotateScrollbar: true, return cm;
onUpdate: updateMatchHighlightCount },
});
} else if (value === 'selection') {
cm.setOption('highlightSelectionMatches', {
showToken: false,
annotateScrollbar: true,
onUpdate: updateMatchHighlightCount
});
} else {
cm.setOption('highlightSelectionMatches', null);
}
});
CodeMirror.defineOption('selectByTokens', prefs.get('editor.selectByTokens'), (cm, value) => { destroy(cm) {
cm.setOption('configureMouse', value ? configureMouseFn : null); cms.delete(cm);
}); },
prefs.subscribe(null, (key, value) => { globalSetOption(key, value) {
const option = key.replace(/^editor\./, ''); CodeMirror.defaults[key] = value;
if (!option) { if (cms.size > 4 && lazyOpt.names.includes(key)) {
console.error('no "cm_option"', key); lazyOpt.set(key, value);
return;
}
// FIXME: this is implemented in `colorpicker-helper.js`.
if (option === 'colorpicker') {
return;
}
if (option === 'theme') {
const themeLink = $('#cm-theme');
// use non-localized 'default' internally
if (value === 'default') {
themeLink.href = '';
} else { } else {
const url = chrome.runtime.getURL('vendor/codemirror/theme/' + value + '.css'); cms.forEach(cm => cm.setOption(key, value));
if (themeLink.href !== url) { }
// avoid flicker: wait for the second stylesheet to load, then apply the theme },
return loadScript(url, true).then(([newThemeLink]) => { };
setOption(option, value);
themeLink.remove(); // focus and blur
newThemeLink.id = 'cm-theme';
}); const onCmFocus = cm => {
rerouteHotkeys.toggle(false);
cm.display.wrapper.classList.add('CodeMirror-active');
cm.lastActive = Date.now();
};
const onCmBlur = cm => {
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));
});
};
CodeMirror.defineInitHook(cm => {
cm.on('focus', onCmFocus);
cm.on('blur', onCmBlur);
});
// propagated preferences
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;
for (const cm of cms) {
let opts = queue.get(cm);
if (!opts) queue.set(cm, opts = {});
opts[key] = value;
observer.observe(cm.display.wrapper);
}
},
setNow({cm, data}) {
cm.operation(() => data.forEach(kv => cm.setOption(...kv)));
},
onView(entries) {
const {queue, observer} = lazyOpt;
const delayed = [];
for (const e of entries) {
const r = e.isIntersecting && e.intersectionRect;
if (!r) continue;
const cm = e.target.CodeMirror;
const data = Object.entries(queue.get(cm) || {});
queue.delete(cm);
observer.unobserve(e.target);
if (!data.every(([key, val]) => cm.getOption(key) === val)) {
if (r.bottom > 0 && r.top < window.innerHeight) {
lazyOpt.setNow({cm, data});
} else {
delayed.push({cm, data});
}
} }
} }
} if (delayed.length) {
// broadcast option setTimeout(() => delayed.forEach(lazyOpt.setNow));
setOption(option, value); }
},
get observer() {
if (!lazyOpt._observer) {
// must exceed refreshOnView's 100%
lazyOpt._observer = new IntersectionObserver(lazyOpt.onView, {rootMargin: '150%'});
lazyOpt.queue = new WeakMap();
}
return lazyOpt._observer;
},
};
//#endregion
//#region Commands
Object.assign(CodeMirror.commands, {
commentSelection(cm) {
cm.blockComment(cm.getCursor('from'), cm.getCursor('to'), {fullLines: false});
},
toggleEditorFocus(cm) {
if (!cm) return;
if (cm.hasFocus()) {
setTimeout(() => cm.display.input.blur());
} else {
cm.focus();
}
},
}); });
return {create, destroy, setOption}; for (const cmd of [
'nextEditor',
'prevEditor',
'save',
'toggleStyle',
]) {
CodeMirror.commands[cmd] = (...args) => editor[cmd](...args);
}
//#endregion
//#region CM option handlers
function updateMatchHighlightCount(cm, state) { function updateMatchHighlightCount(cm, state) {
cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length; cm.display.wrapper.dataset.matchHighlightCount = state.matchesonscroll.matches.length;
@ -143,151 +238,76 @@ const cmFactory = (() => {
}; };
} }
function autocompleteOnTyping(cm, [info], debounced) { //#endregion
const lastLine = info.text[info.text.length - 1]; //#region Bookmarks
if (
cm.state.completionActive || const BM_CLS = 'gutter-bookmark';
info.origin && !info.origin.includes('input') || const BM_BRAND = 'sublimeBookmark';
!lastLine const BM_CLICKER = 'CodeMirror-linenumbers';
) { const BM_DATA = Symbol('data');
return; // TODO: revisit when https://github.com/codemirror/CodeMirror/issues/6716 is fixed
} const tmProto = CodeMirror.TextMarker.prototype;
if (cm.state.autocompletePicked) { const tmProtoOvr = {};
cm.state.autocompletePicked = false; for (const k of ['clear', 'attachLine', 'detachLine']) {
return; tmProtoOvr[k] = function (line) {
} const {cm} = this.doc;
if (!debounced) { const withOp = !cm.curOp;
debounce(autocompleteOnTyping, 100, cm, [info], true); if (withOp) cm.startOperation();
return; tmProto[k].apply(this, arguments);
} cm.curOp.ownsGroup.delayedCallbacks.push(toggleMark.bind(this, this.lines[0], line));
if (lastLine.match(/[-a-z!]+$/i)) { if (withOp) cm.endOperation();
cm.state.autocompletePicked = false; };
cm.options.hintOptions.completeSingle = false;
cm.execCommand('autocomplete');
setTimeout(() => {
cm.options.hintOptions.completeSingle = true;
});
}
} }
for (const name of ['prevBookmark', 'nextBookmark']) {
function autocompletePicked(cm) { const cmdFn = CodeMirror.commands[name];
cm.state.autocompletePicked = true; CodeMirror.commands[name] = cm => {
cm.setSelection = cm.jumpToPos;
cmdFn(cm);
delete cm.setSelection;
};
} }
CodeMirror.defineInitHook(cm => {
function destroy(cm) { cm.on('gutterClick', onGutterClick);
editors.delete(cm); cm.on('gutterContextMenu', onGutterContextMenu);
} cm.on('markerAdded', onMarkAdded);
});
function create(init, options) { // TODO: reimplement bookmarking so next/prev order is decided solely by the line numbers
const cm = CodeMirror(init, options); function onGutterClick(cm, line, name, e) {
cm.lastActive = 0; switch (name === BM_CLICKER && e.button) {
const wrapper = cm.display.wrapper; case 0: {
cm.on('blur', () => { // main button: toggle
rerouteHotkeys(true); const [mark] = cm.findMarks({line, ch: 0}, {line, ch: 1e9}, m => m[BM_BRAND]);
setTimeout(() => { cm.setCursor(mark ? mark.find(-1) : {line, ch: 0});
wrapper.classList.toggle('CodeMirror-active', wrapper.contains(document.activeElement)); cm.execCommand('toggleBookmark');
});
});
cm.on('focus', () => {
rerouteHotkeys(false);
wrapper.classList.add('CodeMirror-active');
cm.lastActive = Date.now();
});
editors.add(cm);
return cm;
}
function getLastActivated() {
let result;
for (const cm of editors) {
if (!result || result.lastActive < cm.lastActive) {
result = cm;
}
}
return result;
}
function setOption(key, value) {
CodeMirror.defaults[key] = value;
if (editors.size > 4 && (key === 'theme' || key === 'lineWrapping')) {
throttleSetOption({key, value, index: 0});
return;
}
for (const cm of editors) {
cm.setOption(key, value);
}
}
function throttleSetOption({
key,
value,
index,
timeStart = performance.now(),
editorsCopy = [...editors],
cmStart = getLastActivated(),
progress,
}) {
if (index === 0) {
if (!cmStart) {
return;
}
cmStart.setOption(key, value);
}
const THROTTLE_AFTER_MS = 100;
const THROTTLE_SHOW_PROGRESS_AFTER_MS = 100;
const t0 = performance.now();
const total = editorsCopy.length;
while (index < total) {
const cm = editorsCopy[index++];
if (cm === cmStart || !editors.has(cm)) {
continue;
}
cm.setOption(key, value);
if (performance.now() - t0 > THROTTLE_AFTER_MS) {
break; break;
} }
case 1:
// middle button: select all marks
cm.execCommand('selectBookmarks');
break;
} }
if (index >= total) {
$.remove(progress);
return;
}
if (!progress &&
index < total / 2 &&
t0 - timeStart > THROTTLE_SHOW_PROGRESS_AFTER_MS) {
let option = $('#editor.' + key);
if (option) {
if (option.type === 'checkbox') {
option = (option.labels || [])[0] || option.nextElementSibling || option;
}
progress = document.body.appendChild(
$create('.set-option-progress', {targetElement: option}));
}
}
if (progress) {
const optionBounds = progress.targetElement.getBoundingClientRect();
const bounds = {
top: optionBounds.top + window.scrollY + 1,
left: optionBounds.left + window.scrollX + 1,
width: (optionBounds.width - 2) * index / total | 0,
height: optionBounds.height - 2,
};
const style = progress.style;
for (const prop in bounds) {
if (bounds[prop] !== parseFloat(style[prop])) {
style[prop] = bounds[prop] + 'px';
}
}
}
setTimeout(throttleSetOption, 0, {
key,
value,
index,
timeStart,
cmStart,
editorsCopy,
progress,
});
} }
function 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

View File

@ -1,115 +0,0 @@
/* global CodeMirror showHelp cmFactory onDOMready $ $create prefs t */
'use strict';
(() => {
onDOMready().then(() => {
$('#colorpicker-settings').onclick = configureColorpicker;
});
prefs.subscribe(['editor.colorpicker.hotkey'], registerHotkey);
prefs.subscribe(['editor.colorpicker'], setColorpickerOption);
setColorpickerOption(null, prefs.get('editor.colorpicker'));
function setColorpickerOption(id, enabled) {
const defaults = CodeMirror.defaults;
const keyName = prefs.get('editor.colorpicker.hotkey');
defaults.colorpicker = enabled;
if (enabled) {
if (keyName) {
CodeMirror.commands.colorpicker = invokeColorpicker;
defaults.extraKeys = defaults.extraKeys || {};
defaults.extraKeys[keyName] = 'colorpicker';
}
defaults.colorpicker = {
// FIXME: who uses this?
// forceUpdate: editor.getEditors().length > 0,
tooltip: t('colorpickerTooltip'),
popup: {
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
hideDelay: 5000,
embedderCallback: state => {
['hexUppercase', 'color']
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
.forEach(name => prefs.set('editor.colorpicker.' + name, state[name]));
},
},
};
} else {
if (defaults.extraKeys) {
delete defaults.extraKeys[keyName];
}
}
cmFactory.setOption('colorpicker', defaults.colorpicker);
}
function registerHotkey(id, hotkey) {
CodeMirror.commands.colorpicker = invokeColorpicker;
const extraKeys = CodeMirror.defaults.extraKeys;
for (const key in extraKeys) {
if (extraKeys[key] === 'colorpicker') {
delete extraKeys[key];
break;
}
}
if (hotkey) {
extraKeys[hotkey] = 'colorpicker';
}
}
function invokeColorpicker(cm) {
cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color'));
}
function configureColorpicker(event) {
event.preventDefault();
const input = $create('input', {
type: 'search',
spellcheck: false,
value: prefs.get('editor.colorpicker.hotkey'),
onkeydown(event) {
event.preventDefault();
event.stopPropagation();
const key = CodeMirror.keyName(event);
switch (key) {
case 'Enter':
if (this.checkValidity()) {
$('#help-popup .dismiss').onclick();
}
return;
case 'Esc':
$('#help-popup .dismiss').onclick();
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('editor.colorpicker.hotkey', key);
},
oninput() {
// fired on pressing "x" to clear the field
prefs.set('editor.colorpicker.hotkey', '');
},
onpaste(event) {
event.preventDefault();
}
});
const popup = showHelp(t('helpKeyMapHotkey'), input);
if (this instanceof Element) {
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();
}
})();

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,327 +1,174 @@
/* global CodeMirror onDOMready prefs setupLivePrefs $ $$ $create t tHTML /* global $$ $ $create */// dom.js
createSourceEditor queryTabs sessionStorageHash getOwnTab FIREFOX API tryCatch /* global API msg */// msg.js
closeCurrentTab messageBox debounce workerUtil /* global CodeMirror */
beautify ignoreChromeError /* global SectionsEditor */
moveFocus msg createSectionsEditor rerouteHotkeys CODEMIRROR_THEMES */ /* global SourceEditor */
/* exported showCodeMirrorPopup editorWorker toggleContextMenuDelete */ /* global clipString createHotkeyInput helpPopup */// util.js
/* global closeCurrentTab deepEqual mapObj sessionStore tryJSONparse */// toolbox.js
/* global cmFactory */
/* global editor EditorHeader */// base.js
/* global linterMan */
/* global prefs */
/* global t */// localization.js
'use strict'; 'use strict';
const editorWorker = workerUtil.createWorker({ //#region init
url: '/edit/editor-worker.js'
document.body.appendChild(t.template.body);
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
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 = () =>
require(['/edit/show-keymap-help'], () => showKeymapHelp()); /* global showKeymapHelp */
$('#linter-settings').onclick = () =>
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',
]);
}); });
let saveSizeOnClose; editor.styleReady.then(async () => {
// Set up mini-header on scroll
// direct & reverse mapping of @-moz-document keywords and internal property names const {isUsercss} = editor;
const propertyToCss = {urls: 'url', urlPrefixes: 'url-prefix', domains: 'domain', regexps: 'regexp'}; const el = $create({
const CssToProperty = Object.entries(propertyToCss) style: `
.reduce((o, v) => { top: 0;
o[v[1]] = v[0]; height: 1px;
return o; position: absolute;
}, {}); visibility: hidden;
`.replace(/;/g, '!important;'),
let editor;
let scrollPointTimer;
document.addEventListener('visibilitychange', beforeUnload);
window.addEventListener('beforeunload', beforeUnload);
msg.onExtension(onRuntimeMessage);
preinit();
(() => {
onDOMready().then(() => {
prefs.subscribe(['editor.keyMap'], showHotkeyInTooltip);
addEventListener('showHotkeyInTooltip', showHotkeyInTooltip);
showHotkeyInTooltip();
buildThemeElement();
buildKeymapElement();
setupLivePrefs();
}); });
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);
initEditor(); /** @param {MediaQueryList} mq */
function onCompactToggled(mq) {
function getCodeMirrorThemes() { for (const el of $$('details[data-pref]')) {
if (!chrome.runtime.getPackageDirectoryEntry) { el.open = mq.matches ? false : prefs.get(el.dataset.pref);
const themes = [
chrome.i18n.getMessage('defaultTheme'),
...CODEMIRROR_THEMES
];
localStorage.codeMirrorThemes = themes.join(' ');
return Promise.resolve(themes);
} }
return new Promise(resolve => { if (mq.matches) {
chrome.runtime.getPackageDirectoryEntry(rootDir => { xo.observe(el);
rootDir.getDirectory('vendor/codemirror/theme', {create: false}, themeDir => {
themeDir.createReader().readEntries(entries => {
const themes = [
chrome.i18n.getMessage('defaultTheme')
].concat(
entries.filter(entry => entry.isFile)
.sort((a, b) => (a.name < b.name ? -1 : 1))
.map(entry => entry.name.replace(/\.css$/, ''))
);
localStorage.codeMirrorThemes = themes.join(' ');
resolve(themes);
});
});
});
});
}
function findKeyForCommand(command, map) {
if (typeof map === 'string') map = CodeMirror.keyMap[map];
let key = Object.keys(map).find(k => map[k] === command);
if (key) {
return key;
}
for (const ft of Array.isArray(map.fallthrough) ? map.fallthrough : [map.fallthrough]) {
key = ft && findKeyForCommand(command, ft);
if (key) {
return key;
}
}
return '';
}
function buildThemeElement() {
const themeElement = $('#editor.theme');
const themeList = localStorage.codeMirrorThemes;
const optionsFromArray = options => {
const fragment = document.createDocumentFragment();
options.forEach(opt => fragment.appendChild($create('option', opt)));
themeElement.appendChild(fragment);
};
if (themeList) {
optionsFromArray(themeList.split(/\s+/));
} else { } else {
// Chrome is starting up and shows our edit.html, but the background page isn't loaded yet xo.disconnect();
const theme = prefs.get('editor.theme');
optionsFromArray([theme === 'default' ? t('defaultTheme') : theme]);
getCodeMirrorThemes().then(() => {
const themes = (localStorage.codeMirrorThemes || '').split(/\s+/);
optionsFromArray(themes);
themeElement.selectedIndex = Math.max(0, themes.indexOf(theme));
});
} }
} }
/** @param {IntersectionObserverEntry[]} entries */
function buildKeymapElement() { function onScrolled(entries) {
// move 'pc' or 'mac' prefix to the end of the displayed label const h = $('#header');
const maps = Object.keys(CodeMirror.keyMap) const sticky = !entries.pop().isIntersecting;
.map(name => ({ if (!isUsercss) scroller.style.paddingTop = sticky ? h.offsetHeight + 'px' : '';
value: name, h.classList.toggle('sticky', sticky);
name: name.replace(/^(pc|mac)(.+)/, (s, arch, baseName) =>
baseName.toLowerCase() + '-' + (arch === 'mac' ? 'Mac' : 'PC')),
}))
.sort((a, b) => a.name < b.name && -1 || a.name > b.name && 1);
const fragment = document.createDocumentFragment();
let bin = fragment;
let groupName;
// group suffixed maps in <optgroup>
maps.forEach(({value, name}, i) => {
groupName = !name.includes('-') ? name : groupName;
const groupWithNext = maps[i + 1] && maps[i + 1].name.startsWith(groupName);
if (groupWithNext) {
if (bin === fragment) {
bin = fragment.appendChild($create('optgroup', {label: name.split('-')[0]}));
}
}
const el = bin.appendChild($create('option', {value}, name));
if (value === prefs.defaults['editor.keyMap']) {
el.dataset.default = '';
el.title = t('defaultTheme');
}
if (!groupWithNext) bin = fragment;
});
$('#editor.keyMap').appendChild(fragment);
} }
});
function showHotkeyInTooltip(_, mapName = prefs.get('editor.keyMap')) { //#endregion
const extraKeys = CodeMirror.defaults.extraKeys; //#region events
for (const el of $$('[data-hotkey-tooltip]')) {
if (el._hotkeyTooltipKeyMap !== mapName) {
el._hotkeyTooltipKeyMap = mapName;
const title = el._hotkeyTooltipTitle = el._hotkeyTooltipTitle || el.title;
const cmd = el.dataset.hotkeyTooltip;
const key = cmd[0] === '=' ? cmd.slice(1) :
findKeyForCommand(cmd, mapName) ||
extraKeys && findKeyForCommand(cmd, extraKeys);
const newTitle = title + (title && key ? '\n' : '') + (key || '');
if (el.title !== newTitle) el.title = newTitle;
}
}
}
function initEditor() { msg.onExtension(request => {
return Promise.all([ const {style} = request;
initStyleData(),
onDOMready(),
prefs.initializing,
])
.then(([style]) => {
const usercss = isUsercss(style);
$('#heading').textContent = t(style.id ? 'editStyleHeading' : 'addStyleTitle');
$('#name').placeholder = t(usercss ? 'usercssEditorNamePlaceholder' : 'styleMissingName');
$('#name').title = usercss ? t('usercssReplaceTemplateName') : '';
$('#preview-label').classList.toggle('hidden', !style.id);
$('#beautify').onclick = () => beautify(editor.getEditors());
window.addEventListener('resize', () => {
debounce(rememberWindowSize, 100);
detectLayout();
});
detectLayout();
editor = (usercss ? createSourceEditor : createSectionsEditor)({
style,
onTitleChanged: updateTitle
});
editor.dirty.onChange(updateDirty);
return Promise.resolve(editor.ready && editor.ready())
.then(updateDirty);
});
}
function updateTitle() {
if (editor) {
const styleName = editor.getStyle().name;
const isDirty = editor.dirty.isDirty();
document.title = (isDirty ? '* ' : '') + styleName;
}
}
function updateDirty() {
const isDirty = editor.dirty.isDirty();
document.body.classList.toggle('dirty', isDirty);
$('#save-button').disabled = !isDirty;
updateTitle();
}
})();
function preinit() {
// preload the theme so that CodeMirror can calculate its metrics in DOMContentLoaded->setupLivePrefs()
new MutationObserver((mutations, observer) => {
const themeElement = $('#cm-theme');
if (themeElement) {
themeElement.href = prefs.get('editor.theme') === 'default' ? ''
: 'vendor/codemirror/theme/' + prefs.get('editor.theme') + '.css';
observer.disconnect();
}
}).observe(document, {subtree: true, childList: true});
if (chrome.windows) {
queryTabs({currentWindow: true}).then(tabs => {
const windowId = tabs[0].windowId;
if (prefs.get('openEditInWindow')) {
if (
/true/.test(sessionStorage.saveSizeOnClose) &&
'left' in prefs.get('windowPosition', {}) &&
!isWindowMaximized()
) {
// window was reopened via Ctrl-Shift-T etc.
chrome.windows.update(windowId, prefs.get('windowPosition'));
}
if (tabs.length === 1 && window.history.length === 1) {
chrome.windows.getAll(windows => {
if (windows.length > 1) {
sessionStorageHash('saveSizeOnClose').set(windowId, true);
saveSizeOnClose = true;
}
});
} else {
saveSizeOnClose = sessionStorageHash('saveSizeOnClose').value[windowId];
}
}
});
}
getOwnTab().then(tab => {
const ownTabId = tab.id;
// use browser history back when 'back to manage' is clicked
if (sessionStorageHash('manageStylesHistory').value[ownTabId] === location.href) {
onDOMready().then(() => {
$('#cancel-button').onclick = event => {
event.stopPropagation();
event.preventDefault();
history.back();
};
});
}
// no windows on android
if (!chrome.windows) {
return;
}
// When an edit page gets attached or detached, remember its state
// so we can do the same to the next one to open.
chrome.tabs.onAttached.addListener((tabId, info) => {
if (tabId !== ownTabId) {
return;
}
if (info.newPosition !== 0) {
prefs.set('openEditInWindow', false);
return;
}
chrome.windows.get(info.newWindowId, {populate: true}, win => {
// If there's only one tab in this window, it's been dragged to new window
const openEditInWindow = win.tabs.length === 1;
if (openEditInWindow && FIREFOX) {
// FF-only because Chrome retardedly resets the size during dragging
chrome.windows.update(info.newWindowId, prefs.get('windowPosition'));
}
prefs.set('openEditInWindow', openEditInWindow);
});
});
});
}
function onRuntimeMessage(request) {
switch (request.method) { switch (request.method) {
case 'styleUpdated': case 'styleUpdated':
if ( if (editor.style.id === style.id) {
editor.getStyleId() === request.style.id && handleExternalUpdate(request);
!['editPreview', 'editPreviewEnd', 'editSave', 'config']
.includes(request.reason)
) {
Promise.resolve(
request.codeIsUpdated === false ?
request.style : API.getStyle(request.style.id)
)
.then(newStyle => {
editor.replaceStyle(newStyle, request.codeIsUpdated);
});
} }
break; break;
case 'styleDeleted': case 'styleDeleted':
if (editor.getStyleId() === request.style.id) { if (editor.style.id === style.id) {
document.removeEventListener('visibilitychange', beforeUnload);
document.removeEventListener('beforeunload', beforeUnload);
closeCurrentTab(); closeCurrentTab();
break;
} }
break; 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 => {
* Invoked for 'visibilitychange' event by default. let pos;
* Invoked for 'beforeunload' event when the style is modified and unsaved. if (editor.isWindowed &&
* See https://developers.google.com/web/updates/2018/07/page-lifecycle-api#legacy-lifecycle-apis-to-avoid document.visibilityState === 'visible' &&
* > Never add a beforeunload listener unconditionally or use it as an end-of-session signal. prefs.get('openEditInWindow') &&
* > Only add it when a user has unsaved work, and remove it as soon as that work has been saved. screenX !== -32000 && // Chrome uses this value for minimized windows
*/ ( // only if not maximized
function beforeUnload(e) { screenX > 0 || outerWidth < screen.availWidth ||
if (saveSizeOnClose) rememberWindowSize(); screenY > 0 || outerHeight < screen.availHeight ||
screenX <= -10 || outerWidth >= screen.availWidth + 10 ||
screenY <= -10 || outerHeight >= screen.availHeight + 10
)
) {
pos = {
left: screenX,
top: screenY,
width: outerWidth,
height: outerHeight,
};
prefs.set('windowPosition', pos);
}
sessionStore.windowPos = JSON.stringify(pos || {});
sessionStore['editorScrollInfo' + editor.style.id] = JSON.stringify(editor.makeScrollInfo());
const activeElement = document.activeElement; const activeElement = document.activeElement;
if (activeElement) { if (activeElement) {
// blurring triggers 'change' or 'input' event if needed // blurring triggers 'change' or 'input' event if needed
@ -329,255 +176,216 @@ function beforeUnload(e) {
// refocus if unloading was canceled // refocus if unloading was canceled
setTimeout(() => activeElement.focus()); setTimeout(() => activeElement.focus());
} }
if (editor && editor.dirty.isDirty()) { if (editor.dirty.isDirty()) {
// neither confirm() nor custom messages work in modern browsers but just in case // neither confirm() nor custom messages work in modern browsers but just in case
e.returnValue = t('styleChangesNotSaved'); e.returnValue = t('styleChangesNotSaved');
} }
}
function isUsercss(style) {
return (
style.usercssData ||
!style.id && prefs.get('newStyleAsUsercss')
);
}
function initStyleData() {
// TODO: remove .replace(/^\?/, '') when minimum_chrome_version >= 52 (https://crbug.com/601425)
const params = new URLSearchParams(location.search.replace(/^\?/, ''));
const id = Number(params.get('id'));
const createEmptyStyle = () => ({
name: params.get('domain') ||
tryCatch(() => new URL(params.get('url-prefix')).hostname) ||
'',
enabled: true,
sections: [
Object.assign({code: ''},
...Object.keys(CssToProperty)
.map(name => ({
[CssToProperty[name]]: params.get(name) && [params.get(name)] || []
}))
)
],
});
return fetchStyle()
.then(style => {
if (style.id) sessionStorage.justEditedStyleId = style.id;
// we set "usercss" class on <html> when <body> is empty
// so there'll be no flickering of the elements that depend on it
if (isUsercss(style)) {
document.documentElement.classList.add('usercss');
}
// strip URL parameters when invoked for a non-existent id
if (!style.id) {
history.replaceState({}, document.title, location.pathname);
}
return style;
});
function fetchStyle() {
if (id) {
return API.getStyle(id);
}
return Promise.resolve(createEmptyStyle());
}
}
function showHelp(title = '', body) {
const div = $('#help-popup');
div.className = '';
const contents = $('.contents', div);
contents.textContent = '';
if (body) {
contents.appendChild(typeof body === 'string' ? tHTML(body) : body);
}
$('.title', div).textContent = title;
showHelp.close = showHelp.close || (event => {
const canClose =
!event ||
event.type === 'click' ||
(
event.which === 27 &&
!event.altKey && !event.ctrlKey && !event.shiftKey && !event.metaKey &&
!$('.CodeMirror-hints, #message-box') &&
(
!document.activeElement ||
!document.activeElement.closest('#search-replace-dialog') &&
document.activeElement.matches(':not(input), .can-close-on-esc')
)
);
if (!canClose) {
return;
}
if (event && div.codebox && !div.codebox.options.readOnly && !div.codebox.isClean()) {
setTimeout(() => {
messageBox.confirm(t('confirmDiscardChanges'))
.then(ok => ok && showHelp.close());
});
return;
}
if (div.contains(document.activeElement) && showHelp.originalFocus) {
showHelp.originalFocus.focus();
}
div.style.display = '';
contents.textContent = '';
clearTimeout(contents.timer);
window.removeEventListener('keydown', showHelp.close, true);
window.dispatchEvent(new Event('closeHelp'));
});
window.addEventListener('keydown', showHelp.close, true);
$('.dismiss', div).onclick = showHelp.close;
// reset any inline styles
div.style = 'display: block';
showHelp.originalFocus = document.activeElement;
return div;
}
function showCodeMirrorPopup(title, html, options) {
const popup = showHelp(title, html);
popup.classList.add('big');
let cm = popup.codebox = CodeMirror($('.contents', popup), Object.assign({
mode: 'css',
lineNumbers: true,
lineWrapping: prefs.get('editor.lineWrapping'),
foldGutter: true,
gutters: ['CodeMirror-linenumbers', 'CodeMirror-foldgutter', 'CodeMirror-lint-markers'],
matchBrackets: true,
styleActiveLine: true,
theme: prefs.get('editor.theme'),
keyMap: prefs.get('editor.keyMap')
}, options));
cm.focus();
rerouteHotkeys(false);
document.documentElement.style.pointerEvents = 'none';
popup.style.pointerEvents = 'auto';
const onKeyDown = event => {
if (event.which === 9 && !event.ctrlKey && !event.altKey && !event.metaKey) {
const search = $('#search-replace-dialog');
const area = search && search.contains(document.activeElement) ? search : popup;
moveFocus(area, event.shiftKey ? -1 : 1);
event.preventDefault();
}
};
window.addEventListener('keydown', onKeyDown, true);
window.addEventListener('closeHelp', function _() {
window.removeEventListener('closeHelp', _);
window.removeEventListener('keydown', onKeyDown, true);
document.documentElement.style.removeProperty('pointer-events');
rerouteHotkeys(true);
cm = popup.codebox = null;
});
return popup;
}
function rememberWindowSize() {
if (
document.visibilityState === 'visible' &&
prefs.get('openEditInWindow') &&
!isWindowMaximized()
) {
prefs.set('windowPosition', {
left: window.screenX,
top: window.screenY,
width: window.outerWidth,
height: window.outerHeight,
});
}
}
prefs.subscribe(['editor.linter'], (key, value) => {
$('body').classList.toggle('linter-disabled', value === '');
}); });
function fixedHeader() { //#endregion
const scrollPoint = $('#header').clientHeight - 40; //#region editor methods
const linterEnabled = prefs.get('editor.linter') !== '';
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
$('body').classList.add('fixed-header');
} else if (window.scrollY < 40 && linterEnabled) {
$('body').classList.remove('fixed-header');
}
}
function detectLayout() { function EditorMethods() {
const body = $('body'); const toc = [];
const options = $('#options'); const {dirty} = editor;
const lint = $('#lint'); let {style} = editor;
const compact = window.innerWidth <= 850; let wasDirty = false;
const shortViewportLinter = window.innerHeight < 692;
const shortViewportNoLinter = window.innerHeight < 554; Object.defineProperties(editor, {
const linterEnabled = prefs.get('editor.linter') !== ''; scrollInfo: {
if (compact) { get: () => style.id && tryJSONparse(sessionStore['editorScrollInfo' + style.id]) || {},
body.classList.add('compact-layout'); },
options.removeAttribute('open'); style: {
options.classList.add('ignore-pref'); get: () => style,
lint.removeAttribute('open'); set: val => (style = val),
lint.classList.add('ignore-pref'); },
if (!$('.usercss')) { });
clearTimeout(scrollPointTimer);
scrollPointTimer = setTimeout(() => { /** @namespace Editor */
const scrollPoint = $('#header').clientHeight - 40; Object.assign(editor, {
if (window.scrollY >= scrollPoint && !$('.fixed-header') && linterEnabled) {
body.classList.add('fixed-header'); applyScrollInfo(cm, si = (editor.scrollInfo.cms || [])[0]) {
if (si && si.sel) {
const bmOpts = {sublimeBookmark: true, clearWhenEmpty: false}; // copied from sublime.js
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
}
},
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() {
const isDirty = dirty.isDirty();
if (wasDirty !== isDirty) {
wasDirty = isDirty;
document.body.classList.toggle('dirty', isDirty);
$('#save-button').disabled = !isDirty;
}
editor.updateTitle();
},
updateEnabledness(enabled) {
dirty.modify('enabled', style.enabled, enabled);
style.enabled = enabled;
editor.updateLivePreview();
},
updateName(isUserInput) {
if (!editor) return;
if (isUserInput) {
const {value} = $('#name');
dirty.modify('name', style[editor.nameTarget] || style.name, value);
style[editor.nameTarget] = value;
}
editor.updateTitle();
},
updateToc(added = editor.sections) {
if (!toc.el) {
toc.el = $('#toc');
toc.elDetails = toc.el.closest('details');
}
if (!toc.elDetails.open) return;
const {sections} = editor;
const first = sections.indexOf(added[0]);
const elFirst = toc.el.children[first];
if (first >= 0 && (!added.focus || !elFirst)) {
for (let el = elFirst, i = first; i < sections.length; i++) {
const entry = sections[i].tocEntry;
if (!deepEqual(entry, toc[i])) {
if (!el) el = toc.el.appendChild($create('li', {tabIndex: 0}));
el.tabIndex = entry.removed ? -1 : 0;
toc[i] = Object.assign({}, entry);
const s = el.textContent = clipString(entry.label) || (
entry.target == null
? t('appliesToEverything')
: clipString(entry.target) + (entry.numTargets > 1 ? ', ...' : ''));
if (s.length > 30) el.title = s;
}
el = el.nextElementSibling;
} }
}, 250);
window.addEventListener('scroll', fixedHeader);
}
} else {
body.classList.remove('compact-layout');
body.classList.remove('fixed-header');
window.removeEventListener('scroll', fixedHeader);
if (shortViewportLinter && linterEnabled || shortViewportNoLinter && !linterEnabled) {
options.removeAttribute('open');
options.classList.add('ignore-pref');
if (prefs.get('editor.lint.expanded')) {
lint.setAttribute('open', '');
} }
while (toc.length > sections.length) {
toc.el.lastElementChild.remove();
toc.length--;
}
if (added.focus) {
const cls = 'current';
const old = $('.' + cls, toc.el);
const el = elFirst || toc.el.children[first];
if (old && old !== el) old.classList.remove(cls);
el.classList.add(cls);
}
},
useSavedStyle(newStyle) {
if (style.id !== newStyle.id) {
history.replaceState({}, '', `?id=${newStyle.id}`);
}
sessionStore.justEditedStyleId = newStyle.id;
Object.assign(style, newStyle);
editor.updateClass();
editor.updateMeta();
},
});
}
//#endregion
//#region colorpickerHelper
(async function colorpickerHelper() {
prefs.subscribe('editor.colorpicker.hotkey', (id, hotkey) => {
CodeMirror.commands.colorpicker = invokeColorpicker;
const extraKeys = CodeMirror.defaults.extraKeys;
for (const key in extraKeys) {
if (extraKeys[key] === 'colorpicker') {
delete extraKeys[key];
break;
}
}
if (hotkey) {
extraKeys[hotkey] = 'colorpicker';
}
});
prefs.subscribe('editor.colorpicker', (id, enabled) => {
const defaults = CodeMirror.defaults;
const keyName = prefs.get('editor.colorpicker.hotkey');
defaults.colorpicker = enabled;
if (enabled) {
if (keyName) {
CodeMirror.commands.colorpicker = invokeColorpicker;
defaults.extraKeys = defaults.extraKeys || {};
defaults.extraKeys[keyName] = 'colorpicker';
}
defaults.colorpicker = {
tooltip: t('colorpickerTooltip'),
popup: {
tooltipForSwitcher: t('colorpickerSwitchFormatTooltip'),
paletteLine: t('numberedLine'),
paletteHint: t('colorpickerPaletteHint'),
hexUppercase: prefs.get('editor.colorpicker.hexUppercase'),
embedderCallback: state => {
['hexUppercase', 'color']
.filter(name => state[name] !== prefs.get('editor.colorpicker.' + name))
.forEach(name => prefs.set('editor.colorpicker.' + name, state[name]));
},
get maxHeight() {
return prefs.get('editor.colorpicker.maxHeight');
},
set maxHeight(h) {
prefs.set('editor.colorpicker.maxHeight', h);
},
},
};
} else { } else {
options.classList.remove('ignore-pref'); if (defaults.extraKeys) {
lint.classList.remove('ignore-pref'); delete defaults.extraKeys[keyName];
if (prefs.get('editor.options.expanded')) {
options.setAttribute('open', '');
}
if (prefs.get('editor.lint.expanded')) {
lint.setAttribute('open', '');
} }
} }
cmFactory.globalSetOption('colorpicker', defaults.colorpicker);
}, {runNow: true});
$('#colorpicker-settings').onclick = function (event) {
event.preventDefault();
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', popup).focus();
};
function invokeColorpicker(cm) {
cm.state.colorpicker.openPopup(prefs.get('editor.colorpicker.color'));
} }
} })();
function isWindowMaximized() { //#endregion
return (
window.screenX <= 0 &&
window.screenY <= 0 &&
window.outerWidth >= screen.availWidth &&
window.outerHeight >= screen.availHeight &&
window.screenX > -10 &&
window.screenY > -10 &&
window.outerWidth < screen.availWidth + 10 &&
window.outerHeight < screen.availHeight + 10
);
}
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);
}
}

View File

@ -1,89 +1,176 @@
/* global importScripts workerUtil CSSLint require metaParser */ /* global createWorkerApi */// worker-util.js
'use strict'; 'use strict';
importScripts('/js/worker-util.js'); (() => {
const {createAPI, loadScript} = workerUtil; let sugarss = false;
createAPI({ /** @namespace EditorWorker */
csslint: (code, config) => { createWorkerApi({
loadScript('/vendor-overwrites/csslint/parserlib.js', '/vendor-overwrites/csslint/csslint.js');
return CSSLint.verify(code, config).messages async csslint(code, config) {
.map(m => Object.assign(m, {rule: {id: m.rule.id}})); require(['/js/csslint/parserlib', '/js/csslint/csslint']); /* global CSSLint */
}, return CSSLint
stylelint: (code, config) => { .verify(code, config).messages
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js'); .map(m => Object.assign(m, {rule: {id: m.rule.id}}));
return require('stylelint').lint({code, config}); },
},
metalint: code => { getCssPropsValues() {
loadScript( require(['/js/csslint/parserlib']); /* global parserlib */
'/js/polyfill.js', const {
'/vendor/usercss-meta/usercss-meta.min.js', css: {Colors, GlobalKeywords, Properties},
'/vendor-overwrites/colorpicker/colorconverter.js', util: {describeProp},
'/js/meta-parser.js' } = parserlib;
); const namedColors = Object.keys(Colors);
const result = metaParser.lint(code); const rxNonWord = /(?:<.+?>|[^-\w<(]+\d*)+/g;
// extract needed info const res = {};
result.errors = result.errors.map(err => // 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
},
metalint(code) {
require(['/js/meta-parser']); /* global metaParser */
const result = metaParser.lint(code);
// extract needed info
result.errors = result.errors.map(err => ({
code: err.code, code: err.code,
args: err.args, args: err.args,
message: err.message, message: err.message,
index: err.index index: err.index,
}) }));
); return result;
return result; },
},
getStylelintRules,
getCsslintRules
});
function getCsslintRules() { async stylelint(opts) {
loadScript('/vendor-overwrites/csslint/csslint.js'); require(['/vendor/stylelint-bundle/stylelint-bundle.min']); /* global stylelint */
return CSSLint.getRules().map(rule => { // Stylus-lang allows a trailing ";" but sugarss doesn't, so we monkeypatch it
const output = {}; stylelint.SugarSSParser.prototype.checkSemicolon = tt => {
for (const [key, value] of Object.entries(rule)) { while (tt.length && tt[tt.length - 1][0] === ';') tt.pop();
if (typeof value !== 'function') { };
output[key] = value; 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. */
return output; opts.config.customSyntax = !pass ? 'sugarss' : '';
}); try {
} const res = await stylelint.createLinter(opts)._lintSource(opts);
if (pass !== -1) sugarss = pass;
function getStylelintRules() { return collectStylelintResults(res, opts);
loadScript('/vendor/stylelint-bundle/stylelint-bundle.min.js'); } catch (e) {
const stylelint = require('stylelint'); const fatal = pass === -1 ||
const options = {}; !pass && !/^CssSyntaxError:.+?Unnecessary curly bracket/.test(e) ||
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g; pass && !/^CssSyntaxError:.+?Unknown word[\s\S]*?\.decl\s/.test(`${e}${e.stack}`);
const rxString = /"([-\w\s]{3,}?)"/g; if (fatal) {
for (const id of Object.keys(stylelint.rules)) { return [{
const ruleCode = String(stylelint.rules[id]); from: {line: e.line - 1, ch: e.column - 1},
const sets = []; to: {line: e.line - 1, ch: e.column - 1},
let m, mStr; message: e.reason,
while ((m = rxPossible.exec(ruleCode))) { severity: 'error',
const possible = m[1]; rule: e.name,
const set = []; }];
while ((mStr = rxString.exec(possible))) { }
const s = mStr[1];
if (s.includes(' ')) {
set.push(...s.split(/\s+/));
} else {
set.push(s);
} }
} }
if (possible.includes('ignoreAtRules')) { },
set.push('ignoreAtRules'); });
const ruleRetriever = {
csslint() {
require(['/js/csslint/csslint']);
return CSSLint.getRuleList().map(rule => {
const output = {};
for (const [key, value] of Object.entries(rule)) {
if (typeof value !== 'function') {
output[key] = value;
}
}
return output;
});
},
stylelint() {
require(['/vendor/stylelint-bundle/stylelint-bundle.min']);
const options = {};
const rxPossible = /\bpossible:("(?:[^"]*?)"|\[(?:[^\]]*?)\]|\{(?:[^}]*?)\})/g;
const rxString = /"([-\w\s]{3,}?)"/g;
for (const [id, rule] of Object.entries(stylelint.rules)) {
const ruleCode = `${rule()}`;
const sets = [];
let m, mStr;
while ((m = rxPossible.exec(ruleCode))) {
const possible = m[1];
const set = [];
while ((mStr = rxString.exec(possible))) {
const s = mStr[1];
if (s.includes(' ')) {
set.push(...s.split(/\s+/));
} else {
set.push(s);
}
}
if (possible.includes('ignoreAtRules')) {
set.push('ignoreAtRules');
}
if (possible.includes('ignoreShorthands')) {
set.push('ignoreShorthands');
}
if (set.length) {
sets.push(set);
}
}
options[id] = sets;
} }
if (possible.includes('ignoreShorthands')) { return options;
set.push('ignoreShorthands'); },
};
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;
} }
if (set.length) { const {rule} = m;
sets.push(set); 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,
});
} }
if (sets.length) { return res;
options[id] = sets;
}
} }
return options; })();
}

109
edit/embedded-popup.js Normal file
View File

@ -0,0 +1,109 @@
/* global $ $create $remove getEventKeyName */// dom.js
/* global CodeMirror */
/* global prefs */
/* global t */// localization.js
'use strict';
(() => {
const ID = 'popup-iframe';
const SEL = '#' + ID;
const URL = chrome.runtime.getManifest().browser_action.default_popup;
const POPUP_HOTKEY = 'Shift-Ctrl-Alt-S';
/** @type {HTMLIFrameElement} */
let frame;
let isLoaded;
let scrollbarWidth;
const btn = $create('img', {
id: 'popup-button',
title: t('optionsCustomizePopup') + '\n' + POPUP_HOTKEY,
onclick: embedPopup,
});
$.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/' : ''}`;
btn.srcset = `${prefix}16.png 1x,${prefix}32.png 2x`;
}, {runNow: true});
window.on('keydown', e => {
if (getEventKeyName(e) === POPUP_HOTKEY) {
embedPopup();
}
});
function embedPopup() {
if ($(SEL)) return;
isLoaded = false;
scrollbarWidth = 0;
frame = $create('iframe', {
id: ID,
src: URL,
height: 600,
width: prefs.get('popupWidth'),
onload: initFrame,
});
window.on('mousedown', removePopup);
document.body.appendChild(frame);
}
function initFrame() {
frame = this;
frame.focus();
const pw = frame.contentWindow;
const body = pw.document.body;
pw.on('keydown', removePopupOnEsc);
pw.close = removePopup;
new pw.IntersectionObserver(onIntersect).observe(body.appendChild(
$create('div', {style: {height: '1px', marginTop: '-1px'}})
));
new pw.MutationObserver(onMutation).observe(body, {
attributes: true,
attributeFilter: ['style'],
});
}
function onMutation() {
const body = frame.contentDocument.body;
const bs = body.style;
const w = parseFloat(bs.minWidth || bs.width) + (scrollbarWidth || 0);
const h = parseFloat(bs.minHeight || body.offsetHeight);
if (frame.width - w) frame.width = w;
if (frame.height - h) frame.height = h;
}
function onIntersect([e]) {
const pw = frame.contentWindow;
const el = pw.document.scrollingElement;
const h = e.isIntersecting && !pw.scrollY ? el.offsetHeight : el.scrollHeight;
const hasSB = h > el.offsetHeight;
const {width} = e.boundingClientRect;
frame.height = h;
if (!hasSB !== !scrollbarWidth || frame.width - width) {
scrollbarWidth = hasSB ? width - el.offsetWidth : 0;
frame.width = width + scrollbarWidth;
}
if (!isLoaded) {
isLoaded = true;
frame.dataset.loaded = '';
}
}
function removePopup() {
frame = null;
$remove(SEL);
window.off('mousedown', removePopup);
}
function removePopupOnEsc(e) {
if (getEventKeyName(e) === 'Escape') {
removePopup();
}
}
})();

View File

@ -61,7 +61,7 @@
border: none; border: none;
background-color: white; background-color: white;
font-weight: bold; font-weight: bold;
white-space: nowrap; white-space: pre; /* issue #1000 */
color: currentColor; /* use the current theme's color instead of UserAgent's CSS */ color: currentColor; /* use the current theme's color instead of UserAgent's CSS */
flex: 1; flex: 1;
} }
@ -183,7 +183,8 @@
/*********** CM search highlight restyling, which shouldn't need color variables ****************/ /*********** CM search highlight restyling, which shouldn't need color variables ****************/
body.find-open .search-target-editor { body.find-open .search-target-editor {
outline-color: darkorange !important; box-shadow: 0 0 0 1px hsl(33, 100%, 50%), 0 0 3px hsla(33, 100%, 50%, .4);
/* same as our global.css focus rule */
} }
body.find-open .cm-searching { body.find-open .cm-searching {

View File

@ -1,8 +1,14 @@
/* global CodeMirror focusAccessibility colorMimicry editor /* global $ $$ $create $remove focusAccessibility setInputValue toggleDataset */// dom.js
onDOMready $ $$ $create t debounce tryRegExp stringAsRegExp template */ /* global CodeMirror */
/* global chromeLocal */// storage-util.js
/* global colorMimicry */
/* global debounce stringAsRegExp tryRegExp */// toolbox.js
/* global editor */
/* global t */// localization.js
'use strict'; 'use strict';
onDOMready().then(() => { (() => {
require(['/edit/global-search.css']);
//region Constants and state //region Constants and state
@ -10,7 +16,6 @@ onDOMready().then(() => {
const ANNOTATE_SCROLLBAR_DELAY = 350; const ANNOTATE_SCROLLBAR_DELAY = 350;
const ANNOTATE_SCROLLBAR_OPTIONS = {maxMatches: 10e3}; const ANNOTATE_SCROLLBAR_OPTIONS = {maxMatches: 10e3};
const STORAGE_UPDATE_DELAY = 500; const STORAGE_UPDATE_DELAY = 500;
const SCROLL_REVEAL_MIN_PX = 50;
const DIALOG_SELECTOR = '#search-replace-dialog'; const DIALOG_SELECTOR = '#search-replace-dialog';
const DIALOG_STYLE_SELECTOR = '#search-replace-dialog-style'; const DIALOG_STYLE_SELECTOR = '#search-replace-dialog-style';
@ -22,6 +27,7 @@ onDOMready().then(() => {
const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/; const RX_MAYBE_REGEXP = /^\s*\/(.+?)\/([simguy]*)\s*$/;
const state = { const state = {
firstRun: true,
// used for case-sensitive matching directly // used for case-sensitive matching directly
find: '', find: '',
// used when /re/ is detected or for case-insensitive matching // used when /re/ is detected or for case-insensitive matching
@ -48,7 +54,7 @@ onDOMready().then(() => {
undoHistory: [], undoHistory: [],
searchInApplies: !document.documentElement.classList.contains('usercss'), searchInApplies: !editor.isUsercss,
}; };
//endregion //endregion
@ -64,10 +70,12 @@ onDOMready().then(() => {
if (found) { if (found) {
const target = $('.' + TARGET_CLASS); const target = $('.' + TARGET_CLASS);
const cm = target.CodeMirror; 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) { if (cm) {
const pos = cm.state.search.searchPos; const {from, to} = cm.state.search.searchPos;
cm.setSelection(pos.from, pos.to); cm.jumpToPos(from, to);
} }
} }
destroyDialog({restoreFocus: !found}); destroyDialog({restoreFocus: !found});
@ -78,7 +86,7 @@ onDOMready().then(() => {
doReplace(); doReplace();
return; return;
} }
return !event.target.closest(focusAccessibility.ELEMENTS.join(',')); return !focusAccessibility.closest(event.target);
}, },
'Esc': () => { 'Esc': () => {
destroyDialog({restoreFocus: true}); destroyDialog({restoreFocus: true});
@ -99,7 +107,7 @@ onDOMready().then(() => {
state.lastFind = ''; state.lastFind = '';
toggleDataset(this, 'enabled', !state.icase); toggleDataset(this, 'enabled', !state.icase);
doSearch({canAdvance: false}); doSearch({canAdvance: false});
} },
}, },
}; };
@ -125,17 +133,17 @@ onDOMready().then(() => {
}, },
onfocusout() { onfocusout() {
if (!state.dialog.contains(document.activeElement)) { if (!state.dialog.contains(document.activeElement)) {
state.dialog.addEventListener('focusin', EVENTS.onfocusin); state.dialog.on('focusin', EVENTS.onfocusin);
state.dialog.removeEventListener('focusout', EVENTS.onfocusout); state.dialog.off('focusout', EVENTS.onfocusout);
} }
}, },
onfocusin() { onfocusin() {
state.dialog.addEventListener('focusout', EVENTS.onfocusout); state.dialog.on('focusout', EVENTS.onfocusout);
state.dialog.removeEventListener('focusin', EVENTS.onfocusin); state.dialog.off('focusin', EVENTS.onfocusin);
trimUndoHistory(); trimUndoHistory();
enableUndoButton(state.undoHistory.length); enableUndoButton(state.undoHistory.length);
if (state.find) doSearch({canAdvance: false}); if (state.find) doSearch({canAdvance: false});
} },
}; };
const DIALOG_PROPS = { const DIALOG_PROPS = {
@ -151,7 +159,7 @@ onDOMready().then(() => {
state.replace = this.value; state.replace = this.value;
adjustTextareaSize(this); adjustTextareaSize(this);
debounce(writeStorage, STORAGE_UPDATE_DELAY); debounce(writeStorage, STORAGE_UPDATE_DELAY);
} },
}, },
}; };
@ -168,7 +176,7 @@ onDOMready().then(() => {
replace(cm) { replace(cm) {
state.reverse = false; state.reverse = false;
focusDialog('replace', cm); focusDialog('replace', cm);
} },
}; };
COMMANDS.replaceAll = COMMANDS.replace; COMMANDS.replaceAll = COMMANDS.replace;
@ -176,7 +184,6 @@ onDOMready().then(() => {
Object.assign(CodeMirror.commands, COMMANDS); Object.assign(CodeMirror.commands, COMMANDS);
readStorage(); readStorage();
return;
//region Find //region Find
@ -241,6 +248,7 @@ onDOMready().then(() => {
} else { } else {
showTally(0, 0); showTally(0, 0);
} }
state.firstRun = false;
return found; return found;
} }
@ -559,15 +567,16 @@ onDOMready().then(() => {
function createDialog(type) { function createDialog(type) {
state.originalFocus = document.activeElement; state.originalFocus = document.activeElement;
state.firstRun = true;
const dialog = state.dialog = template.searchReplaceDialog.cloneNode(true); const dialog = state.dialog = t.template.searchReplaceDialog.cloneNode(true);
Object.assign(dialog, DIALOG_PROPS.dialog); Object.assign(dialog, DIALOG_PROPS.dialog);
dialog.addEventListener('focusout', EVENTS.onfocusout); dialog.on('focusout', EVENTS.onfocusout);
dialog.dataset.type = type; dialog.dataset.type = type;
dialog.style.pointerEvents = 'auto'; dialog.style.pointerEvents = 'auto';
const content = $('[data-type="content"]', dialog); const content = $('[data-type="content"]', dialog);
content.parentNode.replaceChild(template[type].cloneNode(true), content); content.parentNode.replaceChild(t.template[type].cloneNode(true), content);
createInput(0, 'input', state.find); createInput(0, 'input', state.find);
createInput(1, 'input2', state.replace); createInput(1, 'input2', state.replace);
@ -575,11 +584,11 @@ onDOMready().then(() => {
state.tally = $('[data-type="tally"]', dialog); state.tally = $('[data-type="tally"]', dialog);
const colors = { const colors = {
body: colorMimicry.get(document.body, {bg: 'backgroundColor'}), body: colorMimicry(document.body, {bg: 'backgroundColor'}),
input: colorMimicry.get($('input:not(:disabled)'), {bg: 'backgroundColor'}), input: colorMimicry($('input:not(:disabled)'), {bg: 'backgroundColor'}),
icon: colorMimicry.get($$('svg.info')[1], {fill: 'fill'}), icon: colorMimicry($$('svg.info')[1], {fill: 'fill'}),
}; };
document.documentElement.appendChild( $.root.appendChild(
$(DIALOG_STYLE_SELECTOR) || $(DIALOG_STYLE_SELECTOR) ||
$create('style' + DIALOG_STYLE_SELECTOR) $create('style' + DIALOG_STYLE_SELECTOR)
).textContent = ` ).textContent = `
@ -598,10 +607,10 @@ onDOMready().then(() => {
} }
#search-replace-dialog[data-type="replace"] button:hover svg, #search-replace-dialog[data-type="replace"] button:hover svg,
#search-replace-dialog svg:hover { #search-replace-dialog svg:hover {
fill: inherit; fill: var(--cmin);
} }
#search-replace-dialog [data-action="case"]:hover { #search-replace-dialog [data-action="case"]:hover {
color: inherit; color: var(--cmin);
} }
#search-replace-dialog [data-action="clear"] { #search-replace-dialog [data-action="clear"] {
background-color: ${colors.input.bg.replace(/[^,]+$/, '') + '.75)'}; background-color: ${colors.input.bg.replace(/[^,]+$/, '') + '.75)'};
@ -630,14 +639,14 @@ onDOMready().then(() => {
input.value = value; input.value = value;
Object.assign(input, DIALOG_PROPS[name]); Object.assign(input, DIALOG_PROPS[name]);
input.parentElement.appendChild(template.clearSearch.cloneNode(true)); input.parentElement.appendChild(t.template.clearSearch.cloneNode(true));
$('[data-action]', input.parentElement)._input = input; $('[data-action]', input.parentElement)._input = input;
} }
function destroyDialog({restoreFocus = false} = {}) { function destroyDialog({restoreFocus = false} = {}) {
state.input = null; state.input = null;
$.remove(DIALOG_SELECTOR); $remove(DIALOG_SELECTOR);
debounce.unregister(doSearch); debounce.unregister(doSearch);
makeTargetVisible(null); makeTargetVisible(null);
if (restoreFocus) { if (restoreFocus) {
@ -671,7 +680,7 @@ onDOMready().then(() => {
el.style.width = newWidth + 'px'; el.style.width = newWidth + 'px';
} }
const numLines = el.value.split('\n').length; const numLines = el.value.split('\n').length;
if (numLines !== parseInt(el.rows)) { if (numLines !== Number(el.rows)) {
el.rows = numLines; el.rows = numLines;
} }
el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden'; el.style.overflowX = el.scrollWidth > el.clientWidth ? '' : 'hidden';
@ -766,25 +775,22 @@ onDOMready().then(() => {
// scrolls the editor to reveal the match // scrolls the editor to reveal the match
function makeMatchVisible(cm, searchCursor) { function makeMatchVisible(cm, searchCursor) {
const canFocus = !state.dialog || !state.dialog.contains(document.activeElement); const canFocus = !state.firstRun && (!state.dialog || !state.dialog.contains(document.activeElement));
state.cm = cm; state.cm = cm;
// scroll within the editor // scroll within the editor
const pos = searchCursor.pos;
Object.assign(getStateSafe(cm), { Object.assign(getStateSafe(cm), {
cursorPos: { cursorPos: {
from: cm.getCursor('from'), from: cm.getCursor('from'),
to: cm.getCursor('to'), to: cm.getCursor('to'),
}, },
searchPos: searchCursor.pos, searchPos: pos,
unclosedOp: !cm.curOp, unclosedOp: !cm.curOp,
}); });
if (!cm.curOp) cm.startOperation(); if (!cm.curOp) cm.startOperation();
if (canFocus) cm.setSelection(searchCursor.pos.from, searchCursor.pos.to); if (!state.firstRun) {
cm.scrollIntoView(searchCursor.pos, SCROLL_REVEAL_MIN_PX); cm.jumpToPos(pos.from, pos.to);
}
// scroll to the editor itself
editor.scrollToEditor(cm);
// focus or expose as the current search target // focus or expose as the current search target
clearMarker(); clearMarker();
if (canFocus) { if (canFocus) {
@ -793,7 +799,6 @@ onDOMready().then(() => {
} else { } else {
makeTargetVisible(cm.display.wrapper); makeTargetVisible(cm.display.wrapper);
// mark the match // mark the match
const pos = searchCursor.pos;
state.marker = cm.state.search.marker = cm.markText(pos.from, pos.to, { state.marker = cm.state.search.marker = cm.markText(pos.from, pos.to, {
className: MATCH_CLASS, className: MATCH_CLASS,
clearOnEnter: true, clearOnEnter: true,
@ -871,15 +876,6 @@ onDOMready().then(() => {
} }
function toggleDataset(el, prop, state) {
if (state) {
el.dataset[prop] = '';
} else {
delete el.dataset[prop];
}
}
function saveWindowScrollPos() { function saveWindowScrollPos() {
state.scrollX = window.scrollX; state.scrollX = window.scrollX;
state.scrollY = window.scrollY; state.scrollY = window.scrollY;
@ -900,7 +896,9 @@ onDOMready().then(() => {
// produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...] // produces [i, i+1, i-1, i+2, i-2, i+3, i-3, ...]
function radiateArray(arr, focalIndex) { function radiateArray(arr, focalIndex) {
const result = [arr[focalIndex]]; const focus = arr[focalIndex];
if (!focus) return arr;
const result = [focus];
const len = arr.length; const len = arr.length;
for (let i = 1; i < len; i++) { for (let i = 1; i < len; i++) {
if (focalIndex + i < len) { if (focalIndex + i < len) {
@ -915,7 +913,7 @@ onDOMready().then(() => {
function readStorage() { function readStorage() {
chrome.storage.local.get('editor', ({editor = {}}) => { chromeLocal.getValue('editor').then((editor = {}) => {
state.find = editor.find || ''; state.find = editor.find || '';
state.replace = editor.replace || ''; state.replace = editor.replace || '';
state.icase = editor.icase || state.icase; state.icase = editor.icase || state.icase;
@ -924,28 +922,13 @@ onDOMready().then(() => {
function writeStorage() { function writeStorage() {
chrome.storage.local.get('editor', ({editor}) => chromeLocal.getValue('editor').then((editor = {}) =>
chrome.storage.local.set({ chromeLocal.setValue('editor', Object.assign(editor, {
editor: Object.assign(editor || {}, { find: state.find,
find: state.find, replace: state.replace,
replace: state.replace, icase: state.icase,
icase: state.icase, })));
})
}));
}
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 //endregion
}); })();

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