Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
210 changes: 7 additions & 203 deletions src/components/options/AdvancedSkipOptionsComponent.tsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import * as React from "react";
import * as CompileConfig from "../../../config.json";

import Config from "../../config";
import { AdvancedSkipRuleSet, SkipRuleAttribute, SkipRuleOperator } from "../../utils/skipRule";
import { ActionType, ActionTypes, CategorySkipOption } from "../../types";
import { AdvancedSkipRule, configToText, parseConfig, } from "../../utils/skipRule";

let configSaveTimeout: NodeJS.Timeout | null = null;

Expand Down Expand Up @@ -64,210 +62,16 @@ export function AdvancedSkipOptionsComponent() {
);
}

function compileConfig(config: string): AdvancedSkipRuleSet[] | null {
const ruleSets: AdvancedSkipRuleSet[] = [];
function compileConfig(config: string): AdvancedSkipRule[] | null {
const { rules, errors } = parseConfig(config);

let ruleSet: AdvancedSkipRuleSet = {
rules: [],
skipOption: null,
comment: ""
};

for (const line of config.split("\n")) {
if (line.trim().length === 0) {
// Skip empty lines
continue;
}

const comment = line.match(/^\s*\/\/(.+)$/);
if (comment) {
if (ruleSet.rules.length > 0) {
// Rule has already been created, add it to list if valid
if (ruleSet.skipOption !== null && ruleSet.rules.length > 0) {
ruleSets.push(ruleSet);

ruleSet = {
rules: [],
skipOption: null,
comment: ""
};
} else {
return null;
}
}

if (ruleSet.comment.length > 0) {
ruleSet.comment += "; ";
}

ruleSet.comment += comment[1].trim();

// Skip comment lines
continue;
} else if (line.startsWith("if ")) {
if (ruleSet.rules.length > 0) {
// Rule has already been created, add it to list if valid
if (ruleSet.skipOption !== null && ruleSet.rules.length > 0) {
ruleSets.push(ruleSet);

ruleSet = {
rules: [],
skipOption: null,
comment: ""
};
} else {
return null;
}
}

const ruleTexts = [...line.matchAll(/\S+ \S+ (?:"[^"\\]*(?:\\.[^"\\]*)*"|\d+)(?= and |$)/g)];
for (const ruleText of ruleTexts) {
if (!ruleText[0]) return null;

const ruleParts = ruleText[0].match(/(\S+) (\S+) ("[^"\\]*(?:\\.[^"\\]*)*"|\d+)/);
if (ruleParts.length !== 4) {
return null; // Invalid rule format
}

const attribute = getSkipRuleAttribute(ruleParts[1]);
const operator = getSkipRuleOperator(ruleParts[2]);
const value = getSkipRuleValue(ruleParts[3]);
if (attribute === null || operator === null || value === null) {
return null; // Invalid attribute or operator
}

if ([SkipRuleOperator.Equal, SkipRuleOperator.NotEqual].includes(operator)) {
if (attribute === SkipRuleAttribute.Category
&& !CompileConfig.categoryList.includes(value as string)) {
return null; // Invalid category value
} else if (attribute === SkipRuleAttribute.ActionType
&& !ActionTypes.includes(value as ActionType)) {
return null; // Invalid category value
} else if (attribute === SkipRuleAttribute.Source
&& !["local", "youtube", "autogenerated", "server"].includes(value as string)) {
return null; // Invalid category value
}
}

ruleSet.rules.push({
attribute,
operator,
value
});
}

// Make sure all rules were parsed
if (ruleTexts.length === 0 || !line.endsWith(ruleTexts[ruleTexts.length - 1][0])) {
return null;
}
} else {
// Only continue if a rule has been defined
if (ruleSet.rules.length === 0) {
return null; // No rules defined yet
}

switch (line.trim().toLowerCase()) {
case "disabled":
ruleSet.skipOption = CategorySkipOption.Disabled;
break;
case "show overlay":
ruleSet.skipOption = CategorySkipOption.ShowOverlay;
break;
case "manual skip":
ruleSet.skipOption = CategorySkipOption.ManualSkip;
break;
case "auto skip":
ruleSet.skipOption = CategorySkipOption.AutoSkip;
break;
default:
return null; // Invalid skip option
}
}
for (const error of errors) {
console.error(`[SB] Error on line ${error.span.start.line}: ${error.message}`);
}

if (ruleSet.rules.length > 0 && ruleSet.skipOption !== null) {
ruleSets.push(ruleSet);
} else if (ruleSet.rules.length > 0 || ruleSet.skipOption !== null) {
// Incomplete rule set
return null;
}

return ruleSets;
}

function getSkipRuleAttribute(attribute: string): SkipRuleAttribute | null {
if (attribute && Object.values(SkipRuleAttribute).includes(attribute as SkipRuleAttribute)) {
return attribute as SkipRuleAttribute;
}

return null;
}

function getSkipRuleOperator(operator: string): SkipRuleOperator | null {
if (operator && Object.values(SkipRuleOperator).includes(operator as SkipRuleOperator)) {
return operator as SkipRuleOperator;
}

return null;
}

function getSkipRuleValue(value: string): string | number | null {
if (!value) return null;

if (value.startsWith('"')) {
try {
return JSON.parse(value);
} catch (e) {
return null; // Invalid JSON string
}
if (errors.length === 0) {
return rules;
} else {
const numValue = Number(value);
if (!isNaN(numValue)) {
return numValue;
}

return null;
}
}

function configToText(config: AdvancedSkipRuleSet[]): string {
let result = "";

for (const ruleSet of config) {
if (ruleSet.comment) {
result += "// " + ruleSet.comment + "\n";
}

result += "if ";
let firstRule = true;
for (const rule of ruleSet.rules) {
if (!firstRule) {
result += " and ";
}

result += `${rule.attribute} ${rule.operator} ${JSON.stringify(rule.value)}`;
firstRule = false;
}

switch (ruleSet.skipOption) {
case CategorySkipOption.Disabled:
result += "\nDisabled";
break;
case CategorySkipOption.ShowOverlay:
result += "\nShow Overlay";
break;
case CategorySkipOption.ManualSkip:
result += "\nManual Skip";
break;
case CategorySkipOption.AutoSkip:
result += "\nAuto Skip";
break;
default:
return null; // Invalid skip option
}

result += "\n\n";
}

return result.trim();
}
51 changes: 44 additions & 7 deletions src/config.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import * as CompileConfig from "../config.json";
import * as invidiousList from "../ci/invidiouslist.json";
import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType, SegmentListDefaultTab } from "./types";
import { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config";
import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorHideType, SponsorTime, VideoID, SegmentListDefaultTab } from "./types";
import { Keybind, keybindEquals, ProtoConfig } from "../maze-utils/src/config";
import { HashedValue } from "../maze-utils/src/hash";
import { Permission, AdvancedSkipRuleSet } from "./utils/skipRule";
import { AdvancedSkipCheck, AdvancedSkipPredicate, AdvancedSkipRule, Permission, PredicateOperator } from "./utils/skipRule";

interface SBConfig {
userID: string;
Expand Down Expand Up @@ -156,7 +156,7 @@ interface SBStorage {
/* VideoID prefixes to UUID prefixes */
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>;
navigationApiAvailable: boolean;

// Used when sync storage disabled
alreadyInstalled: boolean;

Expand All @@ -167,7 +167,7 @@ interface SBStorage {
skipProfileTemp: { time: number; configID: ConfigurationID } | null;
skipProfiles: Record<ConfigurationID, CustomConfiguration>;

skipRules: AdvancedSkipRuleSet[];
skipRules: AdvancedSkipRule[];
}

class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
Expand All @@ -187,6 +187,43 @@ class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
}

function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
if (local["skipRules"] && local["skipRules"].length !== 0 && local["skipRules"][0]["rules"]) {
const output: AdvancedSkipRule[] = [];

for (const rule of local["skipRules"]) {
const rules: object[] = rule["rules"];

if (rules.length !== 0) {
let predicate: AdvancedSkipPredicate = {
kind: "check",
...rules[0] as AdvancedSkipCheck,
};

for (let i = 1; i < rules.length; i++) {
predicate = {
kind: "operator",
operator: PredicateOperator.And,
left: predicate,
right: {
kind: "check",
...rules[i] as AdvancedSkipCheck,
},
};
}

const comment = rule["comment"] as string;

output.push({
predicate,
skipOption: rule.skipOption,
comments: comment.length === 0 ? [] : comment.split(/;\s*/),
});
}
}

local["skipRules"] = output;
}

if (config["whitelistedChannels"]) {
// convert to skipProfiles
const whitelistedChannels = config["whitelistedChannels"] as string[];
Expand All @@ -213,7 +250,7 @@ function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
for (const channelID of whitelistedChannels) {
local.channelSkipProfileIDs[channelID] = skipProfileID;
}
local.channelSkipProfileIDs = local.channelSkipProfileIDs;
local.channelSkipProfileIDs = local.channelSkipProfileIDs;

chrome.storage.sync.remove("whitelistedChannels");
}
Expand Down Expand Up @@ -247,7 +284,7 @@ function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
name: "chapter" as Category,
option: CategorySkipOption.ShowOverlay
});

config.categorySelections = config.categorySelections;
}
}
Expand Down
Loading