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