首页/新闻资讯/正文详情

解决calibre阅读器无法正确获取锚点元素的href属性问题

发布时间:2026/9/25 18:51:38 来源:云帆数科 栏目:资讯中心
解决calibre阅读器无法正确获取锚点元素的href属性问题
在创建支持交叉链接且鼠标悬停显示注释框的HTML文件 一文中提供了一个js脚本通过读取锚点元素的href目标的文本内容向伪元素传递注释内容但是那个脚本如果制作成EPUB并在Calibre阅读器中打开却会失效在Thorium阅读器中有效核心原因在于 Calibre 阅读器E-book Viewer的 JavaScript 运行环境比较特殊。它并非标准的浏览器环境而是 Calibre 用 Python 和 QtWebEngine 封装的一个沙盒。在 Calibre 的阅读器里书籍内容会被加载到一个受控的 iframe 中并且对a元素的href属性做了一个封装。打开Calibre 阅读器的开发者工具在阅读器上部鼠标右键点击可弹出选项最下面一行能够找到开发者工具通过持续展开折叠的元素可以找到a元素就会发现它的href属性被改写成了“javascript:void(0)”因此该文中通过读取href属性的值找到链接目标的代码会失败导致无法正确向伪元素传递正确的注释内容。原来的href值被装进了一个自定义属性“data-XXX”中其中XXX是一个很长的随机字符串该属性的值是一个JSON字符串其中frag键的值就是正确的href的值如下图所示搞清楚原因后可以对创建支持交叉链接且鼠标悬停显示注释框的HTML文件一文中的脚本进行改造使其兼容Calibre阅读器。改造方法是对锚点元素的属性进行遍历对名字较长的以“data-”开头的自定义属性读取其值如果其值可以成功转换成JSON并且包含键“frag”则返回“frag”键对应的值否则返回null具体方法如下function getCalibreHref(anchor) { //专门用于Calibre阅读器读取a标签的真实href // 遍历该元素的所有属性 for (let i 0; i anchor.attributes.length; i) { const attr anchor.attributes[i]; const name attr.name; const value attr.value; // 只找>function getCalibreHref(anchor) { //专门用于Calibre阅读器读取a标签的href // 遍历该元素的所有属性 for (let i 0; i anchor.attributes.length; i) { const attr anchor.attributes[i]; const name attr.name; const value attr.value; // 只找>####################20260922补充###########################其实兼容性更好、代码逻辑更清晰的方案是自己将a元素的href包装到一个自定义属性中查找注释元素的id时直接从自定义属性里找具体做法如下1、HTML注释引用定义p而三生石上颇悟前因a epub:typenoteref classnoteref href#part0004_note_22 >p classkindle-cn-kai epub:typefootnote idpart0004_note_22a href#part0004_noteBack_22[22]/a而三生石上颇悟前因唐袁郊《甘泽谣·圆观》载李源与圆观和尚 十分友好圆观依据佛家因果预知自己来生将做牧童便约请李源在他死后十二年到杭州天竺寺相见。李源依约 而往在寺前听一牧童唱道“三生石上旧精魂赏月吟风不要论。惭愧情人远相访此身虽异性常存。”李源便晓 得牧童就是圆观的托身。后人附会此事把杭州天竺寺后的山石指为“三生石”。诗文中往往也以“三生石”代指因缘 前定。三生即“三世”。佛教以过去、现在、未来即前生、今生、来生为“三生”或“三世”。前因前生因果。 因梵语意译这里指因缘。/p p classkindle-cn-kai epub:typefootnote idpart0004_note_23a href#part0004_noteBack_23[23]/a放纵之言有未可概以人废者意谓所言虽然恣意放任也有可取 之处不能一概因人废言。放纵放任不循常轨。概一概全部。/p用一个带id的块级元素供注释引用跳转内部再包含一个href指向对应注释引用的a元素实现与注释引用的交叉链接。2、CSS对注释引用及伪元素的定义.noteref { /* 使用伪元素方案时为伪元素提供定位基准 */ position: relative; display: inline-block; cursor: pointer; margin: 0; padding: 0; text-indent: 0; text-align: left; /* 模拟上标显示 */ font‑size: smaller; /*不能使用transform将注释引用显示为上标这会强制建立独立局部 层叠上下文z‑index被禁锢在这个上下文内部从而引起浏览器渲染 管线发生出现绘制碎片、下层内容穿透的情况*/ /*transform: translateY(-0.4em);*/ text-decoration: none; } /* 定义伪元素样式 */ .noteref::after { /* 读取data-note属性的值作为内容 */ content: attr(data-note); /* 相对于宿主元素定为重要勿作修改删除*/ position: absolute; top: var(--note-top); left: var(--note-left); background-color: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 1.1em; line-height: 1.4; white-space: pre-wrap; width: var(--note-width); z-index: 1000; /* 初始完全透明 */ opacity: 0; /* 初始隐藏不占空间 */ visibility: hidden; /* 添加淡入淡出效果 */ transition: opacity 0.2s ease, visibility 0s ease; /* 防止提示框干扰鼠标事件 */ pointer-events: none; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); overflow-wrap: break-word; word-break: break-word; } /* 用CSS类名show作为伪元素显示与隐藏的开关 */ .noteref.show::after { opacity: 1; visibility: visible; box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2); } /* 伪元素不显示时清初相关属性*/ .noteref:not(.show)::after { content: !important; opacity: 0 !important; visibility: hidden !important; box-shadow: none !important; }3、JavaScript处理显示位置计算及向CSS传值function displayNote() { /* 方案一使用伪元素显示注释 */ const noteRefs document.querySelectorAll(.noteref); // 获取内容容器元素无专门的容器时用document.body作为容器 const contentDiv document.getElementById(content); const container contentDiv ? contentDiv : document.body; Array.from(noteRefs).forEach(noteRef { noteRef.addEventListener(mouseenter, function() { // 关闭页面上其他已经打开的注释提示避免伪元素残留 document.querySelectorAll(.noteref.show).forEach(s s.classList.remove(show)); const noteId this.getAttribute(data-href); const noteEl document.getElementById(noteId); if (!noteEl) { console.warn(注释找不到id: ${noteId}); return; } let noteText noteEl.textContent.trim(); // note note.replace(/\[\d\]/, ).trim(); // 替换掉注释内容前面的序号可选 noteText noteText.replace(/(\[\d\])[\r\n\s]/, $1 ); // 替换掉序号与注释内容之间过多的空白可选 // 向CSS传递注释文本 this.setAttribute(data-note, noteText); // 创建临时隐藏元素用来测量提示框渲染尺寸性能影响有限 const measurer document.createElement(div); // 与盒模型尺寸相关的样式须与CSS文件中的定义一致 measurer.style.cssText position:fixed; visibility:hidden; pointer-events:none; box-sizing:border-box; padding:8px 12px; border-radius:4px; font-size:1.1em; line-height:1.4; white-space:pre-wrap; max-width:20em; ; measurer.textContent noteText; document.body.appendChild(measurer); const tipRect measurer.getBoundingClientRect(); const tipW tipRect.width; const tipH tipRect.height; // 在删除measurer前读取noteref和内容容器的视口矩形避免页面布局再次变脏增加一次重排 const noterefRect this.getBoundingClientRect(); const contentRect container.getBoundingClientRect(); measurer.remove(); const gap 5; let tipViewportTop, tipViewportLeft; // 垂直规则优先放在noteref上方 if (noterefRect.top tipH gap) { tipViewportTop noterefRect.top - tipH - gap; // 上方留出间隙 } else { tipViewportTop noterefRect.bottom gap; } // 水平规则基准leftnoterefRect.left右侧溢出左移最小leftgap tipViewportLeft noterefRect.left; const rightLimit Math.min(Math.floor(contentRect.left contentRect.width), window.innerWidth) - 30; if (tipViewportLeft tipW rightLimit) { tipViewportLeft rightLimit - tipW - gap; } if (tipViewportLeft gap) { tipViewportLeft gap; } // 伪元素是absolute相对于noteref。把【视口坐标】转为【相对于noteref的偏移量】 const relTop tipViewportTop - noterefRect.top; const relLeft tipViewportLeft - noterefRect.left; // 设置CSS变量给noteref供给::after使用 this.style.setProperty(--note-top, ${relTop}px); this.style.setProperty(--note-left, ${relLeft}px); this.style.setProperty(--note-width, ${tipW}px); // 添加class开启opacity/visibility显示伪元素 this.classList.add(show); }); noteRef.addEventListener(mouseleave, function() { this.classList.remove(show); }); }); }需要说明的是伪元素方案所用到的CSS属性content只支持字符串所以如果注释中带有图片或者文字格式相关信息会全部丢失。有必要的话可以用下面的注释框显示方案显示带格式的完整注释信息HTML结构相同有必要说明的是这一方案除了Calibre阅读器表现完美Thorium等阅读器都有问题方案一兼容性更好1、注释框样式/* 方案二使用提示框显示注释可以带格式显示注释 */ .note-tip { white-space: pre-wrap; max-width: 20em; overflow-wrap: break-word; background-color: #333; color: white; padding: 8px 12px; border-radius: 4px; font-size: 1.1em; line-height: 1.4; text-indent: 0; /* 添加淡入淡出效果 */ transition: opacity 0.2s ease, visibility 0.2s ease; padding:0.618em; line-height:1.8em; font-size: 16px; } /* 注释框为黑色底纹注释中的元素在注释框中也许需要调整颜色 */ .note-tip a { color: GreenYellow; text-decoration: none; } .note-tip rt { font-size: 0.8em; color: Chartreuse; }2、JavaScript处理注释框的显示内容及位置function displayNoteTip() { /* 方案二使用DOM提示框显示注释支持富文本 */ // 容器只查询一次不要放到mouseenter内重复查询 const contentDiv document.getElementById(content); const container contentDiv ? contentDiv : document.body; const noteRefs document.querySelectorAll(.noteref); Array.from(noteRefs).forEach(noteRef { noteRef.addEventListener(mouseenter, function () { const anchorId this.getAttribute(data-href); const noteEl document.getElementById(anchorId); if (!noteEl) { console.warn(注释找不到id: ${anchorId}); return; } if (!noteEl) return; // 清除旧弹窗 document.querySelectorAll(.note-tip).forEach(oldTip{ oldTip.remove(); }); // 获取注释引用在视口内矩形 const refRect this.getBoundingClientRect(); // 转为【文档绝对坐标】加上视口滚动偏移 const docTop refRect.top window.scrollY; const docLeft refRect.left window.scrollX; // 创建弹窗 const tip document.createElement(div); tip.className note-tip; tip.innerHTML noteEl.innerHTML; tip.style.opacity 0; tip.style.visibility hidden; /* MDN 明确文档如果祖先元素存在 transform、perspective、filter、will‑change: transform * 那么后代的 position:fixed 将不再以视口为包含块改为以这个祖先盒子作为包含块。由于this(a.noteref) * 使用了transform: translateY(-0.4em);显示为上标所以tip不能再挂载到this。 * 即使没有transform在EPUB阅读器里也可能引起堆叠问题。这里直接挂载到body。 */ document.body.appendChild(tip); // requestAnimationFrame等待布局完成再读取尺寸规避拿到0宽高的时序问题 requestAnimationFrame(() { if (!tip.isConnected) return; // 防止布局未完成拿到0宽高 const tipRect tip.getBoundingClientRect(); const gap 5; const tipW tipRect.width; const tipH tipRect.height; let topPos, leftPos; // 优先放引用标记上方文档坐标系 if (refRect.top tipH gap) { // 上方空间足够tip放在ref上方 topPos docTop - tipH - gap; } else { // 上方不够放下方 topPos docTop refRect.height gap; } leftPos docLeft; // 边界限制不超出内容容器 const contentRect container.getBoundingClientRect(); const contentDocRight contentRect.left contentRect.width window.scrollX; const rightLimit contentDocRight - 30; // 如果tip右侧超出视口右边界左移 if (leftPos tipW rightLimit) { leftPos rightLimit - tipW - gap; } if (leftPos gap) leftPos gap; // 强制最小值 left gap // absolute使用文档绝对坐标不是视口 tip.style.position absolute; tip.style.top ${topPos}px; tip.style.left ${leftPos}px; tip.style.opacity 1; tip.style.visibility visible; }); }); noteRef.addEventListener(mouseleave, function () { document.querySelectorAll(.note-tip).forEach(oldTip{ oldTip.remove(); }); }); }); }据AI说如果不依赖JavaScript脚本是在head中还是在文档末尾导入都确保函数在DOM就绪后才执行的比较健壮的调用方式如下function whenDOMReady(callback) { if (document.readyState loading) { document.addEventListener(DOMContentLoaded, callback, { once: true }); } else { // interactive 或 complete 都说明 DOM 已就绪 callback(); } } whenDOMReady(displayNote);

相关推荐

G Hub宏失效的解决方法
G Hub宏失效的解决方法

都是用了5年了的罗技老用户了,虽然lgHub挺好用的,但是还是偶尔会出现一些小问题。比如找不到 lg设备,设备自定义宏失效,自启动失效,lgHub卡在加载动画进不去的问题。这里只说鼠标宏无法触发,常见原因多源于… · 2026/9/25 18:51:25

企业多模型统一管理:服务目录、授权、路由与计量的六步方法
企业多模型统一管理:服务目录、授权、路由与计量的六步方法

摘要:本文解释多模型统一管理的对象与边界,区分统一 API 和完整治理体系,并给出从模型盘点、服务标准到授权、路由和运营计量的六步实施方法。多模型统一管理,是把公有模型、私有模型、自建模型和本地部署模型组织为标准服务对象&… · 2026/9/25 18:51:19

SolonCode v0.0.18 发布:终端智能助手接入 TaoToken 的 config.toml 配置骨架与验证
SolonCode v0.0.18 发布:终端智能助手接入 TaoToken 的 config.toml 配置骨架与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 18:51:07

Hello-Agents
Hello-Agents

目录 1日常助手提示词里面基本要有什么? 2还有文案优化助手 3数据查询助手 4def和class的区别? 5使用Autogen时,遇到的问题? 6异步程序的入口是什么? 7这注解staticmethod有什么用? 8解释下面代码 ? 9解释这… · 2026/9/25 19:14:56

【电气设计】输入功率测试  示波器参数
【电气设计】输入功率测试 示波器参数

近期项目中在使用示波器测试输入功率的时候有一些疑问,在本篇文章中进行总结和整理。输入功率:需要明确的几个概念:1. 相电压2. 线电压3. 有功功率(单位 W,kW)对于常规的星型(Y)接法… · 2026/9/25 19:14:50

时序数据库选型对比:TimescaleDB、InfluxDB 与 ClickHouse 的综合权衡
时序数据库选型对比:TimescaleDB、InfluxDB 与 ClickHouse 的综合权衡

时序数据库选型对比:TimescaleDB、InfluxDB 与 ClickHouse 的综合权衡在多租户智能体与企业级工作流平台的长期运维中,系统每天产生海量的“时序与日志度量数据(Time-Series & Metric Logs)”: 节点级微秒级执行耗… · 2026/9/25 19:14:44

ChatGPT Images 2.0五大硬核能力深度实测:TaoToken统一Key接入AI生图工作流
ChatGPT Images 2.0五大硬核能力深度实测:TaoToken统一Key接入AI生图工作流

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 19:14:44

小白程序员必看!收藏这篇,8天掌握AI Agent,高薪岗位等你来拿!
小白程序员必看!收藏这篇,8天掌握AI Agent,高薪岗位等你来拿!

本文介绍了AI Agent的概念和工作方式,与传统开发的区别在于AI Agent能自主拆解任务、自动查资料、对接工具,具有更高的商业价值和技术复合门槛。 前段时间,有朋友跟我吐槽: 金三银四面试,跑了3家公司,HR和技… · 2026/9/25 19:14:44

什么是AXI:深入lavish-axi的Agent人机工程与Token效率设计
什么是AXI:深入lavish-axi的Agent人机工程与Token效率设计

什么是AXI:深入lavish-axi的Agent人机工程与Token效率设计 【免费下载链接】lavish-axi HTML is the new markdown. Lavish is the new editor for your HTML artifacts. 项目地址: https://gitcode.com/gh_mirrors/la/lavish-axi lavish-axi 是一款面向 AI … · 2026/9/25 19:14:44

数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)
数值优化(Numerical Optimization)学习系列-03-共轭梯度方法(Conjugate Gradient)

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31

MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37

了解更多?预约专属演示

我们的顾问将为您一对一讲解产品与方案

企业微信二维码