GoodSoul

NWZonline – Volltext im Original-Style

Nov 12th, 2026
72
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 8.55 KB | None | 0 0
  1. // ==UserScript==
  2. // @name NWZonline – Volltext im Original-Style
  3. // @namespace https://www.nwzonline.de/
  4. // @version 3.0.0
  5. // @description Plus-Artikel vollstaendig im Original-Layout: blockiert Piano und neutralisiert die Paywall-Flags in Fusion.globalContent, bevor die Seite rendert. Kein eigener Reader.
  6. // @match https://www.nwzonline.de/*
  7. // @match https://nwzonline.de/*
  8. // @run-at document-start
  9. // @grant none
  10. // @noframes
  11. // ==/UserScript==
  12.  
  13. (() => {
  14. "use strict";
  15.  
  16. /* ---------- 1) Piano-Tag blockieren, bevor es laden kann ---------- */
  17.  
  18. const BLOCK = /tinypass|piano\.io/i;
  19. const isBlocked = n => n && n.nodeType === 1 && n.tagName === "SCRIPT" && BLOCK.test(n.src || "");
  20. function patch(proto, name) {
  21. const orig = proto && proto[name];
  22. if (!orig) return;
  23. proto[name] = function (...args) {
  24. if (name === "appendChild") return isBlocked(args[0]) ? args[0] : orig.call(this, args[0]);
  25. if (name === "insertBefore") return isBlocked(args[0]) ? args[0] : orig.call(this, args[0], args[1]);
  26. const clean = args.filter(a => !isBlocked(a));
  27. return clean.length ? orig.apply(this, clean) : undefined;
  28. };
  29. }
  30. patch(Node.prototype, "appendChild");
  31. patch(Node.prototype, "insertBefore");
  32. [Element.prototype, Document.prototype, DocumentFragment.prototype].forEach(p => patch(p, "append"));
  33.  
  34. /* ---------- 2) Paywall-Flags neutralisieren (vor dem Rendern) ---------- */
  35.  
  36. function patchGC(gc) {
  37. try {
  38. if (gc && typeof gc === "object" && gc.type === "story") {
  39. gc.isPaid = false;
  40. gc.isMarkedAsPaid = false;
  41. gc.allowDynamicPaywall = false;
  42. gc.isPianoDisabled = true;
  43. }
  44. } catch (e) {}
  45. return gc;
  46. }
  47. let _fusion;
  48. try {
  49. Object.defineProperty(window, "Fusion", {
  50. configurable: true,
  51. get() { return _fusion; },
  52. set(v) {
  53. _fusion = v;
  54. if (v && typeof v === "object") {
  55. let _gc = patchGC(v.globalContent);
  56. try {
  57. Object.defineProperty(v, "globalContent", {
  58. configurable: true,
  59. get() { return _gc; },
  60. set(nv) { _gc = patchGC(nv); }
  61. });
  62. } catch (e) {}
  63. }
  64. }
  65. });
  66. } catch (e) {}
  67.  
  68. function getGC() {
  69. const gc = window.Fusion && window.Fusion.globalContent;
  70. return gc && gc.type === "story" && Array.isArray(gc.elements) ? gc : null;
  71. }
  72.  
  73. /* ---------- 3) Sicherheitsnetz: Paywall-Reste entfernen, Scroll entsperren ---------- */
  74.  
  75. const PW = '.tp-modal,.tp-backdrop,.tp-iframe-wrapper,[class^="tp-"],[id^="tp-"],[class*="ianoWrapper"],iframe[src*="tinypass"],iframe[src*="piano"]';
  76. function sweep() {
  77. document.querySelectorAll(PW).forEach(n => n.remove());
  78. document.querySelectorAll('dialog[open],[role="dialog"]').forEach(n => {
  79. if (/freischalten|Jetzt abonnieren|Abo abschließen/i.test(n.textContent || "")) n.remove();
  80. });
  81. for (const el of [document.body, document.documentElement]) {
  82. if (!el) continue;
  83. el.classList.remove("tp-modal-open", "tp-modal-close");
  84. el.style.removeProperty("overflow");
  85. el.style.removeProperty("position");
  86. }
  87. }
  88. let pending = false;
  89. new MutationObserver(muts => {
  90. let need = false;
  91. for (const m of muts) {
  92. if (m.type === "childList" && m.addedNodes.length) { need = true; break; }
  93. if (m.type === "attributes" && (m.target === document.body || m.target === document.documentElement)) { need = true; break; }
  94. }
  95. if (need && !pending) { pending = true; setTimeout(() => { pending = false; sweep(); }, 50); }
  96. }).observe(document.documentElement, { childList: true, subtree: true, attributes: true, attributeFilter: ["class", "style"] });
  97.  
  98. /* ---------- 4) Fallback: fehlenden Text im Original-Container ergaenzen ---------- */
  99.  
  100. const esc = s => String(s ?? "").replace(/[&<>"']/g,
  101. c => ({ "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[c]));
  102. const plain = s => String(s || "").replace(/<[^>]*>/g, " ").replace(/\s+/g, " ").trim();
  103. const ALLOW = new Set(["P","BR","B","STRONG","I","EM","U","S","SUB","SUP","A","UL","OL","LI","BLOCKQUOTE","H2","H3","H4","SPAN"]);
  104. const DROP = new Set(["SCRIPT","STYLE","IFRAME","OBJECT","EMBED","FORM","LINK","META","NOSCRIPT"]);
  105. function sanitize(html) {
  106. const tpl = document.createElement("template");
  107. tpl.innerHTML = String(html ?? "");
  108. (function walk(node) {
  109. for (const el of Array.from(node.children)) {
  110. walk(el);
  111. if (DROP.has(el.tagName)) { el.remove(); continue; }
  112. if (!ALLOW.has(el.tagName)) { el.replaceWith(...el.childNodes); continue; }
  113. for (const a of Array.from(el.attributes)) {
  114. if (el.tagName === "A" && a.name === "href" && /^(https?:|mailto:|\/)/i.test(a.value)) continue;
  115. el.removeAttribute(a.name);
  116. }
  117. if (el.tagName === "A") { el.target = "_blank"; el.rel = "noopener noreferrer"; }
  118. }
  119. })(tpl.content);
  120. return tpl.innerHTML;
  121. }
  122. function imgUrl(info) {
  123. if (!info || !info.id || !info.auth) return null;
  124. const token = info.auth["1"] || Object.values(info.auth)[0];
  125. return token ? `${location.origin}/resizer/v2/${info.id}.${info.fileExtension || "jpg"}?auth=${token}&quality=80&width=1200` : null;
  126. }
  127. function otherNode(e) {
  128. const t = (e.type || "").toLowerCase();
  129. if (t === "header") { const h = document.createElement("h2"); h.innerHTML = sanitize(e.text || ""); return h; }
  130. if (t === "quote") {
  131. const b = document.createElement("blockquote");
  132. const parts = (e.elements || []).map(q => typeof q === "string" ? q : (q.text || "")).filter(Boolean);
  133. const who = [e.author, e.authorDescription].filter(Boolean).join(" ");
  134. b.innerHTML = parts.map(p => `<p>${sanitize(p)}</p>`).join("") + (who ? `<footer>— ${esc(who)}</footer>` : "");
  135. return b;
  136. }
  137. if (t === "image") {
  138. const info = e.imageInfo || e, url = imgUrl(info), f = document.createElement("figure");
  139. if (url) f.innerHTML = `<img src="${esc(url)}" alt="${esc(info.alt || "")}" loading="lazy" style="max-width:100%;height:auto">` + (info.caption ? `<figcaption>${esc(info.caption)}</figcaption>` : "");
  140. return f;
  141. }
  142. if (t === "list") {
  143. const el = document.createElement(e.list_type === "ordered" ? "ol" : "ul");
  144. el.innerHTML = (e.items || []).map(i => `<li>${sanitize(i.text || i.content || "")}</li>`).join("");
  145. return el;
  146. }
  147. const p = document.createElement("p"); p.innerHTML = sanitize(e.text || e.content || ""); return p;
  148. }
  149.  
  150. function ensureFull(gc) {
  151. const art = document.querySelector('article[data-element-selector="article"]');
  152. if (!art || art.dataset.nwzFull) return;
  153. const texts = gc.elements.filter(e => e.type === "text" && (e.text || "").trim());
  154. if (!texts.length) return;
  155. const probe = plain(texts[texts.length - 1].text).slice(0, 40);
  156. if (probe && art.textContent.includes(probe)) return; // bereits vollstaendig
  157.  
  158. // Container + Absatz-Vorlage aus dem bereits gerenderten Text finden
  159. let container = art, tmpl = null;
  160. for (const p of art.querySelectorAll("p")) {
  161. const pt = plain(p.textContent);
  162. if (pt.length > 40 && texts.some(e => plain(e.text).slice(0, 40) === pt.slice(0, 40))) {
  163. container = p.parentElement; tmpl = p; break;
  164. }
  165. }
  166. const domText = plain(container.textContent);
  167. const frag = document.createDocumentFragment();
  168. let started = false;
  169. for (const e of gc.elements) {
  170. if (e.type === "text") {
  171. const t = plain(e.text);
  172. if (!started) { if (t && domText.includes(t.slice(0, 40))) continue; started = true; }
  173. const p = tmpl ? tmpl.cloneNode(false) : document.createElement("p");
  174. p.innerHTML = sanitize(e.text || "");
  175. frag.appendChild(p);
  176. } else if (started && !["piano", "ad", "newsletterad", "moreitems"].includes((e.type || "").toLowerCase())) {
  177. frag.appendChild(otherNode(e));
  178. }
  179. }
  180. container.appendChild(frag);
  181. art.dataset.nwzFull = "1";
  182. }
  183.  
  184. /* ---------- 5) Timing / SPA-Navigation ---------- */
  185.  
  186. let checkedId = null, timers = [];
  187. setInterval(() => {
  188. const gc = getGC();
  189. if (!gc) return;
  190. const id = gc._id || gc.canonicalUrl || location.href;
  191. if (id === checkedId) return;
  192. checkedId = id;
  193. timers.forEach(clearTimeout);
  194. timers = [2500, 5000, 8000].map(ms => setTimeout(() => { const g = getGC(); if (g) ensureFull(g); }, ms));
  195. }, 800);
  196. })();
Advertisement
Add Comment
Please, Sign In to add comment