window.onUserInteraction = (() => { let groupStart = 0; // Group multiple interactions within a short time frame to avoid excessive calls const groupTime = 3000; return () => { let now = Date.now(); if (now - groupStart < groupTime) { // We're inside the group } else { // We're the first in a while groupStart = now; reader.post({ type: 'interaction' }); } }; })(); /* eslint-disable no-console */ window.reader = new (function () { const { readerSettings, chapterGeneralSettings, novel, chapter, batteryLevel, autoSaveInterval, DEBUG, strings, } = initialReaderConfig; // state this.hidden = van.state(true); this.batteryLevel = van.state(batteryLevel); this.readerSettings = van.state(readerSettings); this.generalSettings = van.state(chapterGeneralSettings); /** * Bumped whenever the app pushes new adjacent chapters. The chapter is * rendered before its neighbours are known, so any UI that depends on them * has to read this state to re-render when they arrive. */ this.adjacentVersion = van.state(0); this.chapterElement = document.querySelector('#LNReader-chapter'); this.selection = window.getSelection(); this.viewport = document.querySelector('meta[name=viewport]'); this.novel = novel; this.chapter = chapter; this.nextChapter = undefined; this.prevChapter = undefined; this.strings = strings; /** Called by the app once the neighbouring chapters have been resolved. */ this.setAdjacentChapters = ({ nextChapter, prevChapter, strings: texts }) => { this.nextChapter = nextChapter ?? undefined; this.prevChapter = prevChapter ?? undefined; Object.assign(this.strings, texts); this.adjacentVersion.val++; }; this.autoSaveInterval = autoSaveInterval; this.rawHTML = this.chapterElement.innerHTML; //layout props this.paddingTop = parseInt( getComputedStyle(document.querySelector('body')).getPropertyValue( 'padding-top', ), 10, ); this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop; this.layoutHeight = window.innerHeight; this.layoutWidth = window.innerWidth; this.layoutEvent = undefined; this.chapterEndingVisible = van.state(false); this.post = obj => window.ReactNativeWebView.postMessage(JSON.stringify(obj)); this.refresh = () => { this.layoutHeight = window.innerHeight; this.layoutWidth = window.innerWidth; this.paddingTop = parseFloat( getComputedStyle(document.body).getPropertyValue('padding-top'), ) || 0; if (this.generalSettings.val.pageReader) { this.chapterWidth = this.chapterElement.scrollWidth; } else { this.chapterHeight = this.chapterElement.scrollHeight + this.paddingTop; } }; let loadedFontFamily = readerSettings.fontFamily || ''; van.derive(() => { const settings = this.readerSettings.val; document.documentElement.style.setProperty( '--readerSettings-theme', settings.theme, ); document.documentElement.style.setProperty( '--readerSettings-padding', settings.padding + 'px', ); document.documentElement.style.setProperty( '--readerSettings-textSize', settings.textSize + 'px', ); document.documentElement.style.setProperty( '--readerSettings-textColor', settings.textColor, ); document.documentElement.style.setProperty( '--readerSettings-textAlign', settings.textAlign, ); document.documentElement.style.setProperty( '--readerSettings-lineHeight', settings.lineHeight, ); document.documentElement.style.setProperty( '--readerSettings-fontFamily', settings.fontFamily, ); if (settings.fontFamily && settings.fontFamily !== loadedFontFamily) { loadedFontFamily = settings.fontFamily; new FontFace( settings.fontFamily, 'url("file:///android_asset/fonts/' + settings.fontFamily + '.ttf")', ) .load() .then(function (loadedFont) { document.fonts.add(loadedFont); schedulePageCalculation(); }); } else if (!settings.fontFamily && loadedFontFamily) { loadedFontFamily = ''; // have no affect with a font declared in head document.fonts.forEach(fontFace => document.fonts.delete(fontFace)); } schedulePageCalculation(); }); document.onscrollend = () => { onUserInteraction(); if (!this.generalSettings.val.pageReader) { this.post({ type: 'save', data: parseInt( ((window.scrollY + this.layoutHeight) / this.chapterHeight) * 100, 10, ), }); } }; document.onpointerdown = () => onUserInteraction(); document.onpointermove = () => onUserInteraction(); document.onpointerup = () => onUserInteraction(); if (DEBUG) { // eslint-disable-next-line no-global-assign, no-new-object console = new Object(); console.log = function (...data) { reader.post({ 'type': 'console', 'msg': data?.join(' ') }); }; console.debug = console.log; console.info = console.log; console.warn = console.log; console.error = console.log; } // end reader })(); window.tts = new (function () { this.readableNodeNames = [ '#text', 'B', 'I', 'SPAN', 'EM', 'BR', 'STRONG', 'A', 'MARK', ]; this.prevElement = null; this.currentElement = reader.chapterElement; this.started = false; this.reading = false; this.elementsRead = 0; this.totalElements = 0; this.allReadableElements = []; // Store all readable elements at start this.textQueue = []; // Flat list of normalized text for native fallback this.readable = element => { const ele = element ?? this.currentElement; if ( ele.nodeName !== 'SPAN' && this.readableNodeNames.includes(ele.nodeName) ) { return false; } if (!ele.hasChildNodes()) { return false; } for (let i = 0; i < ele.childNodes.length; i++) { if (!this.readableNodeNames.includes(ele.childNodes.item(i).nodeName)) { return false; } } return true; }; this.normalizeText = text => { if (!text) return ''; const normalized = text .replace(/\s+/g, ' ') .trim() .replace(/^["'“”‘’]+|["'“”‘’]+$/g, '') .replace(/\s*([.,!?;:])\s*/g, '$1 ') .trim(); const dashOnlyText = normalized.replace(/\s/g, ''); if ( dashOnlyText.length >= 3 && /^[\-‐‑‒–—―−⁓⸺⸻﹘﹣-]+$/u.test(dashOnlyText) ) { return ''; } return normalized; }; // if can find a readable node, else stop tts // FIXED: Added proper boundary checks to prevent stack overflow this.findNextTextNode = (depth = 0) => { // Prevent deep recursion if (depth > 500) { console.warn('TTS: findNextTextNode max depth reached'); return false; } if (this.currentElement.isSameNode(reader.chapterElement) && this.started) { return false; } else { this.started = true; } // Safety check: ensure currentElement is valid if (!this.currentElement || !this.currentElement.nodeName) { return false; } // is read, have to go next or go back if (this.currentElement.isSameNode(this.prevElement)) { this.prevElement = this.currentElement; if (this.currentElement.nextElementSibling) { this.currentElement = this.currentElement.nextElementSibling; return this.findNextTextNode(depth + 1); } else if ( this.currentElement.parentElement && !this.currentElement.parentElement.isSameNode(document.body) && !this.currentElement.parentElement.isSameNode(document.documentElement) ) { this.currentElement = this.currentElement.parentElement; return this.findNextTextNode(depth + 1); } else { return false; } } else { // can read? read it if (this.readable()) { return true; } if ( !this.prevElement?.parentElement?.isSameNode(this.currentElement) && this.currentElement.firstElementChild ) { // go deep this.prevElement = this.currentElement; this.currentElement = this.currentElement.firstElementChild; return this.findNextTextNode(depth + 1); } else if (this.currentElement.nextElementSibling) { this.prevElement = this.currentElement; this.currentElement = this.currentElement.nextElementSibling; return this.findNextTextNode(depth + 1); } else if ( this.currentElement.parentElement && !this.currentElement.parentElement.isSameNode(document.body) && !this.currentElement.parentElement.isSameNode(document.documentElement) ) { this.prevElement = this.currentElement; this.currentElement = this.currentElement.parentElement; return this.findNextTextNode(depth + 1); } else { return false; } } }; this.next = () => { if (!this.started) return; reader.post({ type: 'tts-command', data: { command: 'next' } }); }; this.previous = () => { if (!this.started) return; reader.post({ type: 'tts-command', data: { command: 'previous' } }); }; this.start = element => { const startElement = element ?? reader.chapterElement; const readableEntries = this.getAllReadableElements(reader.chapterElement) .map(readableElement => ({ element: readableElement, text: this.normalizeText(readableElement.innerText), })) .filter(entry => !!entry.text); this.allReadableElements = readableEntries.map(entry => entry.element); this.totalElements = this.allReadableElements.length; this.textQueue = readableEntries.map(entry => entry.text); const requestedIndex = element && element !== reader.chapterElement ? this.allReadableElements.indexOf(startElement) : 0; const startIndex = requestedIndex >= 0 ? requestedIndex : 0; this.started = this.totalElements > 0; this.reading = this.started; this.setActiveIndex(startIndex); reader.post({ type: 'tts-queue', data: { queue: this.textQueue, startIndex, }, }); }; // Get all readable elements in order this.getAllReadableElements = element => { const elements = []; const traverse = el => { if (!el) return; if (this.readable(el)) { elements.push(el); // innerText already includes readable descendants, so descending any // further would add overlapping text to the speech queue. return; } for (let i = 0; i < el.children.length; i++) { traverse(el.children[i]); } }; traverse(element); return elements; }; this.resume = () => { if (!this.started) return; reader.post({ type: 'tts-command', data: { command: 'play' } }); }; this.pause = () => { if (!this.started) return; reader.post({ type: 'tts-command', data: { command: 'pause' } }); }; this.rewind = () => { if (!this.started) return; reader.post({ type: 'tts-command', data: { command: 'replay' } }); }; this.seekTo = index => { if (!this.started || !this.allReadableElements.length) return; const targetIndex = Math.max(0, Math.min(index, this.totalElements - 1)); reader.post({ type: 'tts-command', data: { command: 'seekTo', index: targetIndex }, }); }; this.stop = () => { reader.post({ type: 'tts-command', data: { command: 'stop' } }); this.reset(); }; this.reset = () => { this.currentElement?.classList?.remove('highlight'); this.prevElement = null; this.currentElement = reader.chapterElement; this.started = false; this.reading = false; this.elementsRead = 0; this.totalElements = 0; this.allReadableElements = []; this.textQueue = []; const playPauseButton = document.getElementById('TTS-PlayPause'); if (playPauseButton) playPauseButton.innerHTML = resumeIcon; const progress = document.getElementById('TTS-Progress'); if (progress) progress.textContent = ''; }; this.setActiveIndex = index => { if (!this.allReadableElements.length) return; const targetIndex = Math.max(0, Math.min(index, this.totalElements - 1)); this.currentElement?.classList?.remove('highlight'); this.currentElement = this.allReadableElements[targetIndex]; this.elementsRead = targetIndex + 1; this.started = true; this.scrollToElement(this.currentElement); this.currentElement.classList.add('highlight'); const progress = document.getElementById('TTS-Progress'); if (progress) { progress.textContent = `${targetIndex + 1}/${this.totalElements}`; } }; this.setPlaybackState = state => { this.reading = state === 'playing'; if (state === 'error') { this.reset(); return; } const playPauseButton = document.getElementById('TTS-PlayPause'); if (playPauseButton) { playPauseButton.innerHTML = this.reading ? pauseIcon : resumeIcon; } }; this.complete = () => { this.reading = false; if ( reader.readerSettings.val.tts?.autoPageAdvance === true && reader.nextChapter ) { reader.post({ type: 'next', autoStartTTS: true }); return; } this.reset(); }; this.isElementInViewport = element => { if (!element) return false; const rect = element.getBoundingClientRect(); const windowHeight = window.innerHeight || document.documentElement.clientHeight; const windowWidth = window.innerWidth || document.documentElement.clientWidth; return ( rect.top >= 0 && rect.left >= 0 && rect.bottom <= windowHeight && rect.right <= windowWidth ); }; // UPDATED: Scroll to top or center based on settings with padding for notch/camera this.scrollToElement = element => { if (!element) return; // Check if element is partially visible (at least some part is in viewport) const rect = element.getBoundingClientRect(); if (reader.generalSettings.val.pageReader) { const relativePage = Math.floor( (rect.left + rect.width / 2) / reader.layoutWidth, ); pageReader.movePage( Math.max( 0, Math.min( pageReader.totalPages.val - 1, pageReader.page.val + relativePage, ), ), ); return; } const windowHeight = window.innerHeight || document.documentElement.clientHeight; const isPartiallyVisible = rect.top < windowHeight && rect.bottom > 0 && rect.left < window.innerWidth && rect.right > 0; // Only scroll if element is not visible or barely visible if (!isPartiallyVisible || rect.top < 0 || rect.bottom > windowHeight) { // Check scrollToTop setting (default to true for better reading experience) const scrollToTop = reader.readerSettings.val.tts?.scrollToTop !== false; if (scrollToTop) { // Scroll to top with padding for notch/camera (80px from top) const elementTop = element.getBoundingClientRect().top + window.pageYOffset; const offsetPosition = elementTop - 80; // 80px padding for notch/camera window.scrollTo({ top: offsetPosition, behavior: 'smooth', }); } else { // Center scroll (original behavior) element.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest', }); } } }; this.speak = () => { this.rewind(); }; })(); // Watch for TTSEnable changes and stop TTS when disabled van.derive(() => { if (!reader.generalSettings.val.TTSEnable && window.tts) { if (tts.reading || tts.started) { tts.stop(); } } }); var pageReader; window.pageReader = pageReader = new (function () { const config = typeof initialPageReaderConfig === 'undefined' ? {} : initialPageReaderConfig; this.page = van.state(0); this.totalPages = van.state(0); this.ignoreClickUntil = 0; this.chapterNavigationPending = false; this.chapterEndingVisible = van.state( config.nextChapterScreenVisible === true, ); this.chapterEnding = document.getElementsByClassName('transition-chapter')[0]; this.showChapterEnding = (bool, instant, left) => { if (!this.chapterEnding) { this.chapterEnding = document.getElementsByClassName('transition-chapter')[0]; if (!this.chapterEnding) return; } const noAnim = Boolean( reader.generalSettings?.val?.pageReaderDisableAnimation, ); this.chapterEnding.style.transition = 'unset'; if (bool) { this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`; requestAnimationFrame(() => { if (!instant && !noAnim) { this.chapterEnding.style.transition = 'transform 200ms'; } this.chapterEnding.style.transform = 'translateX(-100vw)'; }); this.chapterEndingVisible.val = true; } else { if (!instant && !noAnim) { this.chapterEnding.style.transition = 'transform 200ms'; } this.chapterEnding.style.transform = `translateX(${left ? -200 : 0}vw)`; this.chapterEndingVisible.val = false; } }; this.movePage = (destPage, { interaction = true, save = true } = {}) => { if (interaction) { onUserInteraction(); } if (this.chapterEndingVisible.val) { if (this.chapterNavigationPending) { return; } if (destPage < 0) { this.showChapterEnding(false); return; } this.showChapterEnding(false, false, true); return; } destPage = parseInt(destPage, 10); const noAnim = Boolean( reader.generalSettings?.val?.pageReaderDisableAnimation, ); if (destPage < 0) { if (!reader.prevChapter) return; document.getElementsByClassName('transition-chapter')[0].innerText = reader.prevChapter.name; this.showChapterEnding(true, noAnim, true); this.chapterNavigationPending = true; setTimeout( () => { reader.post({ type: 'prev' }); }, noAnim ? 0 : 200, ); return; } if (destPage >= this.totalPages.val) { if (!reader.nextChapter) return; document.getElementsByClassName('transition-chapter')[0].innerText = reader.nextChapter.name; this.showChapterEnding(true, noAnim); this.chapterNavigationPending = true; setTimeout( () => { reader.post({ type: 'next' }); }, noAnim ? 0 : 200, ); return; } this.page.val = destPage; reader.chapterElement.style.transform = 'translateX(-' + destPage * 100 + '%)'; const newProgress = parseInt( ((this.page.val + 1) / this.totalPages.val) * 100, 10, ); if (save && newProgress > reader.chapter.progress) { reader.chapter.progress = newProgress; reader.post({ type: 'save', data: newProgress, }); } }; this.repaginate = ratio => { const previousTotal = this.totalPages.val; const currentRatio = previousTotal > 0 ? (this.page.val + 1) / previousTotal : 0; const positionRatio = Number.isFinite(ratio) ? ratio : currentRatio || reader.chapter.progress / 100; reader.refresh(); const chapterStyle = getComputedStyle(reader.chapterElement); const horizontalPadding = (parseFloat(chapterStyle.paddingLeft) || 0) + (parseFloat(chapterStyle.paddingRight) || 0); this.totalPages.val = Math.max( 1, Math.floor( (reader.chapterWidth + horizontalPadding) / (reader.chapterElement.clientWidth || reader.layoutWidth) + 0.001, ), ); if (this.chapterEndingVisible.val) { return; } const destination = Math.min( this.totalPages.val - 1, Math.max( 0, Math.round( this.totalPages.val * Math.min(1, Math.max(0, positionRatio)), ) - 1, ), ); this.movePage(destination, { interaction: false, save: false }); }; van.derive(() => { // ignore if initial or other states change if ( reader.generalSettings.val.pageReader === reader.generalSettings.oldVal.pageReader ) { return; } if (reader.generalSettings.val.pageReader) { const ratio = Math.min( 0.99, (window.scrollY + reader.layoutHeight) / reader.chapterHeight, ); document.body.classList.add('page-reader'); requestAnimationFrame(() => this.repaginate(ratio)); } else { const ratio = this.totalPages.val > 0 ? (this.page.val + 1) / this.totalPages.val : 0; reader.chapterElement.style.removeProperty('transform'); reader.chapterElement.style.removeProperty('transition'); document.body.classList.remove('page-reader'); requestAnimationFrame(() => { reader.refresh(); window.scrollTo({ top: reader.chapterHeight * ratio - reader.layoutHeight, behavior: 'smooth', }); }); } }); van.derive(() => { document.body.classList.toggle( 'no-animation', Boolean(reader.generalSettings.val.pageReaderDisableAnimation), ); reader.chapterElement?.style.removeProperty('transition'); const chapterEnding = this.chapterEnding || document.getElementsByClassName('transition-chapter')[0]; chapterEnding?.style.removeProperty('transition'); }); })(); document.addEventListener('DOMContentLoaded', () => { if (pageReader.chapterEndingVisible.val) { pageReader.showChapterEnding(true, true); } }); /** Scroll offset the reading position was restored to, in scroll mode. */ let restoredScrollTop = null; let positionRestored = false; function calculatePages(behavior = 'instant') { reader.refresh(); if (reader.generalSettings.val.pageReader) { pageReader.repaginate(reader.chapter.progress / 100); } else { restoredScrollTop = (reader.chapterHeight * reader.chapter.progress) / 100 - reader.layoutHeight; window.scrollTo({ top: restoredScrollTop, behavior }); } } let pageCalculationFrame; let pendingPageRatio; function schedulePageCalculation(ratio) { if (Number.isFinite(ratio)) { pendingPageRatio = ratio; } if ( pageCalculationFrame || !window.pageReader || !reader.generalSettings.val.pageReader ) { return; } pageCalculationFrame = requestAnimationFrame(() => { pageCalculationFrame = undefined; const nextRatio = pendingPageRatio; pendingPageRatio = undefined; pageReader.repaginate(nextRatio); }); } const ro = new ResizeObserver(() => { if (pageReader.totalPages.val && reader.generalSettings.val.pageReader) { schedulePageCalculation(); } }); ro.observe(reader.chapterElement); reader.chapterElement.addEventListener( 'load', () => schedulePageCalculation(), true, ); let viewportResizeTimer; window.addEventListener('resize', () => { clearTimeout(viewportResizeTimer); viewportResizeTimer = setTimeout(() => { if (reader.generalSettings.val.pageReader) { schedulePageCalculation(); } else { reader.refresh(); } }, 100); }); /** * Fonts and images can still change the chapter height after the position was * restored. Correct it when that happens - but only while the reader is sitting * exactly where it was put, otherwise this would yank the page from under * someone who has already started reading. */ const correctReadingPosition = () => { if (reader.generalSettings.val.pageReader || restoredScrollTop === null) { return; } const previousHeight = reader.chapterHeight; reader.refresh(); if ( reader.chapterHeight !== previousHeight && Math.abs(window.scrollY - Math.max(0, restoredScrollTop)) < 4 ) { calculatePages(); } }; const restoreReadingPosition = () => { requestAnimationFrame(() => setTimeout(() => { positionRestored = true; calculatePages(); // Deliberately not awaited before restoring: `document.fonts.ready` does // not resolve until the document has finished loading, which is what this // is trying to avoid waiting for. document.fonts.ready.then(correctReadingPosition); }, 0), ); }; // Restore as soon as the chapter itself is parsed and styled. Waiting for // `load` means waiting for every image in the chapter, which can take seconds - // the reader would sit at the top of the chapter until then. if (document.readyState === 'loading') { document.addEventListener('DOMContentLoaded', restoreReadingPosition, { once: true, }); } else { restoreReadingPosition(); } window.addEventListener('load', () => { if (!positionRestored) { restoreReadingPosition(); return; } if (reader.generalSettings.val.pageReader) { schedulePageCalculation(); } else { correctReadingPosition(); } }); // click handler (function () { const detectTapPosition = (x, y, horizontal) => { if (horizontal) { if (x < 0.33) { return 'left'; } if (x > 0.66) { return 'right'; } } else { if (y < 0.33) { return 'top'; } if (y > 0.66) { return 'bottom'; } } return 'center'; }; document.onclick = e => { if (Date.now() < pageReader.ignoreClickUntil) { return; } const { clientX, clientY } = e; const { x, y } = { x: clientX / reader.layoutWidth, y: clientY / reader.layoutHeight, }; if (reader.generalSettings.val.pageReader) { const position = detectTapPosition(x, y, true); if (position === 'left') { pageReader.movePage(pageReader.page.val - 1); return; } if (position === 'right') { pageReader.movePage(pageReader.page.val + 1); return; } } else { if (reader.generalSettings.val.tapToScroll) { const position = detectTapPosition(x, y, false); if (position === 'top') { window.scrollBy({ top: -reader.layoutHeight * 0.75, behavior: 'smooth', }); return; } if (position === 'bottom') { window.scrollBy({ top: reader.layoutHeight * 0.75, behavior: 'smooth', }); return; } } } reader.post({ type: 'hide' }); }; })(); // swipe handler (function () { this.initialX = null; this.initialY = null; reader.chapterElement.addEventListener('touchstart', e => { this.initialX = e.changedTouches[0].screenX; this.initialY = e.changedTouches[0].screenY; }); reader.chapterElement.addEventListener('touchmove', e => { if (reader.generalSettings.val.pageReader) { const diffX = (e.changedTouches[0].screenX - this.initialX) / reader.layoutWidth; reader.chapterElement.style.transition = 'unset'; reader.chapterElement.style.transform = 'translateX(-' + (pageReader.page.val - diffX) * 100 + '%)'; } }); reader.chapterElement.addEventListener('touchend', e => { const diffX = e.changedTouches[0].screenX - this.initialX; const diffY = e.changedTouches[0].screenY - this.initialY; if (reader.generalSettings.val.pageReader) { if (Math.abs(diffX) > 8 || Math.abs(diffY) > 8) { pageReader.ignoreClickUntil = Date.now() + 400; } reader.chapterElement.style.transition = reader.generalSettings.val .pageReaderDisableAnimation ? 'none' : 'transform 200ms'; reader.chapterElement.style.removeProperty('transition'); const diffXPercentage = diffX / reader.layoutWidth; if (diffXPercentage < -0.3) { pageReader.movePage(pageReader.page.val + 1); } else if (diffXPercentage > 0.3) { pageReader.movePage(pageReader.page.val - 1); } else { pageReader.movePage(pageReader.page.val); } return; } if ( e.target.id?.startsWith('scrollbar') || e.target.id === 'Image-Modal-img' ) { return; } if ( diffY > 80 && Math.abs(diffY) > Math.abs(diffX) * 2 && window.scrollY <= 0 ) { e.preventDefault(); reader.post({ type: 'refresh' }); return; } if ( reader.generalSettings.val.swipeGestures && Math.abs(diffX) > Math.abs(diffY) * 2 && Math.abs(diffX) > 180 ) { if (diffX < 0 && this.initialX >= window.innerWidth / 2) { e.preventDefault(); reader.post({ type: 'next' }); } else if (diffX > 0 && this.initialX <= window.innerWidth / 2) { e.preventDefault(); reader.post({ type: 'prev' }); } } }); })(); // text options (function () { // What the chapter element currently holds. The document is delivered with // the untransformed chapter already parsed, so writing the same markup back // would re-parse and re-layout the whole chapter (and restart image loads) // for nothing - which is exactly what happens when neither transform is on. let appliedHTML = reader.rawHTML; van.derive(() => { let html = reader.rawHTML; if (reader.generalSettings.val.bionicReading) { html = textVide.textVide(reader.rawHTML); } if (reader.generalSettings.val.removeExtraParagraphSpacing) { html = html .replace(/(?: \s*|[\u200b]\s*)+(?=<\/?p[> ])/g, '') .replace(/
\s*
\s*(?:
\s*)+/g, '

') //force max 2 consecutive
, chaining regex .replace( /
\s*
[^]+/, _ => `${ /\/p>/.test(_) ? _.replace( /
\s*
(?:(?=\s*<\/?p[> ])|(?<=<\/?p(?:>| [^>]+>)
\s*
))\s*/g, '', ) : _ }`, ) //if p found, delete all double br near p .replace( /
(?:(?=\s*<\/?p[> ])|(?<=<\/?p(?:>| [^>]+>)(?:<[^>]+>)*\s*
))\s*/g, '', ); } if (html === appliedHTML) { return; } reader.chapterElement.innerHTML = html; appliedHTML = html; reader.refresh(); schedulePageCalculation(); // Replacing the markup dropped the highlights, so restore the search. const searchQuery = window.readerSearch?.query; const searchIndex = window.readerSearch?.index; if (searchQuery) { window.readerSearch.search(searchQuery, searchIndex); } }); })();