AO3 Fix Paragraph Spacing

Removes excessive spacing between paragraphs on AO3.

// ==UserScript==
// @name         AO3 Fix Paragraph Spacing
// @namespace    ao3-remove-double-spacing
// @version      1.7
// @description  Removes excessive spacing between paragraphs on AO3.
// @author       yuube
// @match        http*://*.archiveofourown.org/*
// @icon         https://www.google.com/s2/favicons?domain=archiveofourown.org
// @grant        none
// ==/UserScript==

(function () {
    'use strict';

    function hasMedia (el) {
        var mediaWhitelist = [
            'IMG',
            'EMBED',
            'IFRAME',
            'VIDEO'
        ];

        var whitelisted = false;

        mediaWhitelist.forEach(item => {
            if (el.tagName === item || el.querySelector(item)) {
                whitelisted = true;
            };
        });

        return whitelisted
    };

    function naturallyEmpty (el) {
        var emptyWhitelist = [
          'HR'
        ];

        var whitelisted = false;
        var tagName = el.tagName;

        if (tagName === 'BR') {
          return true
        }

        emptyWhitelist.forEach(item => {
          if (tagName === item || el.querySelector(item)) {
              whitelisted = true;
          };
        });

        return whitelisted
    };

    /** Reduce trailing  's */
    function removeWhitespace (el) {
      const iterator = document.createNodeIterator(el, window.NodeFilter.SHOW_TEXT);
      var textNode;

      while (textNode = iterator.nextNode()) {
        textNode.textContent = textNode.textContent.replace(/(\u00A0){2,}$/, '\u00A0');
      }
    }

    function removeEmptyElement (el) {
      const content = el.textContent && el.textContent.replace(/\u00A0/g, '').trim();

      if (!content) {
          if (hasMedia(el) || naturallyEmpty(el)) {
              return
          }

          el.remove();
          return
      }

      removeWhitespace(el);
    };

    function reduceBrs (userstuff) {
      var el = userstuff.querySelector('br + br + br');

      while (el) {
        el.remove();
        el = userstuff.querySelector('br + br + br');
      }
    }

    function stripTrailingBrs (el) {
      while (el.lastChild && el.lastChild.tagName === 'BR') {
        el.lastChild.remove();
      }
    }

    function stripLeadingBrs (el) {
      while (el.firstChild && el.firstChild.tagName === 'BR') {
        el.firstChild.remove();
      }
    }

    const parent = document.querySelectorAll('.userstuff');

    parent.forEach(userstuff => {
        const allowedTags = [
          'a',
          'abbr',
          'acronym',
          'address',
          'b',
          'big',
          'blockquote',
          'caption',
          'center',
          'cite',
          'code',
          'col',
          'colgroup',
          'dd',
          'del',
          'details',
          'dfn',
          'div',
          'dl',
          'dt',
          'em',
          'figcaption',
          'figure',
          'h1',
          'h2',
          'h3',
          'h4',
          'h5',
          'h6',
          'i',
          'img',
          'ins',
          'kbd',
          'li',
          'ol',
          'p',
          'pre',
          'q',
          'rp',
          'rt',
          'ruby',
          's',
          'samp',
          'small',
          'span',
          'strike',
          'strong',
          'sub',
          'summary',
          'sup',
          'table',
          'tbody',
          'td',
          'tfoot',
          'th',
          'thead',
          'tr',
          'tt',
          'u',
          'ul',
          'var',
          ':empty'
        ]

        allowedTags.forEach(el => {
          userstuff.querySelectorAll(el).forEach((child) => {
            stripLeadingBrs(child);
            stripTrailingBrs(child);
            removeEmptyElement(child);
          })
        });

        reduceBrs(userstuff);
      });
})();