[mv3] Re-work dashboard: move list of rulesets in its own pane

Related issue:
https://github.com/uBlockOrigin/uBOL-home/issues/229

Add "Filter lists" pane in dashboard

The DNR API now supports enabling 50 static rulesets put of a
maximum of 100 (instead of 10 out of 50 originally). Thus given
the potentially growing number of static rulesets, the available
stock rulesets has been moved to its own pane, with the following
improvements:
- Support sublists
- Support search

Aditionally, "RU AdList: Counter" has been added as a stock
ruleset.

Other changes:
- Do not re-evaluate regexes which failed validation
- Better reduce `removeparam` rules
This commit is contained in:
Raymond Hill 2024-11-17 17:27:27 -05:00
parent b4a5b411b5
commit ae4754415c
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
9 changed files with 566 additions and 412 deletions

View File

@ -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;
}

View File

@ -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%;

View File

@ -21,6 +21,7 @@
<div id="dashboard-nav">
<span class="logo"><img data-i18n-title="extName" src="img/ublock.svg" alt="uBO Lite"></span><!--
--><button class="tabButton" type="button" data-pane="settings" data-i18n="settingsPageName" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="rulesets" data-i18n="aboutFilterLists" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="about" data-i18n="aboutPageName" tabindex="0"></button>
</div>
<!-- -------- -->
@ -95,29 +96,14 @@
<p><textarea id="trustedSites" spellcheck="false" placeholder="noFilteringModePlaceholder"></textarea>
</p>
</div>
</section>
<!-- -------- -->
<section data-pane="rulesets">
<div>
<h3 data-i18n="aboutFilterLists"></h3>
<div>
<p id="listsOfBlockedHostsPrompt"></p>
</div>
<div>
<div id="lists"></div>
</div>
</div>
<div id="templates">
<div class="groupEntry">
<div class="geDetails"><span class="geName"></span> <span class="geCount"></span></div>
<div class="listEntries"></div>
</div>
<div class="li listEntry">
<label><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span><span class="listname forinput"></span> <span class="iconbar"><!--
--><a class="fa-icon support" href="#" target="_blank">home</a><!--
--><a class="fa-icon mustread" href="#" target="_blank">info-circle</a><!--
--></span></span></label>
</div>
<p id="listsOfBlockedHostsPrompt"></p>
</div>
<div class="searchfield"><input type="search" spellcheck="false" placeholder="" /><span class="fa-icon">search</span></div>
<div id="lists"></div>
</section>
<!-- -------- -->
<section data-pane="about">
@ -146,6 +132,32 @@
</div>
</section>
<!-- -------- -->
<div id="templates">
<div class="listEntries"></div>
<div class="listEntry" data-role="leaf">
<span class="detailbar">
<label><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span class="listname forinput"></span>
</label>
<span class="iconbar"><!--
--><a class="fa-icon support" href="#" target="_blank">home</a>
</span>
</span>
</div>
<div class="listEntry expandable" data-role="node">
<span class="detailbar">
<label><span class="input checkbox"><input type="checkbox"><svg viewBox="0 0 24 24"><path d="M1.73,12.91 8.1,19.28 22.79,4.59"/></svg></span><span class="listname forinput"></span></label>
<span class="count"></span>
<span class="fa-icon listExpander">angle-up</span>
</span>
</div>
<div class="listEntry expandable" data-role="rootnode">
<span class="detailbar">
<h3 class="listname"></h3>
<span class="count"></span>
<span class="fa-icon listExpander">angle-up</span>
</span>
</div>
</div>
<script src="js/theme.js" type="module"></script>
<script src="js/fa-icons.js" type="module"></script>
<script src="js/i18n.js" type="module"></script>

View File

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

View File

@ -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();
/******************************************************************************/

View File

@ -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) {
}

View File

@ -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`

View File

@ -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

View File

@ -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