npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@embedpdf-editor/chapter-snippet

v1.2.21

Published

框架无关的章节 PDF 阅读器运行时。它把 Preact 阅读器封装成 Web Component。

Readme

@embedpdf-editor/chapter-snippet

框架无关的章节 PDF 阅读器运行时。它把 Preact 阅读器封装成 Web Component。

Vue 2 请使用本包@embedpdf-editor/chapter-snippet),不要用已移除的 @embedpdf-editor/vue2-chapter-viewer。宿主为 Vue 2 / 传统页面 / 微前端,或任何不想引入 React/Vue3 渲染器的场景。

能力与 React/Vue3 渲染器保持一致:多章连续滚动、划线/高亮、附注、段落书签、章节目录和标注导入导出。

安装

pnpm add @embedpdf-editor/chapter-snippet

Vite

chapterSnippetViteResolve() 用于 Vite + Vue2/snippet 场景:

// vite.config.ts
import { defineConfig } from 'vite';
import { chapterSnippetViteResolve } from '@embedpdf-editor/chapter-snippet/vite';

export default defineConfig({
  ...chapterSnippetViteResolve(),
});

它会:

| 配置 | 作用 | | --- | --- | | server.headers | 开发环境添加 COOP/COEP,满足 worker/wasm 常见要求 | | optimizeDeps.exclude | 避免 Vite 二次预构建 snippet 和 PDFium 引擎 |

Vue CLI / Webpack

无需修改 vue.config.js 安装后直接:

import ChapterEmbedPDF from '@embedpdf-editor/chapter-snippet';

发包构建会对 dist/embedpdf-chapter.js 做语法降级(去掉 ?? 等 ES2020 语法),Webpack / Vue CLI 默认可 parse。默认 wasmUrl 指向 jsDelivr 上的 pdfium.wasm,无需复制到 public/

离线或内网部署时,可传 wasmUrlinit 顶层,与 options 同级):

// 相对路径:自行把 dist/pdfium.wasm 放到静态目录
wasmUrl: '/pdfium.wasm',

// 自有 OSS / CDN(完整 HTTPS 地址,示例)
wasmUrl:
  'https://hep-editor.oss-cn-beijing.aliyuncs.com/public/editor-public/js/pdfium.wasm',

详见 docs/get-started/01-installation.md

@embedpdf-editor/chapter-snippet/webpack可选辅助(仅 monorepo Vue 2.6 解析、或自定义 devServer COOP/COEP),普通用户不必使用。

完整示例见 examples/chapter-viewer-demo-vue2

快速开始

不需要把 pdfium.wasm 手动放到宿主项目的 public/。默认会从 @embedpdf-editor/chapter-snippet/dist/pdfium.wasm 加载。只有当你要走 CDN 或自定义静态域名时,才需要传 wasmUrl

import ChapterEmbedPDF, {
  CHAPTER_SNIPPET_EVENTS,
} from '@embedpdf-editor/chapter-snippet';

const viewer = ChapterEmbedPDF.init({
  type: 'container',
  target: document.getElementById('reader'),
  worker: true,
  options: {
    manifest: {
      chapters: [
        {
          chapterId: 'ch-1',
          title: '第一章',
          globalPageRange: [1, 12],
          localPageRange: [0, 11],
          source: { url: '/pdfs/chapter-1.pdf' },
        },
      ],
    },
    notes: {
      loadNotes: async () => [],
      onUpdateNote: async () => {},
      // 两阶段删除:业务先确认外部数据,返回 true 才让插件擦 PDF 笔记
      onRequestDeleteNote: async () => true,
    },
    bookmarks: {
      load: async () => [],
      persist: async () => {},
      onRequestRemove: async () => true,
    },
    features: {
      markup: true,
      bookmarks: true,
      notes: true,
      selectionToolbar: true,
      zoom: { pageWidth: 720 },
    },
  },
});

