diff --git a/platform/mv3/extension/css/dashboard.css b/platform/mv3/extension/css/dashboard.css
index 6506c2272..454a79987 100644
--- a/platform/mv3/extension/css/dashboard.css
+++ b/platform/mv3/extension/css/dashboard.css
@@ -33,6 +33,7 @@
}
body[data-pane="settings"] #dashboard-nav .tabButton[data-pane="settings"],
+body[data-pane="rulesets"] #dashboard-nav .tabButton[data-pane="rulesets"],
body[data-pane="about"] #dashboard-nav .tabButton[data-pane="about"] {
background-color: var(--dashboard-tab-active-surface);
border-bottom: 3px solid var(--dashboard-tab-active-ink);
@@ -44,6 +45,7 @@ body > section {
display: none;
}
body[data-pane="settings"] > section[data-pane="settings"],
+body[data-pane="rulesets"] > section[data-pane="rulesets"],
body[data-pane="about"] > section[data-pane="about"] {
display: block;
}
diff --git a/platform/mv3/extension/css/settings.css b/platform/mv3/extension/css/settings.css
index 3de13560c..da6e13a1a 100644
--- a/platform/mv3/extension/css/settings.css
+++ b/platform/mv3/extension/css/settings.css
@@ -79,92 +79,101 @@ h3[data-i18n="filteringMode0Name"]::first-letter {
}
#lists {
- margin: 0.5em 0 0 0;
- padding: 0;
+ padding-block-end: 8rem;
}
-.groupEntry:not([data-groupkey="user"]) .geDetails::before {
- color: var(--ink-3);
- content: '\2212';
- font-family: monospace;
- font-size: large;
- margin-inline-end: 0.25em;
- -webkit-margin-end: 0.25em;
+.listEntry {
+ display: flex;
+ flex-direction: column;
}
-.groupEntry.hideUnused:not([data-groupkey="user"]) .geDetails::before {
- content: '+';
+.listEntry[data-nodeid] > .detailbar .listExpander {
+ cursor: pointer;
+ top: 2px;
}
-.groupEntry {
- margin: 0.5em 0;
- }
-.groupEntry .geDetails {
+.listEntry[data-role="rootnode"] > .detailbar,
+.listEntry[data-nodeid] > .detailbar .count {
cursor: pointer;
}
-.groupEntry .geName {
+.listEntry[data-role="rootnode"] > .detailbar > *:not(.listExpander) {
pointer-events: none;
}
-.groupEntry .geCount {
+.listEntry .detailbar .count {
+ align-self: flex-end;
color: var(--ink-3);
- font-size: 90%;
+ font-size: small;
pointer-events: none;
}
.listEntries {
- margin-inline-start: 0.6em;
- -webkit-margin-start: 0.6em;
+ display: flex;
+ flex-direction: column;
}
-.groupEntry:not([data-groupkey="user"]) .listEntry:not(.isDefault).unused {
+.listEntry:not([data-role="rootnode"]) > .listEntries {
+ margin-inline-start: var(--checkbox-size);
+ }
+.listEntry.hideUnused > .listEntries > .listEntry:not(.isDefault):has(> .detailbar input:not(:checked)) {
display: none;
}
-
.listEntry.fromAdmin:has(input[disabled]:not(:checked)) {
display: none;
}
.listEntry > * {
- margin-left: 0;
- margin-right: 0;
unicode-bidi: embed;
}
+.listEntry h3 {
+ display: inline-block;
+ margin: 0;
+ }
+.listEntry > .detailbar {
+ align-items: center;
+ display: inline-flex;
+ margin: calc(var(--default-gap-xsmall) / 2 + var(--default-gap-xxsmall) / 2) 0;
+ white-space: nowrap;
+ }
+.listEntry > .detailbar > *:not(:first-child) {
+ margin-inline-start: var(--default-gap-xxsmall);
+ }
+.listEntry[data-nodeid="default"] > .detailbar > .listExpander {
+ display: none;
+ }
+.listEntry > .detailbar > .listExpander svg {
+ transform: rotate(180deg);
+ transform-origin: 50%;
+ }
+.listEntry.hideUnused > .detailbar > .listExpander svg {
+ transform: rotate(90deg);
+ }
.listEntry .checkbox:has(input[disabled]),
.listEntry .checkbox:has(input[disabled]) ~ span {
filter: var(--checkbox-disabled-filter);
}
-.listEntry .listname {
- white-space: nowrap;
- }
.listEntry a,
.listEntry .fa-icon {
color: var(--info0-ink);
fill: var(--info0-ink);
- display: none;
font-size: 120%;
- margin: 0 0.2em 0 0;
}
.listEntry .fa-icon:hover {
transform: scale(1.25);
}
-.listEntry .content {
- display: inline-flex;
- }
-.listEntry a.towiki {
- display: inline-flex;
- }
-.listEntry.support a.support {
- display: inline-flex;
- }
-.listEntry.mustread a.mustread {
- color: var(--info1-ink);
- fill: var(--info1-ink);
- display: inline-flex;
- }
-.listEntry .status {
- cursor: default;
+.listEntry .iconbar a.support[href="#"] {
display: none;
-}
+ }
-body.noMoreRuleset .listEntry:not(.checked) {
+body.noMoreRuleset .listEntry:has(> .detailbar input:not(:checked)) {
opacity: 0.5;
pointer-events: none;
}
+#lists.searchMode > .listEntries .listEntries,
+#lists.searchMode > .listEntries .listEntry.searchMatch {
+ display: flex !important;
+ }
+#lists.searchMode > .listEntries .listEntry {
+ display: none;
+ }
+#lists.searchMode > .listEntries .listExpander {
+ visibility: hidden;
+ }
+
/* touch-screen devices */
:root.mobile .listEntry .fa-icon {
font-size: 120%;
diff --git a/platform/mv3/extension/dashboard.html b/platform/mv3/extension/dashboard.html
index 1cf758205..73dd72ac0 100644
--- a/platform/mv3/extension/dashboard.html
+++ b/platform/mv3/extension/dashboard.html
@@ -21,6 +21,7 @@
@@ -95,29 +96,14 @@
-
+
+
+
+
+
+
+
+
+
+
+
+ angle-up
+
+
+
diff --git a/platform/mv3/extension/js/filter-lists.js b/platform/mv3/extension/js/filter-lists.js
new file mode 100644
index 000000000..a52dcb433
--- /dev/null
+++ b/platform/mv3/extension/js/filter-lists.js
@@ -0,0 +1,402 @@
+/*******************************************************************************
+
+ uBlock Origin Lite - a comprehensive, MV3-compliant 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
+*/
+
+import { dom, qs$, qsa$ } from './dom.js';
+import { i18n, i18n$ } from './i18n.js';
+import { localRead, localWrite, sendMessage } from './ext.js';
+
+/******************************************************************************/
+
+export const rulesetMap = new Map();
+
+let cachedRulesetData = {};
+let hideUnusedSet = new Set([ 'regions' ]);
+
+/******************************************************************************/
+
+function renderNumber(value) {
+ return value.toLocaleString();
+}
+
+function renderRuleCounts() {
+ let rulesetCount = 0;
+ let filterCount = 0;
+ let ruleCount = 0;
+ for ( const liEntry of qsa$('#lists .listEntry[data-role="leaf"][data-rulesetid]') ) {
+ if ( qs$(liEntry, 'input[type="checkbox"]:checked') === null ) { continue; }
+ rulesetCount += 1;
+ const stats = rulesetStats(liEntry.dataset.rulesetid);
+ if ( stats === undefined ) { continue; }
+ ruleCount += stats.ruleCount;
+ filterCount += stats.filterCount;
+ }
+ dom.text('#listsOfBlockedHostsPrompt', i18n$('perRulesetStats')
+ .replace('{{ruleCount}}', ruleCount.toLocaleString())
+ .replace('{{filterCount}}', filterCount.toLocaleString())
+ );
+
+ dom.cl.toggle(dom.body, 'noMoreRuleset',
+ rulesetCount === cachedRulesetData.maxNumberOfEnabledRulesets
+ );
+}
+
+/******************************************************************************/
+
+function updateNodes(listEntries) {
+ listEntries = listEntries || qs$('#lists');
+ for ( const listEntry of qsa$(listEntries, '.listEntry[data-nodeid]') ) {
+ const totalCount = qsa$(listEntry, '.listEntry[data-rulesetid] input').length;
+ const checkedCount = qsa$(listEntry, '.listEntry[data-rulesetid] input:checked').length;
+ dom.text(qs$(listEntry, '.detailbar .count'), `${checkedCount}/${totalCount}`);
+ const checkbox = qs$(listEntry, ':scope > .detailbar .checkbox');
+ if ( checkbox === null ) { continue; }
+ dom.prop(qs$(checkbox, 'input'), 'checked', checkedCount !== 0);
+ dom.cl.toggle(checkbox, 'partial',
+ checkedCount !== 0 && checkedCount !== totalCount
+ );
+ }
+}
+
+/******************************************************************************/
+
+function rulesetStats(rulesetId) {
+ const hasOmnipotence = cachedRulesetData.defaultFilteringMode > 1;
+ const rulesetDetails = rulesetMap.get(rulesetId);
+ if ( rulesetDetails === undefined ) { return; }
+ const { rules, filters } = rulesetDetails;
+ let ruleCount = rules.plain + rules.regex;
+ if ( hasOmnipotence ) {
+ ruleCount += rules.removeparam + rules.redirect + rules.modifyHeaders;
+ }
+ const filterCount = filters.accepted;
+ return { ruleCount, filterCount };
+}
+
+/******************************************************************************/
+
+function isAdminRuleset(listkey) {
+ const { adminRulesets = [] } = cachedRulesetData;
+ for ( const id of adminRulesets ) {
+ const pos = id.indexOf(listkey);
+ if ( pos === 0 ) { return true; }
+ if ( pos !== 1 ) { continue; }
+ const c = id.charAt(0);
+ if ( c === '+' || c === '-' ) { return true; }
+ }
+ return false;
+}
+
+/******************************************************************************/
+
+export function renderFilterLists(rulesetData) {
+ cachedRulesetData = rulesetData;
+ const { enabledRulesets, rulesetDetails } = cachedRulesetData;
+
+ const shouldUpdate = rulesetMap.size !== 0;
+
+ rulesetDetails.forEach(rule => rulesetMap.set(rule.id, rule));
+
+ const listStatsTemplate = i18n$('perRulesetStats');
+
+ const initializeListEntry = (ruleset, listEntry) => {
+ const on = enabledRulesets.includes(ruleset.id);
+ dom.prop(qs$(listEntry, ':scope > .detailbar input'), 'checked', on);
+ if ( ruleset.homeURL ) {
+ dom.attr(qs$(listEntry, 'a.support'), 'href', ruleset.homeURL);
+ }
+ dom.cl.toggle(listEntry, 'isDefault', ruleset.id === 'default');
+ const stats = rulesetStats(ruleset.id);
+ listEntry.title = listStatsTemplate
+ .replace('{{ruleCount}}', renderNumber(stats.ruleCount))
+ .replace('{{filterCount}}', renderNumber(stats.filterCount));
+ const fromAdmin = isAdminRuleset(ruleset.id);
+ dom.cl.toggle(listEntry, 'fromAdmin', fromAdmin);
+ const disabled = stats.ruleCount === 0 || fromAdmin;
+ dom.attr(
+ qs$(listEntry, '.input.checkbox input'),
+ 'disabled',
+ disabled ? '' : null
+ );
+ return listEntry;
+ };
+
+ // Update already rendered DOM lists
+ if ( shouldUpdate ) {
+ for ( const listEntry of qsa$('#lists .listEntry[data-rulesetid]') ) {
+ const rulesetid = listEntry.dataset.rulesetid;
+ const ruleset = rulesetMap.get(rulesetid);
+ initializeListEntry(ruleset, listEntry);
+ }
+ updateNodes();
+ renderRuleCounts();
+ return;
+ }
+
+ const createListEntry = (listDetails, depth) => {
+ if ( listDetails.lists === undefined ) {
+ return dom.clone('#templates .listEntry[data-role="leaf"]');
+ }
+ if ( depth !== 0 ) {
+ return dom.clone('#templates .listEntry[data-role="node"]');
+ }
+ return dom.clone('#templates .listEntry[data-role="rootnode"]');
+ };
+
+ const createListEntries = (parentkey, listTree, depth = 0) => {
+ const listEntries = dom.clone('#templates .listEntries');
+ const treeEntries = Object.entries(listTree);
+ if ( depth !== 0 ) {
+ const reEmojis = /\p{Emoji}+/gu;
+ treeEntries.sort((a ,b) => {
+ const ap = a[1].preferred === true;
+ const bp = b[1].preferred === true;
+ if ( ap !== bp ) { return ap ? -1 : 1; }
+ const as = (a[1].title || a[0]).replace(reEmojis, '');
+ const bs = (b[1].title || b[0]).replace(reEmojis, '');
+ return as.localeCompare(bs);
+ });
+ }
+ for ( const [ listkey, listDetails ] of treeEntries ) {
+ const listEntry = createListEntry(listDetails, depth);
+ if ( listDetails.lists === undefined ) {
+ listEntry.dataset.rulesetid = listkey;
+ } else {
+ listEntry.dataset.nodeid = listkey;
+ dom.cl.toggle(listEntry, 'hideUnused', hideUnusedSet.has(listkey));
+ }
+ qs$(listEntry, ':scope > .detailbar .listname').append(
+ i18n.patchUnicodeFlags(listDetails.name)
+ );
+ if ( listDetails.lists !== undefined ) {
+ listEntry.append(createListEntries(listkey, listDetails.lists, depth+1));
+ dom.cl.toggle(listEntry, 'expanded', true/*listIsExpanded(listkey)*/);
+ //updateListNode(listEntry);
+ } else {
+ initializeListEntry(listDetails, listEntry);
+ }
+ listEntries.append(listEntry);
+ }
+ return listEntries;
+ };
+
+ // Visually split the filter lists in groups
+ const groups = new Map([
+ [
+ 'default',
+ rulesetDetails.filter(ruleset =>
+ ruleset.id === 'default'
+ ),
+ ], [
+ 'annoyances',
+ rulesetDetails.filter(ruleset =>
+ ruleset.group === 'annoyances'
+ ),
+ ], [
+ 'misc',
+ rulesetDetails.filter(ruleset =>
+ ruleset.id !== 'default' &&
+ ruleset.group === undefined &&
+ typeof ruleset.lang !== 'string'
+ ),
+ ], [
+ 'regions',
+ rulesetDetails.filter(ruleset =>
+ ruleset.group === 'regions'
+ ),
+ ],
+ ]);
+
+ dom.cl.toggle(dom.body, 'hideUnused', mustHideUnusedLists('*'));
+
+ // Build list tree
+ const listTree = {};
+ const groupNames = new Map();
+ for ( const [ groupKey, groupRulesets ] of groups ) {
+ let groupName = groupNames.get(groupKey);
+ if ( groupName === undefined ) {
+ groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
+ groupNames.set(groupKey, groupName);
+ }
+ const groupDetails = {
+ name: groupName,
+ lists: {},
+ };
+ listTree[groupKey] = groupDetails;
+ for ( const ruleset of groupRulesets ) {
+ if ( ruleset.parent !== undefined ) {
+ let lists = groupDetails.lists;
+ for ( const parent of ruleset.parent.split('|') ) {
+ if ( lists[parent] === undefined ) {
+ lists[parent] = { name: parent, lists: {} };
+ }
+ lists = lists[parent].lists;
+ }
+ lists[ruleset.id] = ruleset;
+ } else {
+ groupDetails.lists[ruleset.id] = ruleset;
+ }
+ }
+ }
+ // Move lonely sublist to list level
+ const promoteLonelySublist = (parent, depth = 0) => {
+ if ( Boolean(parent.lists) === false ) { return parent; }
+ const childKeys = Object.keys(parent.lists);
+ for ( const childKey of childKeys ) {
+ const child = promoteLonelySublist(parent.lists[childKey], depth + 1);
+ if ( child === parent.lists[childKey] ) { continue; }
+ parent.lists[child.id] = child;
+ delete parent.lists[childKey];
+ }
+ if ( depth === 0 ) { return parent; }
+ if ( childKeys.length > 1 ) { return parent; }
+ return parent.lists[childKeys[0]]
+ };
+ for ( const key of Object.keys(listTree) ) {
+ promoteLonelySublist(listTree[key]);
+ }
+ const listEntries = createListEntries('root', listTree);
+
+ updateNodes(listEntries);
+
+ dom.clear('#lists');
+ qs$('#lists').append(listEntries);
+
+ renderRuleCounts();
+}
+
+/******************************************************************************/
+
+// Collapsing of unused lists.
+
+function mustHideUnusedLists(which) {
+ const hideAll = hideUnusedSet.has('*');
+ if ( which === '*' ) { return hideAll; }
+ return hideUnusedSet.has(which) !== hideAll;
+}
+
+function toggleHideUnusedLists(which) {
+ const doesHideAll = hideUnusedSet.has('*');
+ if ( which === '*' ) {
+ const mustHide = doesHideAll === false;
+ hideUnusedSet.clear();
+ if ( mustHide ) {
+ hideUnusedSet.add(which);
+ }
+ dom.cl.toggle('#lists', 'hideUnused', mustHide);
+ dom.cl.toggle('.listEntry[data-nodeid]', 'hideUnused', mustHide);
+ } else {
+ const doesHide = hideUnusedSet.has(which);
+ if ( doesHide ) {
+ hideUnusedSet.delete(which);
+ } else {
+ hideUnusedSet.add(which);
+ }
+ const mustHide = doesHide === doesHideAll;
+ const groupSelector = `.listEntry[data-nodeid="${which}"]`;
+ dom.cl.toggle(groupSelector, 'hideUnused', mustHide);
+ }
+
+ localWrite('hideUnusedFilterLists', Array.from(hideUnusedSet));
+}
+
+dom.on('#lists', 'click', '.listEntry[data-nodeid] > .detailbar, .listExpander', ev => {
+ toggleHideUnusedLists(
+ dom.attr(ev.target.closest('[data-nodeid]'), 'data-nodeid')
+ );
+});
+
+// Initialize from saved state.
+localRead('hideUnusedFilterLists').then(value => {
+ if ( Array.isArray(value) === false ) { return; }
+ hideUnusedSet = new Set(value);
+ for ( const listEntry of qsa$('[data-nodeid]') ) {
+ dom.cl.toggle(listEntry, 'hideUnused',
+ hideUnusedSet.has(listEntry.dataset.nodeid)
+ );
+ }
+});
+
+/******************************************************************************/
+
+const searchFilterLists = ( ) => {
+ const pattern = dom.prop('.searchfield input', 'value') || '';
+ dom.cl.toggle('#lists', 'searchMode', pattern !== '');
+ if ( pattern === '' ) { return; }
+ const re = new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'i');
+ for ( const listEntry of qsa$('#lists [data-role="leaf"]') ) {
+ const rulesetid = listEntry.dataset.rulesetid;
+ const rulesetDetails = rulesetMap.get(rulesetid);
+ if ( rulesetDetails === undefined ) { continue; }
+ let haystack = perListHaystack.get(rulesetDetails);
+ if ( haystack === undefined ) {
+ haystack = [
+ rulesetDetails.name,
+ listEntry.dataset.nodeid,
+ rulesetDetails.tags || '',
+ ].join(' ').trim();
+ perListHaystack.set(rulesetDetails, haystack);
+ }
+ dom.cl.toggle(listEntry, 'searchMatch', re.test(haystack));
+ }
+ for ( const listEntry of qsa$('#lists .listEntry:not([data-role="leaf"])') ) {
+ dom.cl.toggle(listEntry, 'searchMatch',
+ qs$(listEntry, '.listEntries .listEntry.searchMatch') !== null
+ );
+ }
+};
+
+const perListHaystack = new WeakMap();
+
+dom.on('.searchfield input', 'input', searchFilterLists);
+
+/******************************************************************************/
+
+async function applyEnabledRulesets() {
+ const enabledRulesets = [];
+ for ( const liEntry of qsa$('#lists .listEntry[data-role="leaf"][data-rulesetid]') ) {
+ const checked = qs$(liEntry, 'input[type="checkbox"]:checked') !== null;
+ if ( checked === false ) { continue; }
+ const { rulesetid } = liEntry.dataset;
+ if ( dom.cl.has(liEntry, 'fromAdmin') ) { continue; }
+ enabledRulesets.push(rulesetid);
+ }
+
+ await sendMessage({
+ what: 'applyRulesets',
+ enabledRulesets,
+ });
+}
+
+dom.on('#lists', 'change', '.listEntry input[type="checkbox"]', ev => {
+ const input = ev.target;
+ const listEntry = input.closest('.listEntry');
+ if ( listEntry === null ) { return; }
+ if ( listEntry.dataset.nodeid !== undefined ) {
+ let checkAll = input.checked ||
+ dom.cl.has(qs$(listEntry, ':scope > .detailbar .checkbox'), 'partial');
+ for ( const input of qsa$(listEntry, '.listEntries input') ) {
+ input.checked = checkAll;
+ }
+ }
+ renderRuleCounts();
+ updateNodes();
+ applyEnabledRulesets();
+});
diff --git a/platform/mv3/extension/js/ruleset-manager.js b/platform/mv3/extension/js/ruleset-manager.js
index d19a9f28e..f7d811e7d 100644
--- a/platform/mv3/extension/js/ruleset-manager.js
+++ b/platform/mv3/extension/js/ruleset-manager.js
@@ -101,9 +101,14 @@ async function pruneInvalidRegexRules(realm, rulesIn) {
toCheck.push(true);
continue;
}
+ if ( pruneInvalidRegexRules.invalidRegexes.has(regex) ) {
+ toCheck.push(false);
+ continue;
+ }
toCheck.push(
dnr.isRegexSupported({ regex, isCaseSensitive }).then(result => {
if ( result.isSupported ) { return true; }
+ pruneInvalidRegexRules.invalidRegexes.add(regex);
rejectedRegexRules.push(`\t${regex} ${result.reason}`);
return false;
})
@@ -122,6 +127,7 @@ async function pruneInvalidRegexRules(realm, rulesIn) {
return rulesIn.filter((v, i) => isValid[i]);
}
+pruneInvalidRegexRules.invalidRegexes = new Set();
/******************************************************************************/
diff --git a/platform/mv3/extension/js/settings.js b/platform/mv3/extension/js/settings.js
index a970430f3..81f55d03c 100644
--- a/platform/mv3/extension/js/settings.js
+++ b/platform/mv3/extension/js/settings.js
@@ -19,201 +19,21 @@
Home: https://github.com/gorhill/uBlock
*/
-import { browser, localRead, localWrite, sendMessage } from './ext.js';
-import { dom, qs$, qsa$ } from './dom.js';
-import { i18n, i18n$ } from './i18n.js';
+import { browser, sendMessage } from './ext.js';
+import { dom, qs$ } from './dom.js';
import punycode from './punycode.js';
+import { renderFilterLists } from './filter-lists.js';
/******************************************************************************/
-const rulesetMap = new Map();
let cachedRulesetData = {};
-let hideUnusedSet = new Set([ 'regions' ]);
/******************************************************************************/
-function renderNumber(value) {
- return value.toLocaleString();
-}
-
function hashFromIterable(iter) {
return Array.from(iter).sort().join('\n');
}
-function isAdminRuleset(listkey) {
- const { adminRulesets = [] } = cachedRulesetData;
- for ( const id of adminRulesets ) {
- const pos = id.indexOf(listkey);
- if ( pos === 0 ) { return true; }
- if ( pos !== 1 ) { continue; }
- const c = id.charAt(0);
- if ( c === '+' || c === '-' ) { return true; }
- }
- return false;
-}
-
-/******************************************************************************/
-
-function rulesetStats(rulesetId) {
- const hasOmnipotence = cachedRulesetData.defaultFilteringMode > 1;
- const rulesetDetails = rulesetMap.get(rulesetId);
- if ( rulesetDetails === undefined ) { return; }
- const { rules, filters } = rulesetDetails;
- let ruleCount = rules.plain + rules.regex;
- if ( hasOmnipotence ) {
- ruleCount += rules.removeparam + rules.redirect + rules.modifyHeaders;
- }
- const filterCount = filters.accepted;
- return { ruleCount, filterCount };
-}
-
-/******************************************************************************/
-
-function renderFilterLists() {
- const { enabledRulesets, rulesetDetails } = cachedRulesetData;
- const listGroupTemplate = qs$('#templates .groupEntry');
- const listEntryTemplate = qs$('#templates .listEntry');
- const listStatsTemplate = i18n$('perRulesetStats');
- const groupNames = new Map([ [ 'user', '' ] ]);
-
- const liFromListEntry = function(ruleset, li, hideUnused) {
- if ( !li ) {
- li = dom.clone(listEntryTemplate);
- }
- const on = enabledRulesets.includes(ruleset.id);
- dom.cl.toggle(li, 'checked', on);
- dom.cl.toggle(li, 'unused', hideUnused && !on);
- qs$(li, 'input[type="checkbox"]').checked = on;
- if ( dom.attr(li, 'data-listkey') !== ruleset.id ) {
- dom.attr(li, 'data-listkey', ruleset.id);
- qs$(li, '.listname').append(i18n.patchUnicodeFlags(ruleset.name));
- dom.cl.remove(li, 'toRemove');
- if ( ruleset.homeURL ) {
- dom.cl.add(li, 'support');
- dom.attr(qs$(li, 'a.support'), 'href', ruleset.homeURL);
- } else {
- dom.cl.remove(li, 'support');
- }
- if ( ruleset.instructionURL ) {
- dom.cl.add(li, 'mustread');
- dom.attr(qs$(li, 'a.mustread'), 'href', ruleset.instructionURL);
- } else {
- dom.cl.remove(li, 'mustread');
- }
- dom.cl.toggle(li, 'isDefault', ruleset.id === 'default');
- }
- const stats = rulesetStats(ruleset.id);
- li.title = listStatsTemplate
- .replace('{{ruleCount}}', renderNumber(stats.ruleCount))
- .replace('{{filterCount}}', renderNumber(stats.filterCount));
- const fromAdmin = isAdminRuleset(ruleset.id);
- dom.cl.toggle(li, 'fromAdmin', fromAdmin);
- const disabled = stats.ruleCount === 0 || fromAdmin;
- dom.attr(
- qs$(li, '.input.checkbox input'),
- 'disabled',
- disabled ? '' : null
- );
- dom.cl.remove(li, 'discard');
- return li;
- };
-
- const listEntryCountFromGroup = function(groupRulesets) {
- if ( Array.isArray(groupRulesets) === false ) { return ''; }
- let count = 0,
- total = 0;
- for ( const ruleset of groupRulesets ) {
- if ( enabledRulesets.includes(ruleset.id) ) {
- count += 1;
- }
- total += 1;
- }
- return total !== 0 ?
- `(${count.toLocaleString()}/${total.toLocaleString()})` :
- '';
- };
-
- const liFromListGroup = function(groupKey, groupRulesets) {
- let liGroup = qs$(`#lists > .groupEntry[data-groupkey="${groupKey}"]`);
- if ( liGroup === null ) {
- liGroup = dom.clone(listGroupTemplate);
- let groupName = groupNames.get(groupKey);
- if ( groupName === undefined ) {
- groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
- groupNames.set(groupKey, groupName);
- }
- if ( groupName !== '' ) {
- dom.text(qs$(liGroup, '.geName'), groupName);
- }
- }
- if ( qs$(liGroup, '.geName:empty') === null ) {
- dom.text(
- qs$(liGroup, '.geCount'),
- listEntryCountFromGroup(groupRulesets)
- );
- }
- const hideUnused = mustHideUnusedLists(groupKey);
- dom.cl.toggle(liGroup, 'hideUnused', hideUnused);
- const ulGroup = qs$(liGroup, '.listEntries');
- if ( !groupRulesets ) { return liGroup; }
- groupRulesets.sort(function(a, b) {
- return (a.name || '').localeCompare(b.name || '');
- });
- for ( let i = 0; i < groupRulesets.length; i++ ) {
- const liEntry = liFromListEntry(
- groupRulesets[i],
- ulGroup.children[i],
- hideUnused
- );
- if ( liEntry.parentElement === null ) {
- ulGroup.appendChild(liEntry);
- }
- }
- return liGroup;
- };
-
- // Visually split the filter lists in groups
- const ulLists = qs$('#lists');
- const groups = new Map([
- [
- 'default',
- rulesetDetails.filter(ruleset =>
- ruleset.id === 'default'
- ),
- ],
- [
- 'annoyances',
- rulesetDetails.filter(ruleset =>
- ruleset.group === 'annoyances'
- ),
- ],
- [
- 'misc',
- rulesetDetails.filter(ruleset =>
- ruleset.id !== 'default' &&
- ruleset.group === undefined &&
- typeof ruleset.lang !== 'string'
- ),
- ],
- [
- 'regions',
- rulesetDetails.filter(ruleset =>
- typeof ruleset.lang === 'string'
- ),
- ],
- ]);
-
- dom.cl.toggle(dom.body, 'hideUnused', mustHideUnusedLists('*'));
-
- for ( const [ groupKey, groupRulesets ] of groups ) {
- const liGroup = liFromListGroup(groupKey, groupRulesets);
- dom.attr(liGroup, 'data-groupkey', groupKey);
- if ( liGroup.parentElement === null ) {
- ulLists.appendChild(liGroup);
- }
- }
-}
-
/******************************************************************************/
function renderWidgets() {
@@ -235,27 +55,6 @@ function renderWidgets() {
dom.attr(input, 'disabled', '');
}
}
-
- // Compute total counts
- let rulesetCount = 0;
- let filterCount = 0;
- let ruleCount = 0;
- for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
- if ( qs$(liEntry, 'input[type="checkbox"]:checked') === null ) { continue; }
- rulesetCount += 1;
- const stats = rulesetStats(liEntry.dataset.listkey);
- if ( stats === undefined ) { continue; }
- ruleCount += stats.ruleCount;
- filterCount += stats.filterCount;
- }
- dom.text('#listsOfBlockedHostsPrompt', i18n$('perRulesetStats')
- .replace('{{ruleCount}}', ruleCount.toLocaleString())
- .replace('{{filterCount}}', filterCount.toLocaleString())
- );
-
- dom.cl.toggle(dom.body, 'noMoreRuleset',
- rulesetCount === cachedRulesetData.maxNumberOfEnabledRulesets
- );
}
/******************************************************************************/
@@ -300,7 +99,7 @@ async function onFilteringModeChange(ev) {
default:
break;
}
- renderFilterLists();
+ renderFilterLists(cachedRulesetData);
renderWidgets();
}
@@ -367,89 +166,6 @@ self.addEventListener('beforeunload', changeTrustedSites);
/******************************************************************************/
-async function applyEnabledRulesets() {
- const enabledRulesets = [];
- for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
- const checked = qs$(liEntry, 'input[type="checkbox"]:checked') !== null;
- dom.cl.toggle(liEntry, 'checked', checked);
- if ( checked === false ) { continue; }
- const { listkey } = liEntry.dataset;
- if ( isAdminRuleset(listkey) ) { continue; }
- enabledRulesets.push(listkey);
- }
-
- await sendMessage({
- what: 'applyRulesets',
- enabledRulesets,
- });
-
- renderWidgets();
-}
-
-dom.on('#lists', 'change', '.listEntry input[type="checkbox"]', ( ) => {
- applyEnabledRulesets();
-});
-
-/******************************************************************************/
-
-// Collapsing of unused lists.
-
-function mustHideUnusedLists(which) {
- const hideAll = hideUnusedSet.has('*');
- if ( which === '*' ) { return hideAll; }
- return hideUnusedSet.has(which) !== hideAll;
-}
-
-function toggleHideUnusedLists(which) {
- const doesHideAll = hideUnusedSet.has('*');
- let groupSelector;
- let mustHide;
- if ( which === '*' ) {
- mustHide = doesHideAll === false;
- groupSelector = '';
- hideUnusedSet.clear();
- if ( mustHide ) {
- hideUnusedSet.add(which);
- }
- dom.cl.toggle(dom.body, 'hideUnused', mustHide);
- dom.cl.toggle('.groupEntry[data-groupkey]', 'hideUnused', mustHide);
- } else {
- const doesHide = hideUnusedSet.has(which);
- if ( doesHide ) {
- hideUnusedSet.delete(which);
- } else {
- hideUnusedSet.add(which);
- }
- mustHide = doesHide === doesHideAll;
- groupSelector = `.groupEntry[data-groupkey="${which}"]`;
- dom.cl.toggle(groupSelector, 'hideUnused', mustHide);
- }
-
- for ( const elem of qsa$(`#lists ${groupSelector} .listEntry[data-listkey] input[type="checkbox"]:not(:checked)`) ) {
- dom.cl.toggle(
- elem.closest('.listEntry[data-listkey]'),
- 'unused',
- mustHide
- );
- }
-
- localWrite('hideUnusedFilterLists', Array.from(hideUnusedSet));
-}
-
-dom.on('#lists', 'click', '.groupEntry[data-groupkey] > .geDetails', ev => {
- toggleHideUnusedLists(
- dom.attr(ev.target.closest('[data-groupkey]'), 'data-groupkey')
- );
-});
-
-// Initialize from saved state.
-localRead('hideUnusedFilterLists').then(value => {
- if ( Array.isArray(value) === false ) { return; }
- hideUnusedSet = new Set(value);
-});
-
-/******************************************************************************/
-
function listen() {
const bc = new self.BroadcastChannel('uBOL');
bc.onmessage = listen.onmessage;
@@ -512,7 +228,7 @@ listen.onmessage = ev => {
}
if ( render === false ) { return; }
- renderFilterLists();
+ renderFilterLists(cachedRulesetData);
renderWidgets();
};
@@ -523,10 +239,8 @@ sendMessage({
}).then(data => {
if ( !data ) { return; }
cachedRulesetData = data;
- rulesetMap.clear();
- cachedRulesetData.rulesetDetails.forEach(rule => rulesetMap.set(rule.id, rule));
try {
- renderFilterLists();
+ renderFilterLists(cachedRulesetData);
renderWidgets();
} catch(ex) {
}
diff --git a/platform/mv3/make-rulesets.js b/platform/mv3/make-rulesets.js
index 097191010..501af6f70 100644
--- a/platform/mv3/make-rulesets.js
+++ b/platform/mv3/make-rulesets.js
@@ -1068,8 +1068,10 @@ async function rulesetFromURLs(assetDetails) {
id: assetDetails.id,
name: assetDetails.name,
group: assetDetails.group,
+ parent: assetDetails.parent,
enabled: assetDetails.enabled,
lang: assetDetails.lang,
+ tags: assetDetails.tags,
homeURL: assetDetails.homeURL,
filters: {
total: results.network.filterCount,
@@ -1121,7 +1123,7 @@ async function main() {
// Get assets.json content
const assets = await fs.readFile(
- `./assets.json`,
+ `./assets.dev.json`,
{ encoding: 'utf8' }
).then(text =>
JSON.parse(text)
@@ -1155,55 +1157,6 @@ async function main() {
],
});
- // 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,
- });
- }
-
- await rulesetFromURLs({
- id: 'est-0',
- group: 'regions',
- lang: 'et',
- name: '🇪🇪ee: Eesti saitidele kohandatud filter',
- enabled: false,
- urls: [ 'https://ubol-et.adblock.ee/list.txt' ],
- homeURL: 'https://github.com/sander85/uBOL-et',
- });
-
// Handpicked rulesets from assets.json
const handpicked = [
'block-lan',
@@ -1290,6 +1243,62 @@ async function main() {
homeURL: 'https://github.com/StevenBlack/hosts#readme',
});
+ // 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 ( asset.group !== 'regions' ) { continue; }
+ if ( excludedLists.includes(id) ) { continue; }
+ // Not all "regions" lists have a set language
+ const bundleId = asset.lang ||
+ createHash('sha256').update(randomBytes(16)).digest('hex').slice(0,16);
+ let ids = langToListsMap.get(bundleId);
+ if ( ids === undefined ) {
+ langToListsMap.set(bundleId, 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];
+ const rulesetDetails = {
+ id: id.toLowerCase(),
+ group: 'regions',
+ parent: asset.parent,
+ lang: asset.lang,
+ name: asset.title,
+ tags: asset.tags,
+ enabled: false,
+ urls,
+ homeURL: asset.supportURL,
+ };
+ await rulesetFromURLs(rulesetDetails);
+ }
+
+ await rulesetFromURLs({
+ id: 'est-0',
+ group: 'regions',
+ lang: 'et',
+ name: '🇪🇪ee: Eesti saitidele kohandatud filter',
+ enabled: false,
+ urls: [ 'https://ubol-et.adblock.ee/list.txt' ],
+ homeURL: 'https://github.com/sander85/uBOL-et',
+ });
+
writeFile(
`${rulesetDir}/ruleset-details.json`,
`${JSON.stringify(rulesetDetails, null, 1)}\n`
diff --git a/src/js/static-dnr-filtering.js b/src/js/static-dnr-filtering.js
index 2e87695bf..d3e7732a6 100644
--- a/src/js/static-dnr-filtering.js
+++ b/src/js/static-dnr-filtering.js
@@ -440,9 +440,9 @@ function finalizeRuleset(context, network) {
}
};
mergeRules(rulesetMap, 'resourceTypes');
+ mergeRules(rulesetMap, 'removeParams');
mergeRules(rulesetMap, 'initiatorDomains');
mergeRules(rulesetMap, 'requestDomains');
- mergeRules(rulesetMap, 'removeParams');
mergeRules(rulesetMap, 'responseHeaders');
// Patch id
diff --git a/tools/make-mv3.sh b/tools/make-mv3.sh
index 3bb3c9ce4..58a050531 100755
--- a/tools/make-mv3.sh
+++ b/tools/make-mv3.sh
@@ -108,7 +108,7 @@ if [ "$QUICK" != "yes" ]; then
cp platform/mv3/*.js "$TMPDIR"/
cp platform/mv3/*.mjs "$TMPDIR"/
cp platform/mv3/extension/js/utils.js "$TMPDIR"/js/
- cp "$UBO_DIR"/assets/assets.json "$TMPDIR"/
+ cp "$UBO_DIR"/assets/assets.dev.json "$TMPDIR"/
cp "$UBO_DIR"/assets/resources/*.js "$TMPDIR"/
cp -R platform/mv3/scriptlets "$TMPDIR"/
mkdir -p "$TMPDIR"/web_accessible_resources