ReadViewSDK/Doc/WXRead/resources/js/weread-highlighter.js
2026-05-21 19:40:51 +08:00

1530 lines
50 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 = /&amp;/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]
// ruleweb_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
}