viewer?.addEventListener(CHAPTER_SNIPPET_EVENTS.noteRequestCreate, (event) => {
  const { record, complete } = event.detail;
  openNoteDialog(record, async (content) => {
    const noteId = 'note-' + Date.now();
    const nodeId = await saveNote(content, record);
    await complete({ noteId, content, nodeId });
  });
});

viewer?.addEventListener(CHAPTER_SNIPPET_EVENTS.ready, (event) => {
  console.log('registry ready', event.detail.registry);
});

容器需要稳定高度:

#reader {
  height: 100vh;
  min-height: 0;
}

配置

| 字段 | 默认 | 说明 | | --- | --- | --- | | type | 必填 | 目前使用 'container' | | target | 必填 | 挂载目标元素 | | options | 必填 | 推荐的章节阅读器配置,和 React/Vue3 的 options 语义一致 | | wasmUrl | dist/pdfium.wasm | 自定义 PDFium wasm 地址 | | worker | true | 是否启用 PDFium worker | | fallbackToDirectEngine | true | 首章 worker 加载超时后自动降级到 direct engine | | workerOpenTimeoutMs | 8000 | 首章加载超时时间 | | features | - | 与 options.features 深度合并;顶层同名字段优先。勿放进 createChapterViewerEditorOptions() | | className / viewportClassName | - | 传给内部阅读器容器 |

旧版本的 editorInput 仍可使用,但新代码应改为 options,并把 features 写在 options.features 或顶层 features

PDF 三步加载(Vue 2 推荐)

| 步骤 | 说明 | | --- | --- | | 1 | manifest.chapters:仅页码;segmentPageThreshold 写在章节上 | | 2 | chapterPdfLoader.loadChapterUrls:按章 getOneChap每章只调一次 | | 3 | chapterPdfLoader.openPdf(可选):解密等;省略则直接打开 ctx.url |

options: {
  manifest: {
    chapters: [
      {
        chapterId: item._id,
        title: item.title,
        globalPageRange: [item.startPage, item.endPage],
        localPageRange: [0, item.endPage - item.startPage],
        segmentPageThreshold: item.page,
      },
    ],
  },
  chapterPdfLoader: {
    async loadChapterUrls(chapter) {
      const res = await getOneChap(chapter.chapterId);
      if (!res.success) throw new Error(res.message);
      const raw = res.data.resourceUrl;
      return Array.isArray(raw) ? raw : raw ? [raw] : [];
    },
    async openPdf(ctx) {
      const decrypted = await decryptPdf(ctx.url);
      return { buffer: decrypted }; // ArrayBuffer / Uint8Array / DataView 都支持
    },
  },
  notes: { /* ... */ },
  bookmarks: { /* ... */ },
},

勿在 loader 外再请求章节详情;勿把 segmentPageThreshold 放进 source

03-manifest.md · 12-segmented-pdf-and-per-chapter-storage.md

按章持久化

options.notes / options.bookmarks 回调里的 chapterIdlocalPageIndex 与单 URL 章相同。划线备份用 exportChapterAnnotations,JSON 键为 chapters[chapterId](导出 markup 时会拉全部分段)。

ChapterViewerOptions(与 React / Vue3 一致)

| 字段 | 说明 | | --- | --- | | manifest | 章节目录,见 React README Manifest | | notes | NoteCallbacks,见 React README | | bookmarks | ParagraphBookmarkCallbacks,见 React README | | chapterPdfLoader / overlapStrategy | 自定义 PDF 加载与重叠页策略 | | features | 功能开关与 UI 定制,见下 |

加密 PDF 可在底层 createPdfChapterEditor 选项中设置 passwordProvider(如 CallbackPasswordProvider from @embedpdf-editor/chapter-core)。

features 配置

与 React / Vue3 同一套 ChapterViewerConfig@embedpdf-editor/chapter-viewer)。完整字段表见 React README:features 配置

