Auto-select advertisements

This automatically selects submission notifications, that are advertisements.

目前为 2025-04-01 提交的版本,查看 最新版本

  1. // ==UserScript==
  2. // @name Auto-select advertisements
  3. // @namespace https://github.com/f1r3w4rr10r/fa-utils
  4. // @version 0.2.1
  5. // @description This automatically selects submission notifications, that are advertisements.
  6. // @author f1r3w4rr10r
  7. // @match https://www.furaffinity.net/msg/submissions/*
  8. // @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
  9. // @license MIT
  10. // @grant none
  11. // ==/UserScript==
  12.  
  13. (async function () {
  14. "use strict";
  15.  
  16. const DEFINITELY_AD_THRESHOLD = 50;
  17. const AMBIGUOUS_AD_THRESHOLD = 25;
  18.  
  19. // The second "c" is a Cyrillic "s";
  20. const COMMISSION_REGEX_STRING = "[cс]omm(?:ission)?s?";
  21.  
  22. /** @type {AdSelector | SelectorCombiner} */
  23. const AMBIGUOUS_COMMISSION_SELECTOR = {
  24. operator: "or",
  25. operands: [
  26. { target: "name", pattern: /\bauction\b/i },
  27. {
  28. target: "name",
  29. pattern: new RegExp(`(?:^|\\W)${COMMISSION_REGEX_STRING}\\b`, "i"),
  30. },
  31. { target: "name", pattern: /\bwing.its?\b/i },
  32. ],
  33. };
  34.  
  35. /** @type {AdSelector | SelectorCombiner} */
  36. const AMBIGUOUS_DISCOUNTS_SELECTOR = {
  37. operator: "or",
  38. operands: [
  39. { target: "name", pattern: /\bdiscount\b/i },
  40. { target: "name", pattern: /\bsale\b/i },
  41. ],
  42. };
  43.  
  44. /** @type {AdSelector | SelectorCombiner} */
  45. const AMBIGUOUS_MEMBERSHIPS_SELECTOR = {
  46. operator: "or",
  47. operands: [
  48. { target: "name", pattern: /\bboosty\b/i },
  49. { target: "name", pattern: /\bp[@a]treon\b/i },
  50. { target: "name", pattern: /\bsub(?:scribe)?\s*star\b/i },
  51. { target: "name", pattern: /\bsupporters?\b/i },
  52. { target: "tags", pattern: /\bboosty\b/i },
  53. { target: "tags", pattern: /\bp[@a]treon\b/i },
  54. { target: "tags", pattern: /\bsub(?:scribe)?\s*star\b/i },
  55. ],
  56. };
  57.  
  58. /** @type {AdSelector | SelectorCombiner} */
  59. const AMBIGUOUS_PRICE_LIST_SELECTOR = {
  60. operator: "or",
  61. operands: [
  62. { target: "name", pattern: /\bprice\s+(?:list|sheet)\b/i },
  63. { target: "name", pattern: /\bcommission\s+info/i },
  64. ],
  65. };
  66.  
  67. /** @type {AdSelector | SelectorCombiner} */
  68. const AMBIGUOUS_RAFFLES_SELECTOR = { target: "name", pattern: /\braffle\b/i };
  69.  
  70. /** @type {AdSelector | SelectorCombiner} */
  71. const AMBIGUOUS_SHOPS_SELECTOR = {
  72. operator: "or",
  73. operands: [
  74. { target: "name", pattern: /\bshop\b/i },
  75. { target: "name", pattern: /\bfurplanet\b/i },
  76. { target: "description", pattern: /\bfurplanet\b/i },
  77. { target: "tags", pattern: /\bfurplanet\b/i },
  78. ],
  79. };
  80.  
  81. /** @type {AdSelector | SelectorCombiner} */
  82. const AMBIGUOUS_SLOTS_SELECTOR = {
  83. operator: "or",
  84. operands: [
  85. { target: "name", pattern: /\b(?:multi)?slots?\b/i },
  86. { target: "name", pattern: /\bart\s+marathon\b/i },
  87. ],
  88. };
  89.  
  90. /** @type {AdSelector | SelectorCombiner} */
  91. const AMBIGUOUS_STREAM_SELECTOR = {
  92. target: "name",
  93. pattern: /\b(?:live)?stream\b/i,
  94. };
  95.  
  96. /** @type {AdSelector | SelectorCombiner} */
  97. const AMBIGUOUS_TEASER_SELECTOR = {
  98. operator: "or",
  99. operands: [
  100. { target: "name", pattern: /\bpreview\b/i },
  101. { target: "name", pattern: /\bteaser\b/i },
  102. ],
  103. };
  104.  
  105. /** @type {AdSelector | SelectorCombiner} */
  106. const AMBIGUOUS_YCH_SELECTOR = {
  107. target: "name",
  108. pattern: /\by\s*c\s*h\s*s?\b/i,
  109. };
  110.  
  111. /** @type {AdRules} */
  112. const adRules = [
  113. {
  114. ruleName: "adoptables",
  115. value: DEFINITELY_AD_THRESHOLD,
  116. selector: {
  117. operator: "or",
  118. operands: [
  119. { target: "name", pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i },
  120. { target: "name", pattern: /\bcustoms\b/i },
  121. { target: "tags", pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i },
  122. {
  123. target: "description",
  124. pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i,
  125. },
  126. ],
  127. },
  128. },
  129. {
  130. ruleName: "commission ads (ambiguous)",
  131. value: AMBIGUOUS_AD_THRESHOLD,
  132. selector: AMBIGUOUS_COMMISSION_SELECTOR,
  133. },
  134. {
  135. ruleName: "commission ads (definitive)",
  136. value: DEFINITELY_AD_THRESHOLD,
  137. selector: {
  138. operator: "and",
  139. operands: [
  140. AMBIGUOUS_COMMISSION_SELECTOR,
  141. {
  142. operator: "or",
  143. operands: [
  144. { target: "name", pattern: /\bclosed\b/i },
  145. { target: "name", pattern: /\bhalfbody\b/i },
  146. { target: "name", pattern: /\bopen(?:ed)?\b/i },
  147. { target: "name", pattern: /\bsale\b/i },
  148. { target: "name", pattern: /\bslots?\b/i },
  149. { target: "name", pattern: /\bych\b/i },
  150. ],
  151. },
  152. ],
  153. },
  154. },
  155. {
  156. ruleName: "convention dealers",
  157. value: DEFINITELY_AD_THRESHOLD,
  158. selector: {
  159. operator: "or",
  160. operands: [
  161. { target: "tags", pattern: /\bdealers?\s+den\b/i },
  162. { target: "description", pattern: /\bdealers?\s+den\b/i },
  163. ],
  164. },
  165. },
  166. {
  167. ruleName: "discounts (ambiguous)",
  168. value: AMBIGUOUS_AD_THRESHOLD,
  169. selector: AMBIGUOUS_DISCOUNTS_SELECTOR,
  170. },
  171. {
  172. ruleName: "discounts (definitive)",
  173. value: DEFINITELY_AD_THRESHOLD,
  174. selector: {
  175. operator: "and",
  176. operands: [
  177. AMBIGUOUS_DISCOUNTS_SELECTOR,
  178. {
  179. operator: "or",
  180. operands: [
  181. { target: "name", pattern: /\$/ },
  182. { target: "name", pattern: /\bbase\b/i },
  183. { target: "name", pattern: /\bclaimed\b/i },
  184. { target: "name", pattern: /\b(?:multi)?slot\b/i },
  185. { target: "name", pattern: /\boffer\b/i },
  186. { target: "name", pattern: /\bprice\b/i },
  187. ],
  188. },
  189. ],
  190. },
  191. },
  192. {
  193. ruleName: "memberships (ambgiuous)",
  194. value: AMBIGUOUS_AD_THRESHOLD,
  195. selector: AMBIGUOUS_MEMBERSHIPS_SELECTOR,
  196. },
  197. {
  198. ruleName: "memberships (definitive)",
  199. value: DEFINITELY_AD_THRESHOLD,
  200. selector: {
  201. operator: "and",
  202. operands: [
  203. {
  204. operator: "or",
  205. operands: [
  206. ...AMBIGUOUS_MEMBERSHIPS_SELECTOR.operands,
  207. { target: "description", pattern: /\bboosty\b/i },
  208. { target: "description", pattern: /\bp[@a]treon\b/i },
  209. { target: "description", pattern: /\bsub(?:scribe)?\s*star\b/i },
  210. ],
  211. },
  212. {
  213. operator: "or",
  214. operands: [
  215. { target: "name", pattern: /\bdiscount\b/i },
  216. { target: "name", pattern: /\bnow\s+on\b/i },
  217. { target: "name", pattern: /\bposted\s+to\b/i },
  218. { target: "name", pattern: /\bpreview\b/i },
  219. { target: "name", pattern: /\bteaser?\b/i },
  220. { target: "name", pattern: /\bup\s+on\b/i },
  221. { target: "name", pattern: /\bis\s+up\b/i },
  222. { target: "description", pattern: /\bup\s+on\b/i },
  223. { target: "description", pattern: /\bfull.*\s+on\b/i },
  224. { target: "description", pattern: /\bexclusive(?:ly)?.+for\b/i },
  225. ],
  226. },
  227. ],
  228. },
  229. },
  230. {
  231. ruleName: "price lists (ambiguous)",
  232. value: AMBIGUOUS_AD_THRESHOLD,
  233. selector: AMBIGUOUS_PRICE_LIST_SELECTOR,
  234. },
  235. {
  236. ruleName: "price lists (definitive)",
  237. value: DEFINITELY_AD_THRESHOLD,
  238. selector: {
  239. operator: "and",
  240. operands: [
  241. AMBIGUOUS_PRICE_LIST_SELECTOR,
  242. {
  243. operator: "or",
  244. operands: [],
  245. },
  246. ],
  247. },
  248. },
  249. {
  250. ruleName: "raffles (ambiguous)",
  251. value: AMBIGUOUS_AD_THRESHOLD,
  252. selector: AMBIGUOUS_RAFFLES_SELECTOR,
  253. },
  254. {
  255. ruleName: "raffles (definitive)",
  256. value: DEFINITELY_AD_THRESHOLD,
  257. selector: {
  258. operator: "and",
  259. operands: [
  260. AMBIGUOUS_RAFFLES_SELECTOR,
  261. { target: "name", pattern: /\bwinners?\b/i },
  262. ],
  263. },
  264. },
  265. {
  266. ruleName: "reminders",
  267. value: DEFINITELY_AD_THRESHOLD,
  268. selector: {
  269. operator: "or",
  270. operands: [
  271. { target: "name", pattern: /\breminder+\b/i },
  272. { target: "name", pattern: /^REM$/ },
  273. ],
  274. },
  275. },
  276. {
  277. ruleName: "shops (ambiguous)",
  278. value: AMBIGUOUS_AD_THRESHOLD,
  279. selector: AMBIGUOUS_SHOPS_SELECTOR,
  280. },
  281. {
  282. ruleName: "shops (definitive)",
  283. value: DEFINITELY_AD_THRESHOLD,
  284. selector: {
  285. operator: "or",
  286. operands: [
  287. {
  288. operator: "and",
  289. operands: [
  290. AMBIGUOUS_SHOPS_SELECTOR,
  291. {
  292. operator: "or",
  293. operands: [
  294. { target: "name", pattern: /\bprint\b/i },
  295. { target: "description", pattern: /\bup\s+on\b/i },
  296. ],
  297. },
  298. ],
  299. },
  300. { target: "tags", pattern: /\bmerch\b/i },
  301. ],
  302. },
  303. },
  304. {
  305. ruleName: "slots (ambiguous)",
  306. value: AMBIGUOUS_AD_THRESHOLD,
  307. selector: AMBIGUOUS_SLOTS_SELECTOR,
  308. },
  309. {
  310. ruleName: "slots (definitive)",
  311. value: DEFINITELY_AD_THRESHOLD,
  312. selector: {
  313. operator: "and",
  314. operands: [
  315. AMBIGUOUS_SLOTS_SELECTOR,
  316. {
  317. operator: "or",
  318. operands: [
  319. { target: "name", pattern: /\bavailable\b/i },
  320. { target: "name", pattern: /\bopen\b/i },
  321. { target: "name", pattern: /\bsketch\b/i },
  322. ],
  323. },
  324. ],
  325. },
  326. },
  327. {
  328. ruleName: "stream ads (ambiguous)",
  329. value: AMBIGUOUS_AD_THRESHOLD,
  330. selector: AMBIGUOUS_STREAM_SELECTOR,
  331. },
  332. {
  333. ruleName: "stream ads (definitive)",
  334. value: DEFINITELY_AD_THRESHOLD,
  335. selector: {
  336. operator: "or",
  337. operands: [
  338. { target: "name", pattern: /\bpicarto\.tv\b/i },
  339. { target: "name", pattern: /\bstreaming\b/i },
  340. {
  341. operator: "and",
  342. operands: [
  343. AMBIGUOUS_STREAM_SELECTOR,
  344. {
  345. operator: "or",
  346. operands: [
  347. { target: "name", pattern: /\blive\b/i },
  348. { target: "name", pattern: /\boffline\b/i },
  349. { target: "name", pattern: /\bonline\b/i },
  350. { target: "name", pattern: /\bpreorders?\b/i },
  351. { target: "name", pattern: /\bslots?\b/i },
  352. { target: "name", pattern: /\bup\b/i },
  353. ],
  354. },
  355. ],
  356. },
  357. {
  358. operator: "and",
  359. operands: [
  360. { target: "name", pattern: /\bstream\b/i },
  361. { target: "tags", pattern: /\bstream\b/i },
  362. ],
  363. },
  364. ],
  365. },
  366. },
  367. {
  368. ruleName: "teasers (ambiguous)",
  369. value: AMBIGUOUS_AD_THRESHOLD,
  370. selector: AMBIGUOUS_TEASER_SELECTOR,
  371. },
  372. {
  373. ruleName: "teasers (definitive)",
  374. value: DEFINITELY_AD_THRESHOLD,
  375. selector: {
  376. operator: "and",
  377. operands: [
  378. AMBIGUOUS_TEASER_SELECTOR,
  379. {
  380. target: "description",
  381. pattern:
  382. /\b(?:available|n[eo]w|out)\b.*\bon\b.*\b(?:boosty|p[@a]treon|sub(?:scribe)?\s*star)\b/i,
  383. },
  384. ],
  385. },
  386. },
  387. {
  388. ruleName: "WIPs",
  389. value: DEFINITELY_AD_THRESHOLD,
  390. selector: {
  391. operator: "and",
  392. operands: [
  393. { target: "name", pattern: /\bwip\b/i },
  394. { target: "tags", pattern: /\bwip\b/i },
  395. ],
  396. },
  397. },
  398. {
  399. ruleName: "YCHs (ambiguous)",
  400. value: AMBIGUOUS_AD_THRESHOLD,
  401. selector: AMBIGUOUS_YCH_SELECTOR,
  402. },
  403. {
  404. ruleName: "YCHs (definitive)",
  405. value: DEFINITELY_AD_THRESHOLD,
  406. selector: {
  407. operator: "and",
  408. operands: [
  409. {
  410. operator: "or",
  411. operands: [
  412. AMBIGUOUS_YCH_SELECTOR,
  413. { target: "name", pattern: /^closed$/i },
  414. ],
  415. },
  416. {
  417. operator: "or",
  418. operands: [
  419. { target: "name", pattern: /\bauction\b/i },
  420. { target: "name", pattern: /\bavailable\b/i },
  421. { target: "name", pattern: /\bdiscount\b/i },
  422. { target: "name", pattern: /\bmultislot\b/i },
  423. { target: "name", pattern: /\bo\s+p\s+e\s+n\b/i },
  424. { target: "name", pattern: /\bprice\b/i },
  425. { target: "name", pattern: /\bpreview\b/i },
  426. { target: "name", pattern: /\braffle\b/i },
  427. { target: "name", pattern: /\brem(?:ind(?:er)?)?\d*\b/i },
  428. { target: "name", pattern: /\brmd\b/i },
  429. { target: "name", pattern: /\bsale\b/i },
  430. { target: "name", pattern: /\bslots?\b/i },
  431. { target: "name", pattern: /\bsold\b/i },
  432. { target: "name", pattern: /\btaken\b/i },
  433. { target: "name", pattern: /\busd\b/i },
  434. { target: "name", pattern: /\b\$\d+\b/i },
  435. { target: "tags", pattern: /^$/ },
  436. { target: "tags", pattern: /\bauction\b/i },
  437. { target: "tags", pattern: /\bsale\b/i },
  438. { target: "tags", pattern: /\bych\b/i },
  439. { target: "description", pattern: /\bSB|starting\s+bid\b/i },
  440. ],
  441. },
  442. ],
  443. },
  444. },
  445. {
  446. ruleName: "misc ambiguous",
  447. value: AMBIGUOUS_AD_THRESHOLD,
  448. selector: {
  449. operator: "or",
  450. operands: [
  451. { target: "name", pattern: /\bart\s+pack\b/i },
  452. { target: "name", pattern: /\bart.+earlier\b/i },
  453. { target: "name", pattern: /\bavailable\s+now\b/i },
  454. { target: "name", pattern: /\bclosed\b/i },
  455. { target: "name", pattern: /\bopen\b/i },
  456. { target: "name", pattern: /\bpoll\b/i },
  457. { target: "name", pattern: /\brem\b/i },
  458. { target: "name", pattern: /\bsold\b/i },
  459. { target: "name", pattern: /\bwip\b/i },
  460. { target: "tags", pattern: /\bteaser\b/i },
  461. { target: "description", pattern: /\brules:/i },
  462. ],
  463. },
  464. },
  465. {
  466. ruleName: "misc definitive",
  467. value: DEFINITELY_AD_THRESHOLD,
  468. selector: {
  469. operator: "or",
  470. operands: [
  471. { target: "name", pattern: /\bcharacters?\s+for\s+sale\b/i },
  472. ],
  473. },
  474. },
  475. {
  476. ruleName: "comic pages",
  477. value: -200,
  478. selector: {
  479. operator: "or",
  480. operands: [
  481. {
  482. target: "name",
  483. pattern: /\bpage\s+\d+/i,
  484. },
  485. ],
  486. },
  487. },
  488. {
  489. ruleName: "commission only names",
  490. value: -200,
  491. selector: {
  492. operator: "or",
  493. operands: [
  494. {
  495. target: "name",
  496. pattern: new RegExp(`\\[${COMMISSION_REGEX_STRING}\\]`, "i"),
  497. },
  498. {
  499. target: "name",
  500. pattern: new RegExp(`^${COMMISSION_REGEX_STRING}$`, "i"),
  501. },
  502. ],
  503. },
  504. },
  505. {
  506. ruleName: "completed YCHs",
  507. value: -200,
  508. selector: {
  509. operator: "and",
  510. operands: [
  511. AMBIGUOUS_YCH_SELECTOR,
  512. {
  513. operator: "or",
  514. operands: [
  515. { target: "name", pattern: /\bfinished\b/i },
  516. { target: "name", pattern: /\bresult\b/i },
  517. { target: "description", pattern: /\bfinished\b/i },
  518. ],
  519. },
  520. ],
  521. },
  522. },
  523. {
  524. ruleName: "user reference",
  525. value: -200,
  526. selector: {
  527. operator: "or",
  528. operands: [
  529. {
  530. target: "description",
  531. pattern:
  532. /\b(?:by|for|from)\s+(?::(?:icon[\w-]+|[\w-]+icon):|@@\w+)/i,
  533. },
  534. { target: "description", pattern: /^(?:by|for|from)\s+\w+/i },
  535. { target: "description", pattern: /\bych\s+for\s+\w+/i },
  536. { target: "description", pattern: /\bcharacter\s+©\s+\w+/i },
  537. {
  538. target: "description",
  539. pattern: /\bcharacter\s+belongs\s+to\s+@@\w+/i,
  540. },
  541. { target: "description", pattern: /\bcommission\s+for\b$/im },
  542. {
  543. target: "description",
  544. pattern: /\bpatreon\s+reward\s+for\s+\w+/i,
  545. },
  546. ],
  547. },
  548. },
  549. ];
  550.  
  551. /**
  552. * An evaluator for {@link AdRules} on a submission.
  553. */
  554. class AdRulesEvaluator {
  555. /**
  556. * Create a new {@link AdRulesEvaluator}.
  557. * @param {AdRules} rules - the rules to use
  558. */
  559. constructor(rules) {
  560. this.adRuleEvaluators = rules.map((r) => new AdRuleEvaluator(r));
  561. }
  562.  
  563. /**
  564. * Explain the rating of a submission.
  565. * @param {SubmissionData} submissionData - the data of the submission to explain
  566. */
  567. explain(submissionData) {
  568. const result = this.test(submissionData);
  569.  
  570. console.group(
  571. `Submission: "${submissionData.name}" ${result.rating} -> "${result.level}"`,
  572. );
  573.  
  574. this.adRuleEvaluators.forEach((o) => o.explain(submissionData));
  575.  
  576. console.groupEnd();
  577. }
  578.  
  579. /**
  580. * Test a submission against the rules of the evaluator.
  581. * @param {SubmissionData} submissionData - the data of the submission to test
  582. * @returns {AdRulesResult} the rating result
  583. */
  584. test(submissionData) {
  585. const values = this.adRuleEvaluators
  586. .map((e) => e.test(submissionData))
  587. .filter((e) => e !== null);
  588.  
  589. const rating = values.reduce((t, v) => t + v, 0);
  590.  
  591. /** @type {AdvertisementLevel | null} */
  592. let level = null;
  593. if (rating >= DEFINITELY_AD_THRESHOLD) level = "advertisement";
  594. else if (rating >= AMBIGUOUS_AD_THRESHOLD) level = "ambiguous";
  595.  
  596. return { level, rating };
  597. }
  598. }
  599.  
  600. /**
  601. * Map a selector tree node to an evaluator instance.
  602. * @param {AdSelector | SelectorCombiner} selector
  603. * @return {AdSelectorEvaluator | SelectorCombinerEvaluator}
  604. */
  605. function mapSelectorToEvaluator(selector) {
  606. if ("target" in selector) return new AdSelectorEvaluator(selector);
  607. return new SelectorCombinerEvaluator(selector);
  608. }
  609.  
  610. /**
  611. * An evaluator for a single {@link AdRule} on a submission
  612. */
  613. class AdRuleEvaluator {
  614. /**
  615. * Create a new {@link AdRuleEvaluator}.
  616. * @param {AdRule} rule - the rule to use
  617. */
  618. constructor({ ruleName, value, selector }) {
  619. this.ruleName = ruleName;
  620. this.value = value;
  621. this.selectorEvaluator = mapSelectorToEvaluator(selector);
  622. }
  623.  
  624. /**
  625. * Explain the rating of a submission.
  626. * @param {SubmissionData} submissionData - the data of the submission to explain
  627. */
  628. explain(submissionData) {
  629. const matches = this.selectorEvaluator.test(submissionData);
  630.  
  631. const groupName = `Rule "${this.ruleName}"`;
  632. if (matches) {
  633. console.group(groupName + ` matches: ${this.value}`);
  634. } else {
  635. console.groupCollapsed(groupName);
  636. }
  637.  
  638. this.selectorEvaluator.explain(submissionData);
  639.  
  640. console.groupEnd();
  641. }
  642.  
  643. /**
  644. * Test a submission against the rules of the evaluator.
  645. * @param {SubmissionData} submissionData - the data of the submission to test
  646. * @returns {number | null} the value of the rule, when there's a match; null otherwise
  647. */
  648. test(submissionData) {
  649. if (this.selectorEvaluator.test(submissionData)) return this.value;
  650. return null;
  651. }
  652. }
  653.  
  654. /**
  655. * Map the operands of a selector combiner to evaluators.
  656. * @param {(AdSelector | SelectorCombiner)[]} operands
  657. * @return {(AdSelectorEvaluator | SelectorCombinerEvaluator)[]}
  658. */
  659. function mapOperandsToEvaluators(operands) {
  660. return operands.map((o) => mapSelectorToEvaluator(o));
  661. }
  662.  
  663. /**
  664. * An evaluator for a {@link SelectorCombiner} on a submission
  665. */
  666. class SelectorCombinerEvaluator {
  667. /**
  668. * @param {SelectorCombiner} combiner
  669. */
  670. constructor({ operator, operands }) {
  671. this.operator = operator;
  672. this.operandEvaluators = mapOperandsToEvaluators(operands);
  673. }
  674.  
  675. /**
  676. * Explain the rating of a submission.
  677. * @param {SubmissionData} submissionData - the data of the submission to explain
  678. */
  679. explain(submissionData) {
  680. const matches = this.test(submissionData);
  681.  
  682. const groupName = `Combiner "${this.operator}"`;
  683. if (matches) {
  684. console.group(groupName + " matches");
  685. } else {
  686. console.groupCollapsed(groupName);
  687. }
  688.  
  689. for (const o of this.operandEvaluators) {
  690. const matched = o.explain(submissionData);
  691. if (matched && this.operator === "or") break;
  692. }
  693.  
  694. console.groupEnd();
  695. }
  696.  
  697. /**
  698. * Test a submission against the rules of the combiner's operands.
  699. * @param {SubmissionData} submissionData - the data of the submission to test
  700. * @returns {boolean} whether the combiner in total matches the submission
  701. */
  702. test(submissionData) {
  703. switch (this.operator) {
  704. case "and":
  705. return this.operandEvaluators.every((o) => o.test(submissionData));
  706. case "or":
  707. return this.operandEvaluators.some((o) => o.test(submissionData));
  708. }
  709. }
  710. }
  711.  
  712. /**
  713. * An evaluator for an {@link AdSelector} on a submission
  714. */
  715. class AdSelectorEvaluator {
  716. /**
  717. * @param {AdSelector} selector
  718. */
  719. constructor({ target, pattern }) {
  720. this.target = target;
  721. this.pattern = pattern;
  722. }
  723.  
  724. /**
  725. * Explain the rating of a submission.
  726. * @param {SubmissionData} submissionData - the data of the submission to explain
  727. * @return {boolean} whether the submission matched the selector
  728. */
  729. explain(submissionData) {
  730. const target = this.#getTargetString(submissionData);
  731. const matched = this.pattern.test(target);
  732. console.log(this.pattern, matched, target);
  733. return matched;
  734. }
  735.  
  736. /**
  737. * Test a submission against the rules of the combiner's operands.
  738. * @param {SubmissionData} submissionData - the data of the submission to test
  739. * @returns {boolean} whether the selector matches the submission
  740. */
  741. test(submissionData) {
  742. return this.pattern.test(this.#getTargetString(submissionData));
  743. }
  744.  
  745. /**
  746. * @param {SubmissionData} submissionData
  747. * @return {string}
  748. */
  749. #getTargetString(submissionData) {
  750. switch (this.target) {
  751. case "name":
  752. return submissionData.name;
  753. case "tags":
  754. return submissionData.tags;
  755. case "description":
  756. return submissionData.description;
  757. }
  758. }
  759. }
  760.  
  761. /**
  762. * Iterate over all submissions on a page and test the ad rules against them.
  763. * @returns {[number, number, number]} number of ads, number of ambiguous ads, number of untagged submissions
  764. */
  765. function iterateSubmissions() {
  766. const figures = Array.from(
  767. document.querySelectorAll("section.gallery figure"),
  768. );
  769. let advertisements = 0;
  770. let ambiguous = 0;
  771. let untagged = 0;
  772.  
  773. const evaluator = new AdRulesEvaluator(adRules);
  774.  
  775. for (const figure of figures) {
  776. const figcaption = figure.querySelector("figcaption");
  777. const checkbox = figure.querySelector("input");
  778. const nameAnchor = figcaption?.querySelector("a");
  779. const submissionName = nameAnchor?.textContent;
  780. const tags = figure?.querySelector("img")?.dataset["tags"];
  781. const description = descriptions[checkbox?.value ?? ""]?.description;
  782.  
  783. const submissionData = {
  784. name: submissionName ?? "",
  785. description: description ?? "",
  786. tags: tags ?? "",
  787. };
  788.  
  789. const result = evaluator.test(submissionData);
  790.  
  791. const button = document.createElement("button");
  792. button.type = "button";
  793. button.textContent = `Ad-rating: ${result.rating}`;
  794. button.addEventListener("click", () => evaluator.explain(submissionData));
  795. checkbox?.parentElement?.appendChild(button);
  796.  
  797. if (result.level === "advertisement") {
  798. figure.classList.add("advertisement");
  799. if (checkbox) checkbox.checked = true;
  800. advertisements += 1;
  801. } else if (result.level === "ambiguous") {
  802. figure.classList.add("maybe-advertisement");
  803. ambiguous += 1;
  804. }
  805.  
  806. if (tags === "") {
  807. figcaption?.classList.add("not-tagged");
  808. untagged += 1;
  809. }
  810. }
  811.  
  812. return [advertisements, ambiguous, untagged];
  813. }
  814.  
  815. const sheet = new CSSStyleSheet();
  816. sheet.replaceSync(
  817. `
  818. figure.advertisement { outline: orange 3px solid; }
  819. figure.maybe-advertisement { outline: yellow 3px solid; }
  820. figcaption.not-tagged input { outline: orange 3px solid; }
  821. figcaption button { line-height: 1; margin-left: 1rem; padding: 0; }
  822. `.trim(),
  823. );
  824. document.adoptedStyleSheets.push(sheet);
  825.  
  826. const sectionHeader = document.querySelector(".section-header");
  827.  
  828. const advertisementsSelectMessage = document.createElement("p");
  829. sectionHeader?.appendChild(advertisementsSelectMessage);
  830.  
  831. const [advertisements, ambiguous, untagged] = iterateSubmissions();
  832.  
  833. const message = `Selected ${advertisements} advertisement and ${ambiguous} ambiguous submissions. ${untagged} submissions were not tagged.`;
  834.  
  835. advertisementsSelectMessage.textContent = message;
  836. })();