uBlock Origin

extension name.

Axır ki, prosessor və yaddaş yükünü azaldan səmərəli bir əngəlləyici var.

this will be in the Chrome web store: must be 132 characters or less

uBlock₀ — İdarəetmə paneli

English: uBlock₀ — Dashboard

Xəbərdarlıq! Saxlamadığınız dəyişikliklər var

A warning in the dashboard when navigating away from unsaved changes

Qal

Label for button to prevent navigating away from unsaved changes

Əhəmiyyət vermə

Label for button to ignore unsaved changes

Tənzimləmələr

appears as tab name in dashboard

Filtr siyahıları

appears as tab name in dashboard

Filtrlərim

appears as tab name in dashboard

Qaydalarım

appears as tab name in dashboard

Etibarlı saytlar

appears as tab name in dashboard

Qısayollar

appears as tab name in dashboard

uBlock₀ — Jurnal

Title for the logger window

Haqqında

appears as tab name in dashboard

Dəstək

appears as tab name in dashboard

uBlock₀ — Resurslar

Title for the asset viewer page

Qabaqcıl tənzimləmələr

Title for the advanced settings page

Klikləmə: Bu sayt üçün uBlock₀-u fəallaşdır/sıradan çıxart.

Ctrl+klikləmə: Yalnız bu səhifə üçün uBlock₀-u sıradan çıxart.

English: Click: disable/enable uBlock₀ for this site.

Ctrl+click: disable uBlock₀ only on this page.

Bu sayt üçün uBlock₀-u sıradan çıxartmaq üçün klikləyin.

Yalnız bu səhifə üçün uBlock₀-u sıradan çıxartmaq üçün Ctrl+klikləyin.

Message to be read by screen readers

Bu saytda uBlock₀-u fəallaşdırmaq üçün klikləyin.

Message to be read by screen readers

tələb(request) bloklandı

English: requests blocked

bu səhifədə

English: on this page

{{count}} və ya {{percent}}%

Example: 15 (13%)

quraşdırmadan bəri

English: since install

və ya

English: or

Bu səhifədə əngəllənən

For the new mobile-friendly popup design

Quraşdırmadan bəri əngəllənən

Bağlantı qurulmuş domenlər

İdarəetmə panelini aç

English: Click to open the dashboard

Element silmə rejiminə keç

Tooltip for the element-zapper icon in the popup panel

Element seçmə rejiminə keç

English: Enter element picker mode

Jurnalı aç

Tooltip used for the logger icon in the panel

Bu veb-saytdakı problemi bildir

Tooltip used for the 'chat' icon in the panel

Bu sayt üçün bütün açılan pəncələri aç/bağla

Tooltip for the no-popups per-site switch

Bu saytda bütün açılan pəncərələri əngəlləmək üçün kliklə

Bu saytdakı bütün açılan pəncələri artıq əngəlləməmək üçün kliklə

Bu saytda böyük media elemetlərini əngəlləməyi aç/bağla

Tooltip for the no-large-media per-site switch

Bu saytda böyük media elementlərini əngəlləmək üçün düyməyə basın

Bu saytda böyük media elementlərini daha əngəlləməmək üçün düyməyə basın

Bu saytda kosmetik filtrləri qoş/söndür

Tooltip for the no-cosmetic-filtering per-site switch

Bu saytda kosmetik filtrləri söndürmək üçün düyməyə basın

Bu saytda kosmetik filtrləri qoşmaq üçün düyməyə basın

Bu saytda uzaqdan idarə olunan şriftləri qoş/söndür

Tooltip for the no-remote-fonts per-site switch

Bu saytda uzaqdan idarə olunan şriftləri əngəlləmək üçün düyməyə basın

Bu saytda uzaqdan idarə olunan şriftləri daha əngəlləməmək üçün düyməyə basın

Bu saytda JavaScript-i sıradan çıxartmaq üçün kliklə

Tooltip for the no-scripting per-site switch

Bu saytda JavaScript-i artıq əngəlləməmək üçün düyməyə basın

Peyda olan pəncərələr

Caption for the no-popups per-site switch

Böyük ölçülü media elementləri

Caption for the no-large-media per-site switch

Kosmetik filtrləmə

Caption for the no-cosmetic-filtering per-site switch

Kənar şriftlər

Caption for the no-remote-fonts per-site switch

JavaScript

Caption for the no-scripting per-site switch

Daha çox

Label to be used to show popup panel sections

Daha az

Label to be used to hide popup panel sections

Qlobal qaydalar: bu sütun bütün saytlarda tətbiq olunan qaydalar üçündür.

Tooltip when hovering the top-most cell of the global-rules column.

Lokal qaydalar: bu sütun yalnız bu saytda tətbiq olunan qaydalar üçündür.
Lokal qaydalar qlobal qaydalardan üstün tutulur.

Tooltip when hovering the top-most cell of the local-rules column.

Dəyişiklikləri daimi etmək üçün klikləyin.

Tooltip when hovering over the padlock in the dynamic filtering pane.

Dəyişiklikləri geri qaytarmaq üçün düyməyə basın.

Tooltip when hovering over the eraser in the dynamic filtering pane.

hamısı

təsvirlər

3-cü tərəf

3-cü tərəf CSS/təsvirlər

sətiriçi skriptlər

1-ci tərəf skriptlər

3-cü tərəf skriptlər

3-cü tərəf frame-lər

qoşulmuş domen

appears in popup

{{count}} / {{total}}

appears in popup

Versiya

Example of use: Version 1.26.4

skript

Appears as an option to filter out firewall rows

çərçivə

Yarat

English: Create

Seç

English: Pick

Çıxış

English: Quit

Önbaxış

Element picker preview mode: will cause the elements matching the current filter to be removed from the page

Şəbəkə filtrləri

English: header for a type of filter in the element picker dialog

Kosmetik filtrlər

English: Cosmetic filters

Klik, Ctrl-klik

English: Click, Ctrl-click

Elementi əngəllə...

An entry in the browser's contextual menu

Əngəllənmiş elementlərin tutduğu yeri gizlə

English: Hide placeholders of blocked elements

Əngəllənən tələb sayını nişan üstündə göstər

English: Show the number of blocked requests on the icon

Alət izahlarını söndür

A checkbox in the Settings pane

Uyğun olduqda kontekst menüsündən istifadə et

English: Make use of context menu where appropriate

Rəng seçmə qabiliyyəti olmayanlar üçün rəngləmə rejimi

English: Color-blind friendly

Görünüş

Section for controlling user interface appearance

Mövzu

Label for checkbox to enable a custom dark theme

Xüsusi vurğu rəngi

Label for checkbox to pick an accent color

Bulud yaddaşı dəstəyini işə sal

Mən təcrübəli istifadəçiyəm (mütləq oxuyun)

Checkbox to let user access advanced, technical features

Erkən yükləməni dayandır (əngəllənmiş şəbəkə sorğuları üzrə qoşulmalara yol verməmək üçün)

English:

Hiperkeçidlərin yoxlanılmasını dayandır

English:

Lokal IP-ünvanların WebRTC vasitəsilə sızmasını əngəllə

English:

Standart davranış

Bu standart parametrlər hər bir sayt üçün ayrıca qeyd oluna bilər

Kosmetik filtrləri söndür

{{input}} KB-dan artıq olan media elementlərini əngəllə

Uzaqdan idarə olunan şriftləri əngəllə

JavaScript-i Əngəllə

The default state for the per-site no-scripting switch

CSP hesabatlarını əngəllə

background information: https://github.com/gorhill/uBlock/issues/3150

Canonical adları ortaya çıxarın

background information: https://github.com/uBlockOrigin/uBlock-issues/issues/1513

Təkmil

Section for controlling advanced-user settings

Yalnız texniki istifadəçilərə uyğun parametrlər

Description of section controlling advanced-user settings

əlavə parametrlər

For the tooltip of a link which gives access to advanced settings

Son bərpa əməliyyatının tarixi:

English: Last restore:

Son ehtiyat nüsxəsinin tarixi:

English: Last backup:

{{netFilterCount}} şəbəkə filtri ＋ {{cosmeticFilterCount}} kosmetik filtr:

Appears at the top of the _3rd-party filters_ pane

{{total}} ədəddən {{used}} ədədi istifadə olundu

Appears aside each filter list in the _3rd-party filters_ pane

Filtr siyahılarını avtomatik yenilə

A checkbox in the _3rd-party filters_ pane

Yenilə

A button in the in the _3rd-party filters_ pane

Bütün keşləri sil

Kosmetik filtrləri təhlil və tətbiq et

English: Parse and enforce Adblock+ element hiding filters.

Kosmetik filtrlər bir veb səhifəsində görüntü rahatsızlığına səbəb olan və şəbəkə sorğularının filtrlənməsi vasitəsilə əngəllənə bilməyən elementləri gizlətmək üçün istifadə olunur.

Describes the purpose of the 'Parse and enforce cosmetic filters' feature.

Ümumi kosmetik filtrləri nəzərə alma

This will cause uBO to ignore all generic cosmetic filters.

Ümumi kosmetik filtrlərin bütün veb-saytlarda istifadəsi nəzərdə tutulur. Bu seçimi etdiyiniz təqdirdə veb-səhifələrdə ümumi kosmetik filtrlərin işlənməsi ilə əlaqədar yaddaşdan istifadə və prosessor yükü azalacaq.

Az güclü cihazlarda bu seçimin aktivləşdirilməsi tövsiyə olunur.

Describes the purpose of the 'Ignore generic cosmetic filters' feature.

Filter siyahıları yüklənənə kimi şəbəkə fəaliyyətini dayandır

A checkbox in the 'Filter lists' pane

Lists of blocked hosts

English: Lists of blocked hosts

Dəyişiklikləri tətbiq et

English: Apply changes

Lokal filtrlər

Filter lists section name

Reklam

Filter lists section name

Məxfilik

Filter lists section name

Zərərli domenlər

Filter lists section name

Zəhlətökən elementlər əleyhinə filtrlər

Filter lists section name

Çoxməqsədli filtrlər

Filter lists section name

Bölgələr, dillər

Filter lists section name

Fərdi

Filter lists section name

Əlavə et...

The label for the checkbox used to import external filter lists

Hər sətrdə yalnız bir URL. Yanlış URL-lər xəbərdarlıq edilmədən nəzərə alınmayacaq.

Short information about how to use the textarea to import external filter lists by URL

Köhnəlmişdir.

used as a tooltip for the out-of-date icon beside a list

məzmunu nəzərdən keçir

used as a tooltip for eye icon beside a list

Sonuncu yeniləmə: {{ago}}.
Yeniləmək üçün düyməyə basın.

used as a tooltip for the clock icon beside a list

Yenilənir...

used as a tooltip for the spinner icon beside a list

Şəbəkə xətası üzündən yeniləmə mümkün olmadı.

used as a tooltip for error icon beside a list

Hər sətirdə yalnız bir filtr. Bu, ya domen adı, ya da Adblock Plus formatında yazılmış filtr ola bilər. Əvvəli <code>!</code> ilə başlayan sətirlər nəzərə alınmayacaqdır.

