var highlighter var util = rangy.util var doc = document var body = util.getBody(doc) var reviewClassNames = ['review', 'friend-review'] // 在DOM加载完成后应用字体设置 document.addEventListener('DOMContentLoaded', function() { if (window.wrInitialFontScale) { changeFontSizeWithScale(window.wrInitialFontScale); } }); rangy.ready(function () { rangy.init(); rangy.config.preferTextRange = true; highlighter = rangy.createHighlighter(window.document, 'TextRange'); highlighter.addClassApplier(rangy.createClassApplier("highlight", { ignoreWhiteSpace: true, tagNames: ["span"], elementProperties: { onclick: function (event) { return click(this, event) } } })); util.forEach(reviewClassNames, function (cls) { highlighter.addClassApplier(rangy.createClassApplier(cls, { ignoreWhiteSpace: true, tagNames: ["span"], elementProperties: { onclick: function (event) { return click(this, event) } } })); }) highlighter.addClassApplier(rangy.createClassApplier("reference", { ignoreWhiteSpace: true, tagNames: ["span"] })); highlighter.addClassApplier(rangy.createClassApplier("tts", { ignoreWhiteSpace: true, tagNames: ["span"] })); rangy.util.addListener(document, 'selectionchange', debounce(function () { var rectInfo = getRectInfoInSelection() var result = { cmd: 'selectionChange', hasSelection: rectInfo != null && rectInfo.end > rectInfo.start, hasSelectionRange: rectInfo != null } if (rectInfo != null) { result = rangy.util.extend(result, rectInfo) } wereadBridge.execMPReaderMethod('MPReader', result); })) // 通知音频的播放、暂停 var switches = document.getElementsByClassName('icon_share_audio_switch'); for (var i = 0; i < switches.length; i++) { switches[i].addEventListener('touchstart', function () { wereadBridge.execMPReaderMethod('MPReader', { cmd: 'audioWillToggle' }); }, true); } addClickEventToImage() transferH5Link() mediaPlatform.getAuthor(); }); /** * @startP 开始查询的段落, 如果未负数,则是从倒数第 startP 段往前检索 * @minDetectLength 最小检测长度,如果段落长度小于它,则跳过,查看下一段是否匹配, * 如果没有匹配 minDetectLength 的段落,则返回最长的一段 * @collectLength 收集长度,如果大于minDetectLength 而小于collectLength,则返回整个段落 * 否则截取到 collectLength 之后句子结束的位置 * * @return string */ function getShareAbstractText(startP, minDetectLength, collectLength) { var jsContent = document.getElementById("js_content"); if (jsContent == null) { return ""; } var pList = jsContent.getElementsByTagName("p"); if (pList == null) { return ""; } var length = pList.length; minDetectLength = minDetectLength || 50 collectLength = collectLength || 100 var dir = 1; if (startP < 0) { startP = Math.max(0, length + startP); dir = -1; } else if (startP >= length) { startP = length - 1; dir = -1; } var i = startP, text, failedFindText = "", failedFindTextLength = 0; while (i >= 0 && i < length) { text = pList[i].innerText; text = text && text.replace(/(\s|\t){2,}/g, ' ').trim(); if (text && text.length > minDetectLength) { if (text.length < collectLength) { return text; } var endSequenceIndex = text.indexOf("。", collectLength - 1); if (endSequenceIndex < collectLength) { return text; } return text.substring(0, endSequenceIndex + 1) } else { if (text && text.length > failedFindTextLength) { failedFindText = text; failedFindTextLength = text.length } i += dir; } } return failedFindText; } /** * 从最开始获取接近 collectLength 的字数。末尾加三个点 */ function getBeginAbstractText(collectLength) { var jsContent = document.getElementById("js_content"); if (jsContent == null) { return ""; } var children = jsContent.children if (children == null || children.length == 0) { return ""; } var length = children.length; collectLength = collectLength || 100 var i = 0, text, ret = ""; while (collectLength > 0 && i < length) { text = children[i].innerText; text = text && text.replace(/(\s|\t){2,}/g, ' ').trim(); if(text.length > 0){ if(text.length < collectLength){ if(ret){ ret += '\n' } ret += text collectLength -= text.length }else{ if(ret){ ret += '\n' } ret += text.substring(0, collectLength) + "..." return ret } } i++ } return ret; } function addClickEventToImage() { // if (rangy.Recognizer.isAndroid) { util.addListener(body, 'click', function (event) { var target = event.target if (target.tagName.toLowerCase() === 'img') { var images = getAllImagesAndTargetIndex(target) if (images.targetIndex >= 0) { wereadBridge.execMPReaderMethod('MPReader', { cmd: 'didTapImage', images: images.allImages, index: images.targetIndex }); } } }) // } else { // // click delegate 在 ios 11.4.1 会造成滚动延迟和菜单弹出延迟 // // 且 click delegate 无法在 ios 的 document 或者 body 上生效 // // 如果直接绑定点击事件,则无法选择图片 // var imgs = document.getElementsByTagName('img') // for (var i = 0; i < imgs.length; i++) { // if (!isHidden(imgs[i])) { // // 如果mp 支持了tap事件,就不需要我们自定义的了。否则需要自定义。 // rangy.attachTouch(document, 'tap') // util.addListener(imgs[i], 'tap', function (e) { // var images = getAllImagesAndTargetIndex(e.target) // if (images.targetIndex >= 0) { // wereadBridge.execMPReaderMethod('MPReader', { // cmd: 'didTapImage', // images: images.allImages, // index: images.targetIndex // }); // } // return false // }) // } // } // } } function click(el, event) { var classNameStr = rangy.ClassApplier.util.getClass(el) var classNames = ['highlight'] for (var i = 0; i < reviewClassNames.length; i++) { if (classNameStr.indexOf(reviewClassNames[i]) >= 0) { // 重叠部分,以 review 为主 classNames = reviewClassNames break } } event.preventDefault(); event.stopPropagation(); var range = rangy.createRange(doc); range.selectNodeContents(el); var characterRange = highlighter.converter.rangeToCharacterRange(range, body) var mergedCharacterRange = highlighter.getMergedCharacterRange(characterRange, reviewClassNames, classNames === reviewClassNames) var isClickReview = false if (mergedCharacterRange != null) { isClickReview = true } else { mergedCharacterRange = highlighter.getMergedCharacterRange(characterRange, ['highlight'], true) } if (mergedCharacterRange == null) { return false } var rect = getCharacterRangeRectInfo(mergedCharacterRange) if (rect == null) { return false } var result = rangy.util.extend({ cmd: !isClickReview ? 'didTapHighlight' : 'didTapReview', start: mergedCharacterRange.start, end: mergedCharacterRange.end, content: getContentByPos(mergedCharacterRange.start, mergedCharacterRange.end) }, rect); wereadBridge.execMPReaderMethod('MPReader', result); return true } function getRectInfoByPos(start, end) { return getInfoByPos(start, end, getCharacterRangeRectInfo) } function getTopByPos(start, end) { return getInfoByPos(start, end, getCharacterRangeTop) } function getInfoByPos(start, end, func) { if (start === end) { return null } if (start > end) { var tmp = start start = end end = tmp } var chapterType = highlighter.createCharacterRange(start, end) return func.call(null, chapterType) } function getCharacterRangeTop(chapterRange) { if (!chapterRange || chapterRange.end <= chapterRange.start) { return -1 } return highlighter.converter.characterRangeToRange(document, chapterRange, body).nativeRange.getBoundingClientRect().top } function getCharacterRangeRectInfo(chapterRange) { if (!chapterRange || chapterRange.end <= chapterRange.start) { return null } var testRange = rangy.createRange(doc); testRange.selectCharacters(body, chapterRange.start, chapterRange.start + 1); var firstRect = testRange.nativeRange.getBoundingClientRect() var lastRect = firstRect if (chapterRange.end > chapterRange.start + 1) { testRange.selectCharacters(body, chapterRange.end - 1, chapterRange.end) lastRect = testRange.nativeRange.getBoundingClientRect() } var rangeRect = highlighter.converter.characterRangeToRange(document, chapterRange, body).nativeRange.getBoundingClientRect() return { startX: firstRect.left, startY: firstRect.top, endX: lastRect.right, endY: lastRect.bottom, minLeft: rangeRect.left, maxRight: rangeRect.right, singleLine: firstRect.top === lastRect.top } } function getSelectionRectInfo() { var selection = rangy.getSelection() var nativeSelection = window.getSelection() var ranges = selection.getAllRanges() if (nativeSelection.rangeCount && !nativeSelection.isCollapsed) { var doc = document var body = rangy.util.getBody(doc) var result = { startX: 0, startY: 0, endX: 0, endY: 0, minLeft: Number.MAX_VALUE, maxRight: 0, singleLine: true } var firstTextPos = Number.MAX_VALUE, lastTextPos = 0 rangy.util.forEach(ranges, function (range) { var chapterRange = highlighter.converter.rangeToCharacterRange(range, body) firstTextPos = Math.min(chapterRange.start, firstTextPos) lastTextPos = Math.max(lastTextPos, chapterRange.end) var rect = range.nativeRange.getBoundingClientRect() result.minLeft = Math.min(result.minLeft, rect.left) result.maxRight = Math.max(result.maxRight, rect.right) result.startY = result.startY === 0 ? rect.top : Math.min(result.startY, rect.top) result.endY = result.endY === 0 ? rect.bottom : Math.max(result.endY, rect.bottom) }) if (lastTextPos < firstTextPos) { return null } else if (lastTextPos === firstTextPos) { var text = getSelectedText(false, true) if (text.length > 0) { // image result.startX = result.minLeft result.endX = result.maxRight return result } return null } var testRange = rangy.createRange(doc); testRange.selectCharacters(body, firstTextPos, firstTextPos + 1); var firstRect = testRange.nativeRange.getBoundingClientRect() var lastRect = firstRect if (lastTextPos > firstTextPos + 1) { testRange.selectCharacters(body, lastTextPos - 1, lastTextPos) lastRect = testRange.nativeRange.getBoundingClientRect() } result.startX = firstRect.left result.startY = firstRect.top result.endX = lastRect.right result.endY = lastRect.bottom result.singleLine = firstRect.top === lastRect.top return result } return null } // 初始化划线及高亮方法 function initAnnotations(items) { rangy.load(function () { var scrollTop = -1 removeAllAnnotations(); var serializedHighlights = "type:TextRange"; items = JSON.parse(items); for (var i = 0; i < items.length; i++) { var className if (items[i].type === 'highlight') { if (items[i].isReference) { className = 'reference' items.push({ type: 'highlight', isReference: false, isMySelf: items[i].isMySelf, start: items[i].start, end: items[i].end }) } else { className = 'highlight' } if (items[i].needScrollTo) { scrollTop = getTopByPos(items[i].start, items[i].end) } } else if (items[i].type === 'review') { if (items[i].isReference) { className = 'reference' scrollTop = getTopByPos(items[i].start, items[i].end) items.push({ type: 'review', isReference: false, isMySelf: items[i].isMySelf, start: items[i].start, end: items[i].end }) } else if (items[i].isMySelf) { className = 'review' } else { className = 'friend-review' } if (items[i].needScrollTo) { scrollTop = getTopByPos(items[i].start, items[i].end) } } else { if (items[i].isReference) { className = 'reference' } if (items[i].needScrollTo) { scrollTop = getTopByPos(items[i].start, items[i].end) } } if (className) { serializedHighlights = serializedHighlights + '|' + items[i].start + '$' + items[i].end + '$' + (i + 1) + '$' + className; } } if (scrollTop > 0) { wereadBridge.execMPReaderMethod('MPReader', { cmd: 'scrollToOffsetTop', top: scrollTop }); setTimeout(function () { highlighter.deserialize(serializedHighlights); }, 100) } else { highlighter.deserialize(serializedHighlights); } }) } // 新增划线 function didTapCreateHighlightButton() { var newHighlights = highlighter.highlightSelection("highlight", { exclusive: false }); if (newHighlights.length === 0) { return null } var highlight = newHighlights[newHighlights.length - 1] var result = getStartEndContentFromHighlight(highlight) // 去除聚焦 removeSelection(); return result; } // 对指定位置新增划线 function addNewUnderLineByPosition(start, end) { var result = null; if (start >= 0 && end >= 0) { var range = rangy.createRange(doc) range.selectCharacters(body, start, end) highlighter.highlightRanges("highlight", [range], { exclusive: false }) var characterRange = highlighter.converter.rangeToCharacterRange(range, body) result = { start: characterRange.start, end: characterRange.end, content: range.text() } } return result; } // 新增想法 function didTapCreateReviewButton(start, end) { var selection = rangy.getSelection() var hasSelection = selection.toString().length > 0 var result = null if (hasSelection) { highlighter.highlightSelection("review", { exclusive: false }) result = getStartEndContentFromRanges(selection.getAllRanges()) } else if (start >= 0 && end >= 0) { var range = rangy.createRange(doc) range.selectCharacters(body, start, end) highlighter.highlightRanges("review", [range], { exclusive: false }) var characterRange = highlighter.converter.rangeToCharacterRange(range, body) result = { start: characterRange.start, end: characterRange.end, content: range.text() } } else { return null } // 去除聚焦 removeSelection(); return result; } function getStartEndContentFromHighlight(highlight, includeImages) { var characterRange = highlight.characterRange return { start: characterRange.start, end: characterRange.end, content: highlighter.converter.characterRangeToRange(doc, characterRange, body).text({ includeImages: includeImages }) }; } function getStartEndContentFromRanges(ranges, includeImages) { var start = -1, end = -1, text = "", characterRange util.forEach(ranges, function (range) { characterRange = highlighter.converter.rangeToCharacterRange(range, body) if (start < 0) { start = characterRange.start } else { start = Math.min(start, characterRange.start) } end = Math.max(start, end, characterRange.end) text += range.text({ includeImages: includeImages }) }) return { start: start, end: end, content: text }; } // 需要判断是否是删除划线 function getRectInfoInSelection() { var rectInfo = getSelectionRectInfo(), characterRange if (rectInfo == null) { return null } var classNames = ['highlight'] var highlight = highlighter.highghtWhichContainSelection(classNames) var start = -1, end = -1, isRemove = false if (highlight != null) { isRemove = true characterRange = highlighter.getMergedCharacterRange(highlight.characterRange, classNames, true) start = characterRange.start end = characterRange.end } else { var selection = rangy.getSelection() var ranges = selection.getAllRanges() for (var i = 0; i < ranges.length; i++) { characterRange = highlighter.converter.rangeToCharacterRange(ranges[i], body) if (start < 0) { start = characterRange.start } else { start = Math.min(characterRange.start, start) } end = Math.max(start, end, characterRange.end) } } return rangy.util.extend({ start: start, end: end, isRemove: isRemove, content: getSelectedText(false) }, rectInfo); } // 删除所有想法 / 划线 function removeAllAnnotations() { highlighter.removeAllHighlights(); } // 删除选中的划线 function removeAnnotationFromSelectedText() { highlighter.unhighlightSelection(); } // 删除划线 / 想法 function didTapDeleteAnnotationButton(start, end, type) { var allHighlights = highlighter.highlights, highlight var removeHighlights = [] for (var i = 0; i < allHighlights.length; i++) { highlight = allHighlights[i] if (type === 'highlight') { if (highlight.classApplier.className !== 'highlight') { continue } } else if (type === 'review') { if (highlight.classApplier.className !== 'review') { continue } if (reviewClassNames.indexOf(highlight.classApplier.className) < 0) { continue } } if (highlight.characterRange.start >= start && highlight.characterRange.end <= end) { removeHighlights.push(highlight) } } if (removeHighlights.length > 0) { highlighter.removeHighlights(removeHighlights) return true } return false } // 重新加载页面 function reloadPage(button) { button.form.elements["serializedHighlights"].value = highlighter.serialize(); button.form.submit(); } function removeSelection() { rangy.getSelection().removeAllRanges() } function getAllForwardLinks() { var arr = [], l = document.links; for (var i = 0; i < l.length; i++) { /* 某些公众号的链接通过 data-url 复制,href 是 javascript:; 的。而 iOS 的拦截是通过捕捉所有 link 跳转,所以需要下面这一句 */ if (l[i].getAttribute('data-url')) { l[i].href = l[i].getAttribute("data-url"); } var href = l[i].href; if (href && (href.startsWith('http://') || href.startsWith('https://'))) { var ampRegEx = /&/g; href = href.replace(ampRegEx, '&'); arr.push(href); } } return arr; } function getAllImagesAndTargetIndex(target) { var images = document.getElementsByTagName('img') var result = [], targetIndex = -1 for (var i = 0; i < images.length; i++) { var parent = images[i].parentNode while (parent.nodeName != "A" && parent.nodeName != "BODY" && parent.nodeName != "HTML") { parent = parent.parentNode; } // 小程序、跳转链接的图片不受响应 var invalid = parent.nodeName == "A" && (parent.className.indexOf('h5_image_link') > -1 || parent.className.indexOf('weapp_image_link') > -1 || parent.getAttribute('data-weread-bookid') != null || parent.getAttribute('data-weread-reviewid') != null); var info = getImageInfo(images[i]) if (info != null && !invalid) { if (target === images[i]) { targetIndex = result.length } result.push(info) } } return { allImages: result, targetIndex: targetIndex } } function getAllImages() { var images = document.getElementsByTagName('img') var result = [] for (var i = 0; i < images.length; i++) { var info = getImageInfo(images[i]) if (info) { result.push(info) } } return result } function getImageInfo(image) { if (!isHidden(image)) { var src = image.getAttribute("data-src") if (!src || !src.startsWith("http")) { src = image.getAttribute("src") if (!src || !src.startsWith("http")) { src = image.src } } if (src) { var rect = image.getBoundingClientRect() return { src: src, x: rect.left, y: rect.top, width: rect.width, height: rect.height } } } return null } function getContentByPos(start, end, shouldRemoveSelection, includeImages) { if (start >= 0 && end >= 0) { var range = rangy.createRange(doc) range.selectCharacters(body, start, end) return range.text({ includeImages: includeImages }) } else { return getSelectedText(shouldRemoveSelection, includeImages) } } function getSelectedText(shouldRemoveSelection, includeImages) { var selection = rangy.getSelection() var ranges = selection.getAllRanges() var text = "" if (ranges.length > 0) { util.forEach(ranges, function (range) { text += range.text({ includeImages: includeImages }) }) } if (shouldRemoveSelection) { selection.removeAllRanges() } return text } // sample:https://weread.qq.com/web/appreader/6bb324405e15d46bb6d43d0?wtype=mpArticle // 将以上的 link 在客户端转化为 data-weread-bookId 和 data-weread-chapterUid function transferH5Link() { var detectReader = 'weread.qq.com/web/appreader' var detectLecture = 'weread.qq.com/wrpage/book/lecture' var links = document.getElementsByTagName('a') for (var i = 0; i < links.length; i++) { var link = links[i] if (link.href.indexOf(detectReader) > -1 || link.href.indexOf(detectLecture) > -1) { var encryptString = link.href.split('/').slice(-1)[0] if (encryptString) { // remove params encryptString = encryptString.split('?')[0] // rule:web_encrypt(bookId) + 'k' + web_encrypt(chapterUid) var encryptAry = encryptString.split('k') var bookId, chapterUid, reviewId; if (encryptAry[0]) { bookId = WRLink_decrypt(encryptAry[0]) link.setAttribute('data-weread-bookid', bookId) } if (encryptAry[1]) { chapterUid = WRLink_decrypt(encryptAry[1]) link.setAttribute('data-weread-chapteruid', chapterUid) } if (link.href.indexOf(detectLecture) > -1) { reviewId = encryptString; link.setAttribute('data-weread-reviewid', reviewId) } if (bookId.length || reviewId.length) { // 删掉 href 以致于让 App 不显示微信自带的”即将打开新页面“ link.removeAttribute('href'); link.addEventListener('touchend', function (e) { var bookId = this.getAttribute('data-weread-bookid') ? this.getAttribute('data-weread-bookid') : ''; var chapterUid = this.getAttribute('data-weread-chapteruid') ? this.getAttribute('data-weread-chapteruid') : ''; var reviewId = this.getAttribute('data-weread-reviewid') ? this.getAttribute('data-weread-reviewid') : ''; if (bookId.length || reviewId.length) { wereadBridge.execMPReaderMethod('MPReader', { cmd: 'didTapBook', bookId: bookId, chapterUid: chapterUid, reviewId: reviewId, }); } }, true); } } } } } // copy by https://git.code.oa.com/WeRead/wrencryption/blob/master/src/index.js // need check update function WRLink_decrypt (str) { var md5Length = 3; if (typeof str !== 'string' || str.length <= md5Length) return ''; var type = str.charAt(3); var randomLen = parseInt(str.charAt(4)); var firstContentPartIdx = 5 + randomLen; var contentMaxIdx = str.length - 3; // 后面3位是 md5 var s = ''; var idx = firstContentPartIdx; while (idx < contentMaxIdx && (idx === firstContentPartIdx || str.charAt(idx) === 'g')) { if (idx !== firstContentPartIdx) { idx++; // 跳过 'g' } if (idx + 2 > contentMaxIdx) { return ''; } var contentLength = parseInt(str.substr(idx, 2), 16); if (idx + 2 + contentLength > contentMaxIdx) { return ''; } var substring = str.substr(idx + 2, contentLength); var decodeResult = ''; if (type === '3') { var number = parseInt(substring, 16); if (isNaN(number)) { return ''; } decodeResult = '' + number; } else { for (var i = 0, iLen = substring.length; i < iLen; i += 2) { var charCode = parseInt(substring.substr(i, 2), 16); decodeResult += String.fromCharCode(charCode); } } s += decodeResult; idx = idx + 2 + contentLength; } return s; } // ================================ TTS =================================================== var lastTTSInfo = { lastTTSRange: null, lastTTSHighlightInfo: null, lastIndex: -1, lastText: "", lastTop: -1, lastBottom: -1 } var passTags = ['body', 'html', 'code', 'pre'] var filterTags = ['style', 'script', 'mpvoice', 'iframe', 'br'] var filterClass = ['js_audio_frame'] var mergeTags = ['p', 'li', 'a', 'h1', 'h2', 'h3', 'h4', 'h5'] function TTSRange() { this.range = new rangy.DomRange(document) this.matchedString = "" this.start = false this.lastInfo = null } function TTSText() { this.result = [] this.stack = [] this.restoreIndex = -1 this.needHandleStart = true this.tmpRange = null this.getForCurrentIndex = -1 this.getForCurrentIndexHandled = false } /** * 获取 TTS 文本内容 * @param startY * @returns {string} */ function getTTSText(startY) { var start = new Date - 0 var util = rangy.util var body = util.getBody(document) var ttsText = new TTSText() handleTTSTextGet(body, startY, ttsText) var handleResult = [] var startPos = 0 for (var i = 0; i < ttsText.result.length; i++) { if (ttsText.result[i]) { handleResult.push({index: i, text: ttsText.result[i], startPos: startPos}) startPos += ttsText.result[i].length } } console.log("getTTSText speed: " + (new Date - start)) return handleResult } /** * 获取 currentY 位置文本的 index 以及 pos * @param startY * @param currentY * @returns {*} */ function getCurrentIndex(startY, currentY) { if(currentY <= startY){ return { index: 0, pos: 0 } } var util = rangy.util var body = util.getBody(document) var ttsText = new TTSText() ttsText.getForCurrentIndex = currentY handleTTSTextGet(body, startY, ttsText) var result = ttsText.result if (result.length > 0) { return { index: result.length - 1, pos: result[result.length - 1].length } } else { return { index: 0, pos: 0 } } } /** * 恢复 TTS 播放的上下文记录 * @param startY * @param restoreIndex */ function restoreTTSContext(startY, restoreIndex) { var ttsText = new TTSText() ttsText.restoreIndex = restoreIndex handleTTSTextGet(body, startY, ttsText) } /** * el 过滤 * @param el * @returns {boolean} */ function filter(el) { var tagName = el.nodeName.toLowerCase() if (passTags.indexOf(tagName) >= 0) { return false } if (filterTags.indexOf(tagName) >= 0) { return true } if (el.getAttribute('wr-ignore-no-content')) { return true } if (isNodeHidden(el)) { return true } var parent = el.parentNode if (parent == null) { return true } if (tagName === 'p' && (parseInt(getStyle(el, 'text-indent'), 10) || 0) < -3000) { el.setAttribute('wr-ignore-no-content', true) return true } var offsetParent = el.offsetParent var offsetWidth = el.offsetWidth var offsetLeft = el.offsetLeft var parentOffsetWidth = parent.offsetWidth if (offsetParent == null) { return true } if (parentOffsetWidth <= 0 || offsetWidth == 0) { // offsetWidth 为 0,可能是离线 Webview, 如果检测? // 如果 display 为 inline, 然后包裹其它元素,offsetWidth 可能会出错 return false } if ((offsetWidth != 0 && offsetLeft + offsetWidth < 0) || (offsetParent.offsetWidth != 0 && offsetLeft > offsetParent.offsetWidth)) { el.setAttribute('wr-ignore-no-content', true) return true } var classUtil = rangy.ClassApplier.util for (var i = 0; i < filterClass.length; i++) { if (classUtil.hasClass(el, filterClass[i])) { return true } } return false } function handleTTSTextGet(el, startY, ttsText) { if (el.nodeType === 1) { var tagName = el.nodeName.toLowerCase() if (ttsText.needHandleStart) { var rect = el.getBoundingClientRect() if (rect.bottom <= startY && tagName !== 'body') { // ignore return } if (rect.top >= startY) { ttsText.needHandleStart = false } } if (filter(el)) { return } var child, lastIndex; if (mergeTags.indexOf(tagName) >= 0) { var isInMergeTag = false if (!ttsText.stack.length) { ttsText.stack.push(tagName) ttsText.result.push('') }else{ isInMergeTag = true } child = el.firstChild while (child) { handleTTSTextGet(child, startY, ttsText) if (ttsText.getForCurrentIndexHandled) { break } child = child.nextSibling } if(!isInMergeTag){ ttsText.stack.pop() lastIndex = ttsText.result.length - 1 ttsText.result[lastIndex] = ttsText.result[lastIndex].trim() if (ttsText.getForCurrentIndexHandled) { return } if (ttsText.result[lastIndex] === '') { ttsText.result.pop() el.setAttribute('wr-ignore-no-content', true) } else { if (ttsText.result.length === ttsText.restoreIndex) { lastTTSInfo.lastTTSRange = rangy.createRange(doc) lastTTSInfo.lastTTSRange.selectNodeContents(el) lastTTSInfo.lastTTSRange = highlighter.converter.rangeToCharacterRange(lastTTSInfo.lastTTSRange, body) } } } } else { child = el.firstChild while (child) { handleTTSTextGet(child, startY, ttsText) if (ttsText.getForCurrentIndexHandled) { return } child = child.nextSibling } } } else if (el.nodeType === 3) { if (!emptyTextNode(el)) { if (!ttsText.needHandleStart) { if(ttsText.getForCurrentIndex > startY && !ttsText.getForCurrentIndexHandled){ var range = ttsText.tmpRange if (!range) { range = ttsText.tmpRange = document.createRange() } range.selectNodeContents(el) rect = range.getBoundingClientRect() handleGetForCurrentIndex(ttsText, rect, el, range) if(ttsText.getForCurrentIndexHandled){ return } } collectText(el, ttsText, 0) } else { var range = ttsText.tmpRange if (!range) { range = ttsText.tmpRange = document.createRange() } range.selectNodeContents(el) rect = range.getBoundingClientRect() if (rect.bottom <= startY) { // ignore return } if (rect.top >= startY) { ttsText.needHandleStart = false collectText(el, ttsText, 0) handleGetForCurrentIndex(ttsText, rect, el, range) if(ttsText.getForCurrentIndexHandled){ return } } else { handleGetForCurrentIndex(ttsText, rect, el, range) if(ttsText.getForCurrentIndexHandled){ return } var start = 0, end = el.data.length - 1 var sliceIndex = sliceTextNode(el, range, startY, start, end, rect.top, rect.bottom) if (sliceIndex + 1 <= end) { ttsText.needHandleStart = false collectText(el, ttsText, sliceIndex + 1) } } } } } } function handleGetForCurrentIndex(ttsText, rect, el, range) { if (ttsText.getForCurrentIndex > 0) { if (rect.top > ttsText.getForCurrentIndex) { if (!ttsText.stack.length) { ttsText.result.push("") } ttsText.getForCurrentIndexHandled = true } else if (rect.bottom > ttsText.getForCurrentIndex) { var start = 0, end = el.data.length - 1 var slice = sliceTextNode(el, range, ttsText.getForCurrentIndex, start, end, rect.top, rect.bottom) var data = slice > 0 ? el.data.substring(0, slice) : el.data if (!ttsText.stack.length) { ttsText.result.push(data) } else { var lastI = ttsText.result.length - 1 ttsText.result[lastI] = ttsText.result[lastI] + data } ttsText.getForCurrentIndexHandled = true } } } function sliceTextNode(el, range, startY, start, end) { if (end - start <= 1) { return end } var half = Math.floor((end + start) / 2), rect range.setStart(el, half) range.setEnd(el, half + 1) rect = range.getBoundingClientRect() if (rect.top >= startY) { return sliceTextNode(el, range, startY, 0, half - 1) } else { return sliceTextNode(el, range, startY, half, end) } } function collectText(el, ttsText, sliceIndex) { var data = sliceIndex > 0 ? el.data.substring(sliceIndex) : el.data if (ttsText.stack.length) { var lastIndex = ttsText.result.length - 1 ttsText.result[lastIndex] = ttsText.result[lastIndex] + data } else { ttsText.result.push(data.trim()) if (ttsText.result.length === ttsText.restoreIndex) { lastTTSInfo.lastTTSRange = rangy.createRange(doc) lastTTSInfo.lastTTSRange.selectNodeContents(el) lastTTSInfo.lastTTSRange = highlighter.converter.rangeToCharacterRange(lastTTSInfo.lastTTSRange, body) } } } function highlightTTSText(text, index, startY) { var start = new Date - 0 var range = new TTSRange() if (lastTTSInfo.lastTTSHighlightInfo != null) { // 如果与上次信息相同,直接返回 if (lastTTSInfo.lastIndex === index && lastTTSInfo.lastText === text) { return { top: lastTTSInfo.lastTop, bottom: lastTTSInfo.lastBottom } } highlighter.removeHighlights(lastTTSInfo.lastTTSHighlightInfo) lastTTSInfo.lastTTSHighlightInfo = null } lastTTSInfo.lastIndex = index lastTTSInfo.lastText = text if (lastTTSInfo.lastTTSRange == null && index > 0) { restoreTTSContext(startY, index) } if (lastTTSInfo.lastTTSRange != null) { range.lastInfo = highlighter.converter.characterRangeToRange(doc, lastTTSInfo.lastTTSRange, body) } var remain = handleTTSHighlight(text, body, range) if (remain && remain.length) { // there no matched text in dom return { reason: "there no matched text in dom", top: -1, bottom: -1 } } else { // 必须在 highlightRanges 前转换成 ChapterRange, // highlightRanges 可能会造成 DOM 更改,使得之前所保存到 dom 节点失效 lastTTSInfo.lastTTSRange = highlighter.converter.rangeToCharacterRange(range.range, body) lastTTSInfo.lastTTSHighlightInfo = highlighter.highlightRanges("tts", [range.range], { exclusive: false }); if (!lastTTSInfo.lastTTSHighlightInfo || !lastTTSInfo.lastTTSHighlightInfo.length) { return { reason: "no lastTTSHighlightInfo", top: -1, bottom: -1 } } var highlightRange = highlighter.converter.characterRangeToRange(doc, lastTTSInfo.lastTTSHighlightInfo[0].characterRange, body) var rect = highlightRange.nativeRange.getBoundingClientRect() console.log("highlightTTSText speed: " + (new Date - start)) lastTTSInfo.lastTop = rect.top lastTTSInfo.lastBottom = rect.bottom return { top: rect.top, bottom: rect.bottom } } } function handleTTSHighlight(text, el, range) { if (text == null || text.length === 0) { return null } var offset = 0 // 快速方案: 从上次匹配位置开始向后匹配高亮区域 // if (range.lastInfo != null) { // var isOrIsAncestorOf = rangy.dom.isOrIsAncestorOf // if (!isOrIsAncestorOf(el, range.lastInfo.endContainer)) { // return text // } else if (el === range.lastInfo.endContainer) { // offset = range.lastInfo.endOffset // range.lastInfo = null // } // } var remain, index if (el.nodeType === 1) { if (filter(el)) { return text } var child if (offset > 0) { if (el.children && el.children.length > offset + 1) { child = el.children[offset + 1] } else { return text } } else { child = el.firstChild } while (child) { text = handleTTSHighlight(text, child, range) if (text == null || text.length === 0) { return null } child = child.nextSibling } return text } else if (el.nodeType === 3) { var elData = el.data if (range.start) { if (emptyTextNode(el)) { return text } else if (text.indexOf(elData) === 0) { range.matchedString += elData remain = text.substring(elData.length) if (remain.length === 0) { range.range.setEnd(el, text.length) } return remain } else if (elData.indexOf(text) === 0) { range.matchedString += text range.range.setEnd(el, text.length) return null } else { // 匹配错误, 重新匹配 console.log("matchedString = " + range.matchedString + "; remain = " + text) range.start = false text = range.matchedString + text range.matchedString = "" // TODO 从错误节点重新处理? return handleTTSHighlight(text, el, range) } } else { if (emptyTextNode(el)) { return text } else if (text.indexOf(elData) === offset) { range.matchedString += elData range.start = true range.range.setStart(el, offset) remain = text.substring(elData.length) if (remain.length === 0) { range.range.setEnd(el, text.length) } return remain } else if ((index = elData.indexOf(text, offset)) >= 0) { range.start = true range.matchedString += text range.range.setStart(el, index) range.range.setEnd(el, index + text.length) return null } var matchStart, matchIndex matchStart = matchIndex = elData.indexOf(text[0], offset) if (matchIndex === -1) { return text } else { var textIndex = 0 while (true) { matchIndex++ textIndex++ if (elData.length <= matchIndex || text.length <= textIndex) { range.start = true range.matchedString += elData.substring(matchStart, matchIndex) range.range.setStart(el, matchStart) text = text.substring(textIndex) if (text.length === 0) { range.range.setEnd(el, matchIndex + text.length) } return text } else if (elData[matchIndex] !== text[textIndex]) { matchStart = elData.indexOf(text[0], matchStart + 1) if (matchStart === -1) { return text } else { matchIndex = matchStart textIndex = 0 } } } } } } return text; } function emptyTextNode(el) { return !el.data || /^(\s|\t)+$/g.test(el.data) } function isNodeHidden(node) { if (node.sourceIndex === 0) { return true } // 故事流之后不显示标题,但是为了与之前版本划线兼容,并不能隐藏标题 return getStyle(node, 'display') === 'none' || rangy.ClassApplier.util.hasClass(node, 'rich_media_title') } function isHidden(node) { var ancestors = [node]; while (node.parentNode) { ancestors.unshift(node.parentNode); node = node.parentNode; } for (var i = 0, len = ancestors.length; i < len; ++i) { if (ancestors[i].nodeType === 1 && isNodeHidden(ancestors[i])) { return true; } } return false; } function getStyle(node, name) { if (window.getComputedStyle) { return window.getComputedStyle(node, null)[name] } else { return node.currentStyle[name] } } function finishTTSPlay() { if (lastTTSInfo.lastTTSHighlightInfo != null) { highlighter.removeHighlights(lastTTSInfo.lastTTSHighlightInfo) lastTTSInfo.lastTTSHighlightInfo = null } lastTTSInfo.lastTTSRange = null lastTTSInfo.lastIndex = -1 lastTTSInfo.lastText = "" } function debounce(callback, timeout) { var timerId = null timeout = timeout || 250 return function () { var args = arguments var self = this if (timerId) { clearTimeout(timerId); timerId = null; } timerId = setTimeout(function () { callback.apply(self, args) }, timeout) } } function changeFontSizeWithScale(scale) { // iPad iOS13 不支持 -weikit-text-size-adjust,改用自定义方法 // 注意:传入的 scale 是 0.80 这样的值 var root = document.getElementById('js_content'); var setQueue = []; var textWalker = document.createTreeWalker(root, 4); // 4 === NodeFilter.SHOW_TEXT while (textWalker.nextNode()) { var textContainer = textWalker.currentNode.parentNode; var oriFontSize = textContainer.getAttribute('data-weread-fontsize'); var oriLineHeight = textContainer.getAttribute('data-weread-lineheight'); if (!oriFontSize) { oriFontSize = getComputedStyle(textContainer).fontSize; textContainer.setAttribute('data-weread-fontsize', oriFontSize); } if (!oriLineHeight) { oriLineHeight = getComputedStyle(textContainer).lineHeight; textContainer.setAttribute('data-weread-lineheight', oriLineHeight); } // 先不要马上设置fontSize,会影响子节点的fontSize,存起来最后统一设置 setQueue.push([textContainer, oriFontSize, oriLineHeight]); } setQueue.forEach(function (setPair) { setPair[0].style.fontSize = parseFloat(setPair[1]) * scale + 'px'; setPair[0].style.lineHeight = parseFloat(setPair[2]) * scale + 'px'; }); } function findAllBookTitles() { var container = document.getElementById("js_content"); if (!container) return []; var walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT, null, false ); var node; var regex = /《[^《》]{1,30}》/g; var results = []; while ((node = walker.nextNode())) { var text = node.nodeValue; var match; while ((match = regex.exec(text)) !== null) { results.push({ text: match[0], }); } } return results; } function highlightSelectedBookTitles(indexArray) { // indexArray: [{nodeIndex, start, end}, ...] var container = document.getElementById("js_content"); if (!container) return; // 清除上一次的高亮 var prevSpans = document.querySelectorAll('span.blue-clickable'); prevSpans.forEach(function(span) { var parent = span.parentNode; while (span.firstChild) { parent.insertBefore(span.firstChild, span); } parent.removeChild(span); }); // 重新遍历一遍,找到所有文本节点 var walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT, null, false ); var nodes = []; var node; while ((node = walker.nextNode())) { nodes.push(node); } var highlighter = rangy.createHighlighter(); var applier = rangy.createClassApplier("blue-clickable", { tagNames: ["span"], elementProperties: { onclick: function (event) { return clickHighlightBookTitle(this, event) } }, normalize: false, }); highlighter.addClassApplier(applier); var regex = /《[^《》]{1,30}》/g; const textSet = new Set(indexArray); // 1. 先收集每个 node 里所有需要高亮的区间 nodes.forEach(function(node) { var text = node.nodeValue; var match; var ranges = []; while ((match = regex.exec(text)) !== null) { if (textSet.has(match[0])) { ranges.push({ start: match.index, end: match.index + match[0].length, text: match[0] }); } } // 2. 按 start 降序排序 ranges.sort((a, b) => b.start - a.start); // 3. 倒序高亮 ranges.forEach(function(item) { var range = rangy.createRange(); range.setStart(node, item.start); range.setEnd(node, item.end); highlighter.highlightRanges("blue-clickable", [range]); }); }); } function clickHighlightBookTitle(el, event) { console.log('clickHighlightBookTitle', el, event); event.preventDefault(); event.stopPropagation(); var result = { cmd: 'didTapHighlightBookTitle', content: el.textContent, } wereadBridge.execMPReaderMethod('MPReader', result); return true }