mirror of
https://github.com/ajayyy/SponsorBlock.git
synced 2025-12-11 14:07:13 +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 CompileConfig from "../../../config.json";
|
||||
|
||||
import Config from "../../config";
|
||||
import {AdvancedSkipRuleSet, compileConfigNew, SkipRuleAttribute, SkipRuleOperator} from "../../utils/skipRule";
|
||||
import { ActionType, ActionTypes, CategorySkipOption } from "../../types";
|
||||
import {AdvancedSkipPredicate, AdvancedSkipRule, parseConfig, PredicateOperator,} from "../../utils/skipRule";
|
||||
import {CategorySkipOption} from "../../types";
|
||||
|
||||
let configSaveTimeout: NodeJS.Timeout | null = null;
|
||||
|
||||
@@ -64,206 +63,43 @@ export function AdvancedSkipOptionsComponent() {
|
||||
);
|
||||
}
|
||||
|
||||
function compileConfig(config: string): AdvancedSkipRuleSet[] | null {
|
||||
// Debug
|
||||
compileConfigNew(config);
|
||||
function compileConfig(config: string): AdvancedSkipRule[] | null {
|
||||
const { rules, errors } = parseConfig(config);
|
||||
|
||||
const ruleSets: AdvancedSkipRuleSet[] = [];
|
||||
|
||||
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.log(`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 {
|
||||
function configToText(config: AdvancedSkipRule[]): string {
|
||||
let result = "";
|
||||
|
||||
for (const ruleSet of config) {
|
||||
if (ruleSet.comment) {
|
||||
result += "// " + ruleSet.comment + "\n";
|
||||
for (const rule of config) {
|
||||
for (const comment of rule.comments) {
|
||||
result += "// " + comment + "\n";
|
||||
}
|
||||
|
||||
result += "if ";
|
||||
let firstRule = true;
|
||||
for (const rule of ruleSet.rules) {
|
||||
if (!firstRule) {
|
||||
result += " and ";
|
||||
}
|
||||
result += predicateToText(rule.predicate, PredicateOperator.Or);
|
||||
|
||||
result += `${rule.attribute} ${rule.operator} ${JSON.stringify(rule.value)}`;
|
||||
firstRule = false;
|
||||
}
|
||||
|
||||
switch (ruleSet.skipOption) {
|
||||
switch (rule.skipOption) {
|
||||
case CategorySkipOption.Disabled:
|
||||
result += "\nDisabled";
|
||||
break;
|
||||
case CategorySkipOption.ShowOverlay:
|
||||
result += "\nShow Overlay";
|
||||
result += "\nShow overlay";
|
||||
break;
|
||||
case CategorySkipOption.ManualSkip:
|
||||
result += "\nManual Skip";
|
||||
result += "\nManual skip";
|
||||
break;
|
||||
case CategorySkipOption.AutoSkip:
|
||||
result += "\nAuto Skip";
|
||||
result += "\nAuto skip";
|
||||
break;
|
||||
default:
|
||||
return null; // Invalid skip option
|
||||
@@ -274,3 +110,16 @@ function configToText(config: AdvancedSkipRuleSet[]): string {
|
||||
|
||||
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 { Keybind, ProtoConfig, keybindEquals } from "../maze-utils/src/config";
|
||||
import { HashedValue } from "../maze-utils/src/hash";
|
||||
import { Permission, AdvancedSkipRuleSet } from "./utils/skipRule";
|
||||
import { Permission, AdvancedSkipRule } from "./utils/skipRule";
|
||||
|
||||
interface SBConfig {
|
||||
userID: string;
|
||||
@@ -155,7 +155,7 @@ interface SBStorage {
|
||||
/* VideoID prefixes to UUID prefixes */
|
||||
downvotedSegments: Record<VideoID & HashedValue, VideoDownvotes>;
|
||||
navigationApiAvailable: boolean;
|
||||
|
||||
|
||||
// Used when sync storage disabled
|
||||
alreadyInstalled: boolean;
|
||||
|
||||
@@ -166,7 +166,7 @@ interface SBStorage {
|
||||
skipProfileTemp: { time: number; configID: ConfigurationID } | null;
|
||||
skipProfiles: Record<ConfigurationID, CustomConfiguration>;
|
||||
|
||||
skipRules: AdvancedSkipRuleSet[];
|
||||
skipRules: AdvancedSkipRule[];
|
||||
}
|
||||
|
||||
class ConfigClass extends ProtoConfig<SBConfig, SBStorage> {
|
||||
@@ -212,7 +212,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");
|
||||
}
|
||||
@@ -246,7 +246,7 @@ function migrateOldSyncFormats(config: SBConfig, local: SBStorage) {
|
||||
name: "chapter" as Category,
|
||||
option: CategorySkipOption.ShowOverlay
|
||||
});
|
||||
|
||||
|
||||
config.categorySelections = config.categorySelections;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { getCurrentPageTitle } from "../../maze-utils/src/elements";
|
||||
import { getChannelIDInfo, getVideoDuration } from "../../maze-utils/src/video";
|
||||
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 { VideoLabelsCacheData } from "./videoLabels";
|
||||
import * as CompileConfig from "../../config.json";
|
||||
|
||||
export interface Permission {
|
||||
canSubmit: boolean;
|
||||
@@ -41,23 +42,38 @@ export enum SkipRuleOperator {
|
||||
NotRegexIgnoreCase = "!~i="
|
||||
}
|
||||
|
||||
export interface AdvancedSkipRule {
|
||||
export interface AdvancedSkipCheck {
|
||||
kind: "check";
|
||||
attribute: SkipRuleAttribute;
|
||||
operator: SkipRuleOperator;
|
||||
value: string | number;
|
||||
}
|
||||
|
||||
export interface AdvancedSkipRuleSet {
|
||||
rules: AdvancedSkipRule[];
|
||||
export enum PredicateOperator {
|
||||
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;
|
||||
comment: string;
|
||||
comments: string[];
|
||||
}
|
||||
|
||||
export function getCategorySelection(segment: SponsorTime | VideoLabelsCacheData): CategorySelection {
|
||||
// First check skip rules
|
||||
for (const ruleSet of Config.local.skipRules) {
|
||||
if (ruleSet.rules.every((rule) => isSkipRulePassing(segment, rule))) {
|
||||
return { name: segment.category, option: ruleSet.skipOption } as CategorySelection;
|
||||
for (const rule of Config.local.skipRules) {
|
||||
if (isSkipPredicatePassing(segment, rule.predicate)) {
|
||||
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;
|
||||
}
|
||||
|
||||
function getSkipRuleValue(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipRule): string | number | undefined {
|
||||
function getSkipCheckValue(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipCheck): string | number | undefined {
|
||||
switch (rule.attribute) {
|
||||
case SkipRuleAttribute.StartTime:
|
||||
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 {
|
||||
const value = getSkipRuleValue(segment, rule);
|
||||
function isSkipCheckPassing(segment: SponsorTime | VideoLabelsCacheData, rule: AdvancedSkipCheck): boolean {
|
||||
const value = getSkipCheckValue(segment, rule);
|
||||
|
||||
switch (rule.operator) {
|
||||
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 {
|
||||
for (const selection of Config.config.categorySelections) {
|
||||
if (selection.name === category) {
|
||||
@@ -188,19 +217,19 @@ export function getCategoryDefaultSelection(category: string): CategorySelection
|
||||
type TokenType =
|
||||
| "if" // Keywords
|
||||
| "disabled" | "show overlay" | "manual skip" | "auto skip" // Skip option
|
||||
| keyof typeof SkipRuleAttribute // Segment attributes
|
||||
| keyof typeof SkipRuleOperator // Segment attribute operators
|
||||
| `${SkipRuleAttribute}` // Segment attributes
|
||||
| `${SkipRuleOperator}` // Segment attribute operators
|
||||
| "and" | "or" // Expression operators
|
||||
| "(" | ")" // Syntax
|
||||
| "(" | ")" | "comment" // Syntax
|
||||
| "string" | "number" // Literal values
|
||||
| "eof" | "error"; // Sentinel and special tokens
|
||||
|
||||
interface SourcePos {
|
||||
export interface SourcePos {
|
||||
line: number;
|
||||
// column: number;
|
||||
}
|
||||
|
||||
interface Span {
|
||||
export interface Span {
|
||||
start: SourcePos;
|
||||
end: SourcePos;
|
||||
}
|
||||
@@ -341,218 +370,222 @@ function nextToken(state: LexerState): Token {
|
||||
state.start_pos = state.current_pos;
|
||||
}
|
||||
|
||||
for (;;) {
|
||||
skipWhitespace();
|
||||
resetToCurrent();
|
||||
skipWhitespace();
|
||||
resetToCurrent();
|
||||
|
||||
if (isEof()) {
|
||||
return makeToken("eof");
|
||||
if (isEof()) {
|
||||
return makeToken("eof");
|
||||
}
|
||||
|
||||
const keyword = expectKeyword([
|
||||
"if", "and", "or",
|
||||
"(", ")",
|
||||
"//",
|
||||
].concat(Object.values(SkipRuleAttribute))
|
||||
.concat(Object.values(SkipRuleOperator)), true);
|
||||
|
||||
if (keyword !== null) {
|
||||
switch (keyword) {
|
||||
case "if": return makeToken("if");
|
||||
case "and": return makeToken("and");
|
||||
case "or": return makeToken("or");
|
||||
|
||||
case "(": return makeToken("(");
|
||||
case ")": return makeToken(")");
|
||||
|
||||
case "time.start": return makeToken("time.start");
|
||||
case "time.end": return makeToken("time.end");
|
||||
case "time.duration": return makeToken("time.duration");
|
||||
case "time.startPercent": return makeToken("time.startPercent");
|
||||
case "time.endPercent": return makeToken("time.endPercent");
|
||||
case "time.durationPercent": return makeToken("time.durationPercent");
|
||||
case "category": return makeToken("category");
|
||||
case "actionType": return makeToken("actionType");
|
||||
case "chapter.name": return makeToken("chapter.name");
|
||||
case "chapter.source": return makeToken("chapter.source");
|
||||
case "channel.id": return makeToken("channel.id");
|
||||
case "channel.name": return makeToken("channel.name");
|
||||
case "video.duration": return makeToken("video.duration");
|
||||
case "video.title": return makeToken("video.title");
|
||||
|
||||
case "<": return makeToken("<");
|
||||
case "<=": return makeToken("<=");
|
||||
case ">": return makeToken(">");
|
||||
case ">=": return makeToken(">=");
|
||||
case "==": return makeToken("==");
|
||||
case "!=": return makeToken("!=");
|
||||
case "*=": return makeToken("*=");
|
||||
case "!*=": return makeToken("!*=");
|
||||
case "~=": return makeToken("~=");
|
||||
case "~i=": return makeToken("~i=");
|
||||
case "!~=": return makeToken("!~=");
|
||||
case "!~i=": return makeToken("!~i=");
|
||||
|
||||
case "//":
|
||||
resetToCurrent();
|
||||
skipLine();
|
||||
return makeToken("comment");
|
||||
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
const keyword = expectKeyword([
|
||||
"if", "and", "or",
|
||||
"(", ")",
|
||||
"//",
|
||||
].concat(Object.values(SkipRuleAttribute))
|
||||
.concat(Object.values(SkipRuleOperator)), true);
|
||||
const keyword2 = expectKeyword(
|
||||
[ "disabled", "show overlay", "manual skip", "auto skip" ], false);
|
||||
|
||||
if (keyword !== null) {
|
||||
switch (keyword) {
|
||||
case "if": return makeToken("if");
|
||||
case "and": return makeToken("and");
|
||||
case "or": return makeToken("or");
|
||||
|
||||
case "(": return makeToken("(");
|
||||
case ")": return makeToken(")");
|
||||
|
||||
case "time.start": return makeToken("StartTime");
|
||||
case "time.end": return makeToken("EndTime");
|
||||
case "time.duration": return makeToken("Duration");
|
||||
case "time.startPercent": return makeToken("StartTimePercent");
|
||||
case "time.endPercent": return makeToken("EndTimePercent");
|
||||
case "time.durationPercent": return makeToken("DurationPercent");
|
||||
case "category": return makeToken("Category");
|
||||
case "actionType": return makeToken("ActionType");
|
||||
case "chapter.name": return makeToken("Description");
|
||||
case "chapter.source": return makeToken("Source");
|
||||
case "channel.id": return makeToken("ChannelID");
|
||||
case "channel.name": return makeToken("ChannelName");
|
||||
case "video.duration": return makeToken("VideoDuration");
|
||||
case "video.title": return makeToken("Title");
|
||||
|
||||
case "<": return makeToken("Less");
|
||||
case "<=": return makeToken("LessOrEqual");
|
||||
case ">": return makeToken("Greater");
|
||||
case ">=": return makeToken("GreaterOrEqual");
|
||||
case "==": return makeToken("Equal");
|
||||
case "!=": return makeToken("NotEqual");
|
||||
case "*=": return makeToken("Contains");
|
||||
case "!*=": return makeToken("NotContains");
|
||||
case "~=": return makeToken("Regex");
|
||||
case "~i=": return makeToken("RegexIgnoreCase");
|
||||
case "!~=": return makeToken("NotRegex");
|
||||
case "!~i=": return makeToken("NotRegexIgnoreCase");
|
||||
|
||||
case "//":
|
||||
skipLine();
|
||||
continue;
|
||||
|
||||
default:
|
||||
}
|
||||
if (keyword2 !== null) {
|
||||
switch (keyword2) {
|
||||
case "disabled": return makeToken("disabled");
|
||||
case "show overlay": return makeToken("show overlay");
|
||||
case "manual skip": return makeToken("manual skip");
|
||||
case "auto skip": return makeToken("auto skip");
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
const keyword2 = expectKeyword(
|
||||
[ "disabled", "show overlay", "manual skip", "auto skip" ], false);
|
||||
|
||||
if (keyword2 !== null) {
|
||||
switch (keyword2) {
|
||||
case "disabled": return makeToken("disabled");
|
||||
case "show overlay": return makeToken("show overlay");
|
||||
case "manual skip": return makeToken("manual skip");
|
||||
case "auto skip": return makeToken("auto skip");
|
||||
default:
|
||||
}
|
||||
}
|
||||
let c = consume();
|
||||
|
||||
if (c === '"') {
|
||||
// Parses string according to ECMA-404 2nd edition (JSON), section 9 “String”
|
||||
let output = "";
|
||||
let c = consume();
|
||||
let error = false;
|
||||
|
||||
if (c === '"') {
|
||||
// Parses string according to ECMA-404 2nd edition (JSON), section 9 “String”
|
||||
let output = "";
|
||||
let c = consume();
|
||||
let error = false;
|
||||
while (c !== null && c !== '"') {
|
||||
if (c == '\\') {
|
||||
c = consume();
|
||||
|
||||
while (c !== null && c !== '"') {
|
||||
if (c == '\\') {
|
||||
c = consume();
|
||||
switch (c) {
|
||||
case '"':
|
||||
output = output.concat('"');
|
||||
break;
|
||||
case '\\':
|
||||
output = output.concat('\\');
|
||||
break;
|
||||
case '/':
|
||||
output = output.concat('/');
|
||||
break;
|
||||
case 'b':
|
||||
output = output.concat('\b');
|
||||
break;
|
||||
case 'f':
|
||||
output = output.concat('\f');
|
||||
break;
|
||||
case 'n':
|
||||
output = output.concat('\n');
|
||||
break;
|
||||
case 'r':
|
||||
output = output.concat('\r');
|
||||
break;
|
||||
case 't':
|
||||
output = output.concat('\t');
|
||||
break;
|
||||
case 'u': {
|
||||
// UTF-16 value sequence
|
||||
const digits = state.source.slice(state.current, state.current + 4);
|
||||
|
||||
switch (c) {
|
||||
case '"':
|
||||
output = output.concat('"');
|
||||
break;
|
||||
case '\\':
|
||||
output = output.concat('\\');
|
||||
break;
|
||||
case '/':
|
||||
output = output.concat('/');
|
||||
break;
|
||||
case 'b':
|
||||
output = output.concat('\b');
|
||||
break;
|
||||
case 'f':
|
||||
output = output.concat('\f');
|
||||
break;
|
||||
case 'n':
|
||||
output = output.concat('\n');
|
||||
break;
|
||||
case 'r':
|
||||
output = output.concat('\r');
|
||||
break;
|
||||
case 't':
|
||||
output = output.concat('\t');
|
||||
break;
|
||||
case 'u': {
|
||||
// UTF-16 value sequence
|
||||
const digits = state.source.slice(state.current, state.current + 4);
|
||||
|
||||
if (digits.length < 4 || !/[0-9a-zA-Z]{4}/.test(digits)) {
|
||||
error = true;
|
||||
output = output.concat(`\\u`);
|
||||
c = consume();
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = parseInt(digits, 16);
|
||||
// fromCharCode() takes a UTF-16 value without performing validity checks,
|
||||
// which is exactly what is needed here – in JSON, code units outside the
|
||||
// BMP are represented by two Unicode escape sequences.
|
||||
output = output.concat(String.fromCharCode(value));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
if (digits.length < 4 || !/[0-9a-zA-Z]{4}/.test(digits)) {
|
||||
error = true;
|
||||
output = output.concat(`\\${c}`);
|
||||
break;
|
||||
output = output.concat(`\\u`);
|
||||
c = consume();
|
||||
continue;
|
||||
}
|
||||
|
||||
const value = parseInt(digits, 16);
|
||||
// fromCharCode() takes a UTF-16 value without performing validity checks,
|
||||
// which is exactly what is needed here – in JSON, code units outside the
|
||||
// BMP are represented by two Unicode escape sequences.
|
||||
output = output.concat(String.fromCharCode(value));
|
||||
break;
|
||||
}
|
||||
} else {
|
||||
output = output.concat(c);
|
||||
default:
|
||||
error = true;
|
||||
output = output.concat(`\\${c}`);
|
||||
break;
|
||||
}
|
||||
|
||||
c = consume();
|
||||
} else {
|
||||
output = output.concat(c);
|
||||
}
|
||||
|
||||
return {
|
||||
type: error || c !== '"' ? "error" : "string",
|
||||
span: { start: state.start_pos, end: state.current_pos, },
|
||||
value: output,
|
||||
};
|
||||
} else if (/[0-9-]/.test(c)) {
|
||||
// Parses number according to ECMA-404 2nd edition (JSON), section 8 “Numbers”
|
||||
if (c === '-') {
|
||||
c = consume();
|
||||
c = consume();
|
||||
}
|
||||
|
||||
if (!/[0-9]/.test(c)) {
|
||||
return makeToken("error");
|
||||
}
|
||||
return {
|
||||
type: error || c !== '"' ? "error" : "string",
|
||||
span: { start: state.start_pos, end: state.current_pos, },
|
||||
value: output,
|
||||
};
|
||||
} else if (/[0-9-]/.test(c)) {
|
||||
// Parses number according to ECMA-404 2nd edition (JSON), section 8 “Numbers”
|
||||
if (c === '-') {
|
||||
c = consume();
|
||||
|
||||
if (!/[0-9]/.test(c)) {
|
||||
return makeToken("error");
|
||||
}
|
||||
}
|
||||
|
||||
const leadingZero = c === '0';
|
||||
let next = peek();
|
||||
let error = false;
|
||||
|
||||
while (next !== null && /[0-9]/.test(next)) {
|
||||
consume();
|
||||
next = peek();
|
||||
|
||||
if (leadingZero) {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (next !== null && next === '.') {
|
||||
consume();
|
||||
next = peek();
|
||||
|
||||
if (next === null || !/[0-9]/.test(next)) {
|
||||
return makeToken("error");
|
||||
}
|
||||
|
||||
const leadingZero = c === '0';
|
||||
let next = peek();
|
||||
let error = false;
|
||||
do {
|
||||
consume();
|
||||
next = peek();
|
||||
} while (next !== null && /[0-9]/.test(next));
|
||||
}
|
||||
|
||||
next = peek();
|
||||
|
||||
if (next != null && (next === 'e' || next === 'E')) {
|
||||
consume();
|
||||
next = peek();
|
||||
|
||||
if (next === null) {
|
||||
return makeToken("error");
|
||||
}
|
||||
|
||||
if (next === '+' || next === '-') {
|
||||
consume();
|
||||
next = peek();
|
||||
}
|
||||
|
||||
while (next !== null && /[0-9]/.test(next)) {
|
||||
consume();
|
||||
next = peek();
|
||||
|
||||
if (leadingZero) {
|
||||
error = true;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
if (next !== null && next === '.') {
|
||||
consume();
|
||||
next = peek();
|
||||
|
||||
if (next === null || !/[0-9]/.test(next)) {
|
||||
return makeToken("error");
|
||||
}
|
||||
|
||||
do {
|
||||
consume();
|
||||
next = peek();
|
||||
} while (next !== null && /[0-9]/.test(next));
|
||||
}
|
||||
|
||||
next = peek();
|
||||
|
||||
if (next != null && (next === 'e' || next === 'E')) {
|
||||
consume();
|
||||
next = peek();
|
||||
|
||||
if (next === null) {
|
||||
return makeToken("error");
|
||||
}
|
||||
|
||||
if (next === '+' || next === '-') {
|
||||
consume();
|
||||
next = peek();
|
||||
}
|
||||
|
||||
while (next !== null && /[0-9]/.test(next)) {
|
||||
consume();
|
||||
next = peek();
|
||||
}
|
||||
}
|
||||
|
||||
return makeToken(error ? "error" : "number");
|
||||
}
|
||||
|
||||
return makeToken("error");
|
||||
return makeToken(error ? "error" : "number");
|
||||
}
|
||||
|
||||
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()
|
||||
const lexerState: LexerState = {
|
||||
source: config,
|
||||
@@ -563,14 +596,212 @@ export function compileConfigNew(config: string): AdvancedSkipRuleSet[] | null {
|
||||
current_pos: { line: 1 },
|
||||
};
|
||||
|
||||
let token = nextToken(lexerState);
|
||||
let previous: Token = null;
|
||||
let current: Token = nextToken(lexerState);
|
||||
|
||||
while (token.type !== "eof") {
|
||||
console.log(token);
|
||||
const rules: AdvancedSkipRule[] = [];
|
||||
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,});
|
||||
}
|
||||
|
||||
panicMode ||= panic;
|
||||
erroring = true;
|
||||
}
|
||||
|
||||
// TODO
|
||||
return null;
|
||||
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;
|
||||
}
|
||||
|
||||
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