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.

您需要先安裝使用者腳本管理器擴展,如 TampermonkeyGreasemonkeyViolentmonkey 之後才能安裝該腳本。

You will need to install an extension such as Tampermonkey to install this script.

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyViolentmonkey 後才能安裝該腳本。

您需要先安裝使用者腳本管理器擴充功能,如 TampermonkeyUserscripts 後才能安裝該腳本。

你需要先安裝一款使用者腳本管理器擴展,比如 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;
    }
  }
}