NOTICE: By continued use of this site you understand and agree to the binding Terms of Service and Privacy Policy.
// ==UserScript== // @name KGForum Support // @namespace kgforum.org // @version 2017.12.24 // @copyright 2007-2017, private_lock (http://private-lock.bplaced.net) // @description Diverse kleine Eingriffe in die Seiten des KGForum.org zur Verbesserung der Nutzerfreundlichkeit // @creator private_lock@yahoo.com // @oujs:author private_lock // @homepageURL https://www.kgforum.org/index_5.html // @supportURL http://private-lock.bplaced.net/kgforum // @icon http://private-lock.bplaced.net/kgforum/safe-sex_64x64.gif // @downloadURL https://openuserjs.org/install/private_lock/KGForum_Support.user.js // @updateURL https://openuserjs.org/meta/private_lock/KGForum_Support.meta.js // @include https://*.kgforum.org/* // @include https://kgforum.org/* // @include http://*.kgforum.org/* // @include http://kgforum.org/* // @include https://www.keuschheitsforum.org/* // @include https://keuschheitsforum.org/* // @include http://www.keuschheitsforum.org/* // @include http://keuschheitsforum.org/* // @include https://www.keuschheitsforum.de/* // @include https://keuschheitsforum.de/* // @include http://www.keuschheitsforum.de/* // @include http://keuschheitsforum.de/* // @grant GM.getValue // @grant GM.setValue // @license GPL-3.0 // ==/UserScript== /* Vorweg ------ JavaScript für Erweiterung GreaseMonkey im Internet-Browser Firefox auf den Web-Seiten des kgforum.org Bitte installieren Sie: https://addons.mozilla.org/de/firefox/addon/greasemonkey und laden Sie die @downloadURL mit diesem Skript anschließend erneut zur Installation. Eine bebilderte Anleitung finden Sie auf: http://private-lock.bplaced.net/kgforum Datenschutz-Erklärung --------------------- Mir ist überaus deutlich bewusst, dass die Mehrheit der Menschen, die sich im BDSM-Umfeld bewegen, höllische Angst vor einer unfreiwilligen Bloßstellung haben. Darum benutzen wir (ich eingeschlossen) Pseudonyme, um diese Facette unseres Sexuallebens vor Familie und normalen Freunden zu verbergen. Ich verspreche hiermit, Ihre Privatsphäre nach Kräften zu schützen! Die Verwendung dieses Skriptes soll nach allen technischen Regeln der Kunst nicht dazu gereichen, Sie auffliegen zu lassen. Bitte lesen Sie im folgenden, welche kritischen Punkte es zu bedenken gibt und entscheiden dann, ob die Benutzung für Sie in Frage kommt. Zunächst arbeiten per se alle GreaseMonkey-Skripte lokal im Browser auf dem Rechner des Benutzers mit den Informationen, die der Webserver gesendet hat. Daraus folgt, dass die Skripte nichts erfinden können, was in den ursprünglichen Seiten nicht enthalten ist. Jedoch bietet die systematische Aufbereitung der Seiten einen besseren Überblick und leichteres Verständnis für den Leser und mehr Komfort bei der Wahl der nächsten Aktion. Als Beispiel wird zu jedem Datum der Wochentag eingesetzt. Würde in der Original-Webseite kein Datum stehen, kann eben kein Wochentag bestimmt werden. Mit Kalender neben dem Bildschirm könnte der Benutzer auch dort alle Wochentage nachschlagen. Es ist nur viel bequemer, dem Computer diese eintönige Aufgabe zu überlassen. Das kostet lediglich ein wenig Rechenleistung unterm Schreibtisch des Benutzers, ohne dass der Server beeinträchtigt wird. Einige URLs werden automatisch manipuliert. Aus dem zeitlichen Verhalten sowie den für den Server unbekannten Parametern könnte zurück geschlossen werden, dass der Benutzer dieses Skript einsetzt. Die Auswertung wäre jedoch aufwändig und obendrein ziemlich müßig. Die IP-Adresse des Benutzer ist dem Server eh bekannt, sonst könnten gar keine Daten geschickt werden. Darüber hinaus geben viele sogar ihren Benutzernamen beim Login an und identifizieren sich so gegenüber dem Server. Rein hypothetisch ginge es also lediglich um die zusätzliche Information, ob ein Benutzer so sehr am Forum interessiert ist, dass er sogar ein Skript einsetzt, um die rauen Ecken etwas besser abzurunden? Andererseits besteht für den Server-Betreiber forennet.org auch kein Anlass zur Sorge, denn es werden nur solche Informationen abgerufen, die ohnehin veröffentlicht wurden. Dieses Skript verschafft dem Benutzer also keinerlei Privilegien, die er ohne das Skript nicht auch haben könnte (eiserne Disziplin vorausgesetzt). Des weiteren werden ausdrücklich keine Werbebanner entfernt, denn die finanzieren schließlich einen Großteil der Kosten, die das Forum nun mal verursacht. Etwa einmal pro Woche schaut das Skript nach, ob eine neue Version auf meiner Homepage vorliegt. Ich lade dort nur meine persönliche Homepage hoch, habe aber ansonsten keine Rechte an dem Server. Eine Auswertung, wer wann oder wie oft das Skript verwendet, findet nicht statt und wird es auch in Zukunft nicht geben. Wer mir nicht einmal soweit traut, sollte sich hüten auf seinem Rechner Software auszuführen, die ich entwickelt habe. Schließlich muss das Skript selbst auf Ihrem Rechner gespeichert werden. Außerdem legt es einige Einstellungen in Ihrem Browser ab. D.h. selbst wenn Sie immer fleißig Ihre Historie löschen kann die Installation dieses Skriptes sowie die abgelegten Einstellungen Sie als Leser des kgforum.org kompromittieren, sollte Ihr Rechner oder ein Backup Ihres Firefox-Profils in falsche Hände geraten. Zusammengefasst --------------- 1. Trauen Sie forennet.org nicht, dann sollten Sie deren Seiten einschließlich kgforum.org nicht benutzen. 2. Trauen Sie mir nicht, dann sollten sie meine Software und insbesondere dieses Skript nicht nutzen. 3. Fürchten Sie jemand anders hätte Zugriff auf Ihren Computer, sollten sie den Rechner nicht verwenden. Funktionen ---------- - Automatische Weiterleitung auf eine feste Basis-URL erspart erneutes Anmelden auf allen Domains -> immer Zugriff auf beschränkte Bretter repariert teils kaputte Links in Zitaten - Aussagekräftige, präzise Titel für Fenster bzw, Reiter gemeinsames Präfix KG gefolgt z.B. von dem Titel des Themas oder des Brettes - Permanente direkte Links auf Beiträge nützlich, um auf andere Beiträge Bezug zu nehmen Nummerierung der Beiträge als zusätzliche Orientierung im Thema - Annotation der Wochentage zum Einstelldatum stellt den Kontext her, wenn im Beitrag von "nächsten Freitag" die Rede ist funktioniert für Beiträge, private Nachrichten und Profile - Anklickbare Webseiten in Profilen erspart es, den Text markieren und händisch in die Adressleiste kopieren zu müssen - Such-Knopf nach allen Beiträgen in Profilen besser zu sehen, als der vorhandene Link liefert ca. 5 Mal mehr Ergebnisse als der vorhandene Link, die weiter in die Vergangenheit reichen -> schwankt je nach gesuchtem Benutzer -> entspricht dem manuellen Aufruf des Suchformulars unter Eingabe des Benutzernamens - Löschlinks für Avatare unterm Bild im Profil -> TODO http://kgforum.org/display_5_2399_74116_760618.html#760618 - Suchergebnisse auch auf folgenden Seiten markieren und beim Blättern erhalten in einem mehr als 20 Beiträge langen Thema können so weitere Fundstellen auch auf anderen Seiten betrachtet werden - Zeitstempel/Benutzer auswerten, um vom Suchergebnis direkt zum Beitrag zu springen bei mehrseitigen Themen wird automatisch die richtige Seite geladen funktioniert sowohl aus Suchergebnissen als auch aus der Brett-Übersicht heraus - Hervorhebungen für nachträglich veränderte Beiträge falls nicht der ursprüngliche Einsteller den Text editiert hat -> Eingriff durch Moderatoren / Staffs falls mehr als 10 Minuten seit dem ersten Einstellen vergangen sind -> möglicherweise nicht nur Tippfehler behoben, sondern den Sinn verändert -> folgende Beiträge könnten sich auf den ursprünglichen Sinn beziehen - Visualisieren von zeitlichen Lücken zwischen Beiträgen Dicke des Trennbalkens nimmt mit der Länge der Pause zu. Autoren von Beiträgen vor dem Trennbalken antworten evtl. nicht mehr oder revidieren alte Aussagen. - Berechnung der Anzahl der Lesungen pro Beitrag in der Brett-Übersicht - Persönliche Historie der bereits gelesenen Beiträge Wird ein Thema zum ersten Mal geöffnet oder länger als 60 Sekunden betrachtet, gilt es als gelesen. Der letzte Beitrag wird lokal im Browser vermerkt. Themen mit neuen Beiträgen, die noch nicht mindestens 60 Sekunden lang betrachtet wurden erscheinen grün. In der Übersicht eines Brettes werden beobachtete Themen, in denen sich seither nichts getan hat, türkis markiert. Unbeobachtete Themen oder solche, die älter sind bleiben grau. Letzte Beiträge je Forum und die neusten 40 in Übersichtsseite werden entsprechend Lesestatus eingefärbt. Unabhängig davon bleibt die Funktion der Forensoftware erhalten, ein gelb-schwarzes "new" hinter Beiträgen zu zeigen, die seit dem letzten Login noch nicht gelesen wurden. Einzelne Lesezeichen können im Thema manuell gelöscht werden (mit Sicherheitsabfrage) - Aus der Brett-Übersicht direkt zum Lesezeichen springen, ohne die richtige Seiten wählen zu müssen. - Einstellungen werden in eine SQLite-Datenbank im Installationsverzeichnis des Skriptes im Firefox-Profil gespeichert installiere Firefox-Erweiterung: https://addons.mozilla.org/de/firefox/addon/sqlite-manager Menü Hilfe > Informationen zur Fehlerbehebung > Profilverzeichnis > Ordner öffnen > Unterordner "gm_scripts" Menü Extras > SQLite Manager > neues Fenster Menü Datenbank > Mit Datenbank verbinden Filter von "SQLite-DB-Dateien (*.sqlite)" auf "Alle Dateien" stellen <Profilverzeichnis>/gm_scripts/KGForum_Support.db Die Daten für dieses Skript liegen in der Tabelle "scriptvals" (sortiert nach Spalte "name"): aenderungen_markieren : 10 # Zeit in Minuten, ab der ein nachträglich editierter Beitrag markiert wird. beitraege_pro_seite : 20 font_size_offset : 0 # Zahl etwa von 0 bis 3 mit der Bedeutung: größerer Wert = größere Schrift lese_verzoegerung : 60 # Zeit in Sekunden, nach der eine Seite eines Themas als gelesen gilt. zuletzt_geschrieben : # Zeitstempel in Millisekunden seit 1.1.1970 zur Synchronisation mehrerer Tabs zzz_00 - zzz_ff : # Zeichenketten mit Komma-separierter Liste von zur Basis 36 kodierten Zahlen Beispiel : zzz_01 = "...,9j 3 fbxp dfg0o,..." Themen-ID : (9j)b36 * (100)b16 + (zzz_01)b16 = (9*36 + 19) * 256 + 1 = 87.809 Anzahl-Beiträge : (3)b36 = 3 ID letzter Beitrag : (fbxp)b36 = 15*36³ + 11*36² + 33*36 + 25 = 715.309 Datum letzter Beitr.: (dfg0o)b36 = 13*36^4 + 15*36³ + 16*36² + 24 = 22.555.608 Minuten seit 1.1.1970 UTC http://kgforum.org/display_5__87809_715309.html#715309 am Mo 19.11.2012 15:48 GMT+1 Außerdem finden sich dort überhaupt alle vom Skript gespeicherten Daten. - Plus-Minus-Button für die Schriftgröße +A/a- -> TODO http://kgforum.org/display_5_2388_88090.html - Zitate in vorangegangenen Beiträgen suchen (nur auf der gleichen Seite) Die Druckansicht enthält alle Beiträge des Themas und kann damit auch Zitate über Seitengrenzen zuordnen. Unterschiede einfärben, falls nicht zitierter unbekannter Text eingefügt wurde. - Automatische Updates dieses Skriptes Einmal die Woche prüft GreaseMonkey in der Grundeinstellung, ob eine neue Version verfügbar ist. - Dokumentation der Funktionen mittels Screenshots Als Gast anmelden -> ACHTUNG: Beim Abmelden wird das Heimverzeichnis des Gast in /tmp/guest-XXXXXX gelöscht Fenstergröße auf XGA 1024:768 -> nicht in die Ecke schieben, denn dann fehlen ein oder zwei Pixel in der Breite Fenster-Schatten beachten Galerie auf meine Homepage stellen Neues Thema. Erster Beitrag als Übersicht, jeder weitere Beitrag ein Kapitel mit Bild und Erklärung Zuerst im Moderatoren Brett vorstellen, Feedback der anderen einholen - Statistik in der Brett-Übersicht (Summen: Themen / Beiträge) - Beitrags und PN-Editor verbessert: (TODO https://www.kgforum.org/display_5_2416_83101_671063.html#671063) - an Cursorposition einfügen (function AddText() in https://www.kgforum.org/images/theme1/codebuttons.js) - Text-Auswahl beachten (im Editor) - Shadow, Glow & Flash rausnehmen - Durchgestrichen - Gedankenstrich - Formatierung zurücksetzen - leeres Tabellen-Gerüst einfügen - Links in Beiträgen auf andere Threads einfärben und auf preferredDomain umbiegen - Dialog-Modus für PN als Historie unter Antwort-Editor -> TODO http://kgforum.org/display_5_2416_92324.html Ideen für weitere Versionen --------------------------- - Diese Kommentare aus dem Skript auf die Webseite verpflanzen (trennen in private Mindmap und öffentlich) - Alle Screenshots neu aufnehmen (z.B. geändertes Datumsformat mit 4-stelliger Jahreszahl) - Link für "setze Lesezeichen bis hier her" hinterm Titel eines jeden Beitrages - Dialog für Einstellungen via GM.registerMenuCommand() - tagArray und imgArray refaktorieren und in XPath-Ausdrücke umwandeln - gotoUnread blättert in der Brett-Übersicht / speichert Anzahl gelesener Themen - Suchbegriffe auch erhalten, wenn durch das Brett geblättert wird - Die preferredDomain aus dem ersten Aufruf lernen und konfigurieren als Einblendung in Einstellungen - Vordergrund/Hintergrund/Unterstreichen für Links definieren -> TODO http://kgforum.org/display_5_2416_81098.html - Lesezeichen als Ersatz für Themen-Abos -> TODO http://kgforum.org/display_5_2416_91456.html - Sortiere Beiträge innerhalb eines Themas nach postId http://kgforum.org/display_5_2409_69300_512812.html#512812 - Threads in Suchergebnissen einfärben - Zensierte Worte zählen in Geschichten, z.B. als Anteil aller Worte - Schritfgröße durch onclick ohne Seite neu laden - erste Abfrage liefert möglichereise nur neuste PN (gleiche Abfrage wiederholen, um auch alte zu sehen) - Betreff einer PN in Editor einsetzen als Re, wenn Beitrags-ID & Beitrag bekannt - Zitieren-Buttons fügen an Editor an (Lade als Unterseite und kopiere den Text) - Zitate in den PN zuordnen - Drag'N'Drop Griff, um die Breite = Zeilenlänge einer Beitragszelle zu verkleinern (und alle folgenden) - gleiche Farbe erneut auf andere Selektion anwenden -> nicht nur auf selection-change hören Bekannte Fehler: ---------------- - Keine Historie der PN unter PN-Editor -> XMLHttpRequest in GreaseMonkey ??? evtl. in die Seite injecten? - async Aufrufe kehren sofort zurück -> Fehlermeldung unterdrückt & falsche Zeitmessung */ // anonyme Funktion um das Skript vorzeitig via Rücksprung abbrechen zu können. // Außerdem ein bequemer Weg, alles in den strengen Modus zu heben (async function() { "use strict"; var domainList =["kgforum.org", "keuschheitsforum.org", "keuschheitsforum.de", ], preferredDomain = domainList[0], preferredHost = "https://www." + preferredDomain; // Springe immer automatisch auf die bevorzugte Domain // TODO probiere Tag, dass das Skript ausführt, bevor die Seite geladen wurde // http://wiki.greasespot.net/Metadata_Block if (( window.location.hostname != preferredHost.replace(/^.*\/\//, "") || window.location.protocol != preferredHost.replace(/\/\/.*$/, "") ) && window.location.protocol != "file:") { if ( window.location.hostname.search(/chat/i) < 0 && window.location.hostname.search(/treffen/i) < 0 && window.location.hostname.search(/directory/i) < 0 && window.location.hostname.search(/settings/i) < 0 ) { // location.replace(...) erzeugt keinen zusätzlichen Eintrag in der Historie des Browsers -> transparent window.location.replace(("" + window.location).replace(/^[a-z]*:\/\/[^\/]+\//, preferredHost + "/")); } return; } // erfasse Verarbeitungszeit var startTime = new Date().getTime(), i, j; var fehlermeldung = document.createElement("P"); fehlermeldung.innerHTML = "Fehler in KGForum_Support.user.js bitte melden an <a href='mailto:private_lock@yahoo.com" + "?subject=" + encodeURI("Fehler in KGForum_Support.user.js") + "&body=" + encodeURI("Fehler auf Seite:\n" + window.location + "\n\nFirefox Menü > Entwicklerwerkzeuge > Browser-Konsole meldet:\n???").replace(/[&#]/g, "@") + "'>private_lock</a>"; document.body.appendChild(fehlermeldung); function logTime() { var now = new Date(); fehlermeldung.innerHTML = "Zeit: " + now + " KGForum_Support.user.js: " + ((now.getTime() - startTime) / 1000) + " Sekunden"; } // globale reguläre Ausdrücke statisch extrahiert var reg_quot = /\\"/g, reg_space = / /; // z.B. führende Nullen ergänzen function padding(zahl, character, length) { var string = "" + zahl; while (string.length < length) { string = character + string; } return string; } // baue eine Zeichenkette zusammen indem Vorkommen von %s durch weitere Argumente ersetzt werden var reg_prozentNewLine = /%n/g, reg_percent = /%/, reg_parameter = /((\d+\$|<\$)?(\d+)?s)/; function format(string) { var blocks = string.replace(reg_prozentNewLine, "\n").split(reg_percent); var result = blocks[0]; var index = 1; for (var i = 1; i < blocks.length; i++) { if (blocks[i].length === 0) { // doppeltes Prozent-Zeichen hebt sich weg result += "%" + blocks[++i]; continue; } if (blocks[i].search(reg_parameter) !== 0) { return "INVALID FORMAT SPECIFIED: '%" + blocks[i] + "'"; } // wählt ein bestimmtes Argument (mehrfach) aus var dollar = RegExp.$2; // Breite, auf die das Argument mindestens gebracht werden soll durch voranstellen von Nullen/Leerzeichen var width = RegExp.$3; result += padding( arguments[dollar ? dollar == "<$" ? index - 1 : parseInt(dollar.substr(0, dollar.length - 1)) : index++], width && width[0] == "0" ? "0" : " ", width ? parseInt(width) : 0) + blocks[i].substr(RegExp.$1.length); } return result; } // console.log("formatTest: '" + format("%smy%%string %1$s %02s%nx%3sx %s", "a", "b", "c") + "'"); var reg_date = /(.*?)((\d{1,2})\.(\d{1,2})\.(\d{2,4})[ _um]+(\d{1,2}:\d{2}).*?)/; function parseDate(text) { if (text && text.search(reg_date) >= 0) { // *NICHT* via format(...) -> weitere reguläre Teilausdrücke für den Aufrufer erhalten! return new Date(RegExp.$4 + "/" + RegExp.$3 + "/" + (RegExp.$5.length != 2 ? "" : RegExp.$5[0] > "7" ? "19" : "20") + RegExp.$5 + " " + RegExp.$6); } return null; } function formatDate(date) { return format("%02s.%02s.%04s %02s:%02s", date.getDate(), date.getMonth() + 1, date.getFullYear(), date.getHours(), date.getMinutes()); } // http://de.selfhtml.org/xml/darstellung/xpathsyntax.htm function viaXpath(path, node, ordered) { return document.evaluate(path, node ? node : document, null, ordered ? XPathResult.ORDERED_NODE_SNAPSHOT_TYPE : XPathResult.UNORDERED_NODE_SNAPSHOT_TYPE, null); } // https://developer.mozilla.org/en-US/docs/Introduction_to_using_XPath_in_JavaScript#First_Node function viaXpath0(path, node, ordered) { return document.evaluate(path, node ? node : document, null, ordered ? XPathResult.FIRST_ORDERED_NODE_TYPE : XPathResult.ANY_UNORDERED_NODE_TYPE, null).singleNodeValue; } // Füge Behandlung von Umlauten zu Original-doHighlight(...) hinzu (darf kein '' enthalten!) function doHighlightReplace(bodyText, searchTerm, highlightStartTag, highlightEndTag) { /* ACHTUNG: Diese Funktion wird serialisiert -> keine Zeilenende-Kommentare, nur "" für Zeichenketten! * Es wird in die Seite injiziert -> kein Zugriff auf den GreaseMonkey-Kontext hier im Skript! */ if (!searchTerm) { /* verlasse die Methode bevor es zur Endlosschleife kommt! */ return bodyText; } /* verfremdete Umlaute aus dem Suchbegriff in der URL wiederherstellen * umständen umst%C3%A4nden * zerstörung zerst%C3%B6rung * schlüssel schl%C3%BCssel * schließen schlie%C3%9Fen */ searchTerm = searchTerm .replace(/\u00C3\u00A4/g, "ä") .replace(/\u00C3\u00B6/g, "ö") .replace(/\u00C3\u00BC/g, "ü") .replace(/\u00C3\u0178/g, "ß") ; /* Ausgabe der übergebenen Parameter */ /* bodyText += "<BR>-" + searchTerm + "-"; */ return window.doHighlightOriginal(bodyText, searchTerm, highlightStartTag, highlightEndTag); } var umlautHighLight = document.createElement("SCRIPT"); umlautHighLight.type = "text/javascript"; umlautHighLight.innerHTML = 'window.doHighlightOriginal = window.doHighlight; window.doHighlight = ' + doHighlightReplace + ';'; document.head.appendChild(umlautHighLight); // ***************************************** // ***** BEGINN: Globale Konfiguration ***** // ***************************************** // Verzögerung in Sekunden zwischen dem Öffnen eines Themas und es als gelesen markieren. var lese_verzoegerung = await GM.getValue("lese_verzoegerung"); if (!lese_verzoegerung) { lese_verzoegerung = 1 * 60; GM.setValue("lese_verzoegerung", lese_verzoegerung); } // Minuten, die für nachträgliche Änderungen an einem Beitrag verstreichen dürfen, bevor er gelb markiert wird var aenderungen_markieren = await GM.getValue("aenderungen_markieren"); if (!aenderungen_markieren) { aenderungen_markieren = 10; GM.setValue("aenderungen_markieren", aenderungen_markieren); } var beitraege_pro_seite = await GM.getValue("beitraege_pro_seite"); if (!beitraege_pro_seite) { beitraege_pro_seite = 20; GM.setValue("beitraege_pro_seite", beitraege_pro_seite); } var font_size_offset = await GM.getValue("font_size_offset", 0); if (window.location.search.search(/font-size=(-?[0-9]+)/) != -1) { font_size_offset = parseInt(RegExp.$1); GM.setValue("font_size_offset", font_size_offset); } var debug = await GM.getValue("debug"); // aktualisieren, wenn die entsprechende Seite der Einstellungen im Forum geöffnet wird if (window.location.search == "?action=mysite&mysite=settings&forumid=5") { // Beim Ansehen der Einstellungen den aktuellen Wert merken (beim Ändern wird hinterher diese Seite neu geladen) var val = viaXpath0("//INPUT[@name='setbpt' and @value]").value; if (parseInt(val) != beitraege_pro_seite) { alert(format("Abweichende Anzahl Beiträge pro Seite %s wird ignoriert! Verwende Standard %s", val, beitraege_pro_seite)); // GM.setValue("beitraege_pro_seite", beitraege_pro_seite = parseInt(val)); } } // *************************************** // ***** ENDE: Globale Konfiguration ***** // *************************************** // Behandlung der Schriftgröße // ACHTUNG: Keine Zahlenwerte für die Schriftgröße verwenden! // ACHTUNG: Schriftgröße einstellen, bevor Thema zu Beitrag scrollt -> Verschiebung bei Größenänderung der Elemente function applyFontSize(root) { var fonts = viaXpath(".//FONT[@size]", root); for (i = 0; i < fonts.snapshotLength; i++) { var f = fonts.snapshotItem(i); f.size = parseInt(f.size) + font_size_offset; } } applyFontSize(document.body); // biete Buttons für die Schriftgröße an var config = viaXpath0( "//IMG[contains(@src,'/images/theme1/home.gif')]//ancestor::TD/A[last()]"); if (config) { var control = document.createElement("FONT"); control.size = 3 + font_size_offset; control.innerHTML = "<A href='?font-size=" + (font_size_offset + 1) + "' title='Schrift vergrößern' >+A</A> / " + "<A href='?font-size=" + (font_size_offset - 1) + "' title='Schrift verkleinern'>a-</A>"; config.parentNode.insertBefore(control, config.nextSibling); } // Sprung in die Privaten Nachrichten relativieren config = viaXpath0("//FONT/FONT//A[contains(text(), 'neue Nachricht') and contains(@href, '&mysite=pm')]"); if (config) { config.href = config.href.replace(/.*(\/)/, "$1"); } // Umlaut-Fehler korrigieren var option = viaXpath("//SELECT/OPTION"); var reg_ae = /\u00C3\u00A4/g, reg_oe = /\u00C3\u00B6/g, reg_ue = /\u00C3\u00BC/g, reg_sz = /\u00C3\u0178/g; for (i = 0; i < option.snapshotLength; i++) { var o = option.snapshotItem(i); o.innerHTML = o.innerHTML .replace(reg_ae, "ä") .replace(reg_oe, "ö") .replace(reg_ue, "ü") .replace(reg_sz, "ß") ; } // Ergänze den Wochentag zum Datum const wochentag = [" Sonntag ", " Montag ", " Dienstag ", " Mittwoch ", " Donnerstag ", " Freitag ", " Samstag "]; function showDayOfWeek() { var datum = document.getElementsByTagName("B"); for (var i = 0; i < datum.length; i++) { if (datum[i].innerHTML == "Datum:") { var t = datum[i].nextSibling; var date = parseDate(t.data); if (date) { t.data = wochentag[date.getDay()] + formatDate(date); } } } } const millisDay = 86400000; function formatDateDifference(toShow, reference) { var difference = new Date(Math.abs(reference - toShow)); var tage = Math.floor(difference.getTime() / millisDay); return format("%s%s%n%s%s%s:%02s", wochentag[toShow.getDay()], formatDate(toShow), toShow < reference ? "+" : "-", tage === 0 ? "" : tage + " Tage ", difference.getUTCHours(), difference.getUTCMinutes()); } // In den privaten Nachrichten die Wochentage einblenden. if (window.location.search.search(/mysite=(mysite|pm|delentry)/i) > 0) { document.title = "KG-Postfach"; showDayOfWeek(); logTime(); return; } // alle Attribute und Funktionen eines Objektes ausgeben function analyzeObjectNode(node) { if (!node) { return "" + node; } function Prop(name, value) { this.name = name; this.value = value; this.toString = function() { return this.name + (this.name.length < 8 ? "\t" : "") + "\t= " + this.value; } } function sort(a, b) { a = a.name.toLowerCase(); b = b.name.toLowerCase(); if (a < b) { return -1; } if (b < a) { return 1; } return 0; } var attrs = new Array(); var nulls = new Array(); var funcs = new Array(); var nativ = new Array(); for (var property in node) { var str = node[property]; if (str == null) { nulls.push(new Prop(property, str)); } else { str = "" + str; if (0 == str.search("function ")) { if (0 < str.search("[native code]")) { nativ.push(new Prop(str.replace(/\s+(\[native code\])\s+/, "\t$1 "), "")); } else { funcs.push(new Prop(str, "// " + property + " ENDE")); } } else { attrs.push(new Prop(property, str)); } } } var result = node + "\n\n"; result += attrs.length == 0 ? "" : "===== Attribute: =====\n\n"; attrs.sort(sort); result += attrs.join("\n"); result += nulls.length == 0 ? "" : "\n\n===== Attribute ohne Wert: =====\n\n"; nulls.sort(sort); result += nulls.join("\n"); result += funcs.length == 0 ? "" : "\n\n===== Funktionen: =====\n\n"; funcs.sort(sort); result += funcs.join("\n\n"); result += nativ.length == 0 ? "" : "\n\n===== native Funktionen: =====\n\n"; nativ.sort(sort); result += nativ.join("\n"); return result; } function pimpEditor() { // https://developer.mozilla.org/de/docs/Web/API/HTMLTextAreaElement function insertMetachars(sStartTag, sEndTag) { var bDouble = arguments.length > 1, oMsgInput = document.creator.message, nSelStart = oMsgInput.selectionStart, nSelEnd = oMsgInput.selectionEnd, sOldText = oMsgInput.value; oMsgInput.value = sOldText.substring(0, nSelStart) + (bDouble ? sStartTag + sOldText.substring(nSelStart, nSelEnd) + sEndTag : sStartTag) + sOldText.substring(nSelEnd); oMsgInput.setSelectionRange( bDouble || nSelStart === nSelEnd ? nSelStart + sStartTag.length : nSelStart, (bDouble ? nSelEnd : nSelStart) + sStartTag.length); oMsgInput.focus(); } function insertQuestion(frage, vorgabe, sStartTag, sEndTag) { if (sStartTag.indexOf("[") < 0 && sStartTag.indexOf("<") < 0) { sStartTag = "[" + sStartTag + "]"; } if (!sEndTag) { sEndTag = sStartTag.replace(/([\[<])/, "$1/"); } var param = frage ? prompt(frage, vorgabe) : vorgabe; if (null == param) { // Abbrechen gedrückt return; } else if (0 < param.length) { sStartTag = sStartTag.replace(/([\]>])/, "=" + param + "$1") } insertMetachars(sStartTag, sEndTag); } function insertClear() { var oMsgInput = document.creator.message, nSelStart = oMsgInput.selectionStart, nSelEnd = oMsgInput.selectionEnd, sOldText = oMsgInput.value; var prefix = sOldText.substring(0, nSelStart), selection = sOldText.substring(nSelStart, nSelEnd), suffix = sOldText.substring(nSelEnd); if (nSelStart === nSelEnd) { prefix = prefix.replace(/\[[^\]]*\]/g, "").replace(/<[^>]*>/g, ""); suffix = suffix.replace(/\[[^\]]*\]/g, "").replace(/<[^>]*>/g, ""); } else { selection = selection.replace(/\[[^\]]*\]/g, "").replace(/<[^>]*>/g, ""); } oMsgInput.value = prefix + selection + suffix; oMsgInput.setSelectionRange(prefix.length, prefix.length + selection.length); oMsgInput.focus(); } function insertTable() { var oMsgInput = document.creator.message, nSelStart = oMsgInput.selectionStart, nSelEnd = oMsgInput.selectionEnd, sOldText = oMsgInput.value, structure; var selection = ""; if (nSelStart === nSelEnd) { do { selection = prompt("Wie viele Zeilen / Spalten soll die Tabelle haben?", "3/3"); if (null == selection) { return; } } while (!selection.match(/^ *(\d+) *\/ *(\d+) *$/)) structure = []; var line = ""; for (var j = 0; j < parseInt(RegExp.$2); j++) { line += " "; } for (var i = 0; i < parseInt(RegExp.$1); i++) { structure.push(line); } } else { structure = sOldText.substring(nSelStart, nSelEnd).split("\n"); } selection = ""; for (var line of structure) { selection += "[tr]"; for (var cell of line.split(" ")) { selection += "[td]" + cell + " [/td]"; } selection = selection.replace(/(\[\/td\])$/, "\n$1") + "[/tr]"; } selection = "\n[table] " + selection + "[/table]\n"; oMsgInput.value = sOldText.substring(0, nSelStart) + selection + sOldText.substring(nSelEnd); oMsgInput.setSelectionRange(nSelStart, nSelStart + selection.length); oMsgInput.focus(); } // absolute Skript-URL ohne https in relative umwandeln -> gleicher Server wird nicht blockiert var cb1 = viaXpath0("//FORM//SCRIPT[contains(@src, '/images/theme1/codebuttons.js')]"); if (!cb1) { return; } var cb2 = document.createElement("SCRIPT"); cb2.type = "text/javascript"; // cb2.src = "/images/theme1/codebuttons.js"; cb2.innerHTML = "<!--\n" + insertMetachars + "\n" + insertQuestion + "\n" + insertClear + "\n" + insertTable + "\n-->"; cb1.parentNode.insertBefore(cb2, cb1); cb1.parentNode.removeChild(cb1); // BBC-Syntax-Hilfe: https://www.kgforum.org/?action=help&forumid=5 for (var textAttr of ["color", "size", "font"]) { cb1 = viaXpath0("//FORM//SELECT[@name='" + textAttr + "' and @onchange]"); cb1.setAttribute("onchange", cb1.getAttribute("onchange").replace( new RegExp("show" + textAttr + "(.+)"), "insertQuestion(null, $1, '" + textAttr + "')")); } viaXpath0("//FORM//A[@href='javascript:bold()']" ).href = "javascript:insertMetachars('[b]','[/b]')"; viaXpath0("//FORM//A[@href='javascript:italicize()']").href = "javascript:insertMetachars('[i]','[/i]')"; viaXpath0("//FORM//A[@href='javascript:underline()']").href = "javascript:insertMetachars('[u]','[/u]')"; viaXpath0("//FORM//A[@href='javascript:center()']" ).href = "javascript:insertMetachars('\\n[center]\\n','\\n[/center]\\n')"; viaXpath0("//FORM//A[@href='javascript:hyperlink()']").href = "javascript:insertQuestion('Web-Adresse (leer fügt nur Tags ein)', 'http://www.','url')"; viaXpath0("//FORM//A[@href='javascript:meiler()']" ).href = "javascript:insertQuestion('Email-Adresse (leer fügt nur Tags ein)', '@','email')"; viaXpath0("//FORM//A[@href='javascript:image()']" ).href = "javascript:insertMetachars('\\n[img]','[/img]\\n')"; viaXpath0("//FORM//A[@href='javascript:showcode()']" ).href = "javascript:insertMetachars('\\n[code]\\n','\\n[/code]\\n')"; viaXpath0("//FORM//A[@href='javascript:quote()']" ).href = "javascript:insertMetachars('\\n[quote]\\n','\\n[/quote]\\n')"; viaXpath0("//FORM//A[@href='javascript:move()']" ).href = "javascript:insertMetachars('\\n[move]','[/move]\\n')"; cb1 = viaXpath0("//FORM//A[@href='javascript:list()']"); cb1.href = "javascript:insertQuestion('Aufzählung: leer=bullet 1=1,2,3 A=A,B,C a=a,b,c I=I,II,III i=i,ii,iii', '', '\\n[list]')"; cb2 = cb1.cloneNode(); cb2.innerHTML = "[*]" cb2.title = "Aufzählungs-Punkt einfügen"; cb2.href = "javascript:insertMetachars('\\n[*]', '')"; cb1.parentNode.insertBefore(cb2, cb1.nextSibling); cb1 = viaXpath0("//FORM//A[@href='javascript:shadow()']") cb1.href = "javascript:insertMetachars('<font style=\u0022text-decoration:line-through\u0022>','</font>')"; cb1.title = "Durchgestrichen"; cb1 = viaXpath0("//FORM//A[@href='javascript:glow()']"); cb1.href = "javascript:insertMetachars('\\n-----\\n')"; cb1.title = "Gedankenstrich"; cb1 = viaXpath0("//FORM//A[@href='javascript:flash()']"); cb1.href = "javascript:insertClear()"; cb1.title = "Formatierung zurücksetzen"; cb2 = cb1.cloneNode(); cb2.innerHTML = "[table]"; cb2.title = "Tabelle einfügen"; cb2.href = "javascript:insertTable()"; cb1.parentNode.insertBefore(cb2, cb1.nextSibling); cb1.parentNode.insertBefore(document.createTextNode(" "), cb2); cb1 = viaXpath("//FORM//MAP//AREA[@href and @onclick]"); var reg_smilie = /document.+\+=(.+);/g; for (var i = 0; i < cb1.snapshotLength; i++) { cb2 = cb1.snapshotItem(i); cb2.setAttribute("onclick", "javascript:" + cb2.getAttribute("onclick").replace(reg_smilie, "insertMetachars($1);")); } } // *************************************************** // ***** BEGINN: Verwalte Lesezeichen für Themen ***** // *************************************************** const bits = 8; const mask = (1 << bits) - 1; // Zeitstempel, wann der Cache zuletzt geschrieben wurde (um festzustellen, ob meine Kopie veraltet ist) var known_age = null; async function updateNeeded() { var age = parseInt(await GM.getValue("zuletzt_geschrieben")); if (!age) { // Falle zurück auf das aktuelle Datum, wenn bislang keines gesetzt war known_age = new Date().getTime(); GM.setValue("zuletzt_geschrieben", known_age.toString()); } else if (!known_age) { known_age = age; } else if (known_age < age) { known_age = age; return true; } return false; } function Entry(threadId, maxCount, postId, lastDate) { this.threadId = threadId; this.maxCount = maxCount ? maxCount : 0; this.postId = postId ? postId : 0; this.lastDate = lastDate ? lastDate : 0; const base = 36; this.encode = function() { return Math.floor(this.threadId >> bits).toString(base) + " " + this.maxCount.toString(base) + " " + this.postId.toString(base) + " " + Math.floor(this.lastDate ? this.lastDate / 60000 : 0).toString(base); }; this.decode = function(lo, line) { line = line.split(reg_space); this.threadId = (parseInt(line[0], base) << bits) + lo; this.maxCount = parseInt(line[1], base); this.postId = parseInt(line[2], base); this.lastDate = parseInt(line[3], base) * 60000; return this; }; this.toString = function() { return "threadId=" + this.threadId + " maxCount=" + this.maxCount + " postId=" + this.postId + " lastDate=" + new Date(this.lastDate); }; } function key(threadId) { return "zzz_" + padding((threadId & mask).toString(16), 0, 2); } var reg_comma = /,/; async function readCache(threadId) { var cache = []; var toParse = await GM.getValue(key(threadId)); if (toParse) { toParse = toParse.split(reg_comma); var lo = threadId & mask; for (var i = 0; i < toParse.length - 1; i++) { var entry = new Entry().decode(lo, toParse[i]); // Argh - assoziative Arrays wandeln Zeichenketten mit Ziffern wieder zurück in Zahlen // -> Länge z.B. 90.000 für drei Einträge cache["t" + entry.threadId] = entry; } } return cache; } async function writeCache(threadId, cache) { var toSave = ""; for (var tid in cache) { var entry = cache[tid]; if (entry && entry.maxCount > 0) { toSave += entry.encode() + ","; } } if (await updateNeeded()) { console.log("Concurrent modification on cache for " + threadId); } else { known_age = new Date().getTime(); GM.setValue("zuletzt_geschrieben", known_age.toString()); GM.setValue(key(threadId), toSave); } } // Ein Cache für die eingelesenen Informationen über früher geöffnete Themen (kann Promisses enthalten) var known_threads = []; async function getCache(threadId) { var lo = threadId & mask; var cache = null; if (await updateNeeded()) { // verwerfe veralteten Cache // delete known_threads; known_threads = []; } else { cache = known_threads[lo]; } if (!cache) { // Kein Treffer, es wird neu eingelesen. cache = known_threads[lo] = readCache(threadId); } return cache; } async function getThreadEntry(threadId) { var result = (await getCache(threadId))["t" + threadId]; // inlining result causes strange warning on loading a board-overview // reference to undefined property getCache(...)[("t" + threadId)] return result; } async function getOrCreateThreadEntry(threadId) { var cache = await getCache(threadId); var key = "t" + threadId; var entry = cache[key]; return entry ? entry : cache[key] = new Entry(threadId); } async function saveThreadEntry(entry) { if (entry.threadId) { // Nur schreiben, falls danach var cache = await getCache(entry.threadId); var key = "t" + entry.threadId; if (cache[key] != entry) { delete cache[key]; cache[key] = entry; } writeCache(entry.threadId, cache); } } var deleteParameter = null; var safeTimer = null; async function deleteEntry() { var cache; if (deleteParameter && (cache = await getCache(deleteParameter))) { var key = "t" + deleteParameter; var entry = cache[key]; if (entry && window.confirm("Lesezeichen für diesen Thema wirklich löschen?\n" + entry)) { if (safeTimer) { window.clearTimeout(safeTimer); safeTimer = null; } cache[key] = null; // delete entry; writeCache(deleteParameter, cache); } } } // ************************************************* // ***** ENDE: Verwalte Lesezeichen für Themen ***** // ************************************************* /** * @param string1 the first string to compare (null is identified with the empfy string) * @param string2 the second string to compare * @param maxDistance shortcut calculation, if this maximum as exceeded early * @return 0 if the Strings are identical * a negative value if the shorter string is a prefix or suffix of the longer string * a positive value indicates the minimum number of atomic edit operations to transform string1 into string2 * +1 to delete a range of characters from string1 * +1 to insert a single character from string2 * +1 to substitute a single character not matched between string1 and string2 * @see http://rosettacode.org/wiki/Levenshtein_distance#JavaScript */ var prefix = 0, suffix = 0; function editDistance(string1, string2, maxDistance) { // delete = remove characters from string1 to match string2 // insert = add characters to string2 not present in string1 if (!maxDistance || maxDistance <= 0) { maxDistance = 1e9 + 1e6 + 1e3 + 1e0; } // exclude common prefix p / suffix s as they won't change the result: ed(px,py) = ed(x,y) = ed(xs,ys) // checked in linear TIME prefix = 0; suffix = 0; var min = string1 && string2 ? Math.min(string1.length, string2.length) : 0; var max = min > 0 ? Math.max(string1.length, string2.length) : string1 ? string1.length : string2 ? string2.length : 0; while (prefix < min && string1[prefix] == string2[prefix]) { prefix++; } if (prefix == min) { // matched a true prefix or the strings equal return prefix - max; } while (suffix < min && string1[string1.length - 1 - suffix] == string2[string2.length - 1 - suffix]) { suffix++; } if (suffix == min) { // matched a true suffix return suffix - max; } // now the strings cannot be equal anymore if (maxDistance <= string2.length - string1.length) { // correctly stop on: maxDistance <= max - min // overwhelmed by required number of insertions without even trying to match anything return maxDistance; } if (min - prefix - suffix <= 0) { // prefix and suffix overlap -> the correct result is the difference in length: max - min return string1.length > min ? 1 : max - min; // reduce penalty for streamDelete to 1 } var length = string1.length - prefix - suffix; var other = string2.length - prefix - suffix; // need only two rows of the matrix at any time (use modulo operator % 2 to access even and odd lines) // linear SPACE var twoRows = []; twoRows[0] = []; twoRows[1] = []; // initialize first row (independed of strings as it represents transforming into the empty sequence) for (var i = 0; i <= length; i++) { // horizontal step with a penalty of constant 0 to delete string1.substr(prefix, i) into the empty sequence twoRows[0][i] = 0; // normally would be i } // TIME O(n*m) for (var j = 1; j <= other; j++) { // each iteration calculates the cost of changing string1.substr(prefix, i) into string2.substr(prefix, j) // initialize first column (same as first row) // vertical step with a penalty of j - (j - 1) = 1 to insert a substring(prefix, j) into the empty sequence twoRows[j%2][0] = j; var earlyExit = true, streamDelete = false; for (i = 1; i <= length; i++) { // compare the characters at i and j respectively (penalty 1 for substitution if not matched) var sub = (string1[prefix + i - 1] == string2[prefix + j - 1] ? 0 : 1) + twoRows[(j + 1)%2][i - 1]; var ins = 1 + twoRows[(j + 1)%2][i]; // penalty for n consecutive deletions is constant 1 (remove whole ranges from string1 at cost 1) var del = (streamDelete ? 0 : 1) + twoRows[j%2][i - 1]; twoRows[j%2][i] = Math.min(sub, ins, del); streamDelete = twoRows[j%2][i] == del; if (twoRows[j%2][i] <= maxDistance) { earlyExit = false; } } // console.log("p=" + prefix + " s=" + suffix + " s1='" + string1 + "' s2='" + string2 + "' tr=" + twoRows[j%2]); if (earlyExit) { // entries in the matrix can only move or grow but they won't shrink again // it can only get worse from here return maxDistance; } } return twoRows[other%2][length]; } // var x1 = 'Joooo, die Arch Fraktion wird hier immer größer. Aber Dich kann keiner mehr enholen, wo Du doch damit angefangen hasti=cwm11.gif'; // var x2 = 'Joooo, die Arch Fraktion wird hier immer größer. Aber Dich kann keiner mehr einholen, wo Du doch damit angefangen hasti=cwm11.gif'; // console.log("test editDistance(\n'" + x1 + "',\n'" + x2 + "',10)\n=" + editDistance(x1, x2, 10) // + " unlimited=" + editDistance(x1,x2) + " p=" + prefix + " s=" + suffix); var lightCyan = "#8ee", // Alles OK, bereits gelesen, nichts Neues lightGreen = "#5f7", // Neuigkeiten lightYellow = "#cc7", // Warnung lightRed = "#e88"; // Fehler // Vergleiche den Text direkt innerhalb der beiden Tabellen-Zellen von Zitat und je einem Original-Beitrag var reg_ellipsis = /(\s*\[?\.{3,}\]?\s*)/g, reg_imageName = /^.*\//, reg_multiSpace = /\s{2,}/g, reg_multiZeilen = /\s*?\n\s*/g, reg_newLine = /\n/, reg_singlequot = /[´`]/g; // Sprungmarken für alle Zitate durchnummerieren var hit = 0, partial = 0, miss = 0; function diffTextLines(beitragNr, zitatNr, node, original, notFound, endOfInput) { var logOutput = debug && beitragNr == 2 && zitatNr == 1; // beitragNr < zitatNr var result = "", i, j, unmatched; // erfasse den Text eines Knoten if (notFound || !node.diffTextLines) { for (i = 0; i < node.childNodes.length; i++) { var n = node.childNodes[i]; if (logOutput) { console.log(format("(b=%s z=%s) Erfasse Kinder von %s (%s<%s): %s '%s'", beitragNr, zitatNr, node.nodeName, i, node.childNodes.length, n.nodeName, (n.nodeName == "#text" ? n.data : ""))); } if (n.nodeName == "#text") { result += " " + n.data .replace(reg_multiSpace, " ") .replace(reg_quot, '"') .replace(reg_singlequot, "'"); // Akzente in einfaches Hochkomma umwandeln } else if (n.nodeName == "BR") { result += "\n"; } else if (n.nodeName == "IMG") { // Schneide die URL weg, in der Hoffnung, dass der Dateiname eindeutig genug ist. // lange URL eines Smiley löst sonst falsch-positive partielle Treffer aus result += "I=" + n.src.replace(reg_imageName, ""); } else if (n.nodeName == "A") { result += format(" A={%s,%s,%s}", n.name, n.href, diffTextLines(beitragNr, zitatNr, n, null, notFound, endOfInput && i == node.childNodes.length - 1).replace(reg_ellipsis, "..")); } else if ("B,I,U,FONT".indexOf(n.nodeName) != -1) { result += " " + diffTextLines(beitragNr, zitatNr, n, null, notFound, endOfInput && i == node.childNodes.length - 1); } else if (n.nodeName == "HR" && n.color == "ffffff") { // nicht die Signatur vergleichen break; } else { // kurze Schreibweise: keine falsch Positiven bei kurzen Chat-Telegrammen mit eingebettetem Zitat result += format("%n=%s=%n", n.nodeName == "BLOCKQUOTE" && n.firstChild.nodeName == "TABLE" ? viaXpath0(".//U[@id]", n).id : n.nodeName); } if (notFound) { if (logOutput) { console.log(format("suche nf[0]='%s'%nvon %s in result='%s'", notFound[0], notFound.length, result)); } unmatched = ""; var warning = true; while (notFound.length > 0 // sind noch Fehler zuzuordnen // UND ( wurde die Quell-Zeile bereits gelesen ODER ist der Text komplett ) && ( (j = result.indexOf(notFound[0])) != -1 || endOfInput && i == node.childNodes.length - 1 )) { unmatched += notFound[0] + " "; if (notFound[0].length > 1 && !notFound[0].match(reg_ellipsis)) { // mehr als ein Buchstabe und keine Auslassungszeichen -> dicker Hund wird rot unterlegt! warning = false; } if (logOutput) { console.log(format( "Markiere nicht gefunden: nf[0]='%s'%n@index j=%s in result='%s'%nnode='%s'%nw=%s", notFound[0], j, result, n && n.nodeName == "#text" ? "'" + n.data + "'" : n, warning)); } // verhindern, dass das gleiche Wort noch einmal gefunden wird. (notFound ist sortiert) result = result.substr(0, j) + result.substr(j + notFound[0].length); notFound.shift(); } if (unmatched.length > 0) { var nfMarker = document.createElement("SPAN"); node.insertBefore(nfMarker, n); nfMarker.appendChild(n); unmatched = unmatched.trim(); result = result.trim(); nfMarker.title = "Unbekannter oder geänderter Text:\n'" + unmatched + "'"; nfMarker.style = "background-color:" + (warning ? lightYellow : lightRed); } if (notFound.length === 0) { // alles markiert, nicht mehr weiter suchen return ""; } } } result = result .replace(reg_ellipsis, "\n$1\n") // an Auslassungszeichen umbrechen .replace(reg_multiZeilen, "\n") // Zeilenumbrüche zusammenfassen .replace(reg_multiSpace, " ") // Weißraum zusammenfassen .trim(); // Leerzeichen an Anfang und Ende entfernen // erfassten Text von Beiträgen temporär merken (in GM-Wrapper-Objekt) node.diffTextLines = result; } else { result = node.diffTextLines; } // Zuordnung von Absätzen zwischen Original und Zitat if (original) { // extrahiere den Text aus dem Original-Beitrag var org = diffTextLines(beitragNr, zitatNr, original); if (org !== "" && org.indexOf(result) == -1) { // überspringe komplett leere Beiträge // Suche teilweise Treffer aus ganzen Absätzen var count = 0, expected = result.length, outstanding = expected, expectedQuarter = Math.max(10, expected / 4); if (logOutput) { console.log(format("Overview: %s: org='%s'%n%s: result='%s'%no.indexOf(r)=%s u=%s p=%s s=%s e=%s", beitragNr, org, zitatNr, result, org.indexOf(result), editDistance(org, result), prefix, suffix, expected)); } var found = org.split(reg_newLine); found.removeFirst = function(value) { // suche und entferne erstes Vorkommen for (var i = 0; i < this.length; i++) { if (this[i] == value) { this.splice(i, 1); break; } } }; outstanding -= found.length - 1; // minus die Anzahl der Zeilenumbrüche org = org.split(reg_newLine); result = result.split(reg_newLine); notFound = []; j = 0; NEXT_QUOTE: for (var aj = result.length; 0 < aj; j++, aj--) { // Abbruchbedingung schon in der Schleife prüfen -> frühzeitig aussteigen, wenn nicht mehr erfüllbar if (0 < notFound.length && aj < found.length && count + outstanding < expectedQuarter) { // early miss return null; } unmatched = result[j]; outstanding -= unmatched.length; for (i = 0; i < org.length; i++) { // Edit-Distanz kleiner 5% der Länge (1 Fehler toleriert je 20 gültige Zeichen) var p5 = Math.max(3, Math.trunc(0.05 * Math.min(org[i].length, unmatched.length))); var ed = editDistance(org[i], unmatched, p5); if (logOutput) { console.log(format("Detail: %s: i=%s<%s org='%s'%n" + "%s: j=%s<%s result='%s'%n" + "o.indexOf(r)=%s p5=%s ed=%s u=%s p=%s s=%s nf=%s f=%s c=%s", beitragNr, i, org.length, org[i], zitatNr, j, result.length, result[j], org[i].indexOf(result[j]), p5, ed, editDistance(org[i], result[j]), prefix, suffix, notFound.length, found.length, count)); } // echter Treffer if (0 === ed // ODER unscharfer Treffer (mindestens 5 Zeichen) || 0 < ed && ed < p5 && 5 < unmatched.length // ODER signifikantes Präfix/Suffix gefunden || prefix + suffix > 35 // ODER unmatched als komplettes Präfix/Suffix in org enthalten (> Smiley a 11 Buchstaben) || ed < 0 && 11 < unmatched.length && unmatched.length < org[i].length) { found.removeFirst(org[i]); // das restliche Zitat wurde vollständig gefunden, evtl. mit kleinen Änderungen count += unmatched.length; if (0 < ed && org[i].indexOf(result[j]) == -1) { // markiere den unscharfen Treffer unmatched = unmatched.substring(prefix, unmatched.length - suffix); count -= ed < p5 ? ed : unmatched.length; if (unmatched.length > 11) { // entweder min. 2 echte Änderungen -> erster & letzter Buchstabe von unmatched // oder Änderung = Löschen von zusätzlichem umschließenden Text in org // suche das mittlere Drittel und expandiere die Ränder var drittel = unmatched.length / 3 | 0; var b = drittel, e = 2 * drittel; var mitte = unmatched.substring(b, e); var begin = org[i].indexOf(mitte, prefix); if (begin != -1) { var end = begin + drittel; while (0 < begin && 0 < b && org[i][begin - 1] == unmatched[b - 1]) { begin--; b--; } while (end < org[i].length && e < unmatched.length && org[i][end] == unmatched[e]) { end++; e++; } if (logOutput) { console.log(format( "d=%s (%s,%s) (%s,%s)%norg(%s): '%s'%nunmatched(%s): '%s'%nmitte(%s): '%s'", drittel, begin, end, b, e, org[i] .length, org[i].substr(begin, unmatched.length), unmatched.length, unmatched, mitte .length, mitte)); } unmatched = unmatched.substr(0, b) + (b === 0 || e == unmatched.length ? "" : "...") + unmatched.substr(e); } } if (unmatched.length > 0) { if (unmatched.length > 11) { // versuche den Rest noch gegen die anderen Absätze zu vergleichen continue; } // gib auf, einzelne Worte weiter zuzuordnen break; } } // unmatched wurde vollständig gefunden continue NEXT_QUOTE; } else if (!org[i].match(reg_ellipsis)) { var k = unmatched.indexOf(org[i]); // org ist in unmatched enthalten if (k != -1) { count += org[i].length; // vermutlich wurde ein Zeilenumbruch gelöscht unmatched = (unmatched.substr(0, k).trim() + " " + unmatched.substr(k + org[i].length).trim()) .trim(); found.removeFirst(org[i]); if (logOutput) { console.log(format("%s: starte Suche neu für %s: '%s' wegen '%s'", i, j, unmatched, org[i])); } i = -1; } } } unmatched = unmatched.trim(); if (unmatched.length > 0) { notFound.push(unmatched); } } if (logOutput) { console.log(format("Result: nf=%s: '%s'%nf=%s: '%s'%nc=%s e=%s e/4=%s", notFound.length, notFound, found.length, found, count, expected, expectedQuarter)); } // alle Absätze Zitat ODER alle Absätze Beitrag und 10% Zeichen ODER 25% Zeichen zugeordnet if (notFound.length === 0 || found.length === 0 && count / expected > 0.1 || count > expectedQuarter) { // farbige Markierung für alle nicht passenden Textstellen (erneuter Durchlauf node mit notFound) var result = format("partial nf=%s f=%s c=%s e=%s r %s", notFound.length, found.length, count, expected, (100 * count / expected).toFixed(2)); diffTextLines(beitragNr, zitatNr, node, null, notFound, true); return result; } // miss return null; } // Zitat vollständig im Beitrag enthalten return "hit"; } return result; } // KlammerGruppen: 1 type forumid_2Boar_3Thre4_5MaxB6_7Offset var reg_beitrag = /(display|print|reply)_5_(\d+)_(\d+)(_(\d+)(_(\d+))?)?.html/, reg_params = /(display).*(threadid=(\d+))/, reg_relativeURL = /https?:\/\/[^\/\?]+|([\/\?])/, reg_absoluteURL = new RegExp("^https?://[^/?]*(" + domainList.join("|").replace(/\./g,"\\.") + ")([/?].+)$"), prefix = "contains(@href, '", suffix = "')", dclone = domainList.slice(0); dclone.unshift("display"); var linkXPath = ".//A[" + prefix + dclone.join(suffix + " or " + prefix) + suffix + "]"; async function markThreadLinks(node) { // markiere innerhalb des Beitrages alle Links, die auf ein Lesezeichen gehen. var links = viaXpath(linkXPath, node); for (var j = 0; j < links.snapshotLength; j++) { var a = links.snapshotItem(j); var entry; if ((entry = reg_beitrag.exec(a.href)) || (entry = reg_params.exec(a.href))) { entry = await getThreadEntry(parseInt(entry[3])); if (entry) { a.style = "background-color:" + lightCyan; var stamp = new Date(entry.lastDate); a.title = format("Gelesen bis:%s%s\nBeiträge: %s", wochentag[stamp.getDay()], formatDate(stamp), entry.maxCount); } // relativer Link -> nicht von preferredDomain weg navigieren (Cookie gilt nicht = Logout) a.href = a.href.replace(reg_relativeURL, "$1"); } else if (entry = reg_absoluteURL.exec(a.href)) { a.href = entry[2]; } } } // Übersicht aller Zitate, um Auswirkungen von Änderungen abschätzen zu können (vgl. Ist mit Soll) var quoteOverview = null; var reg_repairLink = /^https?:\/\/.*?\/((%5C|\\)(%22|"))?(https?:.*?)((%5C|\\)(%22|"))?$/i; function highlightQuote(content, i) { // content[i].style = "border:5px solid red"; markThreadLinks(content[i]); var quote = viaXpath(".//BLOCKQUOTE/TABLE/TBODY/TR/following-sibling::TR/TD", content[i]); if (debug && !quoteOverview && quote.snapshotLength > 0) { quoteOverview = document.createElement("TABLE"); quoteOverview.border = 1; quoteOverview.innerHTML = "<TR><TH colspan=4>" + document.title + "<BR>" + window.location.hostname + window.location.pathname + "</TH></TR>" + "\n<TR><TH>Beitrag</TH><TH>Zitat</TH><TH>verweist auf</TH><TH>Typ</TH></TR>\n"; document.body.appendChild(quoteOverview); } var innerQuote = 0; NEXT_QUOTE: // rückwärts: d.h. innere-geschachtelte = ältere *VOR* äußeren-umschließenden Zitaten for (var j = quote.snapshotLength - 1; 0 <= j; j--) { var q = quote.snapshotItem(j); // q.style = "border:10px solid red"; // q.innerHTML = i + ":" + j; // continue; // repariere Links innerhalb des Zitates (überschüssige Anführungsstriche entfernen) var links = viaXpath(".//A[@href]", q); for (var k = 0; k < links.snapshotLength; k++) { var l = links.snapshotItem(k); // http://kgforum.org/%5C%22http://kgforum.org/display_5_2388_74514_562464.html#562464\%22 l.href = l.href.replace(reg_repairLink, "$4"); if (l.target) { l.target = l.target.replace(reg_quot, ""); } } var zitat = viaXpath0("parent::TR/preceding-sibling::TR/TD/FONT", q), a; // suche rückwärts, weil das Original meist dicht dran ist for (k = i - 1; innerQuote <= k; k--) { var connection = diffTextLines(content[k].num, content[i].num, q, content[k]); if (connection) { a = document.createElement("A"); a.name = connection[0] + (connection == "hit" ? hit++ : partial++); q.insertBefore(a, q.firstChild); q.setAttribute("bgcolor", connection == "hit" ? lightCyan : lightGreen); zitat.innerHTML += format( " von <A href='#%s'><FONT color='white'><U id='BQ%s'>%<$s. %s am %s</U></FONT></A> (%s)", content[k].ref, content[k].num, content[k].author, content[k].date, connection == "hit" ? "sicher" : format("wahrscheinlich %5s %%", connection.substr(connection.lastIndexOf(" ")))); if (debug) { quoteOverview.innerHTML += format( "<TR bgcolor='%s'>" + "<TD><A href='#%s'>%s</A></TD>" // Beitrag + "<TD>" + "%s"+ "</TD>" // Zitat + "<TD><A href='#%s'>%s</A></TD>" // verweist auf + "<TD><A href='#%s'>%s</A></TD>" // Typ + "</TR>%n", q.getAttribute("bgcolor"), content[i].ref, content[i].num, // Beitrag j, // Zitat content[k].ref, content[k].num, // verweist auf a.name, connection); // Typ } // Suche nicht nach früheren Beiträgen, die das innere Zitat nicht enthalten können innerQuote = 0 < j && viaXpath0("ancestor::BLOCKQUOTE/parent::*", quote.snapshotItem(j - 1)) != viaXpath0("ancestor::BLOCKQUOTE/parent::*", q) ? k : 0; continue NEXT_QUOTE; } } // Keine Einschränkung für das nächste Zitat aus einem *nicht* gefundenen Zitat innerQuote = 0; // keiner der vorangegangenen Beiträge passt zitat.innerHTML += "<U id='BQ" + content[i].num + "'></U>"; a = document.createElement("A"); a.name = "m" + miss++; q.insertBefore(a, q.firstChild); q.setAttribute("bgcolor", lightYellow); if (i < 2 && i + 1 < content[i].num) { q.title = "Dieses Zitat kann evtl. in der Druckansicht zugeordnet werden,\n" + "weil dort alle Beiträge dieses Themas berücksichtigt werden!"; } if (debug) { quoteOverview.innerHTML += format( "<TR bgcolor='%s'>" + "<TD><A href='#%s'>%s</A></TD>" // Beitrag + "<TD>" + "%s"+ "</TD>" // Zitat + "<TD>" + "?" + "</TD>" // verweist auf nichts + "<TD><A href='#%s'>%s</A></TD>" // Typ + "</TR>%n", q.getAttribute("bgcolor"), content[i].ref, content[i].num, // Beitrag j, // Zitat // verweist auf nichts "m" + (miss - 1), "miss"); // Typ } } } async function formatThreadHistory() { var marker = "geschrieben von: "; var rows = viaXpath( "//TABLE//TABLE//TABLE[@cellspacing=0 and @cellpadding=2]/TBODY/TR/TD/FONT[contains(text(), '" + marker + "')]"); var offset = rows.snapshotLength; if (reg_beitrag.exec(window.location)) { var entry = await getThreadEntry(parseInt(RegExp.$3)); if (entry) { offset = Math.max(offset, entry.maxCount); } } var content = []; // rückwärts um die ältesten Beiträge (möglicherweise mit der Vorlage eines Zitates zuerst zu verarbeiten) for (var i = 0, j = rows.snapshotLength - 1; 0 <= j; j--, i++) { var header = rows.snapshotItem(j); var current = viaXpath0(".//parent::TD/following-sibling::TD/FONT", header); var time = parseDate(current.innerHTML); current.innerHTML = RegExp.$1 + (time = wochentag[time.getDay()] + formatDate(time)); current = viaXpath0(".//following::TD[@colspan=2]", current); current.author = header.innerHTML.substr(marker.length); current.date = time; current.ref = offset - j; current.num = current.ref; header.innerHTML = "<a name=" + current.ref + ">" + current.ref + ": " + header.innerHTML + "</a>"; content.push(current); highlightQuote(content, i); } } function loadPageContent(url) { if (debug) { document.getElementById("loadPNHistory").nextSibling.innerHTML += "\n" + url; } var childDoc = new XMLHttpRequest(); childDoc.open("GET", url, false); childDoc.send(); childDoc = childDoc.responseText; childDoc = childDoc.substring(childDoc.indexOf("<body"), childDoc.lastIndexOf("</body>")).replace(/^.*>/, ""); // weil eine HTML-Seite und kein XML abgefragt wird, weise den Inhalt einem dummy-Knoten zu var childBody = document.createElement("DIV"); childBody.innerHTML = childDoc; return childBody; } var reg_pnUserId = /&searchid2=(\d+)/; function parseOnePageOfPN(a, incomming) { var c = incomming ? 0 : 2, m = c + 1; var last = "&start="; if (null == a.counter[m]) { last += 0; } else { if (a.counter[c] >= a.counter[m]) { return new Date(); } last += a.counter[c]; } var mode = incomming ? "&mode=old" : "&mode=send"; // https://www.kgforum.org/?action=mysite&mysite=pm&mode=old&forumid=5 // https://www.kgforum.org/?action=mysite&mysite=pm&mode=send&forumid=5 var body = loadPageContent("/?action=mysite&mysite=pm" + mode + "&forumid=5" + last); last = parseInt( viaXpath0("//TABLE//TABLE//TABLE//TABLE//TABLE//TD" + (incomming ? "" : "[@colspan=2]") // überspringe die Postausgang-Text-Zelle mit gleichem Link + "/A[contains(@href, '" + mode + "')]", body ).innerHTML); if (null != a.counter[m] && a.counter[m] != last) { alert(format("Post-%sgang hat neues Maximum %s != %s!", (incomming ? "Ein" : "Aus"), a.counter[m], last)); } a.counter[m] = last; var rows = viaXpath(".//TABLE//TABLE//TABLE[.//TR[1]//B[text()='Eintrag ']]/TBODY/TR", body); var targetTable = document.getElementById("PNHistory"); if (!targetTable) { var header = rows.snapshotItem(0); targetTable = header.parentNode.parentNode.cloneNode(); targetTable.innerHTML = "<TBODY id='PNHistory'></TBODY>"; a.parentNode.insertBefore(targetTable, a); targetTable = targetTable.firstChild; targetTable.appendChild(header); } var stamp = new Date(); for (var i = 1; i + 2 < rows.snapshotLength; i += 3) { var header = rows.snapshotItem(i ); var main = rows.snapshotItem(i + 1); var footer = rows.snapshotItem(i + 2); var s = viaXpath0(".//FONT/B[text()='Datum:']", header).nextSibling; stamp = parseDate(s.data); a.counter[c]++; var user = viaXpath0(".//A[@href and IMG[contains(@src, '/images/theme1/message.gif')]]", footer); var author = viaXpath0("TD[@rowspan=3]/FONT/B", header); if (a.userId == parseInt(reg_pnUserId.exec(user.href)[1]) || a.userId == null && 0 < a.userName.length && 0 <= author.innerHTML.toLowerCase().search(a.userName.toLowerCase())) { if (!incomming) { author.innerHTML = "von mir"; author.parentNode.parentNode.removeChild(author.parentNode.parentNode.lastChild); } applyFontSize(header); applyFontSize(main); last = viaXpath0(".//FONT/B/A[@name]", header); header.id = last.name; last.innerHTML = format("%s %s%s - %s", last.name, incomming ? "Ein" : "Aus", a.counter[c], last.innerHTML); var td = viaXpath0("TD", main); td.ref = parseInt(header.id); td.author = author.innerHTML; td.date = s.data = wochentag[stamp.getDay()] + formatDate(stamp); // nicht hinten anhängen sondern an korrekter Position einsortieren! var j = a.content.length - 1; for (; 0 <= j; j--) { if (a.content[j].ref > td.ref) { break; } } last = a.content[j + 1]; if (last) { last = document.getElementById(last.ref); } targetTable.insertBefore(header, last); targetTable.insertBefore(main , last); targetTable.insertBefore(footer, last); a.content.splice(j + 1, 0, td); } } if (debug) { for (var i = 0; i < a.content.length - 1; i++) { if (a.content[i].ref <= a.content[i + 1].ref) { alert("Sortierung verletzt!"); window.location.hash = a.content[i].ref; break; } } a.nextSibling.innerHTML += format(" -> (%s-%s) %s", a.counter[m], a.counter[c], formatDate(stamp)); } return stamp; } function loadPNHistory() { if (!debug) return; // Nicht versuchen, die Historie zu laden -> kaputt var a = document.getElementById("loadPNHistory"); var instamp = parseOnePageOfPN(a, true); var i = 3; // lade maximal 3 Seiten auf einmal, bevor der User erneut klicken muss while ((a.counter[0] >= a.counter[1] || instamp < a.outstamp) && (null == a.counter[3] || a.counter[2] < a.counter[3]) && 0 < i) { a.outstamp = parseOnePageOfPN(a, false); i--; } if (debug) { if (!instamp || !a.outstamp) { alert("FAIL loadPNHistory: Datum nicht gesetzt!"); } if (a.counter[1] == null || a.counter[3] == null) { alert("FAIL loadPNHistory: Counter nicht gesetzt!"); } } // können noch mehr Beiträge geladen werden? if (a.counter[0] < a.counter[1] || a.counter[2] < a.counter[3]) { a.innerHTML = format("Ein=(%s-%s) Aus=(%s-%s) Lade Nachrichten älter als\n%s", a.counter[1], Math.min(a.counter[0], a.counter[1]), a.counter[3], Math.min(a.counter[2], a.counter[3]), formatDateDifference(instamp, a.outstamp)); } else { if (instamp > a.outstamp) { instamp = a.outstamp; } a.outerHTML = "Keine Nachrichten älter als" + wochentag[instamp.getDay()] + formatDate(instamp); } } function resetPNHistory(a, userId, userName) { var targetTable = document.getElementById("PNHistory"); if (targetTable) { targetTable.parentNode.removeChild(targetTable); } if (debug) { a.nextSibling.innerHTML += "\nreset userId=" + userId + " userName=" + userName; } viaXpath0("//INPUT[@name='submit' and @type='SUBMIT' and @accesskey='S' and @value]", document.creator) .value = userId ? "Nachricht an " + userName + " abschicken" : "Suche '" + userName + "' und schicke Nachricht"; document.title = "KG:" + userName + " (neue PN)"; a.userId = userId; a.userName = userName; // a.counter = [860, 888, 740, 761]; /* TODO a.counter = [0, null, 0, null]; // */ a.content = []; a.outstamp = new Date(); loadPNHistory(); } function initPNHistory() { var a = document.getElementById("loadPNHistory"); var pm_empfaenger = document.creator.pm_empfaenger; var searchid2 = document.creator.searchid2; if (!a) { document.creator.message.cols = 120; document.creator.message.rows = 40; a = document.createElement("A"); a.id = "loadPNHistory"; if (searchid2) searchid2.addEventListener("change", initPNHistory); document.creator.adrsel .addEventListener("change", initPNHistory); pm_empfaenger .addEventListener("blur" , initPNHistory); a .addEventListener("click" , loadPNHistory); a.href = "javascript:void(0)"; viaXpath0("//INPUT[@value='Nachricht abschicken']//ancestor::CENTER").appendChild(a); var pre = document.createElement("PRE"); pre.innerHTML = "\n"; a.parentNode.appendChild(pre); } pm_empfaenger = pm_empfaenger.value; // 1. falls ein Empfänger gesucht und ausgewählt wurde if (searchid2) { if (0 == searchid2.selectedIndex) { // suche eine exakte Übereinstimmung var reset = true; for (var i = 1; i < searchid2.options.length; i++) { if (2 == searchid2.options.length || pm_empfaenger.toLowerCase() == searchid2.options[i].innerHTML.toLowerCase() ) { // wenn eindeutig, nimm ihn sofort (feuert nicht den onchange-Listener) searchid2.value = searchid2.options[i].value; reset = false; break; } } if (reset) { a.userId = null; a.userName = null; } } // FALL-THROUG, ausdrücklick kein else! if (0 < searchid2.selectedIndex) { var userId = parseInt(searchid2.options[searchid2.selectedIndex].value); if (!a.userId || a.userId != userId) { resetPNHistory(a, userId, searchid2.options[searchid2.selectedIndex].innerHTML); } } } // 2. falls ein Empfänger über die URL identifiziert wurde (z.B. Nachricht unter Beitrag bzw, Antwort / Zitat auf bestehende PN) if (!a.userId && reg_pnUserId.exec(window.location.search)) { resetPNHistory(a, parseInt(RegExp.$1), pm_empfaenger); } // 3. Der Suchtext für den Empfänger wird zusammen mit der PN an den Server gesendet (hoffentlich eindeutig) if (!a.userId && (!a.userName || pm_empfaenger.toLowerCase() != a.userName.toLowerCase()) || a.userName == null) { resetPNHistory(a, null, pm_empfaenger); } } // Diverse Seiten dienen der Eingabe neuer Daten: nur den Titel setzen und Skript beenden. var tagArray = document.getElementsByTagName("FORM"); if (tagArray.length > 0) { tagArray = tagArray[0].getElementsByTagName("INPUT"); if (tagArray.length > 0) { if (tagArray[0].name == "searchstring") { // Sollte die Suche nach allen Beiträgen nicht mehr funktionieren, kann hiermit eine aktuelle Suchanfrage // ins Addressfeld geholt werden (siehe auch: formatProfile()). // document.getElementsByTagName("FORM")[0].method = "GET"; document.title = "KG:Suche"; // Erlaube die gezielte Suche im Moderatoren-Brett var staff = viaXpath0("//SELECT/OPTION[@value=2399]"); if (debug && staff) { var mod = document.createElement("OPTION"); mod.setAttribute("value", "2439"); mod.innerHTML = "Moderatoren-Ecke"; staff.parentNode.insertBefore(mod, staff); } logTime(); return; } if (tagArray.length > 1) { pimpEditor(); if (tagArray[1].name == "subject") { document.title = "KG:" + tagArray[1].value + " (Beitrag ändern)"; logTime(); return; } if (tagArray[1].name == "pm_empfaenger") { initPNHistory(); logTime(); return; } if (tagArray[1].name == "mailto") { var an = viaXpath0("//FORM//B").nextSibling.data; document.title = "KG:" + an + " (neue eMail)"; logTime(); return; } if (tagArray.length > 3) { if (tagArray[3].name == "subject") { tagArray = tagArray[3].value; if (tagArray) { document.title = "KG:" + tagArray.replace(/^RE:\s*/, "") + " (neue Antwort)"; formatThreadHistory(); } else { document.title = "KG:neues Thema"; } logTime(); return; } } } } } if (window.location.search.indexOf("action=memberlist") > 0) { if (window.location.search.indexOf("list=mlletter") > 0) { document.title = "KG:" + (/letter=([%a-z])/.exec(window.location.search) ? RegExp.$1 : "a") + "-Mitglieder"; } else if (window.location.search.indexOf("list=mltop") > 0) { document.title = "KG:Top Mitglieder"; } else if (window.location.search.indexOf("list=mlall") > 0) { document.title = "KG:alle Mitglieder"; } if (debug) { // zähle Vorkommen von email.gif var em = viaXpath("//TD/FONT/A[IMG[contains(@src, '/images/theme1/email.gif')]]"); for (var i = 0; i < em.snapshotLength; ) { em.snapshotItem(i).parentNode.appendChild(document.createTextNode(" " + ++i)); } } logTime(); return; } else if (window.location.pathname.match(/print_5_\d+.html/)) { // TODO Statistik mit Anzahl Beiträge je User, je Status, (je Gruppe: auto, sub, top, whitelist forenleitung?) // iteriere über alle Beiträge var content = []; var datum = viaXpath("//TABLE//P[@align='right' and contains(text(),'geschrieben von')]"); var header; for (i = 0; i < datum.snapshotLength; i++) { header = datum.snapshotItem(i); var time = parseDate(header.innerHTML); header.innerHTML = RegExp.$1 + wochentag[time.getDay()] + formatDate(time); header = header.parentNode.previousSibling; header.innerHTML = "<A name='" + (i + 1) + "'>" + header.innerHTML + "</A>"; // Inhalt in neues Div packen, um ein umfassendes HTML-Element zu haben. var current = document.createElement("DIV"); // current.appendChild(document.createTextNode(i)); current.time = time.getTime(); header = viaXpath0("ancestor::TABLE", header); header.parentNode.insertBefore(current, header.nextSibling); var next = i + 1 < datum.snapshotLength ? viaXpath0("ancestor::TABLE", datum.snapshotItem(i + 1)) : null; while (current.nextSibling != next) { current.appendChild(current.nextSibling); } // Sortiere Beiträge nach Datum (Bug von einer alten Datenkonvertierung des Forums) j = i - 1; while (0 <= j && time.getTime() < parseInt(content[j].time)) { header = content[j].previousSibling; // Markiere den zu früh einsortieren Beitrag mit der zu niedrigen Nummer für sein junges Alter if (!header.title) { header.setAttribute("bgcolor", lightRed); header.title = "Umsortiert entsprechend Datum!\nVorher Platz: " + (j + 1); } current.parentNode.insertBefore(current.previousSibling, header); current.parentNode.insertBefore(current , header); j--; } // current in die bislang bearbeiteten Beiträgen einsortieren content.splice(j + 1, 0, current); } var t = document.title; if (content.length > 0) { viaXpath0(".//A[@name]", content[0].previousSibling).innerHTML.search(/\d+\. (RE: )?(.*)$/); t = RegExp.$2; } document.title = "KG-" + t + " (Drucken)"; // Zitat mit seiner Herkunft annotieren for (i = 0; i < content.length; i++) { viaXpath0(".//P[@align='right']", content[i].previousSibling).childNodes[0].data .match(/^geschrieben von (.+) am (.+)$/); content[i].author = RegExp.$1; content[i].date = RegExp.$2; // Beitragsnummer entspricht nicht mehr der Position i (umsortiert) header = viaXpath0(".//A[@name]", content[i].previousSibling); header.name = i + 1; header.innerHTML = (i + 1) + header.innerHTML.substr(header.innerHTML.indexOf(".")); content[i].ref = i + 1; content[i].num = content[i].ref; highlightQuote(content, i); } if (debug && quoteOverview) { quoteOverview.innerHTML += format("<TR><TD colspan=4>hit=%s partial=%s miss=%s</TD></TR>%n", hit, partial, miss); } logTime(); return; } // maxCount aus der URL, aus der Unterseiten-Navigation oder null async function threadContentTouchup(threadId, count, maxCount) { // Markiere noch nicht gelesene Beiträge als neu. var entry = await getOrCreateThreadEntry(threadId); // Extrahiere zusätzliche Sprunganweisungen des Suchergebnisses. var gotoDate = null, smaller = false, bigger = false, closest = null, jumpTo = null; var loc = decodeURI(window.location.search); if (loc.search(/.*goto=new/) >= 0) { if (entry.maxCount >= beitraege_pro_seite) { // springe zur Seite mit dem ersten neuen Beitrag var n = Math.min(entry.maxCount, maxCount); loc = window.location.toString(); loc = loc.replace( /(.*\/display_5_\d*_\d+).*(\.html).*/, "$1_" + Math.max(entry.maxCount, maxCount) + "_" + (n - n % beitraege_pro_seite) + "$2"); window.location.replace(loc); return; } } else if (loc.search(/.*goto=([^-]+)-([^&]+)/) >= 0) { // var gotoUser = RegExp.$2; // Reihenfolge: parseDate führt selbst reguläre Suchen durch und verwirft den User. gotoDate = parseDate(RegExp.$1); } // Sprung in print_5_ mit aktuellem Zählerstand als Marke if (count > 0) { viaXpath0("//IMG[contains(@src,'/images/theme1/print.gif')]/parent::A").href += "#" + (count + 1); } // Biete an, das Lesezeichen zu löschen var abo = viaXpath("//IMG[contains(@src,'/images/theme1/notification.gif')]/parent::A"); for (var i = 0; i < abo.snapshotLength; i++) { var delEntryNode = document.createElement("A"); delEntryNode.href = "javascript:void(0);"; deleteParameter = threadId; delEntryNode.addEventListener("click", deleteEntry); delEntryNode.innerHTML = "<FONT size=1>Lesezeichen löschen ...</FONT>"; abo.snapshotItem(i).parentNode.appendChild(delEntryNode); } // Suche nach Pausen zwischen Beiträgen, die mehr als einen Monat auseinander liegen var timeStamps = viaXpath( "//IMG[contains(@src,'/images/theme1/icon3.gif')]/parent::A/preceding-sibling::FONT[@size]/B[1]", null, true); var lastDate = null, time, openEnd = false, postId = -1, boardId = -1; var beitraege = timeStamps.snapshotLength, breaks = 0; var content = []; var reg_newPost = /action=display/, reg_nachrichtenId = /.*nachrichtenid=(\d+)&boardid=(\d+).*/; for (i = 0; i <= beitraege; i++) { if (i < beitraege) { var node = timeStamps.snapshotItem(i); viaXpath0("parent::FONT/following-sibling::A", node).href.match(reg_nachrichtenId); postId = parseInt(RegExp.$1); boardId = parseInt(RegExp.$2); content[i] = viaXpath0("ancestor::TABLE/ancestor::TR/following-sibling::TR/TD", node); // Zitat mit seiner Herkunft annotieren var target = viaXpath("../preceding-sibling::TR", content[i]); target = target.snapshotItem(target.snapshotLength - 1); // console.log(format("%s: %s%n%s", i, target, target.innerHTML)); content[i].author = viaXpath0("./TD[@rowspan=3]//B", target).innerHTML; content[i].date = viaXpath0("./TD[@colspan=2]//TD[@align='right']//B", target).nextSibling.data; content[i].ref = viaXpath0("./TD[@rowspan=3]/A[@name]", target).name; content[i].num = i + 1 + count; highlightQuote(content, i); time = parseDate(node.nextSibling.data); if (threadId && (entry.lastDate < time // aktuellere Beiträge gefunden || entry.maxCount < count + i + 1 // mehr Beiträge gefunden // Zeitstempel des letzten Beitrages stimmt nicht || entry.maxCount == count + i + 1 && (entry.lastDate != time.getTime() || entry.postId != postId))) { if (entry.postId === 0 || window.location.search.search(reg_newPost) >= 0) { // Beitrag beim Anfügen einer neuen Antwort sofort als gelesen markieren entry.direct = true; } entry.lastDate = time.getTime(); entry.maxCount = count + i + 1; entry.postId = postId; entry.changed = true; if (!jumpTo) { // niemals den ersten Beitrag auf einer Seite anspringen jumpTo = i === 0 ? -1 : entry.postId; } // Beitrag als neu markieren var img = viaXpath0("parent::FONT/parent::TD/preceding-sibling::TD/IMG", node); img.setAttribute("srcFallback", img.src); img.src = "/images/theme1/new.gif"; } if (gotoDate) { var diff = gotoDate - time; if (diff < 0) { smaller = true; diff = -diff; } else if (diff > 0) { bigger = true; } else { // gefunden -> nicht weiter suchen gotoDate = null; } if (!closest || diff < closest) { closest = diff; // niemals den ersten Beitrag auf einer Seite anspringen jumpTo = i === 0 ? -1 : postId; } } } else if (i < beitraege_pro_seite || !maxCount || beitraege + count >= maxCount) { // Spanne vom allerletzten Beitrag eines Themas bis zum aktuellen Datum time = new Date(); openEnd = true; if (entry.maxCount > beitraege + count) { // einige Beiträge wurden gelöscht // oder der Zähler in der URL ist falsch // oder ein Link auf eine Unterseite wurde in einen Beitrag / in eine Signatur kopiert var urlCount = reg_beitrag.exec(window.location); if ( urlCount // es ist eine Standard-URL && urlCount[6] // mit 5 Stellen (..._maxAnzahl_offset.html) && beitraege == beitraege_pro_seite // normaler Seitenumbruch nicht von Thread-Ende unterscheidbar && parseInt(urlCount[5]) < beitraege + count // fragliche maxAnzahl nicht vertrauenswürdig ) { // weise auf die fragwürdige Navigation hin var url = "/" + urlCount[1] + "_5_" + urlCount[2] + "_" + urlCount[3] + "_" + entry.maxCount + "_"; urlCount = ((parseInt(urlCount[5]) / beitraege_pro_seite) | 0) + 1; var ziel = ((entry.maxCount / beitraege_pro_seite) | 0); var navigation = viaXpath0("//TD[FONT/A/IMG[contains(@src, '/images/theme1/reply.gif')]]" + "/../TD/TABLE//FONT[not(text() or node()) or FONT/STRONG[contains(text(), '[" + urlCount + "]')]]"); for (i = urlCount; i < Math.min(urlCount + 4, ziel + 1); ) { navigation.innerHTML += ' <A href="' + url + (beitraege_pro_seite * i) + '.html">' + ++i + '?</A>'; } if (1 == urlCount) { // keine Navigationsstruktur vorhanden navigation.innerHTML = 'Seiten (' + (ziel + 1) + ') <IMG src="/images/theme1/end.gif"> <STRONG>« [' + urlCount + ']</STRONG>' + navigation.innerHTML + ' <A href="' + url + (beitraege_pro_seite * urlCount) + '.html">»?</A>' + ' <A href="' + url + (beitraege_pro_seite * ziel) + '.html"><IMG src="/images/theme1/end.gif">?</A>'; } else { // bestehende Navigation erweitern navigation = viaXpath0("../following-sibling::TD/FONT[STRONG[text()='»']]", navigation); navigation.innerHTML = '<A href="' + url + (beitraege_pro_seite * urlCount) + '.html">' + navigation.innerHTML + '?</A>'; navigation = viaXpath0("../following-sibling::TD[IMG[contains(@src, '/images/theme1/end.gif')]]", navigation); navigation.innerHTML = '<A href="' + url + (beitraege_pro_seite * ziel) + '.html">' + navigation.innerHTML + '?</A>'; } // unterdrücke den Trennbalken und die Auswertung des Thread-Endes break; } else { if (debug) { // Zeige den letzten Beitrag window.location.hash = "#" + postId; alert(format("%s Beiträge gelöscht, Lesezeichen überschreiben:\nalt: %s\nneu: %s\nTab schließen zum Abbrechen!", entry.maxCount - beitraege - count, entry, new Entry(threadId, beitraege + count, postId, lastDate.getTime()) )); } entry.maxCount = beitraege + count; entry.postId = postId; entry.lastDate = lastDate.getTime(); entry.changed = true; entry.direct = true; } } } else { // letzter Beitrag dieser Seite, aber nicht auf der letzten Seite! break; } // Trennbalken für zeitliche Lücken zwischen Beiträgen if (lastDate) { var diff = time - lastDate.getTime(); if (openEnd || diff > 30 * millisDay) { var spanne = "<CENTER>"; if ( diff > 365 * millisDay) { spanne += "<BR/><HR/><BR/><P>" + (diff / 365 / millisDay).toFixed(2) + " Jahre später ...</P><BR/><HR/><BR/>"; } else if (diff > 30 * millisDay) { spanne += "<BR/><P>" + (diff / 30 / millisDay).toFixed(2) + " Monate später ...</P><BR/>"; } else { spanne += (diff / millisDay).toFixed(2) + " Tage später ..."; } spanne += "</CENTER>" if (debug && i == beitraege && maxCount && maxCount != entry.maxCount && !entry.changed) { jumpTo = entry.postId; if (maxCount > entry.maxCount) { spanne += format("<BR/>Beitragscounter VERRINGERN %s > %s<BR/>" + "<OL type='a'>" + "<LI>Diese Seite wurde zuletzt geladen? (Wählt diesen Thread aus!)</LI>" + "<LI><A href='/?forumid=5&action=delpost&nachrichtenid=790185' target='_blank'>" + "<IMG src='/images/theme1/delete.gif'> " + "Lösche %s Test-Beiträge</A> (altes Testposting x-mal löschen)</LI>" + "<LI><A href='/threads_5_%s.html'>lade Brett-Übersicht</A></LI>" + "</OL>", maxCount, entry.maxCount, maxCount - entry.maxCount, boardId); } else { spanne += format("<BR/>Beitragscounter ERHÖHEN %s < %s<BR/>" + "<OL type='I'>" + "<LI>Wiederhole %3$s mal:" + "<OL type='a'>" + "<LI><A target='_blank' %5$s Schreibe Test-Beiträge</A> (+1 im Zähler)</LI>" + "<LI><A target='_blank' href='/threads_5_%4$s.html'>Lade Brett-Übersicht in neuem Tab</A> (Löschfunktion verwirren)</LI>" + "<LI><IMG src='/images/theme1/delete.gif'/> Lösche den gerade erstellten Test-Beitrag aus a) und schließe den extra-Tab</LI>" + "</OL>" + "</LI>" + "<LI><A %5$s Schreibe echten Beitrag</A>, um alle Caches zu aktualisieren</LI>" + "</OL><HR/><OL type='I'>" + "<LI>Alternativ wiederhole %3$s mal:" + "<OL type='a'>" + "<LI><A target='_blank' href='/threads_5_%4$s.html'>Lade Brett-Übersicht in neuem Tab</A> (Löschfunktion verwirren)</LI>" + "<LI><IMG src='/images/theme1/delete.gif'/> Lösche einen alten Beitrag (möglichst nicht den letzten)</LI>" + "</OL>" + "</LI>" + "<LI>%6$s aktualisiere den Cache</A></LI>" + "</OL>", maxCount, entry.maxCount, entry.maxCount - maxCount, boardId, abo.snapshotItem(0).previousSibling.outerHTML.replace("</?A>?"), viaXpath0("//IMG[contains(@src, '/images/theme1/del_cache.gif')]/parent::A").outerHTML.replace("</A>")); } } lastDate = document.createElement("TR"); lastDate.innerHTML = "<TD colspan=2><FONT color='white'>" + spanne + "</FONT></TD>"; spanne = viaXpath0( "//FONT[@color='#FFFFFF']/parent::TD[@nowrap and @bgcolor='#008080']/parent::TR/following-sibling::TR[" + (3 * i + breaks++) + "]"); spanne.parentNode.insertBefore(lastDate, spanne.nextSibling); } } lastDate = time; } // keinen passenden Beitrag auf dieser Seite gefunden -> binäre Suche der Seiten if (gotoDate) { if (smaller != bigger && maxCount) { var lo = 0, hi = maxCount; if (window.location.search.search(/&bin=(\d+)-(\d+)/) >= 0) { lo = parseInt(RegExp.$1); hi = parseInt(RegExp.$2); } if (smaller) { hi = count; } else { lo = count + beitraege; } if (0 <= lo && lo <= hi && hi <= maxCount) { var middle = (Math.floor(hi / beitraege_pro_seite) - Math.floor( (Math.floor(hi / beitraege_pro_seite) - Math.floor(lo / beitraege_pro_seite)) / 2)) * beitraege_pro_seite; if (beitraege_pro_seite <= middle && middle == hi) { // Seite 1: 0- 20 von 220 // Seite 7: 120-140 von 220 // Seite 10: 180-200 von 220 // Seite 12: 220-240 von 220 -> per Definition leer // der Beitrag 220 steht als letzter noch auf Seite 11 middle -= beitraege_pro_seite; } middle = window.location.href.replace( /^(.*display_5_\d+_\d+)(_\d+_\d+)?(\.html.*?)(&bin=\d+-\d+)?$/, "$1" + (0 === middle ? "" : "_" + maxCount + "_" + middle) + "$3&bin=" + lo + "-" + hi); if (middle.indexOf(window.location.pathname) < 0) { window.location.replace(middle); return; } } } } if (jumpTo > 0) { // Zum ersten ungelesenen Beitrag springen window.location.hash = "#" + jumpTo; } if (entry.changed) { if (entry.direct) { // Falls ein Thema noch nicht bekannt ist, wird es sofort registriert. Schützt davor, dass ein anderer // Reiter für die nächste Seite aufgemacht werden kann, der noch nicht sieht, dass dieser Reiter bereits bis // zum Ende als gelesen markiert ist. Außerdem kann so schnell angelernt werden, was nicht mehr neu ist. // Dazu muss jedes Thema nur einmal kurz geöffnet werden. saveThreadEntry(entry); } safeTimer = window.setTimeout(function() { // Alle new-Bilder wieder ins Original zurückverwandeln var newImg = viaXpath("//IMG[@srcFallback]"); for (var i = newImg.snapshotLength - 1; 0 <= i; i--) { var ni = newImg.snapshotItem(i); ni.src = ni.getAttribute("srcFallback"); ni.removeAttribute("srcFallback"); } if (!entry.direct) { saveThreadEntry(entry); } entry.changed = null; entry.direct = null; safeTimer = null; }, lese_verzoegerung * 1000); } // Syntax-Highlight für Beiträge die nachträglich editiert wurden var edit = viaXpath("//FONT/child::*[self::STRONG or self::B]/FONT[@size]"); var reg_editNew = /(.*Dieser Eintrag wurde zuletzt) von (.+?) am ([\d\.]+) um ([\d:]+) ge.{1,2}(ndert.*)/, reg_editOld = /(.*Diese Nachricht wurde) am ([\d\.]+) um ([\d:]+) von (.+?) ge.{1,2}(ndert.*)/, reg_dateSimple = /(\d{2}\.\d{2}\.\d{2,4})/; for (i = edit.snapshotLength - 1; 0 <= i; i--) { var message = edit.snapshotItem(i); var anfang = null, aenderer, datum, zeit, ende; if (message.innerHTML.search(reg_editNew) >= 0) { anfang = RegExp.$1; aenderer = RegExp.$2; datum = RegExp.$3; zeit = RegExp.$4; ende = RegExp.$5; } else if (message.innerHTML.search(reg_editOld) >= 0) { anfang = RegExp.$1; datum = RegExp.$2; zeit = RegExp.$3; aenderer = RegExp.$4; ende = RegExp.$5; } if (anfang) { datum = formatDate(parseDate(datum + " 00:00")); datum = datum.substr(0, datum.indexOf(" ")); var parsedZeit = parseDate(message.innerHTML); var einsteller = viaXpath0("ancestor::TR[1]/preceding-sibling::TR[1]//FONT[@size]/B", message).innerHTML; var eingestelltRaw = viaXpath0("ancestor::TR[1]/preceding-sibling::TR[1]//TABLE//FONT[@size]/B[2]", message, true).previousSibling.data; eingestelltRaw.search(reg_dateSimple); var einstelldatum = RegExp.$1; var einstellzeit = parseDate(eingestelltRaw); var difference = formatDateDifference(einstellzeit, parsedZeit); var colorSpan = "<SPAN style='background-color:%s' title='vgl. %s'>%s</SPAN>"; message.innerHTML = format("%s von %s am%s%s um %s geä%s", anfang, einsteller == aenderer ? aenderer : format(colorSpan, lightYellow, einsteller, aenderer), wochentag[parsedZeit.getDay()], einstelldatum == datum ? datum : format(colorSpan, lightYellow, difference, datum), // ignoriere bis zu 10 Minuten zum Ändern des Beitrages einstellzeit.getTime() + aenderungen_markieren * 60000 > parsedZeit ? zeit : format(colorSpan, lightYellow, difference, zeit), ende); } } } // biete direkte Links als Referenz zum Kopieren an function insertLinks(threadId, count, maxCount, searchstring) { // Einfügen links neben/vor dem Button "Zitieren" (ist gleichzeitig Quelle für die IDs) var zitieren = viaXpath("//A[contains(@href, 'action=reply')]/IMG[contains(@src, '/quote.gif')]/parent::A", null, true); var threadpath, offset = count - (count % beitraege_pro_seite); var reg_ids = /nachrichtenid=(\d+)&threadid=(\d+)&boardid=(\d+)/; for (var i = 0; i < zitieren.snapshotLength; i++) { var z = zitieren.snapshotItem(i); if (z.href.search(reg_ids) < 0) { continue; } var nachrichtenid = RegExp.$1; if (!threadpath) { threadId = parseInt(RegExp.$2); // absoluter Pfad ohne Site (überschreibt fehlerhafte Pfade mit mehreren /) threadpath = "/display_5_" + RegExp.$3 + "_" + RegExp.$2; } z = z.parentNode; var reference = document.createElement("A"); reference.title = "Link auf diesen Beitrag" + (searchstring ? " ohne Suchergebnisse" : ""); reference.href = threadpath + "_" + nachrichtenid + ".html#" + nachrichtenid; count++; reference.appendChild(document.createTextNode("#" + padding(count, 0, 2))); z.insertBefore(reference, z.firstChild); if (searchstring) { z.insertBefore(document.createTextNode(" - "), z.firstChild); reference = document.createElement("A"); reference.style = "background-color:yellow"; // lightYellow nicht kräftig genug hinter einzelnen Ziffern reference.title = decodeURI(searchstring); reference.href = threadpath + "_" + Math.max(offset + beitraege_pro_seite, maxCount) + "_" + offset + ".html" + searchstring + "#" + nachrichtenid; reference.appendChild(document.createTextNode("hl" + padding(count, 0, 2))); z.insertBefore(reference, z.firstChild); } } // URL auf alte Smilies geht nicht mehr var smilies = null; var oldImag = viaXpath("//IMG[contains(@src, 'mages/smilies/cwm')]"); for (i = 0; i < oldImag.snapshotLength; i++) { var img = oldImag.snapshotItem(i); if (!smilies) { smilies = /^.*mages\/smilies\/cwm/; } img.src = img.src.replace(smilies, "/images/smilies/cwm"); } return threadId; } var debugBoard; var debugString; function markBeitragsCounter(ziel, entry, beitraege) { // diese Zeile erhält keine weiteren Updates mehr ziel.removeAttribute("threadId"); if (debug) { var threadTitle = viaXpath0("FONT/A[@href]/B", ziel); ziel = viaXpath0("following-sibling::TD[2]", ziel); ziel.title = "Abweichende Anzahl Beiträge " + entry.maxCount; ziel.setAttribute("bgcolor", lightRed); ziel.children[0].innerHTML += " <IMG src='/images/theme1/icon3.gif'/>"; // gelbes Ausrufezeichen debugString.appendChild(document.createTextNode( // baue eine Tabellen-Zeile für eine private Nachricht zum Melden der falschen Beitrags-Zählerstände "[/td][/tr][tr][td]" + beitraege + "[/td][td]" + entry.maxCount + "[/td][td]" + entry.threadId + "[/td][td][url=" + debugBoard + entry.threadId + "_" + entry.postId + ".html#" + entry.postId + "]" + threadTitle.innerHTML + "[/url]" )); debugString.appendChild(document.createElement("BR")); } } async function colorizeBoardOverview() { var row = viaXpath("//TD[@threadId]"); for (var i = row.snapshotLength - 1; 0 <= i ; i--) { var ziel = row.snapshotItem(i); var entry = await getThreadEntry(parseInt(ziel.getAttribute("threadId"))); if (entry) { var beitraege = parseInt(ziel.getAttribute("beitraege")); var lastDate = parseInt(ziel.getAttribute("lastDate")); if (entry.lastDate == lastDate || entry.maxCount == beitraege) { ziel.title = "Letzter Beitrag wurde bereits gelesen!"; ziel.setAttribute("bgcolor", lightCyan); if (debug) { if (entry.lastDate != lastDate) { // diese Zeile erhält keine weiteren Updates mehr ziel.removeAttribute("threadId"); ziel = viaXpath0("following-sibling::TD[4]", ziel); ziel.title = "Abweichendes Datum des letzten Beitrages\n" + formatDate(new Date(entry.lastDate)); ziel.setAttribute("bgcolor", lightRed); } else if (entry.maxCount != beitraege) { markBeitragsCounter(ziel, entry, beitraege); } } } else { var stamp = new Date(entry.lastDate); if (ziel.getElementsByTagName("A")[0].innerHTML.indexOf("verschoben:") >= 0) { ziel.title = format( "Mindestens %s (=%s-%s) neue Beiträge seit dem Verschieben!\nGelesen bis: %s%s", beitraege - entry.maxCount, beitraege, entry.maxCount, wochentag[stamp.getDay()], formatDate(stamp)); ziel.setAttribute("bgcolor", lightCyan); } else if (lastDate < entry.lastDate) { ziel.title = format("Übersicht veraltet: %s (=%s-%s) neue Beiträge\nGelesen bis: %s%s", beitraege - entry.maxCount, beitraege, entry.maxCount, wochentag[stamp.getDay()], formatDate(stamp)); ziel.setAttribute("bgcolor", lightYellow); } else { if (beitraege < entry.maxCount) { markBeitragsCounter(ziel, entry, beitraege); } ziel.title = format("neue Beiträge: %s (=%s-%s)\nGelesen bis: %s%s", beitraege - entry.maxCount, beitraege, entry.maxCount, wochentag[stamp.getDay()], formatDate(stamp)); ziel.setAttribute("bgcolor", lightGreen); } } } } // immer wieder aktualisieren, solange das Brett noch offen ist window.setTimeout(colorizeBoardOverview, 15000); } async function formatBoardOverview(board) { document.title = "KG-" + board.replace(/\(Moderator.*/, ""); if (window.location.search) { window.location.search.search(/&boardid=(\d+)/); board = RegExp.$1; } else { board = null; } var index = null; if (!board) { window.location.pathname.search(/threads_5_(\d+)(_\d+_(\d+))?/); board = RegExp.$1; index = RegExp.$3; } document.title += " [" + (index ? index / 30 + 1 : 1) + "]"; if (debug) { debugString = document.createElement("FONT"); debugString.size = 1; debugString.innerHTML = "<A href='/?forumid=5&action=mysite&mysite=newpm&searchid2=79596&searchid=1'>PN an Bulli</A><BR>"; debugString.appendChild(document.createTextNode( "[table][tr][td]angezeigt[/td][td]tatsächlich[/td][td]ThreadId[/td][td]" + "Board: [url=" + window.location + "]" + document.title.substr(3) + "[/url]")); debugString.appendChild(document.createElement("BR")); document.body.appendChild(debugString); document.body.appendChild(document.createTextNode("[/td][/tr][/table]")); document.body.appendChild(document.createElement("BR")); } board = "/display_5_" + board + "_"; if (debug) { debugBoard = preferredHost + board; } // iteriere Themen-Zeilen und ändere verschiedene Ziel-Zellen der Übersichts-Tabelle var row = viaXpath("//TR/TD[position()=5 and @bgcolor='#dbdbdb']/FONT[@size]"); var reg_markup = /<.*?>|\./g, reg_parenthesis = /\(/g, reg_thread = /_(\d+)\.html/, reg_von = /\s*von\s*/; var anzahlBeitraege = 0, anzahlLesungen = 0; for (var i = row.snapshotLength - 1; 0 <= i ; i--) { var zaehler = row.snapshotItem(i), ziel = viaXpath0("ancestor::TD[1]/following-sibling::TD[@bgcolor='#dbdbdb']/FONT[@size]", zaehler); // Bilde Verhältnis Gelesen / Beiträge² var beitraege = eval(zaehler.innerHTML.replace(reg_markup, "").replace(reg_parenthesis, "+(")); anzahlBeitraege += beitraege; var lesungen = parseInt(ziel.innerHTML.replace(reg_markup, "")); anzahlLesungen += lesungen; ziel.innerHTML += format(" <font color='%s'>/ %s</font>", beitraege < 5 ? "#aaa" : beitraege < 10 ? "#555" : "black", (lesungen / beitraege / beitraege).toFixed(1)); ziel = viaXpath0("ancestor::TD[1]/following-sibling::TD[@bgcolor='#eeeeee']/FONT[@size]", ziel); // annotiere den Wochentag var lastDate = parseDate(ziel.innerHTML); ziel.innerHTML = wochentag[lastDate.getDay()].substr(1, 2) + " " + formatDate(lastDate) + ziel.innerHTML.substr(ziel.innerHTML.indexOf("<")); ziel = ziel.getElementsByTagName("A")[0]; ziel.href.search(reg_thread); var threadId = RegExp.$1; if (debug) { var b = beitraege; var entry = await getThreadEntry(parseInt(threadId)); if (entry && b < entry.maxCount) { b = entry.maxCount; } zaehler.innerHTML = format("<A href='%s%s_%s_%s.html' target='_blank'>%s</A>", // beitraege evtl. < b -> beitraege steuert nur Navigation unter Thread // Beitrags-Zahl aus Board transportieren für Zählerstand-Korrektur board, threadId, beitraege, b - 1 - (b - 1) % beitraege_pro_seite, zaehler.innerHTML); } // letzten Beitrag direkt anspringen aus dem Brett heraus (höhere Priorität als nur "Neu") ziel.href = format("%s%s%s.html?goto=%s-%s", board, threadId, beitraege <= beitraege_pro_seite ? "" : "_" + beitraege + "_" + (beitraege - 1 - (beitraege - 1) % beitraege_pro_seite), formatDate(lastDate).replace(reg_space, "_"), ziel.innerHTML.replace(reg_von, "")); // markiere diesen Beitrag als beobachtet ziel = viaXpath0("ancestor::TD[1]/preceding-sibling::TD[4]", ziel); // nicht nur ein Feld in dem Wrapper setzen, sondern bis ins unterliegende HTML-Element durchschreiben ziel.setAttribute("threadId", threadId); ziel.setAttribute("lastDate", lastDate.getTime()); ziel.setAttribute("beitraege", beitraege); if (beitraege > beitraege_pro_seite) { ziel.getElementsByTagName("A")[0].href += "?goto=new"; } } // Gesamtzahl-Threads und Beiträge var header = viaXpath0("//TABLE//TABLE/TBODY/TR/TD/FONT/B[contains(text(), 'Beiträge')]"); header.innerHTML += "<BR>" + row.snapshotLength + " / " + anzahlBeitraege; header = viaXpath0("//TABLE//TABLE/TBODY/TR/TD/FONT/B[contains(text(), 'Gelesen')]"); header.innerHTML += "<BR>" + anzahlLesungen; colorizeBoardOverview(); } function formatProfile(beitragCounterFontElement, username) { username = username.data.trim(); document.title = "KG-" + username + " (Profil)"; // Benutzername // Nur für Staffs: Link zum Löschen des Avatar-Bildchens var text = viaXpath0("HTML/BODY/TABLE//B[FONT/A/U[contains(text(), 'Live Diskutieren')]]/../FONT/B"); if (text.innerHTML.indexOf(" (Staff-Member)") > 0) { text = "/cache/b_5/avatare/tn_"; var img = viaXpath0("//TABLE//TABLE//TABLE/TBODY/TR/TD/IMG[@border='0' and contains(@src, '" + text + "')]"); if (img) { text = img.src.substr(img.src.indexOf(text) + text.lastIndexOf("tn_")); img = viaXpath0("following-sibling::FONT/STRONG", img); img.innerHTML = "<A href='/?forumid=5&action=delavatar&avatar=forum&file=" + text + "'>Avatar '" + text + "' löschen!</A><BR><BR>" + img.innerHTML; } } // biete großen Button zur Suche nach allen Beiträgen an (mehr / ältere Suchergebnisse durch höheres Limit) var form = document.createElement("FORM"); form.action = "/?action=search&forumid=5"; form.method = "POST"; // ?searchstring=Suchbegriff&searchuser=private_lock&exactname=1&searchmode=keywords&searcharea=all&searchdate=0&zeitdirection=juenger&sortierung=datum&searchdirection=desc&submit=Suchen+starten var strings = new Array( new Array("searchstring", "Suchbegriff"), new Array("searchuser", username), new Array("exactname", "1"), new Array("searchmode", "keywords"), new Array("searcharea", "all"), new Array("searchdate", "0"), new Array("zeitdirection", "juenger"), new Array("sortierung", "datum"), new Array("searchdirection", "desc"), new Array("submit", "Alle Beitr\u00E4ge von " + username) ); for (var k = 0; k < strings.length; k++) { var hiddenInput = document.createElement("INPUT"); hiddenInput.type = "hidden"; hiddenInput.name = strings[k][0]; hiddenInput.value = strings[k][1]; form.appendChild(hiddenInput); } form.lastChild.type = "submit"; form.lastChild.title = "Ausführliche Suche, die weiter in die Vergangenheit reicht"; beitragCounterFontElement.removeChild(beitragCounterFontElement.firstChild); beitragCounterFontElement.appendChild(form); var schnellsuche = viaXpath0("//FONT[@size]/STRONG/A[contains(@href, 'searchpost')]"); schnellsuche.title = "Schnellsuche nach den aktuellsten Beitr\u00E4gen"; schnellsuche.innerHTML = schnellsuche.innerHTML.replace("Alle Nachrichten", "Aktuelle Beiträge"); var lineHead = viaXpath("//TABLE//TABLE//TABLE//TABLE/TBODY/TR/TD/FONT[@size and @face]"); var reg_url = /https?:\/\//i; for (var k = 0; k < lineHead.snapshotLength; k++) { var line = lineHead.snapshotItem(k); var value = line.innerHTML.trim(); // alert(k + "/" + lineHead.snapshotLength + " = '" + value + "'"); if (value == "Mitglied seit" || value == "Zuletzt online") { // annotiere Wochentage an den Daten line = viaXpath0("parent::TD/following-sibling::TD/B/FONT[@size and @face]", line); value = line.innerHTML; var date = parseDate(value); if (date) { line.innerHTML = wochentag[date.getDay()] + formatDate(date); } } else if (value == "Webseite") { // wandle die Webseite in eine anklickbare Verknüpfung line = viaXpath0("parent::TD/following-sibling::TD/B/FONT[@size and @face]", line); value = line.innerHTML.trim(); if (value.length === 0 || "http://www.-".indexOf(value) >= 0) { continue; } if (value.search(reg_url) !== 0) { value = "http://" + value; } line.innerHTML = "<A href=\"" + value + "\">" + line.innerHTML + "</A>"; } } } function addGotoForSuchergebnis(term) { // vermeide überflüssige Leerzeichen in den Suchbegriffen if (term) { term = encodeURI(term.replace(/User:/, "").replace(reg_multiSpace, " ").trim()); } // modifiziere die Verknüpfungen, so dass sie direkt zum Ziel führen var hl = "highlight="; var link = viaXpath("//TR/TD[3]/FONT[@size]/A[contains(@href, '" + hl + "')]"); for (var i = link.snapshotLength - 1; 0 <= i; i--) { var l = link.snapshotItem(i); var timestamp = viaXpath("ancestor::TD[1]/following-sibling::TD[@align='center']/FONT[@size]", l, true); var von = timestamp.snapshotItem(0).innerHTML; timestamp = timestamp.snapshotItem(1); var attr = l.href; if (term) { attr = attr.substr(0, attr.indexOf(hl) + hl.length) + term; } l.href = attr + "&goto=" + encodeURI(timestamp.innerHTML.replace(reg_space, "_") + "-" + von); // annotiere den Wochentag im Suchergebnis von = parseDate(timestamp.innerHTML); timestamp.innerHTML = wochentag[von.getDay()].substr(1,2) + " " + formatDate(von); } markThreadLinks(viaXpath0("//TABLE//TABLE[TBODY//B[text()='Thema']]")); } // Parameter-Links, die in ein Brett springen, funktionieren nur schlecht -> ersetze den ganzen Rattenschwanz function fixJumpMenu(threadTitle) { var jumpurl = viaXpath0("//SELECT[@name='jumpurl']"); if (!jumpurl) { return; } // jumpurl.setAttribute("onchange", jumpurl.getAttribute("onchange").replace(/Go/, "window.location.href = ")) // Die IDs der Hierarchie-Stufen sind nicht in der Seite enthalten -> hart kodiert var codes = [ [2427, "General Board"], [2434, "Keuschhaltung in Perfektion"], [2437, "Medizinische Fesseln"], [2430, "SM-Boards"], [2433, "Fetische"], [2436, "Hier gibt es alle Stories"], [2428, "Wandgeflüster"], [2432, "Sklavenstall"], [2435, "KG-Träger Boards"], [2426, "Key-Holder Boards"], [2429, "Staff-Board (Zugriff nur für Forum-Supporter!!)"], ], j = 0; var reg_uebersicht = /^-+(.*?)-+$/, reg_boardId = /.*boardid=(\d+).*/; // Aktuellen Eintrag ausgrauen var currentBoardID = reg_boardId.exec(window.location), repair = false; if (currentBoardID) { currentBoardID = currentBoardID[1]; } if (!currentBoardID) { var reg_boardId2 = /(threads|display|reply|post)_5_(\d+)/; reg_boardId2.exec(window.location); currentBoardID = RegExp.$2; if (!currentBoardID) { // z.B. wenn gerade ein neuer Beitrag geschrieben wurde var reply = viaXpath0("//IMG[contains(@src,'/images/theme1/reply.gif')]/parent::A[@href]"); if (reply) { repair = true; } else { // oder ein Thread gelockt wurde reply = viaXpath0("//IMG[contains(@src,'/images/theme1/new_thread.gif')]/parent::A[@href]"); } reg_boardId2.exec(reply.href); currentBoardID = RegExp.$2; } } for (var i = 0; i < jumpurl.childNodes.length; i++) { var option = jumpurl.childNodes[i]; // Alle Übersichtseinträge mit -- bekommen die ID des aktuellen Brettes. (macht keinen Sinn, is aber so!) if (option.firstChild.data.match(reg_uebersicht)) { // Baue URLs auf die Hierarchie-Stufen: z.B. http://kgforum.org/?action=cat&forumid=5&cat=2427 while (j < codes.length && codes[j][1] != RegExp.$1) { // springe über für den jeweiligen Benutzer nicht sichtbare Bereiche hinweg j++; } // console.log(format("reg='%s' j='%s' = '%s' followed by '%s'", RegExp.$1, j, codes[j], codes[j+1])); option.value = "/?action=cat&forumid=5&cat=" + codes[j++][0]; if (codes.length == j) { // nur Staffs und Moderatoren können die Moderatoren-Ecke zugreifen var mod = document.createElement("OPTION"); mod.value = "/boardid=2439"; mod.innerHTML = ">Moderatoren-Ecke"; jumpurl.insertBefore(mod, jumpurl.childNodes[++i]); option = mod; } else { continue; } } else if ("Hauptansicht" == option.firstChild.data) { option.value = "/"; // relativ zur Domain ... bleibt nur die Domain übrig continue; } // relative URL auf der aktuellen Domain - löscht bestehende Parameter (z.B. nach Absenden eines Beitrages) option.value = option.value.replace(reg_boardId, "/threads_5_$1.html"); if (RegExp.$1 == currentBoardID) { option.style.fontWeight = "bold"; if (repair) { repair = option.firstChild.data.substr(1); viaXpath0("//IMG[contains(@src,'/images/theme1/folder-2p.gif')]/following-sibling::A[@href]") .innerHTML = repair; document.title = "KG-" + threadTitle + " (" + repair + ")"; repair = null; } } } } async function formatMainPage() { // Letzter Beitrag je Brett var cell = viaXpath("//BODY//TABLE//TABLE//TD/TABLE//TD[@rowspan=2]/parent::TR/parent::TBODY/parent::TABLE/parent::TD"); var now = new Date(); for (var i = 0; i < cell.snapshotLength; i++) { var c = cell.snapshotItem(i); var link = viaXpath0("./TABLE//FONT/parent::A[contains(@href, 'display')]", c); var newStamp = parseDate(link.firstChild.firstChild.data); link.firstChild.firstChild.data = wochentag[newStamp.getDay()] + formatDate(newStamp); var threadId, beitragId; if (reg_beitrag.exec(link)) { threadId = parseInt(RegExp.$3), beitragId = parseInt(RegExp.$5); } else if (/&threadid=(\d+)\|(\d+)/.exec(link)) { threadId = parseInt(RegExp.$1), beitragId = parseInt(RegExp.$2); } var entry = await getThreadEntry(threadId); if (entry) { if (beitragId == entry.postId) { c.title = format("Letzter Beitrag wurde bereits gelesen!%nNichts neues seit:%s", formatDateDifference(newStamp, now)); c.style.background = lightCyan; } else { var stamp = new Date(entry.lastDate); if (stamp < newStamp) { c.title = format("Neue Beiträge!\nGelesen bis:%s", formatDateDifference(stamp, newStamp)); c.style.background = lightGreen; } else { c.title = format("Beitrag verschoben oder gelöscht!\nGelesen bis:%s", formatDateDifference(stamp, newStamp)); c.style.background = lightYellow; } } } } // Die letzten 40 Beiträge cell = viaXpath("//BODY//TABLE//TABLE//TD[@colspan=5]//B/A[contains(@href, '#')]"); for (var i = 0; i < cell.snapshotLength; i++) { var c = cell.snapshotItem(i); var klammer = viaXpath0("parent::B/following-sibling::FONT", c); var newStamp = klammer.firstChild.data; newStamp = parseDate(newStamp.substr(1, newStamp.length - 2)); klammer.style.color = "#777"; klammer.innerHTML = "(" + formatDateDifference(newStamp, now) + ")"; reg_beitrag.exec(c); var threadId = parseInt(RegExp.$3), beitragId = parseInt(RegExp.$5); var entry = await getThreadEntry(threadId); if (entry) { var stamp = new Date(entry.lastDate); if (beitragId <= entry.postId) { // ein Thread kann mehrfach auftauchen und noch ältere Beiträge anbieten c.title = format("Dieser Beitrag wurde bereits gelesen!%nNichts neues seit:%s", formatDateDifference(stamp, now)); c.style.background = lightCyan; } else { c.title = format("Neuer Beitrag\nGelesen bis:%s", formatDateDifference(stamp, newStamp)); c.style.background = lightGreen; } } } } document.title = "KG:Forum"; var main = viaXpath0("//BODY//TABLE//TABLE//FONT/B/IMG[@border=0 and contains(@src, '/images/theme1/open.gif')]"); if (main) { formatMainPage(); logTime(); return; } tagArray = document.getElementsByTagName("FONT"); var reg_beitGesch = /Beitr.{1,2}ge geschrieben/, reg_moderator = /\s*\(Moderator/, reg_suchergebnis = /^Suchergebnis: (.*)/, reg_start = /start=(\d+)/, reg_threadID = /threadid=(\d+)/, reg_highlight = /(\?highlight=[^&]+)/; var board = null; EACHFONT: for (i = 0; i < tagArray.length && i < 15; i++) { // suche nach Profilen if (i > 1 && (board = tagArray[i].firstChild) && board.nodeName == "#text" && reg_beitGesch.exec(board.data)) { board = tagArray[i - 2].firstChild; if (!board || board.nodeName != "#text") { continue EACHFONT; } formatProfile(tagArray[i], board); break EACHFONT; } // suche nach der 2. & 3. Ebene einer Ordnerstruktur, um den Titel von Brett & Thema zu erkennen var imgArray = tagArray[i].getElementsByTagName("IMG"); for (j = 0; j < imgArray.length; j++) { var imgSrc = imgArray[j].src; if (!imgSrc) { continue; } if (imgSrc.indexOf("folder-2.gif") > 0) { board = imgArray[j].nextSibling; if (board.nextSibling.nodeName == "FONT") { board = board.nextSibling.firstChild; if (board.nodeName == "B") { board = board.firstChild; } } fixJumpMenu(""); formatBoardOverview(board.data.trim()); break EACHFONT; } if (imgSrc.indexOf("folder-2p.gif") > 0) { board = imgArray[j].parentNode.getElementsByTagName("A"); if (!board || board.length < 2) { continue; } board = board[1].firstChild; if (!board) { continue; } if (board.nodeName == "FONT") { board = board.firstChild; } if (board.nodeName == "B") { board = board.firstChild; } board = board.data.trim(); var index = board.search(reg_moderator); if (index > 0) { board = board.substr(0, index); } document.title = "KG-" + board; continue; } if (imgSrc.indexOf("folder-3.gif") > 0) { var threadId = null, searchstring = imgArray[j].nextSibling.data.trim(); var count = 0, maxCount = null; if (searchstring.length === 0 && imgArray[j].nextSibling.nextSibling) { // eventuell muss noch ein FONT-tag ausgepackt werden. searchstring = imgArray[j].nextSibling.nextSibling.firstChild.data.trim(); } document.title = "KG-" + searchstring + " (" + board + ")"; if (searchstring.search(reg_suchergebnis) === 0) { addGotoForSuchergebnis(RegExp.$1); break EACHFONT; } fixJumpMenu(searchstring); showDayOfWeek(); if (reg_beitrag.exec(window.location.pathname)) { threadId = parseInt(RegExp.$3); if (RegExp.$7) { maxCount = parseInt(RegExp.$5); count = parseInt(RegExp.$7); } } if (reg_start.exec(window.location.search)) { count = parseInt(RegExp.$1); } if (reg_threadID.exec(window.location.search)) { threadId = parseInt(RegExp.$1); } searchstring = reg_highlight.exec(window.location.search) ? RegExp.$1 : null; if (searchstring || !threadId || !maxCount || count === 0) { // durchsuche die Navigation, falls vorhanden tagArray = viaXpath( "//IMG[contains(@src, '/images/theme1/end.gif')]/ancestor::TABLE[@cellspacing=2 and @cellpadding=0]//A[@href]"); for (i = 0; i < tagArray.snapshotLength; i++) { var link = tagArray.snapshotItem(i); if (reg_beitrag.exec(link.href)) { if (searchstring) { // erhalte die Hervorhebungen der Suche link.href += searchstring; } if (!threadId) { threadId = parseInt(RegExp.$3); } if (RegExp.$7) { if (!maxCount) { maxCount = parseInt(RegExp.$5); } if (count === 0 && link.innerHTML.indexOf("«") >= 0) { count = parseInt(RegExp.$7) + beitraege_pro_seite; } } } } } if (beitraege_pro_seite < maxCount) { document.title = document.title.replace(" (", " [" + (1 + count / beitraege_pro_seite | 0) + "] ("); } threadId = insertLinks(threadId, count, maxCount, searchstring); if (threadId) { threadContentTouchup(threadId, count, maxCount); } break EACHFONT; } } } // Durchlauf ohne Fehler bis hier her -> dann noch die Zeit loggen und fertig logTime(); // die anonyme Funktion sofort aufrufen })();