| 模块 | 要点 | | --- | --- | | markup | styles 四类划线;annotationMenu 默认选中后在下方显示「删除」;onClick 配置后不弹菜单;squiggly.offsetY 默认 4 | | bookmarks | marker / hover 自定义图标;marker.onClick 配置后不弹删除浮窗 | | notes | marker.renderIconrenderMenuActionsiconSizeoffsetX/offsetYhighlightColorhighlightStyle(任意 CSS)、alwaysVisibleonClick(高亮区与图标同一事件)、selectedOutline | | zoom | pageWidthmin / max / enabled;实际上限不超过 [data-chapter-scroll-viewport] 宽度,resize 时自动 clamp | | scrollViewport | background(默认 #f1f5f9),[data-chapter-scroll-viewport] 背景 | | page | background(默认 #ffffff),单页 PDF 画布背景 | | selectionToolbar | 两行浮窗、色盘 markupColorsextraActions[].onClick / onExtraAction;或监听 DOM 事件 selectionExtraAction | | pageOverlays | 页内矩形框;defaultBoxStyle(任意 CSS)、renderContentonClickctx.payload);数据经 importChapterAnnotationspageOverlays 注入 | | markup | 除 styles 外可配 onCreatedonRequestDeletePromise<boolean>true 才真删)、onDeleted | | bookmarksoptions) | 除 load / persist 外可配 onAdded(新增书签,含 position) |

features: {
  markup: {
    styles: {
      highlight: { color: '#fef08a', opacity: 0.45 },
      underline: { color: '#dc2626', thickness: 1.5, offsetY: 2.5 },
      squiggly: { color: '#dc2626', thickness: 1.5, offsetY: 4 },
    },
    annotationMenu: {
      enabled: true,
      renderMenu: ({ onDelete }) => {
        const btn = document.createElement('button');
        btn.textContent = '移除';
        btn.onclick = onDelete;
        return btn;
      },
    },
  },
  notes: {
    marker: {
      renderIcon: () => /* DOM */,
      renderMenuActions: ({ onEdit, onDelete }) => /* DOM */,
      // 默认 false:仅 hover 高亮时显示图标。
      // true:图标常驻显示,hover 感知区仅覆盖图标本身。
      alwaysVisible: true,
    },
  },
  selectionToolbar: {
    selectionBackground: 'rgba(255, 193, 7, 0.35)',
    // 复制默认开启,浮窗最左侧;隐藏:hiddenBuiltinActions: ['copy']
    renderCopyIcon: () => {
      const span = document.createElement('span');
      span.textContent = '📋';
      span.setAttribute('aria-hidden', 'true');
      return span;
    },
    markupColors: ['#93c5fd', '#fde047'],
    extraActions: [
      {
        id: 'translate',
        label: '翻译',
        onClick: ({ selectedText, chapterId, localPageIndex }) => {
          console.log('翻译', selectedText, chapterId, localPageIndex);
        },
      },
    ],
    // 或统一回调:onExtraAction: (ctx) => { ... }
  },
  markup: {
    onCreated: ({ kind, strokeColor, position }) => {
      console.log('新划线', kind, strokeColor, position);
    },
  },
},

划词后点复制会将选中文本写入剪贴板。扩展按钮回调与 DOM 事件 detail 均含 selectedText。程序化:

import { copyTextToClipboard } from '@embedpdf-editor/chapter-snippet';

await copyTextToClipboard('文本');

renderMenu / renderIcon / renderCopyIcon 在 snippet(Preact)内执行,请返回 DOM 或 Preact 节点;Vue 2 宿主不要用 h() 直接塞进 Shadow DOM。

pageOverlays(矩形叠加层)

features: {
  pageOverlays: {
    defaultBoxStyle: {
      border: '2px dashed #ef4444',
      cursor: 'pointer',
    },
    renderContent: ({ payload }) => {
      const span = document.createElement('span');
      span.textContent = payload ?? 'QR';
      return span;
    },
    onClick: ({ overlay, payload }) => {
      console.log(overlay.overlayId, payload);
    },
  },
},

通过 importChapterAnnotations(registry, { chapterId, pageOverlays: [...] }) 注入数据;后端 QR 比例坐标转换见 13-page-overlays.mdqrTargetToPageOverlayRecord 由 React/Vue3 包导出,snippet 可手写 rectPdfCoord 或自行实现相同公式)。

