From 655652494ba1badee5f75856f08d286e7f7583e1 Mon Sep 17 00:00:00 2001 From: Pedro Augusto Date: Fri, 9 Aug 2024 19:29:23 -0300 Subject: [PATCH] feat: finish first implementation of the matching rules interface --- src/scripts/block-dialog.tsx | 31 +++++++++++---- src/scripts/components/textarea.tsx | 15 ++++++- src/scripts/interactive-ruleset.ts | 6 +-- src/scripts/types.ts | 10 ++++- src/scripts/utilities.ts | 62 ++++++++++++++++++++++++++++- 5 files changed, 110 insertions(+), 14 deletions(-) diff --git a/src/scripts/block-dialog.tsx b/src/scripts/block-dialog.tsx index 009fad9d5..865e4ae74 100644 --- a/src/scripts/block-dialog.tsx +++ b/src/scripts/block-dialog.tsx @@ -30,8 +30,8 @@ import type { InteractiveRuleset } from "./interactive-ruleset.ts"; import { translate } from "./locales.ts"; import { PathDepth } from "./path-depth.ts"; import type { LinkProps } from "./ruleset/ruleset.ts"; -import type { DialogTheme } from "./types.ts"; -import { makeAltURL, svgToDataURL } from "./utilities.ts"; +import type { DialogTheme, MatchingRulesText } from "./types.ts"; +import { getMatchingRulesText, makeAltURL, svgToDataURL } from "./utilities.ts"; type BlockDialogContentProps = { blockWholeSite: boolean; @@ -60,6 +60,7 @@ const BlockDialogContent: React.FC = ({ host: "", detailsOpen: false, matchingRulesOpen: false, + matchingRulesText: null as MatchingRulesText | null, pathDepth: null as PathDepth | null, depth: "", rulesToAdd: "", @@ -77,6 +78,8 @@ const BlockDialogContent: React.FC = ({ blockWholeSite ? tldts.getDomain(url.host) ?? url.host : url.host, ); state.detailsOpen = false; + state.matchingRulesOpen = false; + state.matchingRulesText = null; state.pathDepth = enablePathDepth ? new PathDepth(url) : null; state.depth = "0"; state.rulesToAdd = patch.rulesToAdd; @@ -87,6 +90,8 @@ const BlockDialogContent: React.FC = ({ state.unblock = false; state.host = entryProps.url; state.detailsOpen = false; + state.matchingRulesOpen = false; + state.matchingRulesText = null; state.pathDepth = null; state.depth = ""; state.rulesToAdd = ""; @@ -274,9 +279,13 @@ const BlockDialogContent: React.FC = ({ open={state.matchingRulesOpen} onToggle={(e) => { const { open } = e.currentTarget; + const matchingRulesText = open + ? getMatchingRulesText(ruleset, entryProps) + : null; setState((s) => ({ ...s, matchingRulesOpen: open, + matchingRulesText, })); }} > @@ -296,9 +305,11 @@ const BlockDialogContent: React.FC = ({ breakAll id="blocking-rules-text" readOnly - rows={2} + monospace + nowrap + rows={4} resizable - value={""} + value={state.matchingRulesText?.blockRules} /> )} @@ -315,9 +326,11 @@ const BlockDialogContent: React.FC = ({ breakAll id="unblocking-rules-text" readOnly - rows={2} + monospace + nowrap + rows={4} resizable - value={""} + value={state.matchingRulesText?.unblockRules} /> )} @@ -334,9 +347,11 @@ const BlockDialogContent: React.FC = ({ breakAll id="highlight-rules-text" readOnly - rows={2} + monospace + nowrap + rows={4} resizable - value={""} + value={state.matchingRulesText?.highlightRules} /> )} diff --git a/src/scripts/components/textarea.tsx b/src/scripts/components/textarea.tsx index 76370a262..bc832efa6 100644 --- a/src/scripts/components/textarea.tsx +++ b/src/scripts/components/textarea.tsx @@ -6,10 +6,21 @@ import { useClassName } from "./utilities.ts"; export type TextAreaProps = JSX.IntrinsicElements["textarea"] & { breakAll?: boolean; resizable?: boolean; + monospace?: boolean; + nowrap?: boolean; }; export const TextArea = React.forwardRef( - function TextArea({ breakAll = false, resizable = false, ...props }, ref) { + function TextArea( + { + breakAll = false, + resizable = false, + monospace = false, + nowrap = false, + ...props + }, + ref, + ) { const className = useClassName( (theme) => ({ background: "transparent", @@ -23,6 +34,8 @@ export const TextArea = React.forwardRef( ? `calc(1.5em * ${props.rows} + 1em + 2px)` : "auto", lineHeight: "1.5", + fontFamily: monospace ? "monospace" : "inherit", + textWrap: nowrap ? "nowrap" : "wrap", padding: "0.5em 0.625em", resize: resizable ? "vertical" : "none", width: "100%", diff --git a/src/scripts/interactive-ruleset.ts b/src/scripts/interactive-ruleset.ts index 8b1a1aa43..f6be5d9ca 100644 --- a/src/scripts/interactive-ruleset.ts +++ b/src/scripts/interactive-ruleset.ts @@ -77,7 +77,7 @@ type MatchingRule = { lineContent: string | null; }; -type RulesetMatches = { +export type RulesetMatches = { rulesetName: string; blockRules: MatchingRule[]; unblockRules: MatchingRule[]; @@ -286,7 +286,7 @@ export class InteractiveRuleset { // Get rules from user's personal blacklist // Note: I need to internationalize this string later with translate() !! const matches = getMatchesPerRuleset( - "Personal Blacklist", + "personal-blacklist", this.userRuleset, props, ); @@ -345,7 +345,7 @@ function getMatchesPerRuleset( // "Block this website" button. // This ensures the line numbers are accurate without reloading. const rulesetLines = - rulesetName === "Personal Blacklist" ? [...ruleset] : null; + rulesetName === "personal-blacklist" ? [...ruleset] : null; const previousMatchIndexes = new Map(); for (let { lineNumber, specifier } of rawResults) { diff --git a/src/scripts/types.ts b/src/scripts/types.ts index 9de43cdd4..3a00e99e4 100644 --- a/src/scripts/types.ts +++ b/src/scripts/types.ts @@ -1,7 +1,7 @@ import type dayjs from "dayjs"; import type { MessageName0 } from "../common/locales.ts"; import type { SearchEngine as _SearchEngine } from "../common/search-engines.ts"; -import type { QueryResult } from "./interactive-ruleset.ts"; +import type { QueryResult, RulesetMatches } from "./interactive-ruleset.ts"; import type { LinkProps } from "./ruleset/ruleset.ts"; export type { @@ -205,3 +205,11 @@ export type Subscription = { export type Subscriptions = Record; // #endregion Subscriptions + +// #region MatchingRules + +export type MatchingRuleKind = keyof Omit; + +export type MatchingRulesText = Record; + +// #endregion MatchingRules diff --git a/src/scripts/utilities.ts b/src/scripts/utilities.ts index d57bd70db..d35627a9d 100644 --- a/src/scripts/utilities.ts +++ b/src/scripts/utilities.ts @@ -1,7 +1,14 @@ import dayjs from "dayjs"; -import { Ruleset, type RulesetJSON } from "./ruleset/ruleset.ts"; +import type { InteractiveRuleset } from "./interactive-ruleset.ts"; +import { + type LinkProps, + Ruleset, + type RulesetJSON, +} from "./ruleset/ruleset.ts"; import type { ErrorResult, + MatchingRuleKind, + MatchingRulesText, PlainRuleset, Result, SuccessResult, @@ -140,6 +147,59 @@ export function numberEntries( export function lines(s: string): string[] { return s ? s.split("\n") : []; } + +export function getMatchingRulesText( + ruleset: InteractiveRuleset, + props: LinkProps, +): MatchingRulesText { + const ruleTypes: MatchingRuleKind[] = [ + "blockRules", + "unblockRules", + "highlightRules", + ]; + const matchingRulesText: MatchingRulesText = { + blockRules: "", + unblockRules: "", + highlightRules: "", + }; + + const matchingRules = ruleset.getMatchingRules(props); + const biggestLineNumber = Math.max( + ...matchingRules + .flatMap(({ blockRules, unblockRules, highlightRules }) => [ + ...blockRules, + ...unblockRules, + ...highlightRules, + ]) + .map(({ lineNumber }) => lineNumber), + ); + const lineNumberLength = String(biggestLineNumber).length; + + for (const ruleType of ruleTypes) { + for (const match of matchingRules) { + // Check whether the ruleset contains rules of the current rule type + if (match[ruleType].length === 0) continue; + // Add header with ruleset name + matchingRulesText[ruleType] += `# ${match.rulesetName}\n`; + // Add individual rules + matchingRulesText[ruleType] += match[ruleType] + .map(({ lineContent, lineNumber }) => { + const lineNumberStr = String(lineNumber); + const formattedLineNumber = + lineNumberStr.length < lineNumberLength + ? // Add left padding in order to align all line numbers + " ".repeat(lineNumberLength - lineNumberStr.length) + lineNumber + : lineNumberStr; + return `${formattedLineNumber}: ${lineContent}`; + }) + .join("\n"); + matchingRulesText[ruleType] += "\n"; + } + } + + return matchingRulesText; +} + // #endregion string export function downloadTextFile(