1
0
mirror of https://github.com/gorhill/uBlock.git synced 2024-07-05 11:37:01 +02:00

[mv3] Add ability to enable/disable filter lists

This commit is contained in:
Raymond Hill 2022-09-13 17:44:24 -04:00
parent d11a3f2fa3
commit e31637af78
No known key found for this signature in database
GPG Key ID: 25E1490B761470C2
63 changed files with 2344 additions and 742 deletions

View File

@ -4,7 +4,6 @@
"eqeqeq": true,
"esversion": 8,
"globals": {
"browser": false, // global variable in Firefox, Edge
"chrome": false, // global variable in Chromium, Chrome, Opera
"self": false,
"vAPI": false,

View File

@ -1,11 +1,11 @@
# https://stackoverflow.com/a/6273809
run_options := $(filter-out $@,$(MAKECMDGOALS))
.PHONY: all clean test lint chromium firefox npm dig mv3 \
.PHONY: all clean test lint chromium firefox npm dig mv3 mv3-quick \
compare maxcost medcost mincost modifiers record wasm
sources := $(wildcard assets/resources/* dist/version src/* src/*/* src/*/*/* src/*/*/*/*)
platform := $(wildcard platform/* platform/*/* platform/*/*/*)
platform := $(wildcard platform/* platform/*/* platform/*/*/* platform/*/*/*/*)
assets := $(wildcard submodules/uAssets/* \
submodules/uAssets/*/* \
submodules/uAssets/*/*/* \
@ -57,7 +57,7 @@ mv3: tools/make-mv3.sh $(sources) $(platform)
mv3-quick: tools/make-mv3.sh $(sources) $(platform)
tools/make-mv3.sh quick
mv3-full: tools/make-mv3.sh $(sources) $(platform)
tools/make-mv3.sh full

View File

@ -291,6 +291,10 @@ if response.status_code != 204:
# package is higher version than current one.
#
# Be sure in sync with potentially modified files on remote
r = subprocess.run(['git', 'checkout', 'origin/master', '--', 'dist/chromium-mv3/log.txt'], stdout=subprocess.PIPE)
rout = bytes.decode(r.stdout).strip()
print('Update GitHub to point to newly signed self-hosted xpi package...')
updates_json_filepath = os.path.join(projdir, 'dist', 'firefox', 'updates.json')
with open(updates_json_filepath) as f:
@ -305,7 +309,6 @@ with open(updates_json_filepath) as f:
with open(updates_json_filepath, 'w') as f:
f.write(updates_json)
f.close()
# Automatically git add/commit if needed.
# - Stage the changed file
r = subprocess.run(['git', 'status', '-s', updates_json_filepath], stdout=subprocess.PIPE)
rout = bytes.decode(r.stdout).strip()

View File

@ -112,18 +112,6 @@ vAPI.getURL = browser.runtime.getURL;
/******************************************************************************/
vAPI.i18n = browser.i18n.getMessage;
// http://www.w3.org/International/questions/qa-scripts#directions
document.body.setAttribute(
'dir',
['ar', 'he', 'fa', 'ps', 'ur'].indexOf(vAPI.i18n('@@ui_locale')) !== -1
? 'rtl'
: 'ltr'
);
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/3057
// - webNavigation.onCreatedNavigationTarget become broken on Firefox when we
// try to make the popup panel close itself using the original

View File

@ -0,0 +1,48 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title>uBlock Origin Lite — Filter lists</title>
<link rel="stylesheet" type="text/css" href="css/default.css">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/fa-icons.css">
<link rel="stylesheet" type="text/css" href="css/3p-filters.css">
</head>
<body>
<div class="body">
<div id="actions">
<p id="listsOfBlockedHostsPrompt">
<p><button id="buttonApply" class="preferred disabled iconified" type="button" data-i18n-title="genericApplyChanges"><span class="fa-icon">check</span><span data-i18n="genericApplyChanges">_</span><span class="hover"></span></button>
</div>
<div>
<div id="lists"></div>
</div>
</div>
<div id="templates" style="display: none;">
<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>
</div>
<script src="js/fa-icons.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>
<script src="js/3p-filters.js" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,202 @@
{
"extName": {
"message": "uBlock Origin Lite",
"description": "extension name."
},
"extShortDesc": {
"message": "An experimental, permission-less lite content blocker -- block ads, trackers, miners and more by default.",
"description": "this will be in the Chrome web store: must be 132 characters or less"
},
"perRulesetStats": {
"message": "{{ruleCount}} rules, converted from {{filterCount}} network filters",
"description": "Appears aside each filter list in the _3rd-party filters_ pane"
},
"dashboardName": {
"message": "uBO Lite — Dashboard",
"description": "English: uBO Lite — Dashboard"
},
"dashboardUnsavedWarning": {
"message": "Warning! You have unsaved changes",
"description": "A warning in the dashboard when navigating away from unsaved changes"
},
"dashboardUnsavedWarningStay": {
"message": "Stay",
"description": "Label for button to prevent navigating away from unsaved changes"
},
"dashboardUnsavedWarningIgnore": {
"message": "Ignore",
"description": "Label for button to ignore unsaved changes"
},
"settingsPageName": {
"message": "Settings",
"description": "appears as tab name in dashboard"
},
"3pPageName": {
"message": "Filter lists",
"description": "appears as tab name in dashboard"
},
"1pPageName": {
"message": "My filters",
"description": "appears as tab name in dashboard"
},
"whitelistPageName": {
"message": "Trusted sites",
"description": "appears as tab name in dashboard"
},
"aboutPageName": {
"message": "About",
"description": "appears as tab name in dashboard"
},
"aboutPrivacyPolicy": {
"message": "Privacy policy",
"description": "Link to privacy policy on GitHub (English)"
},
"popupPowerSwitchInfo": {
"message": "Disable/enable uBO Lite for this site",
"description": "Tooltip for the main power button in the popup panel"
},
"popupTipDashboard": {
"message": "Open the dashboard",
"description": "English: Click to open the dashboard"
},
"popupTipZapper": {
"message": "Enter element zapper mode",
"description": "Tooltip for the element-zapper icon in the popup panel"
},
"popupTipPicker": {
"message": "Enter element picker mode",
"description": "English: Enter element picker mode"
},
"popupTipReport": {
"message": "Report an issue on this website",
"description": "Tooltip used for the 'chat' icon in the panel"
},
"popupTipSaveRules": {
"message": "Click to make your changes permanent.",
"description": "Tooltip when hovering over the padlock in the dynamic filtering pane."
},
"popupTipRevertRules": {
"message": "Click to revert your changes.",
"description": "Tooltip when hovering over the eraser in the dynamic filtering pane."
},
"settingsIconBadgePrompt": {
"message": "Show the number of blocked requests on the icon",
"description": "English: Show the number of blocked requests on the icon"
},
"settingsAppearance": {
"message": "Appearance",
"description": "Section for controlling user interface appearance"
},
"settingsThemeLabel": {
"message": "Theme",
"description": "Label for checkbox to enable a custom dark theme"
},
"settingsThemeAccent0Label": {
"message": "Custom accent color",
"description": "Label for checkbox to pick an accent color"
},
"settingsNoCSPReportsPrompt": {
"message": "Block CSP reports",
"description": "background information: https://github.com/gorhill/uBlock/issues/3150"
},
"3pGroupDefault": {
"message": "Default",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupAds": {
"message": "Ads",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupPrivacy": {
"message": "Privacy",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupMalware": {
"message": "Malware domains",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupAnnoyances": {
"message": "Annoyances",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupMisc": {
"message": "Miscellaneous",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"3pGroupRegions": {
"message": "Regions, languages",
"description": "Header for a ruleset section in 'Filter lists pane'"
},
"1pFormatHint": {
"message": "One filter per line. A filter can be a plain hostname, or an EasyList-compatible filter. Lines prefixed with <code>!</code> will be ignored.",
"description": "Short information about how to create custom filters"
},
"1pImport": {
"message": "Import and append",
"description": "English: Import and append"
},
"1pExport": {
"message": "Export",
"description": "English: Export"
},
"1pExportFilename": {
"message": "my-ublock-static-filters_{{datetime}}.txt",
"description": "English: my-ublock-static-filters_{{datetime}}.txt"
},
"whitelistPrompt": {
"message": "The trusted site directives dictate on which web pages uBO Lite should be disabled. One entry per line.",
"description": "A concise description of the 'Trusted sites' pane."
},
"whitelistImport": {
"message": "Import and append",
"description": "English: Import and append"
},
"whitelistExport": {
"message": "Export",
"description": "English: Export"
},
"whitelistExportFilename": {
"message": "my-ublock-trusted-sites_{{datetime}}.txt",
"description": "The default filename to use for import/export purpose"
},
"aboutChangelog": {
"message": "Changelog",
"description": ""
},
"aboutCode": {
"message": "Source code (GPLv3)",
"description": "English: Source code (GPLv3)"
},
"aboutContributors": {
"message": "Contributors",
"description": "English: Contributors"
},
"aboutSourceCode": {
"message": "Source code",
"description": "Link text to source code repo"
},
"aboutTranslations": {
"message": "Translations",
"description": "Link text to translations repo"
},
"aboutFilterLists": {
"message": "Filter lists",
"description": "Link text to uBO's own filter lists repo"
},
"aboutDependencies": {
"message": "External dependencies (GPLv3-compatible):",
"description": "Shown in the About pane"
},
"genericSubmit": {
"message": "Submit",
"description": "for generic 'Submit' buttons"
},
"genericApplyChanges": {
"message": "Apply changes",
"description": "for generic 'Apply changes' buttons"
},
"genericRevert": {
"message": "Revert",
"description": "for generic 'Revert' buttons"
}
}

View File

@ -0,0 +1,42 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>uBlock Origin Lite — About</title>
<link rel="stylesheet" type="text/css" href="css/default.css">
<link rel="stylesheet" type="text/css" href="css/common.css">
<link rel="stylesheet" type="text/css" href="css/dashboard-common.css">
<link rel="stylesheet" type="text/css" href="css/about.css">
</head>
<body>
<div class="body">
<div id="aboutNameVer" class="li"></div>
<div class="liul">
<div class="li">Copyright (c) Raymond Hill 2014-present</div>
</div>
<div class="li"><a href="https://github.com/gorhill/uBlock/wiki/Privacy-policy" data-i18n="aboutPrivacyPolicy"></a></div>
<div class="li"><a href="https://github.com/gorhill/uBlock/releases" data-i18n="aboutChangelog"></a></div>
<div class="li"><a href="https://github.com/gorhill/uBlock" data-i18n="aboutCode"></a></div>
<div class="li"><span data-i18n="aboutContributors"></span></div>
<div class="liul">
<div class="li"><a href="https://github.com/gorhill/uBlock/graphs/contributors" data-i18n="aboutSourceCode"></a></div>
<div class="li"><a href="https://crowdin.com/project/ublock" data-i18n="aboutTranslations"></a></div>
<div class="li"><a href="https://github.com/uBlockOrigin/uAssets/graphs/contributors" data-i18n="aboutFilterLists"></a></div>
</div>
<div class="li"><span data-i18n="aboutDependencies"></span></div>
<div class="liul">
<div class="li"><span><a href="https://github.com/chrismsimpson/Metropolis" target="_blank">Metropolis font family</a> by <a href="https://github.com/chrismsimpson">Chris Simpson</a></span></div>
<div class="li"><span><a href="https://github.com/rsms/inter" target="_blank">Inter font family</a> by <a href="https://github.com/rsms">Rasmus Andersson</a></span></div>
<div class="li"><span><a href="https://fontawesome.com/" target="_blank">FontAwesome font family</a> by <a href="https://github.com/davegandy">Dave Gandy</a></span></div>
</div>
</div>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js" type="module"></script>
<script src="js/about.js" type="module"></script>
</body>
</html>

View File

@ -0,0 +1,186 @@
@keyframes spin {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
body {
margin-bottom: 6rem;
}
#actions {
background-color: var(--surface-1);
position: sticky;
top: 0;
z-index: 10;
}
#buttonUpdate.active {
pointer-events: none;
}
#buttonUpdate.active .fa-icon svg {
animation: spin 1s linear infinite;
transform-origin: 50%;
}
#lists {
margin: 0.5em 0 0 0;
padding: 0;
}
.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;
}
.groupEntry.hideUnused:not([data-groupkey="user"]) .geDetails::before {
content: '+';
}
.groupEntry {
margin: 0.5em 0;
}
.groupEntry .geDetails {
cursor: pointer;
}
.groupEntry .geName {
pointer-events: none;
}
.groupEntry .geCount {
color: var(--ink-3);
font-size: 90%;
pointer-events: none;
}
.listEntries {
margin-inline-start: 0.6em;
-webkit-margin-start: 0.6em;
}
.groupEntry:not([data-groupkey="user"]) .listEntry:not(.isDefault).unused {
display: none;
}
.listEntry > * {
margin-left: 0;
margin-right: 0;
unicode-bidi: embed;
}
.listEntry .listname {
white-space: nowrap;
}
.listEntry.toRemove .checkbox {
visibility: hidden;
}
.listEntry.toRemove .listname {
text-decoration: line-through;
}
.listEntry a,
.listEntry .fa-icon,
.listEntry .counts {
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 .remove,
.listEntry .unsecure,
.listEntry .failed {
color: var(--info3-ink);
fill: var(--info3-ink);
cursor: pointer;
}
.listEntry.external .remove {
display: inline-flex;
}
.listEntry.mustread a.mustread {
color: var(--info1-ink);
fill: var(--info1-ink);
display: inline-flex;
}
.listEntry .counts {
font-size: smaller;
}
.listEntry.checked .counts {
display: inline-block;
}
.listEntry .status {
cursor: default;
display: none;
}
.listEntry.checked.unsecure .unsecure {
display: inline-flex;
}
.listEntry.failed .failed {
display: inline-flex;
}
.listEntry .cache {
cursor: pointer;
}
.listEntry.checked.cached:not(.obsolete) .cache {
display: inline-flex;
}
.listEntry .obsolete {
color: var(--info2-ink);
fill: var(--info2-ink);
}
body:not(.updating) .listEntry.checked.obsolete .obsolete {
display: inline-flex;
}
.listEntry .updating {
transform-origin: 50%;
}
body.updating .listEntry.checked.obsolete .updating {
animation: spin 1s steps(8) infinite;
display: inline-flex;
}
.listEntry.toImport {
margin: 0.5em 0;
}
.listEntry.toImport textarea {
border: 1px solid #ccc;
box-sizing: border-box;
display: block;
font-size: smaller;
height: 6em;
margin: 0;
resize: vertical;
visibility: hidden;
white-space: pre;
width: 100%;
}
.listEntry.toImport.checked textarea {
visibility: visible;
}
/* touch-screen devices */
:root.mobile .listEntry .fa-icon {
font-size: 120%;
margin: 0 0.5em 0 0;
}
:root.mobile .listEntries {
margin-inline-start: 0;
-webkit-margin-start: 0;
}
:root.mobile .li.listEntry {
/* background-color: var(--bg-1); */
overflow-x: auto;
}
:root.mobile .li.listEntry label > span:not([class]) {
flex-grow: 1;
}
:root.mobile .li.listEntry .listname,
:root.mobile .li.listEntry .iconbar {
align-items: flex-start;
display: flex;
white-space: nowrap;
}
:root.mobile .li.listEntry .iconbar {
margin-top: 0.2em;
}

View File

@ -0,0 +1,55 @@
body > div.body {
margin: 0 1em;
}
h2, h3 {
margin: 1em 0;
}
h2 {
font-size: 18px;
}
h3 {
font-size: 16px;
}
a {
text-decoration: none;
}
.fa-icon.info {
color: var(--info0-ink);
fill: var(--info0-ink);
font-size: 115%;
}
.fa-icon.info:hover {
transform: scale(1.25);
}
.fa-icon.info.important {
color: var(--info2-ink);
fill: var(--info2-ink);
}
.fa-icon.info.very-important {
color: var(--info3-ink);
fill: var(--info3-ink);
}
input[type="number"] {
width: 5em;
}
@media (max-height: 640px), (max-height: 800px) and (max-width: 480px) {
.body > p,
.body > ul {
margin: 0.5em 0;
}
.vverbose {
display: none !important;
}
}
/**
On mobile device, the on-screen keyboard may take up
so much space that it overlaps the content being edited.
The rule below makes it possible to scroll the edited
content within view.
*/
:root.mobile {
overflow: auto;
}
:root.mobile body {
min-height: 600px;
}

View File

@ -0,0 +1,35 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, minimum-scale=1">
<title data-i18n="dashboardName"></title>
<link rel="stylesheet" href="css/default.css">
<link rel="stylesheet" href="css/common.css">
<link rel="stylesheet" href="css/fa-icons.css">
<link rel="stylesheet" href="css/dashboard.css">
<link rel="shortcut icon" type="image/png" href="img/icon_16.png"/>
</head>
<body>
<div id="dashboard-nav">
<span class="logo"><img data-i18n-title="extName" src="img/ublock.svg"></span><!--
--><button class="tabButton" type="button" data-pane="3p-filters.html" data-i18n="3pPageName" tabindex="0"></button><!--
--><button class="tabButton" type="button" data-pane="about.html" data-i18n="aboutPageName" tabindex="0"></button>
</div>
<section id="unsavedWarning" class="notice">
<div>
<span data-i18n="dashboardUnsavedWarning"></span>&emsp;
<button type="button" data-i18n="dashboardUnsavedWarningStay">_<span class="hover"></span></button>&ensp;
<button type="button" data-i18n="dashboardUnsavedWarningIgnore">_<span class="hover"></span></button>
</div>
<div></div>
</section>
<iframe id="iframe" src=""></iframe>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard.js" type="module"></script>
</body>
</html>

View File

Before

Width:  |  Height:  |  Size: 2.6 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -0,0 +1,379 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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 { sendMessage } from './ext.js';
import { i18n$ } from './i18n.js';
import { dom, qs$, qsa$ } from './dom.js';
import { simpleStorage } from './storage.js';
/******************************************************************************/
let cachedRulesetData = {};
let filteringSettingsHash = '';
let hideUnusedSet = new Set([ 'regions' ]);
/******************************************************************************/
const renderNumber = function(value) {
return value.toLocaleString();
};
/******************************************************************************/
const renderFilterLists = function(soft) {
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 = listEntryTemplate.cloneNode(true);
}
const on = enabledRulesets.includes(ruleset.id);
li.classList.toggle('checked', on);
let elem;
if ( dom.attr(li, 'data-listkey') !== ruleset.id ) {
dom.attr(li, 'data-listkey', ruleset.id);
qs$('input[type="checkbox"]', li).checked = on;
qs$('.listname', li).textContent = ruleset.name || ruleset.id;
dom.removeClass(li, 'toRemove');
if ( ruleset.supportName ) {
dom.addClass(li, 'support');
elem = qs$('a.support', li);
dom.attr(elem, 'href', ruleset.supportURL);
dom.attr(elem, 'title', ruleset.supportName);
} else {
dom.removeClass(li, 'support');
}
if ( ruleset.instructionURL ) {
dom.addClass(li, 'mustread');
dom.attr(qs$('a.mustread', li), 'href', ruleset.instructionURL);
} else {
dom.removeClass(li, 'mustread');
}
dom.toggleClass(li, 'isDefault', ruleset.isDefault === true);
dom.toggleClass(li, 'unused', hideUnused && !on);
}
// https://github.com/gorhill/uBlock/issues/1429
if ( !soft ) {
qs$('input[type="checkbox"]', li).checked = on;
}
li.title = listStatsTemplate
.replace('{{ruleCount}}', renderNumber(ruleset.rules.accepted))
.replace('{{filterCount}}', renderNumber(ruleset.filters.accepted));
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 = listGroupTemplate.cloneNode(true);
let groupName = groupNames.get(groupKey);
if ( groupName === undefined ) {
groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
groupNames.set(groupKey, groupName);
}
if ( groupName !== '' ) {
qs$('.geName', liGroup).textContent = groupName;
}
}
if ( qs$('.geName:empty', liGroup) === null ) {
qs$('.geCount', liGroup).textContent = listEntryCountFromGroup(groupRulesets);
}
const hideUnused = mustHideUnusedLists(groupKey);
liGroup.classList.toggle('hideUnused', hideUnused);
const ulGroup = qs$('.listEntries', liGroup);
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;
};
// Incremental rendering: this will allow us to easily discard unused
// DOM list entries.
dom.addClass(
qsa$('#lists .listEntries .listEntry[data-listkey]'),
'discard'
);
// Visually split the filter lists in three groups
const ulLists = qs$('#lists');
const groups = new Map([
[
'default',
rulesetDetails.filter(ruleset =>
ruleset.id === 'default'
),
],
[
'misc',
rulesetDetails.filter(ruleset =>
ruleset.id !== 'default' && typeof ruleset.lang !== 'string'
),
],
[
'regions',
rulesetDetails.filter(ruleset =>
typeof ruleset.lang === 'string'
),
],
]);
dom.toggleClass(dom.body, 'hideUnused', mustHideUnusedLists('*'));
for ( const [ groupKey, groupRulesets ] of groups ) {
let liGroup = liFromListGroup(groupKey, groupRulesets);
liGroup.setAttribute('data-groupkey', groupKey);
if ( liGroup.parentElement === null ) {
ulLists.appendChild(liGroup);
}
}
dom.remove(qsa$('#lists .listEntries .listEntry.discard'));
// Compute a hash of the settings so that we can keep track of changes
// affecting the loading of filter lists.
if ( !soft ) {
filteringSettingsHash = hashFromCurrentFromSettings();
}
renderWidgets();
};
/******************************************************************************/
const renderWidgets = function() {
dom.toggleClass(
qs$('#buttonApply'),
'disabled',
filteringSettingsHash === hashFromCurrentFromSettings()
);
// Compute total counts
const rulesetMap = new Map(
cachedRulesetData.rulesetDetails.map(rule => [ rule.id, rule ])
);
let filterCount = 0;
let ruleCount = 0;
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) === null ) { continue; }
const ruleset = rulesetMap.get(liEntry.dataset.listkey);
if ( ruleset === undefined ) { continue; }
filterCount += ruleset.filters.accepted;
ruleCount += ruleset.rules.accepted;
}
qs$('#listsOfBlockedHostsPrompt').textContent = i18n$('perRulesetStats')
.replace('{{ruleCount}}', ruleCount.toLocaleString())
.replace('{{filterCount}}', filterCount.toLocaleString());
};
/******************************************************************************/
const hashFromCurrentFromSettings = function() {
const hash = [];
const listHash = [];
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) ) {
listHash.push(dom.attr(liEntry, 'data-listkey'));
}
}
hash.push(listHash.sort().join());
return hash.join();
};
/******************************************************************************/
function onListsetChanged(ev) {
const input = ev.target;
const li = input.closest('.listEntry');
dom.toggleClass(li, 'checked', input.checked);
renderWidgets();
}
dom.on(
qs$('#lists'),
'change',
'.listEntry input',
onListsetChanged
);
/******************************************************************************/
const applyEnabledRulesets = async function() {
const enabledRulesets = [];
for ( const liEntry of qsa$('#lists .listEntry[data-listkey]') ) {
if ( qs$('input[type="checkbox"]:checked', liEntry) === null ) { continue; }
enabledRulesets.push(liEntry.dataset.listkey);
}
await sendMessage({
what: 'applyRulesets',
enabledRulesets,
});
filteringSettingsHash = hashFromCurrentFromSettings();
};
const buttonApplyHandler = async function() {
dom.removeClass(qs$('#buttonApply'), 'enabled');
await applyEnabledRulesets();
renderWidgets();
};
dom.on(
qs$('#buttonApply'),
'click',
( ) => { buttonApplyHandler(); }
);
/******************************************************************************/
// Collapsing of unused lists.
const mustHideUnusedLists = function(which) {
const hideAll = hideUnusedSet.has('*');
if ( which === '*' ) { return hideAll; }
return hideUnusedSet.has(which) !== hideAll;
};
const toggleHideUnusedLists = function(which) {
const doesHideAll = hideUnusedSet.has('*');
let groupSelector;
let mustHide;
if ( which === '*' ) {
mustHide = doesHideAll === false;
groupSelector = '';
hideUnusedSet.clear();
if ( mustHide ) {
hideUnusedSet.add(which);
}
document.body.classList.toggle('hideUnused', mustHide);
dom.toggleClass(qsa$('.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.toggleClass(qsa$(groupSelector), 'hideUnused', mustHide);
}
for ( const elem of qsa$(`#lists ${groupSelector} .listEntry[data-listkey] input[type="checkbox"]:not(:checked)`) ) {
dom.toggleClass(
elem.closest('.listEntry[data-listkey]'),
'unused',
mustHide
);
}
simpleStorage.setItem(
'hideUnusedFilterLists',
Array.from(hideUnusedSet)
);
};
dom.on(
qs$('#lists'),
'click',
'.groupEntry[data-groupkey] > .geDetails',
ev => {
toggleHideUnusedLists(
dom.attr(ev.target.closest('[data-groupkey]'), 'data-groupkey')
);
}
);
// Initialize from saved state.
simpleStorage.getItem('hideUnusedFilterLists').then(value => {
if ( Array.isArray(value) ) {
hideUnusedSet = new Set(value);
}
});
/******************************************************************************/
self.hasUnsavedData = function() {
return hashFromCurrentFromSettings() !== filteringSettingsHash;
};
/******************************************************************************/
dom.on(
qs$('#lists'),
'click',
'.listEntry label *',
ev => {
if ( ev.target.matches('input,.forinput') ) { return; }
ev.preventDefault();
}
);
/******************************************************************************/
sendMessage({
what: 'getRulesetData',
}).then(data => {
if ( !data ) { return; }
cachedRulesetData = data;
try {
renderFilterLists();
} catch(ex) {
}
}).catch(reason => {
console.trace(reason);
});
/******************************************************************************/

View File

@ -0,0 +1,35 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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 { runtime } from './ext.js';
import { qs$ } from './dom.js';
/******************************************************************************/
(async ( ) => {
const manifest = runtime.getManifest();
qs$('#aboutNameVer').textContent = `${manifest.name} ${manifest.version}`;
})();

View File

@ -1,27 +1,151 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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
*/
/* jshint esversion:11 */
'use strict';
import rulesetDetails from '/rulesets/ruleset-details.js';
/******************************************************************************/
import { dnr, i18n, runtime } from './ext.js';
/******************************************************************************/
const dnr = chrome.declarativeNetRequest;
const TRUSTED_DIRECTIVE_BASE_RULE_ID = 1000000;
const RULE_REALM_SIZE = 1000000;
const REGEXES_REALM_START = 1000000;
const REGEXES_REALM_END = REGEXES_REALM_START + RULE_REALM_SIZE;
const TRUSTED_DIRECTIVE_BASE_RULE_ID = 8000000;
const CURRENT_CONFIG_BASE_RULE_ID = 9000000;
const dynamicRuleMap = new Map();
const rulesetDetails = new Map();
const rulesetConfig = {
version: '',
enabledRulesets: [],
};
/******************************************************************************/
async function updateRegexRules() {
function getCurrentVersion() {
return runtime.getManifest().version;
}
async function loadRulesetConfig() {
const configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID);
if ( configRule === undefined ) {
rulesetConfig.enabledRulesets = await defaultRulesetsFromLanguage();
return;
}
const match = /^\|\|example.invalid\/([^\/]+)\/(?:([^\/]+)\/)?/.exec(
configRule.condition.urlFilter
);
if ( match === null ) { return; }
rulesetConfig.version = match[1];
if ( match[2] ) {
rulesetConfig.enabledRulesets =
decodeURIComponent(match[2] || '').split(' ');
}
}
async function saveRulesetConfig() {
let configRule = dynamicRuleMap.get(CURRENT_CONFIG_BASE_RULE_ID);
if ( configRule === undefined ) {
configRule = {
id: CURRENT_CONFIG_BASE_RULE_ID,
action: {
type: 'allow',
},
condition: {
urlFilter: '',
},
};
}
const version = rulesetConfig.version;
const enabledRulesets = encodeURIComponent(rulesetConfig.enabledRulesets.join(' '));
const urlFilter = `||example.invalid/${version}/${enabledRulesets}/`;
if ( urlFilter === configRule.condition.urlFilter ) { return; }
configRule.condition.urlFilter = urlFilter;
return dnr.updateDynamicRules({
addRules: [ configRule ],
removeRuleIds: [ CURRENT_CONFIG_BASE_RULE_ID ],
});
}
/******************************************************************************/
function fetchJSON(filename) {
return fetch(`/rulesets/${filename}.json`).then(response =>
response.json()
).catch(reason => {
console.info(reason);
});
}
/******************************************************************************/
async function updateRegexRules(dynamicRules) {
// Avoid testing already tested regexes
const validRegexSet = new Set(
dynamicRules.filter(rule =>
rule.condition?.regexFilter && true || false
).map(rule =>
rule.condition.regexFilter
)
);
const allRules = [];
const toCheck = [];
for ( const details of rulesetDetails ) {
// Fetch regexes for all enabled rulesets
const toFetch = [];
for ( const details of rulesetDetails.values() ) {
if ( details.enabled !== true ) { continue; }
for ( const rule of details.rules.regexes ) {
const regex = rule.condition.regexFilter;
const isCaseSensitive = rule.condition.isUrlFilterCaseSensitive === true;
toFetch.push(fetchJSON(`${details.id}.regexes`));
}
const regexRulesets = await Promise.all(toFetch);
// Validate fetched regexes
let regexRuleId = REGEXES_REALM_START;
for ( const rules of regexRulesets ) {
if ( Array.isArray(rules) === false ) { continue; }
for ( const rule of rules ) {
rule.id = regexRuleId++;
const {
regexFilter: regex,
isUrlFilterCaseSensitive: isCaseSensitive
} = rule.condition;
allRules.push(rule);
toCheck.push(dnr.isRegexSupported({ regex, isCaseSensitive }));
toCheck.push(
validRegexSet.has(regex)
? { isSupported: true }
: dnr.isRegexSupported({ regex, isCaseSensitive })
);
}
}
// Collate results
const results = await Promise.all(toCheck);
const newRules = [];
for ( let i = 0; i < allRules.length; i++ ) {
@ -33,11 +157,18 @@ async function updateRegexRules() {
console.info(`${result.reason}: ${rule.condition.regexFilter}`);
}
}
console.info(
`Rejected regex filters: ${allRules.length-newRules.length} out of ${allRules.length}`
);
// Add validated regex rules to dynamic ruleset without affecting rules
// outside regex rule realm.
const newRuleMap = new Map(newRules.map(rule => [ rule.id, rule ]));
const addRules = [];
const removeRuleIds = [];
for ( const oldRule of dynamicRuleMap.values() ) {
if ( oldRule.id >= TRUSTED_DIRECTIVE_BASE_RULE_ID ) { continue; }
if ( oldRule.id < REGEXES_REALM_START ) { continue; }
if ( oldRule.id >= REGEXES_REALM_END ) { continue; }
const newRule = newRuleMap.get(oldRule.id);
if ( newRule === undefined ) {
removeRuleIds.push(oldRule.id);
@ -148,50 +279,168 @@ async function toggleTrustedSiteDirective(details) {
/******************************************************************************/
(async ( ) => {
async function enableRulesets(ids) {
const afterIds = new Set(ids);
const beforeIds = new Set(await dnr.getEnabledRulesets());
const enableRulesetIds = [];
const disableRulesetIds = [];
for ( const id of afterIds ) {
if ( beforeIds.has(id) ) { continue; }
enableRulesetIds.push(id);
}
for ( const id of beforeIds ) {
if ( afterIds.has(id) ) { continue; }
disableRulesetIds.push(id);
}
if ( enableRulesetIds.length !== 0 || disableRulesetIds.length !== 0 ) {
return dnr.updateEnabledRulesets({ enableRulesetIds,disableRulesetIds });
}
}
async function getEnabledRulesetsStats() {
const ids = await dnr.getEnabledRulesets();
const out = [];
for ( const id of ids ) {
const ruleset = rulesetDetails.get(id);
if ( ruleset === undefined ) { continue; }
out.push({
name: ruleset.name,
filterCount: ruleset.filters.accepted,
ruleCount: ruleset.rules.accepted,
});
}
return out;
}
async function defaultRulesetsFromLanguage() {
const out = [ 'default' ];
const dropCountry = lang => {
const pos = lang.indexOf('-');
if ( pos === -1 ) { return lang; }
return lang.slice(0, pos);
};
const langSet = new Set();
await i18n.getAcceptLanguages().then(langs => {
for ( const lang of langs.map(dropCountry) ) {
langSet.add(lang);
}
});
langSet.add(dropCountry(i18n.getUILanguage()));
const reTargetLang = new RegExp(
`\\b(${Array.from(langSet).join('|')})\\b`
);
for ( const [ id, details ] of rulesetDetails ) {
if ( typeof details.lang !== 'string' ) { continue; }
if ( reTargetLang.test(details.lang) === false ) { continue; }
out.push(id);
}
return out;
}
/******************************************************************************/
async function start() {
// Fetch enabled rulesets and dynamic rules
const dynamicRules = await dnr.getDynamicRules();
for ( const rule of dynamicRules ) {
dynamicRuleMap.set(rule.id, rule);
}
await updateRegexRules();
// Fetch ruleset details
await fetchJSON('ruleset-details').then(entries => {
if ( entries === undefined ) { return; }
for ( const entry of entries ) {
rulesetDetails.set(entry.id, entry);
}
});
await loadRulesetConfig();
console.log(`Dynamic rule count: ${dynamicRuleMap.size}`);
console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - dynamicRuleMap.size}`);
await enableRulesets(rulesetConfig.enabledRulesets);
// We need to update the regex rules only when ruleset version changes.
const currentVersion = getCurrentVersion();
if ( currentVersion !== rulesetConfig.version ) {
await updateRegexRules(dynamicRules);
console.log(`Version change: ${rulesetConfig.version} => ${currentVersion}`);
rulesetConfig.version = currentVersion;
}
saveRulesetConfig();
const enabledRulesets = await dnr.getEnabledRulesets();
console.log(`Enabled rulesets: ${enabledRulesets}`);
console.log(`Available dynamic rule count: ${dnr.MAX_NUMBER_OF_DYNAMIC_AND_SESSION_RULES - dynamicRuleMap.size}`);
dnr.getAvailableStaticRuleCount().then(count => {
console.log(`Available static rule count: ${count}`);
});
dnr.setExtensionActionOptions({ displayActionCountAsBadgeText: true });
}
chrome.runtime.onMessage.addListener((request, sender, callback) => {
switch ( request.what ) {
case 'popupPanelData':
matchesTrustedSiteDirective(request).then(response => {
callback({
isTrusted: response,
rulesetDetails: rulesetDetails.filter(details =>
details.enabled
).map(details => ({
name: details.name,
filterCount: details.filters.accepted,
ruleCount: details.rules.accepted,
})),
});
/******************************************************************************/
function messageListener(request, sender, callback) {
switch ( request.what ) {
case 'getRulesetData': {
dnr.getEnabledRulesets().then(enabledRulesets => {
callback({
enabledRulesets,
rulesetDetails: Array.from(rulesetDetails.values()),
});
return true;
case 'toggleTrustedSiteDirective':
toggleTrustedSiteDirective(request).then(response => {
callback(response);
});
return true;
}
case 'applyRulesets': {
enableRulesets(request.enabledRulesets).then(( ) => {
rulesetConfig.enabledRulesets = request.enabledRulesets;
return saveRulesetConfig();
}).then(( ) => {
callback();
});
return true;
}
case 'popupPanelData': {
Promise.all([
matchesTrustedSiteDirective(request),
getEnabledRulesetsStats(),
]).then(results => {
callback({
isTrusted: results[0],
rulesetDetails: results[1],
});
return true;
default:
break;
}
});
});
return true;
}
case 'toggleTrustedSiteDirective': {
toggleTrustedSiteDirective(request).then(response => {
callback(response);
});
return true;
}
default:
break;
}
}
/******************************************************************************/
(async ( ) => {
await start();
runtime.onMessage.addListener(messageListener);
})();

View File

@ -0,0 +1,32 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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 { dom, qsa$ } from './dom.js';
/******************************************************************************/
// Open links in the proper window
dom.attr(qsa$('a'), 'target', '_blank');
dom.attr(qsa$('a[href*="dashboard.html"]'), 'target', '_parent');

View File

@ -0,0 +1,135 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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 { simpleStorage } from './storage.js';
import { dom, qs$ } from './dom.js';
/******************************************************************************/
const discardUnsavedData = function(synchronous = false) {
const paneFrame = document.getElementById('iframe');
const paneWindow = paneFrame.contentWindow;
if (
typeof paneWindow.hasUnsavedData !== 'function' ||
paneWindow.hasUnsavedData() === false
) {
return true;
}
if ( synchronous ) {
return false;
}
return new Promise(resolve => {
const modal = document.querySelector('#unsavedWarning');
modal.classList.add('on');
modal.focus();
const onDone = status => {
modal.classList.remove('on');
document.removeEventListener('click', onClick, true);
resolve(status);
};
const onClick = ev => {
const target = ev.target;
if ( target.matches('[data-i18n="dashboardUnsavedWarningStay"]') ) {
return onDone(false);
}
if ( target.matches('[data-i18n="dashboardUnsavedWarningIgnore"]') ) {
return onDone(true);
}
if ( modal.querySelector('[data-i18n="dashboardUnsavedWarning"]').contains(target) ) {
return;
}
onDone(false);
};
document.addEventListener('click', onClick, true);
});
};
const loadDashboardPanel = function(pane, first) {
const tabButton = document.querySelector(`[data-pane="${pane}"]`);
if ( tabButton === null || tabButton.classList.contains('selected') ) {
return;
}
const loadPane = ( ) => {
self.location.replace(`#${pane}`);
for ( const node of document.querySelectorAll('.tabButton.selected') ) {
node.classList.remove('selected');
}
tabButton.classList.add('selected');
tabButton.scrollIntoView();
document.querySelector('#iframe').contentWindow.location.replace(pane);
if ( pane !== 'no-dashboard.html' ) {
simpleStorage.setItem('dashboardLastVisitedPane', pane);
}
};
if ( first ) {
return loadPane();
}
const r = discardUnsavedData();
if ( r === false ) { return; }
if ( r === true ) {
return loadPane();
}
r.then(status => {
if ( status === false ) { return; }
loadPane();
});
};
const onTabClickHandler = function(ev) {
loadDashboardPanel(ev.target.getAttribute('data-pane'));
};
if ( self.location.hash.slice(1) === 'no-dashboard.html' ) {
document.body.classList.add('noDashboard');
}
(async ( ) => {
let pane = null;
if ( self.location.hash !== '' ) {
pane = self.location.hash.slice(1) || null;
}
loadDashboardPanel(pane !== null ? pane : '3p-filters.html', true);
dom.on(
qs$('#dashboard-nav'),
'click',
'.tabButton',
onTabClickHandler
);
// https://developer.mozilla.org/en-US/docs/Web/API/Window/beforeunload_event
window.addEventListener('beforeunload', ( ) => {
if ( discardUnsavedData(true) ) { return; }
event.preventDefault();
event.returnValue = '';
});
})();
/******************************************************************************/

View File

@ -0,0 +1,121 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
function normalizeTarget(target) {
if ( target === null ) { return []; }
if ( Array.isArray(target) ) { return target; }
return target instanceof Element
? [ target ]
: Array.from(target);
}
function makeEventHandler(selector, callback) {
return function(event) {
const dispatcher = event.currentTarget;
if (
dispatcher instanceof HTMLElement === false ||
typeof dispatcher.querySelectorAll !== 'function'
) {
return;
}
const receiver = event.target;
const ancestor = receiver.closest(selector);
if (
ancestor === receiver &&
ancestor !== dispatcher &&
dispatcher.contains(ancestor)
) {
callback.call(receiver, event);
}
};
}
/******************************************************************************/
class dom {
static addClass(target, cl) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.add(cl);
}
}
static toggleClass(target, cl, state = undefined) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.toggle(cl, state);
}
}
static removeClass(target, cl) {
for ( const elem of normalizeTarget(target) ) {
elem.classList.remove(cl);
}
}
static attr(target, attr, value = undefined) {
for ( const elem of normalizeTarget(target) ) {
if ( value === undefined ) {
return elem.getAttribute(attr);
}
elem.setAttribute(attr, value);
}
}
static remove(target) {
for ( const elem of normalizeTarget(target) ) {
elem.remove();
}
}
static on(target, type, selector, callback) {
if ( typeof selector === 'function' ) {
callback = selector;
selector = undefined;
} else {
callback = makeEventHandler(selector, callback);
}
for ( const elem of normalizeTarget(target) ) {
elem.addEventListener(type, callback, selector !== undefined);
}
}
}
dom.body = document.body;
/******************************************************************************/
function qs$(s, elem = undefined) {
return (elem || document).querySelector(s);
}
function qsa$(s, elem = undefined) {
return (elem || document).querySelectorAll(s);
}
/******************************************************************************/
export { dom, qs$, qsa$ };

View File

@ -0,0 +1,64 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
const browser =
self.browser instanceof Object &&
self.browser instanceof Element === false
? self.browser
: self.chrome;
const dnr = browser.declarativeNetRequest;
const i18n = browser.i18n;
const runtime = browser.runtime;
/******************************************************************************/
// The extension's service worker can be evicted at any time, so when we
// send a message, we try a few more times when the message fails to be sent.
function sendMessage(msg) {
return new Promise((resolve, reject) => {
let i = 5;
const send = ( ) => {
runtime.sendMessage(msg).then(response => {
resolve(response);
}).catch(reason => {
i -= 1;
if ( i <= 0 ) {
reject(reason);
} else {
setTimeout(send, 200);
}
});
};
send();
});
}
/******************************************************************************/
export { browser, dnr, i18n, runtime, sendMessage };

View File

@ -1,7 +1,7 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
Copyright (C) 2014-present Raymond Hill
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
@ -19,35 +19,23 @@
Home: https://github.com/gorhill/uBlock
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
import { browser, sendMessage } from './ext.js';
import { i18n$ } from './i18n.js';
import { simpleStorage } from './storage.js';
/******************************************************************************/
let currentTab = {};
let originalTrustedState = false;
/******************************************************************************/
class safeLocalStorage {
static getItem(k) {
try {
return self.localStorage.getItem(k);
}
catch(ex) {
}
return null;
}
static setItem(k, v) {
try {
self.localStorage.setItem(k, v);
}
catch(ex) {
}
}
}
/******************************************************************************/
async function toggleTrustedSiteDirective() {
let url;
try {
@ -57,7 +45,7 @@ async function toggleTrustedSiteDirective() {
}
if ( url instanceof URL === false ) { return; }
const targetTrustedState = document.body.classList.contains('off');
const newTrustedState = await chrome.runtime.sendMessage({
const newTrustedState = await sendMessage({
what: 'toggleTrustedSiteDirective',
origin: url.origin,
state: targetTrustedState,
@ -75,7 +63,7 @@ async function toggleTrustedSiteDirective() {
/******************************************************************************/
function reloadTab(ev) {
chrome.tabs.reload(currentTab.id, {
browser.tabs.reload(currentTab.id, {
bypassCache: ev.ctrlKey || ev.metaKey || ev.shiftKey,
});
document.body.classList.remove('needReload');
@ -85,7 +73,7 @@ function reloadTab(ev) {
/******************************************************************************/
async function init() {
const [ tab ] = await chrome.tabs.query({ active: true });
const [ tab ] = await browser.tabs.query({ active: true });
if ( tab instanceof Object === false ) { return true; }
currentTab = tab;
@ -97,7 +85,7 @@ async function init() {
let popupPanelData;
if ( url !== undefined ) {
popupPanelData = await chrome.runtime.sendMessage({
popupPanelData = await sendMessage({
what: 'popupPanelData',
origin: url.origin,
});
@ -126,7 +114,9 @@ async function init() {
h1.textContent = details.name;
parent.append(h1);
const p = document.createElement('p');
p.textContent = `${details.ruleCount.toLocaleString()} rules, converted from ${details.filterCount.toLocaleString()} network filters`;
p.textContent = i18n$('perRulesetStats')
.replace('{{ruleCount}}', details.ruleCount.toLocaleString())
.replace('{{filterCount}}', details.filterCount.toLocaleString());
parent.append(p);
}
}
@ -189,12 +179,12 @@ async function toggleSections(more) {
}
if ( newBits === currentBits ) { return; }
sectionBitsToAttribute(newBits);
safeLocalStorage.setItem('popupPanelSections', newBits);
simpleStorage.setItem('popupPanelSections', newBits);
}
sectionBitsToAttribute(
parseInt(safeLocalStorage.getItem('popupPanelSections'), 10)
);
simpleStorage.getItem('popupPanelSections').then(s => {
sectionBitsToAttribute(parseInt(s, 10) || 0);
});
document.querySelector('#moreButton').addEventListener('click', ( ) => {
toggleSections(true);

View File

@ -0,0 +1,44 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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
*/
/* jshint esversion:11 */
'use strict';
/******************************************************************************/
export class simpleStorage {
static getItem(k) {
try {
return Promise.resolve(JSON.parse(self.localStorage.getItem(k)));
}
catch(ex) {
}
return Promise.resolve(null);
}
static setItem(k, v) {
try {
self.localStorage.setItem(k, JSON.stringify(v));
}
catch(ex) {
}
}
}

View File

@ -16,6 +16,7 @@
"rule_resources": [
]
},
"default_locale": "en",
"description": "uBO Minus is permission-less experimental MV3-based network request blocker",
"icons": {
"16": "img/icon_16.png",
@ -26,6 +27,7 @@
"manifest_version": 3,
"minimum_chrome_version": "101.0",
"name": "uBlock Origin Lite",
"options_page": "dashboard.html",
"permissions": [
"activeTab",
"declarativeNetRequest"

View File

@ -19,7 +19,7 @@
<span id="saveRules" class="fa-icon" data-i18n-title="popupTipSaveRules">lock</span>
<span id="revertRules" class="fa-icon" data-i18n-title="popupTipRevertRules">eraser</span>
</div>
<div id="switch" role="button" aria-label tabindex="0" title>
<div id="switch" role="button" aria-label tabindex="0" data-i18n-title="popupPowerSwitchInfo">
<span class="fa-icon"><!--
Power button taken from Font Awesome v4.7.0 by Dave Gandy.
Unlike other FA icons, the power button is inlined here so
@ -40,6 +40,13 @@
</div>
<div id="hostname"><span></span>&shy;<span></span></div>
</div>
<div id="basicTools" class="toolRibbon" data-more="c">
<span class="fa-icon tool needPick" data-i18n-title="popupTipZapper">bolt<span class="caption"></span></span>
<span class="fa-icon tool needPick" data-i18n-title="popupTipPicker">eye-dropper<span class="caption"></span></span>
<span class="fa-icon tool needPick" data-i18n-title="popupTipReport">comment-alt<span class="caption"></span></span>
<a href="logger-ui.html#_" class="fa-icon tool" target="uBOLogger" tabindex="0" data-i18n-title="popupTipLog">list-alt<span class="caption"></span></a>
<a href="dashboard.html" class="fa-icon tool enabled" target="uBODashboard" tabindex="0" data-i18n-title="popupTipDashboard">cogs<span class="caption" data-i18n="popupTipDashboard"></span></a>
</div>
<hr data-section="a">
<div id="rulesetStats" data-section="a">
</div>
@ -55,6 +62,7 @@
</div>
<script src="js/fa-icons.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/popup.js" type="module"></script>
</body>

View File

@ -27,8 +27,8 @@ import fs from 'fs/promises';
import https from 'https';
import process from 'process';
import rulesetConfigs from './ruleset-config.js';
import { dnrRulesetFromRawLists } from './js/static-dnr-filtering.js';
import { StaticFilteringParser } from './js/static-filtering-parser.js';
/******************************************************************************/
@ -51,21 +51,75 @@ const commandLineArgs = (( ) => {
/******************************************************************************/
const isUnsupported = rule =>
rule._error !== undefined;
const isRegex = rule =>
rule.condition !== undefined &&
rule.condition.regexFilter !== undefined;
const isRedirect = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.extensionPath !== undefined;
const isCsp = 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 &&
isCsp(rule) === false &&
isRemoveparam(rule) === false;
/******************************************************************************/
const stdOutput = [];
const log = (text, silent = false) => {
stdOutput.push(text);
if ( silent === false ) {
console.log(text);
}
};
/******************************************************************************/
const fetchList = url => {
return new Promise((resolve, reject) => {
log(`\tFetching ${url}`);
https.get(url, response => {
const data = [];
response.on('data', chunk => {
data.push(chunk.toString());
});
response.on('end', ( ) => {
resolve({ url, content: data.join('') });
});
}).on('error', error => {
reject(error);
});
});
};
/******************************************************************************/
async function main() {
const env = [ 'chromium' ];
const writeOps = [];
const ruleResources = [];
const rulesetDetails = [];
const regexRulesetDetails = new Map();
const outputDir = commandLineArgs.get('output') || '.';
const output = [];
const log = (text, silent = false) => {
output.push(text);
if ( silent === false ) {
console.log(text);
}
};
// Get manifest content
const manifest = await fs.readFile(
`${outputDir}/manifest.json`,
@ -104,78 +158,62 @@ async function main() {
return v;
};
const isUnsupported = rule =>
rule._error !== undefined;
const isRegex = rule =>
rule.condition !== undefined &&
rule.condition.regexFilter !== undefined;
const isRedirect = rule =>
rule.action !== undefined &&
rule.action.type === 'redirect' &&
rule.action.redirect.extensionPath !== undefined;
const isCsp = 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 &&
isCsp(rule) === false &&
isRemoveparam(rule) === false
;
const rulesetDir = `${outputDir}/rulesets`;
const rulesetDirPromise = fs.mkdir(`${rulesetDir}`, { recursive: true });
const fetchList = url => {
return new Promise((resolve, reject) => {
https.get(url, response => {
const data = [];
response.on('data', chunk => {
data.push(chunk.toString());
});
response.on('end', ( ) => {
resolve({ name: url, text: data.join('') });
});
}).on('error', error => {
reject(error);
});
});
};
const readList = path =>
fs.readFile(path, { encoding: 'utf8' })
.then(text => ({ name: path, text }));
const writeFile = (path, data) =>
rulesetDirPromise.then(( ) =>
fs.writeFile(path, data));
for ( const ruleset of rulesetConfigs ) {
const lists = [];
const rulesetFromURLS = async function(assetDetails) {
log('============================');
log(`Listset for '${ruleset.id}':`);
log(`Listset for '${assetDetails.id}':`);
if ( Array.isArray(ruleset.paths) ) {
for ( const path of ruleset.paths ) {
log(`\t${path}`);
lists.push(readList(`assets/${path}`));
// 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);
newParts.push(
fetchList(part.url).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: '' };
})
);
}
parts = await Promise.all(newParts);
parts = StaticFilteringParser.utils.preparser.expandIncludes(parts, env);
}
if ( Array.isArray(ruleset.urls) ) {
for ( const url of ruleset.urls ) {
log(`\t${url}`);
lists.push(fetchList(url));
}
const text = parts.join('\n');
if ( text === '' ) {
log('No filterset found');
return;
}
const details = await dnrRulesetFromRawLists(lists, {
env: [ 'chromium' ],
});
const details = await dnrRulesetFromRawLists([ { name: assetDetails.id, text } ], { env });
const { ruleset: rules } = details;
log(`Input filter count: ${details.filterCount}`);
log(`\tAccepted filter count: ${details.acceptedFilterCount}`);
@ -215,17 +253,11 @@ async function main() {
true
);
writeOps.push(
writeFile(
`${rulesetDir}/${ruleset.id}.json`,
`${JSON.stringify(good, replacer, 2)}\n`
)
);
rulesetDetails.push({
id: ruleset.id,
name: ruleset.name,
enabled: ruleset.enabled,
id: assetDetails.id,
name: assetDetails.name,
enabled: assetDetails.enabled,
lang: assetDetails.lang,
filters: {
total: details.filterCount,
accepted: details.acceptedFilterCount,
@ -236,24 +268,100 @@ async function main() {
accepted: good.length,
discarded: redirects.length + headers.length + removeparams.length,
rejected: bad.length,
regexes,
regexes: regexes.length,
},
});
writeOps.push(
writeFile(
`${rulesetDir}/${assetDetails.id}.json`,
`${JSON.stringify(good, replacer, 2)}\n`
)
);
regexRulesetDetails.set(assetDetails.id, regexes);
writeOps.push(
writeFile(
`${rulesetDir}/${assetDetails.id}.regexes.json`,
`${JSON.stringify(regexes, replacer, 2)}\n`
)
);
ruleResources.push({
id: ruleset.id,
enabled: ruleset.enabled,
path: `/rulesets/${ruleset.id}.json`
id: assetDetails.id,
enabled: assetDetails.enabled,
path: `/rulesets/${assetDetails.id}.json`
});
goodTotalCount += good.length;
maybeGoodTotalCount += regexes.length;
};
// Get assets.json content
const assets = await fs.readFile(
`./assets.json`,
{ encoding: 'utf8' }
).then(text =>
JSON.parse(text)
);
// Assemble all default lists as the default ruleset
const contentURLs = [];
for ( const asset of Object.values(assets) ) {
if ( asset.content !== 'filters' ) { continue; }
if ( asset.off === true ) { continue; }
const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0]
: asset.contentURL;
contentURLs.push(contentURL);
}
await rulesetFromURLS({
id: 'default',
name: 'Ads, trackers, miners, and more' ,
enabled: true,
urls: contentURLs,
});
// Regional rulesets
for ( const [ id, asset ] of Object.entries(assets) ) {
if ( asset.content !== 'filters' ) { continue; }
if ( asset.off !== true ) { continue; }
if ( typeof asset.lang !== 'string' ) { continue; }
const contentURL = Array.isArray(asset.contentURL)
? asset.contentURL[0]
: asset.contentURL;
await rulesetFromURLS({
id: id.toLowerCase(),
lang: asset.lang,
name: asset.title,
enabled: false,
urls: [ contentURL ],
});
}
// Handpicked rulesets
const handpicked = [ 'block-lan', 'dpollock-0' ];
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 ],
});
}
writeOps.push(
writeFile(
`${rulesetDir}/ruleset-details.js`,
`export default ${JSON.stringify(rulesetDetails, replacer, 2)};\n`
`${rulesetDir}/ruleset-details.json`,
`${JSON.stringify(rulesetDetails, replacer, 2)}\n`
)
);
@ -276,7 +384,7 @@ async function main() {
);
// Log results
await fs.writeFile(`${outputDir}/log.txt`, output.join('\n') + '\n');
await fs.writeFile(`${outputDir}/log.txt`, stdOutput.join('\n') + '\n');
}
main();

View File

@ -1,74 +0,0 @@
/*******************************************************************************
uBlock Origin - a browser extension to block requests.
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';
export default [
{
id: 'default',
name: 'Default: Ads and trackers',
enabled: true,
paths: [
],
urls: [
'https://ublockorigin.github.io/uAssets/filters/badware.txt',
'https://ublockorigin.github.io/uAssets/filters/filters.txt',
'https://ublockorigin.github.io/uAssets/filters/filters-2020.txt',
'https://ublockorigin.github.io/uAssets/filters/filters-2021.txt',
'https://ublockorigin.github.io/uAssets/filters/filters-2022.txt',
'https://ublockorigin.github.io/uAssets/filters/privacy.txt',
'https://ublockorigin.github.io/uAssets/filters/quick-fixes.txt',
'https://ublockorigin.github.io/uAssets/filters/resource-abuse.txt',
'https://ublockorigin.github.io/uAssets/filters/unbreak.txt',
'https://easylist.to/easylist/easylist.txt',
'https://easylist.to/easylist/easyprivacy.txt',
'https://pgl.yoyo.org/adservers/serverlist.php?hostformat=hosts&showintro=1&mimetype=plaintext',
]
},
{
id: 'DEU-0',
name: 'DEU: EasyList Germany',
enabled: false,
paths: [
],
urls: [
'https://easylist.to/easylistgermany/easylistgermany.txt',
]
},
{
id: 'RUS-0',
name: 'RUS: RU AdList',
enabled: false,
paths: [
],
urls: [
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/adservers.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/first_level.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/general_block.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/specific_antisocial.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/specific_block.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/specific_special.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/thirdparty.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/whitelist.txt',
'https://raw.githubusercontent.com/easylist/ruadlist/master/advblock/AWRL-non-sync.txt',
]
},
];

View File

@ -61,9 +61,9 @@
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/cloud-ui.js" type="module"></script>
<script src="js/1p-filters.js" type="module"></script>
</body>

View File

@ -78,10 +78,10 @@
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/3p-filters.js"></script>
<script src="js/cloud-ui.js" type="module"></script>
<script src="js/3p-filters.js" type="module"></script>
</body>
</html>

View File

@ -52,7 +52,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/about.js"></script>

View File

@ -34,7 +34,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/advanced-settings.js"></script>

View File

@ -43,7 +43,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/asset-viewer.js" type="module"></script>

View File

@ -2,7 +2,7 @@
<html>
<head>
<meta charset="utf-8">
<title>uBlock Origin</title>
<title>uBlock Origin Background Page</title>
</head>
<body>
<script src="lib/lz4/lz4-block-codec-any.js"></script>

View File

@ -40,7 +40,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard.js"></script>
</body>

View File

@ -49,7 +49,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/devtools.js" type="module"></script>

View File

@ -57,7 +57,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/document-blocked.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/document-blocked.js" type="module"></script>
</body>
</html>

View File

@ -58,9 +58,9 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/cloud-ui.js" type="module"></script>
<script src="js/dyna-rules.js" type="module"></script>
</body>

View File

@ -25,6 +25,7 @@
/******************************************************************************/
import { i18n$ } from './i18n.js';
import './codemirror/ubo-static-filtering.js';
/******************************************************************************/
@ -234,7 +235,7 @@ const startImportFilePicker = function() {
const exportUserFiltersToFile = function() {
const val = getEditorText();
if ( val === '' ) { return; }
const filename = vAPI.i18n('1pExportFilename')
const filename = i18n$('1pExportFilename')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_');
vAPI.download({

View File

@ -25,13 +25,12 @@
/******************************************************************************/
{
// >>>>> start of local scope
import { i18n, i18n$ } from './i18n.js';
/******************************************************************************/
const lastUpdateTemplateString = vAPI.i18n('3pLastUpdate');
const obsoleteTemplateString = vAPI.i18n('3pExternalListObsolete');
const lastUpdateTemplateString = i18n$('3pLastUpdate');
const obsoleteTemplateString = i18n$('3pExternalListObsolete');
const reValidExternalList = /^[a-z-]+:\/\/(?:\S+\/\S*|\/\S+)/m;
let listDetails = {};
@ -70,8 +69,8 @@ const renderNumber = function(value) {
const renderFilterLists = function(soft) {
const listGroupTemplate = uDom('#templates .groupEntry');
const listEntryTemplate = uDom('#templates .listEntry');
const listStatsTemplate = vAPI.i18n('3pListsOfBlockedHostsPerListStats');
const renderElapsedTimeToString = vAPI.i18n.renderElapsedTimeToString;
const listStatsTemplate = i18n$('3pListsOfBlockedHostsPerListStats');
const renderElapsedTimeToString = i18n.renderElapsedTimeToString;
const groupNames = new Map([ [ 'user', '' ] ]);
// Assemble a pretty list name if possible
@ -189,7 +188,7 @@ const renderFilterLists = function(soft) {
liGroup = listGroupTemplate.clone().nodeAt(0);
let groupName = groupNames.get(groupKey);
if ( groupName === undefined ) {
groupName = vAPI.i18n('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
groupName = i18n$('3pGroup' + groupKey.charAt(0).toUpperCase() + groupKey.slice(1));
groupNames.set(groupKey, groupName);
}
if ( groupName !== '' ) {
@ -289,7 +288,7 @@ const renderFilterLists = function(soft) {
uDom.nodeFromId('autoUpdate').checked =
listDetails.autoUpdate === true;
uDom.nodeFromId('listsOfBlockedHostsPrompt').textContent =
vAPI.i18n('3pListsOfBlockedHostsPrompt')
i18n$('3pListsOfBlockedHostsPrompt')
.replace(
'{{netFilterCount}}',
renderNumber(details.netFilterCount)
@ -362,7 +361,7 @@ const updateAssetStatus = function(details) {
'title',
lastUpdateTemplateString.replace(
'{{ago}}',
vAPI.i18n.renderElapsedTimeToString(Date.now())
i18n$.renderElapsedTimeToString(Date.now())
)
);
}
@ -711,7 +710,3 @@ uDom('#lists').on('click', '.listEntry label *', ev => {
renderFilterLists();
/******************************************************************************/
// <<<<< end of local scope
}

View File

@ -27,12 +27,13 @@ import cacheStorage from './cachestorage.js';
import logger from './logger.js';
import µb from './background.js';
import { StaticFilteringParser } from './static-filtering-parser.js';
import { i18n$ } from './i18n.js';
/******************************************************************************/
const reIsExternalPath = /^(?:[a-z-]+):\/\//;
const reIsUserAsset = /^user-/;
const errorCantConnectTo = vAPI.i18n('errorCantConnectTo');
const errorCantConnectTo = i18n$('errorCantConnectTo');
const assets = {};

View File

@ -23,6 +23,8 @@
'use strict';
import { i18n, i18n$ } from './i18n.js';
/******************************************************************************/
(( ) => {
@ -58,7 +60,7 @@ const fetchStorageUsed = async function() {
elem.classList.add('hide');
return;
}
const units = ' ' + vAPI.i18n('genericBytes');
const units = ' ' + i18n$('genericBytes');
elem.title = result.max.toLocaleString() + units;
const total = (result.total / result.max * 100).toFixed(1);
elem = elem.firstElementChild;
@ -206,7 +208,7 @@ const onInitialize = function(options) {
faIconsInit(widget);
vAPI.i18n.render(widget);
i18n.render(widget);
widget.classList.remove('hide');
uDom('#cloudPush').on('click', ( ) => { pushData(); });

View File

@ -24,6 +24,7 @@
/******************************************************************************/
import µb from './background.js';
import { i18n$ } from './i18n.js';
/******************************************************************************/
@ -134,28 +135,28 @@ const onEntryClicked = function(details, tab) {
const menuEntries = {
blockElement: {
id: 'uBlock0-blockElement',
title: vAPI.i18n('pickerContextMenuEntry'),
title: i18n$('pickerContextMenuEntry'),
contexts: [ 'all' ],
},
blockElementInFrame: {
id: 'uBlock0-blockElementInFrame',
title: vAPI.i18n('contextMenuBlockElementInFrame'),
title: i18n$('contextMenuBlockElementInFrame'),
contexts: [ 'frame' ],
},
blockResource: {
id: 'uBlock0-blockResource',
title: vAPI.i18n('pickerContextMenuEntry'),
title: i18n$('pickerContextMenuEntry'),
contexts: [ 'audio', 'frame', 'image', 'video' ],
},
subscribeToList: {
id: 'uBlock0-subscribeToList',
title: vAPI.i18n('contextMenuSubscribeToList'),
title: i18n$('contextMenuSubscribeToList'),
contexts: [ 'link' ],
targetUrlPatterns: [ 'abp:*', 'https://subscribe.adblockplus.org/*' ],
},
temporarilyAllowLargeMediaElements: {
id: 'uBlock0-temporarilyAllowLargeMediaElements',
title: vAPI.i18n('contextMenuTemporarilyAllowLargeMediaElements'),
title: i18n$('contextMenuTemporarilyAllowLargeMediaElements'),
contexts: [ 'all' ],
}
};

View File

@ -25,11 +25,6 @@
/******************************************************************************/
{
// >>>>> start of local scope
/******************************************************************************/
const discardUnsavedData = function(synchronous = false) {
const paneFrame = document.getElementById('iframe');
const paneWindow = paneFrame.contentWindow;
@ -148,8 +143,3 @@ if ( self.location.hash.slice(1) === 'no-dashboard.html' ) {
});
}
})();
/******************************************************************************/
// <<<<< end of local scope
}

View File

@ -25,7 +25,7 @@
/******************************************************************************/
(( ) => {
import { i18n$ } from './i18n.js';
/******************************************************************************/
@ -128,7 +128,7 @@ uDom.nodeFromId('why').textContent = details.fs;
if ( search === '' ) { return false; }
url.search = '';
const li = liFromParam(vAPI.i18n('docblockedNoParamsPrompt'), url.href);
const li = liFromParam(i18n$('docblockedNoParamsPrompt'), url.href);
parentNode.appendChild(li);
const params = new self.URLSearchParams(search);
@ -239,7 +239,3 @@ uDom('#proceed').on('click', ( ) => {
});
/******************************************************************************/
})();
/******************************************************************************/

View File

@ -28,6 +28,7 @@
import publicSuffixList from '../lib/publicsuffixlist/publicsuffixlist.js';
import { hostnameFromURI } from './uri-utils.js';
import { i18n$ } from './i18n.js';
import './codemirror/ubo-dynamic-filtering.js';
@ -80,13 +81,13 @@ let isCollapsed = false;
// reliably the default title attribute assigned by CodeMirror.
{
const i18nCommitStr = vAPI.i18n('rulesCommit');
const i18nRevertStr = vAPI.i18n('rulesRevert');
const i18nCommitStr = i18n$('rulesCommit');
const i18nRevertStr = i18n$('rulesRevert');
const commitArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy-reverse:not([title="' + i18nCommitStr + '"])';
const revertArrowSelector = '.CodeMirror-merge-copybuttons-left .CodeMirror-merge-copy:not([title="' + i18nRevertStr + '"])';
uDom.nodeFromSelector('.CodeMirror-merge-scrolllock')
.setAttribute('title', vAPI.i18n('genericMergeViewScrollLock'));
.setAttribute('title', i18n$('genericMergeViewScrollLock'));
const translate = function() {
let elems = document.querySelectorAll(commitArrowSelector);
@ -340,7 +341,7 @@ const startImportFilePicker = function() {
/******************************************************************************/
function exportUserRulesToFile() {
const filename = vAPI.i18n('rulesDefaultFileName')
const filename = i18n$('rulesDefaultFileName')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_');
vAPI.download({

View File

@ -23,270 +23,280 @@
/******************************************************************************/
// This file should always be included at the end of the `body` tag, so as
// to ensure all i18n targets are already loaded.
{
// >>>>> start of local scope
const i18n =
self.browser instanceof Object &&
self.browser instanceof Element === false
? self.browser.i18n
: self.chrome.i18n;
/******************************************************************************/
// https://github.com/gorhill/uBlock/issues/2084
// Anything else than <a>, <b>, <code>, <em>, <i>, and <span> will
// be rendered as plain text.
// For <a>, only href attribute must be present, and it MUST starts with
// `https://`, and includes no single- or double-quotes.
// No HTML entities are allowed, there is code to handle existing HTML
// entities already present in translation files until they are all gone.
const allowedTags = new Set([
'a',
'b',
'code',
'em',
'i',
'span',
'u',
]);
const expandHtmlEntities = (( ) => {
const entities = new Map([
// TODO: Remove quote entities once no longer present in translation
// files. Other entities must stay.
[ '&shy;', '\u00AD' ],
[ '&ldquo;', '“' ],
[ '&rdquo;', '”' ],
[ '&lsquo;', '' ],
[ '&rsquo;', '' ],
[ '&lt;', '<' ],
[ '&gt;', '>' ],
]);
const decodeEntities = match => {
return entities.get(match) || match;
};
return function(text) {
if ( text.indexOf('&') !== -1 ) {
text = text.replace(/&[a-z]+;/g, decodeEntities);
}
return text;
};
})();
const safeTextToTextNode = function(text) {
return document.createTextNode(expandHtmlEntities(text));
};
const sanitizeElement = function(node) {
if ( allowedTags.has(node.localName) === false ) { return null; }
node.removeAttribute('style');
let child = node.firstElementChild;
while ( child !== null ) {
const next = child.nextElementSibling;
if ( sanitizeElement(child) === null ) {
child.remove();
}
child = next;
}
return node;
};
const safeTextToDOM = function(text, parent) {
if ( text === '' ) { return; }
// Fast path (most common).
if ( text.indexOf('<') === -1 ) {
const toInsert = safeTextToTextNode(text);
let toReplace = parent.childCount !== 0
? parent.firstChild
: null;
while ( toReplace !== null ) {
if ( toReplace.nodeType === 3 && toReplace.nodeValue === '_' ) {
break;
}
toReplace = toReplace.nextSibling;
}
if ( toReplace !== null ) {
parent.replaceChild(toInsert, toReplace);
} else {
parent.appendChild(toInsert);
}
return;
}
// Slow path.
// `<p>` no longer allowed. Code below can be removed once all <p>'s are
// gone from translation files.
text = text.replace(/^<p>|<\/p>/g, '')
.replace(/<p>/g, '\n\n');
// Parse allowed HTML tags.
const domParser = new DOMParser();
const parsedDoc = domParser.parseFromString(text, 'text/html');
let node = parsedDoc.body.firstChild;
while ( node !== null ) {
const next = node.nextSibling;
switch ( node.nodeType ) {
case 1: // element
if ( sanitizeElement(node) === null ) { break; }
parent.appendChild(node);
break;
case 3: // text
parent.appendChild(node);
break;
default:
break;
}
node = next;
}
};
/******************************************************************************/
vAPI.i18n.safeTemplateToDOM = function(id, dict, parent) {
if ( parent === undefined ) {
parent = document.createDocumentFragment();
}
let textin = vAPI.i18n(id);
if ( textin === '' ) {
return parent;
}
if ( textin.indexOf('{{') === -1 ) {
safeTextToDOM(textin, parent);
return parent;
}
const re = /\{\{\w+\}\}/g;
let textout = '';
for (;;) {
let match = re.exec(textin);
if ( match === null ) {
textout += textin;
break;
}
textout += textin.slice(0, match.index);
let prop = match[0].slice(2, -2);
if ( dict.hasOwnProperty(prop) ) {
textout += dict[prop].replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
textout += prop;
}
textin = textin.slice(re.lastIndex);
}
safeTextToDOM(textout, parent);
return parent;
};
/******************************************************************************/
// Helper to deal with the i18n'ing of HTML files.
vAPI.i18n.render = function(context) {
const docu = document;
const root = context || docu;
for ( const elem of root.querySelectorAll('[data-i18n]') ) {
let text = vAPI.i18n(elem.getAttribute('data-i18n'));
if ( !text ) { continue; }
if ( text.indexOf('{{') === -1 ) {
safeTextToDOM(text, elem);
continue;
}
// Handle selector-based placeholders: these placeholders tell where
// existing child DOM element are to be positioned relative to the
// localized text nodes.
const parts = text.split(/(\{\{[^}]+\}\})/);
const fragment = document.createDocumentFragment();
let textBefore = '';
for ( let part of parts ) {
if ( part === '' ) { continue; }
if ( part.startsWith('{{') && part.endsWith('}}') ) {
// TODO: remove detection of ':' once it no longer appears
// in translation files.
const pos = part.indexOf(':');
if ( pos !== -1 ) {
part = part.slice(0, pos) + part.slice(-2);
}
const selector = part.slice(2, -2);
let node;
// Ideally, the i18n strings explicitly refer to the
// class of the element to insert. However for now we
// will create a class from what is currently found in
// the placeholder and first try to lookup the resulting
// selector. This way we don't have to revisit all
// translations just for the sake of declaring the proper
// selector in the placeholder field.
if ( selector.charCodeAt(0) !== 0x2E /* '.' */ ) {
node = elem.querySelector(`.${selector}`);
}
if ( node instanceof Element === false ) {
node = elem.querySelector(selector);
}
if ( node instanceof Element ) {
safeTextToDOM(textBefore, fragment);
fragment.appendChild(node);
textBefore = '';
continue;
}
}
textBefore += part;
}
if ( textBefore !== '' ) {
safeTextToDOM(textBefore, fragment);
}
elem.appendChild(fragment);
}
for ( const elem of root.querySelectorAll('[data-i18n-title]') ) {
const text = vAPI.i18n(elem.getAttribute('data-i18n-title'));
if ( !text ) { continue; }
elem.setAttribute('title', expandHtmlEntities(text));
}
for ( const elem of root.querySelectorAll('[placeholder]') ) {
elem.setAttribute(
'placeholder',
vAPI.i18n(elem.getAttribute('placeholder'))
);
}
for ( const elem of root.querySelectorAll('[data-i18n-tip]') ) {
const text = vAPI.i18n(elem.getAttribute('data-i18n-tip'))
.replace(/<br>/g, '\n')
.replace(/\n{3,}/g, '\n\n');
elem.setAttribute('data-tip', text);
if ( elem.getAttribute('aria-label') === 'data-tip' ) {
elem.setAttribute('aria-label', text);
}
}
};
vAPI.i18n.render();
/******************************************************************************/
vAPI.i18n.renderElapsedTimeToString = function(tstamp) {
let value = (Date.now() - tstamp) / 60000;
if ( value < 2 ) {
return vAPI.i18n('elapsedOneMinuteAgo');
}
if ( value < 60 ) {
return vAPI.i18n('elapsedManyMinutesAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 60;
if ( value < 2 ) {
return vAPI.i18n('elapsedOneHourAgo');
}
if ( value < 24 ) {
return vAPI.i18n('elapsedManyHoursAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 24;
if ( value < 2 ) {
return vAPI.i18n('elapsedOneDayAgo');
}
return vAPI.i18n('elapsedManyDaysAgo').replace('{{value}}', Math.floor(value).toLocaleString());
};
/******************************************************************************/
// <<<<< end of local scope
function i18n$(...args) {
return i18n.getMessage(...args);
}
/******************************************************************************/
const isBackgroundProcess = document.title === 'uBlock Origin Background Page';
if ( isBackgroundProcess !== true ) {
// http://www.w3.org/International/questions/qa-scripts#directions
document.body.setAttribute(
'dir',
['ar', 'he', 'fa', 'ps', 'ur'].indexOf(i18n$('@@ui_locale')) !== -1
? 'rtl'
: 'ltr'
);
// https://github.com/gorhill/uBlock/issues/2084
// Anything else than <a>, <b>, <code>, <em>, <i>, and <span> will
// be rendered as plain text.
// For <a>, only href attribute must be present, and it MUST starts with
// `https://`, and includes no single- or double-quotes.
// No HTML entities are allowed, there is code to handle existing HTML
// entities already present in translation files until they are all gone.
const allowedTags = new Set([
'a',
'b',
'code',
'em',
'i',
'span',
'u',
]);
const expandHtmlEntities = (( ) => {
const entities = new Map([
// TODO: Remove quote entities once no longer present in translation
// files. Other entities must stay.
[ '&shy;', '\u00AD' ],
[ '&ldquo;', '“' ],
[ '&rdquo;', '”' ],
[ '&lsquo;', '' ],
[ '&rsquo;', '' ],
[ '&lt;', '<' ],
[ '&gt;', '>' ],
]);
const decodeEntities = match => {
return entities.get(match) || match;
};
return function(text) {
if ( text.indexOf('&') !== -1 ) {
text = text.replace(/&[a-z]+;/g, decodeEntities);
}
return text;
};
})();
const safeTextToTextNode = function(text) {
return document.createTextNode(expandHtmlEntities(text));
};
const sanitizeElement = function(node) {
if ( allowedTags.has(node.localName) === false ) { return null; }
node.removeAttribute('style');
let child = node.firstElementChild;
while ( child !== null ) {
const next = child.nextElementSibling;
if ( sanitizeElement(child) === null ) {
child.remove();
}
child = next;
}
return node;
};
const safeTextToDOM = function(text, parent) {
if ( text === '' ) { return; }
// Fast path (most common).
if ( text.indexOf('<') === -1 ) {
const toInsert = safeTextToTextNode(text);
let toReplace = parent.childCount !== 0
? parent.firstChild
: null;
while ( toReplace !== null ) {
if ( toReplace.nodeType === 3 && toReplace.nodeValue === '_' ) {
break;
}
toReplace = toReplace.nextSibling;
}
if ( toReplace !== null ) {
parent.replaceChild(toInsert, toReplace);
} else {
parent.appendChild(toInsert);
}
return;
}
// Slow path.
// `<p>` no longer allowed. Code below can be removed once all <p>'s are
// gone from translation files.
text = text.replace(/^<p>|<\/p>/g, '')
.replace(/<p>/g, '\n\n');
// Parse allowed HTML tags.
const domParser = new DOMParser();
const parsedDoc = domParser.parseFromString(text, 'text/html');
let node = parsedDoc.body.firstChild;
while ( node !== null ) {
const next = node.nextSibling;
switch ( node.nodeType ) {
case 1: // element
if ( sanitizeElement(node) === null ) { break; }
parent.appendChild(node);
break;
case 3: // text
parent.appendChild(node);
break;
default:
break;
}
node = next;
}
};
i18n.safeTemplateToDOM = function(id, dict, parent) {
if ( parent === undefined ) {
parent = document.createDocumentFragment();
}
let textin = i18n$(id);
if ( textin === '' ) {
return parent;
}
if ( textin.indexOf('{{') === -1 ) {
safeTextToDOM(textin, parent);
return parent;
}
const re = /\{\{\w+\}\}/g;
let textout = '';
for (;;) {
let match = re.exec(textin);
if ( match === null ) {
textout += textin;
break;
}
textout += textin.slice(0, match.index);
let prop = match[0].slice(2, -2);
if ( dict.hasOwnProperty(prop) ) {
textout += dict[prop].replace(/</g, '&lt;')
.replace(/>/g, '&gt;');
} else {
textout += prop;
}
textin = textin.slice(re.lastIndex);
}
safeTextToDOM(textout, parent);
return parent;
};
// Helper to deal with the i18n'ing of HTML files.
i18n.render = function(context) {
const docu = document;
const root = context || docu;
for ( const elem of root.querySelectorAll('[data-i18n]') ) {
let text = i18n$(elem.getAttribute('data-i18n'));
if ( !text ) { continue; }
if ( text.indexOf('{{') === -1 ) {
safeTextToDOM(text, elem);
continue;
}
// Handle selector-based placeholders: these placeholders tell where
// existing child DOM element are to be positioned relative to the
// localized text nodes.
const parts = text.split(/(\{\{[^}]+\}\})/);
const fragment = document.createDocumentFragment();
let textBefore = '';
for ( let part of parts ) {
if ( part === '' ) { continue; }
if ( part.startsWith('{{') && part.endsWith('}}') ) {
// TODO: remove detection of ':' once it no longer appears
// in translation files.
const pos = part.indexOf(':');
if ( pos !== -1 ) {
part = part.slice(0, pos) + part.slice(-2);
}
const selector = part.slice(2, -2);
let node;
// Ideally, the i18n strings explicitly refer to the
// class of the element to insert. However for now we
// will create a class from what is currently found in
// the placeholder and first try to lookup the resulting
// selector. This way we don't have to revisit all
// translations just for the sake of declaring the proper
// selector in the placeholder field.
if ( selector.charCodeAt(0) !== 0x2E /* '.' */ ) {
node = elem.querySelector(`.${selector}`);
}
if ( node instanceof Element === false ) {
node = elem.querySelector(selector);
}
if ( node instanceof Element ) {
safeTextToDOM(textBefore, fragment);
fragment.appendChild(node);
textBefore = '';
continue;
}
}
textBefore += part;
}
if ( textBefore !== '' ) {
safeTextToDOM(textBefore, fragment);
}
elem.appendChild(fragment);
}
for ( const elem of root.querySelectorAll('[data-i18n-title]') ) {
const text = i18n$(elem.getAttribute('data-i18n-title'));
if ( !text ) { continue; }
elem.setAttribute('title', expandHtmlEntities(text));
}
for ( const elem of root.querySelectorAll('[placeholder]') ) {
elem.setAttribute(
'placeholder',
i18n$(elem.getAttribute('placeholder'))
);
}
for ( const elem of root.querySelectorAll('[data-i18n-tip]') ) {
const text = i18n$(elem.getAttribute('data-i18n-tip'))
.replace(/<br>/g, '\n')
.replace(/\n{3,}/g, '\n\n');
elem.setAttribute('data-tip', text);
if ( elem.getAttribute('aria-label') === 'data-tip' ) {
elem.setAttribute('aria-label', text);
}
}
};
i18n.renderElapsedTimeToString = function(tstamp) {
let value = (Date.now() - tstamp) / 60000;
if ( value < 2 ) {
return i18n$('elapsedOneMinuteAgo');
}
if ( value < 60 ) {
return i18n$('elapsedManyMinutesAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 60;
if ( value < 2 ) {
return i18n$('elapsedOneHourAgo');
}
if ( value < 24 ) {
return i18n$('elapsedManyHoursAgo').replace('{{value}}', Math.floor(value).toLocaleString());
}
value /= 24;
if ( value < 2 ) {
return i18n$('elapsedOneDayAgo');
}
return i18n$('elapsedManyDaysAgo').replace('{{value}}', Math.floor(value).toLocaleString());
};
i18n.render();
}
/******************************************************************************/
export { i18n, i18n$ };

View File

@ -26,6 +26,7 @@
/******************************************************************************/
import { hostnameFromURI } from './uri-utils.js';
import { i18n, i18n$ } from './i18n.js';
/******************************************************************************/
@ -869,7 +870,7 @@ const viewPort = (( ) => {
/******************************************************************************/
const updateCurrentTabTitle = (( ) => {
const i18nCurrentTab = vAPI.i18n('loggerCurrentTab');
const i18nCurrentTab = i18n$('loggerCurrentTab');
return function() {
const select = uDom.nodeFromId('pageSelector');
@ -1572,7 +1573,7 @@ const reloadTab = function(ev) {
}
// https://github.com/gorhill/uBlock/issues/2179
if ( rows[1].children[1].childElementCount === 0 ) {
vAPI.i18n.safeTemplateToDOM(
i18n.safeTemplateToDOM(
'loggerStaticFilteringFinderSentence2',
{ filter: rawFilter },
rows[1].children[1]
@ -1728,7 +1729,7 @@ const reloadTab = function(ev) {
};
const fillOriginSelect = function(select, hostname, domain) {
const template = vAPI.i18n('loggerStaticFilteringSentencePartOrigin');
const template = i18n$('loggerStaticFilteringSentencePartOrigin');
let value = hostname;
for (;;) {
const option = document.createElement('option');
@ -1748,7 +1749,7 @@ const reloadTab = function(ev) {
return;
}
const template = vAPI.i18n('loggerStaticFilteringSentence');
const template = i18n$('loggerStaticFilteringSentence');
const rePlaceholder = /\{\{[^}]+?\}\}/g;
const nodes = [];
let pos = 0;
@ -1770,11 +1771,11 @@ const reloadTab = function(ev) {
select.className = 'static action';
option = document.createElement('option');
option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartBlock');
option.textContent = i18n$('loggerStaticFilteringSentencePartBlock');
select.appendChild(option);
option = document.createElement('option');
option.setAttribute('value', '@@');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartAllow');
option.textContent = i18n$('loggerStaticFilteringSentencePartAllow');
select.appendChild(option);
nodes.push(select);
break;
@ -1785,11 +1786,11 @@ const reloadTab = function(ev) {
select.className = 'static type';
option = document.createElement('option');
option.setAttribute('value', filterType);
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartType').replace('{{type}}', filterType);
option.textContent = i18n$('loggerStaticFilteringSentencePartType').replace('{{type}}', filterType);
select.appendChild(option);
option = document.createElement('option');
option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartAnyType');
option.textContent = i18n$('loggerStaticFilteringSentencePartAnyType');
select.appendChild(option);
nodes.push(select);
break;
@ -1813,7 +1814,7 @@ const reloadTab = function(ev) {
fillOriginSelect(select, targetFrameHostname, targetFrameDomain);
option = document.createElement('option');
option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartAnyOrigin');
option.textContent = i18n$('loggerStaticFilteringSentencePartAnyOrigin');
select.appendChild(option);
nodes.push(select);
break;
@ -1823,11 +1824,11 @@ const reloadTab = function(ev) {
select.className = 'static importance';
option = document.createElement('option');
option.setAttribute('value', '');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartNotImportant');
option.textContent = i18n$('loggerStaticFilteringSentencePartNotImportant');
select.appendChild(option);
option = document.createElement('option');
option.setAttribute('value', 'important');
option.textContent = vAPI.i18n('loggerStaticFilteringSentencePartImportant');
option.textContent = i18n$('loggerStaticFilteringSentencePartImportant');
select.appendChild(option);
nodes.push(select);
break;

View File

@ -40,6 +40,7 @@ import µb from './background.js';
import webRequest from './traffic.js';
import { denseBase64 } from './base64-custom.js';
import { dnrRulesetFromRawLists } from './static-dnr-filtering.js';
import { i18n$ } from './i18n.js';
import { redirectEngine } from './redirect-engine.js';
import { StaticFilteringParser } from './static-filtering-parser.js';
@ -1075,7 +1076,7 @@ const backupUserData = async function() {
userFilters: userFilters.content,
};
const filename = vAPI.i18n('aboutBackupFilename')
const filename = i18n$('aboutBackupFilename')
.replace('{{datetime}}', µb.dateNowToSensibleString())
.replace(/ +/g, '_');
µb.restoreBackupSettings.lastBackupFile = filename;
@ -1316,7 +1317,7 @@ const getShortcuts = function(callback) {
let desc = command.description;
let match = /^__MSG_(.+?)__$/.exec(desc);
if ( match !== null ) {
desc = vAPI.i18n(match[1]);
desc = i18n$(match[1]);
}
if ( desc === '' ) { continue; }
command.description = desc;

View File

@ -24,6 +24,7 @@
'use strict';
import punycode from '../lib/punycode.js';
import { i18n$ } from './i18n.js';
/******************************************************************************/
@ -50,8 +51,8 @@ const scopeToSrcHostnameMap = {
'.': ''
};
const hostnameToSortableTokenMap = new Map();
const statsStr = vAPI.i18n('popupBlockedStats');
const domainsHitStr = vAPI.i18n('popupHitDomainCount');
const statsStr = i18n$('popupBlockedStats');
const domainsHitStr = i18n$('popupHitDomainCount');
let popupData = {};
let dfPaneBuilt = false;
@ -643,7 +644,7 @@ const renderTooltips = function(selector) {
if ( selector !== undefined && key !== selector ) { continue; }
const elem = uDom.nodeFromSelector(key);
if ( elem.hasAttribute('title') === false ) { continue; }
const text = vAPI.i18n(
const text = i18n$(
details.i18n +
(uDom.nodeFromSelector(details.state) === null ? '1' : '2')
);

View File

@ -27,6 +27,7 @@ import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js';
import { CompiledListWriter } from './static-filtering-io.js';
import { StaticFilteringParser } from './static-filtering-parser.js';
import { i18n$ } from './i18n.js';
import {
domainFromHostname,
@ -110,7 +111,7 @@ const initWorker = function() {
entries.set(listKey, {
title: listKey !== µb.userFiltersPath ?
entry.title :
vAPI.i18n('1pPageName'),
i18n$('1pPageName'),
supportURL: entry.supportURL || ''
});
}

View File

@ -23,9 +23,7 @@
'use strict';
/******************************************************************************/
(( ) => {
import { i18n$ } from './i18n.js';
/******************************************************************************/
@ -63,11 +61,11 @@ const handleImportFilePicker = function() {
userData = undefined;
}
if ( userData === undefined ) {
window.alert(vAPI.i18n('aboutRestoreDataError'));
window.alert(i18n$('aboutRestoreDataError'));
return;
}
const time = new Date(userData.timeStamp);
const msg = vAPI.i18n('aboutRestoreDataConfirm')
const msg = i18n$('aboutRestoreDataConfirm')
.replace('{{time}}', time.toLocaleString());
const proceed = window.confirm(msg);
if ( proceed !== true ) { return; }
@ -137,9 +135,9 @@ const onLocalDataReceived = function(details) {
unit = '';
}
uDom.nodeFromId('storageUsed').textContent =
vAPI.i18n('storageUsed')
i18n$('storageUsed')
.replace('{{value}}', v.toLocaleString(undefined, { maximumSignificantDigits: 3 }))
.replace('{{unit}}', unit && vAPI.i18n(unit) || '');
.replace('{{unit}}', unit && i18n$(unit) || '');
const timeOptions = {
weekday: 'long',
@ -154,7 +152,7 @@ const onLocalDataReceived = function(details) {
const lastBackupFile = details.lastBackupFile || '';
if ( lastBackupFile !== '' ) {
const dt = new Date(details.lastBackupTime);
const text = vAPI.i18n('settingsLastBackupPrompt');
const text = i18n$('settingsLastBackupPrompt');
const node = uDom.nodeFromId('settingsLastBackupPrompt');
node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions);
node.style.display = '';
@ -163,7 +161,7 @@ const onLocalDataReceived = function(details) {
const lastRestoreFile = details.lastRestoreFile || '';
if ( lastRestoreFile !== '' ) {
const dt = new Date(details.lastRestoreTime);
const text = vAPI.i18n('settingsLastRestorePrompt');
const text = i18n$('settingsLastRestorePrompt');
const node = uDom.nodeFromId('settingsLastRestorePrompt');
node.textContent = text + '\xA0' + dt.toLocaleString('fullwide', timeOptions);
node.style.display = '';
@ -183,7 +181,7 @@ const onLocalDataReceived = function(details) {
/******************************************************************************/
const resetUserData = function() {
const msg = vAPI.i18n('aboutResetDataConfirm');
const msg = i18n$('aboutResetDataConfirm');
const proceed = window.confirm(msg);
if ( proceed !== true ) { return; }
vAPI.messaging.send('dashboard', {
@ -306,5 +304,3 @@ document.querySelector(
);
/******************************************************************************/
})();

View File

@ -91,7 +91,11 @@ async function dnrRulesetFromRawLists(lists, options = {}) {
const toLoad = [];
const toDNR = (context, list) => addToDNR(context, list);
for ( const list of lists ) {
toLoad.push(list.then(list => toDNR(context, list)));
if ( list instanceof Promise ) {
toLoad.push(list.then(list => toDNR(context, list)));
} else {
toLoad.push(toDNR(context, list));
}
}
await Promise.all(toLoad);
return staticNetFilteringEngine.dnrFromCompiled('end', context);

View File

@ -3176,10 +3176,17 @@ Parser.utils = Parser.prototype.utils = (( ) => {
[ 'adguard_ext_safari', 'false' ],
]);
const toURL = url => {
try {
return new URL(url.trim());
} catch (ex) {
}
};
class preparser {
// This method returns an array of indices, corresponding to position in
// the content string which should alternatively be parsed and discarded.
static splitter(content, env) {
static splitter(content, env = []) {
const reIf = /^!#(if|endif)\b([^\n]*)(?:[\n\r]+|$)/gm;
const stack = [];
const shouldDiscard = ( ) => stack.some(v => v);
@ -3205,7 +3212,6 @@ Parser.utils = Parser.prototype.utils = (( ) => {
}
stack.push(startDiscard);
break;
case 'endif':
stack.pop();
const stopDiscard = shouldDiscard() === false;
@ -3214,7 +3220,6 @@ Parser.utils = Parser.prototype.utils = (( ) => {
discard = false;
}
break;
default:
break;
}
@ -3224,6 +3229,47 @@ Parser.utils = Parser.prototype.utils = (( ) => {
return parts;
}
static expandIncludes(parts, env = []) {
const out = [];
const reInclude = /^!#include +(\S+)[^\n\r]*(?:[\n\r]+|$)/gm;
for ( const part of parts ) {
if ( typeof part === 'string' ) {
out.push(part);
continue;
}
if ( part instanceof Object === false ) { continue; }
const content = part.content;
const slices = this.splitter(content, env);
for ( let i = 0, n = slices.length - 1; i < n; i++ ) {
const slice = content.slice(slices[i+0], slices[i+1]);
if ( (i & 1) !== 0 ) {
out.push(slice);
continue;
}
let lastIndex = 0;
for (;;) {
const match = reInclude.exec(slice);
if ( match === null ) { break; }
if ( toURL(match[1]) !== undefined ) { continue; }
if ( match[1].indexOf('..') !== -1 ) { continue; }
// Compute nested list path relative to parent list path
const pos = part.url.lastIndexOf('/');
if ( pos === -1 ) { continue; }
const subURL = part.url.slice(0, pos + 1) + match[1].trim();
out.push(
slice.slice(lastIndex, match.index + match[0].length),
`! >>>>>>>> ${subURL}\n`,
{ url: subURL },
`! <<<<<<<< ${subURL}\n`
);
lastIndex = reInclude.lastIndex;
}
out.push(lastIndex === 0 ? slice : slice.slice(lastIndex));
}
}
return out;
}
static prune(content, env) {
const parts = this.splitter(content, env);
const out = [];

View File

@ -37,6 +37,7 @@ import staticFilteringReverseLookup from './reverselookup.js';
import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js';
import { hostnameFromURI } from './uri-utils.js';
import { i18n, i18n$ } from './i18n.js';
import { redirectEngine } from './redirect-engine.js';
import { sparseBase64 } from './base64-custom.js';
import { StaticFilteringParser } from './static-filtering-parser.js';
@ -625,7 +626,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
newAvailableLists[this.userFiltersPath] = {
content: 'filters',
group: 'user',
title: vAPI.i18n('1pPageName'),
title: i18n$('1pPageName'),
};
// Custom filter lists.
@ -1392,7 +1393,7 @@ self.addEventListener('hiddenSettingsChanged', ( ) => {
if ( typeof details.lang === 'string' ) {
let re = this.listMatchesEnvironment.reLang;
if ( re === undefined ) {
const match = /^[a-z]+/.exec(browser.i18n.getUILanguage());
const match = /^[a-z]+/.exec(i18n.getUILanguage());
if ( match !== null ) {
re = new RegExp('\\b' + match[0] + '\\b');
this.listMatchesEnvironment.reLang = re;

View File

@ -30,6 +30,7 @@ import staticNetFilteringEngine from './static-net-filtering.js';
import µb from './background.js';
import webext from './webext.js';
import { PageStore } from './pagestore.js';
import { i18n$ } from './i18n.js';
import {
sessionFirewall,
@ -1052,7 +1053,7 @@ vAPI.tabs = new vAPI.Tabs();
};
const pageStore = new NoPageStore(vAPI.noTabId);
µb.pageStores.set(pageStore.tabId, pageStore);
pageStore.title = vAPI.i18n('logBehindTheScene');
pageStore.title = i18n$('logBehindTheScene');
}
/******************************************************************************/

View File

@ -23,9 +23,7 @@
'use strict';
/******************************************************************************/
(( ) => {
import { i18n$ } from './i18n.js';
/******************************************************************************/
@ -204,7 +202,7 @@ const exportWhitelistToFile = function() {
const val = getEditorText();
if ( val === '' ) { return; }
const filename =
vAPI.i18n('whitelistExportFilename')
i18n$('whitelistExportFilename')
.replace('{{datetime}}', uBlockDashboard.dateNowToSensibleString())
.replace(/ +/g, '_');
vAPI.download({
@ -262,5 +260,3 @@ uDom('#whitelistRevert').on('click', revertChanges);
renderWhitelist();
/******************************************************************************/
})();

View File

@ -213,7 +213,7 @@
<script src="js/vapi-client.js"></script>
<script src="js/vapi-client-extra.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/logger-ui.js" type="module"></script>
<script src="js/logger-ui-inspector.js" type="module"></script>

View File

@ -21,7 +21,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
</body>
</html>

View File

@ -101,7 +101,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/popup-fenix.js" type="module"></script>
</body>

View File

@ -90,9 +90,9 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/settings.js"></script>
<script src="js/settings.js" type="module"></script>
</body>
</html>

View File

@ -32,7 +32,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/shortcuts.js"></script>

View File

@ -112,7 +112,7 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/support.js"></script>

View File

@ -69,7 +69,7 @@
<script src="../js/vapi-client.js"></script>
<script src="../js/vapi-client-extra.js"></script>
<script src="../js/udom.js"></script>
<script src="../js/i18n.js"></script>
<script src="../js/i18n.js" type="module"></script>
<script src="../js/epicker-ui.js" type="module"></script>
</body>

View File

@ -53,10 +53,10 @@
<script src="js/vapi-common.js"></script>
<script src="js/vapi-client.js"></script>
<script src="js/udom.js"></script>
<script src="js/i18n.js"></script>
<script src="js/i18n.js" type="module"></script>
<script src="js/dashboard-common.js"></script>
<script src="js/cloud-ui.js"></script>
<script src="js/whitelist.js"></script>
<script src="js/cloud-ui.js" type="module"></script>
<script src="js/whitelist.js" type="module"></script>
</body>
</html>

View File

@ -7,72 +7,35 @@ set -e
echo "*** uBlock: Importing from Crowdin archive"
SRC=~/Downloads/crowdin
rm -r $SRC || true
rm -r $SRC || true > /dev/null
unzip -q ~/Downloads/uBlock\ \(translations\).zip -d $SRC
# https://www.assertnotmagic.com/2018/06/20/bash-brackets-quick-reference/
DES=./src/_locales
cp $SRC/ar/messages.json $DES/ar/messages.json
cp $SRC/az/messages.json $DES/az/messages.json
cp $SRC/bg/messages.json $DES/bg/messages.json
cp $SRC/bn/messages.json $DES/bn/messages.json
cp $SRC/bs/messages.json $DES/bs/messages.json
cp $SRC/ca/messages.json $DES/ca/messages.json
cp $SRC/cs/messages.json $DES/cs/messages.json
cp $SRC/cv/messages.json $DES/cv/messages.json
cp $SRC/da/messages.json $DES/da/messages.json
cp $SRC/de/messages.json $DES/de/messages.json
cp $SRC/el/messages.json $DES/el/messages.json
cp $SRC/en-GB/messages.json $DES/en_GB/messages.json
cp $SRC/eo/messages.json $DES/eo/messages.json
cp $SRC/es-ES/messages.json $DES/es/messages.json
cp $SRC/et/messages.json $DES/et/messages.json
cp $SRC/eu/messages.json $DES/eu/messages.json
cp $SRC/fa/messages.json $DES/fa/messages.json
cp $SRC/fi/messages.json $DES/fi/messages.json
cp $SRC/fil/messages.json $DES/fil/messages.json
cp $SRC/fr/messages.json $DES/fr/messages.json
cp $SRC/fy-NL/messages.json $DES/fy/messages.json
cp $SRC/gl/messages.json $DES/gl/messages.json
cp $SRC/he/messages.json $DES/he/messages.json
cp $SRC/hi/messages.json $DES/hi/messages.json
cp $SRC/hr/messages.json $DES/hr/messages.json
cp $SRC/hu/messages.json $DES/hu/messages.json
cp $SRC/hy-AM/messages.json $DES/hy/messages.json
cp $SRC/id/messages.json $DES/id/messages.json
cp $SRC/it/messages.json $DES/it/messages.json
cp $SRC/ja/messages.json $DES/ja/messages.json
cp $SRC/ka/messages.json $DES/ka/messages.json
cp $SRC/kk/messages.json $DES/kk/messages.json
cp $SRC/kn/messages.json $DES/kn/messages.json
cp $SRC/ko/messages.json $DES/ko/messages.json
cp $SRC/lt/messages.json $DES/lt/messages.json
cp $SRC/lv/messages.json $DES/lv/messages.json
cp $SRC/ml-IN/messages.json $DES/ml/messages.json
cp $SRC/mr/messages.json $DES/mr/messages.json
cp $SRC/ms/messages.json $DES/ms/messages.json
cp $SRC/nb/messages.json $DES/nb/messages.json
cp $SRC/nl/messages.json $DES/nl/messages.json
cp $SRC/oc/messages.json $DES/oc/messages.json
cp $SRC/pl/messages.json $DES/pl/messages.json
cp $SRC/pt-BR/messages.json $DES/pt_BR/messages.json
cp $SRC/pt-PT/messages.json $DES/pt_PT/messages.json
cp $SRC/ro/messages.json $DES/ro/messages.json
cp $SRC/ru/messages.json $DES/ru/messages.json
cp $SRC/sk/messages.json $DES/sk/messages.json
cp $SRC/sl/messages.json $DES/sl/messages.json
cp $SRC/so/messages.json $DES/so/messages.json
cp $SRC/sq/messages.json $DES/sq/messages.json
cp $SRC/sr/messages.json $DES/sr/messages.json
cp $SRC/sv-SE/messages.json $DES/sv/messages.json
cp $SRC/ta/messages.json $DES/ta/messages.json
cp $SRC/te/messages.json $DES/te/messages.json
cp $SRC/th/messages.json $DES/th/messages.json
cp $SRC/tr/messages.json $DES/tr/messages.json
cp $SRC/ur-PK/messages.json $DES/ur/messages.json
cp $SRC/uk/messages.json $DES/uk/messages.json
cp $SRC/vi/messages.json $DES/vi/messages.json
cp $SRC/zh-CN/messages.json $DES/zh_CN/messages.json
cp $SRC/zh-TW/messages.json $DES/zh_TW/messages.json
DESMV3=./platform/mv3/extension/_locales
for dir in $SRC/*/; do
srclang=$(basename $dir)
deslang=${srclang/-/_}
deslang=${deslang%_AM}
deslang=${deslang%_ES}
deslang=${deslang%_IE}
deslang=${deslang%_IN}
deslang=${deslang%_LK}
deslang=${deslang%_NL}
deslang=${deslang%_PK}
deslang=${deslang%_SE}
if [[ $deslang == 'en' ]]; then
continue
fi
# ubo
mkdir -p "$DES/$deslang/" && cp "$SRC/$srclang/messages.json" "$DES/$deslang/"
# ubo lite
mkdir -p "$DESMV3/$deslang/" && cp "$SRC/$srclang/uBO Lite/messages.json" "$DESMV3/$deslang/"
# descriptions
cp "$SRC/$srclang/description.txt" "./dist/description/description-${deslang}.txt"
done
# Output files with possible misuse of `$`, as this can lead to severe
# consequences, such as not being able to run the extension at all.
@ -80,71 +43,8 @@ cp $SRC/zh-TW/messages.json $DES/zh_TW/messages.json
# See https://issues.adblockplus.org/ticket/6666
echo "*** uBlock: Instances of '\$':"
grep -FR "$" $DES/ || true
grep -FR "$" $DESMV3/ || true
#
DES=./dist/description
cp $SRC/ar/description.txt $DES/description-ar.txt
cp $SRC/bg/description.txt $DES/description-bg.txt
cp $SRC/bn/description.txt $DES/description-bn.txt
cp $SRC/bs/description.txt $DES/description-bs.txt
cp $SRC/ca/description.txt $DES/description-ca.txt
cp $SRC/cs/description.txt $DES/description-cs.txt
cp $SRC/cv/description.txt $DES/description-cv.txt
cp $SRC/da/description.txt $DES/description-da.txt
cp $SRC/de/description.txt $DES/description-de.txt
cp $SRC/el/description.txt $DES/description-el.txt
cp $SRC/en-GB/description.txt $DES/description-en_GB.txt
cp $SRC/eo/description.txt $DES/description-eo.txt
cp $SRC/es-ES/description.txt $DES/description-es.txt
cp $SRC/et/description.txt $DES/description-et.txt
cp $SRC/eu/description.txt $DES/description-eu.txt
cp $SRC/fa/description.txt $DES/description-fa.txt
cp $SRC/fi/description.txt $DES/description-fi.txt
cp $SRC/fil/description.txt $DES/description-fil.txt
cp $SRC/fr/description.txt $DES/description-fr.txt
cp $SRC/fy-NL/description.txt $DES/description-fy.txt
cp $SRC/gl/description.txt $DES/description-gl.txt
cp $SRC/he/description.txt $DES/description-he.txt
cp $SRC/hi/description.txt $DES/description-hi.txt
cp $SRC/hr/description.txt $DES/description-hr.txt
cp $SRC/hu/description.txt $DES/description-hu.txt
cp $SRC/hy-AM/description.txt $DES/description-hy.txt
cp $SRC/id/description.txt $DES/description-id.txt
cp $SRC/it/description.txt $DES/description-it.txt
cp $SRC/ja/description.txt $DES/description-ja.txt
cp $SRC/ka/description.txt $DES/description-ka.txt
cp $SRC/kk/description.txt $DES/description-kk.txt
cp $SRC/ko/description.txt $DES/description-ko.txt
cp $SRC/kn/description.txt $DES/description-kn.txt
cp $SRC/lt/description.txt $DES/description-lt.txt
cp $SRC/lv/description.txt $DES/description-lv.txt
cp $SRC/ml-IN/description.txt $DES/description-ml.txt
cp $SRC/ms/description.txt $DES/description-ms.txt
cp $SRC/mr/description.txt $DES/description-mr.txt
cp $SRC/nb/description.txt $DES/description-nb.txt
cp $SRC/nl/description.txt $DES/description-nl.txt
cp $SRC/oc/description.txt $DES/description-oc.txt
cp $SRC/pl/description.txt $DES/description-pl.txt
cp $SRC/pt-BR/description.txt $DES/description-pt_BR.txt
cp $SRC/pt-PT/description.txt $DES/description-pt_PT.txt
cp $SRC/ro/description.txt $DES/description-ro.txt
cp $SRC/ru/description.txt $DES/description-ru.txt
cp $SRC/sk/description.txt $DES/description-sk.txt
cp $SRC/sl/description.txt $DES/description-sl.txt
cp $SRC/sq/description.txt $DES/description-sq.txt
cp $SRC/sr/description.txt $DES/description-sr.txt
cp $SRC/sv-SE/description.txt $DES/description-sv.txt
cp $SRC/ta/description.txt $DES/description-ta.txt
cp $SRC/te/description.txt $DES/description-te.txt
cp $SRC/tr/description.txt $DES/description-tr.txt
cp $SRC/ur-PK/description.txt $DES/description-ur.txt
cp $SRC/uk/description.txt $DES/description-uk.txt
cp $SRC/vi/description.txt $DES/description-vi.txt
cp $SRC/zh-CN/description.txt $DES/description-zh_CN.txt
cp $SRC/zh-TW/description.txt $DES/description-zh_TW.txt
#
rm -r $SRC
echo "*** uBlock: Import done."

View File

@ -25,8 +25,12 @@ echo "*** uBlock0.mv3: Copying common files"
cp -R src/css/fonts/* $DES/css/fonts/
cp src/css/themes/default.css $DES/css/
cp src/css/common.css $DES/css/
cp src/css/dashboard.css $DES/css/
cp src/css/dashboard-common.css $DES/css/
cp src/css/fa-icons.css $DES/css/
cp src/js/fa-icons.js $DES/js/
cp src/js/i18n.js $DES/js/
cp LICENSE.txt $DES/
@ -35,6 +39,7 @@ cp platform/mv3/extension/*.html $DES/
cp platform/mv3/extension/css/* $DES/css/
cp platform/mv3/extension/js/* $DES/js/
cp platform/mv3/extension/img/* $DES/img/
cp -R platform/mv3/extension/_locales $DES/
if [ "$1" != "quick" ]; then
echo "*** uBlock0.mv3: Generating rulesets"
@ -44,6 +49,7 @@ if [ "$1" != "quick" ]; then
./tools/make-nodejs.sh $TMPDIR
cp platform/mv3/package.json $TMPDIR/
cp platform/mv3/*.js $TMPDIR/
cp assets/assets.json $TMPDIR/
cd $TMPDIR
node --no-warnings make-rulesets.js output=$DES quick=$QUICK
cd - > /dev/null