Short information about how to create custom filters

Etibarsız mənbələrdən filtrlər əlavə etməyin.

Warning against copy-pasting filters from random sources

İdxal və əlavə et

Button in the 'My filters' pane

İxrac et

Button in the 'My filters' pane

my-ublock-static-filters_{{datetime}}.txt

English: my-ublock-static-filters_{{datetime}}.txt

Dəyişiklikləri tətbiq et

English: Apply changes

Daimi qaydalar

header

Müvəqqəti qaydalar

header

Geri qaytar

This will remove all temporary rules

Təsdiq et

This will persist temporary rules

Redaktə

Will enable manual-edit mode (textarea)

Yadda saxla

Will save manually-edited content and exit manual-edit mode

Nəzərə alma

Will discard manually-edited content and exit manual-edit mode

Fayldan götür...

Fayla yaz

Button in the 'My rules' pane

my-ublock-dynamic-rules_{{datetime}}.txt

default file name to use

Dinamik filtr qaydalarınızın siyahısı.

English: List of your dynamic filtering rules.

Qaydalar sintaksisi: mənbə təyinat növ əməliyyat (bütün sənədlər).

English: dynamic rule syntax and full documentation.

Sırala:

English: label for sort option.

Qayda növü

English: a sort option for list of rules.

Mənbə

Hədəf

İstisnalar siyahısındakı təlimatlar uBlock Origin-in hansı veb-səhifələrdə işləyişinin dayandırılmasını təmin edir. Hər sətirdə yalnız bir təlimat ola bilər. Yanlış təlimatlar xəbərdarlıq edilmədən nəzərə alınmayacaq və şərhə çeviriləcəkdir.

A concise description of the 'Trusted sites' pane.

İdxal və əlavə et

Button in the 'Trusted sites' pane

İxrac et

Button in the 'Trusted sites' pane

The default filename to use for import/export purpose

Dəyişiklikləri tətbiq et

English: Apply changes

Növü

English: Type

Domen

English: Domain

URL

English: URL

Filtr

English: Filter

Hamısı

Appears in the logger's tab selector

Tabsız

Pretty name for behind-the-scene network requests

Cari tab vərəqi

Appears in the logger's tab selector

Tab vərəqinin məzmununu yenidən yüklə

Tooltip for the reload button in the logger page

DOM müfəttişini işə sal/söndür

Tooltip for the DOM inspector button in the logger page

Peyda olan paneli aç/bağla

Tooltip for the popup panel button in the logger page

uBlock Origin viki: Logger

Tooltip for the top-right info label in the logger page

Loggeri təmizlə

Tooltip for the eraser in the logger page; used to blank the content of the logger

Loggeri dayandır (daxil olan bütün məlumatları rədd et)

Tooltip for the pause button in the logger page

Loggeri yenidən işə sal

Tooltip for the play button in the logger page

Logger filtrləməsini işə sal/dayandır

Tooltip for the row filterer button in the logger page

logger qeydlərinin filtrlənməsi

Placeholder string for logger output filtering input field

Logger üçün filtrləmə parametrləri

Tooltip for the button to bring up logger output filtering options

Deyil

A keyword in the built-in row filtering expression

eventful

A keyword in the built-in row filtering expression: all items corresponding to uBO doing something (blocked, allowed, redirected, etc.)

əngəllənmiş

icazə verilmiş

dəyişdirildi

1-ci tərəf resurslar

3-cü tərəf resurslar

Təfsilatlar

Small header to identify the 'Details' pane for a specific logger entry

Filtr

Label to identify a filter field

Filtr siyahısı

Label to identify a filter list field

Qayda

Label to identify a rule field

Məzmun

Label to identify a context field (typically a hostname)

Ana məzmun

Label to identify a root context field (typically a hostname)

Tərəf

Label to identify a field providing partyness information

Növü

Label to identify the type of an entry

URL

Label to identify the URL of an entry

URL qaydası

Small header to identify the dynamic URL filtering section

Məzmun:

Label for the context selector

Növ:

Label for the type selector

Statik filtr

Small header to identify the static filtering section

Müvafiq istisna filtri varsa {{importance}},{{br}}URL ünvanı {{url}} {{br}}ilə eyni olan və mənbəyi {{origin}} {{br}}olan {{type}} növlü şəbəkə sorğularını {{action}}.

Used in the static filtering wizard

Əngəllə

Used in the static filtering wizard

İcazə ver

Used in the static filtering wizard

“{{type}}”

Used in the static filtering wizard

istənilən növ

Used in the static filtering wizard

“{{origin}}”

Used in the static filtering wizard

istənilən yerdən

Used in the static filtering wizard

halından savayı

Used in the static filtering wizard

əgər hətta

Used in the static filtering wizard

<code>{{filter}}</code> statik filtrinin mənbəyi:

Below this sentence, the filter list(s) in which the filter was found

Statik filtr hal-hazırda aktiv olan filtr siyahılarının heç birində tapılmadı

Message to show when a filter cannot be found in any filter lists

Üç şərtin hamısına cavab erməyən aşağıdakı logger qeydləri avtomatik olaraq rədd ediləcək:

Logger setting: A sentence to describe the purpose of the settings below

Son {{input}} dəq. ərzində edilmiş qeydləri saxla

A logger setting

Hər tab vərəqi üzrə ən çox {{input}} səh. yükləməsini saxla

A logger setting

Hər tab vərəqi üzrə ən çox {{input}} qeyd saxla

A logger setting

Şaquli genişləndirilmiş rejimdə hər qeyd üçün {{input}} sətir istifadə et

A logger setting

Sütunları gizlə:

Logger settings: a sentence to describe the purpose of the checkboxes below

{{input}} Vaxt

A label for the time column

{{input}} Filtr/Qayda

A label for the filter or rule column

{{input}} Məzmun

A label for the context column

{{input}} Tərəf

A label for the partyness column

Siyahı

Label for radio-button to pick export format

Cədvəl

Sadə

Label for radio-button to pick export text format

Nişanlı

Aç

Text for button which open an external webpage in Support pane

Yeni hesabat yaradın

Oxşar hesabat tap

A clickable link in the filter issue reporter section

Dokumentasiya

Header of 'Documentation' section in Support pane

UBlock Origin-in bütün xüsusiyyətlərini öyrənmək üçün uBlock/wiki ünvanında sənədləri oxuyun.

First paragraph of 'Documentation' section in Support pane

Suallar və dəstək

Header of 'Questions and support' section in Support pane

Suallara və yardım dəstəyinin digər növlərinə cavablar <code>/r/uBlock Origin</Code > - də təqdim olunur.

First paragraph of 'Questions and support' section in Support pane

Filter problemləri / vebsəhifə işləmir

Header of 'Filter issues' section in Support pane

Müəyyən vebsaytların filtr problemlərini  uBlockOrigin/uAssets problem izləyicisinə bildirin. GitHub hesabı tələb edir.

First paragraph of 'Filter issues' section in Support pane

<b>Vacib:</b> uBlock Origin-lə yanaşı, oxşar bloklayıcı genişləndirmələrin istifadəsindən çəkinin. Əks halda bəzi veb-saytlarda filtr problemləri ilə üzləşə bilərsiniz.

Second paragraph of 'Filter issues' section in Support pane

İpuçları: Filtr siyahınızın yenilənmiş olduğundan əmin olun. Qeydiyyatçı, filtrlərlə bağlı problemləri müəyyənləşdirmək üçün başlıca vasitədir.

Third paragraph of 'Filter issues' section in Support pane

Xəta bildirişi

Header of 'Bug report' section in Support pane

uBlock Origin-lə bağlı problemləri uBlockOrigin/uBlock-issue problem izləyicisinə bildirin. Github hesabı tələb edir.

First paragraph of 'Bug report' section in Support pane

Problemlərin Həlli

Header of 'Troubleshooting Information' section in Support pane

Aşağıdaki könüllülər sizə probleminizi aradan qaldırmağa çalışırkən faydalı ola biləcək texniki məlumatdır.

First paragraph of 'Troubleshooting Information' section in Support pane

<b>Vacib:</b> Gizli və həssas ola biləcək məlumatlar defolt olaraq qısaldılır. Qısaldılmış məlumatlar, bir problemi aradan qaldırmağı çətinləşdirə bilər.

Second paragraph of 'Troubleshooting Information' section in Support pane

Filter problemini bildir

Header of 'Report a filter issue' section in Support pane

Könüllülərə eyni hesabatların narahatçılıq verməməsi üçün, xahiş olunur problemin əvvəlcədən bildirilmədiyinə əmin olun.

A paragraph in the filter issue reporter section

Filtr siyahıları hər gün yenilənir. Probleminizin ən son filtr siyahılarında həll edilmədiyinə əmin olun.

Problemli veb-səhifəni yenidən yüklədikdən sonra problemin hələ də qaldığından əmin olun.

Veb-səhifənin ünvanı:

Label for the URL of the page

Səhifə...

Label for widget to select type of issue

-- Bir daxiletmə seçin --

An entry in the widget used to select the type of issue

Reklam və ya reklam qalıqları göstərir

Üst-üstə düşmə və ya digər nasazlıqları var

uBlock Origin-i aşkarlayır

Məxfiliklə bağlı problemləri var

uBlock Origin aktiv ikən xarab olur

Arzuolunmaz tab-vərəqələr və ya pəncərələr açır

Veb-səhifəni uyğun olmayan (“NSFW”) olaraq işarələ (“Not Safe For Work”)

A checkbox to use for NSFW sites

Redact

Text for 'Redact' button

Unredact

Text for 'Unredact' button

Məxfilik siyasəti

Link to privacy policy on GitHub (English)

Dəyişikliklər siyahısı

Mənbə kodu (GPLv3)

English: Source code (GPLv3)

Layihəyə töhfə verənlər

English: Contributors

Mənbə kodu

Link text to source code repo

Tərcümələr

Link text to translations repo

Filtr siyahıları

Link text to uBO's own filter lists repo

Xarici asılılıqlar (GPLv3-uyumlu):

Shown in the About pane

uBO-nun öz filtr siyahıları ödənişsiz şəkildə bu CDN-lərdə yerləşdirilib:

Shown in the About pane

Bir filtr siyahısının yenilənməsi lazım olduqda təsadüfi seçilən bir CDN istifadə olunur

Shown in the About pane

Fayla yaz

Text for button to create a backup of all settings

English: my-ublock-backup_{{datetime}}.txt

Fayldan bərpa et...

English: Restore from file...

Standart parametrləri yüklə...

English: Reset to default settings...

Bütün parametrləriniz {{time}} tarixində yadda saxlanılmış məlumatlarla əvəz olunacaq və uBlock₀ yenidən başladılacaq.

Bütün cari parametrlər ehtiyat nüsxəsindəki məlumatlarla əvəz olunsun?

Message asking user to confirm restore

Məlumat oxunmur yaxud xətalıdır

Message to display when an error occurred during restore

