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.

  1. // ==UserScript==
  2. // @name AO3 Fix double-spaced paragraphs and text walls
  3. // @description Eliminate gaps from empty/whitespace paragraphs & leading/trailing <br>. Smart split walls of text into spaced paragraphs. Also get rid of justified text.
  4. // @author C89sd
  5. // @version 1.6
  6. // @include /^https:\/\/archiveofourown\.org\/(?:collections\/[^\/]+\/)?(:?works|chapters)\/\d+(?:\/chapters\/\d+)?\/?(?:\?.*)?$/
  7. // @namespace https://greasyfork.org/users/1376767
  8. // @noframes
  9. // ==/UserScript==
  10.  
  11. 'use strict';
  12.  
  13. // Testing:
  14. // https://archiveofourown.org/works/63851347 empty <p>
  15. // https://archiveofourown.org/works/13439460 leading <br>
  16. // https://archiveofourown.org/works/5191202?view_full_work=true formatting
  17. // https://archiveofourown.org/works/44688217/chapters/112432564 split inner <br>
  18.  
  19. const workskin = document.getElementById('workskin');
  20. if (!workskin) return;
  21.  
  22. // Remove justified text
  23. const js = workskin.querySelectorAll('[align="justify"]');
  24. for (const j of js) {
  25. j.align = '';
  26. }
  27.  
  28. const IS_EMPTY = /^\s*$/;
  29. const STARTS_WITH_EM_DASH = /^[\u2013\u2014-]/;
  30. const PRESERVE_BREAK_AFTER = /^(IMG|VIDEO|AUDIO|SOURCE|TRACK|IFRAME|CODE)$/;
  31. function getLastRecursiveChild(el) { return el.lastElementChild ? getLastRecursiveChild(el.lastElementChild) : el; }
  32.  
  33. // Remove gaps in paragraphs
  34. const ps = workskin.querySelectorAll('p');
  35. for (const p of ps) {
  36. // p.innerHTML = "abc;<br>123<br><em>def;</em><em>cde;</em>gdh";
  37. // console.log(p.innerHTML)
  38.  
  39. // Remove gaps from empty paragraphs
  40. if (IS_EMPTY.test(p.textContent)) { p.remove(); continue; }
  41.  
  42. // Remove gaps from leading/trailing <br> nodes or empties
  43. let n;
  44. while ((n = p.firstChild) && (n.tagName === 'BR' || (n.nodeType === 3 && IS_EMPTY.test(n.textContent)))) { // TEXT_NODE (3)
  45. n.remove();
  46. }
  47. while ((n = p.lastChild) && (n.tagName === 'BR' || (n.nodeType === 3 && IS_EMPTY.test(n.textContent)))) { // TEXT_NODE (3)
  48. n.remove();
  49. }
  50.  
  51. // Fast-fail, early rejection if there are no inner <br>,
  52. // like this but non recursive: `if (!p.querySelector('br')) continue;`
  53. let hasBr = false;
  54. for (let n = p.firstChild; n; n = n.nextSibling) {
  55. if (n.tagName === 'BR') { hasBr = true; break; }
  56. }
  57. if (!hasBr) continue;
  58.  
  59. // Assume it's a quote with intended <br> line returns if its parent style is centered.
  60. const textAlign = getComputedStyle(p).textAlign;
  61. if (textAlign === 'center' || textAlign == 'right' || textAlign == 'end') continue;
  62.  
  63. // Assume it's a quote if the last sentence starts with a dash.
  64. if (STARTS_WITH_EM_DASH.test(p.lastChild.textContent)) continue;
  65.  
  66. // Assume it's a quote if the last sentence is a link
  67. if (getLastRecursiveChild(p).tagName === 'A') continue;
  68.  
  69. // Split inner <br>:
  70. // Create a new <p> behind you and shovel nodes into it as you walk.
  71. // Create a new <p> when encountering a <br> to create a gap.
  72. {
  73. let node = p.firstChild;
  74. let newp;
  75. let createnewp = true;
  76. while (node) {
  77. let next = node.nextSibling;
  78. let next2 = next?.nextSibling;
  79.  
  80. let shovel = false;
  81. let shovel2 = false;
  82.  
  83. // It is plausible that the writer intended this <br> as a line return to this adjacent element.
  84. // Keep it by shoveling both at the same time in the new <p>.
  85. if (next && node.tagName === 'BR' && PRESERVE_BREAK_AFTER.test(next.tagName)) { shovel = true; shovel2 = true; }
  86. else if (next && PRESERVE_BREAK_AFTER.test(node.tagName) && next.tagName === 'BR') { shovel = true; shovel2 = true; }
  87.  
  88. // Hit a <br>, delete it and request a new split to create a gap.
  89. else if (node.tagName === 'BR') { p.removeChild(node); createnewp = true; node = next; continue; }
  90.  
  91. // Text-like node (txt,em,i,strong,...), belongs inline while there isn't a <br>. Shovel it in the new <p>.
  92. else { shovel = true; }
  93.  
  94.  
  95. if (createnewp) {
  96. createnewp = false;
  97. // Create a new <p> before the current node, creates a gap.
  98. newp = document.createElement('p');
  99.  
  100. // // NOTE: Moving nodes into this new <p> adds the standard amount of vertical spacing,
  101. // // but we can't be 100% sure that the <br> wasn't on purpose. To mark a difference visually,
  102. // // we can set a slightly lower margin that will still be readable.
  103. // newp.style.margin = '0.8em auto';
  104.  
  105. p.insertBefore(newp, node); // Insert behind
  106. }
  107.  
  108. if (shovel) { shovel = false; newp.appendChild(node); }
  109. if (shovel2) { shovel2 = false; newp.appendChild(next); next = next2; }
  110. node = next;
  111. }
  112. }
  113. }