Auto-select advertisements

This automatically selects submission notifications, that are advertisements.

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