Bütün parametrləriniz silinəcək və uBlock₀ yenidən başladılacaq.

uBlock₀ standart parametrlərə qaytarılsın?

Message asking user to confirm reset

Şəbəkə xətası: {{msg}}

English: Network error: {{msg}}

uBlock₀: Aşağıdakı URL ünvanı fərdi filtr siyahılarınıza əlavə edilsin?

Adı: "{{title}}"
URL ünvanı: {{url}}

No longer used

Abunə ol

For the button used to subscribe to a filter list

bir dəqiqə əvvəl

English: a minute ago

{{value}} dəq. əvvəl

English: {{value}} minutes ago

bir saat əvvəl

English: an hour ago

{{value}} saat əvvəl

English: {{value}} hours ago

bir gün əvvəl

English: a day ago

{{value}} gün əvvəl

English: {{value}} days ago

İdarəetmə panelini göstər

Firefox/Fennec-specific: Show Dashboard

Loggeri göstər

Firefox/Fennec-specific: Show Logger

işləmir

Firefox-specific: appears as 'uBlock₀ (off)'

Səhifə əngəlləndi

Used as a title for the document-blocked page

uBlock Origin bu səhifənin yüklənməsini əngəllədi:

Used in the strict-blocking page

Bu filtrə görə

Used in the strict-blocking page

parametrsiz

label to be used for the parameter-less URL: https://cloud.githubusercontent.com/assets/585534/9832014/bfb1b8f0-593b-11e5-8a27-fba472a5529a.png

Mənbə:

English: List of filter list names follows

Geriyə qayıt

English: Go back

Bu pəncərəni bağla

English: Close this window

Bu sayt barədə məni xəbərdar etmə

Label for checkbox in document-blocked page

{{hostname}} üçün sərt əngəlləməni dayandır

English: Disable strict blocking for {{hostname}} ...

Müvəqqəti

English: Temporarily

Daimi

English: Permanently

Davam et

Button text to navigate to the blocked page

Bulud yaddaşa göndər

tooltip

Bulud yaddaşdan götür

tooltip

Bulud yaddaşdan götür və cari parametrlərlə birləşdir

tooltip

Bu cihazın adı:

used as a prompt for the user to provide a custom device name

Diqqət! Bu əlavə parametrlərdəki dəyişikliklərə görə siz məsuliyyət daşıyırsınız.

A warning to users at the top of 'Advanced settings' page

Təsdiqlə

for generic 'Submit' buttons

Dəyişiklikləri tətbiq et

for generic 'Apply changes' buttons

Geri qaytar

for generic 'Revert' buttons

bayt

Çərçivədəki elementi əngəllə

An entry in the browser's contextual menu

Filter siyahısına abunə ol

Böyük media elementlərinin yüklənməsinə müvəqqəti icazə ver

A context menu entry, present when large media elements have been blocked on the current site

Mənbə kodunu nəzərdən keçir...

A context menu entry, to view the source code of the target resource

Qısayol yaz

Placeholder string for input field used to capture a keyboard shortcut

Məhdud sürüşdürməni işə sal/dayandır

Tooltip for the button used to lock scrolling between the views in the 'My rules' pane

Lövhəyə kopyala

Label for buttons used to copy something to the clipboard

Hamısını seç

Label for buttons used to select all text in editor

Kozmetik filtrləməni yandır/söndür

Label for keyboard shortcut used to toggle cosmetic filtering

Toggle JavaScript

Label for keyboard shortcut used to toggle no-scripting switch

Əngəlləmə rejimini yüngülləşdir

Label for keyboard shortcut used to relax blocking mode

İstifadə olunan anbar: {{value}} {{unit}}

In Setting pane, renders as (example): Storage used: 13.2 MB

KB

short for 'kilobytes'

MB

short for 'megabytes'

GB

short for 'gigabytes'

Yükləmək üçün düyməyə bas

Message used in frame placeholders

Xətalar: {{count}}

Summary of number of errors as reported by the linter

Brauzer açılışında filtrləmə düzgün alınmadı. Düzgün filtrləməni təmin etmək üçün səhifəni yenidən yükləyin.

A warning which will appear in the popup panel if needed

This entry must be the last one

