From 240df1fa419c5244fdd9eaf56266808bd065b35b Mon Sep 17 00:00:00 2001 From: Chris Cochrun Date: Mon, 3 Jul 2023 09:19:49 -0500 Subject: [PATCH] adding nyxt and zola base --- .config/hypr/hyprland.conf | 2 +- .config/nyxt/auto-config.3.lisp | 0 .config/nyxt/auto-config.lisp | 16 - .config/nyxt/config.lisp | 215 + .config/nyxt/dark-reader.lisp | 10 + .config/nyxt/glyphs.lisp | 24 - .config/nyxt/hsplit.lisp | 25 + .config/nyxt/init.lisp | 255 - .config/nyxt/search-engines.lisp | 20 + .config/nyxt/slynk.lisp | 17 - .config/nyxt/status.lisp | 23 + .config/nyxt/style.lisp | 73 + .config/nyxt/unpdf.lisp | 86 + .config/nyxt/userscripts/block-cosmetic.js | 7970 ++++++++++++++++++++ guix/home.scm | 5 + guix/pkgs/zola.scm | 37 + scripts/nyxtlof | 40 +- 17 files changed, 8481 insertions(+), 337 deletions(-) create mode 100644 .config/nyxt/auto-config.3.lisp delete mode 100644 .config/nyxt/auto-config.lisp create mode 100644 .config/nyxt/config.lisp create mode 100644 .config/nyxt/dark-reader.lisp delete mode 100644 .config/nyxt/glyphs.lisp create mode 100644 .config/nyxt/hsplit.lisp delete mode 100644 .config/nyxt/init.lisp create mode 100644 .config/nyxt/search-engines.lisp delete mode 100644 .config/nyxt/slynk.lisp create mode 100644 .config/nyxt/status.lisp create mode 100644 .config/nyxt/style.lisp create mode 100644 .config/nyxt/unpdf.lisp create mode 100644 .config/nyxt/userscripts/block-cosmetic.js create mode 100644 guix/pkgs/zola.scm 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 the 
 colors.
+                          (: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