AO3 Fix double-spaced paragraphs and text walls

Eliminate gaps from empty/whitespace paragraphs & leading/trailing <br>. Smart split walls of text into spaced paragraphs. Also get rid of justified text.

您需要先安装一个扩展,例如 篡改猴Greasemonkey暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴暴力猴,之后才能安装此脚本。

您需要先安装一个扩展,例如 篡改猴Userscripts ,之后才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey,才能安装此脚本。

您需要先安装用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

// ==UserScript==
// @name        AO3 Fix double-spaced paragraphs and text walls
// @description Eliminate gaps from empty/whitespace paragraphs & leading/trailing <br>. Smart split walls of text into spaced paragraphs. Also get rid of justified text.
// @author      C89sd
// @version     1.6
// @include     /^https:\/\/archiveofourown\.org\/(?:collections\/[^\/]+\/)?(:?works|chapters)\/\d+(?:\/chapters\/\d+)?\/?(?:\?.*)?$/
// @namespace    https://greasyfork.org/users/1376767
// @noframes
// ==/UserScript==

'use strict';

// Testing:
// https://archiveofourown.org/works/63851347                     empty <p>
// https://archiveofourown.org/works/13439460                     leading <br>
// https://archiveofourown.org/works/5191202?view_full_work=true  formatting
// https://archiveofourown.org/works/44688217/chapters/112432564  split inner <br>

const workskin = document.getElementById('workskin');
if (!workskin) return;

// Remove justified text
const js = workskin.querySelectorAll('[align="justify"]');
for (const j of js) {
  j.align = '';
}

const IS_EMPTY = /^\s*$/;
const STARTS_WITH_EM_DASH = /^[\u2013\u2014-]/;
const PRESERVE_BREAK_AFTER = /^(IMG|VIDEO|AUDIO|SOURCE|TRACK|IFRAME|CODE)$/;
function getLastRecursiveChild(el) { return el.lastElementChild ? getLastRecursiveChild(el.lastElementChild) : el; }

// Remove gaps in paragraphs
const ps  = workskin.querySelectorAll('p');
for (const p of ps) {
  // p.innerHTML = "abc;<br>123<br><em>def;</em><em>cde;</em>gdh";
  // console.log(p.innerHTML)

  // Remove gaps from empty paragraphs
  if (IS_EMPTY.test(p.textContent)) { p.remove(); continue; }

  // Remove gaps from leading/trailing <br> nodes or empties
  let n;
  while ((n = p.firstChild) && (n.tagName === 'BR' || (n.nodeType === 3 && IS_EMPTY.test(n.textContent)))) { // TEXT_NODE (3)
    n.remove();
  }
  while ((n = p.lastChild) && (n.tagName === 'BR' || (n.nodeType === 3 && IS_EMPTY.test(n.textContent)))) { // TEXT_NODE (3)
    n.remove();
  }

  // Fast-fail, early rejection if there are no inner <br>,
  //   like this but non recursive: `if (!p.querySelector('br')) continue;`
  let hasBr = false;
  for (let n = p.firstChild; n; n = n.nextSibling) {
    if (n.tagName === 'BR') { hasBr = true; break; }
  }
  if (!hasBr) continue;

  // Assume it's a quote with intended <br> line returns if its parent style is centered.
  const textAlign = getComputedStyle(p).textAlign;
  if (textAlign === 'center' || textAlign == 'right' || textAlign == 'end') continue;

  // Assume it's a quote if the last sentence starts with a dash.
  if (STARTS_WITH_EM_DASH.test(p.lastChild.textContent)) continue;

  // Assume it's a quote if the last sentence is a link
  if (getLastRecursiveChild(p).tagName === 'A') continue;

  // Split inner <br>:
  // Create a new <p> behind you and shovel nodes into it as you walk.
  // Create a new <p> when encountering a <br> to create a gap.
  {
    let node = p.firstChild;
    let newp;
    let createnewp = true;
    while (node) {
      let next  = node.nextSibling;
      let next2 = next?.nextSibling;

      let shovel  = false;
      let shovel2 = false;

      // It is plausible that the writer intended this <br> as a line return to this adjacent element.
      // Keep it by shoveling both at the same time in the new <p>.
      if      (next && node.tagName === 'BR' && PRESERVE_BREAK_AFTER.test(next.tagName)) { shovel = true; shovel2 = true; }
      else if (next && PRESERVE_BREAK_AFTER.test(node.tagName) && next.tagName === 'BR') { shovel = true; shovel2 = true; }

      // Hit a <br>, delete it and request a new split to create a gap.
      else if (node.tagName === 'BR') { p.removeChild(node); createnewp = true;     node = next; continue; }

      // Text-like node (txt,em,i,strong,...), belongs inline while there isn't a <br>. Shovel it in the new <p>.
      else { shovel = true; }


      if (createnewp) {
        createnewp = false;
        // Create a new <p> before the current node, creates a gap.
        newp = document.createElement('p');

          // // NOTE: Moving nodes into this new <p> adds the standard amount of vertical spacing,
          // // but we can't be 100% sure that the <br> wasn't on purpose. To mark a difference visually,
          // // we can set a slightly lower margin that will still be readable.
          // newp.style.margin    = '0.8em auto';

        p.insertBefore(newp, node); // Insert behind
      }

      if (shovel)  { shovel  = false; newp.appendChild(node); }
      if (shovel2) { shovel2 = false; newp.appendChild(next); next = next2; }
      node = next;
    }
  }
}