事件

snippet 通过 DOM CustomEvent 抛出动作。常量见 CHAPTER_SNIPPET_EVENTS
若在 options 里已写 onClick / onCreated / onAdded 等回调,仍会派发同名 DOM 事件(便于 Vue 2 只绑 @xxx)。

| 常量 | 事件名 | detail(关键字段) | | --- | --- | --- | | noteRequestCreate | chapter-note-request-create | record(含 selectedTextposition)、complete({ noteId, content?, nodeId? }) | | noteRequestEdit | chapter-note-request-edit | recordnodeId? | | selectionExtraAction | chapter-selection-extra-action | actionIdselectedTextchapterIdrectsPdfCoord | | bookmarkAdded | chapter-bookmark-added | bookmarkposition | | markupCreated | chapter-markup-created | kindannotationIdstrokeColorpositionrecord | | setPdfPageBackground | chapter-set-pdf-page-background | background(CSS 颜色,派发以更新页背景) | | pdfPageBackgroundChange | chapter-pdf-page-background-change | background(更新后通知) | | ready | chapter-viewer-ready | registry(导出标注用) |

el.addEventListener('chapter-selection-extra-action', (e) => {
  const { actionId, selectedText, chapterId } = e.detail;
  if (actionId === 'translate') translate(selectedText, chapterId);
});

el.addEventListener('chapter-markup-created', (e) => {
  const { strokeColor, position, record } = e.detail;
  saveMarkup({ strokeColor, position, record });
});

// 运行时更新 PDF 页背景色
el.dispatchEvent(
  new CustomEvent('chapter-set-pdf-page-background', {
    detail: { background: '#1e293b' },
    bubbles: true,
    composed: true,
  }),
);
el.addEventListener('chapter-pdf-page-background-change', (e) => {
  console.log('page background', e.detail.background);
});

markup record 顶层额外带 localPageIndexglobalPageIndexglobalPageNumber。宿主若持有 ChapterPdfViewer ref 或兼容的滚动对象,可直接调用 scrollToMarkupRecord(viewer, markupRecord)scrollToNoteRecord(viewer, noteRecord)scrollToBookmarkRecord(viewer, bookmarkRecord) 回到对应位置。

外部列表删除标记后,需要同步移除渲染器内存里的对象。snippet 没有 ChapterPdfViewer ref,可在 chapter-viewer-ready 里保存 registry 后调用插件 capability:

import { removeChapterMarkupAnnotation } from '@embedpdf-editor/chapter-snippet';

let registry;
el.addEventListener('chapter-viewer-ready', (event) => {
  registry = event.detail.registry;
});

async function deleteNote(noteId) {
  await api.deleteNote(noteId);
  registry?.getPlugin('note')?.provides()?.removeNote(noteId);
}

async function deleteBookmark(bookmarkId) {
  await api.deleteBookmark(bookmarkId);
  registry?.getPlugin('paragraph-bookmark')?.provides()?.purgeBookmark(bookmarkId);
}

async function deleteMarkup(record) {
  await api.deleteMarkup(record.nodeId ?? record.annotation.id);
  if (registry) await removeChapterMarkupAnnotation(registry, record);
}

removeChapterMarkupAnnotation(registry, record) 会把 chapterId + localPageIndex 解析成实际 documentId + pageIndex,分段 PDF 也可直接使用。

附注创建:宿主弹窗 → 保存 → 调用 complete({ noteId, content?, nodeId? }) 注册锚点(也可事先写 record.nodeId)。划线 onCreated、书签 onAdded 同样可写 record.nodeIdreturn { nodeId }。编辑/删除 DOM 事件与回调均带 nodeId
完整回调表:11-events-callbacks-and-component-api.md

Vue 2 用法

