diff --git a/.config/hypr/hyprland.conf b/.config/hypr/hyprland.conf index fa4f58f..dc4aed3 100644 --- a/.config/hypr/hyprland.conf +++ b/.config/hypr/hyprland.conf @@ -138,7 +138,7 @@ bind = SUPER,w,exec,/home/chris/bin/window.sh bind = SUPER,E,exec,/home/chris/bin/emacslof bind = SUPER,d,exec,emacsclient -c -e '(dired-jump)' bind = SUPER,v,exec,emacsclient -e '(chris/dired-open-videos)' -bind = SUPER,B,exec,/home/chris/bin/fflof +bind = SUPER,B,exec,/home/chris/bin/nyxtlof bind = SUPER,A,exec,alacritty --class pulsemixer -e pulsemixer bind = SUPERCTRL,i,exec,alacritty --class btop -e btop bind = ,Print,exec,screenshot diff --git a/.config/nyxt/auto-config.3.lisp b/.config/nyxt/auto-config.3.lisp new file mode 100644 index 0000000..e69de29 diff --git a/.config/nyxt/auto-config.lisp b/.config/nyxt/auto-config.lisp deleted file mode 100644 index e9b841b..0000000 --- a/.config/nyxt/auto-config.lisp +++ /dev/null @@ -1,16 +0,0 @@ -(DEFINE-CONFIGURATION NYXT/REDUCE-TRACKING-MODE:REDUCE-TRACKING-MODE - ((VISIBLE-IN-STATUS-P NIL))) -(DEFINE-CONFIGURATION NYXT/FORCE-HTTPS-MODE:FORCE-HTTPS-MODE - ((VISIBLE-IN-STATUS-P NIL))) -(DEFINE-CONFIGURATION NYXT/AUTO-MODE:AUTO-MODE - ((VISIBLE-IN-STATUS-P NIL))) -(DEFINE-CONFIGURATION NYXT/CERTIFICATE-EXCEPTION-MODE:CERTIFICATE-EXCEPTION-MODE - ((VISIBLE-IN-STATUS-P NIL))) -(DEFINE-CONFIGURATION NYXT/HELP-MODE:HELP-MODE - ((VISIBLE-IN-STATUS-P NIL))) -(DEFINE-CONFIGURATION NYXT/WEB-MODE:WEB-MODE - ((VISIBLE-IN-STATUS-P NIL))) -(DEFINE-CONFIGURATION NYXT/VI-MODE:USER-VI-NORMAL-MODE - ((VISIBLE-IN-STATUS-P NIL))) -(DEFINE-CONFIGURATION BUFFER - ((CURRENT-ZOOM-RATIO 0.75))) diff --git a/.config/nyxt/config.lisp b/.config/nyxt/config.lisp new file mode 100644 index 0000000..203b947 --- /dev/null +++ b/.config/nyxt/config.lisp @@ -0,0 +1,215 @@ +(in-package #:nyxt-user) ; While implicit, this allows SLY to know which package we are in. + +#+nyxt-3 (reset-asdf-registries) + +(defvar *web-buffer-modes* + '(:blocker-mode :force-https-mode + :reduce-tracking-mode + :user-script-mode :bookmarklets-mode) + "The modes to enable in any web-buffer by default. +Extension files (like dark-reader.lisp) are to append to this list. + +Why the variable? Because it's too much hassle copying it everywhere.") + +;; Create a function to launch mpv with given url +(defun mpv (url) + "MPV launches with given url using the fast profile." + (uiop:launch-program (list "mpv" "--profile=fast" url "&"))) + +;; Create a function to download videos with youtube-dl in alacritty +(defun youtube-dl (url) + "Download videos and audio with youtube-dl in alacritty for feedback" + (uiop:launch-program + (list "dlvideo" url))) + + +(define-configuration :web-buffer + ((default-modes (append (list :vi-normal-mode) %slot-value%)))) + +(define-configuration (:modable-buffer :prompt-buffer :editor-buffer) + "Set up Emacs keybindings everywhere possible. + +If you're the VI person, then use this: +(define-configuration :web-buffer + ((default-modes (append (list :vi-normal-mode) %slot-value%)))) + +You probably want to stay with CUA in :prompt-buffer, because it's too +weird using it with VI bindings. But if you're feeling risky, then: +(define-configuration :prompt-buffer + ((default-modes (append (list :vi-insert-mode) %slot-value%))))" + ((default-modes `(:vi-insert-mode ,@%slot-value%)))) + +(define-configuration :prompt-buffer + "Make the attribute widths adjust to the content in them. + +It's not exactly necessary on master, because there are more or less +intuitive default widths, but these are sometimes inefficient (and +note that I made this feature so I want to have it :P)." + ((dynamic-attribute-width-p t))) + +(define-configuration :prompt-buffer + ((hide-single-source-header-p + t + :doc "This is to hide the header is there's only one source. +There also used to be other settings to make prompt-buffer a bit +more minimalist, but those are internal APIs :("))) + +(define-configuration :web-buffer + "Basic modes setup for web-buffer." + ((default-modes `(,@*web-buffer-modes* ,@%slot-value%)))) + +(define-configuration :browser + "Set new buffer URL (a.k.a. start page, new tab page)." + ((default-new-buffer-url (quri:uri "nyxt:nyxt/mode/repl:repl")))) + +(define-configuration :nosave-buffer + "Enable proxy in nosave (private, incognito) buffers." + ((default-modes `(:proxy-mode ,@*web-buffer-modes* ,@%slot-value%)))) + +(define-configuration :hint-mode + "Set up QWERTY home row as the hint keys." + ((hints-alphabet "DSJKHLFAGNMXCWEIO"))) + + +(defmethod ffi-buffer-make :after ((buffer nyxt/renderer/gtk::gtk-buffer)) + "Setting WebKit-specific settings. +WARNING: Not exactly the best way to configure Nyxt, because it relies +on internal APIs and CFFI... + +See +https://webkitgtk.org/reference/webkit2gtk/stable/WebKitSettings.html +for the full list of settings you can tweak this way." + (when (slot-boundp buffer 'nyxt/renderer/gtk::gtk-object) + (let* ((settings (webkit:webkit-web-view-get-settings + (nyxt/renderer/gtk::gtk-object buffer)))) + (setf + ;; Resizeable textareas. It's not perfect, but still a cool feature to have. + (webkit:webkit-settings-enable-resizable-text-areas settings) t + ;; Write console errors/warnings to the shell, to ease debugging. + (webkit:webkit-settings-enable-write-console-messages-to-stdout settings) t + ;; "Inspect element" context menu option available at any moment. + (webkit:webkit-settings-enable-developer-extras settings) t + ;; Enable WebRTC. + (webkit:webkit-settings-enable-media-stream settings) t + ;; Use Cantarell-18 as the default font. + (webkit:webkit-settings-default-font-family settings) "VictorMono Nerd Font" + (webkit:webkit-settings-default-font-size settings) 16 + ;; Use Hack-17 as the monospace font. + (webkit:webkit-settings-monospace-font-family settings) "VictorMono Nerd Font" + (webkit:webkit-settings-default-monospace-font-size settings) 14 + ;; Use Unifont for pictograms. + (webkit:webkit-settings-pictograph-font-family settings) "Unifont"))) + ;; Set the view background to black. + (cffi:foreign-funcall + "webkit_web_view_set_background_color" + :pointer (g:pointer (nyxt/renderer/gtk:gtk-object buffer)) + ;; GdkRgba is simply an array of four doubles. + :pointer (cffi:foreign-alloc + :double + :count 4 + ;; red green blue alpha + :initial-contents '(0d0 0d0 0d0 1d0)))) + +;; (defmethod customize-instance ((input-buffer input-buffer) &key) +;; (disable-modes* 'nyxt/mode/emacs:emacs-mode input-buffer) +;; (enable-modes* 'nyxt/mode/vi:vi-normal-mode input-buffer)) + +;; (define-configuration browser +;; ((external-editor-program '("emacsclient -c")))) + +;; Let's create a function to hint videos, convert the url to a sting, and play them in MPV +(define-command hint-mpv nil + "Show a set of element hints, and copy the URL of the user inputted one." + (nyxt/mode/hint:query-hints "Copy element URL" + (lambda (nyxt/mode/hint::results) + ;; this converts the url to a string to be used in mpv + (let* + ((url + (format nil "~a" + (url (first nyxt/mode/hint::results))))) + ;; here we take that string and pipe it into mpv + (mpv url))))) + +;; Let's create a function to hint videos, convert the url to a sting, and download with ytdl +(define-command hint-ytdl nil + "Show a set of element hints, and copy the URL of the user inputted one." + (nyxt/mode/hint:query-hints "Copy element URL" + (lambda (nyxt/mode/hint::results) + ;; this converts the url to a string to be used in yt-dlp + (let* + ((url + (format nil "~a" + (url (first nyxt/mode/hint::results))))) + ;; here we take that string and pipe it into yt-dlp + (youtube-dl url))))) + +(define-configuration :web-buffer + "set better scroll distance" + ((scroll-distance 350))) + +(define-configuration :document-mode + "Add basic keybindings." + ((keyscheme-map + (keymaps:define-keyscheme-map + "custom" (list :import %slot-value%) + ;; If you want to have VI bindings overriden, just use + ;; `scheme:vi-normal' or `scheme:vi-insert' instead of + ;; `scheme:emacs'. + nyxt/keyscheme:vi-normal + (list "K" 'switch-buffer-next + "J" 'switch-buffer-previous + "b" 'switch-buffer + "d" 'delete-current-buffer + "D" 'delete-buffer + "r" 'reload-current-buffer + "R" 'reload-buffers + "v" 'hint-mpv + "V" 'hint-ytdl + "L" 'history-forwards + "H" 'history-backwards + "gv" :visual-mode + "C-i" :input-edit-mode))))) + +(defvar *my-search-engines* + (list + '("google" "https://google.com/search?q=~a" "https://google.com") + '("searx" "https://search.tfcconnection.org/search?q=~a" "https://search.tfcconnection.org/")) + "List of search engines.") + +(define-configuration context-buffer + "Go through the search engines above and make-search-engine out of them." + ((search-engines + (append + (mapcar (lambda (engine) (apply 'make-search-engine engine)) + *my-search-engines*) + %slot-default%)))) + +(define-nyxt-user-system-and-load "nyxt-user/search-engines" + :depends-on (:nx-search-engines) :components ("search-engines.lisp")) + +(define-nyxt-user-system-and-load "nyxt-user/dark-reader" + :components ("dark-reader.lisp") + :depends-on (:nx-dark-reader)) + + +;; (define-configuration :prompt-buffer-mode +;; "Add basic keybindings for prompt-buffer." +;; ((keyscheme-map +;; (keymaps:define-keyscheme-map +;; "custom" (list :import %slot-value%) +;; nyxt/keyscheme:emacs +;; (list "C-j" 'nyxt/mode/prompt-buffer:next-suggestion +;; "C-k" 'nyxt/mode/prompt-buffer:previous-suggestion))))) + + +;; (define-configuration buffer +;; ((search-engines +;; (list +;; (make-instance 'search-engine +;; :shortcut "s" +;; :search-url "https://search.tfcconnection.org/?q=~a" +;; :fallback-url "https://search.tfcconnection.org"))))) + +;;; Loading files from the same directory. +(define-nyxt-user-system-and-load nyxt-user/basic-config + :components ("status" "style")) diff --git a/.config/nyxt/dark-reader.lisp b/.config/nyxt/dark-reader.lisp new file mode 100644 index 0000000..9f3b3f7 --- /dev/null +++ b/.config/nyxt/dark-reader.lisp @@ -0,0 +1,10 @@ +(in-package #:nyxt-user) + +(define-configuration nx-dark-reader:dark-reader-mode + ((nxdr:brightness 80) + (nxdr:contrast 80) + (nxdr:text-color "white"))) + +;; Add dark-reader to default modes +;; (define-configuration web-buffer +;; ((default-modes `(nxdr:dark-reader-mode ,@%slot-value%)))) diff --git a/.config/nyxt/glyphs.lisp b/.config/nyxt/glyphs.lisp deleted file mode 100644 index b974208..0000000 --- a/.config/nyxt/glyphs.lisp +++ /dev/null @@ -1,24 +0,0 @@ -(in-package #:nyxt-user) - -;;allow setting glyphs -(define-configuration status-buffer - ((glyph-mode-presentation-p t))) - -;;various glyph settings with no additional configs -(define-configuration nyxt/force-https-mode:force-https-mode ((glyph "ϕ"))) -(define-configuration nyxt/blocker-mode:blocker-mode ((glyph "β"))) -(define-configuration nyxt/proxy-mode:proxy-mode ((glyph "π"))) -(define-configuration nyxt/reduce-tracking-mode:reduce-tracking-mode ((glyph "∅"))) -(define-configuration nyxt/certificate-exception-mode:certificate-exception-mode ((glyph "ɛ"))) -(define-configuration nyxt/style-mode:style-mode ((glyph "s"))) -(define-configuration nyxt/help-mode:help-mode ((glyph "?"))) - -;;configure web mode hints to home row and set glyph -(define-configuration nyxt/web-mode:web-mode - ((nyxt/web-mode:hints-alphabet "ASDFGHJKL") - (glyph "ω"))) - -;;auto-mode config and set glyph -(define-configuration nyxt/auto-mode:auto-mode - ((nyxt/auto-mode:prompt-on-mode-toggle t) - (glyph "α"))) diff --git a/.config/nyxt/hsplit.lisp b/.config/nyxt/hsplit.lisp new file mode 100644 index 0000000..2f49510 --- /dev/null +++ b/.config/nyxt/hsplit.lisp @@ -0,0 +1,25 @@ +(in-package #:nyxt-user) + +(define-panel-command hsplit-internal (&key (url (quri:render-uri (url (current-buffer))))) + (panel "*Duplicate panel*" :right) + "Duplicate the current buffer URL in the panel buffer on the right. + +A poor man's hsplit :)" + (setf (ffi-width panel) 550) + (run-thread "URL loader" + (sleep 0.3) + (buffer-load (quri:uri url) :buffer panel)) + "") + +(define-command-global close-all-panels () + "Close all the panel buffers there are." + (alexandria:when-let ((panels (nyxt/renderer/gtk::panel-buffers-right (current-window)))) + (delete-panel-buffer :window (current-window) :panels panels)) + (alexandria:when-let ((panels (nyxt/renderer/gtk::panel-buffers-left (current-window)))) + (delete-panel-buffer :window (current-window) :panels panels))) + +(define-command-global hsplit () + "Based on `hsplit-internal' above." + (if (nyxt/renderer/gtk::panel-buffers-right (current-window)) + (delete-all-panel-buffers (current-window)) + (hsplit-internal))) diff --git a/.config/nyxt/init.lisp b/.config/nyxt/init.lisp deleted file mode 100644 index e161ef8..0000000 --- a/.config/nyxt/init.lisp +++ /dev/null @@ -1,255 +0,0 @@ -(in-package #:nyxt-user) ; While implicit, this allows SLY to know which package we are in. - -(dolist (file (list (nyxt-init-file "slynk.lisp")))) -(dolist (file (list (nyxt-init-file "glyphs.lisp")))) - -(load-after-system :slynk (nyxt-init-file "slynk.lisp")) -;; (defvar *chris-prompt-keymap (make-keymap "chris-prompt-map")) -;; (define-key *chris-prompt-keymap* -;; "escape" 'cancel-input -;; "C-j" 'select-next -;; "C-k" 'select-previous) - -;; (define-mode chris-prompt-mode () -;; "Dummy mode for the custom key bindings in `*chris-prompt-keymap*'." -;; ((keymap-scheme (keymap:make-scheme -;; scheme:vi-insert *chris-prompt-keymap*)) -;; (visible-in-status-p nil))) - -;; Always restore history and don't ask -(define-configuration browser - ((session-restore-prompt :always-restore))) - -(define-configuration (prompt-buffer) - ((default-modes - (append - '(vi-insert-mode) - %slot-default%)))) - -(define-configuration browser - ((external-editor-program '("/usr/bin/emacsclient")))) - -;; Create a function to launch mpv with given url -(defun mpv (url) - "MPV launches with given url using the fast profile." - (uiop:launch-program (list "mpv" "--profile=fast" url "&"))) - -;; Create a function to download videos with youtube-dl in alacritty -(defun youtube-dl (url) - "Download videos and audio with youtube-dl in alacritty for feedback" - (uiop:launch-program - (list "alacritty" "-e" "yt-dlp" "-o ~/Videos/%(title)s.%(ext)s" url))) - -;; Let's create a function to hint videos, convert the url to a sting, and play them in MPV -(define-command hint-mpv (&key nyxt/web-mode::annotate-visible-only-p) - "Show a set of element hints, and copy the URL of the user inputted one." - (nyxt/web-mode:query-hints "Copy element URL" - (lambda (nyxt/web-mode::result) - ;; this converts the url to a string to be used in mpv - (let* - ((url - (format nil "~a" - (url (first nyxt/web-mode::result))))) - ;; here we take that string and pipe it into mpv - (mpv url))) - :annotate-visible-only-p - nyxt/web-mode::annotate-visible-only-p)) - -;; Let's create a function to hint videos, convert the url to a sting, and download with ytdl -(define-command hint-ytdl (&key nyxt/web-mode::annotate-visible-only-p) - "Show a set of element hints, and copy the URL of the user inputted one." - (nyxt/web-mode:query-hints "Copy element URL" - (lambda (nyxt/web-mode::result) - ;; this converts the url to a string to be used in yt-dlp - (let* - ((url - (format nil "~a" - (url (first nyxt/web-mode::result))))) - ;; here we take that string and pipe it into yt-dlp - (youtube-dl url))) - :annotate-visible-only-p - nyxt/web-mode::annotate-visible-only-p)) - -;; These are my own keys that are layered over vi-normal. A lot of these -;; are similar to qutebrowser. -(defvar *chris-keymap* (make-keymap "chris-map")) -(define-key *chris-keymap* - "K" 'switch-buffer-next - "J" 'switch-buffer-previous - "b" 'switch-buffer - "v" 'hint-mpv - "V" 'hint-ytdl - "d" 'delete-current-buffer - "D" 'delete-buffer - "r" 'reload-current-buffer - "R" 'reload-buffers) - -(define-mode chris-mode () - "Dummy mode for the custom key bindings in `*chris-keymap*'." - ((keymap-scheme - (keymap:make-scheme - scheme:vi-normal *chris-keymap*)) - (visible-in-status-p nil))) - -(define-configuration buffer - ((default-modes - (append - '(vi-normal-mode - auto-mode - reduce-tracking-mode - force-https-mode - chris-mode) - %slot-default%)))) - -(define-configuration buffer - ((search-engines - (list - (make-instance 'search-engine - :shortcut "s" - :search-url "https://search.tfcconnection.org/?q=~a" - :fallback-url "https://search.tfcconnection.org"))))) - -(define-configuration nyxt/vi-mode:vi-normal-mode - ((visible-in-status-p nil))) - -(define-configuration nyxt/vi-mode:vi-insert-mode - ((visible-in-status-p nil))) - -(define-configuration status-buffer - ((style - (str:concat - %slot-default% - (cl-css:css - '((body - :background "#282a36" - :color "#e2e4e5" - :line-height "1fr") - ("#container-vi" - :grid-template-columns "0px 30px 2fr 0px 240px") - ("#controls" - :background-color "#282a36" - :color "#f3f99d" - :width "0px" - :padding-left "0px" - :hidden) - ("#url" - :background-color "#282a36" - :color "#5af78e") - ("#modes" - :background-color "#282a36" - :color "#f3f99d") - ("#tabs" - :background-color "#282a36" - :color "#5af78e") - (".tab" - :background-color "#282a36" - :color "#9aedfe") - (".tab:hover" - :background-color "#282a36" - :color "#f1f1f0") - (".button" - :background-color "#282a36" - :color "#5af78e") - (".button:hover" - :background-color "#282a36" - :color "#f1f1f0") - (".arrow" - :width "0px" - :height "0px") - (".arrow-right" - :background-color "#282a36" - :color "#5af78e") - (".arrow-left" - :background-color "#282a36" - :color "#5af78e") - )))))) - -(define-configuration internal-buffer - ((style - (str:concat - %slot-default% - (cl-css:css - '((body - :background "#282a36" - :color "#e2e4e5") - (.button - :color "#e2e4e5"))))))) - -(define-configuration window - ((message-buffer-style - (str:concat - %slot-default% - (cl-css:css - '((body - :background-color "#282a36" - :color "#e2e4e5"))))))) - -(define-configuration prompt-buffer - ((style - (str:concat - %slot-default% - (cl-css:css - '((body - :background-color "#282a36" - :color "#e2e4e5") - ("#prompt-area-vi" - :background-color "#282a36" - :color "#57c7ff") - ("#prompt" - :background-color "#282a36" - :color "#e2e4e5") - ("#input" - :background-color "#282a36" - :color "#e2e4e5") - ("#suggestions" - :background-color "#282a36" - :color "#e2e4e5") - (.vi-insert-mode - :background-color "#282a36") - (.vi-normal-mode - :background-color "#282a36") - (.source - :margin-left "5px") - (.source-name - :background-color "#282a36" - :color "#e2e4e5") - (.source-content - :background-color "#282a36" - :color "#e2e4e5") - ;; (".source-content td" - ;; :background-color "#282a36" - ;; :color "#e2e4e5") - (".source-content th" - :background-color "#43454f" - :color "#e2e4e5") - ("#selection" - :background-color "#57c7ff" - :color "#34353e") - (.marked - :background-color "#5af78e" - :color "#34353e") - (.selected - :background-color "#57c7ff" - :color "#34353e") - (.button - :color "#e2e4e5"))))))) - - - -(in-package #:nyxt-user) ; While implicit, this allows SLY to know which package we are in. - -;; (load "~/quicklisp/setup.lisp") -;; (ql:quickload :slynk) - -;; (define-command-global start-slynk (&optional (slynk-port *swank-port*)) -;; "Start a Slynk server that can be connected to, for instance, in -;; Emacs via SLY. - -;; Warning: This allows Nyxt to be controlled remotely, that is, to execute -;; arbitrary code with the privileges of the user running Nyxt. Make sure -;; you understand the security risks associated with this before running -;; this command." -;; (slynk:create-server :port slynk-port :dont-close t) -;; (echo "Slynk server started at port ~a" slynk-port)) - -;; (start-slynk) diff --git a/.config/nyxt/search-engines.lisp b/.config/nyxt/search-engines.lisp new file mode 100644 index 0000000..e3db16d --- /dev/null +++ b/.config/nyxt/search-engines.lisp @@ -0,0 +1,20 @@ +(in-package #:nyxt-user) + +(define-configuration (buffer web-buffer) + ((search-engines (list (engines:google :shortcut "gmaps" + :object :maps) + (engines:wordnet :shortcut "wn" + :show-word-frequencies t) + (engines:google :shortcut "g" + :safe-search nil) + (engines:duckduckgo :theme :terminal + :help-improve-duckduckgo nil + :homepage-privacy-tips nil + :privacy-newsletter nil + :newsletter-reminders nil + :install-reminders nil + :install-duckduckgo nil) + (engines:searx :base-search-url "https://search.tfcconnection.org/search?q=~a" + :request-method :get + :infinite-scroll t + :query-title t))))) diff --git a/.config/nyxt/slynk.lisp b/.config/nyxt/slynk.lisp deleted file mode 100644 index 4588dd2..0000000 --- a/.config/nyxt/slynk.lisp +++ /dev/null @@ -1,17 +0,0 @@ -(in-package #:nyxt-user) ; While implicit, this allows SLY to know which package we are in. - -(load "~/quicklisp/setup.lisp") -(ql:quickload :slynk) - -(define-command-global start-slynk (&optional (slynk-port *swank-port*)) - "Start a Slynk server that can be connected to, for instance, in -Emacs via SLY. - -Warning: This allows Nyxt to be controlled remotely, that is, to execute -arbitrary code with the privileges of the user running Nyxt. Make sure -you understand the security risks associated with this before running -this command." - (slynk:create-server :port slynk-port :dont-close t) - (echo "Slynk server started at port ~a" slynk-port)) - -(start-slynk) diff --git a/.config/nyxt/status.lisp b/.config/nyxt/status.lisp new file mode 100644 index 0000000..f7323d6 --- /dev/null +++ b/.config/nyxt/status.lisp @@ -0,0 +1,23 @@ +(in-package #:nyxt-user) + +;;allow setting glyphs +(define-configuration status-buffer + ((glyph-mode-presentation-p t))) + +;; ;;various glyph settings with no additional configs +(define-configuration :force-https-mode ((glyph "ϕ"))) +(define-configuration :blocker-mode ((glyph "β"))) +(define-configuration :proxy-mode ((glyph "π"))) +(define-configuration :reduce-tracking-mode ((glyph "∅"))) +(define-configuration :certificate-exception-mode ((glyph "ɛ"))) +(define-configuration :style-mode ((glyph "s"))) +(define-configuration :user-script-mode ((glyph "ω"))) + +(define-configuration status-buffer + ((style (str:concat %slot-value% + (theme:themed-css (theme *browser*) + ;; See the `describe-class' of `status-buffer' to + ;; understand what to customize + '("#controls" + :display "none" + :important)))))) diff --git a/.config/nyxt/style.lisp b/.config/nyxt/style.lisp new file mode 100644 index 0000000..1404f39 --- /dev/null +++ b/.config/nyxt/style.lisp @@ -0,0 +1,73 @@ +(in-package #:nyxt-user) ; While implicit, this allows SLY to know which package we are in. + +(setf (uiop:getenv "GTK_THEME") "Adwaita:dark") + +(define-configuration browser + "Configuring my reddish theme." + ((theme (make-instance + 'theme:theme + :background-color "#282a36" + :on-background-color "#e2e4e5" + :accent-color "#5af78e" + :on-accent-color "#282a36" + :accent-alt-color "#9aedfe" + :on-accent-alt-color "#282a36" + :warning-color "#ff5c57" + :on-warning-color "#e2e4e5" + :primary-color "#43454f" + :on-primary-color "#57c7ff" + :on-secondary-color "#f3f99d" + :secondary-color "#282a36" + :font-family "VictorMono Nerd Font")))) + +(define-configuration :dark-mode + "Dark-mode is a simple mode for simple HTML pages to color those in a darker palette. + +I don't like the default gray-ish colors, though. Thus, I'm overriding +those to be a bit more laconia-like. + +I'm not using this mode, though: I have nx-dark-reader." + ((style + (theme:themed-css (theme *browser*) + `(* + :background-color ,(if (theme:dark-p theme:theme) + theme:background + theme:on-background) + "!important" + :background-image none "!important" + :color ,(if (theme:dark-p theme:theme) + theme:on-background + theme:background) + "!important") + `(a + :background-color ,(if (theme:dark-p theme:theme) + theme:background + theme:on-background) + "!important" + :background-image none "!important" + :color ,theme:primary "!important"))))) + +(define-configuration :hint-mode + ((style + (theme:themed-css (theme *browser*) + `(".nyxt-hint" :background-color ,theme:accent :color + ,theme:on-accent :font-family "monospace,monospace" :padding + "0px 0.3em" :border-color ,theme:primary :border-radius "0.4em" + :border-width "0.2em" :border-style "solid" :z-index 2147483647) + `(".nyxt-hint.nyxt-mark-hint" :background-color ,theme:secondary :color + ,theme:on-secondary :font-weight "bold") + `(".nyxt-hint.nyxt-select-hint" :background-color ,theme:on-primary :color + ,theme:primary) + `(".nyxt-hint.nyxt-match-hint" :padding "0px" :border-style "none" :opacity + "0.5") + `(".nyxt-element-hint" :background-color ,theme:on-primary))))) + +;; (define-configuration :buffer +;; ((style +;; (theme:themed-css (theme *browser*) +;; `("h1,h2,h3,h4,h5,h6" :color ,theme:on-primary) +;; `(.button :background-color ,theme:primary +;; :color ,theme:on-primary +;; :border-radius "0.6em") +;; `(.link :color ,theme:on-primary) +;; `(a :color ,theme:on-primary)))) diff --git a/.config/nyxt/unpdf.lisp b/.config/nyxt/unpdf.lisp new file mode 100644 index 0000000..8b46457 --- /dev/null +++ b/.config/nyxt/unpdf.lisp @@ -0,0 +1,86 @@ +(in-package #:nyxt-user) + +;; I'm definining a new scheme to redirect PDF requests to. What it does is: +;; - Get the original file (if the URL is a filesystem path, simply use it). +;; - Save it to disk (if remote). +;; - Run pdftotext over the file. +;; - Display pdftotext output in a nice HTML page with interlinkable +;; page numbers and page contents as
tags. +(define-internal-scheme "unpdf" + (lambda (url buffer) + (let* ((url (quri:uri url)) + (original-url (quri:uri (quri:url-decode (quri:uri-path url)))) + (local-p (or (null (quri:uri-scheme original-url)) + (string= "file" (quri:uri-scheme original-url)))) + (original-content (unless local-p + (dex:get (quri:render-uri original-url) :force-binary t)))) + (flet ((display-pdf-contents (file) + (if (uiop:file-exists-p file) + (let ((pages (ignore-errors + (uiop:split-string + (uiop:run-program `("pdftotext" "-nodiag" ,(uiop:native-namestring file) "-") + :output '(:string :stripped t)) + :separator '(#\Page))))) + (spinneret:with-html-string + (:head + (:style (style buffer)) + ;; A class to override thecolors. + (:style (theme:themed-css (theme *browser*) + #+(or nyxt-2 nyxt-3-pre-release-1) + (.override + :background-color theme:background + :color theme:on-background + :font-size "150%" + :line-height "150%") + #+(and nyxt-3 (not (or nyxt-2 nyxt-3-pre-release-1))) + `(.override + :background-color ,theme:background + :color ,theme:on-background + :font-size "150%" + :line-height "150%")))) + (loop for page in pages + for number from 1 + unless (uiop:emptyp page) + do (:section + :id (princ-to-string number) + (:h2.override (:a :href (format nil "#~d" number) + (princ-to-string number))) + (:pre.override (or page "")))))) + ""))) + (if local-p + (display-pdf-contents (pathname (quri:uri-path original-url))) + (uiop:with-temporary-file (:pathname path :type "pdf" :keep t) + (log:debug "Temp file for ~a is ~a" url path) + (alexandria:write-byte-vector-into-file + (coerce original-content '(vector (unsigned-byte 8))) path :if-exists :supersede) + (display-pdf-contents path)))))) + :local-p t) + +(define-command-global unpdf-download-this () + "A helper for unpdf: pages to download the original PDF to the regular destination. + +Unpdf redirects all requests, even those that you need to read +elsewhere, thus I need this command." + (let* ((buffer (current-buffer)) + (url (url buffer))) + (if (string= "unpdf" (quri:uri-scheme url)) + (ffi-buffer-download buffer (quri:uri-path url)) + ;; I need to turn it into a mode someday... + (echo-warning "This command is for unpdf: pages only, it's useless elsewhere!")))) + +(defun redirect-pdf (request-data) + (if (and (toplevel-p request-data) + (uiop:string-prefix-p "application/pdf" (mime-type request-data))) + ;; I should somehow prompt about downloading instead... + (progn + (echo "Redirecting to the unpdf URL...") + (make-buffer-focus :url (quri:uri (str:concat "unpdf:" (render-url (url request-data))))) + ;; Return nil to prevent Nyxt from downloading this PDF. + nil) + request-data)) + +(define-configuration :web-buffer + ((request-resource-hook (hooks:add-hook %slot-value% 'redirect-pdf)))) + +(define-configuration nyxt/mode/file-manager:file-source + ((supported-media-types `("pdf" ,@%slot-value%)))) diff --git a/.config/nyxt/userscripts/block-cosmetic.js b/.config/nyxt/userscripts/block-cosmetic.js new file mode 100644 index 0000000..9024007 --- /dev/null +++ b/.config/nyxt/userscripts/block-cosmetic.js @@ -0,0 +1,7970 @@ +// ==UserScript== +// @name AdBlock Script for WebView +// @name:zh-CN 套壳油猴的广告拦截脚本 +// @author Lemon399 +// @version 2.5.7 +// @description Parse ABP Cosmetic rules to CSS and apply it. +// @description:zh-CN 将 ABP 中的元素隐藏规则转换为 CSS 使用 +// @resource jiekouAD https://raw.fastgit.ixmu.net/damengzhu/banad/main/jiekouAD.txt +// @resource CSSRule https://raw.fastgit.ixmu.net/damengzhu/abpmerge/main/CSSRule.txt +// @match *://*/* +// @run-at document-start +// @grant unsafeWindow +// @grant GM_registerMenuCommand +// @grant GM.registerMenuCommand +// @grant GM_unregisterMenuCommand +// @grant GM.unregisterMenuCommand +// @grant GM_getValue +// @grant GM.getValue +// @grant GM_deleteValue +// @grant GM.deleteValue +// @grant GM_setValue +// @grant GM.setValue +// @grant GM_addStyle +// @grant GM.addStyle +// @grant GM_xmlhttpRequest +// @grant GM.xmlHttpRequest +// @grant GM_getResourceText +// @grant GM.getResourceText +// @grant GM_download +// @grant GM.download +// @grant GM_listValues +// @grant GM.listValues +// @namespace https://lemon399-bitbucket-io.vercel.app/ +// @source https://gitee.com/lemon399/tampermonkey-cli/tree/master/projects/abp_parse +// @source https://bitbucket.org/lemon399/tampermonkey-cli/src/master/projects/abp_parse/ +// @connect raw.fastgit.ixmu.net +// @copyright GPL-3.0 +// @license GPL-3.0 +// ==/UserScript== + +(function () { + "use strict"; + + const $presets = { + defaultRules: ` + ! 不支持的规则和开头为 ! 的行会忽略 + ! + ! 由于语法限制,此处规则中 + ! 一个反斜杠需要改成两个,像这样 \\ + + `, + userConfig: { + css: `{ + display: none !important; + width: 0 !important; + height: 0 !important; + }`, + timeout: 10000, + headTimeout: 2000, + tryCount: 5, + tryTimeout: 500, + }, + onlineRules: [ + { + 标识: `jiekouAD`, + 地址: `https://raw.fastgit.ixmu.net/damengzhu/banad/main/jiekouAD.txt`, + 在线更新: true, + 筛选后存储: true, + }, + { + 标识: `CSSRule`, + 地址: `https://raw.fastgit.ixmu.net/damengzhu/abpmerge/main/CSSRule.txt`, + 在线更新: true, + 筛选后存储: false, + }, + ], + }; + + /* ==UserConfig== +配置: + css: + title: 隐藏 CSS 规则 + description: 隐藏广告使用的 CSS 规则 + type: textarea + rows: 7 + default: |- + { + display: none !important; + width: 0 !important; + height: 0 !important; + } + timeout: + title: 规则下载超时 + description: 更新规则时,规则下载超时时间 + type: number + default: 10000 + min: 0 + unit: 毫秒 + headTimeout: + title: 获取规则信息超时 + description: 更新规则时,获取规则信息 (HEAD 请求) 超时时间 + type: number + default: 2000 + min: 0 + unit: 毫秒 + tryCount: + title: CSS 注入尝试次数 + description: 某些框架会重建页面,需要多次注入,只有检测到 CSS 不存在时才会尝试再次注入 + type: number + default: 5 + min: 0 + unit: 次 + tryTimeout: + title: CSS 注入尝试间隔 + description: 两次注入尝试的间隔时间 + type: number + default: 500 + min: 100 + unit: 毫秒 +==/UserConfig== */ + + /* eslint-disable no-redeclare, no-unused-vars, require-yield */ + /* global GM_info, GM, unsafeWindow, GM_registerMenuCommand, GM_unregisterMenuCommand, GM_getValue, GM_deleteValue, GM_setValue, GM_addStyle, GM_xmlhttpRequest, GM_getResourceText, GM_download, GM_listValues */ + let $vcls = [], + $vcla = false; + + const $polyfills = { + // 以下 polyfills 修改自 NullMonkey,使用 MPL-2.0 发布 + /* This Source Code Form is subject to the terms of the + * Mozilla Public License, v. 2.0. If a copy of the MPL + * was not distributed with this file, You can obtain + * one at https://mozilla.org/MPL/2.0/. */ + GM_info: + typeof GM_info == "object" + ? GM_info + : { + script: { + author: "Lemon399", + copyright: "GPL-3.0", + description: "Parse ABP Cosmetic rules to CSS and apply it.", + downloadURL: null, + excludes: [], + excludeMatches: [], + grant: [ + "unsafeWindow", + "GM_registerMenuCommand", + "GM.registerMenuCommand", + "GM_unregisterMenuCommand", + "GM.unregisterMenuCommand", + "GM_getValue", + "GM.getValue", + "GM_deleteValue", + "GM.deleteValue", + "GM_setValue", + "GM.setValue", + "GM_addStyle", + "GM.addStyle", + "GM_xmlhttpRequest", + "GM.xmlHttpRequest", + "GM_getResourceText", + "GM.getResourceText", + "GM_download", + "GM.download", + "GM_listValues", + "GM.listValues", + ], + homepage: null, + icon: null, + icon64: null, + includes: [], + matches: ["*://*/*"], + name: "AdBlock Script for WebView", + namespace: "https://lemon399-bitbucket-io.vercel.app/", + noframes: false, + "run-at": "document-start", + resources: [ + { + name: "jiekouAD", + url: "https://raw.fastgit.ixmu.net/damengzhu/banad/main/jiekouAD.txt", + }, + { + name: "CSSRule", + url: "https://raw.fastgit.ixmu.net/damengzhu/abpmerge/main/CSSRule.txt", + }, + ], + supportURL: null, + unwrap: false, + updateURL: null, + version: "2.5.7", + webRequest: null, + }, + scriptWillUpdate: false, + }, + parseValue: function parseValue(stor) { + const v = + typeof stor == "string" && stor.startsWith("[") && stor.endsWith("]") + ? JSON.parse(stor)[0] + : void 0; + return v === "__$NaN" + ? NaN + : v === "__$UdF" + ? undefined + : v === "__$FnT" + ? Infinity + : v === "__$XnT" + ? -Infinity + : v; + }, + unsafeWindow: typeof unsafeWindow == "object" ? unsafeWindow : window, + GM_registerMenuCommand: + typeof GM_registerMenuCommand == "function" + ? GM_registerMenuCommand + : void 0, + GM_unregisterMenuCommand: + typeof GM_unregisterMenuCommand == "function" + ? GM_unregisterMenuCommand + : void 0, + GM_getValue: + typeof GM_getValue == "function" + ? GM_getValue + : function DM_getValue(k, d) { + const stor = window.localStorage.getItem( + "$DMValue$AdBlock Script for WebView$" + k + ); + return typeof stor == "string" && + stor.startsWith("[") && + stor.endsWith("]") + ? $polyfills.parseValue(stor) + : d; + }, + GM_deleteValue: + typeof GM_deleteValue == "function" + ? GM_deleteValue + : function DM_deleteValue(k) { + window.localStorage.removeItem( + "$DMValue$AdBlock Script for WebView$" + k + ); + }, + GM_setValue: + typeof GM_setValue == "function" + ? GM_setValue + : function DM_setValue(k, v) { + const packed = JSON.stringify([ + typeof v == "function" + ? v.toString() + : typeof v == "number" && isNaN(v) + ? "__$NaN" + : typeof v == "number" && !isFinite(v) + ? v > 0 + ? "__$FnT" + : "__$XnT" + : typeof v == "undefined" + ? "__$UdF" + : typeof v == "bigint" + ? v.toString() + : v, + ]); + $vcls.forEach((vcla, i) => { + if (vcla[0] === k) { + const o = $polyfills.GM_getValue(k); + vcla[1].call( + { + id: i, + key: k, + cb: vcla[1], + }, + k, + o, + v, + false + ); + } + }); + window.localStorage.setItem( + "$DMValue$AdBlock Script for WebView$" + k, + packed + ); + }, + GM_addStyle: + typeof GM_addStyle == "function" + ? GM_addStyle + : function DM_addStyle(c) { + const s = document.createElement("style"); + s.innerText = c; + ( + document.head || + document.body || + document.documentElement + ).appendChild(s); + return s; + }, + GM_xmlhttpRequest: + typeof GM_xmlhttpRequest == "function" ? GM_xmlhttpRequest : void 0, + GM_getResourceText: + typeof GM_getResourceText == "function" ? GM_getResourceText : void 0, + GM_download: + // 以下浏览器的 GM_download 不支持 blob: 需要使用 Polyfill + typeof GM_download == "function" && + // X 浏览器 + !GM_download.toString().includes("mbrowser.GM_download") && + // Via 浏览器 + !GM_download.toString().includes("via_gm.download") && + // MDM 浏览器 + !( + typeof window.moe == "object" && + typeof window.moe.download == "function" + ) && + // 海阔世界 / 嗅觉 + !GM_download.toString().includes("window.open") && + // Rains 浏览器 + !Array.isArray(GM_download.toString().match(/load\(\) {};$/)) + ? GM_download + : function DM_download(o, n) { + var _a; + const a = document.createElement("a"); + if (typeof o == "object") { + a.href = o.url; + a.download = o.name; + a.onclick = (_a = o.onload) !== null && _a !== void 0 ? _a : null; + } else { + a.href = o; + a.download = n !== null && n !== void 0 ? n : ""; + } + a.style.cssText = "position:absolute;top:-100%"; + document.body.appendChild(a); + setTimeout(() => { + a.click(); + a.remove(); + }, 0); + return { + abort: () => void 0, + }; + }, + GM_listValues: + typeof GM_listValues == "function" + ? GM_listValues + : function DM_listValues() { + const a = []; + for (let i = 0; i < window.localStorage.length; i++) { + const key = window.localStorage.key(i); + if ( + key === null || key === void 0 + ? void 0 + : key.startsWith("$DMValue$AdBlock Script for WebView$") + ) + a.push(key.replace("$DMValue$AdBlock Script for WebView$", "")); + } + return a; + }, + GM: + typeof GM == "object" + ? GM + : { + getValue: function DM_getValue4(...args) { + return Promise.resolve($polyfills.GM_getValue(...args)); + }, + deleteValue: function DM_deleteValue4(...args) { + return Promise.resolve($polyfills.GM_deleteValue(...args)); + }, + setValue: function DM_setValue4(...args) { + return Promise.resolve($polyfills.GM_setValue(...args)); + }, + addStyle: function DM_addStyle4(...args) { + return Promise.resolve($polyfills.GM_addStyle(...args)); + }, + download: function DM_download4(...args) { + return Promise.resolve($polyfills.GM_download(...args)); + }, + listValues: function DM_listValues4(...args) { + return Promise.resolve($polyfills.GM_listValues(...args)); + }, + }, + // polyfills 结束 + }; + + (function (preset, tm) { + "use strict"; + + /****************************************************************************** + Copyright (c) Microsoft Corporation. + Permission to use, copy, modify, and/or distribute this software for any + purpose with or without fee is hereby granted. + THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH + REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY + AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, + INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM + LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR + OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR + PERFORMANCE OF THIS SOFTWARE. + ***************************************************************************** */ + /* global Reflect, Promise, SuppressedError, Symbol */ + function __awaiter(thisArg, _arguments, P, generator) { + function adopt(value) { + return value instanceof P + ? value + : new P(function (resolve) { + resolve(value); + }); + } + return new (P || (P = Promise))(function (resolve, reject) { + function fulfilled(value) { + try { + step(generator.next(value)); + } catch (e) { + reject(e); + } + } + function rejected(value) { + try { + step(generator["throw"](value)); + } catch (e) { + reject(e); + } + } + function step(result) { + result.done + ? resolve(result.value) + : adopt(result.value).then(fulfilled, rejected); + } + step((generator = generator.apply(thisArg, _arguments || [])).next()); + }); + } + typeof SuppressedError === "function" + ? SuppressedError + : function (error, suppressed, message) { + var e = new Error(message); + return ( + (e.name = "SuppressedError"), + (e.error = error), + (e.suppressed = suppressed), + e + ); + }; + var _a, _b, _c; + function makeRuleBox() { + return { + black: [], + white: [], + }; + } + const data = { + isFrame: tm.unsafeWindow.self !== tm.unsafeWindow.top, + isClean: false, + disabled: false, + saved: false, + update: true, + updating: false, + alertLog: false, + receivedRules: "", + customRules: + preset.defaultRules + + ("\n" + + ((_c = + (_b = + (_a = tm.GM_info.script) === null || _a === void 0 + ? void 0 + : _a.options) === null || _b === void 0 + ? void 0 + : _b.comment) !== null && _c !== void 0 + ? _c + : "")), + allRules: "", + genHideCss: "", + genExtraCss: "", + spcHideCss: "", + spcExtraCss: "", + selectors: makeRuleBox(), + extSelectors: makeRuleBox(), + styles: makeRuleBox(), + extStyles: makeRuleBox(), + bRules: [], + appliedLevel: 0, + appliedCount: 0, + mutex: "__lemon__abp__parser__$__", + preset: preset.userConfig.css, + timeout: preset.userConfig.timeout, + headTimeout: preset.userConfig.headTimeout, + tryCount: preset.userConfig.tryCount, + tryTimeout: preset.userConfig.tryTimeout, + }; + const defaultValues = { + get black() { + return ""; + }, + get rules() { + return {}; + }, + get css() { + return { + needUpdate: true, + genHideCss: "", + genExtraCss: "", + spcHideCss: "", + spcExtraCss: "", + }; + }, + get time() { + return "0/0/0 0:0:0"; + }, + get etags() { + return {}; + }, + get brules() { + return []; + }, + get hash() { + return ""; + }, + }; + const values = { + black(value) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + if (typeof value == "undefined") { + const arrStr = yield gmValue( + "get", + false, + "ajs_disabled_domains", + defaultValues.black + ); + return typeof arrStr == "string" && arrStr.length > 0 + ? arrStr.split(",") + : []; + } else { + return yield gmValue( + "set", + false, + "ajs_disabled_domains", + (_a = + value === null || value === void 0 + ? void 0 + : value.join()) !== null && _a !== void 0 + ? _a + : defaultValues.black + ); + } + }); + }, + rules(value) { + return __awaiter(this, void 0, void 0, function* () { + return typeof value == "undefined" + ? yield gmValue( + "get", + true, + "ajs_saved_abprules", + defaultValues.rules + ) + : yield gmValue( + "set", + true, + "ajs_saved_abprules", + value !== null && value !== void 0 + ? value + : defaultValues.rules + ); + }); + }, + css(value, host = location.hostname) { + return __awaiter(this, void 0, void 0, function* () { + return typeof value == "undefined" + ? yield gmValue( + "get", + true, + `ajs_saved_styles_${host}`, + defaultValues.css + ) + : yield gmValue( + "set", + true, + `ajs_saved_styles_${host}`, + value !== null && value !== void 0 ? value : defaultValues.css + ); + }); + }, + time(value) { + return __awaiter(this, void 0, void 0, function* () { + return typeof value == "undefined" + ? yield gmValue("get", false, "ajs_rules_ver", defaultValues.time) + : yield gmValue( + "set", + false, + "ajs_rules_ver", + value !== null && value !== void 0 + ? value + : defaultValues.time + ); + }); + }, + etags(value) { + return __awaiter(this, void 0, void 0, function* () { + return typeof value == "undefined" + ? yield gmValue( + "get", + true, + "ajs_rules_etags", + defaultValues.etags + ) + : yield gmValue( + "set", + true, + "ajs_rules_etags", + value !== null && value !== void 0 + ? value + : defaultValues.etags + ); + }); + }, + brules(value) { + return __awaiter(this, void 0, void 0, function* () { + return typeof value == "undefined" + ? yield gmValue( + "get", + true, + "ajs_modifier_rules", + defaultValues.brules + ) + : yield gmValue( + "set", + true, + "ajs_modifier_rules", + value !== null && value !== void 0 + ? value + : defaultValues.brules + ); + }); + }, + hash(value) { + return __awaiter(this, void 0, void 0, function* () { + return typeof value == "undefined" + ? yield gmValue( + "get", + false, + "ajs_custom_hash", + defaultValues.hash + ) + : yield gmValue( + "set", + false, + "ajs_custom_hash", + value !== null && value !== void 0 + ? value + : defaultValues.hash + ); + }); + }, + }, + menus = { + disable: { + id: undefined, + text() { + return __awaiter(this, void 0, void 0, function* () { + return `在此域名${data.disabled ? "启用" : "禁用"}拦截`; + }); + }, + }, + update: { + id: undefined, + text() { + return __awaiter(this, void 0, void 0, function* () { + const time = yield values.time(); + return data.updating + ? "正在更新..." + : `点击更新 ${ + (time === null || time === void 0 + ? void 0 + : time.slice(0, 1)) === "0" + ? "未知时间" + : time + }`; + }); + }, + }, + count: { + id: undefined, + text() { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + let cssCount = ""; + if (!data.disabled) { + if ((data.appliedLevel & 1) == 0) + cssCount += data.genHideCss + data.genExtraCss; + if ((data.appliedLevel & 2) == 0) + cssCount += data.spcHideCss + data.spcExtraCss; + } + return data.isClean + ? `已清空,点击刷新${data.disabled ? "网页" : "重新加载规则"}` + : data.disabled + ? "点击清空存储规则" + : `点击清空 ${ + data.saved + ? (_b = + "CSS " + + ((_a = cssCount.match(/{/g)) === null || _a === void 0 + ? void 0 + : _a.length)) !== null && _b !== void 0 + ? _b + : "未知" + : "规则 " + + data.appliedCount + + "/" + + data.allRules.split("\n").length + }`; + }); + }, + }, + export: { + id: undefined, + text() { + return __awaiter(this, void 0, void 0, function* () { + return "下载统计报告"; + }); + }, + }, + }; + function gmChooser(gm1, gm4) { + const gm1dm = + gm1 === null || gm1 === void 0 ? void 0 : gm1.name.startsWith("DM_"); + const gm4dm = + gm4 === null || gm4 === void 0 ? void 0 : gm4.name.startsWith("DM_"); + if (gm1dm !== gm4dm) { + return gm1dm ? gm4 : gm1; + } else { + return gm1; + } + } + function gmMenu(name, cb) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const id = + (_a = menus[name].id) !== null && _a !== void 0 ? _a : undefined; + const gmr = gmChooser( + tm.GM_registerMenuCommand, + tm.GM === null || tm.GM === void 0 + ? void 0 + : tm.GM.registerMenuCommand + ); + const gmu = gmChooser( + tm.GM_unregisterMenuCommand, + tm.GM === null || tm.GM === void 0 + ? void 0 + : tm.GM.unregisterMenuCommand + ); + if (typeof gmr != "function" || data.isFrame) return; + if (typeof id != "undefined" && typeof gmu == "function") { + menus[name].id = undefined; + yield gmu(id); + } + if (typeof cb == "function") { + menus[name].id = yield gmr(yield menus[name].text(), cb); + } + }); + } + function gmValue(action, json, key, value) { + var _a, _b, _c, _d; + return __awaiter(this, void 0, void 0, function* () { + switch (action) { + case "get": + try { + let v = + (_a = gmChooser( + tm.GM_getValue, + tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.getValue + )) === null || _a === void 0 + ? void 0 + : _a(key, json ? JSON.stringify(value) : value); + v = v instanceof Promise ? yield v : v; + return Promise.resolve( + json && typeof v == "string" ? JSON.parse(v) : v + ); + } catch (error) { + return Promise.resolve(value); + } + case "set": + try { + return value === null || value === undefined + ? Promise.resolve( + (_b = gmChooser( + tm.GM_deleteValue, + tm.GM === null || tm.GM === void 0 + ? void 0 + : tm.GM.deleteValue + )) === null || _b === void 0 + ? void 0 + : _b(key) + ) + : Promise.resolve( + (_c = gmChooser( + tm.GM_setValue, + tm.GM === null || tm.GM === void 0 + ? void 0 + : tm.GM.setValue + )) === null || _c === void 0 + ? void 0 + : _c(key, json ? JSON.stringify(value) : value) + ); + } catch (error) { + Promise.reject( + (_d = gmChooser( + tm.GM_deleteValue, + tm.GM === null || tm.GM === void 0 + ? void 0 + : tm.GM.deleteValue + )) === null || _d === void 0 + ? void 0 + : _d(key) + ); + } + break; + } + }); + } + function getUserConfig(prop) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + { + return (_a = yield gmValue("get", false, `配置.${prop}`)) !== null && + _a !== void 0 + ? _a + : preset.userConfig[prop]; + } + }); + } + function addStyle(css, pass = 0) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + if (pass >= data.tryCount) return; + const el = yield (_a = gmChooser( + tm.GM_addStyle, + tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.addStyle + )) === null || _a === void 0 + ? void 0 + : _a(css); + if (!el || !document.documentElement.contains(el)) { + setTimeout(() => { + addStyle(css, pass + 1); + }, data.tryTimeout); + } + }); + } + function promiseXhr(details) { + return __awaiter(this, void 0, void 0, function* () { + let loaded = false; + const gmXhr = gmChooser( + tm.GM_xmlhttpRequest, + tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.xmlHttpRequest + ); + if (typeof gmXhr != "function") + return Promise.reject({ + error: "noxhr", + }); + return yield new Promise((resolve, reject) => { + gmXhr( + Object.assign( + { + onload(e) { + loaded = true; + resolve(e); + }, + onabort(e) { + loaded = true; + reject({ + error: "abort", + resp: e, + }); + }, + onerror(e) { + loaded = true; + reject({ + error: "error", + resp: e, + }); + }, + ontimeout(e) { + loaded = true; + reject({ + error: "timeout", + resp: e, + }); + }, + onreadystatechange(e) { + // Via 浏览器超时中断,不给成功状态... + if ( + (e === null || e === void 0 ? void 0 : e.readyState) === 3 + ) { + setTimeout( + () => { + if (!loaded) + reject({ + error: "Via timeout", + resp: e, + }); + }, + details.method === "HEAD" + ? data.headTimeout + : data.timeout + ); + } + }, + timeout: + details.method === "HEAD" ? data.headTimeout : data.timeout, + }, + details + ) + ); + }); + }); + } + function getRuleFromResource(key) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + try { + return yield (_a = gmChooser( + tm.GM_getResourceText, + tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.getResourceText + )) === null || _a === void 0 + ? void 0 + : _a(key); + } catch (error) { + return null; + } + }); + } + function runOnce(key, func) { + if (key in tm.unsafeWindow) return Promise.reject(); + tm.unsafeWindow[key] = true; + return func(); + } + const downUrl = tm.GM_download; + function getSavedHosts(host) { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + const keys = + (_b = yield (_a = gmChooser( + tm.GM_listValues, + tm.GM === null || tm.GM === void 0 ? void 0 : tm.GM.listValues + )) === null || _a === void 0 + ? void 0 + : _a()) !== null && _b !== void 0 + ? _b + : []; + const domains = ( + Array.isArray(keys) + ? keys + : // Rains + Object.keys(keys) + ) + .filter((key) => key.startsWith("ajs_saved_styles_")) + .map((key) => key.replace("ajs_saved_styles_", "")); + return host ? domains.includes(host) : domains; + }); + } + const CRRE = + /^(\[\$domain=)?(~?[\w-]+(?:\.[\w-]+)*(?:\.[\w-]+|\.\*)(?:(?:,|\|)~?[\w-]+(?:\.[\w-]+)*(?:\.[\w-]+|\.\*))*)?(?:])?(#@?\$?\??#)([^\s^+].*)/, + BRRE = + /^(?:@@?)(?:\/(.*[^\\])\/|(\|\|?)?(https?:\/\/)?([^\s"<>`]+?[|^]?))?\$((?:(?:~?[\w-]+(?:=[^$]+)?|_+)(?:[^\\],|$))+)/, + CCRE = + /^\/\*\s(\d)(\|)?(.+?)\s\*\/\s((.+?)\s*{\s*[a-zA-Z-]+\s*:\s*.+})\s*$/, + BROpts = [ + "elemhide", + "ehide", + "specifichide", + "shide", + "generichide", + "ghide", + ], + CRFlags = ["##", "#@#", "#?#", "#@?#", "#$#", "#@$#", "#$?#", "#@$?#"], + styleBoxes = ["genHideCss", "genExtraCss", "spcHideCss", "spcExtraCss"], + dataBoxes = ["selectors", "extSelectors", "styles", "extStyles"]; + function bRuleSpliter(rule) { + const group = rule.match(BRRE); + if (!group) return null; + const [, regex, pipe, proto, body, option] = group, + options = option.split(","), + sepChar = "[^\\w\\.%-]", + anyChar = '(?:[^\\s"<>`]*)', + eh = hasSome(options, ["elemhide", "ehide"]), + sh = hasSome(options, ["specifichide", "shide"]), + gh = hasSome(options, ["generichide", "ghide"]); + let domains = []; + options.forEach((opt) => { + if (opt.startsWith("domain=")) { + domains = opt.split("=")[1].split("|"); + } + }); + let match = ""; + if (regex) { + match = regex; + } else if (body) { + match += pipe + ? proto + ? `^${proto}` + : `^https?://(?:[\\w-]+\\.)*?` + : `^${anyChar}`; + match += body + .replace(/[-\\$+.()[\]{}]/g, "\\$&") + .replace(/\^/g, "$^") + .replace(/\|$/, "$") + .replace(/\|/g, "\\|") + .replace(/\*$/g, "") + .replace(/\*/g, anyChar) + .replace(/\$\^$/, `(?:${sepChar}.*|$)`) + .replace(/\$\^/g, sepChar); + } else if (domains.length > 0) { + match = domains; + } + return { + rule, + match, + level: eh || (gh && sh) ? 3 : sh ? 2 : gh ? 1 : 0, + }; + } + function isBasicRule(rule) { + return BRRE.test(rule) && hasSome(rule, BROpts); + } + function bRuleParser(rule, url = location.href) { + return rule + ? (Array.isArray(rule.match) && domainChecker(rule.match)[0]) || + (!Array.isArray(rule.match) && new RegExp(rule.match).test(url)) + ? rule.level + : 0 + : 0; + } + function getEtag(header) { + var _a; + let result = null; + if (!header) return null; + [ + /[e|E][t|T]ag:\s(?:W\/)?"(\w+)"/, + // WebMonkey 系 + /[e|E][t|T]ag:\s\[(?:W\/)?"(\w+)"\]/, + // 书签地球 + /[e|E][t|T]ag=(?:W\/)?"(\w+)"/, + // 海阔世界 + /^(?:W\/)?"(\w+)"/, + ].forEach((re) => { + result !== null && result !== void 0 + ? result + : (result = header.match(re)); + }); + return (_a = + result === null || result === void 0 ? void 0 : result[1]) !== null && + _a !== void 0 + ? _a + : null; + } + function domainChecker(domains) { + const results = [], + invResults = [], + currDomain = location.hostname, + urlSuffix = /\.+?[\w-]+$/.exec(currDomain); + let totalResult = [0, false], + black = false, + white = false, + match = false; + domains.forEach((domain) => { + const invert = domain[0] === "~"; + if (invert) domain = domain.slice(1); + if (domain.endsWith(".*") && Array.isArray(urlSuffix)) { + domain = domain.replace(".*", urlSuffix[0]); + } + const result = currDomain.endsWith(domain); + if (invert) { + if (result) white = true; + invResults.push([domain.length, !result]); + } else { + if (result) black = true; + results.push([domain.length, result]); + } + }); + if (results.length > 0 && !black) { + match = false; + } else if (invResults.length > 0 && !white) { + match = true; + } else { + results.forEach((r) => { + if (r[0] >= totalResult[0] && r[1]) { + totalResult = r; + } + }); + invResults.forEach((r) => { + if (r[0] >= totalResult[0] && !r[1]) { + totalResult = r; + } + }); + match = totalResult[1]; + } + return [match, results.length === 0]; + } + function hasSome(str, arr) { + return arr.some((word) => str.includes(word)); + } + function ruleSpliter(rule) { + const group = rule.match(CRRE); + if (group) { + const [, isDomain, place = "*", flag, sel] = group, + type = CRFlags.indexOf(flag), + matchResult = + place === "*" + ? [true, true] + : domainChecker(place.split(isDomain ? "|" : ",")); + if (sel && matchResult[0]) { + return { + black: type % 2 ? "white" : "black", + type: Math.floor(type / 2), + place: (isDomain ? "|" : "") + place, + generic: matchResult[1], + sel, + }; + } + } + } + function ruleLoader(rule) { + if ( + hasSome(rule, [ + ":matches-path(", + ":min-text-length(", + ":watch-attr(", + ":-abp-properties(", + ":matches-property(", + ]) + ) + return; + // 去掉开头末尾空格 + rule = rule.trim(); + // 如果 #$# 不包含 {} 就排除 + // 可以尽量排除 Snippet Filters + if ( + /(?:\w|\*|]|^)#\$#/.test(rule) && + !/{\s*[a-zA-Z-]+\s*:\s*.+}\s*$/.test(rule) + ) + return; + // ## -> #?# + if ( + /(?:\w|\*|]|^)#@?\$?#/.test(rule) && + hasSome(rule, [ + ":has(", + ":-abp-has(", + "[-ext-has=", + ":has-text(", + ":contains(", + ":-abp-contains(", + "[-ext-contains=", + ":matches-css(", + "[-ext-matches-css=", + ":matches-css-before(", + "[-ext-matches-css-before=", + ":matches-css-after(", + "[-ext-matches-css-after=", + ":matches-attr(", + ":nth-ancestor(", + ":upward(", + ":xpath(", + ":remove()", + ":not(", + ]) + ) { + rule = rule.replace(/(\w|\*|]|^)#(@?\$?)#/, "$1#$2?#"); + } + // :style(...) 转换 + // example.com#?##id:style(color: red) + // example.com#$?##id { color: red } + if (rule.includes(":style(")) { + rule = rule + .replace(/(\w|\*|]|^)#(@?)(\??)#/, "$1#$2$$$3#") + .replace(/:style\(\s*/, " { ") + .replace(/\s*\)$/, " }"); + } + return ruleSpliter(rule); + } + function ruleToCss(rule, preset) { + var _a, _b; + const isStyle = /}\s*$/.test(rule.sel); + return [ + `/* ${rule.type}${rule.place} */ ${ + rule.sel + + (!isStyle ? preset.replace(/\s{2,}/g, " ").replace(/\n/g, "") : "") + } \n`, + isStyle + ? (_b = + (_a = rule.sel.match(/^(.+?)\s*{\s*[a-zA-Z-]+\s*:\s*.+}\s*$/)) === + null || _a === void 0 + ? void 0 + : _a[1]) !== null && _b !== void 0 + ? _b + : rule.sel + : rule.sel, + ]; + } + function cssToAbp(css) { + var _a; + const flags = ["##", "#?#", "#$#", "#$?#"]; + const [, typeStr, isDomain, place, sel, style] = + (_a = css.match(CCRE)) !== null && _a !== void 0 ? _a : []; + if (typeStr === undefined) return null; + const type = parseInt(typeStr); + return [ + `${place === "*" ? "" : isDomain ? `[$domain=${place}]` : place}${ + flags[type] + }${type >= 2 ? sel : style}`, + type, + style, + ]; + } + // https://developer.mozilla.org/en-US/docs/Web/API/XMLHttpRequest/Using_XMLHttpRequest#bypassing_the_cache + function addTimeParam(url) { + return url + (url.includes("?") ? "&" : "?") + new Date().getTime(); + } + function saveCss() { + return __awaiter(this, void 0, void 0, function* () { + const styles = { + needUpdate: false, + genHideCss: data.genHideCss, + genExtraCss: data.genExtraCss, + spcHideCss: data.spcHideCss, + spcExtraCss: data.spcExtraCss, + }; + yield values.css(styles); + }); + } + function readCss() { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const styles = + (_a = yield values.css()) !== null && _a !== void 0 + ? _a + : defaultValues.css; + if (!hasSome(Object.keys(styles), styleBoxes)) { + yield values.css(defaultValues.css); + return; + } + styleBoxes.forEach((sname) => { + var _a; + if (styles[sname].length > 0) { + data.saved = true; + data.update = + (_a = styles.needUpdate) !== null && _a !== void 0 ? _a : true; + data[sname] = styles[sname]; + } + }); + }); + } + + /** + * @adguard/extended-css - v2.0.52 - Fri Apr 14 2023 + * https://github.com/AdguardTeam/ExtendedCss#homepage + * Copyright (c) 2023 AdGuard. Licensed GPL-3.0 + */ + function _defineProperty(obj, key, value) { + if (key in obj) { + Object.defineProperty(obj, key, { + value: value, + enumerable: true, + configurable: true, + writable: true, + }); + } else { + obj[key] = value; + } + return obj; + } + /** + * Possible ast node types. + * + * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects + * during ExtendedCss import into other libraries. + */ + const NODE = { + SELECTOR_LIST: "SelectorList", + SELECTOR: "Selector", + REGULAR_SELECTOR: "RegularSelector", + EXTENDED_SELECTOR: "ExtendedSelector", + ABSOLUTE_PSEUDO_CLASS: "AbsolutePseudoClass", + RELATIVE_PSEUDO_CLASS: "RelativePseudoClass", + }; + /** + * Class needed for creating ast nodes while selector parsing. + * Used for SelectorList, Selector, ExtendedSelector. + */ + class AnySelectorNode { + /** + * Creates new ast node. + * + * @param type Ast node type. + */ + constructor(type) { + _defineProperty(this, "children", []); + this.type = type; + } + /** + * Adds child node to children array. + * + * @param child Ast node. + */ + addChild(child) { + this.children.push(child); + } + } + /** + * Class needed for creating RegularSelector ast node while selector parsing. + */ + class RegularSelectorNode extends AnySelectorNode { + /** + * Creates RegularSelector ast node. + * + * @param value Value of RegularSelector node. + */ + constructor(value) { + super(NODE.REGULAR_SELECTOR); + this.value = value; + } + } + /** + * Class needed for creating RelativePseudoClass ast node while selector parsing. + */ + class RelativePseudoClassNode extends AnySelectorNode { + /** + * Creates RegularSelector ast node. + * + * @param name Name of RelativePseudoClass node. + */ + constructor(name) { + super(NODE.RELATIVE_PSEUDO_CLASS); + this.name = name; + } + } + /** + * Class needed for creating AbsolutePseudoClass ast node while selector parsing. + */ + class AbsolutePseudoClassNode extends AnySelectorNode { + /** + * Creates AbsolutePseudoClass ast node. + * + * @param name Name of AbsolutePseudoClass node. + */ + constructor(name) { + super(NODE.ABSOLUTE_PSEUDO_CLASS); + _defineProperty(this, "value", ""); + this.name = name; + } + } + /* eslint-disable jsdoc/require-description-complete-sentence */ + /** + * Root node. + * + * SelectorList + * : Selector + * ... + * ; + */ + /** + * Selector node. + * + * Selector + * : RegularSelector + * | ExtendedSelector + * ... + * ; + */ + /** + * Regular selector node. + * It can be selected by querySelectorAll(). + * + * RegularSelector + * : type + * : value + * ; + */ + /** + * Extended selector node. + * + * ExtendedSelector + * : AbsolutePseudoClass + * | RelativePseudoClass + * ; + */ + /** + * Absolute extended pseudo-class node, + * i.e. none-selector args. + * + * AbsolutePseudoClass + * : type + * : name + * : value + * ; + */ + /** + * Relative extended pseudo-class node + * i.e. selector as arg. + * + * RelativePseudoClass + * : type + * : name + * : SelectorList + * ; + */ + // + // ast example + // + // div.banner > div:has(span, p), a img.ad + // + // SelectorList - div.banner > div:has(span, p), a img.ad + // Selector - div.banner > div:has(span, p) + // RegularSelector - div.banner > div + // ExtendedSelector - :has(span, p) + // PseudoClassSelector - :has + // SelectorList - span, p + // Selector - span + // RegularSelector - span + // Selector - p + // RegularSelector - p + // Selector - a img.ad + // RegularSelector - a img.ad + // + const LEFT_SQUARE_BRACKET = "["; + const RIGHT_SQUARE_BRACKET = "]"; + const LEFT_PARENTHESIS = "("; + const RIGHT_PARENTHESIS = ")"; + const LEFT_CURLY_BRACKET = "{"; + const RIGHT_CURLY_BRACKET = "}"; + const BRACKET = { + SQUARE: { + LEFT: LEFT_SQUARE_BRACKET, + RIGHT: RIGHT_SQUARE_BRACKET, + }, + PARENTHESES: { + LEFT: LEFT_PARENTHESIS, + RIGHT: RIGHT_PARENTHESIS, + }, + CURLY: { + LEFT: LEFT_CURLY_BRACKET, + RIGHT: RIGHT_CURLY_BRACKET, + }, + }; + const SLASH = "/"; + const BACKSLASH = "\\"; + const SPACE = " "; + const COMMA = ","; + const DOT = "."; + const SEMICOLON = ";"; + const COLON = ":"; + const SINGLE_QUOTE = "'"; + const DOUBLE_QUOTE = '"'; // do not consider hyphen `-` as separated mark + // to avoid pseudo-class names splitting + // e.g. 'matches-css' or 'if-not' + const CARET = "^"; + const DOLLAR_SIGN = "$"; + const EQUAL_SIGN = "="; + const TAB = "\t"; + const CARRIAGE_RETURN = "\r"; + const LINE_FEED = "\n"; + const FORM_FEED = "\f"; + const WHITE_SPACE_CHARACTERS = [ + SPACE, + TAB, + CARRIAGE_RETURN, + LINE_FEED, + FORM_FEED, + ]; // for universal selector and attributes + const ASTERISK = "*"; + const ID_MARKER = "#"; + const CLASS_MARKER = DOT; + const DESCENDANT_COMBINATOR = SPACE; + const CHILD_COMBINATOR = ">"; + const NEXT_SIBLING_COMBINATOR = "+"; + const SUBSEQUENT_SIBLING_COMBINATOR = "~"; + const COMBINATORS = [ + DESCENDANT_COMBINATOR, + CHILD_COMBINATOR, + NEXT_SIBLING_COMBINATOR, + SUBSEQUENT_SIBLING_COMBINATOR, + ]; + const SUPPORTED_SELECTOR_MARKS = [ + LEFT_SQUARE_BRACKET, + RIGHT_SQUARE_BRACKET, + LEFT_PARENTHESIS, + RIGHT_PARENTHESIS, + LEFT_CURLY_BRACKET, + RIGHT_CURLY_BRACKET, + SLASH, + BACKSLASH, + SEMICOLON, + COLON, + COMMA, + SINGLE_QUOTE, + DOUBLE_QUOTE, + CARET, + DOLLAR_SIGN, + ASTERISK, + ID_MARKER, + CLASS_MARKER, + DESCENDANT_COMBINATOR, + CHILD_COMBINATOR, + NEXT_SIBLING_COMBINATOR, + SUBSEQUENT_SIBLING_COMBINATOR, + TAB, + CARRIAGE_RETURN, + LINE_FEED, + FORM_FEED, + ]; + const SUPPORTED_STYLE_DECLARATION_MARKS = [ + // divider between property and value in declaration + COLON, + // divider between declarations + SEMICOLON, + // sometimes is needed for value wrapping + // e.g. 'content: "-"' + SINGLE_QUOTE, + DOUBLE_QUOTE, + // needed for quote escaping inside the same-type quotes + BACKSLASH, + // whitespaces + SPACE, + TAB, + CARRIAGE_RETURN, + LINE_FEED, + FORM_FEED, + ]; // absolute: + const CONTAINS_PSEUDO = "contains"; + const HAS_TEXT_PSEUDO = "has-text"; + const ABP_CONTAINS_PSEUDO = "-abp-contains"; + const MATCHES_CSS_PSEUDO = "matches-css"; + const MATCHES_CSS_BEFORE_PSEUDO = "matches-css-before"; + const MATCHES_CSS_AFTER_PSEUDO = "matches-css-after"; + const MATCHES_ATTR_PSEUDO_CLASS_MARKER = "matches-attr"; + const MATCHES_PROPERTY_PSEUDO_CLASS_MARKER = "matches-property"; + const XPATH_PSEUDO_CLASS_MARKER = "xpath"; + const NTH_ANCESTOR_PSEUDO_CLASS_MARKER = "nth-ancestor"; + const CONTAINS_PSEUDO_NAMES = [ + CONTAINS_PSEUDO, + HAS_TEXT_PSEUDO, + ABP_CONTAINS_PSEUDO, + ]; + /** + * Pseudo-class :upward() can get number or selector arg + * and if the arg is selector it should be standard, not extended + * so :upward pseudo-class is always absolute. + */ + const UPWARD_PSEUDO_CLASS_MARKER = "upward"; + /** + * Pseudo-class `:remove()` and pseudo-property `remove` + * are used for element actions, not for element selecting. + * + * Selector text should not contain the pseudo-class + * so selector parser should consider it as invalid + * and both are handled by stylesheet parser. + */ + const REMOVE_PSEUDO_MARKER = "remove"; // relative: + const HAS_PSEUDO_CLASS_MARKER = "has"; + const ABP_HAS_PSEUDO_CLASS_MARKER = "-abp-has"; + const HAS_PSEUDO_CLASS_MARKERS = [ + HAS_PSEUDO_CLASS_MARKER, + ABP_HAS_PSEUDO_CLASS_MARKER, + ]; + const IS_PSEUDO_CLASS_MARKER = "is"; + const NOT_PSEUDO_CLASS_MARKER = "not"; + const ABSOLUTE_PSEUDO_CLASSES = [ + CONTAINS_PSEUDO, + HAS_TEXT_PSEUDO, + ABP_CONTAINS_PSEUDO, + MATCHES_CSS_PSEUDO, + MATCHES_CSS_BEFORE_PSEUDO, + MATCHES_CSS_AFTER_PSEUDO, + MATCHES_ATTR_PSEUDO_CLASS_MARKER, + MATCHES_PROPERTY_PSEUDO_CLASS_MARKER, + XPATH_PSEUDO_CLASS_MARKER, + NTH_ANCESTOR_PSEUDO_CLASS_MARKER, + UPWARD_PSEUDO_CLASS_MARKER, + ]; + const RELATIVE_PSEUDO_CLASSES = [ + ...HAS_PSEUDO_CLASS_MARKERS, + IS_PSEUDO_CLASS_MARKER, + NOT_PSEUDO_CLASS_MARKER, + ]; + const SUPPORTED_PSEUDO_CLASSES = [ + ...ABSOLUTE_PSEUDO_CLASSES, + ...RELATIVE_PSEUDO_CLASSES, + ]; // these pseudo-classes should be part of RegularSelector value + // if its arg does not contain extended selectors. + // the ast will be checked after the selector is completely parsed + const OPTIMIZATION_PSEUDO_CLASSES = [ + NOT_PSEUDO_CLASS_MARKER, + IS_PSEUDO_CLASS_MARKER, + ]; + /** + * ':scope' is used for extended pseudo-class :has(), if-not(), :is() and :not(). + */ + const SCOPE_CSS_PSEUDO_CLASS = ":scope"; + /** + * ':after' and ':before' are needed for :matches-css() pseudo-class + * all other are needed for :has() limitation after regular pseudo-elements. + * + * @see {@link https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54} [case 3] + */ + const REGULAR_PSEUDO_ELEMENTS = { + AFTER: "after", + BACKDROP: "backdrop", + BEFORE: "before", + CUE: "cue", + CUE_REGION: "cue-region", + FIRST_LETTER: "first-letter", + FIRST_LINE: "first-line", + FILE_SELECTION_BUTTON: "file-selector-button", + GRAMMAR_ERROR: "grammar-error", + MARKER: "marker", + PART: "part", + PLACEHOLDER: "placeholder", + SELECTION: "selection", + SLOTTED: "slotted", + SPELLING_ERROR: "spelling-error", + TARGET_TEXT: "target-text", + }; // ExtendedCss does not support at-rules + // https://developer.mozilla.org/en-US/docs/Web/CSS/At-rule + const AT_RULE_MARKER = "@"; + const CONTENT_CSS_PROPERTY = "content"; + const PSEUDO_PROPERTY_POSITIVE_VALUE = "true"; + const DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE = "global"; + const NO_SELECTOR_ERROR_PREFIX = "Selector should be defined"; + const STYLE_ERROR_PREFIX = { + NO_STYLE: "No style declaration found", + NO_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} before style declaration in stylesheet`, + INVALID_STYLE: "Invalid style declaration", + UNCLOSED_STYLE: "Unclosed style declaration", + NO_PROPERTY: "Missing style property in declaration", + NO_VALUE: "Missing style value in declaration", + NO_STYLE_OR_REMOVE: + "Style should be declared or :remove() pseudo-class should used", + NO_COMMENT: "Comments are not supported", + }; + const NO_AT_RULE_ERROR_PREFIX = "At-rules are not supported"; + const REMOVE_ERROR_PREFIX = { + INVALID_REMOVE: "Invalid :remove() pseudo-class in selector", + NO_TARGET_SELECTOR: `${NO_SELECTOR_ERROR_PREFIX} before :remove() pseudo-class`, + MULTIPLE_USAGE: + "Pseudo-class :remove() appears more than once in selector", + INVALID_POSITION: + "Pseudo-class :remove() should be at the end of selector", + }; + const MATCHING_ELEMENT_ERROR_PREFIX = "Error while matching element"; + const MAX_STYLE_PROTECTION_COUNT = 50; + /** + * Regexp that matches backward compatible syntaxes. + */ + const REGEXP_VALID_OLD_SYNTAX = + /\[-(?:ext)-([a-z-_]+)=(["'])((?:(?=(\\?))\4.)*?)\2\]/g; + /** + * Marker for checking invalid selector after old-syntax normalizing by selector converter. + */ + const INVALID_OLD_SYNTAX_MARKER = "[-ext-"; + /** + * Complex replacement function. + * Undo quote escaping inside of an extended selector. + * + * @param match Whole matched string. + * @param name Group 1. + * @param quoteChar Group 2. + * @param rawValue Group 3. + * + * @returns Converted string. + */ + const evaluateMatch = (match, name, quoteChar, rawValue) => { + // Unescape quotes + const re = new RegExp(`([^\\\\]|^)\\\\${quoteChar}`, "g"); + const value = rawValue.replace(re, `$1${quoteChar}`); + return `:${name}(${value})`; + }; // ':scope' pseudo may be at start of :has() argument + // but ExtCssDocument.querySelectorAll() already use it for selecting exact element descendants + const SCOPE_MARKER_REGEXP = /\(:scope >/g; + const SCOPE_REPLACER = "(>"; + const MATCHES_CSS_PSEUDO_ELEMENT_REGEXP = + /(:matches-css)-(before|after)\(/g; + const convertMatchesCss = ( + match, + extendedPseudoClass, + regularPseudoElement + ) => { + // ':matches-css-before(' --> ':matches-css(before, ' + // ':matches-css-after(' --> ':matches-css(after, ' + return `${extendedPseudoClass}${BRACKET.PARENTHESES.LEFT}${regularPseudoElement}${COMMA}`; + }; + /** + * Handles old syntax and :scope inside :has(). + * + * @param selector Trimmed selector to normalize. + * + * @returns Normalized selector. + * @throws An error on invalid old extended syntax selector. + */ + const normalize = (selector) => { + const normalizedSelector = selector + .replace(REGEXP_VALID_OLD_SYNTAX, evaluateMatch) + .replace(SCOPE_MARKER_REGEXP, SCOPE_REPLACER) + .replace(MATCHES_CSS_PSEUDO_ELEMENT_REGEXP, convertMatchesCss); // validate old syntax after normalizing + // e.g. '[-ext-matches-css-before=\'content: /^[A-Z][a-z]' + if (normalizedSelector.includes(INVALID_OLD_SYNTAX_MARKER)) { + throw new Error( + `Invalid extended-css old syntax selector: '${selector}'` + ); + } + return normalizedSelector; + }; + /** + * Prepares the rawSelector before tokenization: + * 1. Trims it. + * 2. Converts old syntax `[-ext-pseudo-class="..."]` to new one `:pseudo-class(...)`. + * 3. Handles :scope pseudo inside :has() pseudo-class arg. + * + * @param rawSelector Selector with no style declaration. + * @returns Prepared selector with no style declaration. + */ + const convert = (rawSelector) => { + const trimmedSelector = rawSelector.trim(); + return normalize(trimmedSelector); + }; + /** + * Possible token types. + * + * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects + * during ExtendedCss import into other libraries. + */ + const TOKEN_TYPE = { + MARK: "mark", + WORD: "word", + }; + /** + * Splits `input` string into tokens. + * + * @param input Input string to tokenize. + * @param supportedMarks Array of supported marks to considered as `TOKEN_TYPE.MARK`; + * all other will be considered as `TOKEN_TYPE.WORD`. + * + * @returns Array of tokens. + */ + const tokenize = (input, supportedMarks) => { + // buffer is needed for words collecting while iterating + let wordBuffer = ""; // result collection + const tokens = []; + const selectorSymbols = input.split(""); // iterate through selector chars and collect tokens + selectorSymbols.forEach((symbol) => { + if (supportedMarks.includes(symbol)) { + // if anything was collected to the buffer before + if (wordBuffer.length > 0) { + // now it is time to stop buffer collecting and save is as "word" + tokens.push({ + type: TOKEN_TYPE.WORD, + value: wordBuffer, + }); // reset the buffer + wordBuffer = ""; + } // save current symbol as "mark" + tokens.push({ + type: TOKEN_TYPE.MARK, + value: symbol, + }); + return; + } // otherwise collect symbol to the buffer + wordBuffer += symbol; + }); // save the last collected word + if (wordBuffer.length > 0) { + tokens.push({ + type: TOKEN_TYPE.WORD, + value: wordBuffer, + }); + } + return tokens; + }; + /** + * Prepares `rawSelector` and splits it into tokens. + * + * @param rawSelector Raw css selector. + * + * @returns Array of tokens supported for selector. + */ + const tokenizeSelector = (rawSelector) => { + const selector = convert(rawSelector); + return tokenize(selector, SUPPORTED_SELECTOR_MARKS); + }; + /** + * Splits `attribute` into tokens. + * + * @param attribute Input attribute. + * + * @returns Array of tokens supported for attribute. + */ + const tokenizeAttribute = (attribute) => { + // equal sigh `=` in attribute is considered as `TOKEN_TYPE.MARK` + return tokenize(attribute, [...SUPPORTED_SELECTOR_MARKS, EQUAL_SIGN]); + }; + /** + * Some browsers do not support Array.prototype.flat() + * e.g. Opera 42 which is used for browserstack tests. + * + * @see {@link https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/flat} + * + * @param input Array needed to be flatten. + * + * @returns Flatten array. + * @throws An error if array cannot be flatten. + */ + const flatten = (input) => { + const stack = []; + input.forEach((el) => stack.push(el)); + const res = []; + while (stack.length) { + // pop value from stack + const next = stack.pop(); + if (!next) { + throw new Error("Unable to make array flat"); + } + if (Array.isArray(next)) { + // push back array items, won't modify the original input + next.forEach((el) => stack.push(el)); + } else { + res.push(next); + } + } // reverse to restore input order + return res.reverse(); + }; + /** + * Returns first item from `array`. + * + * @param array Input array. + * + * @returns First array item, or `undefined` if there is no such item. + */ + const getFirst = (array) => { + return array[0]; + }; + /** + * Returns last item from array. + * + * @param array Input array. + * + * @returns Last array item, or `undefined` if there is no such item. + */ + const getLast = (array) => { + return array[array.length - 1]; + }; + /** + * Returns array item which is previous to the last one + * e.g. for `[5, 6, 7, 8]` returns `7`. + * + * @param array Input array. + * + * @returns Previous to last array item, or `undefined` if there is no such item. + */ + const getPrevToLast = (array) => { + return array[array.length - 2]; + }; + /** + * Takes array of ast node `children` and returns the child by the `index`. + * + * @param array Array of ast node children. + * @param index Index of needed child in the array. + * @param errorMessage Optional error message to throw. + * + * @returns Array item at `index` position. + * @throws An error if there is no child with specified `index` in array. + */ + const getItemByIndex = (array, index, errorMessage) => { + const indexChild = array[index]; + if (!indexChild) { + throw new Error( + errorMessage || `No array item found by index ${index}` + ); + } + return indexChild; + }; + const NO_REGULAR_SELECTOR_ERROR = + "At least one of Selector node children should be RegularSelector"; + /** + * Checks whether the type of `astNode` is SelectorList. + * + * @param astNode Ast node. + * + * @returns True if astNode.type === SelectorList. + */ + const isSelectorListNode = (astNode) => { + return ( + (astNode === null || astNode === void 0 ? void 0 : astNode.type) === + NODE.SELECTOR_LIST + ); + }; + /** + * Checks whether the type of `astNode` is Selector. + * + * @param astNode Ast node. + * + * @returns True if astNode.type === Selector. + */ + const isSelectorNode = (astNode) => { + return ( + (astNode === null || astNode === void 0 ? void 0 : astNode.type) === + NODE.SELECTOR + ); + }; + /** + * Checks whether the type of `astNode` is RegularSelector. + * + * @param astNode Ast node. + * + * @returns True if astNode.type === RegularSelector. + */ + const isRegularSelectorNode = (astNode) => { + return ( + (astNode === null || astNode === void 0 ? void 0 : astNode.type) === + NODE.REGULAR_SELECTOR + ); + }; + /** + * Checks whether the type of `astNode` is ExtendedSelector. + * + * @param astNode Ast node. + * + * @returns True if astNode.type === ExtendedSelector. + */ + const isExtendedSelectorNode = (astNode) => { + return astNode.type === NODE.EXTENDED_SELECTOR; + }; + /** + * Checks whether the type of `astNode` is AbsolutePseudoClass. + * + * @param astNode Ast node. + * + * @returns True if astNode.type === AbsolutePseudoClass. + */ + const isAbsolutePseudoClassNode = (astNode) => { + return ( + (astNode === null || astNode === void 0 ? void 0 : astNode.type) === + NODE.ABSOLUTE_PSEUDO_CLASS + ); + }; + /** + * Checks whether the type of `astNode` is RelativePseudoClass. + * + * @param astNode Ast node. + * + * @returns True if astNode.type === RelativePseudoClass. + */ + const isRelativePseudoClassNode = (astNode) => { + return ( + (astNode === null || astNode === void 0 ? void 0 : astNode.type) === + NODE.RELATIVE_PSEUDO_CLASS + ); + }; + /** + * Returns name of `astNode`. + * + * @param astNode AbsolutePseudoClass or RelativePseudoClass node. + * + * @returns Name of `astNode`. + * @throws An error on unsupported ast node or no name found. + */ + const getNodeName = (astNode) => { + if (astNode === null) { + throw new Error("Ast node should be defined"); + } + if ( + !isAbsolutePseudoClassNode(astNode) && + !isRelativePseudoClassNode(astNode) + ) { + throw new Error( + "Only AbsolutePseudoClass or RelativePseudoClass ast node can have a name" + ); + } + if (!astNode.name) { + throw new Error("Extended pseudo-class should have a name"); + } + return astNode.name; + }; + /** + * Returns value of `astNode`. + * + * @param astNode RegularSelector or AbsolutePseudoClass node. + * @param errorMessage Optional error message if no value found. + * + * @returns Value of `astNode`. + * @throws An error on unsupported ast node or no value found. + */ + const getNodeValue = (astNode, errorMessage) => { + if (astNode === null) { + throw new Error("Ast node should be defined"); + } + if ( + !isRegularSelectorNode(astNode) && + !isAbsolutePseudoClassNode(astNode) + ) { + throw new Error( + "Only RegularSelector ot AbsolutePseudoClass ast node can have a value" + ); + } + if (!astNode.value) { + throw new Error( + errorMessage || + "Ast RegularSelector ot AbsolutePseudoClass node should have a value" + ); + } + return astNode.value; + }; + /** + * Returns only RegularSelector nodes from `children`. + * + * @param children Array of ast node children. + * + * @returns Array of RegularSelector nodes. + */ + const getRegularSelectorNodes = (children) => { + return children.filter(isRegularSelectorNode); + }; + /** + * Returns the first RegularSelector node from `children`. + * + * @param children Array of ast node children. + * @param errorMessage Optional error message if no value found. + * + * @returns Ast RegularSelector node. + * @throws An error if no RegularSelector node found. + */ + const getFirstRegularChild = (children, errorMessage) => { + const regularSelectorNodes = getRegularSelectorNodes(children); + const firstRegularSelectorNode = getFirst(regularSelectorNodes); + if (!firstRegularSelectorNode) { + throw new Error(errorMessage || NO_REGULAR_SELECTOR_ERROR); + } + return firstRegularSelectorNode; + }; + /** + * Returns the last RegularSelector node from `children`. + * + * @param children Array of ast node children. + * + * @returns Ast RegularSelector node. + * @throws An error if no RegularSelector node found. + */ + const getLastRegularChild = (children) => { + const regularSelectorNodes = getRegularSelectorNodes(children); + const lastRegularSelectorNode = getLast(regularSelectorNodes); + if (!lastRegularSelectorNode) { + throw new Error(NO_REGULAR_SELECTOR_ERROR); + } + return lastRegularSelectorNode; + }; + /** + * Returns the only child of `node`. + * + * @param node Ast node. + * @param errorMessage Error message. + * + * @returns The only child of ast node. + * @throws An error if none or more than one child found. + */ + const getNodeOnlyChild = (node, errorMessage) => { + if (node.children.length !== 1) { + throw new Error(errorMessage); + } + const onlyChild = getFirst(node.children); + if (!onlyChild) { + throw new Error(errorMessage); + } + return onlyChild; + }; + /** + * Takes ExtendedSelector node and returns its only child. + * + * @param extendedSelectorNode ExtendedSelector ast node. + * + * @returns AbsolutePseudoClass or RelativePseudoClass. + * @throws An error if there is no specific pseudo-class ast node. + */ + const getPseudoClassNode = (extendedSelectorNode) => { + return getNodeOnlyChild( + extendedSelectorNode, + "Extended selector should be specified" + ); + }; + /** + * Takes RelativePseudoClass node and returns its only child + * which is relative SelectorList node. + * + * @param pseudoClassNode RelativePseudoClass. + * + * @returns Relative SelectorList node. + * @throws An error if no selector list found. + */ + const getRelativeSelectorListNode = (pseudoClassNode) => { + if (!isRelativePseudoClassNode(pseudoClassNode)) { + throw new Error( + "Only RelativePseudoClass node can have relative SelectorList node as child" + ); + } + return getNodeOnlyChild( + pseudoClassNode, + `Missing arg for :${getNodeName(pseudoClassNode)}() pseudo-class` + ); + }; + const ATTRIBUTE_CASE_INSENSITIVE_FLAG = "i"; + /** + * Limited list of available symbols before slash `/` + * to check whether it is valid regexp pattern opening. + */ + const POSSIBLE_MARKS_BEFORE_REGEXP = { + COMMON: [ + // e.g. ':matches-attr(/data-/)' + BRACKET.PARENTHESES.LEFT, + // e.g. `:matches-attr('/data-/')` + SINGLE_QUOTE, + // e.g. ':matches-attr("/data-/")' + DOUBLE_QUOTE, + // e.g. ':matches-attr(check=/data-v-/)' + EQUAL_SIGN, + // e.g. ':matches-property(inner./_test/=null)' + DOT, + // e.g. ':matches-css(height:/20px/)' + COLON, + // ':matches-css-after( content : /(\\d+\\s)*me/ )' + SPACE, + ], + CONTAINS: [ + // e.g. ':contains(/text/)' + BRACKET.PARENTHESES.LEFT, + // e.g. `:contains('/text/')` + SINGLE_QUOTE, + // e.g. ':contains("/text/")' + DOUBLE_QUOTE, + ], + }; + /** + * Checks whether the passed token is supported extended pseudo-class. + * + * @param tokenValue Token value to check. + * + * @returns True if `tokenValue` is one of supported extended pseudo-class names. + */ + const isSupportedPseudoClass = (tokenValue) => { + return SUPPORTED_PSEUDO_CLASSES.includes(tokenValue); + }; + /** + * Checks whether the passed pseudo-class `name` should be optimized, + * i.e. :not() and :is(). + * + * @param name Pseudo-class name. + * + * @returns True if `name` is one if pseudo-class which should be optimized. + */ + const isOptimizationPseudoClass = (name) => { + return OPTIMIZATION_PSEUDO_CLASSES.includes(name); + }; + /** + * Checks whether next to "space" token is a continuation of regular selector being processed. + * + * @param nextTokenType Type of token next to current one. + * @param nextTokenValue Value of token next to current one. + * + * @returns True if next token seems to be a part of current regular selector. + */ + const doesRegularContinueAfterSpace = (nextTokenType, nextTokenValue) => { + // regular selector does not continues after the current token + if (!nextTokenType || !nextTokenValue) { + return false; + } + return ( + COMBINATORS.includes(nextTokenValue) || + nextTokenType === TOKEN_TYPE.WORD || // e.g. '#main *:has(> .ad)' + nextTokenValue === ASTERISK || + nextTokenValue === ID_MARKER || + nextTokenValue === CLASS_MARKER || // e.g. 'div :where(.content)' + nextTokenValue === COLON || // e.g. "div[class*=' ']" + nextTokenValue === SINGLE_QUOTE || // e.g. 'div[class*=" "]' + nextTokenValue === DOUBLE_QUOTE || + nextTokenValue === BRACKET.SQUARE.LEFT + ); + }; + /** + * Checks whether the regexp pattern for pseudo-class arg starts. + * Needed for `context.isRegexpOpen` flag. + * + * @param context Selector parser context. + * @param prevTokenValue Value of previous token. + * @param bufferNodeValue Value of bufferNode. + * + * @returns True if current token seems to be a start of regexp pseudo-class arg pattern. + * @throws An error on invalid regexp pattern. + */ + const isRegexpOpening = (context, prevTokenValue, bufferNodeValue) => { + const lastExtendedPseudoClassName = getLast( + context.extendedPseudoNamesStack + ); + if (!lastExtendedPseudoClassName) { + throw new Error( + "Regexp pattern allowed only in arg of extended pseudo-class" + ); + } // for regexp pattens the slash should not be escaped + // const isRegexpPatternSlash = prevTokenValue !== BACKSLASH; + // regexp pattern can be set as arg of pseudo-class + // which means limited list of available symbols before slash `/`; + // for :contains() pseudo-class regexp pattern should be at the beginning of arg + if (CONTAINS_PSEUDO_NAMES.includes(lastExtendedPseudoClassName)) { + return POSSIBLE_MARKS_BEFORE_REGEXP.CONTAINS.includes(prevTokenValue); + } + if ( + prevTokenValue === SLASH && + lastExtendedPseudoClassName !== XPATH_PSEUDO_CLASS_MARKER + ) { + const rawArgDesc = bufferNodeValue + ? `in arg part: '${bufferNodeValue}'` + : "arg"; + throw new Error( + `Invalid regexp pattern for :${lastExtendedPseudoClassName}() pseudo-class ${rawArgDesc}` + ); + } // for other pseudo-classes regexp pattern can be either the whole arg or its part + return POSSIBLE_MARKS_BEFORE_REGEXP.COMMON.includes(prevTokenValue); + }; + /** + * Checks whether the attribute starts. + * + * @param tokenValue Value of current token. + * @param prevTokenValue Previous token value. + * + * @returns True if combination of current and previous token seems to be **a start** of attribute. + */ + const isAttributeOpening = (tokenValue, prevTokenValue) => { + return tokenValue === BRACKET.SQUARE.LEFT && prevTokenValue !== BACKSLASH; + }; + /** + * Checks whether the attribute ends. + * + * @param context Selector parser context. + * + * @returns True if combination of current and previous token seems to be **an end** of attribute. + * @throws An error on invalid attribute. + */ + const isAttributeClosing = (context) => { + var _getPrevToLast; + if (!context.isAttributeBracketsOpen) { + return false; + } // valid attributes may have extra spaces inside. + // we get rid of them just to simplify the checking and they are skipped only here: + // - spaces will be collected to the ast with spaces as they were declared is selector + // - extra spaces in attribute are not relevant to attribute syntax validity + // e.g. 'a[ title ]' is the same as 'a[title]' + // 'div[style *= "MARGIN" i]' is the same as 'div[style*="MARGIN"i]' + const noSpaceAttr = context.attributeBuffer.split(SPACE).join(""); // tokenize the prepared attribute string + const attrTokens = tokenizeAttribute(noSpaceAttr); + const firstAttrToken = getFirst(attrTokens); + const firstAttrTokenType = + firstAttrToken === null || firstAttrToken === void 0 + ? void 0 + : firstAttrToken.type; + const firstAttrTokenValue = + firstAttrToken === null || firstAttrToken === void 0 + ? void 0 + : firstAttrToken.value; // signal an error on any mark-type token except backslash + // e.g. '[="margin"]' + if ( + firstAttrTokenType === TOKEN_TYPE.MARK && // backslash is allowed at start of attribute + // e.g. '[\\:data-service-slot]' + firstAttrTokenValue !== BACKSLASH + ) { + // eslint-disable-next-line max-len + throw new Error( + `'[${context.attributeBuffer}]' is not a valid attribute due to '${firstAttrTokenValue}' at start of it` + ); + } + const lastAttrToken = getLast(attrTokens); + const lastAttrTokenType = + lastAttrToken === null || lastAttrToken === void 0 + ? void 0 + : lastAttrToken.type; + const lastAttrTokenValue = + lastAttrToken === null || lastAttrToken === void 0 + ? void 0 + : lastAttrToken.value; + if (lastAttrTokenValue === EQUAL_SIGN) { + // e.g. '[style=]' + throw new Error( + `'[${context.attributeBuffer}]' is not a valid attribute due to '${EQUAL_SIGN}'` + ); + } + const equalSignIndex = attrTokens.findIndex((token) => { + return token.type === TOKEN_TYPE.MARK && token.value === EQUAL_SIGN; + }); + const prevToLastAttrTokenValue = + (_getPrevToLast = getPrevToLast(attrTokens)) === null || + _getPrevToLast === void 0 + ? void 0 + : _getPrevToLast.value; + if (equalSignIndex === -1) { + // if there is no '=' inside attribute, + // it must be just attribute name which means the word-type token before closing bracket + // e.g. 'div[style]' + if (lastAttrTokenType === TOKEN_TYPE.WORD) { + return true; + } + return ( + prevToLastAttrTokenValue === BACKSLASH && // some weird attribute are valid too + // e.g. '[class\\"ads-article\\"]' + (lastAttrTokenValue === DOUBLE_QUOTE || // e.g. "[class\\'ads-article\\']" + lastAttrTokenValue === SINGLE_QUOTE) + ); + } // get the value of token next to `=` + const nextToEqualSignToken = getItemByIndex( + attrTokens, + equalSignIndex + 1 + ); + const nextToEqualSignTokenValue = nextToEqualSignToken.value; // check whether the attribute value wrapper in quotes + const isAttrValueQuote = + nextToEqualSignTokenValue === SINGLE_QUOTE || + nextToEqualSignTokenValue === DOUBLE_QUOTE; // for no quotes after `=` the last token before `]` should be a word-type one + // e.g. 'div[style*=margin]' + // 'div[style*=MARGIN i]' + if (!isAttrValueQuote) { + if (lastAttrTokenType === TOKEN_TYPE.WORD) { + return true; + } // otherwise signal an error + // e.g. 'table[style*=border: 0px"]' + throw new Error( + `'[${context.attributeBuffer}]' is not a valid attribute` + ); + } // otherwise if quotes for value are present + // the last token before `]` can still be word-type token + // e.g. 'div[style*="MARGIN" i]' + if ( + lastAttrTokenType === TOKEN_TYPE.WORD && + (lastAttrTokenValue === null || lastAttrTokenValue === void 0 + ? void 0 + : lastAttrTokenValue.toLocaleLowerCase()) === + ATTRIBUTE_CASE_INSENSITIVE_FLAG + ) { + return prevToLastAttrTokenValue === nextToEqualSignTokenValue; + } // eventually if there is quotes for attribute value and last token is not a word, + // the closing mark should be the same quote as opening one + return lastAttrTokenValue === nextToEqualSignTokenValue; + }; + /** + * Checks whether the `tokenValue` is a whitespace character. + * + * @param tokenValue Token value. + * + * @returns True if `tokenValue` is a whitespace character. + */ + const isWhiteSpaceChar = (tokenValue) => { + if (!tokenValue) { + return false; + } + return WHITE_SPACE_CHARACTERS.includes(tokenValue); + }; + /** + * Checks whether the passed `str` is a name of supported absolute extended pseudo-class, + * e.g. :contains(), :matches-css() etc. + * + * @param str Token value to check. + * + * @returns True if `str` is one of absolute extended pseudo-class names. + */ + const isAbsolutePseudoClass = (str) => { + return ABSOLUTE_PSEUDO_CLASSES.includes(str); + }; + /** + * Checks whether the passed `str` is a name of supported relative extended pseudo-class, + * e.g. :has(), :not() etc. + * + * @param str Token value to check. + * + * @returns True if `str` is one of relative extended pseudo-class names. + */ + const isRelativePseudoClass = (str) => { + return RELATIVE_PSEUDO_CLASSES.includes(str); + }; + /** + * Returns the node which is being collected + * or null if there is no such one. + * + * @param context Selector parser context. + * + * @returns Buffer node or null. + */ + const getBufferNode = (context) => { + if (context.pathToBufferNode.length === 0) { + return null; + } // buffer node is always the last in the pathToBufferNode stack + return getLast(context.pathToBufferNode) || null; + }; + /** + * Returns the parent node to the 'buffer node' — which is the one being collected — + * or null if there is no such one. + * + * @param context Selector parser context. + * + * @returns Parent node of buffer node or null. + */ + const getBufferNodeParent = (context) => { + // at least two nodes should exist — the buffer node and its parent + // otherwise return null + if (context.pathToBufferNode.length < 2) { + return null; + } // since the buffer node is always the last in the pathToBufferNode stack + // its parent is previous to it in the stack + return getPrevToLast(context.pathToBufferNode) || null; + }; + /** + * Returns last RegularSelector ast node. + * Needed for parsing of the complex selector with extended pseudo-class inside it. + * + * @param context Selector parser context. + * + * @returns Ast RegularSelector node. + * @throws An error if: + * - bufferNode is absent; + * - type of bufferNode is unsupported; + * - no RegularSelector in bufferNode. + */ + const getContextLastRegularSelectorNode = (context) => { + const bufferNode = getBufferNode(context); + if (!bufferNode) { + throw new Error("No bufferNode found"); + } + if (!isSelectorNode(bufferNode)) { + throw new Error("Unsupported bufferNode type"); + } + const lastRegularSelectorNode = getLastRegularChild(bufferNode.children); + context.pathToBufferNode.push(lastRegularSelectorNode); + return lastRegularSelectorNode; + }; + /** + * Updates needed buffer node value while tokens iterating. + * For RegularSelector also collects token values to context.attributeBuffer + * for proper attribute parsing. + * + * @param context Selector parser context. + * @param tokenValue Value of current token. + * + * @throws An error if: + * - no bufferNode; + * - bufferNode.type is not RegularSelector or AbsolutePseudoClass. + */ + const updateBufferNode = (context, tokenValue) => { + const bufferNode = getBufferNode(context); + if (bufferNode === null) { + throw new Error("No bufferNode to update"); + } + if (isAbsolutePseudoClassNode(bufferNode)) { + bufferNode.value += tokenValue; + } else if (isRegularSelectorNode(bufferNode)) { + bufferNode.value += tokenValue; + if (context.isAttributeBracketsOpen) { + context.attributeBuffer += tokenValue; + } + } else { + // eslint-disable-next-line max-len + throw new Error( + `${bufferNode.type} node cannot be updated. Only RegularSelector and AbsolutePseudoClass are supported` + ); + } + }; + /** + * Adds SelectorList node to context.ast at the start of ast collecting. + * + * @param context Selector parser context. + */ + const addSelectorListNode = (context) => { + const selectorListNode = new AnySelectorNode(NODE.SELECTOR_LIST); + context.ast = selectorListNode; + context.pathToBufferNode.push(selectorListNode); + }; + /** + * Adds new node to buffer node children. + * New added node will be considered as buffer node after it. + * + * @param context Selector parser context. + * @param type Type of node to add. + * @param tokenValue Optional, defaults to `''`, value of processing token. + * + * @throws An error if no bufferNode. + */ + const addAstNodeByType = function (context, type) { + let tokenValue = + arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : ""; + const bufferNode = getBufferNode(context); + if (bufferNode === null) { + throw new Error("No buffer node"); + } + let node; + if (type === NODE.REGULAR_SELECTOR) { + node = new RegularSelectorNode(tokenValue); + } else if (type === NODE.ABSOLUTE_PSEUDO_CLASS) { + node = new AbsolutePseudoClassNode(tokenValue); + } else if (type === NODE.RELATIVE_PSEUDO_CLASS) { + node = new RelativePseudoClassNode(tokenValue); + } else { + // SelectorList || Selector || ExtendedSelector + node = new AnySelectorNode(type); + } + bufferNode.addChild(node); + context.pathToBufferNode.push(node); + }; + /** + * The very beginning of ast collecting. + * + * @param context Selector parser context. + * @param tokenValue Value of regular selector. + */ + const initAst = (context, tokenValue) => { + addSelectorListNode(context); + addAstNodeByType(context, NODE.SELECTOR); // RegularSelector node is always the first child of Selector node + addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue); + }; + /** + * Inits selector list subtree for relative extended pseudo-classes, e.g. :has(), :not(). + * + * @param context Selector parser context. + * @param tokenValue Optional, defaults to `''`, value of inner regular selector. + */ + const initRelativeSubtree = function (context) { + let tokenValue = + arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : ""; + addAstNodeByType(context, NODE.SELECTOR_LIST); + addAstNodeByType(context, NODE.SELECTOR); + addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue); + }; + /** + * Goes to closest parent specified by type. + * Actually updates path to buffer node for proper ast collecting of selectors while parsing. + * + * @param context Selector parser context. + * @param parentType Type of needed parent node in ast. + */ + const upToClosest = (context, parentType) => { + for (let i = context.pathToBufferNode.length - 1; i >= 0; i -= 1) { + var _context$pathToBuffer; + if ( + ((_context$pathToBuffer = context.pathToBufferNode[i]) === null || + _context$pathToBuffer === void 0 + ? void 0 + : _context$pathToBuffer.type) === parentType + ) { + context.pathToBufferNode = context.pathToBufferNode.slice(0, i + 1); + break; + } + } + }; + /** + * Returns needed buffer node updated due to complex selector parsing. + * + * @param context Selector parser context. + * + * @returns Ast node for following selector parsing. + * @throws An error if there is no upper SelectorNode is ast. + */ + const getUpdatedBufferNode = (context) => { + // it may happen during the parsing of selector list + // which is an argument of relative pseudo-class + // e.g. '.banner:has(~span, ~p)' + // parser position is here ↑ + // so if after the comma the buffer node type is SelectorList and parent type is RelativePseudoClass + // we should simply return the current buffer node + const bufferNode = getBufferNode(context); + if ( + bufferNode && + isSelectorListNode(bufferNode) && + isRelativePseudoClassNode(getBufferNodeParent(context)) + ) { + return bufferNode; + } + upToClosest(context, NODE.SELECTOR); + const selectorNode = getBufferNode(context); + if (!selectorNode) { + throw new Error( + "No SelectorNode, impossible to continue selector parsing by ExtendedCss" + ); + } + const lastSelectorNodeChild = getLast(selectorNode.children); + const hasExtended = + lastSelectorNodeChild && + isExtendedSelectorNode(lastSelectorNodeChild) && // parser position might be inside standard pseudo-class brackets which has space + // e.g. 'div:contains(/а/):nth-child(100n + 2)' + context.standardPseudoBracketsStack.length === 0; + const supposedPseudoClassNode = + hasExtended && getFirst(lastSelectorNodeChild.children); + let newNeededBufferNode = selectorNode; + if (supposedPseudoClassNode) { + // name of pseudo-class for last extended-node child for Selector node + const lastExtendedPseudoName = + hasExtended && supposedPseudoClassNode.name; + const isLastExtendedNameRelative = + lastExtendedPseudoName && + isRelativePseudoClass(lastExtendedPseudoName); + const isLastExtendedNameAbsolute = + lastExtendedPseudoName && + isAbsolutePseudoClass(lastExtendedPseudoName); + const hasRelativeExtended = + isLastExtendedNameRelative && + context.extendedPseudoBracketsStack.length > 0 && + context.extendedPseudoBracketsStack.length === + context.extendedPseudoNamesStack.length; + const hasAbsoluteExtended = + isLastExtendedNameAbsolute && + lastExtendedPseudoName === getLast(context.extendedPseudoNamesStack); + if (hasRelativeExtended) { + // return relative selector node to update later + context.pathToBufferNode.push(lastSelectorNodeChild); + newNeededBufferNode = supposedPseudoClassNode; + } else if (hasAbsoluteExtended) { + // return absolute selector node to update later + context.pathToBufferNode.push(lastSelectorNodeChild); + newNeededBufferNode = supposedPseudoClassNode; + } + } else if (hasExtended) { + // return selector node to add new regular selector node later + newNeededBufferNode = selectorNode; + } else { + // otherwise return last regular selector node to update later + newNeededBufferNode = getContextLastRegularSelectorNode(context); + } // update the path to buffer node properly + context.pathToBufferNode.push(newNeededBufferNode); + return newNeededBufferNode; + }; + /** + * Checks values of few next tokens on colon token `:` and: + * - updates buffer node for following standard pseudo-class; + * - adds extended selector ast node for following extended pseudo-class; + * - validates some cases of `:remove()` and `:has()` usage. + * + * @param context Selector parser context. + * @param selector Selector. + * @param tokenValue Value of current token. + * @param nextTokenValue Value of token next to current one. + * @param nextToNextTokenValue Value of token next to next to current one. + * + * @throws An error on :remove() pseudo-class in selector + * or :has() inside regular pseudo limitation. + */ + const handleNextTokenOnColon = ( + context, + selector, + tokenValue, + nextTokenValue, + nextToNextTokenValue + ) => { + if (!nextTokenValue) { + throw new Error( + `Invalid colon ':' at the end of selector: '${selector}'` + ); + } + if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) { + if (nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER) { + // :remove() pseudo-class should be handled before + // as it is not about element selecting but actions with elements + // e.g. 'body > div:empty:remove()' + throw new Error( + `${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'` + ); + } // if following token is not an extended pseudo + // the colon should be collected to value of RegularSelector + // e.g. '.entry_text:nth-child(2)' + updateBufferNode(context, tokenValue); // check the token after the pseudo and do balance parentheses later + // only if it is functional pseudo-class (standard with brackets, e.g. ':lang()'). + // no brackets balance needed for such case, + // parser position is on first colon after the 'div': + // e.g. 'div:last-child:has(button.privacy-policy__btn)' + if ( + nextToNextTokenValue && + nextToNextTokenValue === BRACKET.PARENTHESES.LEFT && // no brackets balance needed for parentheses inside attribute value + // e.g. 'a[href="javascript:void(0)"]' <-- parser position is on colon `:` + // before `void` ↑ + !context.isAttributeBracketsOpen + ) { + context.standardPseudoNamesStack.push(nextTokenValue); + } + } else { + // it is supported extended pseudo-class. + // Disallow :has() inside the pseudos accepting only compound selectors + // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [2] + if ( + HAS_PSEUDO_CLASS_MARKERS.includes(nextTokenValue) && + context.standardPseudoNamesStack.length > 0 + ) { + // eslint-disable-next-line max-len + throw new Error( + `Usage of :${nextTokenValue}() pseudo-class is not allowed inside regular pseudo: '${getLast( + context.standardPseudoNamesStack + )}'` + ); + } else { + // stop RegularSelector value collecting + upToClosest(context, NODE.SELECTOR); // add ExtendedSelector to Selector children + addAstNodeByType(context, NODE.EXTENDED_SELECTOR); + } + } + }; + // e.g. ':is(.page, .main) > .banner' or '*:not(span):not(p)' + const IS_OR_NOT_PSEUDO_SELECTING_ROOT = `html ${ASTERISK}`; + /** + * Checks if there are any ExtendedSelector node in selector list. + * + * @param selectorList Ast SelectorList node. + * + * @returns True if `selectorList` has any inner ExtendedSelector node. + */ + const hasExtendedSelector = (selectorList) => { + return selectorList.children.some((selectorNode) => { + return selectorNode.children.some((selectorNodeChild) => { + return isExtendedSelectorNode(selectorNodeChild); + }); + }); + }; + /** + * Converts selector list of RegularSelector nodes to string. + * + * @param selectorList Ast SelectorList node. + * + * @returns String representation for selector list of regular selectors. + */ + const selectorListOfRegularsToString = (selectorList) => { + // if there is no ExtendedSelector in relative SelectorList + // it means that each Selector node has single child — RegularSelector node + // and their values should be combined to string + const standardCssSelectors = selectorList.children.map((selectorNode) => { + const selectorOnlyChild = getNodeOnlyChild( + selectorNode, + "Ast Selector node should have RegularSelector node" + ); + return getNodeValue(selectorOnlyChild); + }); + return standardCssSelectors.join(`${COMMA}${SPACE}`); + }; + /** + * Updates children of `node` replacing them with `newChildren`. + * Important: modifies input `node` which is passed by reference. + * + * @param node Ast node to update. + * @param newChildren Array of new children for ast node. + * + * @returns Updated ast node. + */ + const updateNodeChildren = (node, newChildren) => { + node.children = newChildren; + return node; + }; + /** + * Recursively checks whether the ExtendedSelector node should be optimized. + * It has to be recursive because RelativePseudoClass has inner SelectorList node. + * + * @param currExtendedSelectorNode Ast ExtendedSelector node. + * + * @returns True is ExtendedSelector should be optimized. + */ + const shouldOptimizeExtendedSelector = (currExtendedSelectorNode) => { + if (currExtendedSelectorNode === null) { + return false; + } + const extendedPseudoClassNode = getPseudoClassNode( + currExtendedSelectorNode + ); + const pseudoName = getNodeName(extendedPseudoClassNode); + if (isAbsolutePseudoClass(pseudoName)) { + return false; + } + const relativeSelectorList = getRelativeSelectorListNode( + extendedPseudoClassNode + ); + const innerSelectorNodes = relativeSelectorList.children; // simple checking for standard selectors in arg of :not() or :is() pseudo-class + // e.g. 'div > *:is(div, a, span)' + if (isOptimizationPseudoClass(pseudoName)) { + const areAllSelectorNodeChildrenRegular = innerSelectorNodes.every( + (selectorNode) => { + try { + const selectorOnlyChild = getNodeOnlyChild( + selectorNode, + "Selector node should have RegularSelector" + ); // it means that the only child is RegularSelector and it can be optimized + return isRegularSelectorNode(selectorOnlyChild); + } catch (e) { + return false; + } + } + ); + if (areAllSelectorNodeChildrenRegular) { + return true; + } + } // for other extended pseudo-classes than :not() and :is() + return innerSelectorNodes.some((selectorNode) => { + return selectorNode.children.some((selectorNodeChild) => { + if (!isExtendedSelectorNode(selectorNodeChild)) { + return false; + } // check inner ExtendedSelector recursively + // e.g. 'div:has(*:not(.header))' + return shouldOptimizeExtendedSelector(selectorNodeChild); + }); + }); + }; + /** + * Returns optimized ExtendedSelector node if it can be optimized + * or null if ExtendedSelector is fully optimized while function execution + * which means that value of `prevRegularSelectorNode` is updated. + * + * @param currExtendedSelectorNode Current ExtendedSelector node to optimize. + * @param prevRegularSelectorNode Previous RegularSelector node. + * + * @returns Ast node or null. + */ + const getOptimizedExtendedSelector = ( + currExtendedSelectorNode, + prevRegularSelectorNode + ) => { + if (!currExtendedSelectorNode) { + return null; + } + const extendedPseudoClassNode = getPseudoClassNode( + currExtendedSelectorNode + ); + const relativeSelectorList = getRelativeSelectorListNode( + extendedPseudoClassNode + ); + const hasInnerExtendedSelector = + hasExtendedSelector(relativeSelectorList); + if (!hasInnerExtendedSelector) { + // if there is no extended selectors for :not() or :is() + // e.g. 'div:not(.content, .main)' + const relativeSelectorListStr = + selectorListOfRegularsToString(relativeSelectorList); + const pseudoName = getNodeName(extendedPseudoClassNode); // eslint-disable-next-line max-len + const optimizedExtendedStr = `${COLON}${pseudoName}${BRACKET.PARENTHESES.LEFT}${relativeSelectorListStr}${BRACKET.PARENTHESES.RIGHT}`; + prevRegularSelectorNode.value = `${getNodeValue( + prevRegularSelectorNode + )}${optimizedExtendedStr}`; + return null; + } // eslint-disable-next-line @typescript-eslint/no-use-before-define + const optimizedRelativeSelectorList = + optimizeSelectorListNode(relativeSelectorList); + const optimizedExtendedPseudoClassNode = updateNodeChildren( + extendedPseudoClassNode, + [optimizedRelativeSelectorList] + ); + return updateNodeChildren(currExtendedSelectorNode, [ + optimizedExtendedPseudoClassNode, + ]); + }; + /** + * Combines values of `previous` and `current` RegularSelector nodes. + * It may happen during the optimization when ExtendedSelector between RegularSelector node was optimized. + * + * @param current Current RegularSelector node. + * @param previous Previous RegularSelector node. + */ + const optimizeCurrentRegularSelector = (current, previous) => { + previous.value = `${getNodeValue(previous)}${SPACE}${getNodeValue( + current + )}`; + }; + /** + * Optimizes ast Selector node. + * + * @param selectorNode Ast Selector node. + * + * @returns Optimized ast node. + * @throws An error while collecting optimized nodes. + */ + const optimizeSelectorNode = (selectorNode) => { + // non-optimized list of SelectorNode children + const rawSelectorNodeChildren = selectorNode.children; // for collecting optimized children list + const optimizedChildrenList = []; + let currentIndex = 0; // iterate through all children in non-optimized ast Selector node + while (currentIndex < rawSelectorNodeChildren.length) { + const currentChild = getItemByIndex( + rawSelectorNodeChildren, + currentIndex, + "currentChild should be specified" + ); // no need to optimize the very first child which is always RegularSelector node + if (currentIndex === 0) { + optimizedChildrenList.push(currentChild); + } else { + const prevRegularChild = getLastRegularChild(optimizedChildrenList); + if (isExtendedSelectorNode(currentChild)) { + // start checking with point is null + let optimizedExtendedSelector = null; // check whether the optimization is needed + let isOptimizationNeeded = + shouldOptimizeExtendedSelector(currentChild); // update optimizedExtendedSelector so it can be optimized recursively + // i.e. `getOptimizedExtendedSelector(optimizedExtendedSelector)` below + optimizedExtendedSelector = currentChild; + while (isOptimizationNeeded) { + // recursively optimize ExtendedSelector until no optimization needed + // e.g. div > *:is(.banner:not(.block)) + optimizedExtendedSelector = getOptimizedExtendedSelector( + optimizedExtendedSelector, + prevRegularChild + ); + isOptimizationNeeded = shouldOptimizeExtendedSelector( + optimizedExtendedSelector + ); + } // if it was simple :not() of :is() with standard selector arg + // e.g. 'div:not([class][id])' + // or '.main > *:is([data-loaded], .banner)' + // after the optimization the ExtendedSelector node become part of RegularSelector + // so nothing to save eventually + // otherwise the optimized ExtendedSelector should be saved + // e.g. 'div:has(:not([class]))' + if (optimizedExtendedSelector !== null) { + optimizedChildrenList.push(optimizedExtendedSelector); // if optimization is not needed + const optimizedPseudoClass = getPseudoClassNode( + optimizedExtendedSelector + ); + const optimizedPseudoName = getNodeName(optimizedPseudoClass); // parent element checking is used to apply :is() and :not() pseudo-classes as extended. + // as there is no parentNode for root element (html) + // so element selection should be limited to it's children + // e.g. '*:is(:has(.page))' -> 'html *:is(has(.page))' + // or '*:not(:has(span))' -> 'html *:not(:has(span))' + if ( + getNodeValue(prevRegularChild) === ASTERISK && + isOptimizationPseudoClass(optimizedPseudoName) + ) { + prevRegularChild.value = IS_OR_NOT_PSEUDO_SELECTING_ROOT; + } + } + } else if (isRegularSelectorNode(currentChild)) { + // in non-optimized ast, RegularSelector node may follow ExtendedSelector which should be optimized + // for example, for 'div:not(.content) > .banner' schematically it looks like + // non-optimized ast: [ + // 1. RegularSelector: 'div' + // 2. ExtendedSelector: 'not(.content)' + // 3. RegularSelector: '> .banner' + // ] + // which after the ExtendedSelector looks like + // partly optimized ast: [ + // 1. RegularSelector: 'div:not(.content)' + // 2. RegularSelector: '> .banner' + // ] + // so second RegularSelector value should be combined with first one + // optimized ast: [ + // 1. RegularSelector: 'div:not(.content) > .banner' + // ] + // here we check **children of selectorNode** after previous optimization if it was + const lastOptimizedChild = getLast(optimizedChildrenList) || null; + if (isRegularSelectorNode(lastOptimizedChild)) { + optimizeCurrentRegularSelector(currentChild, prevRegularChild); + } + } + } + currentIndex += 1; + } + return updateNodeChildren(selectorNode, optimizedChildrenList); + }; + /** + * Optimizes ast SelectorList node. + * + * @param selectorListNode SelectorList node. + * + * @returns Optimized ast node. + */ + const optimizeSelectorListNode = (selectorListNode) => { + return updateNodeChildren( + selectorListNode, + selectorListNode.children.map((s) => optimizeSelectorNode(s)) + ); + }; + /** + * Optimizes ast: + * If arg of :not() and :is() pseudo-classes does not contain extended selectors, + * native Document.querySelectorAll() can be used to query elements. + * It means that ExtendedSelector ast nodes can be removed + * and value of relevant RegularSelector node should be updated accordingly. + * + * @param ast Non-optimized ast. + * + * @returns Optimized ast. + */ + const optimizeAst = (ast) => { + // ast is basically the selector list of selectors + return optimizeSelectorListNode(ast); + }; + // https://github.com/AdguardTeam/ExtendedCss/issues/115 + const XPATH_PSEUDO_SELECTING_ROOT = "body"; + const NO_WHITESPACE_ERROR_PREFIX = + "No white space is allowed before or after extended pseudo-class name in selector"; + /** + * Parses selector into ast for following element selection. + * + * @param selector Selector to parse. + * + * @returns Parsed ast. + * @throws An error on invalid selector. + */ + const parse = (selector) => { + const tokens = tokenizeSelector(selector); + const context = { + ast: null, + pathToBufferNode: [], + extendedPseudoNamesStack: [], + extendedPseudoBracketsStack: [], + standardPseudoNamesStack: [], + standardPseudoBracketsStack: [], + isAttributeBracketsOpen: false, + attributeBuffer: "", + isRegexpOpen: false, + shouldOptimize: false, + }; + let i = 0; + while (i < tokens.length) { + const token = tokens[i]; + if (!token) { + break; + } // Token to process + const { type: tokenType, value: tokenValue } = token; // needed for SPACE and COLON tokens checking + const nextToken = tokens[i + 1]; + const nextTokenType = + nextToken === null || nextToken === void 0 ? void 0 : nextToken.type; + const nextTokenValue = + nextToken === null || nextToken === void 0 ? void 0 : nextToken.value; // needed for limitations + // - :not() and :is() root element + // - :has() usage + // - white space before and after pseudo-class name + const nextToNextToken = tokens[i + 2]; + const nextToNextTokenValue = + nextToNextToken === null || nextToNextToken === void 0 + ? void 0 + : nextToNextToken.value; // needed for COLON token checking for none-specified regular selector before extended one + // e.g. 'p, :hover' + // or '.banner, :contains(ads)' + const previousToken = tokens[i - 1]; + const prevTokenType = + previousToken === null || previousToken === void 0 + ? void 0 + : previousToken.type; + const prevTokenValue = + previousToken === null || previousToken === void 0 + ? void 0 + : previousToken.value; // needed for proper parsing of regexp pattern arg + // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)' + const previousToPreviousToken = tokens[i - 2]; + const prevToPrevTokenValue = + previousToPreviousToken === null || previousToPreviousToken === void 0 + ? void 0 + : previousToPreviousToken.value; + let bufferNode = getBufferNode(context); + switch (tokenType) { + case TOKEN_TYPE.WORD: + if (bufferNode === null) { + // there is no buffer node only in one case — no ast collecting has been started + initAst(context, tokenValue); + } else if (isSelectorListNode(bufferNode)) { + // add new selector to selector list + addAstNodeByType(context, NODE.SELECTOR); + addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue); + } else if (isRegularSelectorNode(bufferNode)) { + updateBufferNode(context, tokenValue); + } else if (isExtendedSelectorNode(bufferNode)) { + // No white space is allowed between the name of extended pseudo-class + // and its opening parenthesis + // https://www.w3.org/TR/selectors-4/#pseudo-classes + // e.g. 'span:contains (text)' + if ( + isWhiteSpaceChar(nextTokenValue) && + nextToNextTokenValue === BRACKET.PARENTHESES.LEFT + ) { + throw new Error(`${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'`); + } + const lowerCaseTokenValue = tokenValue.toLowerCase(); // save pseudo-class name for brackets balance checking + context.extendedPseudoNamesStack.push(lowerCaseTokenValue); // extended pseudo-class name are parsed in lower case + // as they should be case-insensitive + // https://www.w3.org/TR/selectors-4/#pseudo-classes + if (isAbsolutePseudoClass(lowerCaseTokenValue)) { + addAstNodeByType( + context, + NODE.ABSOLUTE_PSEUDO_CLASS, + lowerCaseTokenValue + ); + } else { + // if it is not absolute pseudo-class, it must be relative one + // add RelativePseudoClass with tokenValue as pseudo-class name to ExtendedSelector children + addAstNodeByType( + context, + NODE.RELATIVE_PSEUDO_CLASS, + lowerCaseTokenValue + ); // for :not() and :is() pseudo-classes parsed ast should be optimized later + if (isOptimizationPseudoClass(lowerCaseTokenValue)) { + context.shouldOptimize = true; + } + } + } else if (isAbsolutePseudoClassNode(bufferNode)) { + // collect absolute pseudo-class arg + updateBufferNode(context, tokenValue); + } else if (isRelativePseudoClassNode(bufferNode)) { + initRelativeSubtree(context, tokenValue); + } + break; + case TOKEN_TYPE.MARK: + switch (tokenValue) { + case COMMA: + if ( + !bufferNode || + (typeof bufferNode !== "undefined" && !nextTokenValue) + ) { + // consider the selector is invalid if there is no bufferNode yet (e.g. ', a') + // or there is nothing after the comma while bufferNode is defined (e.g. 'div, ') + throw new Error(`'${selector}' is not a valid selector`); + } else if (isRegularSelectorNode(bufferNode)) { + if (context.isAttributeBracketsOpen) { + // the comma might be inside element attribute value + // e.g. 'div[data-comma="0,1"]' + updateBufferNode(context, tokenValue); + } else { + // new Selector should be collected to upper SelectorList + upToClosest(context, NODE.SELECTOR_LIST); + } + } else if (isAbsolutePseudoClassNode(bufferNode)) { + // the comma inside arg of absolute extended pseudo + // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)' + updateBufferNode(context, tokenValue); + } else if (isSelectorNode(bufferNode)) { + // new Selector should be collected to upper SelectorList + // if parser position is on Selector node + upToClosest(context, NODE.SELECTOR_LIST); + } + break; + case SPACE: + // it might be complex selector with extended pseudo-class inside it + // and the space is between that complex selector and following regular selector + // parser position is on ` ` before `span` now: + // e.g. 'div:has(img).banner span' + // so we need to check whether the new ast node should be added (example above) + // or previous regular selector node should be updated + if ( + isRegularSelectorNode(bufferNode) && // no need to update the buffer node if attribute value is being parsed + // e.g. 'div:not([id])[style="position: absolute; z-index: 10000;"]' + // parser position inside attribute ↑ + !context.isAttributeBracketsOpen + ) { + bufferNode = getUpdatedBufferNode(context); + } + if (isRegularSelectorNode(bufferNode)) { + // standard selectors with white space between colon and name of pseudo + // are invalid for native document.querySelectorAll() anyway, + // so throwing the error here is better + // than proper parsing of invalid selector and passing it further. + // first of all do not check attributes + // e.g. div[style="text-align: center"] + if ( + !context.isAttributeBracketsOpen && // check the space after the colon and before the pseudo + // e.g. '.block: nth-child(2) + ((prevTokenValue === COLON && + nextTokenType === TOKEN_TYPE.WORD) || // or after the pseudo and before the opening parenthesis + // e.g. '.block:nth-child (2) + (prevTokenType === TOKEN_TYPE.WORD && + nextTokenValue === BRACKET.PARENTHESES.LEFT)) + ) { + throw new Error(`'${selector}' is not a valid selector`); + } // collect current tokenValue to value of RegularSelector + // if it is the last token or standard selector continues after the space. + // otherwise it will be skipped + if ( + !nextTokenValue || + doesRegularContinueAfterSpace( + nextTokenType, + nextTokenValue + ) || // we also should collect space inside attribute value + // e.g. `[onclick^="window.open ('https://example.com/share?url="]` + // parser position ↑ + context.isAttributeBracketsOpen + ) { + updateBufferNode(context, tokenValue); + } + } + if (isAbsolutePseudoClassNode(bufferNode)) { + // space inside extended pseudo-class arg + // e.g. 'span:contains(some text)' + updateBufferNode(context, tokenValue); + } + if (isRelativePseudoClassNode(bufferNode)) { + // init with empty value RegularSelector + // as the space is not needed for selector value + // e.g. 'p:not( .content )' + initRelativeSubtree(context); + } + if (isSelectorNode(bufferNode)) { + // do NOT add RegularSelector if parser position on space BEFORE the comma in selector list + // e.g. '.block:has(> img) , .banner)' + if ( + doesRegularContinueAfterSpace(nextTokenType, nextTokenValue) + ) { + // regular selector might be after the extended one. + // extra space before combinator or selector should not be collected + // e.g. '.banner:upward(2) .block' + // '.banner:upward(2) > .block' + // so no tokenValue passed to addAnySelectorNode() + addAstNodeByType(context, NODE.REGULAR_SELECTOR); + } + } + break; + case DESCENDANT_COMBINATOR: + case CHILD_COMBINATOR: + case NEXT_SIBLING_COMBINATOR: + case SUBSEQUENT_SIBLING_COMBINATOR: + case SEMICOLON: + case SLASH: + case BACKSLASH: + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + case CARET: + case DOLLAR_SIGN: + case BRACKET.CURLY.LEFT: + case BRACKET.CURLY.RIGHT: + case ASTERISK: + case ID_MARKER: + case CLASS_MARKER: + case BRACKET.SQUARE.LEFT: + // it might be complex selector with extended pseudo-class inside it + // and the space is between that complex selector and following regular selector + // e.g. 'div:has(img).banner' // parser position is on `.` before `banner` now + // 'div:has(img)[attr]' // parser position is on `[` before `attr` now + // so we need to check whether the new ast node should be added (example above) + // or previous regular selector node should be updated + if (COMBINATORS.includes(tokenValue)) { + if (bufferNode === null) { + // cases where combinator at very beginning of a selector + // e.g. '> div' + // or '~ .banner' + // or even '+js(overlay-buster)' which not a selector at all + // but may be validated by FilterCompiler so error message should be appropriate + throw new Error(`'${selector}' is not a valid selector`); + } + bufferNode = getUpdatedBufferNode(context); + } + if (bufferNode === null) { + // no ast collecting has been started + // e.g. '.banner > p' + // or '#top > div.ad' + // or '[class][style][attr]' + // or '*:not(span)' + initAst(context, tokenValue); + if (isAttributeOpening(tokenValue, prevTokenValue)) { + // e.g. '[class^="banner-"]' + context.isAttributeBracketsOpen = true; + } + } else if (isRegularSelectorNode(bufferNode)) { + if ( + tokenValue === BRACKET.CURLY.LEFT && + !(context.isAttributeBracketsOpen || context.isRegexpOpen) + ) { + // e.g. 'div { content: "' + throw new Error(`'${selector}' is not a valid selector`); + } // collect the mark to the value of RegularSelector node + updateBufferNode(context, tokenValue); + if (isAttributeOpening(tokenValue, prevTokenValue)) { + // needed for proper handling element attribute value with comma + // e.g. 'div[data-comma="0,1"]' + context.isAttributeBracketsOpen = true; + } + } else if (isAbsolutePseudoClassNode(bufferNode)) { + // collect the mark to the arg of AbsolutePseudoClass node + updateBufferNode(context, tokenValue); // 'isRegexpOpen' flag is needed for brackets balancing inside extended pseudo-class arg + if ( + tokenValue === SLASH && + context.extendedPseudoNamesStack.length > 0 + ) { + if ( + prevTokenValue === SLASH && + prevToPrevTokenValue === BACKSLASH + ) { + // it may be specific url regexp pattern in arg of pseudo-class + // e.g. ':matches-css(background-image: /^url\(https:\/\/example\.org\//)' + // parser position is on final slash before `)` ↑ + context.isRegexpOpen = false; + } else if (prevTokenValue && prevTokenValue !== BACKSLASH) { + if ( + isRegexpOpening( + context, + prevTokenValue, + getNodeValue(bufferNode) + ) + ) { + context.isRegexpOpen = !context.isRegexpOpen; + } else { + // otherwise force `isRegexpOpen` flag to `false` + context.isRegexpOpen = false; + } + } + } + } else if (isRelativePseudoClassNode(bufferNode)) { + // add SelectorList to children of RelativePseudoClass node + initRelativeSubtree(context, tokenValue); + if (isAttributeOpening(tokenValue, prevTokenValue)) { + // besides of creating the relative subtree + // opening square bracket means start of attribute + // e.g. 'div:not([class="content"])' + // 'div:not([href*="window.print()"])' + context.isAttributeBracketsOpen = true; + } + } else if (isSelectorNode(bufferNode)) { + // after the extended pseudo closing parentheses + // parser position is on Selector node + // and regular selector can be after the extended one + // e.g. '.banner:upward(2)> .block' + // or '.inner:nth-ancestor(1)~ .banner' + if (COMBINATORS.includes(tokenValue)) { + addAstNodeByType( + context, + NODE.REGULAR_SELECTOR, + tokenValue + ); + } else if (!context.isRegexpOpen) { + // it might be complex selector with extended pseudo-class inside it. + // parser position is on `.` now: + // e.g. 'div:has(img).banner' + // so we need to get last regular selector node and update its value + bufferNode = getContextLastRegularSelectorNode(context); + updateBufferNode(context, tokenValue); + if (isAttributeOpening(tokenValue, prevTokenValue)) { + // handle attribute in compound selector after extended pseudo-class + // e.g. 'div:not(.top)[style="z-index: 10000;"]' + // parser position ↑ + context.isAttributeBracketsOpen = true; + } + } + } else if (isSelectorListNode(bufferNode)) { + // add Selector to SelectorList + addAstNodeByType(context, NODE.SELECTOR); // and RegularSelector as it is always the first child of Selector + addAstNodeByType(context, NODE.REGULAR_SELECTOR, tokenValue); + if (isAttributeOpening(tokenValue, prevTokenValue)) { + // handle simple attribute selector in selector list + // e.g. '.banner, [class^="ad-"]' + context.isAttributeBracketsOpen = true; + } + } + break; + case BRACKET.SQUARE.RIGHT: + if (isRegularSelectorNode(bufferNode)) { + // unescaped `]` in regular selector allowed only inside attribute value + if ( + !context.isAttributeBracketsOpen && + prevTokenValue !== BACKSLASH + ) { + // e.g. 'div]' + // eslint-disable-next-line max-len + throw new Error( + `'${selector}' is not a valid selector due to '${tokenValue}' after '${getNodeValue( + bufferNode + )}'` + ); + } // needed for proper parsing regular selectors after the attributes with comma + // e.g. 'div[data-comma="0,1"] > img' + if (isAttributeClosing(context)) { + context.isAttributeBracketsOpen = false; // reset attribute buffer on closing `]` + context.attributeBuffer = ""; + } // collect the bracket to the value of RegularSelector node + updateBufferNode(context, tokenValue); + } + if (isAbsolutePseudoClassNode(bufferNode)) { + // :xpath() expended pseudo-class arg might contain square bracket + // so it should be collected + // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)' + updateBufferNode(context, tokenValue); + } + break; + case COLON: + // No white space is allowed between the colon and the following name of the pseudo-class + // https://www.w3.org/TR/selectors-4/#pseudo-classes + // e.g. 'span: contains(text)' + if ( + isWhiteSpaceChar(nextTokenValue) && + nextToNextTokenValue && + SUPPORTED_PSEUDO_CLASSES.includes(nextToNextTokenValue) + ) { + throw new Error( + `${NO_WHITESPACE_ERROR_PREFIX}: '${selector}'` + ); + } + if (bufferNode === null) { + // no ast collecting has been started + if (nextTokenValue === XPATH_PSEUDO_CLASS_MARKER) { + // limit applying of "naked" :xpath pseudo-class + // https://github.com/AdguardTeam/ExtendedCss/issues/115 + initAst(context, XPATH_PSEUDO_SELECTING_ROOT); + } else if ( + nextTokenValue === UPWARD_PSEUDO_CLASS_MARKER || + nextTokenValue === NTH_ANCESTOR_PSEUDO_CLASS_MARKER + ) { + // selector should be specified before :nth-ancestor() or :upward() + // e.g. ':nth-ancestor(3)' + // or ':upward(span)' + throw new Error( + `${NO_SELECTOR_ERROR_PREFIX} before :${nextTokenValue}() pseudo-class` + ); + } else { + // make it more obvious if selector starts with pseudo with no tag specified + // e.g. ':has(a)' -> '*:has(a)' + // or ':empty' -> '*:empty' + initAst(context, ASTERISK); + } // bufferNode should be updated for following checking + bufferNode = getBufferNode(context); + } + if (isSelectorListNode(bufferNode)) { + // bufferNode is SelectorList after comma has been parsed. + // parser position is on colon now: + // e.g. 'img,:not(.content)' + addAstNodeByType(context, NODE.SELECTOR); // add empty value RegularSelector anyway as any selector should start with it + // and check previous token on the next step + addAstNodeByType(context, NODE.REGULAR_SELECTOR); // bufferNode should be updated for following checking + bufferNode = getBufferNode(context); + } + if (isRegularSelectorNode(bufferNode)) { + // it can be extended or standard pseudo + // e.g. '#share, :contains(share it)' + // or 'div,:hover' + // of 'div:has(+:contains(text))' // position is after '+' + if ( + (prevTokenValue && COMBINATORS.includes(prevTokenValue)) || + prevTokenValue === COMMA + ) { + // case with colon at the start of string - e.g. ':contains(text)' + // is covered by 'bufferNode === null' above at start of COLON checking + updateBufferNode(context, ASTERISK); + } + handleNextTokenOnColon( + context, + selector, + tokenValue, + nextTokenValue, + nextToNextTokenValue + ); + } + if (isSelectorNode(bufferNode)) { + // e.g. 'div:contains(text):' + if (!nextTokenValue) { + throw new Error( + `Invalid colon ':' at the end of selector: '${selector}'` + ); + } // after the extended pseudo closing parentheses + // parser position is on Selector node + // and there is might be another extended selector. + // parser position is on colon before 'upward': + // e.g. 'p:contains(PR):upward(2)' + if (isSupportedPseudoClass(nextTokenValue.toLowerCase())) { + // if supported extended pseudo-class is next to colon + // add ExtendedSelector to Selector children + addAstNodeByType(context, NODE.EXTENDED_SELECTOR); + } else if ( + nextTokenValue.toLowerCase() === REMOVE_PSEUDO_MARKER + ) { + // :remove() pseudo-class should be handled before + // as it is not about element selecting but actions with elements + // e.g. '#banner:upward(2):remove()' + throw new Error( + `${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'` + ); + } else { + // otherwise it is standard pseudo after extended pseudo-class in complex selector + // and colon should be collected to value of previous RegularSelector + // e.g. 'body *:not(input)::selection' + // 'input:matches-css(padding: 10):checked' + bufferNode = getContextLastRegularSelectorNode(context); + handleNextTokenOnColon( + context, + selector, + tokenValue, + nextTokenType, + nextToNextTokenValue + ); + } + } + if (isAbsolutePseudoClassNode(bufferNode)) { + // :xpath() pseudo-class should be the last of extended pseudo-classes + if ( + getNodeName(bufferNode) === XPATH_PSEUDO_CLASS_MARKER && + nextTokenValue && + SUPPORTED_PSEUDO_CLASSES.includes(nextTokenValue) && + nextToNextTokenValue === BRACKET.PARENTHESES.LEFT + ) { + throw new Error( + `:xpath() pseudo-class should be the last in selector: '${selector}'` + ); + } // collecting arg for absolute pseudo-class + // e.g. 'div:matches-css(width:400px)' + updateBufferNode(context, tokenValue); + } + if (isRelativePseudoClassNode(bufferNode)) { + if (!nextTokenValue) { + // e.g. 'div:has(:' + throw new Error( + `Invalid pseudo-class arg at the end of selector: '${selector}'` + ); + } // make it more obvious if selector starts with pseudo with no tag specified + // parser position is on colon inside :has() arg + // e.g. 'div:has(:contains(text))' + // or 'div:not(:empty)' + initRelativeSubtree(context, ASTERISK); + if (!isSupportedPseudoClass(nextTokenValue.toLowerCase())) { + // collect the colon to value of RegularSelector + // e.g. 'div:not(:empty)' + updateBufferNode(context, tokenValue); // parentheses should be balanced only for functional pseudo-classes + // e.g. '.yellow:not(:nth-child(3))' + if (nextToNextTokenValue === BRACKET.PARENTHESES.LEFT) { + context.standardPseudoNamesStack.push(nextTokenValue); + } + } else { + // add ExtendedSelector to Selector children + // e.g. 'div:has(:contains(text))' + upToClosest(context, NODE.SELECTOR); + addAstNodeByType(context, NODE.EXTENDED_SELECTOR); + } + } + break; + case BRACKET.PARENTHESES.LEFT: + // start of pseudo-class arg + if (isAbsolutePseudoClassNode(bufferNode)) { + // no brackets balancing needed inside + // 1. :xpath() extended pseudo-class arg + // 2. regexp arg for other extended pseudo-classes + if ( + getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER && + context.isRegexpOpen + ) { + // if the parentheses is escaped it should be part of regexp + // collect it to arg of AbsolutePseudoClass + // e.g. 'div:matches-css(background-image: /^url\\("data:image\\/gif;base64.+/)' + updateBufferNode(context, tokenValue); + } else { + // otherwise brackets should be balanced + // e.g. 'div:xpath(//h3[contains(text(),"Share it!")]/..)' + context.extendedPseudoBracketsStack.push(tokenValue); // eslint-disable-next-line max-len + if ( + context.extendedPseudoBracketsStack.length > + context.extendedPseudoNamesStack.length + ) { + updateBufferNode(context, tokenValue); + } + } + } + if (isRegularSelectorNode(bufferNode)) { + // continue RegularSelector value collecting for standard pseudo-classes + // e.g. '.banner:where(div)' + if (context.standardPseudoNamesStack.length > 0) { + updateBufferNode(context, tokenValue); + context.standardPseudoBracketsStack.push(tokenValue); + } // parentheses inside attribute value should be part of RegularSelector value + // e.g. 'div:not([href*="window.print()"])' <-- parser position + // is on the `(` after `print` ↑ + if (context.isAttributeBracketsOpen) { + updateBufferNode(context, tokenValue); + } + } + if (isRelativePseudoClassNode(bufferNode)) { + // save opening bracket for balancing + // e.g. 'div:not()' // position is on `(` + context.extendedPseudoBracketsStack.push(tokenValue); + } + break; + case BRACKET.PARENTHESES.RIGHT: + if (isAbsolutePseudoClassNode(bufferNode)) { + // no brackets balancing needed inside + // 1. :xpath() extended pseudo-class arg + // 2. regexp arg for other extended pseudo-classes + if ( + getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER && + context.isRegexpOpen + ) { + // if closing bracket is part of regexp + // simply save it to pseudo-class arg + updateBufferNode(context, tokenValue); + } else { + // remove stacked open parentheses for brackets balance + // e.g. 'h3:contains((Ads))' + // or 'div:xpath(//h3[contains(text(),"Share it!")]/..)' + context.extendedPseudoBracketsStack.pop(); + if (getNodeName(bufferNode) !== XPATH_PSEUDO_CLASS_MARKER) { + // for all other absolute pseudo-classes except :xpath() + // remove stacked name of extended pseudo-class + context.extendedPseudoNamesStack.pop(); // eslint-disable-next-line max-len + if ( + context.extendedPseudoBracketsStack.length > + context.extendedPseudoNamesStack.length + ) { + // if brackets stack is not empty yet, + // save tokenValue to arg of AbsolutePseudoClass + // parser position on first closing bracket after 'Ads': + // e.g. 'h3:contains((Ads))' + updateBufferNode(context, tokenValue); + } else if ( + context.extendedPseudoBracketsStack.length >= 0 && + context.extendedPseudoNamesStack.length >= 0 + ) { + // assume it is combined extended pseudo-classes + // parser position on first closing bracket after 'advert': + // e.g. 'div:has(.banner, :contains(advert))' + upToClosest(context, NODE.SELECTOR); + } + } else { + // for :xpath() + // eslint-disable-next-line max-len + if ( + context.extendedPseudoBracketsStack.length < + context.extendedPseudoNamesStack.length + ) { + // remove stacked name of extended pseudo-class + // if there are less brackets than pseudo-class names + // with means last removes bracket was closing for pseudo-class + context.extendedPseudoNamesStack.pop(); + } else { + // otherwise the bracket is part of arg + updateBufferNode(context, tokenValue); + } + } + } + } + if (isRegularSelectorNode(bufferNode)) { + if (context.isAttributeBracketsOpen) { + // parentheses inside attribute value should be part of RegularSelector value + // e.g. 'div:not([href*="window.print()"])' <-- parser position + // is on the `)` after `print(` ↑ + updateBufferNode(context, tokenValue); + } else if ( + context.standardPseudoNamesStack.length > 0 && + context.standardPseudoBracketsStack.length > 0 + ) { + // standard pseudo-class was processing. + // collect the closing bracket to value of RegularSelector + // parser position is on bracket after 'class' now: + // e.g. 'div:where(.class)' + updateBufferNode(context, tokenValue); // remove bracket and pseudo name from stacks + context.standardPseudoBracketsStack.pop(); + const lastStandardPseudo = + context.standardPseudoNamesStack.pop(); + if (!lastStandardPseudo) { + // standard pseudo should be in standardPseudoNamesStack + // as related to standardPseudoBracketsStack + throw new Error( + `Parsing error. Invalid selector: ${selector}` + ); + } // Disallow :has() after regular pseudo-elements + // https://bugs.chromium.org/p/chromium/issues/detail?id=669058#c54 [3] + if ( + Object.values(REGULAR_PSEUDO_ELEMENTS).includes( + lastStandardPseudo + ) && // check token which is next to closing parentheses and token after it + // parser position is on bracket after 'foo' now: + // e.g. '::part(foo):has(.a)' + nextTokenValue === COLON && + nextToNextTokenValue && + HAS_PSEUDO_CLASS_MARKERS.includes(nextToNextTokenValue) + ) { + // eslint-disable-next-line max-len + throw new Error( + `Usage of :${nextToNextTokenValue}() pseudo-class is not allowed after any regular pseudo-element: '${lastStandardPseudo}'` + ); + } + } else { + // extended pseudo-class was processing. + // e.g. 'div:has(h3)' + // remove bracket and pseudo name from stacks + context.extendedPseudoBracketsStack.pop(); + context.extendedPseudoNamesStack.pop(); + upToClosest(context, NODE.EXTENDED_SELECTOR); // go to upper selector for possible selector continuation after extended pseudo-class + // e.g. 'div:has(h3) > img' + upToClosest(context, NODE.SELECTOR); + } + } + if (isSelectorNode(bufferNode)) { + // after inner extended pseudo-class bufferNode is Selector. + // parser position is on last bracket now: + // e.g. 'div:has(.banner, :contains(ads))' + context.extendedPseudoBracketsStack.pop(); + context.extendedPseudoNamesStack.pop(); + upToClosest(context, NODE.EXTENDED_SELECTOR); + upToClosest(context, NODE.SELECTOR); + } + if (isRelativePseudoClassNode(bufferNode)) { + // save opening bracket for balancing + // e.g. 'div:not()' // position is on `)` + // context.extendedPseudoBracketsStack.push(tokenValue); + if ( + context.extendedPseudoNamesStack.length > 0 && + context.extendedPseudoBracketsStack.length > 0 + ) { + context.extendedPseudoBracketsStack.pop(); + context.extendedPseudoNamesStack.pop(); + } + } + break; + case LINE_FEED: + case FORM_FEED: + case CARRIAGE_RETURN: + // such characters at start and end of selector should be trimmed + // so is there is one them among tokens, it is not valid selector + throw new Error(`'${selector}' is not a valid selector`); + case TAB: + // allow tab only inside attribute value + // as there are such valid rules in filter lists + // e.g. 'div[style^="margin-right: auto; text-align: left;', + // parser position ↑ + if ( + isRegularSelectorNode(bufferNode) && + context.isAttributeBracketsOpen + ) { + updateBufferNode(context, tokenValue); + } else { + // otherwise not valid + throw new Error(`'${selector}' is not a valid selector`); + } + } + break; + // no default statement for Marks as they are limited to SUPPORTED_SELECTOR_MARKS + // and all other symbol combinations are tokenized as Word + // so error for invalid Word will be thrown later while element selecting by parsed ast + default: + throw new Error(`Unknown type of token: '${tokenValue}'`); + } + i += 1; + } + if (context.ast === null) { + throw new Error(`'${selector}' is not a valid selector`); + } + if ( + context.extendedPseudoNamesStack.length > 0 || + context.extendedPseudoBracketsStack.length > 0 + ) { + // eslint-disable-next-line max-len + throw new Error( + `Unbalanced brackets for extended pseudo-class: '${getLast( + context.extendedPseudoNamesStack + )}'` + ); + } + if (context.isAttributeBracketsOpen) { + throw new Error( + `Unbalanced attribute brackets in selector: '${selector}'` + ); + } + return context.shouldOptimize ? optimizeAst(context.ast) : context.ast; + }; + const natives = { + MutationObserver: + window.MutationObserver || window.WebKitMutationObserver, + }; + /** + * Class NativeTextContent is needed to intercept and save the native Node textContent getter + * for proper work of :contains() pseudo-class as it may be mocked. + * + * @see {@link https://github.com/AdguardTeam/ExtendedCss/issues/127} + */ + class NativeTextContent { + /** + * Native Node. + */ + /** + * Native Node textContent getter. + */ + /** + * Stores native node. + */ + constructor() { + this.nativeNode = window.Node || Node; + } + /** + * Sets native Node textContext getter to `getter` class field. + */ + setGetter() { + var _Object$getOwnPropert; + this.getter = + (_Object$getOwnPropert = Object.getOwnPropertyDescriptor( + this.nativeNode.prototype, + "textContent" + )) === null || _Object$getOwnPropert === void 0 + ? void 0 + : _Object$getOwnPropert.get; + } + } + const nativeTextContent = new NativeTextContent(); + /** + * Returns textContent of passed domElement. + * + * @param domElement DOM element. + * + * @returns DOM element textContent. + */ + const getNodeTextContent = (domElement) => { + if (nativeTextContent.getter) { + return nativeTextContent.getter.apply(domElement); + } // if ExtendedCss.init() has not been executed and there is no nodeTextContentGetter, + // use simple approach, especially when init() is not really needed, e.g. local tests + return domElement.textContent || ""; + }; + /** + * Returns element selector text based on it's tagName and attributes. + * + * @param element DOM element. + * + * @returns String representation of `element`. + */ + const getElementSelectorDesc = (element) => { + let selectorText = element.tagName.toLowerCase(); + selectorText += Array.from(element.attributes) + .map((attr) => { + return `[${attr.name}="${element.getAttribute(attr.name)}"]`; + }) + .join(""); + return selectorText; + }; + /** + * Returns path to a DOM element as a selector string. + * + * @param inputEl Input element. + * + * @returns String path to a DOM element. + * @throws An error if `inputEl` in not instance of `Element`. + */ + const getElementSelectorPath = (inputEl) => { + if (!(inputEl instanceof Element)) { + throw new Error("Function received argument with wrong type"); + } + let el; + el = inputEl; + const path = []; // we need to check '!!el' first because it is possible + // that some ancestor of the inputEl was removed before it + while (!!el && el.nodeType === Node.ELEMENT_NODE) { + let selector = el.nodeName.toLowerCase(); + if (el.id && typeof el.id === "string") { + selector += `#${el.id}`; + path.unshift(selector); + break; + } + let sibling = el; + let nth = 1; + while (sibling.previousElementSibling) { + sibling = sibling.previousElementSibling; + if ( + sibling.nodeType === Node.ELEMENT_NODE && + sibling.nodeName.toLowerCase() === selector + ) { + nth += 1; + } + } + if (nth !== 1) { + selector += `:nth-of-type(${nth})`; + } + path.unshift(selector); + el = el.parentElement; + } + return path.join(" > "); + }; + /** + * Checks whether the element is instance of HTMLElement. + * + * @param element Element to check. + * + * @returns True if `element` is HTMLElement. + */ + const isHtmlElement = (element) => { + return element instanceof HTMLElement; + }; + /** + * Takes `element` and returns its parent element. + * + * @param element Element. + * @param errorMessage Optional error message to throw. + * + * @returns Parent of `element`. + * @throws An error if element has no parent element. + */ + const getParent = (element, errorMessage) => { + const { parentElement } = element; + if (!parentElement) { + throw new Error(errorMessage || "Element does no have parent element"); + } + return parentElement; + }; + /** + * Checks whether the `error` has `message` property which type is string. + * + * @param error Error object. + * + * @returns True if `error` has message. + */ + const isErrorWithMessage = (error) => { + return ( + typeof error === "object" && + error !== null && + "message" in error && + typeof error.message === "string" + ); + }; + /** + * Converts `maybeError` to error object with message. + * + * @param maybeError Possible error. + * + * @returns Error object with defined `message` property. + */ + const toErrorWithMessage = (maybeError) => { + if (isErrorWithMessage(maybeError)) { + return maybeError; + } + try { + return new Error(JSON.stringify(maybeError)); + } catch { + // fallback in case if there is an error happened during the maybeError stringifying + // like with circular references for example + return new Error(String(maybeError)); + } + }; + /** + * Returns error message from `error`. + * May be helpful to handle caught errors. + * + * @param error Error object. + * + * @returns Message of `error`. + */ + const getErrorMessage = (error) => { + return toErrorWithMessage(error).message; + }; + const logger = { + /** + * Safe console.error version. + */ + error: + typeof console !== "undefined" && console.error && console.error.bind + ? console.error.bind(window.console) + : console.error, + /** + * Safe console.info version. + */ + info: + typeof console !== "undefined" && console.info && console.info.bind + ? console.info.bind(window.console) + : console.info, + }; + /** + * Returns string without suffix. + * + * @param str Input string. + * @param suffix Needed to remove. + * + * @returns String without suffix. + */ + const removeSuffix = (str, suffix) => { + const index = str.indexOf(suffix, str.length - suffix.length); + if (index >= 0) { + return str.substring(0, index); + } + return str; + }; + /** + * Replaces all `pattern`s with `replacement` in `input` string. + * String.replaceAll() polyfill because it is not supported by old browsers, e.g. Chrome 55. + * + * @see {@link https://caniuse.com/?search=String.replaceAll} + * + * @param input Input string to process. + * @param pattern Find in the input string. + * @param replacement Replace the pattern with. + * + * @returns Modified string. + */ + const replaceAll = (input, pattern, replacement) => { + if (!input) { + return input; + } + return input.split(pattern).join(replacement); + }; + /** + * Converts string pattern to regular expression. + * + * @param str String to convert. + * + * @returns Regular expression converted from pattern `str`. + */ + const toRegExp = (str) => { + if (str.startsWith(SLASH) && str.endsWith(SLASH)) { + return new RegExp(str.slice(1, -1)); + } + const escaped = str.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); + return new RegExp(escaped); + }; + /** + * Converts any simple type value to string type, + * e.g. `undefined` -> `'undefined'`. + * + * @param value Any type value. + * + * @returns String representation of `value`. + */ + const convertTypeIntoString = (value) => { + let output; + switch (value) { + case undefined: + output = "undefined"; + break; + case null: + output = "null"; + break; + default: + output = value.toString(); + } + return output; + }; + /** + * Converts instance of string value into other simple types, + * e.g. `'null'` -> `null`, `'true'` -> `true`. + * + * @param value String-type value. + * + * @returns Its own type representation of string-type `value`. + */ + const convertTypeFromString = (value) => { + const numValue = Number(value); + let output; + if (!Number.isNaN(numValue)) { + output = numValue; + } else { + switch (value) { + case "undefined": + output = undefined; + break; + case "null": + output = null; + break; + case "true": + output = true; + break; + case "false": + output = false; + break; + default: + output = value; + } + } + return output; + }; + const SAFARI_USER_AGENT_REGEXP = + /\sVersion\/(\d{2}\.\d)(.+\s|\s)(Safari)\//; + const isSafariBrowser = SAFARI_USER_AGENT_REGEXP.test(navigator.userAgent); + /** + * Checks whether the browser userAgent is supported. + * + * @param userAgent User agent of browser. + * + * @returns False only for Internet Explorer. + */ + const isUserAgentSupported = (userAgent) => { + // do not support Internet Explorer + if (userAgent.includes("MSIE") || userAgent.includes("Trident/")) { + return false; + } + return true; + }; + /** + * Checks whether the current browser is supported. + * + * @returns False for Internet Explorer, otherwise true. + */ + const isBrowserSupported = () => { + return isUserAgentSupported(navigator.userAgent); + }; + /** + * CSS_PROPERTY is needed for style values normalization. + * + * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects + * during ExtendedCss import into other libraries. + */ + const CSS_PROPERTY = { + BACKGROUND: "background", + BACKGROUND_IMAGE: "background-image", + CONTENT: "content", + OPACITY: "opacity", + }; + const REGEXP_ANY_SYMBOL = ".*"; + const REGEXP_WITH_FLAGS_REGEXP = /^\s*\/.*\/[gmisuy]*\s*$/; + /** + * Removes quotes for specified content value. + * + * For example, content style declaration with `::before` can be set as '-' (e.g. unordered list) + * which displayed as simple dash `-` with no quotes. + * But CSSStyleDeclaration.getPropertyValue('content') will return value + * wrapped into quotes, e.g. '"-"', which should be removed + * because filters maintainers does not use any quotes in real rules. + * + * @param str Input string. + * + * @returns String with no quotes for content value. + */ + const removeContentQuotes = (str) => { + return str.replace(/^(["'])([\s\S]*)\1$/, "$2"); + }; + /** + * Adds quotes for specified background url value. + * + * If background-image is specified **without** quotes: + * e.g. 'background: url(data:image/gif;base64,R0lGODlhAQA7)'. + * + * CSSStyleDeclaration.getPropertyValue('background-image') may return value **with** quotes: + * e.g. 'background: url("data:image/gif;base64,R0lGODlhAQA7")'. + * + * So we add quotes for compatibility since filters maintainers might use quotes in real rules. + * + * @param str Input string. + * + * @returns String with unified quotes for background url value. + */ + const addUrlPropertyQuotes = (str) => { + if (!str.includes('url("')) { + const re = /url\((.*?)\)/g; + return str.replace(re, 'url("$1")'); + } + return str; + }; + /** + * Adds quotes to url arg for consistent property value matching. + */ + const addUrlQuotesTo = { + regexpArg: (str) => { + // e.g. /^url\\([a-z]{4}:[a-z]{5}/ + // or /^url\\(data\\:\\image\\/gif;base64.+/ + const re = /(\^)?url(\\)?\\\((\w|\[\w)/g; + return str.replace(re, '$1url$2\\(\\"?$3'); + }, + noneRegexpArg: addUrlPropertyQuotes, + }; + /** + * Escapes regular expression string. + * + * @see {@link https://developer.mozilla.org/en/JavaScript/Reference/Global_Objects/regexp} + * + * @param str Input string. + * + * @returns Escaped regular expression string. + */ + const escapeRegExp = (str) => { + // should be escaped . * + ? ^ $ { } ( ) | [ ] / \ + // except of * | ^ + const specials = [ + ".", + "+", + "?", + "$", + "{", + "}", + "(", + ")", + "[", + "]", + "\\", + "/", + ]; + const specialsRegex = new RegExp(`[${specials.join("\\")}]`, "g"); + return str.replace(specialsRegex, "\\$&"); + }; + /** + * Converts :matches-css() arg property value match to regexp. + * + * @param rawValue Style match value pattern. + * + * @returns Arg of :matches-css() converted to regular expression. + */ + const convertStyleMatchValueToRegexp = (rawValue) => { + let value; + if (rawValue.startsWith(SLASH) && rawValue.endsWith(SLASH)) { + // For regex patterns double quotes `"` and backslashes `\` should be escaped + value = addUrlQuotesTo.regexpArg(rawValue); + value = value.slice(1, -1); + } else { + // For non-regex patterns parentheses `(` `)` and square brackets `[` `]` + // should be unescaped, because their escaping in filter rules is required + value = addUrlQuotesTo.noneRegexpArg(rawValue); + value = value.replace(/\\([\\()[\]"])/g, "$1"); + value = escapeRegExp(value); // e.g. div:matches-css(background-image: url(data:*)) + value = replaceAll(value, ASTERISK, REGEXP_ANY_SYMBOL); + } + return new RegExp(value, "i"); + }; + /** + * Makes some properties values compatible. + * + * @param propertyName Name of style property. + * @param propertyValue Value of style property. + * + * @returns Normalized values for some CSS properties. + */ + const normalizePropertyValue = (propertyName, propertyValue) => { + let normalized = ""; + switch (propertyName) { + case CSS_PROPERTY.BACKGROUND: + case CSS_PROPERTY.BACKGROUND_IMAGE: + // sometimes url property does not have quotes + // so we add them for consistent matching + normalized = addUrlPropertyQuotes(propertyValue); + break; + case CSS_PROPERTY.CONTENT: + normalized = removeContentQuotes(propertyValue); + break; + case CSS_PROPERTY.OPACITY: + // https://bugs.webkit.org/show_bug.cgi?id=93445 + normalized = isSafariBrowser + ? (Math.round(parseFloat(propertyValue) * 100) / 100).toString() + : propertyValue; + break; + default: + normalized = propertyValue; + } + return normalized; + }; + /** + * Returns domElement style property value + * by css property name and standard pseudo-element. + * + * @param domElement DOM element. + * @param propertyName CSS property name. + * @param regularPseudoElement Standard pseudo-element — '::before', '::after' etc. + * + * @returns String containing the value of a specified CSS property. + */ + const getComputedStylePropertyValue = ( + domElement, + propertyName, + regularPseudoElement + ) => { + const style = window.getComputedStyle(domElement, regularPseudoElement); + const propertyValue = style.getPropertyValue(propertyName); + return normalizePropertyValue(propertyName, propertyValue); + }; + /** + * Parses arg of absolute pseudo-class into 'name' and 'value' if set. + * + * Used for :matches-css() - with COLON as separator, + * for :matches-attr() and :matches-property() - with EQUAL_SIGN as separator. + * + * @param pseudoArg Arg of pseudo-class. + * @param separator Divider symbol. + * + * @returns Parsed 'matches' pseudo-class arg data. + */ + const getPseudoArgData = (pseudoArg, separator) => { + const index = pseudoArg.indexOf(separator); + let name; + let value; + if (index > -1) { + name = pseudoArg.substring(0, index).trim(); + value = pseudoArg.substring(index + 1).trim(); + } else { + name = pseudoArg; + } + return { + name, + value, + }; + }; + /** + * Parses :matches-css() pseudo-class arg + * where regular pseudo-element can be a part of arg + * e.g. 'div:matches-css(before, color: rgb(255, 255, 255))' <-- obsolete `:matches-css-before()`. + * + * @param pseudoName Pseudo-class name. + * @param rawArg Pseudo-class arg. + * + * @returns Parsed :matches-css() pseudo-class arg data. + * @throws An error on invalid `rawArg`. + */ + const parseStyleMatchArg = (pseudoName, rawArg) => { + const { name, value } = getPseudoArgData(rawArg, COMMA); + let regularPseudoElement = name; + let styleMatchArg = value; // check whether the string part before the separator is valid regular pseudo-element, + // otherwise `regularPseudoElement` is null, and `styleMatchArg` is rawArg + if (!Object.values(REGULAR_PSEUDO_ELEMENTS).includes(name)) { + regularPseudoElement = null; + styleMatchArg = rawArg; + } + if (!styleMatchArg) { + throw new Error( + `Required style property argument part is missing in :${pseudoName}() arg: '${rawArg}'` + ); + } // if regularPseudoElement is not `null` + if (regularPseudoElement) { + // pseudo-element should have two colon marks for Window.getComputedStyle() due to the syntax: + // https://www.w3.org/TR/selectors-4/#pseudo-element-syntax + // ':matches-css(before, content: ads)' ->> '::before' + regularPseudoElement = `${COLON}${COLON}${regularPseudoElement}`; + } + return { + regularPseudoElement, + styleMatchArg, + }; + }; + /** + * Checks whether the domElement is matched by :matches-css() arg. + * + * @param argsData Pseudo-class name, arg, and dom element to check. + * + @returns True if DOM element is matched. + * @throws An error on invalid pseudo-class arg. + */ + const isStyleMatched = (argsData) => { + const { pseudoName, pseudoArg, domElement } = argsData; + const { regularPseudoElement, styleMatchArg } = parseStyleMatchArg( + pseudoName, + pseudoArg + ); + const { name: matchName, value: matchValue } = getPseudoArgData( + styleMatchArg, + COLON + ); + if (!matchName || !matchValue) { + throw new Error( + `Required property name or value is missing in :${pseudoName}() arg: '${styleMatchArg}'` + ); + } + let valueRegexp; + try { + valueRegexp = convertStyleMatchValueToRegexp(matchValue); + } catch (e) { + logger.error(getErrorMessage(e)); + throw new Error( + `Invalid argument of :${pseudoName}() pseudo-class: '${styleMatchArg}'` + ); + } + const value = getComputedStylePropertyValue( + domElement, + matchName, + regularPseudoElement + ); + return valueRegexp && valueRegexp.test(value); + }; + /** + * Validates string arg for :matches-attr() and :matches-property(). + * + * @param arg Pseudo-class arg. + * + * @returns True if 'matches' pseudo-class string arg is valid. + */ + const validateStrMatcherArg = (arg) => { + if (arg.includes(SLASH)) { + return false; + } + if (!/^[\w-]+$/.test(arg)) { + return false; + } + return true; + }; + /** + * Returns valid arg for :matches-attr() and :matcher-property(). + * + * @param rawArg Arg pattern. + * @param [isWildcardAllowed=false] Flag for wildcard (`*`) using as pseudo-class arg. + * + * @returns Valid arg for :matches-attr() and :matcher-property(). + * @throws An error on invalid `rawArg`. + */ + const getValidMatcherArg = function (rawArg) { + let isWildcardAllowed = + arguments.length > 1 && arguments[1] !== undefined + ? arguments[1] + : false; + // if rawArg is missing for pseudo-class + // e.g. :matches-attr() + // error will be thrown before getValidMatcherArg() is called: + // name or arg is missing in AbsolutePseudoClass + let arg; + if ( + rawArg.length > 1 && + rawArg.startsWith(DOUBLE_QUOTE) && + rawArg.endsWith(DOUBLE_QUOTE) + ) { + rawArg = rawArg.slice(1, -1); + } + if (rawArg === "") { + // e.g. :matches-property("") + throw new Error("Argument should be specified. Empty arg is invalid."); + } + if (rawArg.startsWith(SLASH) && rawArg.endsWith(SLASH)) { + // e.g. :matches-property("//") + if (rawArg.length > 2) { + arg = toRegExp(rawArg); + } else { + throw new Error(`Invalid regexp: '${rawArg}'`); + } + } else if (rawArg.includes(ASTERISK)) { + if (rawArg === ASTERISK && !isWildcardAllowed) { + // e.g. :matches-attr(*) + throw new Error(`Argument should be more specific than ${rawArg}`); + } + arg = replaceAll(rawArg, ASTERISK, REGEXP_ANY_SYMBOL); + arg = new RegExp(arg); + } else { + if (!validateStrMatcherArg(rawArg)) { + throw new Error(`Invalid argument: '${rawArg}'`); + } + arg = rawArg; + } + return arg; + }; + /** + * Parses pseudo-class argument and returns parsed data. + * + * @param pseudoName Extended pseudo-class name. + * @param pseudoArg Extended pseudo-class argument. + * + * @returns Parsed pseudo-class argument data. + * @throws An error if attribute name is missing in pseudo-class arg. + */ + const getRawMatchingData = (pseudoName, pseudoArg) => { + const { name: rawName, value: rawValue } = getPseudoArgData( + pseudoArg, + EQUAL_SIGN + ); + if (!rawName) { + throw new Error( + `Required attribute name is missing in :${pseudoName} arg: ${pseudoArg}` + ); + } + return { + rawName, + rawValue, + }; + }; + /** + * Checks whether the domElement is matched by :matches-attr() arg. + * + * @param argsData Pseudo-class name, arg, and dom element to check. + * + @returns True if DOM element is matched. + * @throws An error on invalid arg of pseudo-class. + */ + const isAttributeMatched = (argsData) => { + const { pseudoName, pseudoArg, domElement } = argsData; + const elementAttributes = domElement.attributes; // no match if dom element has no attributes + if (elementAttributes.length === 0) { + return false; + } + const { rawName: rawAttrName, rawValue: rawAttrValue } = + getRawMatchingData(pseudoName, pseudoArg); + let attrNameMatch; + try { + attrNameMatch = getValidMatcherArg(rawAttrName); + } catch (e) { + const errorMessage = getErrorMessage(e); + logger.error(errorMessage); + throw new SyntaxError(errorMessage); + } + let isMatched = false; + let i = 0; + while (i < elementAttributes.length && !isMatched) { + const attr = elementAttributes[i]; + if (!attr) { + break; + } + const isNameMatched = + attrNameMatch instanceof RegExp + ? attrNameMatch.test(attr.name) + : attrNameMatch === attr.name; + if (!rawAttrValue) { + // for rules with no attribute value specified + // e.g. :matches-attr("/regex/") or :matches-attr("attr-name") + isMatched = isNameMatched; + } else { + let attrValueMatch; + try { + attrValueMatch = getValidMatcherArg(rawAttrValue); + } catch (e) { + const errorMessage = getErrorMessage(e); + logger.error(errorMessage); + throw new SyntaxError(errorMessage); + } + const isValueMatched = + attrValueMatch instanceof RegExp + ? attrValueMatch.test(attr.value) + : attrValueMatch === attr.value; + isMatched = isNameMatched && isValueMatched; + } + i += 1; + } + return isMatched; + }; + /** + * Parses raw :matches-property() arg which may be chain of properties. + * + * @param input Argument of :matches-property(). + * + * @returns Arg of :matches-property() as array of strings or regular expressions. + * @throws An error on invalid chain. + */ + const parseRawPropChain = (input) => { + if ( + input.length > 1 && + input.startsWith(DOUBLE_QUOTE) && + input.endsWith(DOUBLE_QUOTE) + ) { + input = input.slice(1, -1); + } + const chainChunks = input.split(DOT); + const chainPatterns = []; + let patternBuffer = ""; + let isRegexpPattern = false; + let i = 0; + while (i < chainChunks.length) { + const chunk = getItemByIndex( + chainChunks, + i, + `Invalid pseudo-class arg: '${input}'` + ); + if ( + chunk.startsWith(SLASH) && + chunk.endsWith(SLASH) && + chunk.length > 2 + ) { + // regexp pattern with no dot in it, e.g. /propName/ + chainPatterns.push(chunk); + } else if (chunk.startsWith(SLASH)) { + // if chunk is a start of regexp pattern + isRegexpPattern = true; + patternBuffer += chunk; + } else if (chunk.endsWith(SLASH)) { + isRegexpPattern = false; // restore dot removed while splitting + // e.g. testProp./.{1,5}/ + patternBuffer += `.${chunk}`; + chainPatterns.push(patternBuffer); + patternBuffer = ""; + } else { + // if there are few dots in regexp pattern + // so chunk might be in the middle of it + if (isRegexpPattern) { + patternBuffer += chunk; + } else { + // otherwise it is string pattern + chainPatterns.push(chunk); + } + } + i += 1; + } + if (patternBuffer.length > 0) { + throw new Error(`Invalid regexp property pattern '${input}'`); + } + const chainMatchPatterns = chainPatterns.map((pattern) => { + if (pattern.length === 0) { + // e.g. '.prop.id' or 'nested..test' + throw new Error( + `Empty pattern '${pattern}' is invalid in chain '${input}'` + ); + } + let validPattern; + try { + validPattern = getValidMatcherArg(pattern, true); + } catch (e) { + logger.error(getErrorMessage(e)); + throw new Error( + `Invalid property pattern '${pattern}' in property chain '${input}'` + ); + } + return validPattern; + }); + return chainMatchPatterns; + }; + /** + * Checks if the property exists in the base object (recursively). + * + * @param base Element to check. + * @param chain Array of objects - parsed string property chain. + * @param [output=[]] Result acc. + * + * @returns Array of parsed data — representation of `base`-related `chain`. + */ + const filterRootsByRegexpChain = function (base, chain) { + let output = + arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : []; + const tempProp = getFirst(chain); + if (chain.length === 1) { + let key; + for (key in base) { + if (tempProp instanceof RegExp) { + if (tempProp.test(key)) { + output.push({ + base, + prop: key, + value: base[key], + }); + } + } else if (tempProp === key) { + output.push({ + base, + prop: tempProp, + value: base[key], + }); + } + } + return output; + } // if there is a regexp prop in input chain + // e.g. 'unit./^ad.+/.src' for 'unit.ad-1gf2.src unit.ad-fgd34.src'), + // every base keys should be tested by regexp and it can be more that one results + if (tempProp instanceof RegExp) { + const nextProp = chain.slice(1); + const baseKeys = []; + for (const key in base) { + if (tempProp.test(key)) { + baseKeys.push(key); + } + } + baseKeys.forEach((key) => { + var _Object$getOwnPropert; + const item = + (_Object$getOwnPropert = Object.getOwnPropertyDescriptor( + base, + key + )) === null || _Object$getOwnPropert === void 0 + ? void 0 + : _Object$getOwnPropert.value; + filterRootsByRegexpChain(item, nextProp, output); + }); + } + if (base && typeof tempProp === "string") { + var _Object$getOwnPropert2; + const nextBase = + (_Object$getOwnPropert2 = Object.getOwnPropertyDescriptor( + base, + tempProp + )) === null || _Object$getOwnPropert2 === void 0 + ? void 0 + : _Object$getOwnPropert2.value; + chain = chain.slice(1); + if (nextBase !== undefined) { + filterRootsByRegexpChain(nextBase, chain, output); + } + } + return output; + }; + /** + * Checks whether the domElement is matched by :matches-property() arg. + * + * @param argsData Pseudo-class name, arg, and dom element to check. + * + @returns True if DOM element is matched. + * @throws An error on invalid prop in chain. + */ + const isPropertyMatched = (argsData) => { + const { pseudoName, pseudoArg, domElement } = argsData; + const { rawName: rawPropertyName, rawValue: rawPropertyValue } = + getRawMatchingData(pseudoName, pseudoArg); // chained property name cannot include '/' or '.' + // so regex prop names with such escaped characters are invalid + if (rawPropertyName.includes("\\/") || rawPropertyName.includes("\\.")) { + throw new Error( + `Invalid :${pseudoName} name pattern: ${rawPropertyName}` + ); + } + let propChainMatches; + try { + propChainMatches = parseRawPropChain(rawPropertyName); + } catch (e) { + const errorMessage = getErrorMessage(e); + logger.error(errorMessage); + throw new SyntaxError(errorMessage); + } + const ownerObjArr = filterRootsByRegexpChain( + domElement, + propChainMatches + ); + if (ownerObjArr.length === 0) { + return false; + } + let isMatched = true; + if (rawPropertyValue) { + let propValueMatch; + try { + propValueMatch = getValidMatcherArg(rawPropertyValue); + } catch (e) { + const errorMessage = getErrorMessage(e); + logger.error(errorMessage); + throw new SyntaxError(errorMessage); + } + if (propValueMatch) { + for (let i = 0; i < ownerObjArr.length; i += 1) { + var _ownerObjArr$i; + const realValue = + (_ownerObjArr$i = ownerObjArr[i]) === null || + _ownerObjArr$i === void 0 + ? void 0 + : _ownerObjArr$i.value; + if (propValueMatch instanceof RegExp) { + isMatched = propValueMatch.test(convertTypeIntoString(realValue)); + } else { + // handle 'null' and 'undefined' property values set as string + if (realValue === "null" || realValue === "undefined") { + isMatched = propValueMatch === realValue; + break; + } + isMatched = convertTypeFromString(propValueMatch) === realValue; + } + if (isMatched) { + break; + } + } + } + } + return isMatched; + }; + /** + * Checks whether the textContent is matched by :contains arg. + * + * @param argsData Pseudo-class name, arg, and dom element to check. + * + @returns True if DOM element is matched. + * @throws An error on invalid arg of pseudo-class. + */ + const isTextMatched = (argsData) => { + const { pseudoName, pseudoArg, domElement } = argsData; + const textContent = getNodeTextContent(domElement); + let isTextContentMatched; + let pseudoArgToMatch = pseudoArg; + if ( + pseudoArgToMatch.startsWith(SLASH) && + REGEXP_WITH_FLAGS_REGEXP.test(pseudoArgToMatch) + ) { + // regexp arg + const flagsIndex = pseudoArgToMatch.lastIndexOf("/"); + const flagsStr = pseudoArgToMatch.substring(flagsIndex + 1); + pseudoArgToMatch = pseudoArgToMatch + .substring(0, flagsIndex + 1) + .slice(1, -1) + .replace(/\\([\\"])/g, "$1"); + let regex; + try { + regex = new RegExp(pseudoArgToMatch, flagsStr); + } catch (e) { + throw new Error( + `Invalid argument of :${pseudoName}() pseudo-class: ${pseudoArg}` + ); + } + isTextContentMatched = regex.test(textContent); + } else { + // none-regexp arg + pseudoArgToMatch = pseudoArgToMatch.replace(/\\([\\()[\]"])/g, "$1"); + isTextContentMatched = textContent.includes(pseudoArgToMatch); + } + return isTextContentMatched; + }; + /** + * Validates number arg for :nth-ancestor() and :upward() pseudo-classes. + * + * @param rawArg Raw arg of pseudo-class. + * @param pseudoName Pseudo-class name. + * + * @returns Valid number arg for :nth-ancestor() and :upward(). + * @throws An error on invalid `rawArg`. + */ + const getValidNumberAncestorArg = (rawArg, pseudoName) => { + const deep = Number(rawArg); + if (Number.isNaN(deep) || deep < 1 || deep >= 256) { + throw new Error( + `Invalid argument of :${pseudoName} pseudo-class: '${rawArg}'` + ); + } + return deep; + }; + /** + * Returns nth ancestor by 'deep' number arg OR undefined if ancestor range limit exceeded. + * + * @param domElement DOM element to find ancestor for. + * @param nth Depth up to needed ancestor. + * @param pseudoName Pseudo-class name. + * + * @returns Ancestor element found in DOM, or null if not found. + * @throws An error on invalid `nth` arg. + */ + const getNthAncestor = (domElement, nth, pseudoName) => { + let ancestor = null; + let i = 0; + while (i < nth) { + ancestor = domElement.parentElement; + if (!ancestor) { + throw new Error( + `Out of DOM: Argument of :${pseudoName}() pseudo-class is too big — '${nth}'.` + ); + } + domElement = ancestor; + i += 1; + } + return ancestor; + }; + /** + * Validates standard CSS selector. + * + * @param selector Standard selector. + * + * @returns True if standard CSS selector is valid. + */ + const validateStandardSelector = (selector) => { + let isValid; + try { + document.querySelectorAll(selector); + isValid = true; + } catch (e) { + isValid = false; + } + return isValid; + }; + /** + * Wrapper to run matcher `callback` with `args` + * and throw error with `errorMessage` if `callback` run fails. + * + * @param callback Matcher callback. + * @param argsData Args needed for matcher callback. + * @param errorMessage Error message. + * + * @returns True if `callback` returns true. + * @throws An error if `callback` fails. + */ + const matcherWrapper = (callback, argsData, errorMessage) => { + let isMatched; + try { + isMatched = callback(argsData); + } catch (e) { + logger.error(getErrorMessage(e)); + throw new Error(errorMessage); + } + return isMatched; + }; + /** + * Generates common error message to throw while matching element `propDesc`. + * + * @param propDesc Text to describe what element 'prop' pseudo-class is trying to match. + * @param pseudoName Pseudo-class name. + * @param pseudoArg Pseudo-class arg. + * + * @returns Generated error message string. + */ + const getAbsolutePseudoError = (propDesc, pseudoName, pseudoArg) => { + // eslint-disable-next-line max-len + return `${MATCHING_ELEMENT_ERROR_PREFIX} ${propDesc}, may be invalid :${pseudoName}() pseudo-class arg: '${pseudoArg}'`; + }; + /** + * Checks whether the domElement is matched by absolute extended pseudo-class argument. + * + * @param domElement Page element. + * @param pseudoName Pseudo-class name. + * @param pseudoArg Pseudo-class arg. + * + * @returns True if `domElement` is matched by absolute pseudo-class. + * @throws An error on unknown absolute pseudo-class. + */ + const isMatchedByAbsolutePseudo = (domElement, pseudoName, pseudoArg) => { + let argsData; + let errorMessage; + let callback; + switch (pseudoName) { + case CONTAINS_PSEUDO: + case HAS_TEXT_PSEUDO: + case ABP_CONTAINS_PSEUDO: + callback = isTextMatched; + argsData = { + pseudoName, + pseudoArg, + domElement, + }; + errorMessage = getAbsolutePseudoError( + "text content", + pseudoName, + pseudoArg + ); + break; + case MATCHES_CSS_PSEUDO: + case MATCHES_CSS_AFTER_PSEUDO: + case MATCHES_CSS_BEFORE_PSEUDO: + callback = isStyleMatched; + argsData = { + pseudoName, + pseudoArg, + domElement, + }; + errorMessage = getAbsolutePseudoError("style", pseudoName, pseudoArg); + break; + case MATCHES_ATTR_PSEUDO_CLASS_MARKER: + callback = isAttributeMatched; + argsData = { + domElement, + pseudoName, + pseudoArg, + }; + errorMessage = getAbsolutePseudoError( + "attributes", + pseudoName, + pseudoArg + ); + break; + case MATCHES_PROPERTY_PSEUDO_CLASS_MARKER: + callback = isPropertyMatched; + argsData = { + domElement, + pseudoName, + pseudoArg, + }; + errorMessage = getAbsolutePseudoError( + "properties", + pseudoName, + pseudoArg + ); + break; + default: + throw new Error(`Unknown absolute pseudo-class :${pseudoName}()`); + } + return matcherWrapper(callback, argsData, errorMessage); + }; + const findByAbsolutePseudoPseudo = { + /** + * Returns list of nth ancestors relative to every dom node from domElements list. + * + * @param domElements DOM elements. + * @param rawPseudoArg Number arg of :nth-ancestor() or :upward() pseudo-class. + * @param pseudoName Pseudo-class name. + * + * @returns Array of ancestor DOM elements. + */ + nthAncestor: (domElements, rawPseudoArg, pseudoName) => { + const deep = getValidNumberAncestorArg(rawPseudoArg, pseudoName); + const ancestors = domElements + .map((domElement) => { + let ancestor = null; + try { + ancestor = getNthAncestor(domElement, deep, pseudoName); + } catch (e) { + logger.error(getErrorMessage(e)); + } + return ancestor; + }) + .filter(isHtmlElement); + return ancestors; + }, + /** + * Returns list of elements by xpath expression, evaluated on every dom node from domElements list. + * + * @param domElements DOM elements. + * @param rawPseudoArg Arg of :xpath() pseudo-class. + * + * @returns Array of DOM elements matched by xpath expression. + */ + xpath: (domElements, rawPseudoArg) => { + const foundElements = domElements.map((domElement) => { + const result = []; + let xpathResult; + try { + xpathResult = document.evaluate( + rawPseudoArg, + domElement, + null, + window.XPathResult.UNORDERED_NODE_ITERATOR_TYPE, + null + ); + } catch (e) { + logger.error(getErrorMessage(e)); + throw new Error( + `Invalid argument of :xpath() pseudo-class: '${rawPseudoArg}'` + ); + } + let node = xpathResult.iterateNext(); + while (node) { + if (isHtmlElement(node)) { + result.push(node); + } + node = xpathResult.iterateNext(); + } + return result; + }); + return flatten(foundElements); + }, + /** + * Returns list of closest ancestors relative to every dom node from domElements list. + * + * @param domElements DOM elements. + * @param rawPseudoArg Standard selector arg of :upward() pseudo-class. + * + * @returns Array of closest ancestor DOM elements. + * @throws An error if `rawPseudoArg` is not a valid standard selector. + */ + upward: (domElements, rawPseudoArg) => { + if (!validateStandardSelector(rawPseudoArg)) { + throw new Error( + `Invalid argument of :upward pseudo-class: '${rawPseudoArg}'` + ); + } + const closestAncestors = domElements + .map((domElement) => { + // closest to parent element should be found + // otherwise `.base:upward(.base)` will return itself too, not only ancestor + const parent = domElement.parentElement; + if (!parent) { + return null; + } + return parent.closest(rawPseudoArg); + }) + .filter(isHtmlElement); + return closestAncestors; + }, + }; + /** + * Calculated selector text which is needed to :has(), :is() and :not() pseudo-classes. + * Contains calculated part (depends on the processed element) + * and value of RegularSelector which is next to selector by. + * + * Native Document.querySelectorAll() does not select exact descendant elements + * but match all page elements satisfying the selector, + * so extra specification is needed for proper descendants selection + * e.g. 'div:has(> img)'. + * + * Its calculation depends on extended selector. + */ + /** + * Combined `:scope` pseudo-class and **child** combinator — `:scope>`. + */ + const scopeDirectChildren = `${SCOPE_CSS_PSEUDO_CLASS}${CHILD_COMBINATOR}`; + /** + * Combined `:scope` pseudo-class and **descendant** combinator — `:scope `. + */ + const scopeAnyChildren = `${SCOPE_CSS_PSEUDO_CLASS}${DESCENDANT_COMBINATOR}`; + /** + * Type for relative pseudo-class helpers args. + */ + /** + * Returns the first of RegularSelector child node for `selectorNode`. + * + * @param selectorNode Ast Selector node. + * @param pseudoName Name of relative pseudo-class. + * + * @returns Ast RegularSelector node. + */ + const getFirstInnerRegularChild = (selectorNode, pseudoName) => { + return getFirstRegularChild( + selectorNode.children, + `RegularSelector is missing for :${pseudoName}() pseudo-class` + ); + }; // TODO: fix for+ // https://github.com/AdguardTeam/ExtendedCss/issues/154 + /** + * Checks whether the element has all relative elements specified by pseudo-class arg. + * Used for :has() pseudo-class. + * + * @param argsData Relative pseudo-class helpers args data. + * + * @returns True if **all selectors** from argsData.relativeSelectorList is **matched** for argsData.element. + */ + const hasRelativesBySelectorList = (argsData) => { + const { element, relativeSelectorList, pseudoName } = argsData; + return relativeSelectorList.children // Array.every() is used here as each Selector node from SelectorList should exist on page + .every((selectorNode) => { + // selectorList.children always starts with regular selector as any selector generally + const relativeRegularSelector = getFirstInnerRegularChild( + selectorNode, + pseudoName + ); + let specifiedSelector = ""; + let rootElement = null; + const regularSelector = getNodeValue(relativeRegularSelector); + if ( + regularSelector.startsWith(NEXT_SIBLING_COMBINATOR) || + regularSelector.startsWith(SUBSEQUENT_SIBLING_COMBINATOR) + ) { + /** + * For matching the element by "element:has(+ next-sibling)" and "element:has(~ sibling)" + * we check whether the element's parentElement has specific direct child combination, + * e.g. 'h1:has(+ .share)' -> `h1Node.parentElement.querySelectorAll(':scope > h1 + .share')`. + * + * @see {@link https://www.w3.org/TR/selectors-4/#relational} + */ + rootElement = element.parentElement; + const elementSelectorText = getElementSelectorDesc(element); + specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${regularSelector}`; + } else if (regularSelector === ASTERISK) { + /** + * :scope specification is needed for proper descendants selection + * as native element.querySelectorAll() does not select exact element descendants + * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')`. + * + * For 'any selector' as arg of relative simplicity should be set for all inner elements + * e.g. 'div:has(*)' -> `divNode.querySelectorAll(':scope *')` + * which means empty div with no child element. + */ + rootElement = element; + specifiedSelector = `${scopeAnyChildren}${ASTERISK}`; + } else { + /** + * As it described above, inner elements should be found using `:scope` pseudo-class + * e.g. 'a:has(> img)' -> `aNode.querySelectorAll(':scope > img')` + * OR '.block(div > span)' -> `blockClassNode.querySelectorAll(':scope div > span')`. + */ + specifiedSelector = `${scopeAnyChildren}${regularSelector}`; + rootElement = element; + } + if (!rootElement) { + throw new Error( + `Selection by :${pseudoName}() pseudo-class is not possible` + ); + } + let relativeElements; + try { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + relativeElements = getElementsForSelectorNode( + selectorNode, + rootElement, + specifiedSelector + ); + } catch (e) { + logger.error(getErrorMessage(e)); // fail for invalid selector + throw new Error( + `Invalid selector for :${pseudoName}() pseudo-class: '${regularSelector}'` + ); + } + return relativeElements.length > 0; + }); + }; + /** + * Checks whether the element is an any element specified by pseudo-class arg. + * Used for :is() pseudo-class. + * + * @param argsData Relative pseudo-class helpers args data. + * + * @returns True if **any selector** from argsData.relativeSelectorList is **matched** for argsData.element. + */ + const isAnyElementBySelectorList = (argsData) => { + const { element, relativeSelectorList, pseudoName } = argsData; + return relativeSelectorList.children // Array.some() is used here as any selector from selector list should exist on page + .some((selectorNode) => { + // selectorList.children always starts with regular selector + const relativeRegularSelector = getFirstInnerRegularChild( + selectorNode, + pseudoName + ); + /** + * For checking the element by 'div:is(.banner)' + * we check whether the element's parentElement has any specific direct child. + */ + const rootElement = getParent( + element, + `Selection by :${pseudoName}() pseudo-class is not possible` + ); + /** + * So we calculate the element "description" by it's tagname and attributes for targeting + * and use it to specify the selection + * e.g. `div:is(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`. + */ + const specifiedSelector = `${scopeDirectChildren}${getNodeValue( + relativeRegularSelector + )}`; + let anyElements; + try { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + anyElements = getElementsForSelectorNode( + selectorNode, + rootElement, + specifiedSelector + ); + } catch (e) { + // do not fail on invalid selectors for :is() + return false; + } // TODO: figure out how to handle complex selectors with extended pseudo-classes + // (check readme - extended-css-is-limitations) + // because `element` and `anyElements` may be from different DOM levels + return anyElements.includes(element); + }); + }; + /** + * Checks whether the element is not an element specified by pseudo-class arg. + * Used for :not() pseudo-class. + * + * @param argsData Relative pseudo-class helpers args data. + * + * @returns True if **any selector** from argsData.relativeSelectorList is **not matched** for argsData.element. + */ + const notElementBySelectorList = (argsData) => { + const { element, relativeSelectorList, pseudoName } = argsData; + return relativeSelectorList.children // Array.every() is used here as element should not be selected by any selector from selector list + .every((selectorNode) => { + // selectorList.children always starts with regular selector + const relativeRegularSelector = getFirstInnerRegularChild( + selectorNode, + pseudoName + ); + /** + * For checking the element by 'div:not([data="content"]) + * we check whether the element's parentElement has any specific direct child. + */ + const rootElement = getParent( + element, + `Selection by :${pseudoName}() pseudo-class is not possible` + ); + /** + * So we calculate the element "description" by it's tagname and attributes for targeting + * and use it to specify the selection + * e.g. `div:not(.banner)` --> `divNode.parentElement.querySelectorAll(':scope > .banner')`. + */ + const specifiedSelector = `${scopeDirectChildren}${getNodeValue( + relativeRegularSelector + )}`; + let anyElements; + try { + // eslint-disable-next-line @typescript-eslint/no-use-before-define + anyElements = getElementsForSelectorNode( + selectorNode, + rootElement, + specifiedSelector + ); + } catch (e) { + // fail on invalid selectors for :not() + logger.error(getErrorMessage(e)); // eslint-disable-next-line max-len + throw new Error( + `Invalid selector for :${pseudoName}() pseudo-class: '${getNodeValue( + relativeRegularSelector + )}'` + ); + } // TODO: figure out how to handle up-looking pseudo-classes inside :not() + // (check readme - extended-css-not-limitations) + // because `element` and `anyElements` may be from different DOM levels + return !anyElements.includes(element); + }); + }; + /** + * Selects dom elements by value of RegularSelector. + * + * @param regularSelectorNode RegularSelector node. + * @param root Root DOM element. + * @param specifiedSelector @see {@link SpecifiedSelector}. + * + * @returns Array of DOM elements. + * @throws An error if RegularSelector node value is an invalid selector. + */ + const getByRegularSelector = ( + regularSelectorNode, + root, + specifiedSelector + ) => { + const selectorText = specifiedSelector + ? specifiedSelector + : getNodeValue(regularSelectorNode); + let selectedElements = []; + try { + selectedElements = Array.from(root.querySelectorAll(selectorText)); + } catch (e) { + throw new Error( + `Error: unable to select by '${selectorText}' — ${getErrorMessage(e)}` + ); + } + return selectedElements; + }; + /** + * Returns list of dom elements filtered or selected by ExtendedSelector node. + * + * @param domElements Array of DOM elements. + * @param extendedSelectorNode ExtendedSelector node. + * + * @returns Array of DOM elements. + * @throws An error on unknown pseudo-class, + * absent or invalid arg of extended pseudo-class, etc. + */ + const getByExtendedSelector = (domElements, extendedSelectorNode) => { + let foundElements = []; + const extendedPseudoClassNode = getPseudoClassNode(extendedSelectorNode); + const pseudoName = getNodeName(extendedPseudoClassNode); + if (isAbsolutePseudoClass(pseudoName)) { + // absolute extended pseudo-classes should have an argument + const absolutePseudoArg = getNodeValue( + extendedPseudoClassNode, + `Missing arg for :${pseudoName}() pseudo-class` + ); + if (pseudoName === NTH_ANCESTOR_PSEUDO_CLASS_MARKER) { + // :nth-ancestor() + foundElements = findByAbsolutePseudoPseudo.nthAncestor( + domElements, + absolutePseudoArg, + pseudoName + ); + } else if (pseudoName === XPATH_PSEUDO_CLASS_MARKER) { + // :xpath() + try { + document.createExpression(absolutePseudoArg, null); + } catch (e) { + throw new Error( + `Invalid argument of :${pseudoName}() pseudo-class: '${absolutePseudoArg}'` + ); + } + foundElements = findByAbsolutePseudoPseudo.xpath( + domElements, + absolutePseudoArg + ); + } else if (pseudoName === UPWARD_PSEUDO_CLASS_MARKER) { + // :upward() + if (Number.isNaN(Number(absolutePseudoArg))) { + // so arg is selector, not a number + foundElements = findByAbsolutePseudoPseudo.upward( + domElements, + absolutePseudoArg + ); + } else { + foundElements = findByAbsolutePseudoPseudo.nthAncestor( + domElements, + absolutePseudoArg, + pseudoName + ); + } + } else { + // all other absolute extended pseudo-classes + // e.g. contains, matches-attr, etc. + foundElements = domElements.filter((element) => { + return isMatchedByAbsolutePseudo( + element, + pseudoName, + absolutePseudoArg + ); + }); + } + } else if (isRelativePseudoClass(pseudoName)) { + const relativeSelectorList = getRelativeSelectorListNode( + extendedPseudoClassNode + ); + let relativePredicate; + switch (pseudoName) { + case HAS_PSEUDO_CLASS_MARKER: + case ABP_HAS_PSEUDO_CLASS_MARKER: + relativePredicate = (element) => + hasRelativesBySelectorList({ + element, + relativeSelectorList, + pseudoName, + }); + break; + case IS_PSEUDO_CLASS_MARKER: + relativePredicate = (element) => + isAnyElementBySelectorList({ + element, + relativeSelectorList, + pseudoName, + }); + break; + case NOT_PSEUDO_CLASS_MARKER: + relativePredicate = (element) => + notElementBySelectorList({ + element, + relativeSelectorList, + pseudoName, + }); + break; + default: + throw new Error(`Unknown relative pseudo-class: '${pseudoName}'`); + } + foundElements = domElements.filter(relativePredicate); + } else { + // extra check is parser missed something + throw new Error(`Unknown extended pseudo-class: '${pseudoName}'`); + } + return foundElements; + }; + /** + * Returns list of dom elements which is selected by RegularSelector value. + * + * @param domElements Array of DOM elements. + * @param regularSelectorNode RegularSelector node. + * + * @returns Array of DOM elements. + * @throws An error if RegularSelector has not value. + */ + const getByFollowingRegularSelector = ( + domElements, + regularSelectorNode + ) => { + // array of arrays because of Array.map() later + let foundElements = []; + const value = getNodeValue(regularSelectorNode); + if (value.startsWith(CHILD_COMBINATOR)) { + // e.g. div:has(> img) > .banner + foundElements = domElements.map((root) => { + const specifiedSelector = `${SCOPE_CSS_PSEUDO_CLASS}${value}`; + return getByRegularSelector( + regularSelectorNode, + root, + specifiedSelector + ); + }); + } else if ( + value.startsWith(NEXT_SIBLING_COMBINATOR) || + value.startsWith(SUBSEQUENT_SIBLING_COMBINATOR) + ) { + // e.g. div:has(> img) + .banner + // or div:has(> img) ~ .banner + foundElements = domElements.map((element) => { + const rootElement = element.parentElement; + if (!rootElement) { + // do not throw error if there in no parent for element + // e.g. '*:contains(text)' selects `html` which has no parentElement + return []; + } + const elementSelectorText = getElementSelectorDesc(element); + const specifiedSelector = `${scopeDirectChildren}${elementSelectorText}${value}`; + const selected = getByRegularSelector( + regularSelectorNode, + rootElement, + specifiedSelector + ); + return selected; + }); + } else { + // space-separated regular selector after extended one + // e.g. div:has(> img) .banner + foundElements = domElements.map((root) => { + const specifiedSelector = `${scopeAnyChildren}${getNodeValue( + regularSelectorNode + )}`; + return getByRegularSelector( + regularSelectorNode, + root, + specifiedSelector + ); + }); + } // foundElements should be flattened + // as getByRegularSelector() returns elements array, and Array.map() collects them to array + return flatten(foundElements); + }; + /** + * Returns elements nodes for Selector node. + * As far as any selector always starts with regular part, + * it selects by RegularSelector first and checks found elements later. + * + * Relative pseudo-classes has it's own subtree so getElementsForSelectorNode is called recursively. + * + * 'specifiedSelector' is needed for :has(), :is(), and :not() pseudo-classes + * as native querySelectorAll() does not select exact element descendants even if it is called on 'div' + * e.g. ':scope' specification is needed for proper descendants selection for 'div:has(> img)'. + * So we check `divNode.querySelectorAll(':scope > img').length > 0`. + * + * @param selectorNode Selector node. + * @param root Root DOM element. + * @param specifiedSelector Needed element specification. + * + * @returns Array of DOM elements. + * @throws An error if there is no selectorNodeChild. + */ + const getElementsForSelectorNode = ( + selectorNode, + root, + specifiedSelector + ) => { + let selectedElements = []; + let i = 0; + while (i < selectorNode.children.length) { + const selectorNodeChild = getItemByIndex( + selectorNode.children, + i, + "selectorNodeChild should be specified" + ); + if (i === 0) { + // any selector always starts with regular selector + selectedElements = getByRegularSelector( + selectorNodeChild, + root, + specifiedSelector + ); + } else if (isExtendedSelectorNode(selectorNodeChild)) { + // filter previously selected elements by next selector nodes + selectedElements = getByExtendedSelector( + selectedElements, + selectorNodeChild + ); + } else if (isRegularSelectorNode(selectorNodeChild)) { + selectedElements = getByFollowingRegularSelector( + selectedElements, + selectorNodeChild + ); + } + i += 1; + } + return selectedElements; + }; + /** + * Selects elements by ast. + * + * @param ast Ast of parsed selector. + * @param doc Document. + * + * @returns Array of DOM elements. + */ + const selectElementsByAst = function (ast) { + let doc = + arguments.length > 1 && arguments[1] !== undefined + ? arguments[1] + : document; + const selectedElements = []; // ast root is SelectorList node; + // it has Selector nodes as children which should be processed separately + ast.children.forEach((selectorNode) => { + selectedElements.push(...getElementsForSelectorNode(selectorNode, doc)); + }); // selectedElements should be flattened as it is array of arrays with elements + const uniqueElements = [...new Set(flatten(selectedElements))]; + return uniqueElements; + }; + /** + * Class of ExtCssDocument is needed for caching. + * For making cache related to each new instance of class, not global. + */ + class ExtCssDocument { + /** + * Cache with selectors and their AST parsing results. + */ + /** + * Creates new ExtCssDocument and inits new `astCache`. + */ + constructor() { + this.astCache = new Map(); + } + /** + * Saves selector and it's ast to cache. + * + * @param selector Standard or extended selector. + * @param ast Selector ast. + */ + saveAstToCache(selector, ast) { + this.astCache.set(selector, ast); + } + /** + * Returns ast from cache for given selector. + * + * @param selector Standard or extended selector. + * + * @returns Previously parsed ast found in cache, or null if not found. + */ + getAstFromCache(selector) { + const cachedAst = this.astCache.get(selector) || null; + return cachedAst; + } + /** + * Returns selector ast: + * - if cached ast exists — returns it; + * - if no cached ast — saves newly parsed ast to cache and returns it. + * + * @param selector Standard or extended selector. + * + * @returns Ast for `selector`. + */ + getSelectorAst(selector) { + let ast = this.getAstFromCache(selector); + if (!ast) { + ast = parse(selector); + } + this.saveAstToCache(selector, ast); + return ast; + } + /** + * Selects elements by selector. + * + * @param selector Standard or extended selector. + * + * @returns Array of DOM elements. + */ + querySelectorAll(selector) { + const ast = this.getSelectorAst(selector); + return selectElementsByAst(ast); + } + } + const extCssDocument = new ExtCssDocument(); + /** + * Converts array of `entries` to object. + * Object.fromEntries() polyfill because it is not supported by old browsers, e.g. Chrome 55. + * Only first two elements of `entries` array matter, other will be skipped silently. + * + * @see {@link https://caniuse.com/?search=Object.fromEntries} + * + * @param entries Array of pairs. + * + * @returns Object converted from `entries`. + */ + const getObjectFromEntries = (entries) => { + const object = {}; + entries.forEach((el) => { + const [key, value] = el; + object[key] = value; + }); + return object; + }; + const DEBUG_PSEUDO_PROPERTY_KEY = "debug"; + /** + * Checks the presence of :remove() pseudo-class and validates it while parsing the selector part of css rule. + * + * @param rawSelector Selector which may contain :remove() pseudo-class. + * + * @returns Parsed selector data with selector and styles. + * @throws An error on invalid :remove() position. + */ + const parseRemoveSelector = (rawSelector) => { + /** + * No error will be thrown on invalid selector as it will be validated later + * so it's better to explicitly specify 'any' selector for :remove() pseudo-class by '*', + * e.g. '.banner > *:remove()' instead of '.banner > :remove()'. + */ + // ':remove()' + // eslint-disable-next-line max-len + const VALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKET.PARENTHESES.LEFT}${BRACKET.PARENTHESES.RIGHT}`; // ':remove(' - needed for validation rules like 'div:remove(2)' + const INVALID_REMOVE_MARKER = `${COLON}${REMOVE_PSEUDO_MARKER}${BRACKET.PARENTHESES.LEFT}`; + let selector; + let shouldRemove = false; + const firstIndex = rawSelector.indexOf(VALID_REMOVE_MARKER); + if (firstIndex === 0) { + // e.g. ':remove()' + throw new Error( + `${REMOVE_ERROR_PREFIX.NO_TARGET_SELECTOR}: '${rawSelector}'` + ); + } else if (firstIndex > 0) { + if (firstIndex !== rawSelector.lastIndexOf(VALID_REMOVE_MARKER)) { + // rule with more than one :remove() pseudo-class is invalid + // e.g. '.block:remove() > .banner:remove()' + throw new Error( + `${REMOVE_ERROR_PREFIX.MULTIPLE_USAGE}: '${rawSelector}'` + ); + } else if ( + firstIndex + VALID_REMOVE_MARKER.length < + rawSelector.length + ) { + // remove pseudo-class should be last in the rule + // e.g. '.block:remove():upward(2)' + throw new Error( + `${REMOVE_ERROR_PREFIX.INVALID_POSITION}: '${rawSelector}'` + ); + } else { + // valid :remove() pseudo-class position + selector = rawSelector.substring(0, firstIndex); + shouldRemove = true; + } + } else if (rawSelector.includes(INVALID_REMOVE_MARKER)) { + // it is not valid if ':remove()' is absent in rule but just ':remove(' is present + // e.g. 'div:remove(0)' + throw new Error( + `${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${rawSelector}'` + ); + } else { + // there is no :remove() pseudo-class in rule + selector = rawSelector; + } + const stylesOfSelector = shouldRemove + ? [ + { + property: REMOVE_PSEUDO_MARKER, + value: PSEUDO_PROPERTY_POSITIVE_VALUE, + }, + ] + : []; + return { + selector, + stylesOfSelector, + }; + }; + /** + * Parses cropped selector part found before `{`. + * + * @param selectorBuffer Buffered selector to parse. + * @param extCssDoc Needed for caching of selector ast. + * + * @returns Parsed validation data for cropped part of stylesheet which may be a selector. + * @throws An error on unsupported CSS features, e.g. at-rules. + */ + const parseSelectorRulePart = (selectorBuffer, extCssDoc) => { + let selector = selectorBuffer.trim(); + if (selector.startsWith(AT_RULE_MARKER)) { + throw new Error(`${NO_AT_RULE_ERROR_PREFIX}: '${selector}'.`); + } + let removeSelectorData; + try { + removeSelectorData = parseRemoveSelector(selector); + } catch (e) { + logger.error(getErrorMessage(e)); + throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`); + } + let stylesOfSelector = []; + let success = false; + let ast; + try { + selector = removeSelectorData.selector; + stylesOfSelector = removeSelectorData.stylesOfSelector; // validate found selector by parsing it to ast + // so if it is invalid error will be thrown + ast = extCssDoc.getSelectorAst(selector); + success = true; + } catch (e) { + success = false; + } + return { + success, + selector, + ast, + stylesOfSelector, + }; + }; + /** + * Creates a map for storing raw results of css rules parsing. + * Used for merging styles for same selector. + * + * @returns Map where **key** is `selector` + * and **value** is object with `ast` and `styles`. + */ + const createRawResultsMap = () => { + return new Map(); + }; + /** + * Saves rules data for unique selectors. + * + * @param rawResults Previously collected results of parsing. + * @param rawRuleData Parsed rule data. + * + * @throws An error if there is no rawRuleData.styles or rawRuleData.ast. + */ + const saveToRawResults = (rawResults, rawRuleData) => { + const { selector, ast, rawStyles } = rawRuleData; + if (!rawStyles) { + throw new Error(`No style declaration for selector: '${selector}'`); + } + if (!ast) { + throw new Error(`No ast parsed for selector: '${selector}'`); + } + const storedRuleData = rawResults.get(selector); + if (!storedRuleData) { + rawResults.set(selector, { + ast, + styles: rawStyles, + }); + } else { + storedRuleData.styles.push(...rawStyles); + } + }; + /** + * Checks whether the 'remove' property positively set in styles + * with only one positive value - 'true'. + * + * @param styles Array of styles. + * + * @returns True if there is 'remove' property with 'true' value in `styles`. + */ + const isRemoveSetInStyles = (styles) => { + return styles.some((s) => { + return ( + s.property === REMOVE_PSEUDO_MARKER && + s.value === PSEUDO_PROPERTY_POSITIVE_VALUE + ); + }); + }; + /** + * Returns 'debug' property value which is set in styles. + * + * @param styles Array of styles. + * + * @returns Value of 'debug' property if it is set in `styles`, + * or `undefined` if the property is not found. + */ + const getDebugStyleValue = (styles) => { + const debugStyle = styles.find((s) => { + return s.property === DEBUG_PSEUDO_PROPERTY_KEY; + }); + return debugStyle === null || debugStyle === void 0 + ? void 0 + : debugStyle.value; + }; + /** + * Prepares final RuleData. + * Handles `debug` and `remove` in raw rule data styles. + * + * @param rawRuleData Raw data of selector css rule parsing. + * + * @returns Parsed ExtendedCss rule data. + * @throws An error if rawRuleData.ast or rawRuleData.rawStyles not defined. + */ + const prepareRuleData = (rawRuleData) => { + const { selector, ast, rawStyles } = rawRuleData; + if (!ast) { + throw new Error(`AST should be parsed for selector: '${selector}'`); + } + if (!rawStyles) { + throw new Error(`Styles should be parsed for selector: '${selector}'`); + } + const ruleData = { + selector, + ast, + }; + const debugValue = getDebugStyleValue(rawStyles); + const shouldRemove = isRemoveSetInStyles(rawStyles); + let styles = rawStyles; + if (debugValue) { + // get rid of 'debug' from styles + styles = rawStyles.filter( + (s) => s.property !== DEBUG_PSEUDO_PROPERTY_KEY + ); // and set it as separate property only if its value is valid + // which is 'true' or 'global' + if ( + debugValue === PSEUDO_PROPERTY_POSITIVE_VALUE || + debugValue === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE + ) { + ruleData.debug = debugValue; + } + } + if (shouldRemove) { + // no other styles are needed to apply if 'remove' is set + ruleData.style = { + [REMOVE_PSEUDO_MARKER]: PSEUDO_PROPERTY_POSITIVE_VALUE, + }; + /** + * 'content' property is needed for ExtCssConfiguration.beforeStyleApplied(). + * + * @see {@link BeforeStyleAppliedCallback} + */ + const contentStyle = styles.find( + (s) => s.property === CONTENT_CSS_PROPERTY + ); + if (contentStyle) { + ruleData.style[CONTENT_CSS_PROPERTY] = contentStyle.value; + } + } else { + // otherwise all styles should be applied. + // every style property will be unique because of their converting into object + if (styles.length > 0) { + const stylesAsEntries = styles.map((style) => { + const { property, value } = style; + return [property, value]; + }); + const preparedStyleData = getObjectFromEntries(stylesAsEntries); + ruleData.style = preparedStyleData; + } + } + return ruleData; + }; + /** + * Combines previously parsed css rules data objects + * into rules which are ready to apply. + * + * @param rawResults Previously parsed css rules data objects. + * + * @returns Parsed ExtendedCss rule data. + */ + const combineRulesData = (rawResults) => { + const results = []; + rawResults.forEach((value, key) => { + const selector = key; + const { ast, styles: rawStyles } = value; + results.push( + prepareRuleData({ + selector, + ast, + rawStyles, + }) + ); + }); + return results; + }; + /** + * Trims `rawStyle` and splits it into tokens. + * + * @param rawStyle Style declaration block content inside curly bracket — `{` and `}` — + * can be a single style declaration or a list of declarations. + * + * @returns Array of tokens supported for style declaration block. + */ + const tokenizeStyleBlock = (rawStyle) => { + const styleDeclaration = rawStyle.trim(); + return tokenize(styleDeclaration, SUPPORTED_STYLE_DECLARATION_MARKS); + }; + /** + * Describes possible style declaration parts. + * + * IMPORTANT: it is used as 'const' instead of 'enum' to avoid side effects + * during ExtendedCss import into other libraries. + */ + const DECLARATION_PART = { + PROPERTY: "property", + VALUE: "value", + }; + /** + * Checks whether the quotes has been opened for style value. + * + * @param context Style block parser context. + * + * @returns True if style value has already opened quotes. + */ + const isValueQuotesOpen = (context) => { + return context.bufferValue !== "" && context.valueQuoteMark !== null; + }; + /** + * Saves parsed property and value to collection of parsed styles. + * Prunes context buffers for property and value. + * + * @param context Style block parser context. + */ + const collectStyle = (context) => { + context.styles.push({ + property: context.bufferProperty.trim(), + value: context.bufferValue.trim(), + }); // reset buffers + context.bufferProperty = ""; + context.bufferValue = ""; + }; + /** + * Handles token which is supposed to be a part of style **property**. + * + * @param context Style block parser context. + * @param styleBlock Whole style block which is being parsed. + * @param token Current token. + * + * @throws An error on invalid token. + */ + const processPropertyToken = (context, styleBlock, token) => { + const { value: tokenValue } = token; + switch (token.type) { + case TOKEN_TYPE.WORD: + if (context.bufferProperty.length > 0) { + // e.g. 'padding top: 0;' - current tokenValue is 'top' which is not valid + throw new Error( + `Invalid style property in style block: '${styleBlock}'` + ); + } + context.bufferProperty += tokenValue; + break; + case TOKEN_TYPE.MARK: + // only colon and whitespaces are allowed while style property parsing + if (tokenValue === COLON) { + if (context.bufferProperty.trim().length === 0) { + // e.g. such style block: '{ : none; }' + throw new Error( + `Missing style property before ':' in style block: '${styleBlock}'` + ); + } // the property successfully collected + context.bufferProperty = context.bufferProperty.trim(); // prepare for value collecting + context.processing = DECLARATION_PART.VALUE; // the property buffer shall be reset after the value is successfully collected + } else if (WHITE_SPACE_CHARACTERS.includes(tokenValue)); + else { + // if after the property there is anything other than ':' except whitespace, this is a parse error + // https://www.w3.org/TR/css-syntax-3/#consume-declaration + throw new Error( + `Invalid style declaration in style block: '${styleBlock}'` + ); + } + break; + default: + throw new Error( + `Unsupported style property character: '${tokenValue}' in style block: '${styleBlock}'` + ); + } + }; + /** + * Handles token which is supposed to be a part of style **value**. + * + * @param context Style block parser context. + * @param styleBlock Whole style block which is being parsed. + * @param token Current token. + * + * @throws An error on invalid token. + */ + const processValueToken = (context, styleBlock, token) => { + const { value: tokenValue } = token; + if (token.type === TOKEN_TYPE.WORD) { + // simply collect to buffer + context.bufferValue += tokenValue; + } else { + // otherwise check the mark + switch (tokenValue) { + case COLON: + // the ':' character inside of the value should be inside of quotes + // otherwise the value is not valid + // e.g. 'content: display: none' + // parser is here ↑ + if (!isValueQuotesOpen(context)) { + // eslint-disable-next-line max-len + throw new Error( + `Invalid style value for property '${context.bufferProperty}' in style block: '${styleBlock}'` + ); + } // collect the colon inside quotes + // e.g. 'content: "test:123"' + // parser is here ↑ + context.bufferValue += tokenValue; + break; + case SEMICOLON: + if (isValueQuotesOpen(context)) { + // ';' inside quotes is part of style value + // e.g. 'content: "test;"' + context.bufferValue += tokenValue; + } else { + // otherwise the value is successfully collected + // save parsed style + collectStyle(context); // prepare for value collecting + context.processing = DECLARATION_PART.PROPERTY; + } + break; + case SINGLE_QUOTE: + case DOUBLE_QUOTE: + // if quotes are not open + if (context.valueQuoteMark === null) { + // save the opening quote mark for later comparison + context.valueQuoteMark = tokenValue; + } else if ( + !context.bufferValue.endsWith(BACKSLASH) && // otherwise a quote appeared in the value earlier, + // and non-escaped quote should be checked whether it is a closing quote + context.valueQuoteMark === tokenValue + ) { + context.valueQuoteMark = null; + } // always save the quote to the buffer + // but after the context.bufferValue is checked for BACKSLASH above + // e.g. 'content: "test:123"' + // 'content: "\""' + context.bufferValue += tokenValue; + break; + case BACKSLASH: + if (!isValueQuotesOpen(context)) { + // eslint-disable-next-line max-len + throw new Error( + `Invalid style value for property '${context.bufferProperty}' in style block: '${styleBlock}'` + ); + } // collect the backslash inside quotes + // e.g. ' content: "\"" ' + // parser is here ↑ + context.bufferValue += tokenValue; + break; + case SPACE: + case TAB: + case CARRIAGE_RETURN: + case LINE_FEED: + case FORM_FEED: + // whitespace should be collected only if the value collecting started + // which means inside of the value + // e.g. 'width: 100% !important' + // parser is here ↑ + if (context.bufferValue.length > 0) { + context.bufferValue += tokenValue; + } // otherwise it can be omitted + // e.g. 'width: 100% !important' + // here ↑ + break; + default: + throw new Error(`Unknown style declaration token: '${tokenValue}'`); + } + } + }; + /** + * Parses css rule style block. + * + * @param rawStyleBlock Style block to parse. + * + * @returns Array of style declarations. + * @throws An error on invalid style block. + */ + const parseStyleBlock = (rawStyleBlock) => { + const styleBlock = rawStyleBlock.trim(); + const tokens = tokenizeStyleBlock(styleBlock); + const context = { + // style declaration parsing always starts with 'property' + processing: DECLARATION_PART.PROPERTY, + styles: [], + bufferProperty: "", + bufferValue: "", + valueQuoteMark: null, + }; + let i = 0; + while (i < tokens.length) { + const token = tokens[i]; + if (!token) { + break; + } + if (context.processing === DECLARATION_PART.PROPERTY) { + processPropertyToken(context, styleBlock, token); + } else if (context.processing === DECLARATION_PART.VALUE) { + processValueToken(context, styleBlock, token); + } else { + throw new Error("Style declaration parsing failed"); + } + i += 1; + } // unbalanced value quotes + // e.g. 'content: "test} ' + if (isValueQuotesOpen(context)) { + throw new Error( + `Unbalanced style declaration quotes in style block: '${styleBlock}'` + ); + } // collected property and value have not been saved to styles; + // it is possible for style block with no semicolon at the end + // e.g. such style block: '{ display: none }' + if (context.bufferProperty.length > 0) { + if (context.bufferValue.length === 0) { + // e.g. such style blocks: + // '{ display: }' + // '{ remove }' + // eslint-disable-next-line max-len + throw new Error( + `Missing style value for property '${context.bufferProperty}' in style block '${styleBlock}'` + ); + } + collectStyle(context); + } // rule with empty style block + // e.g. 'div { }' + if (context.styles.length === 0) { + throw new Error(STYLE_ERROR_PREFIX.NO_STYLE); + } + return context.styles; + }; + /** + * Returns array of positions of `{` in `cssRule`. + * + * @param cssRule CSS rule. + * + * @returns Array of left curly bracket indexes. + */ + const getLeftCurlyBracketIndexes = (cssRule) => { + const indexes = []; + for (let i = 0; i < cssRule.length; i += 1) { + if (cssRule[i] === BRACKET.CURLY.LEFT) { + indexes.push(i); + } + } + return indexes; + }; // TODO: use `extCssDoc` for caching of style block parser results + /** + * Parses CSS rule into rules data object: + * 1. Find the last `{` mark in the rule + * which supposed to be a divider between selector and style block. + * 2. Validates found string part before the `{` via selector parser; and if: + * - parsing failed – get the previous `{` in the rule, + * and validates a new rule part again [2]; + * - parsing successful — saves a found rule part as selector and parses the style block. + * + * @param rawCssRule Single CSS rule to parse. + * @param extCssDoc ExtCssDocument which is used for selector ast caching. + * + * @returns Array of rules data which contains: + * - selector as string; + * - ast to query elements by; + * - map of styles to apply. + * @throws An error on invalid css rule syntax: + * - unsupported CSS features – comments and at-rules + * - invalid selector or style block. + */ + const parseRule = (rawCssRule, extCssDoc) => { + var _rawRuleData$selector; + const cssRule = rawCssRule.trim(); + if ( + cssRule.includes(`${SLASH}${ASTERISK}`) && + cssRule.includes(`${ASTERISK}${SLASH}`) + ) { + throw new Error(STYLE_ERROR_PREFIX.NO_COMMENT); + } + const leftCurlyBracketIndexes = getLeftCurlyBracketIndexes(cssRule); // rule with style block but no selector + // e.g. '{ display: none; }' + if (getFirst(leftCurlyBracketIndexes) === 0) { + throw new Error(NO_SELECTOR_ERROR_PREFIX); + } + let selectorData; // if rule has `{` but there is no `}` + if ( + leftCurlyBracketIndexes.length > 0 && + !cssRule.includes(BRACKET.CURLY.RIGHT) + ) { + throw new Error( + `${STYLE_ERROR_PREFIX.NO_STYLE} OR ${STYLE_ERROR_PREFIX.UNCLOSED_STYLE}` + ); + } + if ( + // if rule has no `{` + leftCurlyBracketIndexes.length === 0 || // or `}` + !cssRule.includes(BRACKET.CURLY.RIGHT) + ) { + try { + // the whole css rule considered as "selector part" + // which may contain :remove() pseudo-class + selectorData = parseSelectorRulePart(cssRule, extCssDoc); + if (selectorData.success) { + var _selectorData$stylesO; + // rule with no style block has valid :remove() pseudo-class + // which is parsed into "styles" + // e.g. 'div:remove()' + // but also it can be just selector with no styles + // e.g. 'div' + // which should not be considered as valid css rule + if ( + ((_selectorData$stylesO = selectorData.stylesOfSelector) === + null || _selectorData$stylesO === void 0 + ? void 0 + : _selectorData$stylesO.length) === 0 + ) { + throw new Error(STYLE_ERROR_PREFIX.NO_STYLE_OR_REMOVE); + } + return { + selector: selectorData.selector.trim(), + ast: selectorData.ast, + rawStyles: selectorData.stylesOfSelector, + }; + } else { + // not valid selector + throw new Error("Invalid selector"); + } + } catch (e) { + throw new Error(getErrorMessage(e)); + } + } + let selectorBuffer; + let styleBlockBuffer; + const rawRuleData = { + selector: "", + }; // css rule should be parsed from its end + for (let i = leftCurlyBracketIndexes.length - 1; i > -1; i -= 1) { + const index = leftCurlyBracketIndexes[i]; + if (!index) { + throw new Error( + `Impossible to continue, no '{' to process for rule: '${cssRule}'` + ); + } // selector is before `{`, style block is after it + selectorBuffer = cssRule.slice(0, index); // skip curly brackets + styleBlockBuffer = cssRule.slice(index + 1, cssRule.length - 1); + selectorData = parseSelectorRulePart(selectorBuffer, extCssDoc); + if (selectorData.success) { + var _rawRuleData$rawStyle; + // selector successfully parsed + rawRuleData.selector = selectorData.selector.trim(); + rawRuleData.ast = selectorData.ast; + rawRuleData.rawStyles = selectorData.stylesOfSelector; // style block should be parsed + // TODO: add cache for style block parsing + const parsedStyles = parseStyleBlock(styleBlockBuffer); + (_rawRuleData$rawStyle = rawRuleData.rawStyles) === null || + _rawRuleData$rawStyle === void 0 + ? void 0 + : _rawRuleData$rawStyle.push(...parsedStyles); // stop rule parsing + break; + } else { + // if selector was not parsed successfully + // continue with next index of `{` + continue; + } + } + if ( + ((_rawRuleData$selector = rawRuleData.selector) === null || + _rawRuleData$selector === void 0 + ? void 0 + : _rawRuleData$selector.length) === 0 + ) { + // skip the rule as selector + throw new Error("Selector in not valid"); + } + return rawRuleData; + }; + /** + * Parses array of CSS rules into array of rules data objects. + * Invalid rules are skipped and not applied, + * and the errors are logged. + * + * @param rawCssRules Array of rules to parse. + * @param extCssDoc Needed for selector ast caching. + * + * @returns Array of parsed valid rules data. + */ + const parseRules$1 = (rawCssRules, extCssDoc) => { + const rawResults = createRawResultsMap(); + const warnings = []; // trim all rules and find unique ones + const uniqueRules = [...new Set(rawCssRules.map((r) => r.trim()))]; + uniqueRules.forEach((rule) => { + try { + saveToRawResults(rawResults, parseRule(rule, extCssDoc)); + } catch (e) { + // skip the invalid rule + const errorMessage = getErrorMessage(e); + warnings.push(`'${rule}' - error: '${errorMessage}'`); + } + }); // log info about skipped invalid rules + if (warnings.length > 0) { + logger.info(`Invalid rules:\n ${warnings.join("\n ")}`); + } + return combineRulesData(rawResults); + }; + const REGEXP_DECLARATION_END = /[;}]/g; + const REGEXP_DECLARATION_DIVIDER = /[;:}]/g; + const REGEXP_NON_WHITESPACE = /\S/g; + /** + * Interface for stylesheet parser context. + */ + /** + * Resets rule data buffer to init value after rule successfully collected. + * + * @param context Stylesheet parser context. + */ + const restoreRuleAcc = (context) => { + context.rawRuleData = { + selector: "", + }; + }; + /** + * Parses cropped selector part found before `{` previously. + * + * @param context Stylesheet parser context. + * @param extCssDoc Needed for caching of selector ast. + * + * @returns Parsed validation data for cropped part of stylesheet which may be a selector. + * @throws An error on unsupported CSS features, e.g. at-rules. + */ + const parseSelectorPart = (context, extCssDoc) => { + let selector = context.selectorBuffer.trim(); + if (selector.startsWith(AT_RULE_MARKER)) { + throw new Error(`${NO_AT_RULE_ERROR_PREFIX}: '${selector}'.`); + } + let removeSelectorData; + try { + removeSelectorData = parseRemoveSelector(selector); + } catch (e) { + logger.error(getErrorMessage(e)); + throw new Error(`${REMOVE_ERROR_PREFIX.INVALID_REMOVE}: '${selector}'`); + } + if (context.nextIndex === -1) { + if (selector === removeSelectorData.selector) { + // rule should have style or pseudo-class :remove() + throw new Error( + `${STYLE_ERROR_PREFIX.NO_STYLE_OR_REMOVE}: '${context.cssToParse}'` + ); + } // stop parsing as there is no style declaration and selector parsed fine + context.cssToParse = ""; + } + let stylesOfSelector = []; + let success = false; + let ast; + try { + selector = removeSelectorData.selector; + stylesOfSelector = removeSelectorData.stylesOfSelector; // validate found selector by parsing it to ast + // so if it is invalid error will be thrown + ast = extCssDoc.getSelectorAst(selector); + success = true; + } catch (e) { + success = false; + } + if (context.nextIndex > 0) { + // slice found valid selector part off + // and parse rest of stylesheet later + context.cssToParse = context.cssToParse.slice(context.nextIndex); + } + return { + success, + selector, + ast, + stylesOfSelector, + }; + }; + /** + * Recursively parses style declaration string into `Style`s. + * + * @param context Stylesheet parser context. + * @param styles Array of styles. + * + * @throws An error on invalid style declaration. + * @returns A number index of the next `}` in `this.cssToParse`. + */ + const parseUntilClosingBracket = (context, styles) => { + // Expects ":", ";", and "}". + REGEXP_DECLARATION_DIVIDER.lastIndex = context.nextIndex; + let match = REGEXP_DECLARATION_DIVIDER.exec(context.cssToParse); + if (match === null) { + throw new Error( + `${STYLE_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'` + ); + } + let matchPos = match.index; + let matched = match[0]; + if (matched === BRACKET.CURLY.RIGHT) { + const declarationChunk = context.cssToParse.slice( + context.nextIndex, + matchPos + ); + if (declarationChunk.trim().length === 0) { + // empty style declaration + // e.g. 'div { }' + if (styles.length === 0) { + throw new Error( + `${STYLE_ERROR_PREFIX.NO_STYLE}: '${context.cssToParse}'` + ); + } // else valid style parsed before it + // e.g. '{ display: none; }' -- position is after ';' + } else { + // closing curly bracket '}' is matched before colon ':' + // trimmed declarationChunk is not a space, between ';' and '}', + // e.g. 'visible }' in style '{ display: none; visible }' after part before ';' is parsed + throw new Error( + `${STYLE_ERROR_PREFIX.INVALID_STYLE}: '${context.cssToParse}'` + ); + } + return matchPos; + } + if (matched === COLON) { + const colonIndex = matchPos; // Expects ";" and "}". + REGEXP_DECLARATION_END.lastIndex = colonIndex; + match = REGEXP_DECLARATION_END.exec(context.cssToParse); + if (match === null) { + throw new Error( + `${STYLE_ERROR_PREFIX.UNCLOSED_STYLE}: '${context.cssToParse}'` + ); + } + matchPos = match.index; + matched = match[0]; // Populates the `styleMap` key-value map. + const property = context.cssToParse + .slice(context.nextIndex, colonIndex) + .trim(); + if (property.length === 0) { + throw new Error( + `${STYLE_ERROR_PREFIX.NO_PROPERTY}: '${context.cssToParse}'` + ); + } + const value = context.cssToParse.slice(colonIndex + 1, matchPos).trim(); + if (value.length === 0) { + throw new Error( + `${STYLE_ERROR_PREFIX.NO_VALUE}: '${context.cssToParse}'` + ); + } + styles.push({ + property, + value, + }); // finish style parsing if '}' is found + // e.g. '{ display: none }' -- no ';' at the end of declaration + if (matched === BRACKET.CURLY.RIGHT) { + return matchPos; + } + } // matchPos is the position of the next ';' + // crop 'cssToParse' and re-run the loop + context.cssToParse = context.cssToParse.slice(matchPos + 1); + context.nextIndex = 0; + return parseUntilClosingBracket(context, styles); // Should be a subject of tail-call optimization. + }; + /** + * Parses next style declaration part in stylesheet. + * + * @param context Stylesheet parser context. + * + * @returns Array of style data objects. + */ + const parseNextStyle = (context) => { + const styles = []; + const styleEndPos = parseUntilClosingBracket(context, styles); // find next rule after the style declaration + REGEXP_NON_WHITESPACE.lastIndex = styleEndPos + 1; + const match = REGEXP_NON_WHITESPACE.exec(context.cssToParse); + if (match === null) { + context.cssToParse = ""; + return styles; + } + const matchPos = match.index; // cut out matched style declaration for previous selector + context.cssToParse = context.cssToParse.slice(matchPos); + return styles; + }; + /** + * Parses stylesheet of rules into rules data objects (non-recursively): + * 1. Iterates through stylesheet string. + * 2. Finds first `{` which can be style declaration start or part of selector. + * 3. Validates found string part via selector parser; and if: + * - it throws error — saves string part to buffer as part of selector, + * slice next stylesheet part to `{` [2] and validates again [3]; + * - no error — saves found string part as selector and starts to parse styles (recursively). + * + * @param rawStylesheet Raw stylesheet as string. + * @param extCssDoc ExtCssDocument which uses cache while selectors parsing. + * @throws An error on unsupported CSS features, e.g. comments or invalid stylesheet syntax. + * @returns Array of rules data which contains: + * - selector as string; + * - ast to query elements by; + * - map of styles to apply. + */ + const parseStylesheet = (rawStylesheet, extCssDoc) => { + const stylesheet = rawStylesheet.trim(); + if ( + stylesheet.includes(`${SLASH}${ASTERISK}`) && + stylesheet.includes(`${ASTERISK}${SLASH}`) + ) { + throw new Error( + `${STYLE_ERROR_PREFIX.NO_COMMENT} in stylesheet: '${stylesheet}'` + ); + } + const context = { + // any stylesheet should start with selector + isSelector: true, + // init value of parser position + nextIndex: 0, + // init value of cssToParse + cssToParse: stylesheet, + // buffer for collecting selector part + selectorBuffer: "", + // accumulator for rules + rawRuleData: { + selector: "", + }, + }; + const rawResults = createRawResultsMap(); + let selectorData; // context.cssToParse is going to be cropped while its parsing + while (context.cssToParse) { + if (context.isSelector) { + // find index of first opening curly bracket + // which may mean start of style part and end of selector one + context.nextIndex = context.cssToParse.indexOf(BRACKET.CURLY.LEFT); // rule should not start with style, selector is required + // e.g. '{ display: none; }' + if (context.selectorBuffer.length === 0 && context.nextIndex === 0) { + throw new Error( + `${STYLE_ERROR_PREFIX.NO_SELECTOR}: '${context.cssToParse}'` + ); + } + if (context.nextIndex === -1) { + // no style declaration in rule + // but rule still may contain :remove() pseudo-class + context.selectorBuffer = context.cssToParse; + } else { + // collect string parts before opening curly bracket + // until valid selector collected + context.selectorBuffer += context.cssToParse.slice( + 0, + context.nextIndex + ); + } + selectorData = parseSelectorPart(context, extCssDoc); + if (selectorData.success) { + // selector successfully parsed + context.rawRuleData.selector = selectorData.selector.trim(); + context.rawRuleData.ast = selectorData.ast; + context.rawRuleData.rawStyles = selectorData.stylesOfSelector; + context.isSelector = false; // save rule data if there is no style declaration + if (context.nextIndex === -1) { + saveToRawResults(rawResults, context.rawRuleData); // clean up ruleContext + restoreRuleAcc(context); + } else { + // skip the opening curly bracket at the start of style declaration part + context.nextIndex = 1; + context.selectorBuffer = ""; + } + } else { + // if selector was not successfully parsed parseSelectorPart(), continue stylesheet parsing: + // save the found bracket to buffer and proceed to next loop iteration + context.selectorBuffer += BRACKET.CURLY.LEFT; // delete `{` from cssToParse + context.cssToParse = context.cssToParse.slice(1); + } + } else { + var _context$rawRuleData$; + // style declaration should be parsed + const parsedStyles = parseNextStyle(context); // styles can be parsed from selector part if it has :remove() pseudo-class + // e.g. '.banner:remove() { debug: true; }' + (_context$rawRuleData$ = context.rawRuleData.rawStyles) === null || + _context$rawRuleData$ === void 0 + ? void 0 + : _context$rawRuleData$.push(...parsedStyles); // save rule data to results + saveToRawResults(rawResults, context.rawRuleData); + context.nextIndex = 0; // clean up ruleContext + restoreRuleAcc(context); // parse next rule selector after style successfully parsed + context.isSelector = true; + } + } + return combineRulesData(rawResults); + }; + /** + * Checks whether passed `arg` is number type. + * + * @param arg Value to check. + * + * @returns True if `arg` is number and not NaN. + */ + const isNumber = (arg) => { + return typeof arg === "number" && !Number.isNaN(arg); + }; + /** + * The purpose of ThrottleWrapper is to throttle calls of the function + * that applies ExtendedCss rules. The reasoning here is that the function calls + * are triggered by MutationObserver and there may be many mutations in a short period of time. + * We do not want to apply rules on every mutation so we use this helper to make sure + * that there is only one call in the given amount of time. + */ + class ThrottleWrapper { + /** + * Creates new ThrottleWrapper. + * The {@link callback} should be executed not more often than {@link ThrottleWrapper.THROTTLE_DELAY_MS}. + * + * @param callback The callback. + */ + constructor(callback) { + this.callback = callback; + this.executeCallback = this.executeCallback.bind(this); + } + /** + * Calls the {@link callback} function and update bounded throttle wrapper properties. + */ + executeCallback() { + this.lastRunTime = performance.now(); + if (isNumber(this.timerId)) { + clearTimeout(this.timerId); + delete this.timerId; + } + this.callback(); + } + /** + * Schedules the {@link executeCallback} function execution via setTimeout. + * It may triggered by MutationObserver job which may occur too ofter, so we limit the function execution: + * + * 1. If {@link timerId} is set, ignore the call, because the function is already scheduled to be executed; + * + * 2. If {@link lastRunTime} is set, we need to check the time elapsed time since the last call. If it is + * less than {@link ThrottleWrapper.THROTTLE_DELAY_MS}, we schedule the function execution after the remaining time. + * + * Otherwise, we execute the function asynchronously to ensure that it is executed + * in the correct order with respect to DOM events, by deferring its execution until after + * those tasks have completed. + */ + run() { + if (isNumber(this.timerId)) { + // there is a pending execution scheduled + return; + } + if (isNumber(this.lastRunTime)) { + const elapsedTime = performance.now() - this.lastRunTime; + if (elapsedTime < ThrottleWrapper.THROTTLE_DELAY_MS) { + this.timerId = window.setTimeout( + this.executeCallback, + ThrottleWrapper.THROTTLE_DELAY_MS - elapsedTime + ); + return; + } + } + /** + * We use `setTimeout` instead `requestAnimationFrame` + * here because requestAnimationFrame can be delayed for a long time + * when the browser saves battery or the engine is heavily loaded. + */ + this.timerId = window.setTimeout(this.executeCallback); + } + } + _defineProperty(ThrottleWrapper, "THROTTLE_DELAY_MS", 150); + const LAST_EVENT_TIMEOUT_MS = 10; + const IGNORED_EVENTS = [ + "mouseover", + "mouseleave", + "mouseenter", + "mouseout", + ]; + const SUPPORTED_EVENTS = [ + // keyboard events + "keydown", + "keypress", + "keyup", + // mouse events + "auxclick", + "click", + "contextmenu", + "dblclick", + "mousedown", + "mouseenter", + "mouseleave", + "mousemove", + "mouseover", + "mouseout", + "mouseup", + "pointerlockchange", + "pointerlockerror", + "select", + "wheel", + ]; // 'wheel' event makes scrolling in Safari twitchy + // https://github.com/AdguardTeam/ExtendedCss/issues/120 + const SAFARI_PROBLEMATIC_EVENTS = ["wheel"]; + /** + * We use EventTracker to track the event that is likely to cause the mutation. + * The problem is that we cannot use `window.event` directly from the mutation observer call + * as we're not in the event handler context anymore. + */ + class EventTracker { + /** + * Creates new EventTracker. + */ + constructor() { + _defineProperty(this, "getLastEventType", () => this.lastEventType); + _defineProperty(this, "getTimeSinceLastEvent", () => { + if (!this.lastEventTime) { + return null; + } + return Date.now() - this.lastEventTime; + }); + this.trackedEvents = isSafariBrowser + ? SUPPORTED_EVENTS.filter( + (event) => !SAFARI_PROBLEMATIC_EVENTS.includes(event) + ) + : SUPPORTED_EVENTS; + this.trackedEvents.forEach((eventName) => { + document.documentElement.addEventListener( + eventName, + this.trackEvent, + true + ); + }); + } + /** + * Callback for event listener for events tracking. + * + * @param event Any event. + */ + trackEvent(event) { + this.lastEventType = event.type; + this.lastEventTime = Date.now(); + } + /** + * Checks whether the last caught event should be ignored. + * + * @returns True if event should be ignored. + */ + isIgnoredEventType() { + const lastEventType = this.getLastEventType(); + const sinceLastEventTime = this.getTimeSinceLastEvent(); + return ( + !!lastEventType && + IGNORED_EVENTS.includes(lastEventType) && + !!sinceLastEventTime && + sinceLastEventTime < LAST_EVENT_TIMEOUT_MS + ); + } + /** + * Stops event tracking by removing event listener. + */ + stopTracking() { + this.trackedEvents.forEach((eventName) => { + document.documentElement.removeEventListener( + eventName, + this.trackEvent, + true + ); + }); + } + } + /** + * We are trying to limit the number of callback calls by not calling it on all kind of "hover" events. + * The rationale behind this is that "hover" events often cause attributes modification, + * but re-applying extCSS rules will be useless as these attribute changes are usually transient. + * + * @param mutations DOM elements mutation records. + * @returns True if all mutations are about attributes changes, otherwise false. + */ + function shouldIgnoreMutations(mutations) { + // ignore if all mutations are about attributes changes + return !mutations.some((m) => m.type !== "attributes"); + } + /** + * Adds new {@link context.domMutationObserver} instance and connect it to document. + * + * @param context ExtendedCss context. + */ + function observeDocument(context) { + if (context.isDomObserved) { + return; + } // enable dynamically added elements handling + context.isDomObserved = true; + context.domMutationObserver = new natives.MutationObserver( + (mutations) => { + if (!mutations || mutations.length === 0) { + return; + } + const eventTracker = new EventTracker(); + if ( + eventTracker.isIgnoredEventType() && + shouldIgnoreMutations(mutations) + ) { + return; + } // save instance of EventTracker to context + // for removing its event listeners on disconnectDocument() while mainDisconnect() + context.eventTracker = eventTracker; + context.scheduler.run(); + } + ); + context.domMutationObserver.observe(document, { + childList: true, + subtree: true, + attributes: true, + attributeFilter: ["id", "class"], + }); + } + /** + * Disconnect from {@link context.domMutationObserver}. + * + * @param context ExtendedCss context. + */ + function disconnectDocument(context) { + if (!context.isDomObserved) { + return; + } // disable dynamically added elements handling + context.isDomObserved = false; + if (context.domMutationObserver) { + context.domMutationObserver.disconnect(); + } // clean up event listeners + if (context.eventTracker) { + context.eventTracker.stopTracking(); + } + } + const CONTENT_ATTR_PREFIX_REGEXP = /^("|')adguard.+?/; + /** + * Removes affectedElement.node from DOM. + * + * @param context ExtendedCss context. + * @param affectedElement Affected element. + */ + const removeElement = (context, affectedElement) => { + const { node } = affectedElement; + affectedElement.removed = true; + const elementSelector = getElementSelectorPath(node); // check if the element has been already removed earlier + const elementRemovalsCounter = + context.removalsStatistic[elementSelector] || 0; // if removals attempts happened more than specified we do not try to remove node again + if (elementRemovalsCounter > MAX_STYLE_PROTECTION_COUNT) { + logger.error( + `ExtendedCss: infinite loop protection for selector: '${elementSelector}'` + ); + return; + } + if (node.parentElement) { + node.parentElement.removeChild(node); + context.removalsStatistic[elementSelector] = elementRemovalsCounter + 1; + } + }; + /** + * Sets style to the specified DOM node. + * + * @param node DOM element. + * @param style Style to set. + */ + const setStyleToElement = (node, style) => { + if (!(node instanceof HTMLElement)) { + return; + } + Object.keys(style).forEach((prop) => { + // Apply this style only to existing properties + // We cannot use hasOwnProperty here (does not work in FF) + if ( + typeof node.style.getPropertyValue(prop.toString()) !== "undefined" + ) { + let value = style[prop]; + if (!value) { + return; + } // do not apply 'content' style given by tsurlfilter + // which is needed only for BeforeStyleAppliedCallback + if ( + prop === CONTENT_CSS_PROPERTY && + value.match(CONTENT_ATTR_PREFIX_REGEXP) + ) { + return; + } // First we should remove !important attribute (or it won't be applied') + value = removeSuffix(value.trim(), "!important").trim(); + node.style.setProperty(prop, value, "important"); + } + }); + }; + /** + * Checks the required properties of `affectedElement` + * **before** `beforeStyleApplied()` execution. + * + * @param affectedElement Affected element. + * + * @returns False if there is no `node` or `rules` + * or `rules` is not an array. + */ + const isIAffectedElement = (affectedElement) => { + return ( + "node" in affectedElement && + "rules" in affectedElement && + affectedElement.rules instanceof Array + ); + }; + /** + * Checks the required properties of `affectedElement` + * **after** `beforeStyleApplied()` execution. + * These properties are needed for proper internal usage. + * + * @param affectedElement Affected element. + * + * @returns False if there is no `node` or `rules` + * or `rules` is not an array. + */ + const isAffectedElement = (affectedElement) => { + return ( + "node" in affectedElement && + "originalStyle" in affectedElement && + "rules" in affectedElement && + affectedElement.rules instanceof Array + ); + }; + /** + * Applies style to the specified DOM node. + * + * @param context ExtendedCss context. + * @param rawAffectedElement Object containing DOM node and rule to be applied. + * + * @throws An error if affectedElement has no style to apply. + */ + const applyStyle = (context, rawAffectedElement) => { + if (rawAffectedElement.protectionObserver) { + // style is already applied and protected by the observer + return; + } + let affectedElement; + if (context.beforeStyleApplied) { + if (!isIAffectedElement(rawAffectedElement)) { + throw new Error( + "Returned IAffectedElement should have 'node' and 'rules' properties" + ); + } + affectedElement = context.beforeStyleApplied(rawAffectedElement); + if (!affectedElement) { + throw new Error( + "Callback 'beforeStyleApplied' should return IAffectedElement" + ); + } + } else { + affectedElement = rawAffectedElement; + } + if (!isAffectedElement(affectedElement)) { + throw new Error( + "Returned IAffectedElement should have 'node' and 'rules' properties" + ); + } + const { node, rules } = affectedElement; + for (let i = 0; i < rules.length; i += 1) { + const rule = rules[i]; + const selector = + rule === null || rule === void 0 ? void 0 : rule.selector; + const style = rule === null || rule === void 0 ? void 0 : rule.style; + const debug = rule === null || rule === void 0 ? void 0 : rule.debug; // rule may not have style to apply + // e.g. 'div:has(> a) { debug: true }' -> means no style to apply, and enable debug mode + if (style) { + if (style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE) { + removeElement(context, affectedElement); + return; + } + setStyleToElement(node, style); + } else if (!debug) { + // but rule should not have both style and debug properties + throw new Error( + `No style declaration in rule for selector: '${selector}'` + ); + } + } + }; + /** + * Reverts style for the affected object. + * + * @param affectedElement Affected element. + */ + const revertStyle = (affectedElement) => { + if (affectedElement.protectionObserver) { + affectedElement.protectionObserver.disconnect(); + } + affectedElement.node.style.cssText = affectedElement.originalStyle; + }; + /** + * ExtMutationObserver is a wrapper over regular MutationObserver with one additional function: + * it keeps track of the number of times we called the "ProtectionCallback". + * + * We use an instance of this to monitor styles added by ExtendedCss + * and to make sure these styles are recovered if the page script attempts to modify them. + * + * However, we want to avoid endless loops of modification if the page script repeatedly modifies the styles. + * So we keep track of the number of calls and observe() makes a decision + * whether to continue recovering the styles or not. + */ + class ExtMutationObserver { + /** + * Extra property for keeping 'style fix counts'. + */ + /** + * Creates new ExtMutationObserver. + * + * @param protectionCallback Callback which execution should be counted. + */ + constructor(protectionCallback) { + this.styleProtectionCount = 0; + this.observer = new natives.MutationObserver((mutations) => { + if (!mutations.length) { + return; + } + this.styleProtectionCount += 1; + protectionCallback(mutations, this); + }); + } + /** + * Starts to observe target element, + * prevents infinite loop of observing due to the limited number of times of callback runs. + * + * @param target Target to observe. + * @param options Mutation observer options. + */ + observe(target, options) { + if (this.styleProtectionCount < MAX_STYLE_PROTECTION_COUNT) { + this.observer.observe(target, options); + } else { + logger.error("ExtendedCss: infinite loop protection for style"); + } + } + /** + * Stops ExtMutationObserver from observing any mutations. + * Until the `observe()` is used again, `protectionCallback` will not be invoked. + */ + disconnect() { + this.observer.disconnect(); + } + } + const PROTECTION_OBSERVER_OPTIONS = { + attributes: true, + attributeOldValue: true, + attributeFilter: ["style"], + }; + /** + * Creates MutationObserver protection callback. + * + * @param styles Styles data object. + * + * @returns Callback for styles protection. + */ + const createProtectionCallback = (styles) => { + const protectionCallback = (mutations, extObserver) => { + if (!mutations[0]) { + return; + } + const { target } = mutations[0]; + extObserver.disconnect(); + styles.forEach((style) => { + setStyleToElement(target, style); + }); + extObserver.observe(target, PROTECTION_OBSERVER_OPTIONS); + }; + return protectionCallback; + }; + /** + * Sets up a MutationObserver which protects style attributes from changes. + * + * @param node DOM node. + * @param rules Rule data objects. + * @returns Mutation observer used to protect attribute or null if there's nothing to protect. + */ + const protectStyleAttribute = (node, rules) => { + if (!natives.MutationObserver) { + return null; + } + const styles = []; + rules.forEach((ruleData) => { + const { style } = ruleData; // some rules might have only debug property in style declaration + // e.g. 'div:has(> a) { debug: true }' -> parsed to boolean `ruleData.debug` + // so no style is fine, and here we should collect only valid styles to protect + if (style) { + styles.push(style); + } + }); + const protectionObserver = new ExtMutationObserver( + createProtectionCallback(styles) + ); + protectionObserver.observe(node, PROTECTION_OBSERVER_OPTIONS); + return protectionObserver; + }; + const STATS_DECIMAL_DIGITS_COUNT = 4; + /** + * A helper class for applied rule stats. + */ + class TimingStats { + /** + * Creates new TimingStats. + */ + constructor() { + this.appliesTimings = []; + this.appliesCount = 0; + this.timingsSum = 0; + this.meanTiming = 0; + this.squaredSum = 0; + this.standardDeviation = 0; + } + /** + * Observe target element and mark observer as active. + * + * @param elapsedTimeMs Time in ms. + */ + push(elapsedTimeMs) { + this.appliesTimings.push(elapsedTimeMs); + this.appliesCount += 1; + this.timingsSum += elapsedTimeMs; + this.meanTiming = this.timingsSum / this.appliesCount; + this.squaredSum += elapsedTimeMs * elapsedTimeMs; + this.standardDeviation = Math.sqrt( + this.squaredSum / this.appliesCount - Math.pow(this.meanTiming, 2) + ); + } + } + /** + * Makes the timestamps more readable. + * + * @param timestamp Raw timestamp. + * + * @returns Fine-looking timestamps. + */ + const beautifyTimingNumber = (timestamp) => { + return Number(timestamp.toFixed(STATS_DECIMAL_DIGITS_COUNT)); + }; + /** + * Improves timing stats readability. + * + * @param rawTimings Collected timings with raw timestamp. + * + * @returns Fine-looking timing stats. + */ + const beautifyTimings = (rawTimings) => { + return { + appliesTimings: rawTimings.appliesTimings.map((t) => + beautifyTimingNumber(t) + ), + appliesCount: beautifyTimingNumber(rawTimings.appliesCount), + timingsSum: beautifyTimingNumber(rawTimings.timingsSum), + meanTiming: beautifyTimingNumber(rawTimings.meanTiming), + standardDeviation: beautifyTimingNumber(rawTimings.standardDeviation), + }; + }; + /** + * Prints timing information if debugging mode is enabled. + * + * @param context ExtendedCss context. + */ + const printTimingInfo = (context) => { + if (context.areTimingsPrinted) { + return; + } + context.areTimingsPrinted = true; + const timingsLogData = {}; + context.parsedRules.forEach((ruleData) => { + if (ruleData.timingStats) { + const { selector, style, debug, matchedElements } = ruleData; // style declaration for some rules is parsed to debug property and no style to apply + // e.g. 'div:has(> a) { debug: true }' + if (!style && !debug) { + throw new Error( + `Rule should have style declaration for selector: '${selector}'` + ); + } + const selectorData = { + selectorParsed: selector, + timings: beautifyTimings(ruleData.timingStats), + }; // `ruleData.style` may contain `remove` pseudo-property + // and make logs look better + if ( + style && + style[REMOVE_PSEUDO_MARKER] === PSEUDO_PROPERTY_POSITIVE_VALUE + ) { + selectorData.removed = true; // no matchedElements for such case as they are removed after ExtendedCss applied + } else { + selectorData.styleApplied = style || null; + selectorData.matchedElements = matchedElements; + } + timingsLogData[selector] = selectorData; + } + }); + if (Object.keys(timingsLogData).length === 0) { + return; + } // add location.href to the message to distinguish frames + logger.info( + "[ExtendedCss] Timings in milliseconds for %o:\n%o", + window.location.href, + timingsLogData + ); + }; + /** + * Finds affectedElement object for the specified DOM node. + * + * @param affElements Array of affected elements — context.affectedElements. + * @param domNode DOM node. + * @returns Found affectedElement or undefined. + */ + const findAffectedElement = (affElements, domNode) => { + return affElements.find((affEl) => affEl.node === domNode); + }; + /** + * Applies specified rule and returns list of elements affected. + * + * @param context ExtendedCss context. + * @param ruleData Rule to apply. + * @returns List of elements affected by the rule. + */ + const applyRule = (context, ruleData) => { + // debugging mode can be enabled in two ways: + // 1. for separate rules - by `{ debug: true; }` + // 2. for all rules simultaneously by: + // - `{ debug: global; }` in any rule + // - positive `debug` property in ExtCssConfiguration + const isDebuggingMode = !!ruleData.debug || context.debug; + let startTime; + if (isDebuggingMode) { + startTime = performance.now(); + } + const { ast } = ruleData; + const nodes = []; // selector can be successfully parser into ast with no error + // but its applying by native Document.querySelectorAll() still may throw an error + // e.g. 'div[..banner]' + try { + nodes.push(...selectElementsByAst(ast)); + } catch (e) { + // log the error only in debug mode + if (context.debug) { + logger.error(getErrorMessage(e)); + } + } + nodes.forEach((node) => { + let affectedElement = findAffectedElement( + context.affectedElements, + node + ); + if (affectedElement) { + affectedElement.rules.push(ruleData); + applyStyle(context, affectedElement); + } else { + // Applying style first time + const originalStyle = node.style.cssText; + affectedElement = { + node, + // affected DOM node + rules: [ruleData], + // rule to be applied + originalStyle, + // original node style + protectionObserver: null, // style attribute observer + }; + + applyStyle(context, affectedElement); + context.affectedElements.push(affectedElement); + } + }); + if (isDebuggingMode && startTime) { + const elapsedTimeMs = performance.now() - startTime; + if (!ruleData.timingStats) { + ruleData.timingStats = new TimingStats(); + } + ruleData.timingStats.push(elapsedTimeMs); + } + return nodes; + }; + /** + * Applies filtering rules. + * + * @param context ExtendedCss context. + */ + const applyRules = (context) => { + const newSelectedElements = []; // some rules could make call - selector.querySelectorAll() temporarily to change node id attribute + // this caused MutationObserver to call recursively + // https://github.com/AdguardTeam/ExtendedCss/issues/81 + disconnectDocument(context); + context.parsedRules.forEach((ruleData) => { + const nodes = applyRule(context, ruleData); + Array.prototype.push.apply(newSelectedElements, nodes); // save matched elements to ruleData as linked to applied rule + // only for debugging purposes + if (ruleData.debug) { + ruleData.matchedElements = nodes; + } + }); // Now revert styles for elements which are no more affected + let affLength = context.affectedElements.length; // do nothing if there is no elements to process + while (affLength) { + const affectedElement = context.affectedElements[affLength - 1]; + if (!affectedElement) { + break; + } + if (!newSelectedElements.includes(affectedElement.node)) { + // Time to revert style + revertStyle(affectedElement); + context.affectedElements.splice(affLength - 1, 1); + } else if (!affectedElement.removed) { + // Add style protection observer + // Protect "style" attribute from changes + if (!affectedElement.protectionObserver) { + affectedElement.protectionObserver = protectStyleAttribute( + affectedElement.node, + affectedElement.rules + ); + } + } + affLength -= 1; + } // After styles are applied we can start observe again + observeDocument(context); + printTimingInfo(context); + }; + /** + * Result of selector validation. + */ + /** + * Main class of ExtendedCss lib. + * + * Parses css stylesheet with any selectors (passed to its argument as styleSheet), + * and guarantee its applying as mutation observer is used to prevent the restyling of needed elements by other scripts. + * This style protection is limited to 50 times to avoid infinite loop (MAX_STYLE_PROTECTION_COUNT). + * Our own ThrottleWrapper is used for styles applying to avoid too often lib reactions on page mutations. + * + * Constructor creates the instance of class which should be run be `apply()` method to apply the rules, + * and the applying can be stopped by `dispose()`. + * + * Can be used to select page elements by selector with `query()` method (similar to `Document.querySelectorAll()`), + * which does not require instance creating. + */ + class ExtendedCss { + /** + * Creates new ExtendedCss. + * + * @param configuration ExtendedCss configuration. + */ + constructor(configuration) { + if (!configuration) { + throw new Error("ExtendedCss configuration should be provided."); + } + this.applyRulesCallbackListener = + this.applyRulesCallbackListener.bind(this); + this.context = { + beforeStyleApplied: configuration.beforeStyleApplied, + debug: false, + affectedElements: [], + isDomObserved: false, + removalsStatistic: {}, + parsedRules: [], + scheduler: new ThrottleWrapper(this.applyRulesCallbackListener), + }; // TODO: throw an error instead of logging and handle it in related products. + if (!isBrowserSupported()) { + logger.error("Browser is not supported by ExtendedCss"); + return; + } // at least 'styleSheet' or 'cssRules' should be provided + if (!configuration.styleSheet && !configuration.cssRules) { + throw new Error( + "ExtendedCss configuration should have 'styleSheet' or 'cssRules' defined." + ); + } // 'styleSheet' and 'cssRules' are optional + // and both can be provided at the same time + // so both should be parsed and applied in such case + if (configuration.styleSheet) { + // stylesheet parsing can fail on some invalid selectors + try { + this.context.parsedRules.push( + ...parseStylesheet(configuration.styleSheet, extCssDocument) + ); + } catch (e) { + // eslint-disable-next-line max-len + throw new Error( + `Pass the rules as configuration.cssRules since configuration.styleSheet cannot be parsed because of: '${getErrorMessage( + e + )}'` + ); + } + } + if (configuration.cssRules) { + this.context.parsedRules.push( + ...parseRules$1(configuration.cssRules, extCssDocument) + ); + } // true if set in configuration + // or any rule in styleSheet has `debug: global` + this.context.debug = + configuration.debug || + this.context.parsedRules.some((ruleData) => { + return ruleData.debug === DEBUG_PSEUDO_PROPERTY_GLOBAL_VALUE; + }); + if ( + this.context.beforeStyleApplied && + typeof this.context.beforeStyleApplied !== "function" + ) { + // eslint-disable-next-line max-len + throw new Error( + `Invalid configuration. Type of 'beforeStyleApplied' should be a function, received: '${typeof this + .context.beforeStyleApplied}'` + ); + } + } + /** + * Invokes {@link applyRules} function with current app context. + * + * This method is bound to the class instance in the constructor because it is called + * in {@link ThrottleWrapper} and on the DOMContentLoaded event. + */ + applyRulesCallbackListener() { + applyRules(this.context); + } + /** + * Initializes ExtendedCss. + * + * Should be executed on page ASAP, + * otherwise the :contains() pseudo-class may work incorrectly. + */ + init() { + /** + * Native Node textContent getter must be intercepted as soon as possible, + * and stored as it is needed for proper work of :contains() pseudo-class + * because DOM Node prototype 'textContent' property may be mocked. + * + * @see {@link https://github.com/AdguardTeam/ExtendedCss/issues/127} + */ + nativeTextContent.setGetter(); + } + /** + * Applies stylesheet rules on page. + */ + apply() { + applyRules(this.context); + if (document.readyState !== "complete") { + document.addEventListener( + "DOMContentLoaded", + this.applyRulesCallbackListener, + false + ); + } + } + /** + * Disposes ExtendedCss and removes our styles from matched elements. + */ + dispose() { + disconnectDocument(this.context); + this.context.affectedElements.forEach((el) => { + revertStyle(el); + }); + document.removeEventListener( + "DOMContentLoaded", + this.applyRulesCallbackListener, + false + ); + } + /** + * Exposed for testing purposes only. + * + * @returns Array of AffectedElement data objects. + */ + getAffectedElements() { + return this.context.affectedElements; + } + /** + * Returns a list of the document's elements that match the specified selector. + * Uses ExtCssDocument.querySelectorAll(). + * + * @param selector Selector text. + * @param [noTiming=true] If true — do not print the timings to the console. + * + * @throws An error if selector is not valid. + * @returns A list of elements that match the selector. + */ + static query(selector) { + let noTiming = + arguments.length > 1 && arguments[1] !== undefined + ? arguments[1] + : true; + if (typeof selector !== "string") { + throw new Error("Selector should be defined as a string."); + } + const start = performance.now(); + try { + return extCssDocument.querySelectorAll(selector); + } finally { + const end = performance.now(); + if (!noTiming) { + logger.info( + `[ExtendedCss] Elapsed: ${Math.round((end - start) * 1000)} μs.` + ); + } + } + } + /** + * Validates selector. + * + * @param inputSelector Selector text to validate. + * + * @returns Result of selector validation. + */ + static validate(inputSelector) { + try { + // ExtendedCss in general supports :remove() in selector + // but ExtendedCss.query() does not support it as it should be parsed by stylesheet parser. + // so for validation we have to handle selectors with `:remove()` in it + const { selector } = parseRemoveSelector(inputSelector); + ExtendedCss.query(selector); + return { + ok: true, + error: null, + }; + } catch (e) { + // not valid input `selector` should be logged eventually + const error = `Error: Invalid selector: '${inputSelector}' -- ${getErrorMessage( + e + )}`; + return { + ok: false, + error, + }; + } + } + } + function parseBRules() { + var _a; + return __awaiter(this, void 0, void 0, function* () { + data.appliedLevel = 0; + const brules = + (_a = yield values.brules()) !== null && _a !== void 0 + ? _a + : defaultValues.brules; + brules.forEach((br) => { + const level = bRuleParser(br); + if (level > 0) data.bRules.push(br); + if (level > data.appliedLevel) data.appliedLevel = level; + }); + }); + } + function canApplyCss(type) { + return ( + (data.appliedLevel & (type >= 2 ? 2 : 1)) == 0 && + data[styleBoxes[type]].length > 0 + ); + } + function cleanRules() { + if ( + confirm(`是否清空存储规则 ? + +如果要卸载脚本,点击 确定 以后不要刷新,也不要打开任何新页面, +(如果可以)清空脚本存储(全选,删除,填 {},保存),然后删除脚本`) + ) { + values.rules(null); + values.time(null); + values.etags(null); + values.brules(null); + getSavedHosts().then((saves) => + saves.forEach((host) => values.css(null, host)) + ); + data.isClean = true; + gmMenu("update"); + gmMenu("export"); + gmMenu("count", () => location.reload()); + } + } + function reportRecord() { + let text = ""; + function pushRecord(css) { + const match = cssToAbp(css); + if (match === null) return; + const [item, type, sel] = match, + count = + type % 2 === 1 + ? ExtendedCss.query(sel).length + : document.querySelectorAll(sel).length; + if (count > 0) { + text += ` +! 匹配元素数量: ${count} +${item} +`; + } + } + data.bRules.forEach((br) => { + if (br.level > 0) { + text += ` +! 禁用${["", "通用", "特定", "所有"][br.level]}元素隐藏 +${br.rule} +`; + } + }); + styleBoxes.forEach((box, i) => { + if (canApplyCss(i)) { + data[box] + .split("\n") + .filter((css, i, csss) => csss.indexOf(css) === i) + .forEach((css) => pushRecord(css)); + } + }); + if (text.length > 0) { + downUrl( + URL.createObjectURL( + new Blob([ + `[Adblock Plus 2.0] +! 应用地址: +! ${location.href} +${text}`, + ]) + ), + `拦截报告_${location.hostname}.txt` + ); + } else { + alert("这个页面没有任何规则生效"); + } + } + function switchDisabledStat() { + values.black().then((disaList) => { + const disas = disaList !== null && disaList !== void 0 ? disaList : []; + data.disabled = !disas.includes(location.hostname); + if (data.disabled) { + disas.push(location.hostname); + } else { + disas.splice(disas.indexOf(location.hostname), 1); + } + values.black(disas).finally(() => location.reload()); + }); + } + function getCustomHash(saveHash) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + { + if (location.protocol === "https:") { + const hash = new Uint32Array( + yield window.crypto.subtle.digest( + "SHA-1", + yield new Blob([data.customRules]).arrayBuffer() + ) + ).toString(); + if (saveHash) yield values.hash(hash); + return hash; + } else { + return (_a = yield values.hash()) !== null && _a !== void 0 + ? _a + : defaultValues.hash; + } + } + }); + } + function initRules(apply) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + let abpRules = {}; + data.receivedRules = ""; + abpRules = + (_a = yield values.rules()) !== null && _a !== void 0 + ? _a + : defaultValues.rules; + { + yield Promise.all( + preset.onlineRules.map((rule) => + __awaiter(this, void 0, void 0, function* () { + const resRule = yield getRuleFromResource(rule.标识); + if (resRule && !abpRules[rule.标识]) + abpRules[rule.标识] = resRule; + }) + ) + ); + } + Object.keys(abpRules).forEach((name) => { + data.receivedRules += "\n" + abpRules[name]; + }); + data.allRules = data.customRules + data.receivedRules; + if (apply) yield splitRules(); + }); + } + function styleApplyExec(type) { + if (canApplyCss(type)) { + const csss = data[styleBoxes[type]]; + new ExtendedCss({ + styleSheet: csss.replaceAll(/\/\*\s*\d.+?\s*\*\//g, ""), + }).apply(); + if (!(type % 2 == 1)) addStyle(csss); + } + } + function styleApply() { + return __awaiter(this, void 0, void 0, function* () { + { + yield parseBRules(); + if (data.appliedLevel == 3) return; + } + for (let type = 0; type < 4; type++) styleApplyExec(type); + gmMenu("export", reportRecord); + }); + } + function parseRules() { + return __awaiter(this, void 0, void 0, function* () { + function addRule(rule, exten) { + const [full, selector] = ruleToCss(rule, data.preset); + const index = exten + (rule.generic ? 0 : 2); + const checkResult = ExtendedCss.validate(selector); + if (checkResult.ok) { + data[styleBoxes[index]] += full; + data.appliedCount++; + } else { + console.error("选择器检查错误:", rule, checkResult.error); + } + } + styleBoxes.forEach((box) => { + data[box] = ""; + }); + data.appliedCount = 0; + [ + data.styles, + data.extStyles, + data.selectors, + data.extSelectors, + ].forEach((r, t) => { + const sels = new Set(); + r.white.forEach((obj) => !sels.has(obj.sel) && sels.add(obj.sel)); + r.black + .filter((obj) => !sels.has(obj.sel) && sels.add(obj.sel)) + .forEach((s) => addRule(s, t % 2)); + }); + yield gmMenu("count", cleanRules); + saveCss(); + if (!data.saved) styleApply(); + }); + } + function splitRules() { + const bRuleSet = new Set(), + sels = new Set(); + dataBoxes.forEach((box) => { + data[box] = makeRuleBox(); + }); + data.allRules.split("\n").forEach((rule) => { + if (isBasicRule(rule)) { + const brule = bRuleSpliter(rule); + if (brule) { + if (rule.match(/,?badfilter,?/)) { + bRuleSet.delete(brule); + } else { + bRuleSet.add(brule); + } + } + } else { + const ruleObj = ruleLoader(rule); + if (typeof ruleObj !== "undefined") { + if (ruleObj.black === "black" && sels.has(ruleObj.sel)) return; + if (ruleObj.black === "white" && !sels.has(ruleObj.sel)) + sels.add(ruleObj.sel); + data[dataBoxes[ruleObj.type]][ruleObj.black].push(ruleObj); + } + } + }); + { + const bRules = []; + bRuleSet.forEach((brule) => bRules.push(brule)); + values.brules(bRules); + } + return parseRules(); + } + function makeInitMenu() { + return __awaiter(this, void 0, void 0, function* () { + yield gmMenu("count", cleanRules); + { + yield gmMenu("update", () => { + performUpdate(true).then(() => { + location.reload(); + }); + }); + } + }); + } + function extrEtag(resp) { + var _a, _b, _c; + const etag = getEtag( + typeof (resp === null || resp === void 0 ? void 0 : resp.headers) == + "object" + ? // 海阔世界 + (_b = + (_a = + resp === null || resp === void 0 ? void 0 : resp.headers) === + null || _a === void 0 + ? void 0 + : _a.etag) === null || _b === void 0 + ? void 0 + : _b[0] + : typeof (resp === null || resp === void 0 + ? void 0 + : resp.responseHeaders) == "string" + ? // Tampermonkey + resp === null || resp === void 0 + ? void 0 + : resp.responseHeaders + : // Appara + (_c = + resp === null || resp === void 0 + ? void 0 + : resp.getAllResponseHeaders) === null || _c === void 0 + ? void 0 + : _c.call(resp) + ); + return etag; + } + function storeRule(rule, resp) { + var _a, _c; + return __awaiter(this, void 0, void 0, function* () { + let savedRules = {}; + savedRules = + (_a = yield values.rules()) !== null && _a !== void 0 + ? _a + : defaultValues.rules; + if (resp.responseText) { + let parsed = resp.responseText; + if (rule.筛选后存储) { + parsed = resp.responseText + .split("\n") + .filter((rule) => CRRE.test(rule) || isBasicRule(rule)) + .join("\n"); + } + savedRules[rule.标识] = parsed; + { + yield values.rules(savedRules); + if (savedRules[rule.标识].length !== 0) { + const etag = extrEtag(resp), + savedEtags = + (_c = yield values.etags()) !== null && _c !== void 0 + ? _c + : defaultValues.etags; + if (etag) { + savedEtags[rule.标识] = etag; + yield values.etags(savedEtags); + } + } + } + data.receivedRules += "\n" + savedRules[rule.标识]; + } + return; + }); + } + function fetchRuleBody(rule) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const url = addTimeParam(rule.地址); + const getResp = yield promiseXhr({ + method: "GET", + responseType: "text", + url, + }).catch((error) => { + console.error("规则: ", url, " 下载错误: ", error); + }); + if ( + (_a = + getResp === null || getResp === void 0 + ? void 0 + : getResp.responseText) === null || _a === void 0 + ? void 0 + : _a.length + ) { + yield storeRule(rule, getResp); + return true; + } else return false; + }); + } + function fetchRuleGet(resp, rule) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + const etag = extrEtag(resp), + savedEtags = yield values.etags(); + if ( + (_a = + resp === null || resp === void 0 ? void 0 : resp.responseText) === + null || _a === void 0 + ? void 0 + : _a.length + ) { + yield storeRule(rule, resp); + if ( + etag !== + (savedEtags === null || savedEtags === void 0 + ? void 0 + : savedEtags[rule.标识]) + ) { + return; + } else { + return Promise.reject("ETag 一致"); + } + } else { + if ( + etag !== + (savedEtags === null || savedEtags === void 0 + ? void 0 + : savedEtags[rule.标识]) + ) { + if (yield fetchRuleBody(rule)) { + return; + } else { + return Promise.reject("GET 失败"); + } + } else return Promise.reject("ETag 一致"); + } + }); + } + function fetchRule(rule) { + var _a; + return __awaiter(this, void 0, void 0, function* () { + let headRespError = {}; + const url = addTimeParam(rule.地址); + const headResp = yield promiseXhr({ + method: "HEAD", + responseType: "text", + url, + }).catch((error) => { + headRespError = error; + console.error("规则: ", url, " HEAD 错误: ", error); + }); + if (!headResp) { + // Via HEAD 会超时,但可以得到 ETag + if ( + (_a = + headRespError === null || headRespError === void 0 + ? void 0 + : headRespError.resp) === null || _a === void 0 + ? void 0 + : _a.responseHeaders + ) { + return yield fetchRuleGet(headRespError.resp, rule); + } else { + return Promise.reject("HEAD 失败"); + } + } else { + return yield fetchRuleGet(headResp, rule); + } + }); + } + function fetchRules() { + return __awaiter(this, void 0, void 0, function* () { + let hasUpdate = preset.onlineRules.length; + data.updating = true; + yield gmMenu("update", () => void 0); + for (const rule of preset.onlineRules) { + if (rule.在线更新) { + yield fetchRule(rule).catch((error) => { + console.error("获取规则 ", rule, " 发生错误: ", error); + hasUpdate--; + }); + } else { + hasUpdate--; + } + } + values.time(new Date().toLocaleString("zh-CN")); + data.updating = false; + yield makeInitMenu(); + if (hasUpdate > 0) { + yield Promise.all( + (yield getSavedHosts()).map((host) => + __awaiter(this, void 0, void 0, function* () { + if (host === location.hostname) { + initRules(true); + } else { + const save = yield values.css(void 0, host); + if (save) { + save.needUpdate = true; + yield values.css(save, host); + } + } + }) + ) + ); + } + }); + } + function performUpdate(force) { + var _b; + return __awaiter(this, void 0, void 0, function* () { + if (data.isFrame) return Promise.reject(); + return force || + new Date( + (_b = yield values.time()) !== null && _b !== void 0 + ? _b + : defaultValues.time + ).getDate() !== new Date().getDate() + ? fetchRules() + : Promise.resolve(); + }); + } + function main() { + var _a, _b; + return __awaiter(this, void 0, void 0, function* () { + if (!location.protocol.startsWith("http")) return; + // 初始化 data + data.disabled = + (_b = + (_a = yield values.black()) === null || _a === void 0 + ? void 0 + : _a.includes(location.hostname)) !== null && _b !== void 0 + ? _b + : false; + data.preset = yield getUserConfig("css"); + data.timeout = yield getUserConfig("timeout"); + data.tryCount = yield getUserConfig("tryCount"); + data.tryTimeout = yield getUserConfig("tryTimeout"); + data.headTimeout = yield getUserConfig("headTimeout"); + let finish = false; + gmMenu("disable", switchDisabledStat); + if (data.disabled) { + gmMenu("count", cleanRules); + return; + } + if (yield getSavedHosts(location.hostname)) yield readCss(); + const hash = yield getCustomHash(false); + saved: { + yield makeInitMenu(); + if ((yield values.hash()) !== hash) { + yield getCustomHash(true); + yield initRules(true); + break saved; + } + if (data.saved) { + styleApply(); + if (!data.update) break saved; + } + yield initRules(false); + if (data.receivedRules.length === 0) { + yield performUpdate(true); + yield gmMenu("count"); + yield initRules(true); + finish = true; + } else yield splitRules(); + } + if (!finish) { + try { + yield performUpdate(false); + } catch (_error) { + console.warn("iframe: ", location.href, " 取消更新"); + } + } + }); + } + { + runOnce(data.mutex, main); + } + })($presets, $polyfills); +})(); diff --git a/guix/home.scm b/guix/home.scm index afa161e..cbc409f 100644 --- a/guix/home.scm +++ b/guix/home.scm @@ -75,6 +75,9 @@ "imv" "phinger-cursors-theme" "firefox" + "icecat" + "qutebrowser" + "nyxt" "openjdk" "kwallet" "kwallet-pam" @@ -119,6 +122,7 @@ "blesh" "ncurses" "transmission" + "sbcl" ;; Emacs and packages "emacs-next-pgtk" @@ -176,6 +180,7 @@ "emacs-projectile" "emacs-simple-httpd" "emacs-direnv" + "emacs-sly" "emacs-diredfl" "emacs-pdf-tools" "emacs-vterm" diff --git a/guix/pkgs/zola.scm b/guix/pkgs/zola.scm new file mode 100644 index 0000000..decbf11 --- /dev/null +++ b/guix/pkgs/zola.scm @@ -0,0 +1,37 @@ +(define-module (zola) + #:use-module (gnu packages) + #:use-module (gnu packages base) + #:use-module (gnu packages compression) + #:use-module (gnu services) + #:use-module (guix utils) + #:use-module (guix gexp) + #:use-module (guix packages) + #:use-module (guix git-download) + #:use-module (guix build-system cargo) + #:use-module ((guix licenses) #:prefix license:)) + +(define-public zola + (let ((commit "8dd1b30594dcfa5344ba36b6b057a5b0aa9bd277") + (revision "0")) + (package + (name "zola") + (version "0.17.2") + (source (origin + (method git-fetch) + (uri (git-reference + (url "https://github.com/getzola/zola.git") + (commit commit))) + (sha256 + (base32 + "11ynfizxgfm0dy8i4s0dfws4g9chf12n41hzai8n936wxb3vb3r0")))) + (build-system cargo-build-system) + (native-inputs (list tar bzip2)) + ;; (arguments + ;; ) + (home-page "https://github.com/phisch/phinger-cursors") + (synopsis "Most likely the most over engineered cursor theme out there") + (description + "Say hello to your new cursor theme. Phinger cursors is most likely the most over engineered cursor theme out there.") + (license license:cc-by-sa4.0)))) + +zola diff --git a/scripts/nyxtlof b/scripts/nyxtlof index 84dc02a..79fd84c 100755 --- a/scripts/nyxtlof +++ b/scripts/nyxtlof @@ -1,45 +1,37 @@ -#!/bin/bash +#!/usr/bin/env bash # Check to see if nyxt is running -if pgrep -x nyxt > /dev/null; then - echo running +if [ $(pgrep -c nyxt) -gt 0 ]; then + echo "ff running" - if [ $XDG_SESSION_TYPE = "x11" ]; then + if [ "$XDG_SESSION_TYPE" = "x11" ]; then #X11 - nyxtrg=$(wmctrl -lx | rg nyxt | awk '{print $1}') + ffrg=$(wmctrl -lx | rg nyxt | awk '{print $1}') # echo $emacsrg - if [ -z $nyxtrg ]; then - echo regnyxt - nyxt + if [ -z $ffrg ]; then + exec nyxt exit else - wmctrl -ia $nyxtrg + exec wmctrl -ia $ffrg exit fi else - echo wayland - if [ $KDE_FULL_SESSION = "true" ]; then - ww -f nyxt -c nyxt + if [ "$KDE_FULL_SESSION" = "true" ]; then + echo "KDE" + exec /home/chris/bin/ww -fa nyxt -c nyxt exit else # WAYLAND - nyxtrg=$(wlrctl window list | rg nyxt:) + ffrg=$(hyprctl clients | rg nyxt) - nyxtwin=$(echo $nyxtrg | sed 's/.*\: //') - # echo $nyxtwin + ffwin=$(echo $ffrg | sed 's/.*\: //') + # echo $ffwin - wlrctl toplevel focus nyxt + exec hyprctl dispatch focuswindow nyxt exit fi fi else - echo not running - machine=$(hostname) - if [ $machine = "syl" ]; then - # env GDK_DPI_SCALE=0.5 GDK_SCALE=2 nyxt - nyxt - else - nyxt - fi + exec nyxt fi