Compare commits

..

5 Commits

29 changed files with 1074 additions and 32 deletions

View File

@ -20,7 +20,6 @@
"@holllo/preact-components": "^0.2.3",
"htm": "^3.1.1",
"modern-normalize": "^1.1.0",
"nanoid": "^4.0.0",
"preact": "^10.11.0",
"webextension-polyfill": "^0.10.0"
},

View File

@ -10,7 +10,6 @@ specifiers:
c8: ^7.12.0
htm: ^3.1.1
modern-normalize: ^1.1.0
nanoid: ^4.0.0
postcss: ^8.4.16
preact: ^10.11.0
sass: ^1.55.0
@ -30,7 +29,6 @@ dependencies:
'@holllo/preact-components': 0.2.3_htm@3.1.1+preact@10.11.0
htm: 3.1.1
modern-normalize: 1.1.0
nanoid: 4.0.0
preact: 10.11.0
webextension-polyfill: 0.10.0
@ -5199,12 +5197,6 @@ packages:
hasBin: true
dev: true
/nanoid/4.0.0:
resolution: {integrity: sha512-IgBP8piMxe/gf73RTQx7hmnhwz0aaEXYakvqZyE302IXW3HyVNhdNGC+O2MwMAVhLEnvXlvKtGbtJf6wvHihCg==}
engines: {node: ^14 || ^16 || >=18}
hasBin: true
dev: false
/natural-compare/1.4.0:
resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==}
dev: true

View File

@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64">
<path fill="#e6deff" d="M0,16 l48,0 l-24,32" />
</svg>

After

Width:  |  Height:  |  Size: 121 B

View File