Vue 2 组件只需要在 mounted 时初始化,在 beforeDestroy 时销毁:

import ChapterEmbedPDF from '@embedpdf-editor/chapter-snippet';

export default {
  props: {
    options: { type: Object, required: true },
  },
  mounted() {
    this.viewer = ChapterEmbedPDF.init({
      type: 'container',
      target: this.$refs.host,
      options: this.options,
      worker: true,
    });
  },
  beforeDestroy() {
    this.viewer?.destroy?.();
  },
};

完整示例见 examples/chapter-viewer-demo-vue2

标注导入导出

包内直接导出章节标注 IO:

import {
  exportAllChapterAnnotations,
  importChapterAnnotationsArchive,
  chapterAnnotationsArchiveToJson,
  parseChapterAnnotationsArchiveJson,
} from '@embedpdf-editor/chapter-snippet';

在 snippet 中可通过 chapter-viewer-ready 事件拿到 registry,再调用这些 API。导出格式包含 bookmarksnotesmarkuppageOverlays,版本常量为 CHAPTER_ANNOTATIONS_ARCHIVE_VERSION

| 选项 | 说明 | | --- | --- | | mode | replace 清空后导入;merge 合并 | | ensureChapterLoaded | 默认 true;含 markup 的分段章会加载 全部段 再合并页码 | | bookmarks / notes / markup / pageOverlays | 默认 true,可关闭某一类 | | persistNotes / persistBookmarks | 导入后写回业务存储 |

示例见 examples/chapter-viewer-demo-vue2/src/components/AnnotationsDemoBar.vue。详见 10-annotations-io.md

教程索引

| 主题 | 文档 | | --- | --- | | 目录 | docs/get-started/README.md | | wasmUrl(含 OSS 示例) | 01-installation.md | | 划词复制 | 07-selection-toolbar.md | | 事件常量 | 11-events-callbacks-and-component-api.md | | 分段 + 按章存储 | 12-segmented-pdf-and-per-chapter-storage.md | | 页内矩形叠加层 | 13-page-overlays.md |

与 React / Vue3 渲染器的区别

| 项 | React / Vue3 | chapter-snippet(本包,含 Vue 2) | | --- | --- | --- | | 运行时 | 框架内 editor-engine 组件 | Shadow DOM Web Component + Preact | | 初始化 | <ChapterPdfViewer options={...} /> | ChapterEmbedPDF.init({ type: 'container', target, options }) | | 动态 manifest | 改 options.manifest(自动 setManifest) | 改 initoptions.manifest(Preact 内同步) | | Registry | onInitialized / useRegistry() | chapter-viewer-readydetail.registry | | Vite | chapterViewerViteResolve() | chapterSnippetViteResolve() |

许可

  • 本包 JavaScript / 类型定义:MIT(见 LICENSENOTICE
  • dist/pdfium.wasm:来自 PDFium,适用 Apache License 2.0(见 LICENSE.pdfiumNOTICE.pdfiumTHIRD-PARTY-NOTICES.md
  • 再分发本包或随产品打包 wasm 时,须保留上述许可文件

常见问题

| 现象 | 处理 | | --- | --- | | 卡在“正在加载 PDFium” | 检查 dist/pdfium.wasm 是否随包发布并能被浏览器访问;自定义部署时传 wasmUrl | | 卡在“正在加载 xxx.pdf” | 优先确认章节 PDF URL 是否 200;如只在 worker 模式出现,可保留默认 fallback 或临时设 worker: false 定位环境问题 | | 只显示空白 | 确认宿主元素有高度,且没有被父容器 overflow/flex 布局压到 0 | | 划词后没有笔记弹窗 | 监听 chapter-note-request-create,保存后调用 detail.complete({ noteId, ... }) | | Vue/Vite 开发环境异常预构建 | 使用 chapterSnippetViteResolve(),确保 snippet 和 PDFium 引擎没有被 optimizeDeps 预构建 | | 分段章存了 #sN | 业务层只用 chapterId;引擎段 ID 勿写入库 |