so we dont need to deal with comma for last entry


} else if ( line === `!#trusted off ${context.secret}` ) {

keep keep replace keep keep keep keep keep

uBO Lite (uBOL) is a *permission-less* MV3-based content blocker.

The default ruleset corresponds to uBlock Origin's default filterset:

- uBlock Origin's built-in filter lists
- EasyList
- EasyPrivacy
- Peter Lowe’s Ad and tracking server list

You can add more rulesets by visiting the options page -- click the _Cogs_ icon in the popup panel.

uBOL is entirely declarative, meaning there is no need for a permanent uBOL process for the filtering to occur, and CSS/JS injection-based content filtering is performed reliably by the browser itself rather than by the extension. This means that uBOL itself does not consume CPU/memory resources while content blocking is ongoing -- uBOL's service worker process is required _only_ when you interact with the popup panel or the option pages.

uBOL does not require broad "read and modify data" permission at install time, hence its limited capabilities out of the box compared to uBlock Origin or other content blockers requiring broad "read and modify data" permissions at install time.

However, uBOL allows you to *explicitly* grant extended permissions on specific sites of your choice so that it can better filter on those sites using cosmetic filtering and scriptlet injections.

To grant extended permissions on a given site, open the popup panel and pick a higher filtering mode such as Optimal or Complete.

The browser will then warn you about the effects of granting the additional permissions requested by the extension on the current site, and you will have to tell the browser whether you accept or decline the request.

If you accept uBOL's request for additional permissions on the current site, it will be able to better filter content for the current site.

You can set the default filtering mode from uBOL's options page. If you pick the Optimal or Complete mode as the default one, you will need to grant uBOL the permission to read and modify data on all websites.

Keep in mind this is still a work in progress, with these end goals:

- No broad host permissions at install time -- extended permissions are granted explicitly by the user on a per-site basis.

- Entirely declarative for reliability and CPU/memory efficiency.


/*******************************************************************************

    uBlock Origin - a comprehensive, efficient content blocker
    Copyright (C) 2014-present Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

'use strict';

/******************************************************************************/

import staticNetFilteringEngine from './static-net-filtering.js';
import { LineIterator } from './text-utils.js';
import * as sfp from './static-filtering-parser.js';

import {
    CompiledListReader,
    CompiledListWriter,
} from './static-filtering-io.js';

/******************************************************************************/

// http://www.cse.yorku.ca/~oz/hash.html#djb2
//   Must mirror content script surveyor's version

const hashFromStr = (type, s) => {
    const len = s.length;
    const step = len + 7 >>> 3;
    let hash = (type << 5) + type ^ len;
    for ( let i = 0; i < len; i += step ) {
        hash = (hash << 5) + hash ^ s.charCodeAt(i);
    }
    return hash & 0xFFFFFF;
};

/******************************************************************************/

// Copied from cosmetic-filter.js for the time being to avoid unwanted
// dependencies

const rePlainSelector = /^[#.][\w\\-]+/;
const rePlainSelectorEx = /^[^#.\[(]+([#.][\w-]+)|([#.][\w-]+)$/;
const rePlainSelectorEscaped = /^[#.](?:\\[0-9A-Fa-f]+ |\\.|\w|-)+/;
const reEscapeSequence = /\\([0-9A-Fa-f]+ |.)/g;

const keyFromSelector = selector => {
    let key = '';
    let matches = rePlainSelector.exec(selector);
    if ( matches ) {
        key = matches[0];
    } else {
        matches = rePlainSelectorEx.exec(selector);
        if ( matches === null ) { return; }
        key = matches[1] || matches[2];
    }
    if ( key.indexOf('\\') === -1 ) { return key; }
    matches = rePlainSelectorEscaped.exec(selector);
    if ( matches === null ) { return; }
    key = '';
    const escaped = matches[0];
    let beg = 0;
    reEscapeSequence.lastIndex = 0;
    for (;;) {
        matches = reEscapeSequence.exec(escaped);
        if ( matches === null ) {
            return key + escaped.slice(beg);
        }
        key += escaped.slice(beg, matches.index);
        beg = reEscapeSequence.lastIndex;
        if ( matches[1].length === 1 ) {
            key += matches[1];
        } else {
            key += String.fromCharCode(parseInt(matches[1], 16));
        }
    }
};

/******************************************************************************/

function addExtendedToDNR(context, parser) {
    if ( parser.isExtendedFilter() === false ) { return false; }

    // Scriptlet injection
    if ( parser.isScriptletFilter() ) {
        if ( parser.hasOptions() === false ) { return; }
        if ( context.scriptletFilters === undefined ) {
            context.scriptletFilters = new Map();
        }
        const exception = parser.isException();
        const args = parser.getScriptletArgs();
        const argsToken = JSON.stringify(args);
        for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
            if ( bad ) { continue; }
            if ( exception ) { continue; }
            let details = context.scriptletFilters.get(argsToken);
            if ( details === undefined ) {
                context.scriptletFilters.set(argsToken, details = { args });
                if ( context.trustedSource ) {
                    details.trustedSource = true;
                }
            }
            if ( not ) {
                if ( details.excludeMatches === undefined ) {
                    details.excludeMatches = [];
                }
                details.excludeMatches.push(hn);
                continue;
            }
            if ( details.matches === undefined ) {
                details.matches = [];
            }
            if ( details.matches.includes('*') ) { continue; }
            if ( hn === '*' ) {
                details.matches = [ '*' ];
                continue;
            }
            details.matches.push(hn);
        }
        return;
    }

    // Response header filtering
    if ( parser.isResponseheaderFilter() ) {
        if ( parser.hasError() ) { return; }
        if ( parser.hasOptions() === false ) { return; }
        if ( parser.isException() ) { return; }
        const node = parser.getBranchFromType(sfp.NODE_TYPE_EXT_PATTERN_RESPONSEHEADER);
        if ( node === 0 ) { return; }
        const header = parser.getNodeString(node);
        if ( context.responseHeaderRules === undefined ) {
            context.responseHeaderRules = [];
        }
        const rule =  {
            action: {
                responseHeaders: [
                    {
                        header,
                        operation: 'remove',
                    }
                ],
                type: 'modifyHeaders'
            },
            condition: {
                resourceTypes: [
                    'main_frame',
                    'sub_frame'
                ]
            },
        };
        for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
            if ( bad ) { continue; }
            if ( not ) {
                if ( rule.condition.excludedInitiatorDomains === undefined ) {
                    rule.condition.excludedInitiatorDomains = [];
                }
                rule.condition.excludedInitiatorDomains.push(hn);
                continue;
            }
            if ( hn === '*' ) {
                if ( rule.condition.initiatorDomains !== undefined ) {
                    rule.condition.initiatorDomains = undefined;
                }
                continue;
            }
            if ( rule.condition.initiatorDomains === undefined ) {
                rule.condition.initiatorDomains = [];
            }
            rule.condition.initiatorDomains.push(hn);
        }
        context.responseHeaderRules.push(rule);
        return;
    }

    // HTML filtering
    if ( (parser.flavorBits & parser.BITFlavorExtHTML) !== 0 ) {
        return;
    }

    // Cosmetic filtering

    // Generic cosmetic filtering
    if ( parser.hasOptions() === false ) {
        const { compiled } = parser.result;
        if ( compiled === undefined ) { return; }
        if ( compiled.length <= 1 ) { return; }
        if ( parser.isException() ) {
            if ( context.genericCosmeticExceptions === undefined ) {
                context.genericCosmeticExceptions = new Set();
            }
            context.genericCosmeticExceptions.add(compiled);
            return;
        }
        if ( compiled.charCodeAt(0) === 0x7B /* '{' */ ) { return; }
        const key = keyFromSelector(compiled);
        if ( key === undefined ) {
            if ( context.genericHighCosmeticFilters === undefined ) {
                context.genericHighCosmeticFilters = new Set();
            }
            context.genericHighCosmeticFilters.add(compiled);
            return;
        }
        const type = key.charCodeAt(0);
        const hash = hashFromStr(type, key.slice(1));
        if ( context.genericCosmeticFilters === undefined ) {
            context.genericCosmeticFilters = new Map();
        }
        let bucket = context.genericCosmeticFilters.get(hash);
        if ( bucket === undefined ) {
            context.genericCosmeticFilters.set(hash, bucket = []);
        }
        bucket.push(compiled);
        return;
    }

    // Specific cosmetic filtering
    // https://github.com/chrisaljoudi/uBlock/issues/151
    //   Negated hostname means the filter applies to all non-negated hostnames
    //   of same filter OR globally if there is no non-negated hostnames.
    if ( context.specificCosmeticFilters === undefined ) {
        context.specificCosmeticFilters = new Map();
    }
    for ( const { hn, not, bad } of parser.getExtFilterDomainIterator() ) {
        if ( bad ) { continue; }
        let { compiled, exception, raw } = parser.result;
        if ( exception ) { continue; }
        let rejected;
        if ( compiled === undefined ) {
            rejected = `Invalid filter: ${hn}##${raw}`;
        }
        if ( rejected ) {
            compiled = rejected;
        }
        let details = context.specificCosmeticFilters.get(compiled);
        if ( details === undefined ) {
            details = {};
            if ( rejected ) { details.rejected = true; }
            context.specificCosmeticFilters.set(compiled, details);
        }
        if ( rejected ) { continue; }
        if ( not ) {
            if ( details.excludeMatches === undefined ) {
                details.excludeMatches = [];
            }
            details.excludeMatches.push(hn);
            continue;
        }
        if ( details.matches === undefined ) {
            details.matches = [];
        }
        if ( details.matches.includes('*') ) { continue; }
        if ( hn === '*' ) {
            details.matches = [ '*' ];
            continue;
        }
        details.matches.push(hn);
    }
}

/******************************************************************************/

function addToDNR(context, list) {
    const env = context.env || [];
    const writer = new CompiledListWriter();
    const lineIter = new LineIterator(
        sfp.utils.preparser.prune(list.text, env)
    );
    const parser = new sfp.AstFilterParser({
        toDNR: true,
        nativeCssHas: env.includes('native_css_has'),
        badTypes: [ sfp.NODE_TYPE_NET_OPTION_NAME_REDIRECTRULE ],
    });
    const compiler = staticNetFilteringEngine.createCompiler();

    writer.properties.set('name', list.name);
    compiler.start(writer);

    while ( lineIter.eot() === false ) {
        let line = lineIter.next();
        while ( line.endsWith(' \\') ) {
            if ( lineIter.peek(4) !== '    ' ) { break; }
            line = line.slice(0, -2).trim() + lineIter.next().trim();
        }

        parser.parse(line);

        if ( parser.isComment() ) {
            if ( line === `!#trusted on ${context.secret}` ) {
                parser.trustedSource = true;
                context.trustedSource = true;
            } else if ( line === `!#trusted off ${context.secret}` ) {
                parser.trustedSource = false;
                context.trustedSource = false;
            }
            continue;
        }

        if ( parser.isFilter() === false ) { continue; }
        if ( parser.hasError() ) {
            if ( parser.astError === sfp.AST_ERROR_OPTION_EXCLUDED ) {
                context.invalid.add(`Incompatible with DNR: ${line}`);
            }
            continue;
        }

        if ( parser.isExtendedFilter() ) {
            addExtendedToDNR(context, parser);
            continue;
        }
        if ( parser.isNetworkFilter() === false ) { continue; }

        if ( compiler.compile(parser, writer) ) { continue; }

        if ( compiler.error !== undefined ) {
            context.invalid.add(compiler.error);
        }
    }

    compiler.finish(writer);

    staticNetFilteringEngine.dnrFromCompiled(
        'add',
        context,
        new CompiledListReader(writer.toString())
    );
}

/******************************************************************************/

function finalizeRuleset(context, network) {
    const ruleset = network.ruleset;

    // Assign rule ids
    const rulesetMap = new Map();
    {
        let ruleId = 1;
        for ( const rule of ruleset ) {
            rulesetMap.set(ruleId++, rule);
        }
    }
    // Merge rules where possible by merging arrays of a specific property.
    //
    // https://github.com/uBlockOrigin/uBOL-home/issues/10#issuecomment-1304822579
    //   Do not merge rules which have errors.
    const mergeRules = (rulesetMap, mergeTarget) => {
        const mergeMap = new Map();
        const sorter = (_, v) => {
            if ( Array.isArray(v) ) {
                return typeof v[0] === 'string' ? v.sort() : v;
            }
            if ( v instanceof Object ) {
                const sorted = {};
                for ( const kk of Object.keys(v).sort() ) {
                    sorted[kk] = v[kk];
                }
                return sorted;
            }
            return v;
        };
        const ruleHasher = (rule, target) => {
            return JSON.stringify(rule, (k, v) => {
                if ( k.startsWith('_') ) { return; }
                if ( k === target ) { return; }
                return sorter(k, v);
            });
        };
        const extractTargetValue = (obj, target) => {
            for ( const [ k, v ] of Object.entries(obj) ) {
                if ( Array.isArray(v) && k === target ) { return v; }
                if ( v instanceof Object ) {
                    const r = extractTargetValue(v, target);
                    if ( r !== undefined ) { return r; }
                }
            }
        };
        const extractTargetOwner = (obj, target) => {
            for ( const [ k, v ] of Object.entries(obj) ) {
                if ( Array.isArray(v) && k === target ) { return obj; }
                if ( v instanceof Object ) {
                    const r = extractTargetOwner(v, target);
                    if ( r !== undefined ) { return r; }
                }
            }
        };
        for ( const [ id, rule ] of rulesetMap ) {
            if ( rule._error !== undefined ) { continue; }
            const hash = ruleHasher(rule, mergeTarget);
            if ( mergeMap.has(hash) === false ) {
                mergeMap.set(hash, []);
            }
            mergeMap.get(hash).push(id);
        }
        for ( const ids of mergeMap.values() ) {
            if ( ids.length === 1 ) { continue; }
            const leftHand = rulesetMap.get(ids[0]);
            const leftHandSet = new Set(
                extractTargetValue(leftHand, mergeTarget) || []
            );
            for ( let i = 1; i < ids.length; i++ ) {
                const rightHandId = ids[i];
                const rightHand = rulesetMap.get(rightHandId);
                const rightHandArray =  extractTargetValue(rightHand, mergeTarget);
                if ( rightHandArray !== undefined ) {
                    if ( leftHandSet.size !== 0 ) {
                        for ( const item of rightHandArray ) {
                            leftHandSet.add(item);
                        }
                    }
                } else {
                    leftHandSet.clear();
                }
                rulesetMap.delete(rightHandId);
            }
            const leftHandOwner = extractTargetOwner(leftHand, mergeTarget);
            if ( leftHandSet.size > 1 ) {
                //if ( leftHandOwner === undefined ) { debugger; }
                leftHandOwner[mergeTarget] = Array.from(leftHandSet).sort();
            } else if ( leftHandSet.size === 0 ) {
                if ( leftHandOwner !== undefined ) {
                    leftHandOwner[mergeTarget] = undefined;
                }
            }
        }
    };
    mergeRules(rulesetMap, 'resourceTypes');
    mergeRules(rulesetMap, 'initiatorDomains');
    mergeRules(rulesetMap, 'requestDomains');
    mergeRules(rulesetMap, 'removeParams');
    mergeRules(rulesetMap, 'responseHeaders');

    // Patch id
    const rulesetFinal = [];
    {
        let ruleId = 1;
        for ( const rule of rulesetMap.values() ) {
            if ( rule._error === undefined ) {
                rule.id = ruleId++;
            } else {
                rule.id = 0;
            }
            rulesetFinal.push(rule);
        }
        for ( const invalid of context.invalid ) {
            rulesetFinal.push({ _error: [ invalid ] });
        }
    }

    network.ruleset = rulesetFinal;
}

/******************************************************************************/

async function dnrRulesetFromRawLists(lists, options = {}) {
    const context = Object.assign({}, options);
    staticNetFilteringEngine.dnrFromCompiled('begin', context);
    context.extensionPaths = new Map(context.extensionPaths || []);
    const toLoad = [];
    const toDNR = (context, list) => addToDNR(context, list);
    for ( const list of lists ) {
        if ( list instanceof Promise ) {
            toLoad.push(list.then(list => toDNR(context, list)));
        } else {
            toLoad.push(toDNR(context, list));
        }
    }
    await Promise.all(toLoad);
    const result = {
        network: staticNetFilteringEngine.dnrFromCompiled('end', context),
        genericCosmetic: context.genericCosmeticFilters,
        genericHighCosmetic: context.genericHighCosmeticFilters,
        genericCosmeticExceptions: context.genericCosmeticExceptions,
        specificCosmetic: context.specificCosmeticFilters,
        scriptlet: context.scriptletFilters,
    };
    if ( context.responseHeaderRules ) {
        result.network.ruleset.push(...context.responseHeaderRules);
    }
    finalizeRuleset(context, result.network);
    return result;
}

/******************************************************************************/

export { dnrRulesetFromRawLists };


# CodeMirror

[![Build Status](https://travis-ci.org/codemirror/CodeMirror.svg)](https://travis-ci.org/codemirror/CodeMirror)
[![NPM version](https://img.shields.io/npm/v/codemirror.svg)](https://www.npmjs.org/package/codemirror)

CodeMirror is a versatile text editor implemented in JavaScript for
the browser. It is specialized for editing code, and comes with over
100 language modes and various addons that implement more advanced
editing functionality. Every language comes with fully-featured code
and syntax highlighting to help with reading and editing complex code.

A rich programming API and a CSS theming system are available for
customizing CodeMirror to fit your application, and extending it with
new functionality.

You can find more information (and the
[manual](https://codemirror.net/doc/manual.html)) on the [project
page](https://codemirror.net). For questions and discussion, use the
[discussion forum](https://discuss.codemirror.net/).

See
[CONTRIBUTING.md](https://github.com/codemirror/CodeMirror/blob/master/CONTRIBUTING.md)
for contributing guidelines.

The CodeMirror community aims to be welcoming to everybody. We use the
[Contributor Covenant
(1.1)](http://contributor-covenant.org/version/1/1/0/) as our code of
conduct.

### Installation

Either get the [zip file](https://codemirror.net/codemirror.zip) with
the latest version, or make sure you have [Node](https://nodejs.org/)
installed and run:

    npm install codemirror

**NOTE**: This is the source repository for the library, and not the
distribution channel. Cloning it is not the recommended way to install
the library, and will in fact not work unless you also run the build
step.

### Quickstart

To build the project, make sure you have Node.js installed (at least version 6)
and then `npm install`. To run, just open `index.html` in your
browser (you don't need to run a webserver). Run the tests with `npm test`.


/*******************************************************************************

    uBlock Origin - a comprehensive, efficient content blocker
    Copyright (C) 2022-present Raymond Hill

    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see {http://www.gnu.org/licenses/}.

    Home: https://github.com/gorhill/uBlock
*/

'use strict';

/******************************************************************************/

import fs from 'fs/promises';
import https from 'https';
import path from 'path';
import process from 'process';
import { createHash, randomBytes } from 'crypto';
import redirectResourcesMap from './js/redirect-resources.js';
import { dnrRulesetFromRawLists } from './js/static-dnr-filtering.js';
import * as sfp from './js/static-filtering-parser.js';
import * as makeScriptlet from './make-scriptlets.js';
import { safeReplace } from './safe-replace.js';

/******************************************************************************/

const commandLineArgs = (( ) => {
    const args = new Map();
    let name, value;
    for ( const arg of process.argv.slice(2) ) {
        const pos = arg.indexOf('=');
        if ( pos === -1 ) {
            name = arg;
            value = '';
        } else {
            name = arg.slice(0, pos);
            value = arg.slice(pos+1);
        }
        args.set(name, value);
    }
    return args;
})();

const platform = commandLineArgs.get('platform') || 'chromium';
const outputDir = commandLineArgs.get('output') || '.';
const cacheDir = `${outputDir}/../mv3-data`;
const rulesetDir = `${outputDir}/rulesets`;
const scriptletDir = `${rulesetDir}/scripting`;
const env = [
    platform,
    'mv3',
    'ublock',
    'ubol',
    'user_stylesheet',
];

if ( platform !== 'firefox' ) {
    env.push('native_css_has');
}

/******************************************************************************/

const jsonSetMapReplacer = (k, v) => {
    if ( v instanceof Set || v instanceof Map ) {
        if ( v.size === 0 ) { return; }
        return Array.from(v);
    }
    return v;
};

const uidint32 = (s) => {
    const h = createHash('sha256').update(s).digest('hex').slice(0,8);
    return parseInt(h,16) & 0x7FFFFFFF;
};

const hnSort = (a, b) =>
    a.split('.').reverse().join('.').localeCompare(
        b.split('.').reverse().join('.')
    );

/******************************************************************************/

const stdOutput = [];

const log = (text, silent = false) => {
    stdOutput.push(text);
    if ( silent === false ) {
        console.log(text);
    }
};

/******************************************************************************/

const urlToFileName = url => {
    return url
        .replace(/^https?:\/\//, '')
        .replace(/\//g, '_')
        ;
};

const fetchText = (url, cacheDir) => {
    return new Promise((resolve, reject) => {
        const fname = urlToFileName(url);
        fs.readFile(`${cacheDir}/${fname}`, { encoding: 'utf8' }).then(content => {
            log(`\tFetched local ${url}`);
            resolve({ url, content });
        }).catch(( ) => {
            log(`\tFetching remote ${url}`);
            https.get(url, response => {
                const data = [];
                response.on('data', chunk => {
                    data.push(chunk.toString());
                });
                response.on('end', ( ) => {
                    const content = data.join('');
                    try {
                        writeFile(`${cacheDir}/${fname}`, content);
                    } catch (ex) {
                    }
                    resolve({ url, content });
                });
            }).on('error', error => {
                reject(error);
            });
        });
    });
};

/******************************************************************************/

const writeFile = async (fname, data) => {
    const dir = path.dirname(fname);
    await fs.mkdir(dir, { recursive: true });
    const promise = fs.writeFile(fname, data);
    writeOps.push(promise);
    return promise;
};

const copyFile = async (from, to) => {
    const dir = path.dirname(to);
    await fs.mkdir(dir, { recursive: true });
    const promise = fs.copyFile(from, to);
    writeOps.push(promise);
    return promise;
};

const writeOps = [];

/******************************************************************************/

const ruleResources = [];
const rulesetDetails = [];
const scriptletStats = new Map();
const genericDetails = new Map();
const requiredRedirectResources = new Set();

/******************************************************************************/

async function fetchList(assetDetails) {
    // Remember fetched URLs
    const fetchedURLs = new Set();

    // Fetch list and expand `!#include` directives
    let parts = assetDetails.urls.map(url => ({ url }));
    while (  parts.every(v => typeof v === 'string') === false ) {
        const newParts = [];
        for ( const part of parts ) {
            if ( typeof part === 'string' ) {
                newParts.push(part);
                continue;
            }
            if ( fetchedURLs.has(part.url) ) {
                newParts.push('');
                continue;
            }
            fetchedURLs.add(part.url);
            if ( part.url.startsWith('https://ublockorigin.github.io/uAssets/filters/') ) {
                newParts.push(`!#trusted on ${assetDetails.secret}`);
            }
            newParts.push(
                fetchText(part.url, cacheDir).then(details => {
                    const { url } = details;
                    const content = details.content.trim();
                    if ( typeof content === 'string' && content !== '' ) {
                        if (
                            content.startsWith('<') === false ||
                            content.endsWith('>') === false
                        ) {
                            return { url, content };
                        }
                    }
                    log(`No valid content for ${details.name}`);
                    return { url, content: '' };
                })
            );
            newParts.push(`!#trusted off ${assetDetails.secret}`);
        }
        parts = await Promise.all(newParts);
        parts = sfp.utils.preparser.expandIncludes(parts, env);
    }
    const text = parts.join('\n');

    if ( text === '' ) {
        log('No filterset found');
    }
    return text;
}

/******************************************************************************/

const isUnsupported = rule =>
    rule._error !== undefined;

const isRegex = rule =>
    rule.condition !== undefined &&
    rule.condition.regexFilter !== undefined;

const isRedirect = rule => {
    if ( rule.action === undefined ) { return false; }
    if ( rule.action.type !== 'redirect' ) { return false; }
    if ( rule.action.redirect?.extensionPath !== undefined ) { return true; }
    if ( rule.action.redirect?.transform?.path !== undefined ) { return true; }
    return false;
};

const isModifyHeaders = rule =>
    rule.action !== undefined &&
    rule.action.type === 'modifyHeaders';

const isRemoveparam = rule =>
    rule.action !== undefined &&
    rule.action.type === 'redirect' &&
    rule.action.redirect.transform !== undefined;

const isGood = rule =>
    isUnsupported(rule) === false &&
    isRedirect(rule) === false &&
    isModifyHeaders(rule) === false &&
    isRemoveparam(rule) === false;

/******************************************************************************/

// Two distinct hostnames:
//   www.example.com
//   example.com
// Can be reduced to a single one:
//   example.com
// Since if example.com matches, then www.example.com (or any other subdomain
// of example.com) will always match.

function pruneHostnameArray(hostnames) {
    const rootMap = new Map();
    for ( const hostname of hostnames ) {
        const labels = hostname.split('.');
        let currentMap = rootMap;
        let i = labels.length;
        while ( i-- ) {
            const label = labels[i];
            let nextMap = currentMap.get(label);
            if ( nextMap === null ) { break; }
            if ( nextMap === undefined ) {
                if ( i === 0 ) {
                    currentMap.set(label, (nextMap = null));
                } else {
                    currentMap.set(label, (nextMap = new Map()));
                }
            } else if ( i === 0 ) {
                currentMap.set(label, null);
            }
            currentMap = nextMap;
        }
    }
    const assemble = (currentMap, currentHostname, out) => {
        for ( const [ label, nextMap ] of currentMap ) {
            const nextHostname = currentHostname === ''
                ? label
                : `${label}.${currentHostname}`;
            if ( nextMap === null ) {
                out.push(nextHostname);
            } else {
                assemble(nextMap, nextHostname, out);
            }
        }
        return out;
    };
    return assemble(rootMap, '', []);
}

/*******************************************************************************
 *
 * For large rulesets, one rule per line for compromise between size and
 * readability. This also means that the number of lines in resulting file
 * representative of the number of rules in the ruleset.
 *
 * */

function toJSONRuleset(ruleset) {
    const replacer = (k, v) => {
        if ( k.startsWith('_') ) { return; }
        if ( Array.isArray(v) ) {
            return v.sort();
        }
        if ( v instanceof Object ) {
            const sorted = {};
            for ( const kk of Object.keys(v).sort() ) {
                sorted[kk] = v[kk];
            }
            return sorted;
        }
        return v;
    };
    const indent = ruleset.length > 10 ? undefined : 1;
    const out = [];
    for ( const rule of ruleset ) {
        out.push(JSON.stringify(rule, replacer, indent));
    }
    return `[\n${out.join(',\n')}\n]\n`;
}

/******************************************************************************/

async function processNetworkFilters(assetDetails, network) {
    const { ruleset: rules } = network;
    log(`Input filter count: ${network.filterCount}`);
    log(`\tAccepted filter count: ${network.acceptedFilterCount}`);
    log(`\tRejected filter count: ${network.rejectedFilterCount}`);
    log(`Output rule count: ${rules.length}`);

    // https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/declarativeNetRequest/RuleCondition#browser_compatibility
    // isUrlFilterCaseSensitive is true by default in Chromium. It will be
    // false by default in Chromium 118+.
    if ( platform !== 'firefox' ) {
        for ( const rule of rules ) {
            const { condition } = rule;
            if ( condition === undefined ) { continue; }
            if ( condition.urlFilter === undefined ) {
                if ( condition.regexFilter === undefined ) { continue; }
            }
            if ( condition.isUrlFilterCaseSensitive === undefined ) {
                condition.isUrlFilterCaseSensitive = false;
            } else if ( condition.isUrlFilterCaseSensitive === true ) {
                condition.isUrlFilterCaseSensitive = undefined;
            }
        }
    }

    // Minimize requestDomains arrays
    for ( const rule of rules ) {
        const condition = rule.condition;
        if ( condition === undefined ) { continue; }
        const requestDomains = condition.requestDomains;
        if ( requestDomains === undefined ) { continue; }
        const beforeCount = requestDomains.length;
        condition.requestDomains = pruneHostnameArray(requestDomains);
        const afterCount = condition.requestDomains.length;
        if ( afterCount !== beforeCount ) {
            log(`\tPruning requestDomains: from ${beforeCount} to ${afterCount}`);
        }
    }

    // Add native DNR ruleset if present
    if ( assetDetails.dnrURL ) {
        const result = await fetchText(assetDetails.dnrURL, cacheDir);
        for ( const rule of JSON.parse(result.content) ) {
            rules.push(rule);
        }
    }

    const plainGood = rules.filter(rule => isGood(rule) && isRegex(rule) === false);
    log(`\tPlain good: ${plainGood.length}`);
    log(plainGood
        .filter(rule => Array.isArray(rule._warning))
        .map(rule => rule._warning.map(v => `\t\t${v}`))
        .join('\n'),
        true
    );

    const regexes = rules.filter(rule => isGood(rule) && isRegex(rule));
    log(`\tMaybe good (regexes): ${regexes.length}`);

    const redirects = rules.filter(rule =>
        isUnsupported(rule) === false &&
        isRedirect(rule)
    );
    redirects.forEach(rule => {
        if ( rule.action.redirect.extensionPath === undefined ) { return; }
        requiredRedirectResources.add(
            rule.action.redirect.extensionPath.replace(/^\/+/, '')
        );
    });
    log(`\tredirect=: ${redirects.length}`);

    const removeparamsGood = rules.filter(rule =>
        isUnsupported(rule) === false && isRemoveparam(rule)
    );
    const removeparamsBad = rules.filter(rule =>
        isUnsupported(rule) && isRemoveparam(rule)
    );
    log(`\tremoveparams= (accepted/discarded): ${removeparamsGood.length}/${removeparamsBad.length}`);

    const modifyHeaders = rules.filter(rule =>
        isUnsupported(rule) === false &&
        isModifyHeaders(rule)
    );
    log(`\tmodifyHeaders=: ${modifyHeaders.length}`);

    const bad = rules.filter(rule =>
        isUnsupported(rule)
    );
    log(`\tUnsupported: ${bad.length}`);
    log(bad.map(rule => rule._error.map(v => `\t\t${v}`)).join('\n'), true);

    writeFile(
        `${rulesetDir}/main/${assetDetails.id}.json`,
        toJSONRuleset(plainGood)
    );

    if ( regexes.length !== 0 ) {
        writeFile(
            `${rulesetDir}/regex/${assetDetails.id}.json`,
            toJSONRuleset(regexes)
        );
    }

    if ( removeparamsGood.length !== 0 ) {
        writeFile(
            `${rulesetDir}/removeparam/${assetDetails.id}.json`,
            toJSONRuleset(removeparamsGood)
        );
    }

    if ( redirects.length !== 0 ) {
        writeFile(
            `${rulesetDir}/redirect/${assetDetails.id}.json`,
            toJSONRuleset(redirects)
        );
    }

    if ( modifyHeaders.length !== 0 ) {
        writeFile(
            `${rulesetDir}/modify-headers/${assetDetails.id}.json`,
            toJSONRuleset(modifyHeaders)
        );
    }

    return {
        total: rules.length,
        plain: plainGood.length,
        discarded: removeparamsBad.length,
        rejected: bad.length,
        regex: regexes.length,
        removeparam: removeparamsGood.length,
        redirect: redirects.length,
        modifyHeaders: modifyHeaders.length,
    };
}

/******************************************************************************/

// TODO: unify css/scriptlet processing code since now css styles are
// injected using scriptlet injection.

// Load all available scriptlets into a key-val map, where the key is the
// scriptlet token, and val is the whole content of the file.

let scriptletsMapPromise;

function loadAllSourceScriptlets() {
    if ( scriptletsMapPromise !== undefined ) {
        return scriptletsMapPromise;
    }

    scriptletsMapPromise = fs.readdir('./scriptlets').then(files => {
        const readTemplateFile = file =>
            fs.readFile(`./scriptlets/${file}`, { encoding: 'utf8' })
              .then(text => ({ file, text }));
        const readPromises = [];
        for ( const file of files ) {
            readPromises.push(readTemplateFile(file));
        }
        return Promise.all(readPromises).then(results => {
            const originalScriptletMap = new Map();
            for ( const details of results ) {
                originalScriptletMap.set(
                    details.file.replace('.template.js', '')
                                .replace('.template.css', ''),
                    details.text
                );
            }
            return originalScriptletMap;
        });
    });

    return scriptletsMapPromise;
}

/******************************************************************************/

async function processGenericCosmeticFilters(assetDetails, bucketsMap, exceptionSet) {
    if ( bucketsMap === undefined ) { return 0; }
    if ( exceptionSet ) {
        for ( const [ hash, selectors ] of bucketsMap ) {
            let i = selectors.length;
            while ( i-- ) {
                const selector = selectors[i];
                if ( exceptionSet.has(selector) === false ) { continue; }
                selectors.splice(i, 1);
                //log(`\tRemoving excepted generic filter ##${selector}`);
            }
            if ( selectors.length === 0 ) {
                bucketsMap.delete(hash);
            }
        }
    }
    if ( bucketsMap.size === 0 ) { return 0; }
    const bucketsList = Array.from(bucketsMap);
    const count = bucketsList.reduce((a, v) => a += v[1].length, 0);
    if ( count === 0 ) { return 0; }
    const selectorLists = bucketsList.map(v => [ v[0], v[1].join(',') ]);
    const originalScriptletMap = await loadAllSourceScriptlets();

    let patchedScriptlet = originalScriptletMap.get('css-generic').replace(
        '$rulesetId$',
        assetDetails.id
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$genericSelectorMap\$/,
        `${JSON.stringify(selectorLists, scriptletJsonReplacer)}`
    );

    writeFile(
        `${scriptletDir}/generic/${assetDetails.id}.js`,
        patchedScriptlet
    );

    log(`CSS-generic: ${count} plain CSS selectors`);

    return count;
}

/******************************************************************************/

async function processGenericHighCosmeticFilters(assetDetails, selectorSet, exceptionSet) {
    if ( selectorSet === undefined ) { return 0; }
    if ( exceptionSet ) {
        for ( const selector of selectorSet ) {
            if ( exceptionSet.has(selector) === false ) { continue; }
            selectorSet.delete(selector);
            //log(`\tRemoving excepted generic filter ##${selector}`);
        }
    }
    if ( selectorSet.size === 0 ) { return 0; }
    const selectorLists = Array.from(selectorSet).sort().join(',\n');
    const originalScriptletMap = await loadAllSourceScriptlets();

    let patchedScriptlet = originalScriptletMap.get('css-generichigh').replace(
        '$rulesetId$',
        assetDetails.id
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\$selectorList\$/,
        selectorLists
    );

    writeFile(
        `${scriptletDir}/generichigh/${assetDetails.id}.css`,
        patchedScriptlet
    );

    log(`CSS-generic-high: ${selectorSet.size} plain CSS selectors`);

    return selectorSet.size;
}

/******************************************************************************/

// This merges selectors which are used by the same hostnames

function groupSelectorsByHostnames(mapin) {
    if ( mapin === undefined ) { return []; }
    const merged = new Map();
    for ( const [ selector, details ] of mapin ) {
        if ( details.rejected ) { continue; }
        const json = JSON.stringify(details);
        let entries = merged.get(json);
        if ( entries === undefined ) {
            entries = new Set();
            merged.set(json, entries);
        }
        entries.add(selector);
    }
    const out = [];
    for ( const [ json, entries ] of merged ) {
        const details = JSON.parse(json);
        details.selectors = Array.from(entries).sort();
        out.push(details);
    }
    return out;
}

// This merges hostnames which have the same set of selectors.
//
// Also, we sort the hostnames to increase likelihood that selector with
// same hostnames will end up in same generated scriptlet.

function groupHostnamesBySelectors(arrayin) {
    const contentMap = new Map();
    for ( const entry of arrayin ) {
        const id = uidint32(JSON.stringify(entry.selectors));
        let details = contentMap.get(id);
        if ( details === undefined ) {
            details = { a: entry.selectors };
            contentMap.set(id, details);
        }
        if ( entry.matches !== undefined ) {
            if ( details.y === undefined ) {
                details.y = new Set();
            }
            for ( const hn of entry.matches ) {
                details.y.add(hn);
            }
        }
        if ( entry.excludeMatches !== undefined ) {
            if ( details.n === undefined ) {
                details.n = new Set();
            }
            for ( const hn of entry.excludeMatches ) {
                details.n.add(hn);
            }
        }
    }
    const out = Array.from(contentMap).map(a => [
        a[0], {
            a: a[1].a,
            y: a[1].y ? Array.from(a[1].y).sort(hnSort) : '*',
            n: a[1].n ? Array.from(a[1].n) : undefined,
        }
    ]).sort((a, b) => {
        const ha = Array.isArray(a[1].y) ? a[1].y[0] : '*';
        const hb = Array.isArray(b[1].y) ? b[1].y[0] : '*';
        return hnSort(ha, hb);
    });
    return out;
}

const scriptletHostnameToIdMap = (hostnames, id, map) => {
    for ( const hn of hostnames ) {
        const existing = map.get(hn);
        if ( existing === undefined ) {
            map.set(hn, id);
        } else if ( Array.isArray(existing) ) {
            existing.push(id);
        } else {
            map.set(hn, [ existing, id ]);
        }
    }
};

const scriptletJsonReplacer = (k, v) => {
    if ( k === 'n' ) {
        if ( v === undefined || v.size === 0 ) { return; }
        return Array.from(v);
    }
    if ( v instanceof Set || v instanceof Map ) {
        if ( v.size === 0 ) { return; }
        return Array.from(v);
    }
    return v;
};

/******************************************************************************/

function argsMap2List(argsMap, hostnamesMap) {
    const argsList = [];
    const indexMap = new Map();
    for ( const [ id, details ] of argsMap ) {
        indexMap.set(id, argsList.length);
        argsList.push(details);
    }
    for ( const [ hn, ids ] of hostnamesMap ) {
        if ( typeof ids === 'number' ) {
            hostnamesMap.set(hn, indexMap.get(ids));
            continue;
        }
        for ( let i = 0; i < ids.length; i++ ) {
            ids[i] = indexMap.get(ids[i]);
        }
    }
    return argsList;
}

/******************************************************************************/

async function processCosmeticFilters(assetDetails, mapin) {
    if ( mapin === undefined ) { return 0; }
    if ( mapin.size === 0 ) { return 0; }

    const domainBasedEntries = groupHostnamesBySelectors(
        groupSelectorsByHostnames(mapin)
    );
    // We do not want more than n CSS files per subscription, so we will
    // group multiple unrelated selectors in the same file, and distinct
    // css declarations will be injected programmatically according to the
    // hostname of the current document.
    //
    // The cosmetic filters will be injected programmatically as content
    // script and the decisions to activate the cosmetic filters will be
    // done at injection time according to the document's hostname.
    const generatedFiles = [];

    const argsMap = domainBasedEntries.map(entry => [
        entry[0],
        {
            a: entry[1].a ? entry[1].a.join(',\n') : undefined,
            n: entry[1].n
        }
    ]);
    const hostnamesMap = new Map();
    for ( const [ id, details ] of domainBasedEntries ) {
        if ( details.y === undefined ) { continue; }
        scriptletHostnameToIdMap(details.y, id, hostnamesMap);
    }
    const argsList = argsMap2List(argsMap, hostnamesMap);
    const entitiesMap = new Map();
    for ( const [ hn, details ] of hostnamesMap ) {
        if ( hn.endsWith('.*') === false ) { continue; }
        hostnamesMap.delete(hn);
        entitiesMap.set(hn.slice(0, -2), details);
    }

    // Extract exceptions from argsList, simplify argsList entries
    const exceptionsMap = new Map();
    for ( let i = 0; i < argsList.length; i++ ) {
        const details = argsList[i];
        if ( details.n ) {
            for ( const hn of details.n ) {
                if ( exceptionsMap.has(hn) === false ) {
                    exceptionsMap.set(hn, []);
                }
                exceptionsMap.get(hn).push(i);
            }
        }
        argsList[i] = details.a;
    }

    const originalScriptletMap = await loadAllSourceScriptlets();
    let patchedScriptlet = originalScriptletMap.get('css-specific').replace(
        '$rulesetId$',
        assetDetails.id
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$argsList\$/,
        `${JSON.stringify(argsList, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$hostnamesMap\$/,
        `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$entitiesMap\$/,
        `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$exceptionsMap\$/,
        `${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}`
    );
    writeFile(`${scriptletDir}/specific/${assetDetails.id}.js`, patchedScriptlet);
    generatedFiles.push(`${assetDetails.id}`);

    if ( generatedFiles.length !== 0 ) {
        log(`CSS-specific: ${mapin.size} distinct filters`);
        log(`\tCombined into ${hostnamesMap.size} distinct hostnames`);
        log(`\tCombined into ${entitiesMap.size} distinct entities`);
    }

    return hostnamesMap.size + entitiesMap.size;
}

/******************************************************************************/

async function processDeclarativeCosmeticFilters(assetDetails, mapin) {
    if ( mapin === undefined ) { return 0; }
    if ( mapin.size === 0 ) { return 0; }

    // Distinguish declarative-compiled-as-procedural from actual procedural.
    const declaratives = new Map();
    mapin.forEach((details, jsonSelector) => {
        const selector = JSON.parse(jsonSelector);
        if ( selector.cssable !== true ) { return; }
        selector.cssable = undefined;
        declaratives.set(JSON.stringify(selector), details);
    });
    if ( declaratives.size === 0 ) { return 0; }

    const contentArray = groupHostnamesBySelectors(
        groupSelectorsByHostnames(declaratives)
    );

    const argsMap = contentArray.map(entry => [
        entry[0],
        {
            a: entry[1].a,
            n: entry[1].n,
        }
    ]);
    const hostnamesMap = new Map();
    for ( const [ id, details ] of contentArray ) {
        if ( details.y === undefined ) { continue; }
        scriptletHostnameToIdMap(details.y, id, hostnamesMap);
    }
    const argsList = argsMap2List(argsMap, hostnamesMap);
    const entitiesMap = new Map();
    for ( const [ hn, details ] of hostnamesMap ) {
        if ( hn.endsWith('.*') === false ) { continue; }
        hostnamesMap.delete(hn);
        entitiesMap.set(hn.slice(0, -2), details);
    }

    // Extract exceptions from argsList, simplify argsList entries
    const exceptionsMap = new Map();
    for ( let i = 0; i < argsList.length; i++ ) {
        const details = argsList[i];
        if ( details.n ) {
            for ( const hn of details.n ) {
                if ( exceptionsMap.has(hn) === false ) {
                    exceptionsMap.set(hn, []);
                }
                exceptionsMap.get(hn).push(i);
            }
        }
        argsList[i] = details.a;
    }

    const originalScriptletMap = await loadAllSourceScriptlets();
    let patchedScriptlet = originalScriptletMap.get('css-declarative').replace(
        '$rulesetId$',
        assetDetails.id
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$argsList\$/,
        `${JSON.stringify(argsList, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$hostnamesMap\$/,
        `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$entitiesMap\$/,
        `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$exceptionsMap\$/,
        `${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}`
    );
    writeFile(`${scriptletDir}/declarative/${assetDetails.id}.js`, patchedScriptlet);

    if ( contentArray.length !== 0 ) {
        log(`CSS-declarative: ${declaratives.size} distinct filters`);
        log(`\tCombined into ${hostnamesMap.size} distinct hostnames`);
        log(`\tCombined into ${entitiesMap.size} distinct entities`);
    }

    return hostnamesMap.size + entitiesMap.size;
}

/******************************************************************************/

async function processProceduralCosmeticFilters(assetDetails, mapin) {
    if ( mapin === undefined ) { return 0; }
    if ( mapin.size === 0 ) { return 0; }

    // Distinguish declarative-compiled-as-procedural from actual procedural.
    const procedurals = new Map();
    mapin.forEach((details, jsonSelector) => {
        const selector = JSON.parse(jsonSelector);
        if ( selector.cssable ) { return; }
        procedurals.set(jsonSelector, details);
    });
    if ( procedurals.size === 0 ) { return 0; }

    const contentArray = groupHostnamesBySelectors(
        groupSelectorsByHostnames(procedurals)
    );

    const argsMap = contentArray.map(entry => [
        entry[0],
        {
            a: entry[1].a,
            n: entry[1].n,
        }
    ]);
    const hostnamesMap = new Map();
    for ( const [ id, details ] of contentArray ) {
        if ( details.y === undefined ) { continue; }
        scriptletHostnameToIdMap(details.y, id, hostnamesMap);
    }
    const argsList = argsMap2List(argsMap, hostnamesMap);
    const entitiesMap = new Map();
    for ( const [ hn, details ] of hostnamesMap ) {
        if ( hn.endsWith('.*') === false ) { continue; }
        hostnamesMap.delete(hn);
        entitiesMap.set(hn.slice(0, -2), details);
    }

    // Extract exceptions from argsList, simplify argsList entries
    const exceptionsMap = new Map();
    for ( let i = 0; i < argsList.length; i++ ) {
        const details = argsList[i];
        if ( details.n ) {
            for ( const hn of details.n ) {
                if ( exceptionsMap.has(hn) === false ) {
                    exceptionsMap.set(hn, []);
                }
                exceptionsMap.get(hn).push(i);
            }
        }
        argsList[i] = details.a;
    }

    const originalScriptletMap = await loadAllSourceScriptlets();
    let patchedScriptlet = originalScriptletMap.get('css-procedural').replace(
        '$rulesetId$',
        assetDetails.id
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$argsList\$/,
        `${JSON.stringify(argsList, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$hostnamesMap\$/,
        `${JSON.stringify(hostnamesMap, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$entitiesMap\$/,
        `${JSON.stringify(entitiesMap, scriptletJsonReplacer)}`
    );
    patchedScriptlet = safeReplace(patchedScriptlet,
        /\bself\.\$exceptionsMap\$/,
        `${JSON.stringify(exceptionsMap, scriptletJsonReplacer)}`
    );
    writeFile(`${scriptletDir}/procedural/${assetDetails.id}.js`, patchedScriptlet);

    if ( contentArray.length !== 0 ) {
        log(`Procedural-related distinct filters: ${procedurals.size} distinct combined selectors`);
        log(`\tCombined into ${hostnamesMap.size} distinct hostnames`);
        log(`\tCombined into ${entitiesMap.size} distinct entities`);
    }

    return hostnamesMap.size + entitiesMap.size;
}

/******************************************************************************/

async function processScriptletFilters(assetDetails, mapin) {
    if ( mapin === undefined ) { return 0; }
    if ( mapin.size === 0 ) { return 0; }

    makeScriptlet.init();

    for ( const details of mapin.values() ) {
        makeScriptlet.compile(details);
    }
    const stats = await makeScriptlet.commit(
        assetDetails.id,
        `${scriptletDir}/scriptlet`,
        writeFile
    );
    if ( stats.length !== 0 ) {
        scriptletStats.set(assetDetails.id, stats);
    }
    makeScriptlet.reset();
    return stats.length;
}

/******************************************************************************/

async function rulesetFromURLs(assetDetails) {
    log('============================');
    log(`Listset for '${assetDetails.id}':`);

    if ( assetDetails.text === undefined ) {
        const text = await fetchList(assetDetails);
        if ( text === '' ) { return; }
        assetDetails.text = text;
    }

    const extensionPaths = [];
    for ( const [ fname, details ] of redirectResourcesMap ) {
        const path = `/web_accessible_resources/${fname}`;
        extensionPaths.push([ fname, path ]);
        if ( details.alias === undefined ) { continue; }
        if ( typeof details.alias === 'string' ) {
            extensionPaths.push([ details.alias, path ]);
            continue;
        }
        if ( Array.isArray(details.alias) === false ) { continue; }
        for ( const alias of details.alias ) {
            extensionPaths.push([ alias, path ]);
        }
    }

    const results = await dnrRulesetFromRawLists(
        [ { name: assetDetails.id, text: assetDetails.text } ],
        { env, extensionPaths, secret: assetDetails.secret }
    );

    const netStats = await processNetworkFilters(
        assetDetails,
        results.network
    );

    // Split cosmetic filters into two groups: declarative and procedural
    const declarativeCosmetic = new Map();
    const proceduralCosmetic = new Map();
    const rejectedCosmetic = [];
    if ( results.specificCosmetic ) {
        for ( const [ selector, details ] of results.specificCosmetic ) {
            if ( details.rejected ) {
                rejectedCosmetic.push(selector);
                continue;
            }
            if ( selector.startsWith('{') === false ) {
                declarativeCosmetic.set(selector, details);
                continue;
            }
            const parsed = JSON.parse(selector);
            parsed.raw = undefined;
            proceduralCosmetic.set(JSON.stringify(parsed), details);
        }
    }
    if ( rejectedCosmetic.length !== 0 ) {
        log(`Rejected cosmetic filters: ${rejectedCosmetic.length}`);
        log(rejectedCosmetic.map(line => `\t${line}`).join('\n'), true);
    }

    if (
        Array.isArray(results.network.generichideExclusions) &&
        results.network.generichideExclusions.length !== 0
    ) {
        genericDetails.set(
            assetDetails.id,
            results.network.generichideExclusions.filter(hn => hn.endsWith('.*') === false).sort()
        );
    }

    const genericCosmeticStats = await processGenericCosmeticFilters(
        assetDetails,
        results.genericCosmetic,
        results.genericCosmeticExceptions
    );
    const genericHighCosmeticStats = await processGenericHighCosmeticFilters(
        assetDetails,
        results.genericHighCosmetic,
        results.genericCosmeticExceptions
    );
    const specificCosmeticStats = await processCosmeticFilters(
        assetDetails,
        declarativeCosmetic
    );
    const declarativeStats = await processDeclarativeCosmeticFilters(
        assetDetails,
        proceduralCosmetic
    );
    const proceduralStats = await processProceduralCosmeticFilters(
        assetDetails,
        proceduralCosmetic
    );
    const scriptletStats = await processScriptletFilters(
        assetDetails,
        results.scriptlet
    );

    rulesetDetails.push({
        id: assetDetails.id,
        name: assetDetails.name,
        group: assetDetails.group,
        enabled: assetDetails.enabled,
        lang: assetDetails.lang,
        homeURL: assetDetails.homeURL,
        filters: {
            total: results.network.filterCount,
            accepted: results.network.acceptedFilterCount,
            rejected: results.network.rejectedFilterCount,
        },
        rules: {
            total: netStats.total,
            plain: netStats.plain,
            regex: netStats.regex,
            removeparam: netStats.removeparam,
            redirect: netStats.redirect,
            modifyHeaders: netStats.modifyHeaders,
            discarded: netStats.discarded,
            rejected: netStats.rejected,
        },
        css: {
            generic: genericCosmeticStats,
            generichigh: genericHighCosmeticStats,
            specific: specificCosmeticStats,
            declarative: declarativeStats,
            procedural: proceduralStats,
        },
        scriptlets: scriptletStats,
    });

    ruleResources.push({
        id: assetDetails.id,
        enabled: assetDetails.enabled,
        path: `/rulesets/main/${assetDetails.id}.json`
    });
}

/******************************************************************************/

async function main() {

    let version = '';
    {
        const now = new Date();
        const yearPart = now.getUTCFullYear();
        const monthPart = now.getUTCMonth() + 1;
        const dayPart = now.getUTCDate();
        const hourPart = Math.floor(now.getUTCHours());
        const minutePart = Math.floor(now.getUTCMinutes());
        version = `${yearPart}.${monthPart}.${dayPart}.${hourPart * 60 + minutePart}`;
    }
    log(`Version: ${version}`);

    // Get assets.json content
    const assets = await fs.readFile(
        `./assets.json`,
        { encoding: 'utf8' }
    ).then(text =>
        JSON.parse(text)
    );

    // This will be used to sign our inserted `!#trusted on` directives
    const secret = createHash('sha256').update(randomBytes(16)).digest('hex').slice(0,16);
    log(`Secret: ${secret}`);

    // Assemble all default lists as the default ruleset
    const contentURLs = [
        'https://ublockorigin.github.io/uAssets/filters/filters.min.txt',
        'https://ublockorigin.github.io/uAssets/filters/badware.txt',
        'https://ublockorigin.github.io/uAssets/filters/privacy.min.txt',
        'https://ublockorigin.github.io/uAssets/filters/unbreak.min.txt',
        'https://ublockorigin.github.io/uAssets/filters/quick-fixes.txt',
        'https://ublockorigin.github.io/uAssets/filters/ubol-filters.txt',
        'https://ublockorigin.github.io/uAssets/thirdparties/easylist.txt',
        'https://ublockorigin.github.io/uAssets/thirdparties/easyprivacy.txt',
        'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
    ];
    await rulesetFromURLs({
        id: 'default',
        name: 'Ads, trackers, miners, and more' ,
        enabled: true,
        secret,
        urls: contentURLs,
        dnrURL: 'https://ublockorigin.github.io/uAssets/dnr/default.json',
        homeURL: 'https://github.com/uBlockOrigin/uAssets',
    });

    // Regional rulesets
    const excludedLists = [
        'ara-0',
        'EST-0',
    ];
    // Merge lists which have same target languages
    const langToListsMap = new Map();
    for ( const [ id, asset ] of Object.entries(assets) ) {
        if ( asset.content !== 'filters' ) { continue; }
        if ( asset.off !== true ) { continue; }
        if ( typeof asset.lang !== 'string' ) { continue; }
        if ( excludedLists.includes(id) ) { continue; }
        let ids = langToListsMap.get(asset.lang);
        if ( ids === undefined ) {
            langToListsMap.set(asset.lang, ids = []);
        }
        ids.push(id);
    }
    for ( const ids of langToListsMap.values() ) {
        const urls = [];
        for ( const id of ids ) {
            const asset = assets[id];
            const contentURL = Array.isArray(asset.contentURL)
                ? asset.contentURL[0]
                : asset.contentURL;
            urls.push(contentURL);
        }
        const id = ids[0];
        const asset = assets[id];
        await rulesetFromURLs({
            id: id.toLowerCase(),
            lang: asset.lang,
            name: asset.title,
            enabled: false,
            urls,
            homeURL: asset.supportURL,
        });
    }

    // Handpicked rulesets from assets.json
    const handpicked = [
        'block-lan',
        'dpollock-0',
        'adguard-spyware-url',
    ];
    for ( const id of handpicked ) {
        const asset = assets[id];
        if ( asset.content !== 'filters' ) { continue; }

        const contentURL = Array.isArray(asset.contentURL)
            ? asset.contentURL[0]
            : asset.contentURL;
        await rulesetFromURLs({
            id: id.toLowerCase(),
            name: asset.title,
            enabled: false,
            urls: [ contentURL ],
            homeURL: asset.supportURL,
        });
    }

    // Handpicked annoyance rulesets from assets.json
    await rulesetFromURLs({
        id: 'annoyances-cookies',
        name: 'EasyList/uBO – Cookie Notices',
        group: 'annoyances',
        enabled: false,
        secret,
        urls: [
            'https://ublockorigin.github.io/uAssets/thirdparties/easylist-cookies.txt',
            'https://ublockorigin.github.io/uAssets/filters/annoyances-cookies.txt',
        ],
        homeURL: 'https://github.com/easylist/easylist#fanboy-lists',
    });
    await rulesetFromURLs({
        id: 'annoyances-overlays',
        name: 'AdGuard/uBO – Overlays',
        group: 'annoyances',
        enabled: false,
        secret,
        urls: [
            'https://filters.adtidy.org/extension/ublock/filters/19.txt',
            'https://ublockorigin.github.io/uAssets/filters/annoyances-others.txt',
        ],
        homeURL: 'https://github.com/AdguardTeam/AdguardFilters#adguard-filters',
    });
    await rulesetFromURLs({
        id: 'annoyances-social',
        name: 'AdGuard – Social Media',
        group: 'annoyances',
        enabled: false,
        urls: [
            'https://filters.adtidy.org/extension/ublock/filters/4.txt',
        ],
        homeURL: 'https://github.com/AdguardTeam/AdguardFilters#adguard-filters',
    });
    await rulesetFromURLs({
        id: 'annoyances-widgets',
        name: 'AdGuard – Widgets',
        group: 'annoyances',
        enabled: false,
        urls: [
            'https://filters.adtidy.org/extension/ublock/filters/22.txt',
        ],
        homeURL: 'https://github.com/AdguardTeam/AdguardFilters#adguard-filters',
    });
    await rulesetFromURLs({
        id: 'annoyances-others',
        name: 'AdGuard – Other Annoyances',
        group: 'annoyances',
        enabled: false,
        urls: [
            'https://filters.adtidy.org/extension/ublock/filters/21.txt',
        ],
        homeURL: 'https://github.com/AdguardTeam/AdguardFilters#adguard-filters',
    });

    // Handpicked rulesets from abroad
    await rulesetFromURLs({
        id: 'stevenblack-hosts',
        name: 'Steven Black\'s hosts file',
        enabled: false,
        urls: [ 'https://raw.githubusercontent.com/StevenBlack/hosts/master/hosts' ],
        homeURL: 'https://github.com/StevenBlack/hosts#readme',
    });

    writeFile(
        `${rulesetDir}/ruleset-details.json`,
        `${JSON.stringify(rulesetDetails, null, 1)}\n`
    );

    writeFile(
        `${rulesetDir}/scriptlet-details.json`,
        `${JSON.stringify(scriptletStats, jsonSetMapReplacer, 1)}\n`
    );

    writeFile(
        `${rulesetDir}/generic-details.json`,
        `${JSON.stringify(genericDetails, jsonSetMapReplacer, 1)}\n`
    );

    // Copy required redirect resources
    for ( const path of requiredRedirectResources ) {
        copyFile(`./${path}`, `${outputDir}/${path}`);
    }

    await Promise.all(writeOps);

    // Patch manifest
    // Get manifest content
    const manifest = await fs.readFile(
        `${outputDir}/manifest.json`,
        { encoding: 'utf8' }
    ).then(text =>
        JSON.parse(text)
    );
    // Patch declarative_net_request key
    manifest.declarative_net_request = { rule_resources: ruleResources };
    // Patch web_accessible_resources key
    const web_accessible_resources = {
        resources: Array.from(requiredRedirectResources).map(path => `/${path}`),
        matches: [ '<all_urls>' ],
    };
    if ( platform === 'chromium' ) {
        web_accessible_resources.use_dynamic_url = true;
    }
    manifest.web_accessible_resources = [ web_accessible_resources ];

    // Patch manifest version property
    manifest.version = version;
    // Commit changes
    await fs.writeFile(
        `${outputDir}/manifest.json`,
        JSON.stringify(manifest, null, 2) + '\n'
    );

    // Log results
    const logContent = stdOutput.join('\n') + '\n';
    await fs.writeFile(`${cacheDir}/log.txt`, logContent);
}

main();

/******************************************************************************/


一款高效的请求过滤工具：占用极低的内存和 CPU，与其他常见的过滤器相比，它能够多加载并执行上千条过滤规则。

带图表的效率概述说明：
https://github.com/gorhill/uBlock/wiki/uBlock-vs.-ABP:-efficiency-compared

用法：点击弹出窗口中的电源按钮，uBlock 将对当前网站永久禁用/启用过滤功能。 它仅应用于当前网站，而非全局性的开关。

***

它很灵活，不只是一个“广告拦截器”：它还能从 hosts 文件中读取、创建过滤规则。

初始默认加载和执行下列过滤规则：

- EasyList
- Peter Lowe’s Ad server list
- EasyPrivacy
- Malware domains

这里还有更多规则列表可供您选择：

- Fanboy’s Enhanced Tracking List
- Dan Pollock’s hosts file
- hpHosts’s Ad and tracking servers
- MVPS HOSTS
- Spam404
- 等等

当然，启用的过滤规则越多，占用的内存也就越高。 然而，即使再添加 Fanboy 的两个额外规则列表与 hpHosts’s Ad and tracking servers，uBlock 的内存占用依然比其他流行的过滤器更低。

另外，请注意，一些额外的规则列表可能会导致网站出现错误 —— 尤其是那些一般被作为 hosts 文件使用的列表。

***

没有这些过滤规则列表，这个扩展就没有了意义。 所以如果你真的想做点贡献，想想那些维护过滤规则的人们，是他们让所有人能够免费使用这一切变得可能。

***

自由并且免费。
使用 GPLv3 公共许可协议开源。
来自用户，只为用户。

Github 上的贡献者们：
https://github.com/gorhill/uBlock/graphs/contributors
Crowdin 上的翻译者们：
https://crowdin.net/project/ublock

***

它还是一个相当早期的版本，在您评论的时候请记住这一点。

项目更新日志：
https://github.com/gorhill/uBlock/releases