@ -1,6 +1,6 @@
import browser from 'webextension-polyfill';
import {parseRedirect} from '../redirect/exports.js';
import storage from '../redirect/storage.js';
async function browserActionClicked() {
await browser.runtime.openOptionsPage();
@ -35,11 +35,8 @@ browser.webNavigation.onBeforeNavigate.addListener(async (details) => {
return;
}
for (const [id, parameters] of Object.entries(
await browser.storage.local.get(),
)) {
const redirect = parseRedirect(parameters, id);
if (redirect === undefined || !redirect.parameters.enabled) {
for (const redirect of await storage.getRedirects()) {
if (!redirect.parameters.enabled) {
continue;
}

View File

@ -0,0 +1,208 @@
import {ConfirmButton} from '@holllo/preact-components';
import {html} from 'htm/preact';
import {Component} from 'preact';
import browser from 'webextension-polyfill';
import {
matcherTypes,
narrowMatcherType,
narrowRedirectType,
parseRedirect,
Redirect,
RedirectParameters,
redirectTypes,
} from '../../redirect/exports.js';
import storage from '../../redirect/storage.js';
type Props = {
redirect: Redirect;
removeRedirect: (id: number) => void;
saveRedirect: (redirect: Redirect) => void;
};
type State = {
hasBeenEdited: boolean;
} & RedirectParameters;
export default class Editor extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
hasBeenEdited: false,
...props.redirect.parameters,
};
}
onInput = (event: Event, input: 'matcher' | 'redirect') => {
const target = event.target as HTMLInputElement;
const value = target.value;
const newState: Partial<State> = {
hasBeenEdited: true,
};
if (input === 'matcher') {
newState.matcherValue = value;
} else if (input === 'redirect') {
newState.redirectValue = value;
} else {
throw new Error(`Unexpected input changed: ${input as string}`);
}
this.setState(newState);
};
onSelectChange = (event: Event, select: 'matcher' | 'redirect') => {
const target = event.target as HTMLSelectElement;
const value = target.value;
const newState: Partial<State> = {
hasBeenEdited: true,
};
if (select === 'matcher' && narrowMatcherType(value)) {
newState.matcherType = value;
} else if (select === 'redirect' && narrowRedirectType(value)) {
newState.redirectType = value;
} else {
throw new Error(`${value} is not a valid MatcherType or RedirectType`);
}
this.setState(newState);
};
onMatcherInput = (event: Event) => {
this.onInput(event, 'matcher');
};
onMatcherTypeChange = (event: Event) => {
this.onSelectChange(event, 'matcher');
};
onRedirectInput = (event: Event) => {
this.onInput(event, 'redirect');
};
onRedirectTypeChange = (event: Event) => {
this.onSelectChange(event, 'redirect');
};
parseRedirect = (): Redirect => {
const redirect = parseRedirect({
enabled: this.state.enabled,
id: this.state.id,
matcherType: this.state.matcherType,
matcherValue: this.state.matcherValue,
redirectType: this.state.redirectType,
redirectValue: this.state.redirectValue,
});
if (redirect === undefined) {
throw new Error('Failed to parse redirect');
}
return redirect;
};
remove = async () => {
const redirect = this.props.redirect;
await browser.storage.local.remove(redirect.idString());
this.props.removeRedirect(redirect.parameters.id);
};
save = async () => {
const redirect = this.parseRedirect();
await storage.save(redirect);
this.props.saveRedirect(redirect);
this.setState({hasBeenEdited: false});
};
toggleEnabled = async () => {
const enabled = !this.state.enabled;
const redirect = this.props.redirect;
const prepared = await storage.prepareForStorage(redirect);
prepared[redirect.idString()].enabled = enabled;
await browser.storage.local.set(prepared);
this.setState({enabled});
};
render() {
const {
enabled,
hasBeenEdited,
matcherType,
matcherValue,
redirectType,
redirectValue,
} = this.state;
const matcherTypeOptions = matcherTypes.map(
(value) => html`<option value=${value}>${value}</option>`,
);
const redirectTypeOptions = redirectTypes.map(
(value) => html`<option value=${value}>${value}</option>`,
);
return html`
<div class="editor ${hasBeenEdited ? 'has-been-edited' : ''}">
<select
class="select"
id="matcher-type"
title="Matcher Type"
value=${matcherType}
onChange=${this.onMatcherTypeChange}
>
${matcherTypeOptions}
</select>
<input
class="input"
id="matcher-value"
onInput=${this.onMatcherInput}
placeholder="Matcher Value"
title="Matcher Value"
value=${matcherValue}
/>
<span class="arrow-span"></span>
<select
class="select"
id="redirect-type"
title="Redirect Type"
value=${redirectType}
onChange=${this.onRedirectTypeChange}
>
${redirectTypeOptions}
</select>
<input
class="input"
id="redirect-value"
onInput=${this.onRedirectInput}
placeholder="Redirect Value"
title="Redirect Value"
value=${redirectValue}
/>
<${ConfirmButton}
attributes=${{title: 'Remove Redirect'}}
class="button destructive"
click=${this.remove}
confirmClass="confirm"
confirmText="✓"
text="✗"
timeout=${5 * 1000}
/>
<button class="button" title="Save Redirect" onClick=${this.save}>
💾
</button>
<button
class="button ${enabled ? 'enabled' : 'disabled'}"
title="${enabled ? 'Currently Enabled' : 'Currently Disabled'}"
onClick=${this.toggleEnabled}
>
${enabled ? '●' : '○'}
</button>
</div>
`;
}
}

View File

@ -0,0 +1,34 @@
import {PrivacyLink} from '@holllo/preact-components';
import {html} from 'htm/preact';
import {Component} from 'preact';
import browser from 'webextension-polyfill';
export class PageFooter extends Component {
render() {
const manifest = browser.runtime.getManifest();
const version = manifest.version;
const donateAttributes = {
href: 'https://liberapay.com/Holllo',
};
const donateLink = html`
<${PrivacyLink} attributes="${donateAttributes}">Donate<//>
`;
const versionLinkAttributes = {
href: `https://git.bauke.xyz/Holllo/re-nav/releases/tag/${version}`,
};
const versionLink = html`
<${PrivacyLink} attributes=${versionLinkAttributes}>v${version}<//>
`;
return html`
<footer class="page-footer">
<p>
${donateLink} 💖 ${versionLink} © Holllo Free and open-source,
forever.
</p>
</footer>
`;
}
}

View File

@ -0,0 +1,15 @@
import {html} from 'htm/preact';
import {Component} from 'preact';
export class PageHeader extends Component {
render() {
return html`
<header class="page-header">
<h1>
<img alt="Re-Nav Logo" src="/assets/re-nav.png" />
Re-Nav
</h1>
</header>
`;
}
}

View File

@ -0,0 +1,129 @@
import {html} from 'htm/preact';
import {Component} from 'preact';
import {Redirect, SimpleRedirect} from '../../redirect/exports.js';
import storage from '../../redirect/storage.js';
import Editor from './editor.js';
import Usage from './usage.js';
type Props = Record<string, unknown>;
type State = {
redirects: Redirect[];
};
export class PageMain extends Component<Props, State> {
constructor(props: Props) {
super(props);
this.state = {
redirects: [],
};
}
async componentDidMount() {
const redirects = await storage.getRedirects();
// Sort the redirects by:
// * Matcher Type
// * then Matcher Value
// * then Redirect Type
// * finally Redirect Value
redirects.sort((a, b) => {
const {
matcherType: mTypeA,
matcherValue: mValueA,
redirectType: rTypeA,
redirectValue: rValueA,
} = a.parameters;
const {
matcherType: mTypeB,
matcherValue: mValueB,
redirectType: rTypeB,
redirectValue: rValueB,
} = b.parameters;
if (mTypeA !== mTypeB) {
return mTypeA.localeCompare(mTypeB);
}
if (mValueA !== mValueB) {
return mValueA.localeCompare(mValueB);
}
if (rTypeA !== rTypeB) {
return rTypeA.localeCompare(rTypeB);
}
return rValueA.localeCompare(rValueB);
});
this.setState({redirects});
}
addNewRedirect = async () => {
const redirect = new SimpleRedirect({
enabled: true,
id: await storage.nextRedirectId(),
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'simple',
redirectValue: 'example.org',
});
await storage.savePrepared(await storage.prepareForStorage(redirect));
this.setState({
redirects: [redirect, ...this.state.redirects],
});
};
removeRedirect = (id: number) => {
this.setState({
redirects: this.state.redirects.filter(
(redirect) => redirect.parameters.id !== id,
),
});
};
saveRedirect = (redirect: Redirect) => {
const redirectIndex = this.state.redirects.findIndex(
(found) => found.parameters.id === redirect.parameters.id,
);
if (redirectIndex === -1) {
this.setState({
redirects: [redirect, ...this.state.redirects],
});
} else {
const redirects = [...this.state.redirects];
redirects[redirectIndex] = redirect;
this.setState({redirects});
}
};
render() {
const editors = this.state.redirects.map(
(redirect) =>
html`
<${Editor}
key=${redirect.idString()}
redirect=${redirect}
removeRedirect=${this.removeRedirect}
saveRedirect=${this.saveRedirect}
/>
`,
);
return html`
<main class="page-main">
<div class="editors">
<button class="button new-redirect" onClick=${this.addNewRedirect}>
Add New Redirect
</button>
${editors}
</div>
<${Usage} />
</main>
`;
}
}

View File

@ -0,0 +1,161 @@
import {html} from 'htm/preact';
import {Component} from 'preact';
export default class Usage extends Component {
render() {
return html`
<details class="usage">
<summary>How do I use Re-Nav?</summary>
<p>Creating redirects:</p>
<ul>
<li>Click the green "Add new redirect" button.</li>
<li>Select a matcher type and enter what it should match on.</li>
<li>
Select a redirect type and enter where you want to be redirected.
</li>
<li>
See the "Matchers" and "Redirects" sections below for lists of
everything available with examples.
</li>
</ul>
<p>Using redirects:</p>
<ul>
<li>
Any time you are navigated to a link by your browser, the URL will
first be checked and you will be redirected automatically.
</li>
</ul>
<p>Editing redirects:</p>
<ul>
<li>
If a redirect has been edited, a yellow border will be shown around
it.
</li>
<li>
Changes to redirects are only saved when you click the save button.
</li>
<li>
To enable or disable a redirect, click the button with the circle.
If it's filled in the redirect is enabled.
</li>
<li>To remove a redirect click the red button with the twice.</li>
</ul>
<p>Some miscellaneous notes:</p>
<ul>
<li>Only URLs starting with "http" will be checked.</li>
<li>
Navigation events won't be checked if it has been less than 100
milliseconds since the last successful redirect.
</li>
<li>
A redirect will be cancelled if the exact same redirect happened
less than 30 seconds ago. This acts as a quick bypass so you don't
have to disable redirects in the options page whenever you don't
want to be redirected.
</li>
</ul>
<p>As a quick-start you can also insert the examples from below:</p>
<ul>
<li>
Note that this will reload the page so make sure your redirects have
been saved.
</li>
<li>
<button class="button" onClick=${window.Holllo.insertExamples}>
Insert Examples
</button>
</li>
</ul>
</details>
<details class="usage table">
<summary>Matchers</summary>
<table>
<thead>
<tr>
<th>Type</th>
<th>Match Directive</th>
<th>Match Examples</th>
</tr>
</thead>
<tbody>
<tr>
<td class="bold center-text" rowspan="2">Hostname</td>
<td class="center-text" rowspan="2">tildes.net</td>
<td>http://<b>www.tildes.net</b>/<sup>1</sup></td>
</tr>
<tr>
<td>https://<b>tildes.net</b>/~creative.timasomo</td>
</tr>
<tr class="alt">
<td class="bold center-text" rowspan="2">Regex</td>
<td class="center-text">HOL{3}O</td>
<td>https://git.bauke.xyz/<b>holllo</b><sup>2</sup></td>
</tr>
<tr class="alt">
<td class="center-text">^https?://www\\.holllo\\.org/?$</td>
<td><b>https://www.holllo.org/</b></td>
</tr>
</tbody>
</table>
<ol class="footnotes">
<li>
Hostname matchers always remove "www." automatically, for
convenience.
</li>
<li>
Regular expressions are always tested with global and
case-insensitive flags enabled.
</li>
</ol>
</details>
<details class="usage table">
<summary>Redirects</summary>
<table>
<thead>
<tr>
<th>Type</th>
<th>Change To</th>
<th>Example URL</th>
<th>Redirected URL<sup>1</sup></th>
</tr>
</thead>
<tbody>
<tr>
<td class="bold center-text" rowspan="2">Hostname</td>
<td>nitter.net</td>
<td>https://twitter.com/therealTDH</td>
<td>https://<b>nitter.net</b>/therealTDH</td>
</tr>
<tr>
<td>r.nf</td>
<td>https://www.reddit.com/r/TheDearHunter</td>
<td>https://<b>r.nf</b>/r/TheDearHunter</td>
</tr>
<tr class="alt">
<td class="bold center-text">Simple</td>
<td>https://holllo.org</td>
<td>https://holllo.org/home</td>
<td><b>https://holllo.org</b></td>
</tr>
</tbody>
</table>
<ol class="footnotes">
<li>The bold highlighted text shows what will be changed.</li>
</ol>
</details>
`;
}
}

View File

@ -0,0 +1,46 @@
import browser from 'webextension-polyfill';
import {RedirectParameters} from '../redirect/base.js';
import storage from '../redirect/storage.js';
const examples: RedirectParameters[] = [
{
enabled: true,
id: -1,
matcherType: 'hostname',
matcherValue: 'twitter.com',
redirectType: 'hostname',
redirectValue: 'nitter.net',
},
{
enabled: true,
id: -1,
matcherType: 'hostname',
matcherValue: 'reddit.com',
redirectType: 'hostname',
redirectValue: 'r.nf',
},
{
enabled: true,
id: -1,
matcherType: 'regex',
matcherValue: '^https?://holllo\\.org/renav/?$',
redirectType: 'simple',
redirectValue: 'https://holllo.org/re-nav',
},
];
export async function generateExamples(): Promise<
Record<string, RedirectParameters>
> {
const prepared: Record<string, RedirectParameters> = {};
let nextId = await storage.nextRedirectId();
for (const example of examples) {
example.id = nextId;
prepared[`redirect:${nextId}`] = example;
nextId += 1;
}
await browser.storage.local.set({latestId: nextId - 1});
return prepared;
}

21
source/options/index.html Normal file
View File

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Re-Nav</title>
<link rel="shortcut icon" href="/assets/re-nav.png" type="image/png">
<link rel="stylesheet" href="./index.scss">
</head>
<body class="love">
<noscript>
This WebExtension doesn't work without JavaScript enabled, sorry! 😭
</noscript>
<script type="module" src="./index.ts"></script>
</body>
</html>

41
source/options/index.scss Normal file
View File

@ -0,0 +1,41 @@
@use '../../node_modules/modern-normalize/modern-normalize.css';
@use 'scss/reset';
@use 'scss/mixins';
@use 'scss/love';
@use 'scss/components/page-header';
@use 'scss/components/page-main';
@use 'scss/components/page-footer';
@use 'scss/components/editor';
@use 'scss/components/usage';
html {
font-size: 62.5%;
}
body {
background-color: var(--db-1);
color: var(--df-1);
font-size: 1.5rem;
padding: 16px;
}
a {
color: var(--da-3);
&:hover {
color: var(--df-2);
}
}
:focus {
outline-color: var(--df-1);
outline-width: 2px;
}
.bold {
font-weight: bold;
}
.center-text {
text-align: center;
}

29
source/options/index.ts Normal file
View File

@ -0,0 +1,29 @@
import {html} from 'htm/preact';
import {Component, render} from 'preact';
import storage from '../redirect/storage.js';
import {PageFooter} from './components/page-footer.js';
import {PageHeader} from './components/page-header.js';
import {PageMain} from './components/page-main.js';
import {generateExamples} from './examples.js';
window.addEventListener('DOMContentLoaded', () => {
window.Holllo = {
async insertExamples() {
await storage.savePrepared(await generateExamples());
location.reload();
},
};
render(html`<${OptionsPage} />`, document.body);
});
class OptionsPage extends Component {
render() {
return html`
<${PageHeader} />
<${PageMain} />
<${PageFooter} />
`;
}
}

View File

@ -0,0 +1,59 @@
.editor {
align-items: center;
background-color: var(--db-2);
border: 1px solid transparent;
display: flex;
padding: 8px;
&.has-been-edited {
border: 1px solid var(--da-3);
}
.arrow-span {
aspect-ratio: 1;
align-items: center;
display: flex;
height: 100%;
font-weight: bold;
justify-content: center;
}
.button {
aspect-ratio: 1;
height: 100%;
margin-left: 8px;
padding: 0;
&.enabled {
background-color: var(--da-4);
}
&.disabled {
background-color: var(--da-2);
}
}
.input {
background-color: var(--db-1);
border: 1px solid var(--df-2);
color: var(--df-1);
height: 100%;
padding: 4px;
width: 100%;
}
.select {
appearance: none;
background: url('../../../assets/down-arrow.svg') no-repeat center right;
background-size: 1.5rem;
background-color: var(--db-1);
border: 1px solid var(--df-2);
border-radius: 0;
color: var(--df-1);
cursor: pointer;
height: 100%;
margin-right: 8px;
padding: 4px 24px 4px 4px;
width: fit-content;
}
}

View File

@ -0,0 +1,9 @@
@use '../mixins';
.page-footer {
@include mixins.responsive-container;
border: 1px solid var(--df-2);
margin-bottom: 16px;
padding: 16px;
}

View File

@ -0,0 +1,22 @@
@use '../mixins';
.page-header {
@include mixins.responsive-container;
border: 1px solid var(--df-2);
margin-bottom: 16px;
h1 {
align-items: center;
display: flex;
}
img {
background-color: var(--df-2);
display: inline-block;
height: 4.5rem;
margin-right: 1rem;
padding: 8px;
width: 4.5rem;
}
}

View File

@ -0,0 +1,38 @@
@use '../mixins';
.page-main {
@include mixins.responsive-container;
display: grid;
gap: 16px;
margin-bottom: 16px;
.button {
background-color: var(--da-3);
border: none;
color: var(--db-1);
cursor: pointer;
font-weight: bold;
&.destructive {
background-color: var(--da-1);
}
&.new-redirect {
background-color: var(--da-4);
padding: 8px;
width: fit-content;
}
&:hover {
background-color: var(--df-2);
}
}
.editors {
border: 1px solid var(--df-2);
display: grid;
gap: 16px;
padding: 16px;
}
}

View File

@ -0,0 +1,95 @@
.usage {
border: 1px solid var(--df-2);
&[open] {
summary {
background-color: var(--df-2);
color: var(--db-1);
}
&:not(.table) summary {
margin-bottom: 16px;
}
> :not(summary) {
padding-left: 16px;
padding-right: 16px;
}
}
summary {
cursor: pointer;
font-weight: bold;
padding: 16px;
&:hover {
background-color: var(--df-1);
color: var(--db-1);
}
}
ul {
list-style: square;
margin: 4px 0 2rem 16px;
}
table {
border-collapse: collapse;
width: 100%;
&:not(:last-child) {
border-bottom: 1px solid var(--df-2);
}
}
th {
border-bottom: 1px solid var(--df-2);
padding: 8px;
}
td,
th {
&:not(:last-child) {
border-right: 1px solid var(--df-2);
}
}
tr {
&.alt {
background-color: var(--db-2);
}
&:not(:last-child) {
border-bottom: 1px solid var(--df-2);
}
td {
padding: 4px;
b {
color: var(--da-3);
}
}
}
.footnotes {
margin-left: 12px;
padding: 8px;
code {
background-color: var(--db-2);
}
li {
margin-bottom: 4px;
&:last-child {
margin-bottom: 0;
}
}
}
.button {
padding: 4px;
}
}

View File

@ -0,0 +1,45 @@
/*
The Love Theme CSS Custom Properties
https://love.holllo.cc - version 0.1.0
MIT license
*/
.love {
/* Love Dark */
--df-1: #f2efff;
--df-2: #e6deff;
--db-1: #1f1731;
--db-2: #2a2041;
--da-1: #f99fb1;
--da-2: #faa56c;
--da-3: #d2b83a;
--da-4: #96c839;
--da-5: #3bd18a;
--da-6: #3ecdbf;
--da-7: #41c8e5;
--da-8: #98b9f8;
--da-9: #d5a6f8;
--da-10: #f99add;
--dg-1: #e2e2e2;
--dg-2: #c6c6c6;
--dg-3: #ababab;
/* Love Light */
--lf-1: #1f1731;
--lf-2: #2a2041;
--lb-1: #f2efff;
--lb-2: #e6deff;
--la-1: #8b123c;
--la-2: #6a3b11;
--la-3: #514610;
--la-4: #384d10;
--la-5: #115133;
--la-6: #124f49;
--la-7: #144d5a;
--la-8: #17477e;
--la-9: #6f1995;
--la-10: #81156a;
--lg-1: #1b1b1b;
--lg-2: #303030;
--lg-3: #474747;
}

View File

@ -0,0 +1,11 @@
@use 'variables';
@mixin responsive-container {
margin-left: auto;
margin-right: auto;
width: variables.$large-breakpoint;
@media (max-width: variables.$large-breakpoint) {
width: 100%;
}
}

View File

@ -0,0 +1,12 @@
h1,
h2,
h3,
h4,
h5,
ol,
ul,
li,
p {
margin: 0;
padding: 0;
}

View File

@ -0,0 +1,4 @@
$small-breakpoint: 600px;
$medium-breakpoint: 900px;
$large-breakpoint: 1200px;
$extra-large-breakpoint: 1800px;

View File

@ -1,5 +1,3 @@
import {customAlphabet} from 'nanoid';
export const matcherTypes = ['hostname', 'regex'] as const;
export const redirectTypes = ['hostname', 'simple'] as const;
@ -16,6 +14,7 @@ export function narrowRedirectType(value: string): value is RedirectType {
export type RedirectParameters = {
enabled: boolean;
id: number;
matcherType: MatcherType;
matcherValue: string;
redirectType: RedirectType;
@ -23,16 +22,14 @@ export type RedirectParameters = {
};
export abstract class Redirect {
public static generateId(): string {
const alphabet = 'abcdefghijklmnopqrstuvwxyz';
const nanoid = customAlphabet(`${alphabet}${alphabet.toUpperCase()}`, 20);
return nanoid();
public static idString(id: number): string {
return `redirect:${id}`;
}
public id: string;
constructor(public parameters: RedirectParameters) {}
constructor(public parameters: RedirectParameters, id?: string) {
this.id = id ?? Redirect.generateId();
public idString(): string {
return Redirect.idString(this.parameters.id);
}
public isMatch(url: URL): boolean {

View File

@ -10,15 +10,14 @@ export type Redirects = HostnameRedirect | SimpleRedirect;
export function parseRedirect(
parameters: RedirectParameters,
id: string,
): Redirects | undefined {
const redirectType = parameters?.redirectType;
if (redirectType === 'hostname') {
return new HostnameRedirect(parameters, id);
return new HostnameRedirect(parameters);
}
if (redirectType === 'simple') {
return new SimpleRedirect(parameters, id);
return new SimpleRedirect(parameters);
}
}

View File

@ -0,0 +1,68 @@
import browser from 'webextension-polyfill';
import {Redirect, RedirectParameters} from './base.js';
import {parseRedirect} from './exports.js';
const redirectKeyRegex = /^redirect:\d+$/i;
async function getRedirects(): Promise<Redirect[]> {
const redirects: Redirect[] = [];
const stored = await browser.storage.local.get();
for (const [key, value] of Object.entries(stored)) {
if (!redirectKeyRegex.test(key)) {
continue;
}
const redirect = parseRedirect(value);
if (redirect !== undefined) {
redirects.push(redirect);
}
}
return redirects;
}
async function nextRedirectId(): Promise<number> {
const {latestId} = await browser.storage.local.get('latestId');
const id = Number(latestId);
let nextId: number | undefined;
if (Number.isNaN(id)) {
const redirects = await getRedirects();
nextId = redirects.length + 1;
} else {
nextId = id + 1;
}
await browser.storage.local.set({latestId: nextId});
return nextId;
}
async function prepareForStorage(
redirect: Redirect,
): Promise<Record<string, RedirectParameters>> {
const prepared: Record<string, RedirectParameters> = {};
prepared[redirect.idString()] = redirect.parameters;
return prepared;
}
async function save(redirect: Redirect): Promise<void> {
await savePrepared(await prepareForStorage(redirect));
}
async function savePrepared(
prepared: Record<string, RedirectParameters>,
): Promise<void> {
await browser.storage.local.set(prepared);
}
const storage = {
getRedirects,
nextRedirectId,
prepareForStorage,
redirectKeyRegex,
save,
savePrepared,
};
export default storage;

6
source/types.d.ts vendored
View File

@ -16,5 +16,11 @@ declare global {
readonly VITE_BROWSER: 'chromium' | 'firefox';
}
interface Window {
Holllo: {
insertExamples(): Promise<void>;
};
}
type HtmComponent = ReturnType<typeof html>;
}

View File

@ -17,6 +17,7 @@ import {
const hostnameParameters: RedirectParameters = {
enabled: true,
id: 1,
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'hostname',
@ -25,6 +26,7 @@ const hostnameParameters: RedirectParameters = {
const simpleParameters: RedirectParameters = {
enabled: true,
id: 2,
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'simple',
@ -42,13 +44,12 @@ test('parseRedirect', (t) => {
];
for (const sample of samples) {
const redirect = parseRedirect(sample, Redirect.generateId());
const redirect = parseRedirect(sample);
if (redirect === undefined) {
t.pass('parseRedirect returned undefined');
} else {
t.regex(redirect.id, /^[a-z]{20}$/i);
redirect.id = 'id';
t.regex(redirect?.idString(), /^redirect:\d+$/i);
t.snapshot(redirect, `Class ${redirect.constructor.name}`);
}
}
@ -92,6 +93,7 @@ test('Redirect.isMatch', (t) => {
const regexMatch = new HostnameRedirect({
enabled: true,
id: 3,
matcherType: 'regex',
matcherValue: String.raw`^https://(www\.)?example.org/$`,
redirectType: 'simple',

View File

@ -9,9 +9,9 @@ Generated by [AVA](https://avajs.dev).
> Class HostnameRedirect
HostnameRedirect {
id: 'id',
parameters: {
enabled: true,
id: 1,
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'hostname',
@ -22,9 +22,9 @@ Generated by [AVA](https://avajs.dev).
> Class SimpleRedirect
SimpleRedirect {
id: 'id',
parameters: {
enabled: true,
id: 2,
matcherType: 'hostname',
matcherValue: 'example.com',
redirectType: 'simple',