Empl*ym*nt Term Censor

Censors those pesky offensive words...

// ==UserScript==
// @name         Empl*ym*nt Term Censor
// @namespace    http://tampermonkey.net/
// @version      1.3
// @description  Censors those pesky offensive words...
// @author       Meolsei
// @match        *://*/*
// @grant        none
// @run-at       document-idle
// @license      MIT
// ==/UserScript==

(function () {
  'use strict';

  const EMPLOYMENT_TERMS = [
    "accountant","applicant","apprenticeship","benefit","bonus","boss",
    "career","colleague","contract","contractor","corporation","cover_letter","cv",
    "deadline","demotion","employee","employer","employment","entrepreneur","equity",
    "freelance","freelancer","gig","hiring","hire","human_resources","income",
    "independent_contractor","internship","interview","job","job_offer","job_search",
    "labor","layoff","leave","leave_of_absence","manager","management","mentorship",
    "motivation","networking","onboarding","overtime","part_time","pay","paycheck",
    "payroll","pension","position","promotion","qualification","recruiter","recruitment",
    "remote","resignation","resume","salary","shift","skill","staff","startup",
    "subcontractor","subcontracting","temp","temporary","tenure","timekeeping","timesheet",
    "training","unemployment","union","vacation","wage","work","work_from_home",
    "workload","workplace","workforce"
  ];

  const EXCLUDE_SELECTOR = 'script, style, noscript, textarea, code, pre, input, select, option, iframe, svg, canvas';

  function escapeRegex(s) {
    return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
  }

  const pattern = EMPLOYMENT_TERMS.map(escapeRegex).join('|');
  const regex = new RegExp(`\\b(${pattern})([a-zA-Z'’-]*)\\b`, 'gi');

  // Replace only middle vowels, preserve first and last char
  function censorMiddleVowels(word) {
    if (word.length <= 2) return word;
    const vowels = /[aeiouAEIOU]/;
    let chars = word.split('');
    for (let i = 1; i < chars.length - 1; i++) {
      if (vowels.test(chars[i])) {
        chars[i] = '*';
      }
    }
    return chars.join('');
  }

  function censorWordWithSuffix(base, suffix) {
    return censorMiddleVowels(base) + suffix;
  }

  function isExcludedTextNode(textNode) {
    const parent = textNode.parentElement;
    if (!parent) return true;
    if (parent.closest(EXCLUDE_SELECTOR)) return true;
    if (parent.isContentEditable) return true;
    return false;
  }

  function censorTextNodeValue(val) {
    return val.replace(regex, (match, base, suffix) => {
      return censorWordWithSuffix(base, suffix);
    });
  }

  function walkAndReplace(root) {
    try {
      const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, null, false);
      const toProcess = [];
      let node;
      while (node = walker.nextNode()) toProcess.push(node);

      for (const textNode of toProcess) {
        if (isExcludedTextNode(textNode)) continue;
        const oldVal = textNode.nodeValue;
        if (!oldVal || !/[A-Za-z]/.test(oldVal)) continue;
        const newVal = censorTextNodeValue(oldVal);
        if (newVal !== oldVal) textNode.nodeValue = newVal;
      }
    } catch (e) {
      console.error('Censor script error', e);
    }
  }

  walkAndReplace(document.body || document.documentElement);

  const observer = new MutationObserver(muts => {
    for (const m of muts) {
      for (const n of m.addedNodes) {
        if (n.nodeType === Node.TEXT_NODE) {
          if (!isExcludedTextNode(n) && /[A-Za-z]/.test(n.nodeValue)) {
            n.nodeValue = censorTextNodeValue(n.nodeValue);
          }
        } else if (n.nodeType === Node.ELEMENT_NODE) {
          if (!n.closest(EXCLUDE_SELECTOR)) walkAndReplace(n);
        }
      }
    }
  });

  observer.observe(document.body || document.documentElement, { childList: true, subtree: true });

})();