| 
          const carriageReturnIndicator = "⏭️"; //WARNING: There is a regexp replace that uses this literal value. | 
        
        
           | 
          const spaceReplacerChar = " "; // ◽ | 
        
        
           | 
          const pluginClassName = "speedReadingPlugin"; | 
        
        
           | 
          
 | 
        
        
           | 
          function updateReadTimeEstimate(phrases, speedWPM) { | 
        
        
           | 
            var readTimeEstimateEl = document.getElementById("readTimeEstimate"); | 
        
        
           | 
            readTimeEstimateEl.innerText = | 
        
        
           | 
              "Expected time to read the whole document at current speed: " + | 
        
        
           | 
              ((phrases.length * 60000) / speedWPM / 1000 / 60).toFixed(1) + | 
        
        
           | 
              "min."; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          function getProgressIndexFromAbsoluteProgress( | 
        
        
           | 
            absoluteProgress, | 
        
        
           | 
            splittedPhrases | 
        
        
           | 
          ) { | 
        
        
           | 
            var spaceCount = 0; | 
        
        
           | 
            for (const [index, phrase] of splittedPhrases.entries()) { | 
        
        
           | 
              spaceCount = spaceCount + 1 + (phrase[0].match(/ /g) || []).length; | 
        
        
           | 
              if (spaceCount > absoluteProgress) { | 
        
        
           | 
                if (index - 1 < 0) { | 
        
        
           | 
                  return 0; | 
        
        
           | 
                } else { | 
        
        
           | 
                  return index - 1; | 
        
        
           | 
                } | 
        
        
           | 
              } | 
        
        
           | 
            } | 
        
        
           | 
            return index; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          function getAbsoluteProgressFromProgressIndex(progressIndex, splittedPhrases) { | 
        
        
           | 
            return splittedPhrases | 
        
        
           | 
              .slice(0, progressIndex) | 
        
        
           | 
              .flatMap((phrase) => phrase[0].split(" ")).length; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          class SpeedReader { | 
        
        
           | 
            constructor(speedReaderConfig) { | 
        
        
           | 
              this.totalTime = 0; | 
        
        
           | 
              this.totalWordsRead = 0; | 
        
        
           | 
              this.obsidianWindow = speedReaderConfig.obsidian; | 
        
        
           | 
              this.tp = speedReaderConfig.tp; | 
        
        
           | 
              this.filePath = false; | 
        
        
           | 
              this.speedWPM = speedReaderConfig.speedWPM; | 
        
        
           | 
              this.maxReadableCharacters = speedReaderConfig.maxReadableCharacters; | 
        
        
           | 
              this.running = false; | 
        
        
           | 
              this.activeIntervalId = false; | 
        
        
           | 
              this.textFontSize = speedReaderConfig.textFontSize; | 
        
        
           | 
              this.createSpeedReadingWidget(); | 
        
        
           | 
              this.changeSpeed(0); | 
        
        
           | 
              document.getElementById("read_hotkey_focus").focus(); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            readToggle() { | 
        
        
           | 
              if (this.running) { | 
        
        
           | 
                this.pauseReading(); | 
        
        
           | 
              } else { | 
        
        
           | 
                this.startReading(); | 
        
        
           | 
              } | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            hotkeyPressed(event) { | 
        
        
           | 
              if ( | 
        
        
           | 
                event.defaultPrevented || | 
        
        
           | 
                document.activeElement.id != "read_hotkey_focus" | 
        
        
           | 
              ) { | 
        
        
           | 
                return; // Do nothing if the event was already processed | 
        
        
           | 
              } | 
        
        
           | 
          
 | 
        
        
           | 
              switch (event.key) { | 
        
        
           | 
                case "Down": // IE/Edge specific value | 
        
        
           | 
                case "ArrowDown": | 
        
        
           | 
                case "s": | 
        
        
           | 
                  document.getElementById("read_slower").click(); | 
        
        
           | 
                  break; | 
        
        
           | 
                case "Up": // IE/Edge specific value | 
        
        
           | 
                case "ArrowUp": | 
        
        
           | 
                case "w": | 
        
        
           | 
                  document.getElementById("read_faster").click(); | 
        
        
           | 
                  break; | 
        
        
           | 
                case "Left": // IE/Edge specific value | 
        
        
           | 
                case "ArrowLeft": | 
        
        
           | 
                case "a": | 
        
        
           | 
                  document.getElementById("read_rewind").click(); | 
        
        
           | 
                  break; | 
        
        
           | 
                case "Right": // IE/Edge specific value | 
        
        
           | 
                case "ArrowRight": | 
        
        
           | 
                case "d": | 
        
        
           | 
                  document.getElementById("read_forward").click(); | 
        
        
           | 
                  break; | 
        
        
           | 
                // case "Enter": | 
        
        
           | 
                //   // Do something for "enter" or "return" key press. | 
        
        
           | 
                //   break; | 
        
        
           | 
                case "Esc": // IE/Edge specific value | 
        
        
           | 
                case "Escape": | 
        
        
           | 
                  document.getElementById("read_kill").click(); | 
        
        
           | 
                  break; | 
        
        
           | 
                case " ": | 
        
        
           | 
                  document.getElementById("read_toggle").click(); | 
        
        
           | 
                  break; | 
        
        
           | 
                default: | 
        
        
           | 
                  return; // Quit when this doesn't handle the key event. | 
        
        
           | 
              } | 
        
        
           | 
          
 | 
        
        
           | 
              // Cancel the default action to avoid it being handled twice | 
        
        
           | 
              event.preventDefault(); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            createSpeedReadingWidget() { | 
        
        
           | 
              const alreadyExists = document.getElementsByClassName(pluginClassName); | 
        
        
           | 
              if (alreadyExists.length > 0) { | 
        
        
           | 
                document.getElementById("read_kill").click(); | 
        
        
           | 
              } | 
        
        
           | 
              var parentElement = document.getElementsByClassName( | 
        
        
           | 
                "CodeMirror cm-s-obsidian" | 
        
        
           | 
              )[0]; | 
        
        
           | 
              var newEl = createElementFromHTML(readHtmlString); | 
        
        
           | 
              newEl.classList.add(pluginClassName); | 
        
        
           | 
              parentElement.insertBefore(newEl, parentElement.firstChild); | 
        
        
           | 
              addStyle(styleAsString(this.textFontSize), pluginClassName); | 
        
        
           | 
          
 | 
        
        
           | 
              this.attachButtonFunctions(); | 
        
        
           | 
          
 | 
        
        
           | 
              window.addEventListener("keydown", (evt) => this.hotkeyPressed(evt), true); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            killSpeedReader() { | 
        
        
           | 
              this.running = false; | 
        
        
           | 
              this.stopActiveInterval(); | 
        
        
           | 
              window.removeEventListener( | 
        
        
           | 
                "keydown", | 
        
        
           | 
                (evt) => this.hotkeyPressed(evt), | 
        
        
           | 
                true | 
        
        
           | 
              ); | 
        
        
           | 
              const alreadyExists = document.getElementsByClassName(pluginClassName); | 
        
        
           | 
              Array.from(alreadyExists).forEach((elem) => | 
        
        
           | 
                elem.parentNode.removeChild(elem) | 
        
        
           | 
              ); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            setReadProgressToCursor() { | 
        
        
           | 
              let cmEditor = this.obsidianWindow.app.workspace.activeLeaf.view.editor; | 
        
        
           | 
              const line = parseInt(cmEditor.getCursor("from").line); | 
        
        
           | 
              var idx = 0; | 
        
        
           | 
              var lastValidIndex = 0; | 
        
        
           | 
              for (const phrase of this.splittedPhrases) { | 
        
        
           | 
                const currentLine = phrase[1][0]; | 
        
        
           | 
                if (line <= currentLine) { | 
        
        
           | 
                  this.progressIndex = lastValidIndex; | 
        
        
           | 
                  break; | 
        
        
           | 
                } | 
        
        
           | 
                lastValidIndex = idx; | 
        
        
           | 
                idx++; | 
        
        
           | 
              } | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            goToLocation() { | 
        
        
           | 
              const [useless, open, close] = this.splittedPhrases[this.progressIndex]; | 
        
        
           | 
          
 | 
        
        
           | 
              const from = { line: open[0], ch: open[1] }; | 
        
        
           | 
              const to = { line: close[0], ch: close[1] }; | 
        
        
           | 
          
 | 
        
        
           | 
              let cmEditor = this.obsidianWindow.app.workspace.activeLeaf.view.editor; | 
        
        
           | 
              cmEditor.setSelection(from, to); | 
        
        
           | 
          
 | 
        
        
           | 
              let scrollInfo = cmEditor.getScrollInfo(); | 
        
        
           | 
              cmEditor.scrollTo(0, scrollInfo.top + 2 * scrollInfo.clientHeight); | 
        
        
           | 
          
 | 
        
        
           | 
              cmEditor.scrollIntoView({ | 
        
        
           | 
                from: { line: Math.abs(open[0] - 3), ch: open[1] }, | 
        
        
           | 
                to: { line: Math.abs(close[0] - 3), ch: close[1] }, | 
        
        
           | 
              }); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            attachButtonFunctions() { | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_progress_to_cursor") | 
        
        
           | 
                .addEventListener("click", (evt) => this.setReadProgressToCursor()); | 
        
        
           | 
          
 | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_wpm") | 
        
        
           | 
                .addEventListener("click", (evt) => this.goToLocation(evt)); | 
        
        
           | 
          
 | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_kill") | 
        
        
           | 
                .addEventListener("click", (evt) => this.killSpeedReader()); | 
        
        
           | 
          
 | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_toggle") | 
        
        
           | 
                .addEventListener("click", (evt) => this.readToggle(evt)); | 
        
        
           | 
          
 | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_faster") | 
        
        
           | 
                .addEventListener("click", (evt) => this.changeSpeed(+25)); | 
        
        
           | 
          
 | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_slower") | 
        
        
           | 
                .addEventListener("click", (evt) => this.changeSpeed(-25)); | 
        
        
           | 
          
 | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_rewind") | 
        
        
           | 
                .addEventListener("click", (evt) => this.forwardLines(-10)); | 
        
        
           | 
          
 | 
        
        
           | 
              document | 
        
        
           | 
                .getElementById("read_forward") | 
        
        
           | 
                .addEventListener("click", (evt) => this.forwardLines(10)); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            loadTextFromNote() { | 
        
        
           | 
              this.filePath = this.obsidianWindow.app.workspace.getActiveFile().path; | 
        
        
           | 
          
 | 
        
        
           | 
              var textToRead = this.tp.file.content; | 
        
        
           | 
              let maybeAbsoluteProgress = parseInt(this.tp.frontmatter.readProgress); | 
        
        
           | 
              let absoluteProgress = isNaN(maybeAbsoluteProgress) | 
        
        
           | 
                ? 0 | 
        
        
           | 
                : maybeAbsoluteProgress; | 
        
        
           | 
              this.splittedPhrases = this.textToArrayToShow(textToRead); | 
        
        
           | 
              this.progressIndex = getProgressIndexFromAbsoluteProgress( | 
        
        
           | 
                absoluteProgress, | 
        
        
           | 
                this.splittedPhrases | 
        
        
           | 
              ); | 
        
        
           | 
          
 | 
        
        
           | 
              updateReadTimeEstimate( | 
        
        
           | 
                this.splittedPhrases.slice( | 
        
        
           | 
                  this.progressIndex, | 
        
        
           | 
                  this.splittedPhrases.length | 
        
        
           | 
                ), | 
        
        
           | 
                this.speedWPM | 
        
        
           | 
              ); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            updateValues(i) { | 
        
        
           | 
              var p = getPhraseCenter(this.splittedPhrases[i][0]); | 
        
        
           | 
              document.getElementById("read_result").innerHTML = p; | 
        
        
           | 
              document.getElementById("read_progress").value = | 
        
        
           | 
                (100 * this.progressIndex) / this.splittedPhrases.length; | 
        
        
           | 
              this.goToLocation(); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            textToArrayToShow(input) { | 
        
        
           | 
              const charsNeedSpacing = ["?", "-", "—", "!", ":", ";", ")", "-", "]", "["]; | 
        
        
           | 
              var splittedText = input | 
        
        
           | 
                .replace(/(\r\n|\n|\r)/gm, carriageReturnIndicator) | 
        
        
           | 
                .replace(/(⏭️)+/gm, carriageReturnIndicator + " "); | 
        
        
           | 
              charsNeedSpacing.forEach( | 
        
        
           | 
                (x) => (splittedText = splittedText.replaceAll(x, x + " ")) | 
        
        
           | 
              ); | 
        
        
           | 
              splittedText = splittedText.split(/\s+/); | 
        
        
           | 
          
 | 
        
        
           | 
              const phrasedText = mergeSmallWords( | 
        
        
           | 
                splittedText, | 
        
        
           | 
                this.maxReadableCharacters | 
        
        
           | 
              ); | 
        
        
           | 
          
 | 
        
        
           | 
              var indexedPhrasedText = []; | 
        
        
           | 
              var inputLine = 0; | 
        
        
           | 
              var inputCol = 0; | 
        
        
           | 
              var phrasedTextIndex = 0; | 
        
        
           | 
              var phrasedTextCol = 0; | 
        
        
           | 
          
 | 
        
        
           | 
              var opening = 0; | 
        
        
           | 
          
 | 
        
        
           | 
              input.split("").forEach((inputCharacter, inputIndex) => { | 
        
        
           | 
                if (phrasedTextIndex == phrasedText.length) { | 
        
        
           | 
                  console.log(inputCharacter); // We finished our arranged text but there are still chars on the input text | 
        
        
           | 
                } else { | 
        
        
           | 
                  const phrasedTextString = phrasedText[phrasedTextIndex] | 
        
        
           | 
                    .split("") | 
        
        
           | 
                    .filter((char) => char.match(/[A-Z0-9]/gi)); | 
        
        
           | 
          
 | 
        
        
           | 
                  const phrasedTextCharacter = phrasedTextString[phrasedTextCol]; | 
        
        
           | 
                  if (inputCharacter == "\n") { | 
        
        
           | 
                    inputLine++; | 
        
        
           | 
                    inputCol = 0; | 
        
        
           | 
                  } else { | 
        
        
           | 
                    if ( | 
        
        
           | 
                      inputCharacter == phrasedTextCharacter && | 
        
        
           | 
                      inputCharacter.match(/[A-Z0-9]/gi) | 
        
        
           | 
                    ) { | 
        
        
           | 
                      if (phrasedTextCol == 0) { | 
        
        
           | 
                        opening = [inputLine, inputCol]; | 
        
        
           | 
                      } | 
        
        
           | 
                      if (phrasedTextCol == phrasedTextString.length - 1) { | 
        
        
           | 
                        indexedPhrasedText.push([ | 
        
        
           | 
                          phrasedText[phrasedTextIndex], | 
        
        
           | 
                          opening, | 
        
        
           | 
                          [inputLine, inputCol + 1], | 
        
        
           | 
                        ]); | 
        
        
           | 
                        phrasedTextIndex++; | 
        
        
           | 
                        phrasedTextCol = 0; | 
        
        
           | 
                      } else { | 
        
        
           | 
                        phrasedTextCol++; | 
        
        
           | 
                      } | 
        
        
           | 
                    } | 
        
        
           | 
                    inputCol++; | 
        
        
           | 
                  } | 
        
        
           | 
                } | 
        
        
           | 
              }); | 
        
        
           | 
          
 | 
        
        
           | 
              // Returning: [phrase, startingAbsolutePos, endingAbsolutePos] | 
        
        
           | 
          
 | 
        
        
           | 
              return indexedPhrasedText; | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            startReading() { | 
        
        
           | 
              document.getElementById("read_toggle").textContent = "⏸️"; | 
        
        
           | 
              // Going to stick to the originally open file | 
        
        
           | 
              let currentFileTextIsLoaded = this.filePath; // && this.filePath == this.obsidianWindow.app.workspace.getActiveFile().path; | 
        
        
           | 
          
 | 
        
        
           | 
              if (!currentFileTextIsLoaded) { | 
        
        
           | 
                this.loadTextFromNote(); | 
        
        
           | 
              } | 
        
        
           | 
          
 | 
        
        
           | 
              this.running = true; | 
        
        
           | 
          
 | 
        
        
           | 
              this.startReadingProgressIndex = this.progressIndex; | 
        
        
           | 
              this.startReadingTime = new Date().getTime(); | 
        
        
           | 
              this.startInterval(); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            intervalUpdateValues(speedReader) { | 
        
        
           | 
              if ( | 
        
        
           | 
                speedReader.running && | 
        
        
           | 
                speedReader.progressIndex < speedReader.splittedPhrases.length | 
        
        
           | 
              ) { | 
        
        
           | 
                speedReader.updateValues(speedReader.progressIndex); | 
        
        
           | 
                speedReader.progressIndex++; | 
        
        
           | 
              } else { | 
        
        
           | 
                speedReader.pauseReading(); | 
        
        
           | 
              } | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            changeSpeed(amount) { | 
        
        
           | 
              this.speedWPM = parseInt(this.speedWPM) + amount; | 
        
        
           | 
              const currentStatus = this.running; | 
        
        
           | 
              document.getElementById("read_wpm").textContent = this.speedWPM + " WPM"; | 
        
        
           | 
          
 | 
        
        
           | 
              if (currentStatus) { | 
        
        
           | 
                this.stopActiveInterval(true); | 
        
        
           | 
                this.startInterval(); | 
        
        
           | 
              } | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            calculateUserInfo(thisSessionTime, thisSessionWords) { | 
        
        
           | 
              var userInfo = ""; | 
        
        
           | 
              var end = new Date().getTime(); | 
        
        
           | 
              var time = ( | 
        
        
           | 
                parseInt(thisSessionTime) + | 
        
        
           | 
                (end - this.startReadingTime) / 1000 | 
        
        
           | 
              ).toFixed(0); | 
        
        
           | 
              userInfo += | 
        
        
           | 
                "Time read: " + time + "sec OR " + (time / 60).toFixed(1) + "min. "; | 
        
        
           | 
          
 | 
        
        
           | 
              const totalWordsRead = | 
        
        
           | 
                this.splittedPhrases | 
        
        
           | 
                  .slice(this.startReadingProgressIndex, this.progressIndex) | 
        
        
           | 
                  .flatMap((phrase) => | 
        
        
           | 
                    phrase[0].replace(spaceReplacerChar, " ").split(" ") | 
        
        
           | 
                  ) | 
        
        
           | 
                  .filter((word) => word.replace(/[^A-Z0-9]/gi, "").length > 0).length + | 
        
        
           | 
                thisSessionWords; | 
        
        
           | 
              userInfo += "Speed: " + ((60 * totalWordsRead) / time).toFixed(0) + " wpm."; | 
        
        
           | 
          
 | 
        
        
           | 
              return [userInfo, time, totalWordsRead]; | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            forwardLines(amountOfWords = 10) { | 
        
        
           | 
              const newCW = this.progressIndex + amountOfWords; | 
        
        
           | 
              this.progressIndex = newCW < 0 ? 0 : newCW; | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            startInterval() { | 
        
        
           | 
              this.activeIntervalId = setInterval( | 
        
        
           | 
                this.intervalUpdateValues, | 
        
        
           | 
                60000 / this.speedWPM, | 
        
        
           | 
                this | 
        
        
           | 
              ); | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            stopActiveInterval(keepReading = false) { | 
        
        
           | 
              this.running = keepReading; | 
        
        
           | 
              if (this.activeIntervalId) { | 
        
        
           | 
                clearInterval(this.activeIntervalId); | 
        
        
           | 
                this.activeIntervalId = false; | 
        
        
           | 
              } | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            async pauseReading() { | 
        
        
           | 
              this.stopActiveInterval(); | 
        
        
           | 
          
 | 
        
        
           | 
              updateReadTimeEstimate( | 
        
        
           | 
                this.splittedPhrases.slice( | 
        
        
           | 
                  this.progressIndex, | 
        
        
           | 
                  this.splittedPhrases.length | 
        
        
           | 
                ), | 
        
        
           | 
                this.speedWPM | 
        
        
           | 
              ); | 
        
        
           | 
          
 | 
        
        
           | 
              let readProgress = getAbsoluteProgressFromProgressIndex( | 
        
        
           | 
                this.progressIndex, | 
        
        
           | 
                this.splittedPhrases | 
        
        
           | 
              ); | 
        
        
           | 
          
 | 
        
        
           | 
              const { update } = this.obsidianWindow.app.plugins.plugins["metaedit"].api; | 
        
        
           | 
              await update("readProgress", readProgress, this.filePath); | 
        
        
           | 
          
 | 
        
        
           | 
              const [userInfoText, totalTime, totalWordsRead] = this.calculateUserInfo( | 
        
        
           | 
                this.totalTime, | 
        
        
           | 
                this.totalWordsRead | 
        
        
           | 
              ); | 
        
        
           | 
              this.totalTime = totalTime; | 
        
        
           | 
              this.totalWordsRead = totalWordsRead; | 
        
        
           | 
          
 | 
        
        
           | 
              document.getElementById("lastWordsReadInfo").innerText = userInfoText; | 
        
        
           | 
          
 | 
        
        
           | 
              document.getElementById("read_toggle").textContent = "▶️"; | 
        
        
           | 
            } | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          function mergeSmallWords(splittedText, maxReadableCharacters) { | 
        
        
           | 
            var newSplittedText = []; | 
        
        
           | 
            var lastWord = ""; | 
        
        
           | 
          
 | 
        
        
           | 
            for (const word of splittedText.filter((word) => word.trim() != "")) { | 
        
        
           | 
              // We only count alphanumeric, so we avoid newlines with just a parenthesis close | 
        
        
           | 
              const possibleNewMixedWord = lastWord.trim() + " " + word.trim(); | 
        
        
           | 
              const readableCharsInNewWord = possibleNewMixedWord.replace( | 
        
        
           | 
                /[^A-Z0-9]/gi, | 
        
        
           | 
                "" | 
        
        
           | 
              ).length; | 
        
        
           | 
              if (word.replace(/[^A-Z0-9]/gi, "") == "") { | 
        
        
           | 
                lastWord = possibleNewMixedWord.trim(); | 
        
        
           | 
              } else if ( | 
        
        
           | 
                readableCharsInNewWord > maxReadableCharacters || | 
        
        
           | 
                possibleNewMixedWord.includes(carriageReturnIndicator) || | 
        
        
           | 
                possibleNewMixedWord.includes(".") // new lines in dots to make everything more readable. | 
        
        
           | 
              ) { | 
        
        
           | 
                if (lastWord.replace(/[^A-Z0-9]/gi, "") != "") { | 
        
        
           | 
                  newSplittedText.push( | 
        
        
           | 
                    lastWord.trim().replace(carriageReturnIndicator, "") // Removed the indicator because in texts with bad carriage return the text became illegible. | 
        
        
           | 
                  ); | 
        
        
           | 
                } | 
        
        
           | 
                lastWord = word.trim(); | 
        
        
           | 
              } else { | 
        
        
           | 
                lastWord = possibleNewMixedWord.trim();  | 
        
        
           | 
              } | 
        
        
           | 
            } | 
        
        
           | 
            if (lastWord.replace(/[^A-Z0-9]/gi, "") != "") { | 
        
        
           | 
              newSplittedText.push(lastWord.trim()); | 
        
        
           | 
            } | 
        
        
           | 
            return newSplittedText; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          function getPhraseCenter(phrase) { | 
        
        
           | 
            var length = phrase.length; | 
        
        
           | 
          
 | 
        
        
           | 
            var highlightIndex = parseInt((length / 2).toFixed(0)) - 1; | 
        
        
           | 
          
 | 
        
        
           | 
            var highlightChar = phrase[highlightIndex]; | 
        
        
           | 
            if (highlightChar == " ") { | 
        
        
           | 
              highlightChar = spaceReplacerChar; | 
        
        
           | 
            } | 
        
        
           | 
          
 | 
        
        
           | 
            var result = | 
        
        
           | 
              '<div class="leftSide">' + | 
        
        
           | 
              phrase.slice(0, highlightIndex) + | 
        
        
           | 
              '</div><div class="highlight">' + | 
        
        
           | 
              highlightChar + | 
        
        
           | 
              '</div><div class="rightSide">' + | 
        
        
           | 
              phrase.slice(highlightIndex + 1, phrase.length) + | 
        
        
           | 
              "</div>"; | 
        
        
           | 
            return result; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          // HTML and CSS as string | 
        
        
           | 
          
 | 
        
        
           | 
          let readHtmlString = `<div id="read_holder"> | 
        
        
           | 
            <div id="read_container" style="width:800px;"> | 
        
        
           | 
              <button type="button" id="read_wpm"></button> | 
        
        
           | 
              <button type="button" id="read_toggle">▶️</button> | 
        
        
           | 
              <button type="button" id="read_rewind">⬅️</button> | 
        
        
           | 
              <button type="button" id="read_forward">➡️</button> | 
        
        
           | 
              <button type="button" id="read_faster">⬆️</button> | 
        
        
           | 
              <button type="button" id="read_slower">⬇️</button> | 
        
        
           | 
              <button type="button" id="read_kill">❌</button> | 
        
        
           | 
              <input type="input" placeholder="Focusme for hotkeys" id="read_hotkey_focus"></input> | 
        
        
           | 
              <details> | 
        
        
           | 
                <summary> | 
        
        
           | 
                  <progress id="read_progress" max="100" value="0"></progress> Show stats<button id="read_progress_to_cursor">Set progress to cursor position</button> | 
        
        
           | 
                </summary> | 
        
        
           | 
                <p id="readTimeEstimate"></p> | 
        
        
           | 
                <p id="lastWordsReadInfo">Stats will be available as soon as you pause your reading. Instructions:<br>Click on the input for the hotkeys to work: <br>Space -> play/pause. <br>Escape -> Close the reader. <br>Left right arrows: advance / go back 10 words. <br>Up down arrows: Faster / Slower reading. <br>Click on the "WPM" button to jump to the current word being read.<br>Click on the "Set progress to cursor position" button to keep reading on the selected line. This can only be done after having started reading previously.</p> | 
        
        
           | 
              </details> | 
        
        
           | 
              <div class="leftSide"></div> | 
        
        
           | 
              <div class="highlight">↓</div> | 
        
        
           | 
              <div class="rightSide"></div> | 
        
        
           | 
              <div id="read_result"> ▶️ : Start reading. ⬅️/➡️: forward/back 10 words, ⬆️ /⬇️ faster/slower. </div> | 
        
        
           | 
              <div class="leftSide"></div> | 
        
        
           | 
              <div class="highlight">↑</div> | 
        
        
           | 
              <div class="rightSide"></div> | 
        
        
           | 
            </div> | 
        
        
           | 
          </div> | 
        
        
           | 
          </div>`; | 
        
        
           | 
          
 | 
        
        
           | 
          /** | 
        
        
           | 
           * Utility function to add replaceable CSS. | 
        
        
           | 
           * @param {string} styleString | 
        
        
           | 
           */ | 
        
        
           | 
          function addStyle(styleString, pluginClassName) { | 
        
        
           | 
            const style = document.createElement("style"); | 
        
        
           | 
            document.head.append(style); | 
        
        
           | 
            style.classList.add(pluginClassName); | 
        
        
           | 
            style.textContent = styleString; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          function styleAsString(textFontSize) { | 
        
        
           | 
            return ( | 
        
        
           | 
              ` | 
        
        
           | 
          .highlight { | 
        
        
           | 
            /*color: red;*/ | 
        
        
           | 
            white-space: pre-wrap; | 
        
        
           | 
            font-weight: bold; | 
        
        
           | 
            font-family: "Droid Sans Mono", sans-serif; | 
        
        
           | 
            font-size: ` + | 
        
        
           | 
              textFontSize + | 
        
        
           | 
              `px; | 
        
        
           | 
          
 | 
        
        
           | 
            display: table-cell; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          .leftSide { | 
        
        
           | 
            white-space: pre-wrap; | 
        
        
           | 
            display: table-cell; | 
        
        
           | 
            font-family: "Droid Sans Mono", sans-serif; | 
        
        
           | 
            font-size: ` + | 
        
        
           | 
              textFontSize + | 
        
        
           | 
              `px; | 
        
        
           | 
          
 | 
        
        
           | 
            width: 40%; | 
        
        
           | 
            text-align: right; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          .rightSide { | 
        
        
           | 
            white-space: pre-wrap; | 
        
        
           | 
            display: table-cell; | 
        
        
           | 
            font-family: "Droid Sans Mono", sans-serif; | 
        
        
           | 
            font-size: ` + | 
        
        
           | 
              textFontSize + | 
        
        
           | 
              `px; | 
        
        
           | 
            width: 60%; | 
        
        
           | 
            text-align: left; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          #maxWantedCharacters { | 
        
        
           | 
            width: 60px; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          #read_container { | 
        
        
           | 
            background-color: #eeeeee; | 
        
        
           | 
            /* 600px+; small tablet portrait */ | 
        
        
           | 
            margin-left: auto; | 
        
        
           | 
            margin-right: auto; | 
        
        
           | 
            line-height: 43px; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
            #read_spacer { | 
        
        
           | 
              min-height: 105px; | 
        
        
           | 
            }` | 
        
        
           | 
            ); | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          function createElementFromHTML(htmlString) { | 
        
        
           | 
            var div = document.createElement("div"); | 
        
        
           | 
            div.innerHTML = htmlString.trim(); | 
        
        
           | 
          
 | 
        
        
           | 
            // Change this to div.childNodes to support multiple top-level nodes | 
        
        
           | 
            return div.firstChild; | 
        
        
           | 
          } | 
        
        
           | 
          
 | 
        
        
           | 
          module.exports = function (speedReaderConfig) { | 
        
        
           | 
            new SpeedReader(speedReaderConfig); | 
        
        
           | 
          }; | 
        
  
I am updating and improving this rather frequently, if someone else is using it please let me know and I will include changelog.