Auto-select advertisements

This automatically selects submission notifications, that are advertisements.

// ==UserScript==
// @name         Auto-select advertisements
// @namespace    https://github.com/f1r3w4rr10r/fa-utils
// @version      0.2.2
// @description  This automatically selects submission notifications, that are advertisements.
// @author       f1r3w4rr10r
// @match        https://www.furaffinity.net/msg/submissions/*
// @icon         data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @license      MIT
// @grant        none
// ==/UserScript==

(async function () {
  "use strict";

  const DEFINITELY_AD_THRESHOLD = 50;
  const AMBIGUOUS_AD_THRESHOLD = 25;

  // The second "c" is a Cyrillic "s";
  const COMMISSION_REGEX_STRING = "[cс]omm?(?:ission)?s?";

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_COMMISSION_SELECTOR = {
    operator: "or",
    operands: [
      { target: "name", pattern: /\bauction\b/i },
      {
        target: "name",
        pattern: new RegExp(`(?:^|\\W)${COMMISSION_REGEX_STRING}\\b`, "i"),
      },
      { target: "name", pattern: /\bwing.its?\b/i },
    ],
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_DISCOUNTS_SELECTOR = {
    operator: "or",
    operands: [
      { target: "name", pattern: /\bdiscount\b/i },
      { target: "name", pattern: /\bsale\b/i },
    ],
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_MEMBERSHIPS_SELECTOR = {
    operator: "or",
    operands: [
      { target: "name", pattern: /\bboosty\b/i },
      { target: "name", pattern: /\bp[@a]treon\b/i },
      { target: "name", pattern: /\bsub(?:scribe)?\s*star\b/i },
      { target: "name", pattern: /\bsupporters?\b/i },
      { target: "tags", pattern: /\bboosty\b/i },
      { target: "tags", pattern: /\bp[@a]treon\b/i },
      { target: "tags", pattern: /\bsub(?:scribe)?\s*star\b/i },
    ],
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_PRICE_LIST_SELECTOR = {
    operator: "or",
    operands: [
      { target: "name", pattern: /\bprice\s+(?:list|sheet)\b/i },
      { target: "name", pattern: /\bcommission\s+info/i },
    ],
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_RAFFLES_SELECTOR = { target: "name", pattern: /\braffle\b/i };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_SHOPS_SELECTOR = {
    operator: "or",
    operands: [
      { target: "name", pattern: /\bshop\b/i },
      { target: "name", pattern: /\bfurplanet\b/i },
      { target: "description", pattern: /\bfurplanet\b/i },
      { target: "tags", pattern: /\bfurplanet\b/i },
    ],
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_SLOTS_SELECTOR = {
    operator: "or",
    operands: [
      { target: "name", pattern: /\b(?:multi)?slots?\b/i },
      { target: "name", pattern: /\bart\s+marathon\b/i },
      { target: "description", pattern: /\d\s+slots\b/i },
    ],
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_STREAM_SELECTOR = {
    target: "name",
    pattern: /\b(?:live)?stream\b/i,
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_TEASER_SELECTOR = {
    operator: "or",
    operands: [
      { target: "name", pattern: /\bpreview\b/i },
      { target: "name", pattern: /\bspoiler\b/i },
      { target: "name", pattern: /\bteaser\b/i },
    ],
  };

  /** @type {AdSelector | SelectorCombiner} */
  const AMBIGUOUS_YCH_SELECTOR = {
    operator: "or",
    operands: [
      {
        target: "name",
        pattern: /\by\s*c\s*h\s*s?\b/i,
      },
      {
        target: "description",
        pattern: /\by\s*c\s*h\s*s?\b/i,
      },
    ],
  };

  /** @type {AdRules} */
  const adRules = [
    {
      ruleName: "adoptables",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "or",
        operands: [
          { target: "name", pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i },
          { target: "name", pattern: /\bcustoms\b/i },
          { target: "tags", pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i },
          {
            target: "description",
            pattern: /\badopt(?:(?:able)?s?|ing|ion)\b/i,
          },
        ],
      },
    },
    {
      ruleName: "commission ads (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_COMMISSION_SELECTOR,
    },
    {
      ruleName: "commission ads (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          AMBIGUOUS_COMMISSION_SELECTOR,
          {
            operator: "or",
            operands: [
              { target: "name", pattern: /\bclosed\b/i },
              { target: "name", pattern: /\bhalfbody\b/i },
              { target: "name", pattern: /\bopen(?:ed)?\b/i },
              { target: "name", pattern: /\bsale\b/i },
              { target: "name", pattern: /\bslots?\b/i },
              { target: "name", pattern: /\bych\b/i },
            ],
          },
        ],
      },
    },
    {
      ruleName: "convention dealers",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "or",
        operands: [
          { target: "tags", pattern: /\bdealers?\s+den\b/i },
          { target: "description", pattern: /\bdealers?\s+den\b/i },
        ],
      },
    },
    {
      ruleName: "discounts (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_DISCOUNTS_SELECTOR,
    },
    {
      ruleName: "discounts (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          AMBIGUOUS_DISCOUNTS_SELECTOR,
          {
            operator: "or",
            operands: [
              { target: "name", pattern: /\$/ },
              { target: "name", pattern: /\bbase\b/i },
              { target: "name", pattern: /\bclaimed\b/i },
              { target: "name", pattern: /\b(?:multi)?slot\b/i },
              { target: "name", pattern: /\boffer\b/i },
              { target: "name", pattern: /\bprice\b/i },
            ],
          },
        ],
      },
    },
    {
      ruleName: "memberships (ambgiuous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_MEMBERSHIPS_SELECTOR,
    },
    {
      ruleName: "memberships (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          {
            operator: "or",
            operands: [
              ...AMBIGUOUS_MEMBERSHIPS_SELECTOR.operands,
              { target: "description", pattern: /\bboosty\b/i },
              { target: "description", pattern: /\bp[@a]treon\b/i },
              { target: "description", pattern: /\bsub(?:scribe)?\s*star\b/i },
            ],
          },
          {
            operator: "or",
            operands: [
              { target: "name", pattern: /\bdiscount\b/i },
              { target: "name", pattern: /\b(?:available|now)\s+on\b/i },
              { target: "name", pattern: /\bposted\s+to\b/i },
              { target: "name", pattern: /\bpreview\b/i },
              { target: "name", pattern: /\bteaser?\b/i },
              { target: "name", pattern: /\bup\s+on\b/i },
              { target: "name", pattern: /\bis\s+up\b/i },
              { target: "description", pattern: /\bup\s+on\b/i },
              { target: "description", pattern: /\bfull.*\s+on\b/i },
              { target: "description", pattern: /\bexclusive(?:ly)?.+for\b/i },
            ],
          },
        ],
      },
    },
    {
      ruleName: "price lists (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_PRICE_LIST_SELECTOR,
    },
    {
      ruleName: "price lists (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          AMBIGUOUS_PRICE_LIST_SELECTOR,
          {
            operator: "or",
            operands: [],
          },
        ],
      },
    },
    {
      ruleName: "raffles (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_RAFFLES_SELECTOR,
    },
    {
      ruleName: "raffles (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          AMBIGUOUS_RAFFLES_SELECTOR,
          { target: "name", pattern: /\bwinners?\b/i },
        ],
      },
    },
    {
      ruleName: "reminders",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "or",
        operands: [
          { target: "name", pattern: /\breminder+\b/i },
          { target: "name", pattern: /^REM$/ },
        ],
      },
    },
    {
      ruleName: "shops (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_SHOPS_SELECTOR,
    },
    {
      ruleName: "shops (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "or",
        operands: [
          {
            operator: "and",
            operands: [
              AMBIGUOUS_SHOPS_SELECTOR,
              {
                operator: "or",
                operands: [
                  { target: "name", pattern: /\bprint\b/i },
                  { target: "description", pattern: /\bup\s+on\b/i },
                ],
              },
            ],
          },
          { target: "tags", pattern: /\bmerch\b/i },
        ],
      },
    },
    {
      ruleName: "slots (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_SLOTS_SELECTOR,
    },
    {
      ruleName: "slots (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          AMBIGUOUS_SLOTS_SELECTOR,
          {
            operator: "or",
            operands: [
              { target: "name", pattern: /\bavailable\b/i },
              { target: "name", pattern: /\bopen\b/i },
              { target: "name", pattern: /\bsketch\b/i },
            ],
          },
        ],
      },
    },
    {
      ruleName: "stream ads (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_STREAM_SELECTOR,
    },
    {
      ruleName: "stream ads (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "or",
        operands: [
          { target: "name", pattern: /\bpicarto\.tv\b/i },
          { target: "name", pattern: /\bstreaming\b/i },
          {
            operator: "and",
            operands: [
              AMBIGUOUS_STREAM_SELECTOR,
              {
                operator: "or",
                operands: [
                  { target: "name", pattern: /\blive\b/i },
                  { target: "name", pattern: /\boffline\b/i },
                  { target: "name", pattern: /\bonline\b/i },
                  { target: "name", pattern: /\bpreorders?\b/i },
                  { target: "name", pattern: /\bslots?\b/i },
                  { target: "name", pattern: /\bup\b/i },
                ],
              },
            ],
          },
          {
            operator: "and",
            operands: [
              { target: "name", pattern: /\bstream\b/i },
              { target: "tags", pattern: /\bstream\b/i },
            ],
          },
        ],
      },
    },
    {
      ruleName: "teasers (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_TEASER_SELECTOR,
    },
    {
      ruleName: "teasers (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          AMBIGUOUS_TEASER_SELECTOR,
          {
            target: "description",
            pattern:
              /\b(?:available|n[eo]w|out)\b.*\bon\b.*\b(?:boosty|p[@a]treon|sub(?:scribe)?\s*star)\b/i,
          },
        ],
      },
    },
    {
      ruleName: "WIPs",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          { target: "name", pattern: /\bwip\b/i },
          { target: "tags", pattern: /\bwip\b/i },
        ],
      },
    },
    {
      ruleName: "YCHs (ambiguous)",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: AMBIGUOUS_YCH_SELECTOR,
    },
    {
      ruleName: "YCHs (definitive)",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "and",
        operands: [
          {
            operator: "or",
            operands: [
              AMBIGUOUS_YCH_SELECTOR,
              { target: "name", pattern: /^closed$/i },
            ],
          },
          {
            operator: "or",
            operands: [
              { target: "name", pattern: /\bauction\b/i },
              { target: "name", pattern: /\bavailable\b/i },
              { target: "name", pattern: /\bdiscount\b/i },
              { target: "name", pattern: /\bmultislot\b/i },
              { target: "name", pattern: /\bo\s+p\s+e\s+n\b/i },
              { target: "name", pattern: /\bprice\b/i },
              { target: "name", pattern: /\bpreview\b/i },
              { target: "name", pattern: /\braffle\b/i },
              { target: "name", pattern: /\brem(?:ind(?:er)?)?\d*\b/i },
              { target: "name", pattern: /\brmd\b/i },
              { target: "name", pattern: /\bsale\b/i },
              { target: "name", pattern: /\bslots?\b/i },
              { target: "name", pattern: /\bsold\b/i },
              { target: "name", pattern: /\btaken\b/i },
              { target: "name", pattern: /\busd\b/i },
              { target: "name", pattern: /\b\$\d+\b/i },
              { target: "tags", pattern: /^$/ },
              { target: "tags", pattern: /\bauction\b/i },
              { target: "tags", pattern: /\bsale\b/i },
              { target: "tags", pattern: /\bych\b/i },
              { target: "description", pattern: /\bSB|starting\s+bid\b/i },
              {
                target: "description",
                pattern: /https?:\/\/ych\.art\/auction\/\d+/i,
              },
            ],
          },
        ],
      },
    },
    {
      ruleName: "misc ambiguous",
      value: AMBIGUOUS_AD_THRESHOLD,
      selector: {
        operator: "or",
        operands: [
          { target: "name", pattern: /\bart\s+pack\b/i },
          { target: "name", pattern: /\bart.+earlier\b/i },
          { target: "name", pattern: /\bavailable\s+now\b/i },
          { target: "name", pattern: /\bclosed\b/i },
          { target: "name", pattern: /\bopen\b/i },
          { target: "name", pattern: /\bpoll\b/i },
          { target: "name", pattern: /\brem\b/i },
          { target: "name", pattern: /\bsold\b/i },
          { target: "name", pattern: /\bwip\b/i },
          { target: "tags", pattern: /\bteaser\b/i },
          { target: "description", pattern: /\brules:/i },
        ],
      },
    },
    {
      ruleName: "misc definitive",
      value: DEFINITELY_AD_THRESHOLD,
      selector: {
        operator: "or",
        operands: [
          { target: "name", pattern: /\bcharacters?\s+for\s+sale\b/i },
          { target: "description", pattern: /\bpoll\s+is\s+up\b/i },
        ],
      },
    },
    {
      ruleName: "comic pages",
      value: -200,
      selector: {
        operator: "or",
        operands: [
          {
            target: "name",
            pattern: /\bpage\s+\d+/i,
          },
        ],
      },
    },
    {
      ruleName: "commission only names",
      value: -200,
      selector: {
        operator: "or",
        operands: [
          {
            target: "name",
            pattern: new RegExp(`\\[${COMMISSION_REGEX_STRING}\\]`, "i"),
          },
          {
            target: "name",
            pattern: new RegExp(`^${COMMISSION_REGEX_STRING}$`, "i"),
          },
        ],
      },
    },
    {
      ruleName: "completed YCHs",
      value: -200,
      selector: {
        operator: "and",
        operands: [
          AMBIGUOUS_YCH_SELECTOR,
          {
            operator: "or",
            operands: [
              { target: "name", pattern: /\bfinished\b/i },
              { target: "name", pattern: /\bresult\b/i },
              { target: "description", pattern: /\bfinished\b/i },
            ],
          },
        ],
      },
    },
    {
      ruleName: "user reference",
      value: -200,
      selector: {
        operator: "or",
        operands: [
          { target: "name", pattern: /\bfor\s+[\w\-.]+/i },
          {
            target: "description",
            pattern:
              /\b(?:by|for|from):?\s+(?::(?:icon[\w\-.]+|[\w\-.]+icon):|@?@\w+)/i,
          },
          {
            target: "description",
            pattern: /^:(?:icon[\w\-.]+|[\w\-.]+icon):$/i,
          },
          { target: "description", pattern: /^(?:by|for|from)\s+\w+/i },
          { target: "description", pattern: /\bych\s+for\s+\w+/i },
          { target: "description", pattern: /\bcharacter\s+©\s+\w+/i },
          {
            target: "description",
            pattern: /\bcharacter\s+belongs\s+to\s+@?@\w+/i,
          },
          { target: "description", pattern: /\bcommission\s+for\b$/im },
          {
            target: "description",
            pattern:
              /(?:©|\(c\))\s*(?::(?:icon[\w\-.]+|[\w\-.]+icon):|@?@\w+)$/im,
          },
          {
            target: "description",
            pattern: /\bpatreon\s+reward\s+for\s+\w+/i,
          },
        ],
      },
    },
    {
      ruleName: "artist reference",
      value: -200,
      selector: {
        operator: "or",
        operands: [
          {
            target: "description",
            pattern: /🎨\s*:\s*(?::(?:icon[\w\-.]+|[\w\-.]+icon):|@?@\w+)/i,
          },
        ],
      },
    },
    {
      ruleName: "rewards",
      value: -200,
      selector: {
        operator: "or",
        operands: [{ target: "description", pattern: /\breward\s+sketch\b/i }],
      },
    },
  ];

  /**
   * An evaluator for {@link AdRules} on a submission.
   */
  class AdRulesEvaluator {
    /**
     * Create a new {@link AdRulesEvaluator}.
     * @param {AdRules} rules - the rules to use
     */
    constructor(rules) {
      this.adRuleEvaluators = rules.map((r) => new AdRuleEvaluator(r));
    }

    /**
     * Explain the rating of a submission.
     * @param {SubmissionData} submissionData - the data of the submission to explain
     */
    explain(submissionData) {
      const result = this.test(submissionData);

      console.group(
        `Submission: "${submissionData.name}" ${result.rating} -> "${result.level}"`,
      );

      this.adRuleEvaluators.forEach((o) => o.explain(submissionData));

      console.groupEnd();
    }

    /**
     * Test a submission against the rules of the evaluator.
     * @param {SubmissionData} submissionData - the data of the submission to test
     * @returns {AdRulesResult} the rating result
     */
    test(submissionData) {
      const values = this.adRuleEvaluators
        .map((e) => e.test(submissionData))
        .filter((e) => e !== null);

      const rating = values.reduce((t, v) => t + v, 0);

      /** @type {AdvertisementLevel | null} */
      let level = null;
      if (rating >= DEFINITELY_AD_THRESHOLD) level = "advertisement";
      else if (rating >= AMBIGUOUS_AD_THRESHOLD) level = "ambiguous";

      return { level, rating };
    }
  }

  /**
   * Map a selector tree node to an evaluator instance.
   * @param {AdSelector | SelectorCombiner} selector
   * @return {AdSelectorEvaluator | SelectorCombinerEvaluator}
   */
  function mapSelectorToEvaluator(selector) {
    if ("target" in selector) return new AdSelectorEvaluator(selector);
    return new SelectorCombinerEvaluator(selector);
  }

  /**
   * An evaluator for a single {@link AdRule} on a submission
   */
  class AdRuleEvaluator {
    /**
     * Create a new {@link AdRuleEvaluator}.
     * @param {AdRule} rule - the rule to use
     */
    constructor({ ruleName, value, selector }) {
      this.ruleName = ruleName;
      this.value = value;
      this.selectorEvaluator = mapSelectorToEvaluator(selector);
    }

    /**
     * Explain the rating of a submission.
     * @param {SubmissionData} submissionData - the data of the submission to explain
     */
    explain(submissionData) {
      const matches = this.selectorEvaluator.test(submissionData);

      const groupName = `Rule "${this.ruleName}"`;
      if (matches) {
        console.group(groupName + ` matches: ${this.value}`);
      } else {
        console.groupCollapsed(groupName);
      }

      this.selectorEvaluator.explain(submissionData);

      console.groupEnd();
    }

    /**
     * Test a submission against the rules of the evaluator.
     * @param {SubmissionData} submissionData - the data of the submission to test
     * @returns {number | null} the value of the rule, when there's a match; null otherwise
     */
    test(submissionData) {
      if (this.selectorEvaluator.test(submissionData)) return this.value;
      return null;
    }
  }

  /**
   * Map the operands of a selector combiner to evaluators.
   * @param {(AdSelector | SelectorCombiner)[]} operands
   * @return {(AdSelectorEvaluator | SelectorCombinerEvaluator)[]}
   */
  function mapOperandsToEvaluators(operands) {
    return operands.map((o) => mapSelectorToEvaluator(o));
  }

  /**
   * An evaluator for a {@link SelectorCombiner} on a submission
   */
  class SelectorCombinerEvaluator {
    /**
     * @param {SelectorCombiner} combiner
     */
    constructor({ operator, operands }) {
      this.operator = operator;
      this.operandEvaluators = mapOperandsToEvaluators(operands);
    }

    /**
     * Explain the rating of a submission.
     * @param {SubmissionData} submissionData - the data of the submission to explain
     */
    explain(submissionData) {
      const matches = this.test(submissionData);

      const groupName = `Combiner "${this.operator}"`;
      if (matches) {
        console.group(groupName + " matches");
      } else {
        console.groupCollapsed(groupName);
      }

      for (const o of this.operandEvaluators) {
        const matched = o.explain(submissionData);
        if (matched && this.operator === "or") break;
      }

      console.groupEnd();
    }

    /**
     * Test a submission against the rules of the combiner's operands.
     * @param {SubmissionData} submissionData - the data of the submission to test
     * @returns {boolean} whether the combiner in total matches the submission
     */
    test(submissionData) {
      switch (this.operator) {
        case "and":
          return this.operandEvaluators.every((o) => o.test(submissionData));
        case "or":
          return this.operandEvaluators.some((o) => o.test(submissionData));
      }
    }
  }

  /**
   * An evaluator for an {@link AdSelector} on a submission
   */
  class AdSelectorEvaluator {
    /**
     * @param {AdSelector} selector
     */
    constructor({ target, pattern }) {
      this.target = target;
      this.pattern = pattern;
    }

    /**
     * Explain the rating of a submission.
     * @param {SubmissionData} submissionData - the data of the submission to explain
     * @return {boolean} whether the submission matched the selector
     */
    explain(submissionData) {
      const target = this.#getTargetString(submissionData);
      const matched = this.pattern.test(target);
      console.log(this.pattern, matched, target);
      return matched;
    }

    /**
     * Test a submission against the rules of the combiner's operands.
     * @param {SubmissionData} submissionData - the data of the submission to test
     * @returns {boolean} whether the selector matches the submission
     */
    test(submissionData) {
      return this.pattern.test(this.#getTargetString(submissionData));
    }

    /**
     * @param {SubmissionData} submissionData
     * @return {string}
     */
    #getTargetString(submissionData) {
      switch (this.target) {
        case "name":
          return submissionData.name;
        case "tags":
          return submissionData.tags;
        case "description":
          return submissionData.description;
      }
    }
  }

  /**
   * Iterate over all submissions on a page and test the ad rules against them.
   * @returns {[number, number, number]} number of ads, number of ambiguous ads, number of untagged submissions
   */
  function iterateSubmissions() {
    const figures = Array.from(
      document.querySelectorAll("section.gallery figure"),
    );
    let advertisements = 0;
    let ambiguous = 0;
    let untagged = 0;

    const evaluator = new AdRulesEvaluator(adRules);

    for (const figure of figures) {
      const figcaption = figure.querySelector("figcaption");
      const checkbox = figure.querySelector("input");
      const nameAnchor = figcaption?.querySelector("a");
      const submissionName = nameAnchor?.textContent;
      const tags = figure?.querySelector("img")?.dataset["tags"];
      const description = descriptions[checkbox?.value ?? ""]?.description;

      const submissionData = {
        name: submissionName ?? "",
        description: description ?? "",
        tags: tags ?? "",
      };

      const result = evaluator.test(submissionData);

      const button = document.createElement("button");
      button.type = "button";
      button.textContent = `Ad-rating: ${result.rating}`;
      button.addEventListener("click", () => evaluator.explain(submissionData));
      checkbox?.parentElement?.appendChild(button);

      if (result.level === "advertisement") {
        figure.classList.add("advertisement");
        if (checkbox) checkbox.checked = true;
        advertisements += 1;
      } else if (result.level === "ambiguous") {
        figure.classList.add("maybe-advertisement");
        ambiguous += 1;
      }

      if (tags === "") {
        figcaption?.classList.add("not-tagged");
        untagged += 1;
      }
    }

    return [advertisements, ambiguous, untagged];
  }

  const sheet = new CSSStyleSheet();
  sheet.replaceSync(
    `
figure.advertisement { outline: orange 3px solid; }
figure.maybe-advertisement { outline: yellow 3px solid; }
figcaption.not-tagged input { outline: orange 3px solid; }
figcaption button { line-height: 1; margin-left: 1rem; padding: 0; }
`.trim(),
  );
  document.adoptedStyleSheets.push(sheet);

  const sectionHeader = document.querySelector(".section-header");

  const advertisementsSelectMessage = document.createElement("p");
  sectionHeader?.appendChild(advertisementsSelectMessage);

  const [advertisements, ambiguous, untagged] = iterateSubmissions();

  const message = `Selected ${advertisements} advertisement and ${ambiguous} ambiguous submissions. ${untagged} submissions were not tagged.`;

  advertisementsSelectMessage.textContent = message;
})();