mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-12 14:37:23 +03:00
Implement parser and configToText, remove old parser
This commit is contained in:
@@ -1,9 +1,8 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import * as CompileConfig from "../../../config.json";
|
|
||||||
|
|
||||||
import Config from "../../config";
|
import Config from "../../config";
|
||||||
import {AdvancedSkipRuleSet, compileConfigNew, SkipRuleAttribute, SkipRuleOperator} from "../../utils/skipRule";
|
import {AdvancedSkipPredicate, AdvancedSkipRule, parseConfig, PredicateOperator,} from "../../utils/skipRule";
|
||||||
import { ActionType, ActionTypes, CategorySkipOption } from "../../types";
|
import {CategorySkipOption} from "../../types";
|
||||||
|
|
||||||
let configSaveTimeout: NodeJS.Timeout | null = null;
|
let configSaveTimeout: NodeJS.Timeout | null = null;
|
||||||
|
|
||||||
@@ -64,206 +63,43 @@ export function AdvancedSkipOptionsComponent() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function compileConfig(config: string): AdvancedSkipRuleSet[] | null {
|
function compileConfig(config: string): AdvancedSkipRule[] | null {
|
||||||
// Debug
|
const { rules, errors } = parseConfig(config);
|
||||||
compileConfigNew(config);
|
|
||||||
|
|
||||||
const ruleSets: AdvancedSkipRuleSet[] = [];
|
for (const error of errors) {
|
||||||
|
console.log(`Error on line ${error.span.start.line}: ${error.message}`);
|
||||||
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 (errors.length === 0) {
|
||||||
if (comment) {
|
return rules;
|
||||||
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 {
|
} 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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
const numValue = Number(value);
|
|
||||||
if (!isNaN(numValue)) {
|
|
||||||
return numValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function configToText(config: AdvancedSkipRuleSet[]): string {
|
function configToText(config: AdvancedSkipRule[]): string {
|
||||||
let result = "";
|
let result = "";
|
||||||
|
|
||||||
for (const ruleSet of config) {
|
for (const rule of config) {
|
||||||
if (ruleSet.comment) {
|
for (const comment of rule.comments) {
|
||||||
result += "// " + ruleSet.comment + "\n";
|
result += "// " + comment + "\n";
|
||||||
}
|
}
|
||||||
|
|
||||||
result += "if ";
|
result += "if ";
|
||||||
let firstRule = true;
|
result += predicateToText(rule.predicate, PredicateOperator.Or);
|
||||||
for (const rule of ruleSet.rules) {
|
|
||||||
if (!firstRule) {
|
|
||||||
result += " and ";
|
|
||||||
}
|
|
||||||
|
|
||||||
result += `${rule.attribute} ${rule.operator} ${JSON.stringify(rule.value)}`;
|
switch (rule.skipOption) {
|
||||||
firstRule = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (ruleSet.skipOption) {
|
|
||||||
case CategorySkipOption.Disabled:
|
case CategorySkipOption.Disabled:
|
||||||
result += "\nDisabled";
|
result += "\nDisabled";
|
||||||
break;
|
break;
|
||||||
case CategorySkipOption.ShowOverlay:
|
case CategorySkipOption.ShowOverlay:
|
||||||
result += "\nShow Overlay";
|
result += "\nShow overlay";
|
||||||
break;
|
break;
|
||||||
case CategorySkipOption.ManualSkip:
|
case CategorySkipOption.ManualSkip:
|
||||||
result += "\nManual Skip";
|
result += "\nManual skip";
|
||||||
break;
|
break;
|
||||||
case CategorySkipOption.AutoSkip:
|
case CategorySkipOption.AutoSkip:
|
||||||
result += "\nAuto Skip";
|
result += "\nAuto skip";
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
return null; // Invalid skip option
|
return null; // Invalid skip option
|
||||||
@@ -274,3 +110,16 @@ function configToText(config: AdvancedSkipRuleSet[]): string {
|
|||||||
|
|
||||||
return result.trim();
|
return result.trim();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function predicateToText(predicate: AdvancedSkipPredicate, highestPrecedence: PredicateOperator): string {
|
||||||
|
if (predicate.kind === "check") {
|
||||||
|
return `${predicate.attribute} ${predicate.operator} ${JSON.stringify(predicate.value)}`;
|
||||||
|
} else {
|
||||||
|
if (predicate.operator === PredicateOperator.And) {
|
||||||
|
return `${predicateToText(predicate.left, PredicateOperator.And)} and ${predicateToText(predicate.right, PredicateOperator.And)}`;
|
||||||
|
} else { // Or
|
||||||
|
const text = `${predicateToText(predicate.left, PredicateOperator.Or)} or ${predicateToText(predicate.right, PredicateOperator.Or)}`;
|
||||||
|
return highestPrecedence == PredicateOperator.And ? `(${text})` : text;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import * as invidiousList from "../ci/invidiouslist.json";
|
|||||||
import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType } from "./types";
|
import { Category, CategorySelection, CategorySkipOption, NoticeVisibilityMode, PreviewBarOption, SponsorTime, VideoID, SponsorHideType } from "./types";
|
||||||
import { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config";
|
import { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config";
|
||||||
import { HashedValue } from "../maze-utils/src/hash";
|
import { HashedValue } from "../maze-utils/src/hash";
|
||||||
import { Permission, AdvancedSkipRuleSet } from "./utils/skipRule";
|
import { Permission, AdvancedSkipRule } from "./utils/skipRule";
|
||||||
|
|
||||||
interface SBConfig {
|
interface SBConfig {
|
||||||
userID: string;
|
userID: string;
|
||||||
@@ -166,7 +166,7 @@ interface SBStorage {
|
|||||||
skipProfileTemp: { time: number; configID: ConfigurationID } | null;
|
skipProfileTemp: { time: number; configID: ConfigurationID } | null;
|
||||||
skipProfiles: Record<ConfigurationID, CustomConfiguration>;
|
skipProfiles: Record<ConfigurationID, CustomConfiguration>;
|
||||||
|
|
||||||
skipRules: AdvancedSkipRuleSet[];
|
skipRules: AdvancedSkipRule[];
|
||||||
}
|
}
|
||||||
|
|
||||||
class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
|
class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { getCurrentPageTitle } from "../../maze-utils/src/elements";
|
import { getCurrentPageTitle } from "../../maze-utils/src/elements";
|
||||||
import { getChannelIDInfo, getVideoDuration } from "../../maze-utils/src/video";
|
import { getChannelIDInfo, getVideoDuration } from "../../maze-utils/src/video";
|
||||||
import Config from "../config";
|
import Config from "../config";
|
||||||
import { CategorySelection, CategorySkipOption, SponsorSourceType, SponsorTime } from "../types";
|
import {ActionType, ActionTypes, CategorySelection, CategorySkipOption, SponsorSourceType, SponsorTime} from "../types";
|
||||||
import { getSkipProfile, getSkipProfileBool } from "./skipProfiles";
|
import { getSkipProfile, getSkipProfileBool } from "./skipProfiles";
|
||||||
import { VideoLabelsCacheData } from "./videoLabels";
|
import { VideoLabelsCacheData } from "./videoLabels";
|
||||||
|
import * as CompileConfig from "../../config.json";
|
||||||
|
|
||||||
export interface Permission {
|
export interface Permission {
|
||||||
canSubmit: boolean;
|
canSubmit: boolean;
|
||||||
@@ -41,23 +42,38 @@ export enum SkipRuleOperator {
|
|||||||
NotRegexIgnoreCase = "!~i="
|
NotRegexIgnoreCase = "!~i="
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdvancedSkipRule {
|
export interface AdvancedSkipCheck {
|
||||||
|
kind: "check";
|
||||||
attribute: SkipRuleAttribute;
|
attribute: SkipRuleAttribute;
|
||||||
operator: SkipRuleOperator;
|
operator: SkipRuleOperator;
|
||||||
value: string | number;
|
value: string | number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AdvancedSkipRuleSet {
|
export enum PredicateOperator {
|
||||||
rules: AdvancedSkipRule[];
|
And = "and",
|
||||||
|
Or = "or",
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AdvancedSkipOperator {
|
||||||
|
kind: "operator";
|
||||||
|
operator: PredicateOperator;
|
||||||
|
left: AdvancedSkipPredicate;
|
||||||
|
right: AdvancedSkipPredicate;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type AdvancedSkipPredicate = AdvancedSkipCheck | AdvancedSkipOperator;
|
||||||
|
|
||||||
|
export interface AdvancedSkipRule {
|
||||||
|
predicate: AdvancedSkipPredicate;
|
||||||
skipOption: CategorySkipOption;
|
skipOption: CategorySkipOption;
|
||||||
comment: string;
|
comments: string[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getCategorySelection(segment: SponsorTime | VideoLabelsCacheData): CategorySelection {
|
export function getCategorySelection(segment: SponsorTime | VideoLabelsCacheData): CategorySelection {
|
||||||
// First check skip rules
|
// First check skip rules
|
||||||
for (const ruleSet of Config.local.skipRules) {
|
for (const rule of Config.local.skipRules) {
|
||||||
if (ruleSet.rules.every((rule) => isSkipRulePassing(segment, rule))) {
|
if (isSkipPredicatePassing(segment, rule.predicate)) {
|
||||||
return { name: segment.category, option: ruleSet.skipOption } as CategorySelection;
|
return { name: segment.category, option: rule.skipOption } as CategorySelection;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -84,7 +100,7 @@ export function getCategorySelection(segment: SponsorTime | VideoLabelsCacheData
|
|||||||
return { name: segment.category, option: CategorySkipOption.Disabled} as CategorySelection;
|
return { name: segment.category, option: CategorySkipOption.Disabled} as CategorySelection;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSkipRuleValue(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipRule): string | number | undefined {
|
function getSkipCheckValue(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipCheck): string | number | undefined {
|
||||||
switch (rule.attribute) {
|
switch (rule.attribute) {
|
||||||
case SkipRuleAttribute.StartTime:
|
case SkipRuleAttribute.StartTime:
|
||||||
return (segment as SponsorTime).segment?.[0];
|
return (segment as SponsorTime).segment?.[0];
|
||||||
@@ -143,8 +159,8 @@ function getSkipRuleValue(segment: SponsorTime | VideoLabelsCacheData, rule: Adv
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function isSkipRulePassing(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipRule): boolean {
|
function isSkipCheckPassing(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipCheck): boolean {
|
||||||
const value = getSkipRuleValue(segment, rule);
|
const value = getSkipCheckValue(segment, rule);
|
||||||
|
|
||||||
switch (rule.operator) {
|
switch (rule.operator) {
|
||||||
case SkipRuleOperator.Less:
|
case SkipRuleOperator.Less:
|
||||||
@@ -176,6 +192,19 @@ function isSkipRulePassing(segment: SponsorTime | VideoLabelsCacheData, rule: Ad
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSkipPredicatePassing(segment: SponsorTime | VideoLabelsCacheData, predicate: AdvancedSkipPredicate): boolean {
|
||||||
|
if (predicate.kind === "check") {
|
||||||
|
return isSkipCheckPassing(segment, predicate as AdvancedSkipCheck);
|
||||||
|
} else { // predicate.kind === "operator"
|
||||||
|
// TODO Is recursion fine to use here?
|
||||||
|
if (predicate.operator == PredicateOperator.And) {
|
||||||
|
return isSkipPredicatePassing(segment, predicate.left) && isSkipPredicatePassing(segment, predicate.right);
|
||||||
|
} else { // predicate.operator === PredicateOperator.Or
|
||||||
|
return isSkipPredicatePassing(segment, predicate.left) || isSkipPredicatePassing(segment, predicate.right);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function getCategoryDefaultSelection(category: string): CategorySelection {
|
export function getCategoryDefaultSelection(category: string): CategorySelection {
|
||||||
for (const selection of Config.config.categorySelections) {
|
for (const selection of Config.config.categorySelections) {
|
||||||
if (selection.name === category) {
|
if (selection.name === category) {
|
||||||
@@ -188,19 +217,19 @@ export function getCategoryDefaultSelection(category: string): CategorySelection
|
|||||||
type TokenType =
|
type TokenType =
|
||||||
| "if" // Keywords
|
| "if" // Keywords
|
||||||
| "disabled" | "show overlay" | "manual skip" | "auto skip" // Skip option
|
| "disabled" | "show overlay" | "manual skip" | "auto skip" // Skip option
|
||||||
| keyof typeof SkipRuleAttribute // Segment attributes
|
| `${SkipRuleAttribute}` // Segment attributes
|
||||||
| keyof typeof SkipRuleOperator // Segment attribute operators
|
| `${SkipRuleOperator}` // Segment attribute operators
|
||||||
| "and" | "or" // Expression operators
|
| "and" | "or" // Expression operators
|
||||||
| "(" | ")" // Syntax
|
| "(" | ")" | "comment" // Syntax
|
||||||
| "string" | "number" // Literal values
|
| "string" | "number" // Literal values
|
||||||
| "eof" | "error"; // Sentinel and special tokens
|
| "eof" | "error"; // Sentinel and special tokens
|
||||||
|
|
||||||
interface SourcePos {
|
export interface SourcePos {
|
||||||
line: number;
|
line: number;
|
||||||
// column: number;
|
// column: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface Span {
|
export interface Span {
|
||||||
start: SourcePos;
|
start: SourcePos;
|
||||||
end: SourcePos;
|
end: SourcePos;
|
||||||
}
|
}
|
||||||
@@ -341,7 +370,6 @@ function nextToken(state: LexerState): Token {
|
|||||||
state.start_pos = state.current_pos;
|
state.start_pos = state.current_pos;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (;;) {
|
|
||||||
skipWhitespace();
|
skipWhitespace();
|
||||||
resetToCurrent();
|
resetToCurrent();
|
||||||
|
|
||||||
@@ -365,37 +393,38 @@ function nextToken(state: LexerState): Token {
|
|||||||
case "(": return makeToken("(");
|
case "(": return makeToken("(");
|
||||||
case ")": return makeToken(")");
|
case ")": return makeToken(")");
|
||||||
|
|
||||||
case "time.start": return makeToken("StartTime");
|
case "time.start": return makeToken("time.start");
|
||||||
case "time.end": return makeToken("EndTime");
|
case "time.end": return makeToken("time.end");
|
||||||
case "time.duration": return makeToken("Duration");
|
case "time.duration": return makeToken("time.duration");
|
||||||
case "time.startPercent": return makeToken("StartTimePercent");
|
case "time.startPercent": return makeToken("time.startPercent");
|
||||||
case "time.endPercent": return makeToken("EndTimePercent");
|
case "time.endPercent": return makeToken("time.endPercent");
|
||||||
case "time.durationPercent": return makeToken("DurationPercent");
|
case "time.durationPercent": return makeToken("time.durationPercent");
|
||||||
case "category": return makeToken("Category");
|
case "category": return makeToken("category");
|
||||||
case "actionType": return makeToken("ActionType");
|
case "actionType": return makeToken("actionType");
|
||||||
case "chapter.name": return makeToken("Description");
|
case "chapter.name": return makeToken("chapter.name");
|
||||||
case "chapter.source": return makeToken("Source");
|
case "chapter.source": return makeToken("chapter.source");
|
||||||
case "channel.id": return makeToken("ChannelID");
|
case "channel.id": return makeToken("channel.id");
|
||||||
case "channel.name": return makeToken("ChannelName");
|
case "channel.name": return makeToken("channel.name");
|
||||||
case "video.duration": return makeToken("VideoDuration");
|
case "video.duration": return makeToken("video.duration");
|
||||||
case "video.title": return makeToken("Title");
|
case "video.title": return makeToken("video.title");
|
||||||
|
|
||||||
case "<": return makeToken("Less");
|
case "<": return makeToken("<");
|
||||||
case "<=": return makeToken("LessOrEqual");
|
case "<=": return makeToken("<=");
|
||||||
case ">": return makeToken("Greater");
|
case ">": return makeToken(">");
|
||||||
case ">=": return makeToken("GreaterOrEqual");
|
case ">=": return makeToken(">=");
|
||||||
case "==": return makeToken("Equal");
|
case "==": return makeToken("==");
|
||||||
case "!=": return makeToken("NotEqual");
|
case "!=": return makeToken("!=");
|
||||||
case "*=": return makeToken("Contains");
|
case "*=": return makeToken("*=");
|
||||||
case "!*=": return makeToken("NotContains");
|
case "!*=": return makeToken("!*=");
|
||||||
case "~=": return makeToken("Regex");
|
case "~=": return makeToken("~=");
|
||||||
case "~i=": return makeToken("RegexIgnoreCase");
|
case "~i=": return makeToken("~i=");
|
||||||
case "!~=": return makeToken("NotRegex");
|
case "!~=": return makeToken("!~=");
|
||||||
case "!~i=": return makeToken("NotRegexIgnoreCase");
|
case "!~i=": return makeToken("!~i=");
|
||||||
|
|
||||||
case "//":
|
case "//":
|
||||||
|
resetToCurrent();
|
||||||
skipLine();
|
skipLine();
|
||||||
continue;
|
return makeToken("comment");
|
||||||
|
|
||||||
default:
|
default:
|
||||||
}
|
}
|
||||||
@@ -549,10 +578,14 @@ function nextToken(state: LexerState): Token {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return makeToken("error");
|
return makeToken("error");
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function compileConfigNew(config: string): AdvancedSkipRuleSet[] | null {
|
export interface ParseError {
|
||||||
|
span: Span;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function parseConfig(config: string): { rules: AdvancedSkipRule[]; errors: ParseError[] } {
|
||||||
// Mutated by calls to nextToken()
|
// Mutated by calls to nextToken()
|
||||||
const lexerState: LexerState = {
|
const lexerState: LexerState = {
|
||||||
source: config,
|
source: config,
|
||||||
@@ -563,14 +596,212 @@ export function compileConfigNew(config: string): AdvancedSkipRuleSet[] | null {
|
|||||||
current_pos: { line: 1 },
|
current_pos: { line: 1 },
|
||||||
};
|
};
|
||||||
|
|
||||||
let token = nextToken(lexerState);
|
let previous: Token = null;
|
||||||
|
let current: Token = nextToken(lexerState);
|
||||||
|
|
||||||
while (token.type !== "eof") {
|
const rules: AdvancedSkipRule[] = [];
|
||||||
console.log(token);
|
const errors: ParseError[] = [];
|
||||||
|
let erroring = false;
|
||||||
|
let panicMode = false;
|
||||||
|
|
||||||
token = nextToken(lexerState);
|
function errorAt(span: Span, message: string, panic: boolean) {
|
||||||
|
if (!panicMode) {
|
||||||
|
errors.push({span, message,});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO
|
panicMode ||= panic;
|
||||||
|
erroring = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
function error(message: string, panic: boolean) {
|
||||||
|
errorAt(previous.span, message, panic);
|
||||||
|
}
|
||||||
|
|
||||||
|
function errorAtCurrent(message: string, panic: boolean) {
|
||||||
|
errorAt(current.span, message, panic);
|
||||||
|
}
|
||||||
|
|
||||||
|
function consume() {
|
||||||
|
previous = current;
|
||||||
|
current = nextToken(lexerState);
|
||||||
|
|
||||||
|
while (current.type === "error") {
|
||||||
|
errorAtCurrent(`Unexpected token: ${JSON.stringify(current)}`, true);
|
||||||
|
current = nextToken(lexerState);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function match(expected: readonly TokenType[]): boolean {
|
||||||
|
if (expected.includes(current.type)) {
|
||||||
|
consume();
|
||||||
|
return true;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function expect(expected: readonly TokenType[], message: string, panic: boolean) {
|
||||||
|
if (!match(expected)) {
|
||||||
|
errorAtCurrent(message.concat(`, got: \`${current.type}\``), panic);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function synchronize() {
|
||||||
|
panicMode = false;
|
||||||
|
|
||||||
|
while (!isEof()) {
|
||||||
|
if (current.type === "if") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
consume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function isEof(): boolean {
|
||||||
|
return current.type === "eof";
|
||||||
|
}
|
||||||
|
|
||||||
|
while (!isEof()) {
|
||||||
|
erroring = false;
|
||||||
|
const rule = parseRule();
|
||||||
|
|
||||||
|
if (!erroring) {
|
||||||
|
rules.push(rule);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (panicMode) {
|
||||||
|
synchronize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { rules, errors, };
|
||||||
|
|
||||||
|
function parseRule(): AdvancedSkipRule {
|
||||||
|
const rule: AdvancedSkipRule = {
|
||||||
|
predicate: null,
|
||||||
|
skipOption: null,
|
||||||
|
comments: [],
|
||||||
|
};
|
||||||
|
|
||||||
|
while (match(["comment"])) {
|
||||||
|
rule.comments.push(previous.value.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
expect(["if"], "Expected `if`", true);
|
||||||
|
|
||||||
|
rule.predicate = parsePredicate();
|
||||||
|
|
||||||
|
expect(["disabled", "show overlay", "manual skip", "auto skip"], "Expected skip option after predicate", true);
|
||||||
|
|
||||||
|
switch (previous.type) {
|
||||||
|
case "disabled":
|
||||||
|
rule.skipOption = CategorySkipOption.Disabled;
|
||||||
|
break;
|
||||||
|
case "show overlay":
|
||||||
|
rule.skipOption = CategorySkipOption.ShowOverlay;
|
||||||
|
break;
|
||||||
|
case "manual skip":
|
||||||
|
rule.skipOption = CategorySkipOption.ManualSkip;
|
||||||
|
break;
|
||||||
|
case "auto skip":
|
||||||
|
rule.skipOption = CategorySkipOption.AutoSkip;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Ignore, should have already errored
|
||||||
|
}
|
||||||
|
|
||||||
|
return rule;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePredicate(): AdvancedSkipPredicate {
|
||||||
|
return parseOr();
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseOr(): AdvancedSkipPredicate {
|
||||||
|
let left = parseAnd();
|
||||||
|
|
||||||
|
while (match(["or"])) {
|
||||||
|
const right = parseAnd();
|
||||||
|
|
||||||
|
left = {
|
||||||
|
kind: "operator",
|
||||||
|
operator: PredicateOperator.Or,
|
||||||
|
left, right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseAnd(): AdvancedSkipPredicate {
|
||||||
|
let left = parsePrimary();
|
||||||
|
|
||||||
|
while (match(["and"])) {
|
||||||
|
const right = parsePrimary();
|
||||||
|
|
||||||
|
left = {
|
||||||
|
kind: "operator",
|
||||||
|
operator: PredicateOperator.And,
|
||||||
|
left, right,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return left;
|
||||||
|
}
|
||||||
|
|
||||||
|
function parsePrimary(): AdvancedSkipPredicate {
|
||||||
|
if (match(["("])) {
|
||||||
|
const predicate = parsePredicate();
|
||||||
|
expect([")"], "Expected `)` after predicate", true);
|
||||||
|
return predicate;
|
||||||
|
} else {
|
||||||
|
return parseCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseCheck(): AdvancedSkipCheck {
|
||||||
|
expect(Object.values(SkipRuleAttribute), "Expected attribute", true);
|
||||||
|
|
||||||
|
if (erroring) {
|
||||||
return null;
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const attribute = previous.type as SkipRuleAttribute;
|
||||||
|
expect(Object.values(SkipRuleOperator), "Expected operator after attribute", true);
|
||||||
|
|
||||||
|
if (erroring) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const operator = previous.type as SkipRuleOperator;
|
||||||
|
expect(["string", "number"], "Expected string or number after operator", true);
|
||||||
|
|
||||||
|
if (erroring) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = previous.type === "number" ? Number(previous.value) : previous.value;
|
||||||
|
|
||||||
|
if ([SkipRuleOperator.Equal, SkipRuleOperator.NotEqual].includes(operator)) {
|
||||||
|
if (attribute === SkipRuleAttribute.Category
|
||||||
|
&& !CompileConfig.categoryList.includes(value as string)) {
|
||||||
|
error(`Unknown category: \`${value}\``, false);
|
||||||
|
return null;
|
||||||
|
} else if (attribute === SkipRuleAttribute.ActionType
|
||||||
|
&& !ActionTypes.includes(value as ActionType)) {
|
||||||
|
error(`Unknown action type: \`${value}\``, false);
|
||||||
|
return null;
|
||||||
|
} else if (attribute === SkipRuleAttribute.Source
|
||||||
|
&& !["local", "youtube", "autogenerated", "server"].includes(value as string)) {
|
||||||
|
error(`Unknown chapter source: \`${value}\``, false);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
kind: "check",
|
||||||
|
attribute, operator, value,
|
||||||
|
};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user