Repository Wiki
Naptie/endfield-docmaker

主页面与 PDF 实时预览

主页面(src/routes/+page.svelte)是 endfield-docmaker 的核心 UI 入口,它将动态表单、Typst 浏览器编译器与多级 PDF 预览回退策略组合成一个"填写即出稿"的单页体验。本文深入剖析该页面的状态模型、预览模式判定逻辑与持久化机制。

目的与范围(Purpose and Scope)

本页覆盖 主页面组件(+page.svelte) 及其直接编排的 PDF 实时预览链路,包括:

  • 页面的顶层状态模型(生成中、可下载、可分享等标志位)
  • 预览模式的三级判定:内联 <object type="pdf"> → Typst 向量 SVG → 栅格化页面图片
  • 运行时"PDF 查看器失效"探测与跨会话记忆机制
  • 模板选择、按模板隔离的表单值状态(valuesMap)与 localStorage 持久化
  • 主页面引用的各协作模块的接线方式(typst.svelte、utils/render、utils/svg、stores/docs 等)

以下主题有意留给兄弟页面,本文只做入口级指引:

  • 模板定义与注册机制(TEMPLATES、getTemplate、template.defaults() 的实现)——见模板系统页
  • Typst 编译器的加载与封装——见 Typst 编译封装页
  • 文档库与云端同步的完整实现(stores/docs、stores/cloud)——见文档库页
  • 分享链接 / 二维码 / 云剪贴板(utils/link、utils/paste)——见分享与持久化页
  • 动态表单渲染(DynamicForm.svelte)——见动态表单页

概述(Overview)

endfield-docmaker 是一个纯前端(无后端)的文档生成器:用户在左侧表单中填写模板参数,页面调用浏览器内 WASM 版 Typst 编译器实时产出 PDF,并在右侧预览。README 中将其列为项目核心特性:

  • 在浏览器中实时生成 PDF 文件,无需后端服务
  • PDF 实时预览、在新标签页中打开、下载

主页面承担的职责可以概括为"编排者":它本身不实现编译、渲染或存储,而是把这些能力装配成一条从表单输入到可预览、可下载、可分享文档的流水线。它最复杂的部分是预览模式自适应——因为该应用预期运行在多种环境中(普通浏览器、移动端 WebView、Bilibili App 内嵌的沙盒 iframe),不同环境对内联 PDF 的支持差异极大,页面实现了一套确定性的判定 + 运行时探测 + 失败记忆的组合策略。

关键概念与术语:

术语含义
内联预览用 <object type="pdf"> 让浏览器原生 PDF 插件直接渲染
向量预览调用 typst.svg() 直接从编译器排版模型产出整篇文档的 SVG,不经 pdf.js 往返
栅格预览用 renderPdfPages 将 PDF 每页位图化(图片兜底)
Toy 路径Bilibili App 内嵌运行时的 /toy/<slug>/… URL 前缀
断定失效<object> 的 fallback 内容被布局 → 说明 PDF 插件未成功实例化,记为 pdfObjectBroken

架构(Architecture)

Loading diagram...

主页面处于编排层中心:表单侧(DynamicForm + 模板注册表)产生状态,编译层(typst.svelte)消费状态产出 PDF 字节,预览侧根据模式判定选择 <object>、SVG 或位图;文档库与分享模块从同一份状态快照读取/写入。

依赖装配(imports 即接线图)

页面顶部的 import 列表直接反映了依赖边界——这是阅读该组件的第一入口:

ts
1import typst, { 2 loadingState, 3 packageLoadingState, 4 downloadProgress, 5 waitForTypst 6} from '$lib/typst.svelte'; 7import DynamicForm from '$lib/components/DynamicForm.svelte'; 8import CompileError from '$lib/components/CompileError.svelte'; 9import DocLibrary from '$lib/components/DocLibrary.svelte'; 10import ShareQrModal from '$lib/components/ShareQrModal.svelte'; 11import PageImageViewer from '$lib/components/PageImageViewer.svelte'; 12import { TEMPLATES, getTemplate } from '$lib/templates'; 13import { 14 getAllDocs, getDoc, putDoc, 15 deleteDoc as removeDocFromLibrary, 16 stripThumbnail, type LibraryDoc 17} from '$lib/stores/docs'; 18import { pushDocToCloud, pullDocsFromCloud, deleteDocFromCloud } from '$lib/stores/cloud'; 19import { initToy, supportsToy, toyEnv } from '$lib/toy.svelte'; 20import { 21 decodeDocState, buildToyPath, buildShareUrl, resolveToyPath, type DocState 22} from '$lib/utils/link'; 23import { uploadDocState, fetchPaste, externalStorageAvailable } from '$lib/utils/paste'; 24import { renderPdfPages } from '$lib/utils/render'; 25import { scopeTypstSvg } from '$lib/utils/svg'; 26import { bytesToBase64Url } from '$lib/utils/base64';

Source: +page.svelte

几点设计意图值得注意:

  • 编译进度与文档生成解耦:从 typst.svelte 只引入加载态信号(loadingState、packageLoadingState、downloadProgress、waitForTypst),说明编译器 WASM/字体包下载是首屏体验的一部分,由独立模块管理,主页面只消费。
  • 本地/云端双通道存储:stores/docs(本地)与 stores/cloud(远端)同时被引入,页面在保存时通过快照比较决定是否需要入库。
  • bytesToBase64Url:用于把 PDF 字节编码进分享 URL 或数据 URI,是"无后端分享"链路的基础。

核心机制一:PDF 预览模式的三级判定

这是该页面最精巧的部分。pdfViewerEnabled 等 API 在移动 WebView 中会"说谎",页面因此实现了一条从静态判定到运行时探测、再到失败记忆的完整链条。

1. 内联 PDF 支持的静态判定 pdfInlineSupported

ts
1/** 2 * Whether the environment can display PDFs inline (`<object type="pdf">`). 3 * Mobile webviews (e.g. inside the Bilibili app) typically cannot – in that 4 * case the preview falls back to rasterized page images. `?nopdf` forces 5 * the fallback locally for testing. 6 */ 7const pdfInlineSupported = $derived.by(() => { 8 if (typeof navigator === 'undefined') return true; 9 if (new URLSearchParams(window.location.search).has('nopdf')) return false; 10 if ('pdfViewerEnabled' in navigator) return navigator.pdfViewerEnabled === true; 11 return true; 12});

Source: +page.svelte

规则按顺序短路:

  1. SSR 阶段(navigator 未定义)默认乐观返回 true,避免服务端/客户端首帧不一致;
  2. ?nopdf 查询参数强制进入图片回退(本地测试图片路径用);
  3. 存在 navigator.pdfViewerEnabled 时以它为准;
  4. 老浏览器没有该 API 时默认 true(信任 <object> 的 fallback 探测兜底)。

2. 运行时"断定失效"探测

静态判定不够——源码注释明确指出:"pdfViewerEnabled 可能在查看器仍实例化失败时报告 true(例如 Bilibili 的沙盒预览 iframe)。当 <object> 的 fallback 内容可见时,我们一次性检测到它,然后在整个会话(以及重载后)记住该结论,而不是在每次生成时重新探测这个坏掉的 object。"

ts
1$effect(() => { 2 // A learned failure sticks, and a failed raster must not loop forever – 3 // `generatePDF` resets `previewFailed` so a fresh doc retries. 4 if (!pdf || previewFailed) return; 5 6 if (rasterRequired) { 7 if (!previewSvg && !isRenderingPreview) void refreshPreviewSvg(); 8 return; 9 } 10 11 const marker = () => document.getElementById('pdf-object-fallback'); 12 const PROBE_DEADLINE_MS = 8000; 13 let strikes = 0; 14 const started = Date.now(); 15 const fail = () => { 16 clearInterval(interval); 17 if (pdfObjectBroken) return; 18 console.warn('Inline PDF viewer unavailable – switching to image preview.'); 19 pdfObjectBroken = true; 20 try { 21 localStorage.setItem(PDF_OBJECT_BROKEN_KEY, '1'); 22 } catch { 23 /* private mode etc. – the in-session verdict still applies */ 24 } 25 }; 26 const interval = setInterval(() => { 27 if (Date.now() - started > PROBE_DEADLINE_MS) { 28 clearInterval(interval); 29 return; 30 } 31 if (marker() && (marker()!.offsetHeight > 0 || marker()!.getClientRects().length > 0)) { 32 if (++strikes >= 2) fail(); 33 } else { 34 strikes = 0; 35 } 36 }, 400); 37 return () => { 38 clearInterval(interval); 39 }; 40});

Source: +page.svelte

实现细节与设计意图:

  • 探测原理:成功加载的 PDF <object> 的子元素永远不会被布局;若 id 为 pdf-object-fallback 的元素 offsetHeight > 0 或 getClientRects().length > 0,说明 fallback 内容被渲染,即插件未实例化。
  • 两次命中(strikes ≥ 2)防抖:加载瞬间的瞬时布局可能造成一次假阳性,连续两次命中才判死。
  • 8 秒探测死线:沙盒环境可能既不加载 PDF 也不布局 fallback,死线防止无限轮询;死线内无失败信号即视为查看器健康。
  • 探测贯穿对象生命周期:清理函数里 clearInterval(interval) 保证 $effect 依赖变化时定时器被回收,且注释强调"失败可能较晚浮现,需要持续观察"。
  • 记忆跨会话持久化:PDF_OBJECT_BROKEN_KEY = 'pdf-object-broken' 写入 localStorage,重载后不再重复探测(见下文读取代码)。

3. 读回历史结论(防御式)

ts
1// Read the persisted verdict defensively – sandboxed iframes may throw 2// `SecurityError` on `localStorage` access, which must not abort the page. 3try { 4 pdfObjectBroken = 5 typeof localStorage !== 'undefined' && localStorage.getItem(PDF_OBJECT_BROKEN_KEY) === '1'; 6} catch { 7 /* storage unavailable – the runtime probe below decides instead */ 8}

Source: +page.svelte

沙盒 iframe 访问 localStorage 会抛 SecurityError,必须吞掉——页面在无痕模式/受限环境下依然要可用。

4. 最终决策 rasterRequired

ts
1/** 2 * Whether the preview must fall back to rasterized page images. 3 * 4 * Deterministic on Bilibili Toy: the sandboxed iframe reports 5 * `pdfViewerEnabled` even though its PDF plugin never instantiates, so we 6 * never trust inline PDF there. `?pdf` forces the inline `<object>` 7 * (debugging) and `?nopdf` forces raster (testing), regardless of env. 8 */ 9const rasterRequired = $derived.by(() => { 10 if (typeof window === 'undefined') return false; 11 const params = new URLSearchParams(window.location.search); 12 if (params.has('pdf')) return false; 13 if (!pdfInlineSupported) return true; 14 if (pdfObjectBroken) return true; 15 return isOnToyPath(); 16});

Source: +page.svelte

判定优先级(自上而下短路):

顺序条件结果理由
1?pdf 查询参数强制内联调试用途,覆盖一切环境判定
2pdfInlineSupported === false栅格回退静态判定已否定(含 ?nopdf)
3pdfObjectBroken === true栅格回退学习到的失效结论(跨会话)
4isOnToyPath()栅格回退Bilibili Toy 沙盒从不信任内联 PDF

isOnToyPath 用正则 ^\/toy\/[^/]+(?:\/|$) 匹配路径名,识别 Bilibili App 内嵌运行时;源码注释称此路径上的判定是确定性的,因为其沙盒 iframe 会谎报 pdfViewerEnabled。

预览模式决策流程图

Loading diagram...

核心机制二:向量 SVG 预览(为何绕开 pdf.js)

页面为栅格回退分支准备了独立的向量预览状态:

ts
1// Vector preview of the whole document (all pages, stacked vertically). 2// `typst.svg()` renders straight from the compiler's layout model – no 3// pdf.js round-trip, so it cannot hit the sandbox's per-page raster hang, 4// and it stays crisp at any zoom level. 5let previewSvg = $state<string | undefined>(undefined); 6let isRenderingPreview = $state(false); 7let previewFailed = $state(false); 8 9/** Raster scale (px per pt) for library thumbnails – ~446 px wide A4. */ 10const THUMBNAIL_PPP = 0.75;

Source: +page.svelte

设计意图写在注释里,值得逐条拆解:

  • 直达排版模型:typst.svg() 直接从 Typst 编译器的布局模型渲染 SVG,绕开 pdf.js 的字节往返。
  • 规避沙盒栅格挂起:沙盒 iframe 内 pdf.js 的按页栅格化可能挂死,SVG 路径天然不会触发。
  • 任意缩放不失真:矢量输出在任何缩放级别都保持清晰,这对文档预览体验至关重要。
  • THUMBNAIL_PPP = 0.75:文档库缩略图专用的栅格比例(px/pt),对应 A4 约 446px 宽——缩略图与整页预览使用不同的渲染预算,缩略图刻意低清以省资源。

配套的 PageImageViewer.svelte 与 utils/render.renderPdfPages / utils/svg.scopeTypstSvg 共同构成图片查看器分支:renderPdfPages 产出逐页位图数组,scopeTypstSvg 负责将 Typst 原始 SVG 包裹上作用域化的样式(避免页面全局 CSS 与 SVG 内联样式互相污染)。

核心机制三:模板选择与按模板隔离的表单状态

主页面用一个双层键控的状态结构同时支持"多模板"与"每模板一份进行中文档":

ts
1// Template selection 2const STORAGE_META_KEY = 'endfield-doc:meta'; 3let templateId = $state(TEMPLATES[0].id); 4let template = $derived(getTemplate(templateId)); 5 6/** Library entry ids keyed by template – one working document per template. */ 7let docIds = $state<Record<string, string>>({}); 8const currentDocId = () => docIds[templateId]; 9 10// Form values (keyed by template id) 11let valuesMap = $state<Record<string, Record<string, unknown>>>({});

Source: +page.svelte

  • valuesMap: Record<templateId, Record<fieldKey, unknown>> —— 表单值以模板 id 为第一层键,切换模板时无需重新加载,天然保留每个模板各自的填写进度。
  • docIds: Record<templateId, docId> —— 每个模板对应文档库中的一条工作文档记录,"一个模板一份进行中文档"。
  • 模板元信息本身存于 endfield-doc:meta,与表单值分开存储,读写互不阻塞。

表单值的初始化是惰性的,通过 $effect 保证只在首次访问某模板时落默认值:

ts
1// Ensure values are initialized for the current template 2$effect(() => { 3 if (!valuesMap[templateId]) { 4 valuesMap[templateId] = template.defaults(); 5 } 6});

Source: +page.svelte

这种"缺省即建"的模式避免了在顶层一次性为所有模板预生成默认值——模板可能很多,且大多数用户只会用到一两个。

核心机制四:localStorage 持久化与脏标记

ts
1// Storage 2const storageKey = (tid: string) => `endfield-doc:${tid}`; 3 4const saveToStorage = () => { 5 try { 6 localStorage.setItem(STORAGE_META_KEY, JSON.stringify({ templateId, docIds })); 7 const tpl = getTemplate(templateId); 8 localStorage.setItem( 9 storageKey(templateId), 10 JSON.stringify({ version: tpl.storageVersion, values: valuesMap[templateId] }) 11 ); 12 } catch (e) { 13 console.error('Error saving to storage:', e); 14 } 15};

Source: +page.svelte

设计要点:

  • 键命名约定:endfield-doc:meta(全局元信息)与 endfield-doc:<templateId>(每模板表单值)分离,清理单模板数据不影响其它模板。
  • 版本化存储:每条记录带 version: tpl.storageVersion,为将来模板字段演进预留迁移通道。
  • 写失败容错:try/catch 包裹整个写入(配额满、隐私模式),失败只打 console.error,绝不打断 UI 流程——持久化是增强项而非关键路径。
  • 脏标记防抖:页面另维护 lastSavedSnapshot(上次自动保存时 { templateId, values } 的快照字符串)与 isDirty,用于判断"当前文档是否有值得自动保存的未保存编辑",避免高频无意义写入。

配合编译错误组件与自动保存,形成了"编辑 → 编译 → 预览/报错 → 脏则入库"的闭环。

核心流程(Core Flow)

端到端时序:从表单输入到可预览文档

Loading diagram...

页面生命周期状态机

Loading diagram...

使用示例(Usage Examples)

示例 1:控制生成与下载按钮的可用性标志

页面用一组布尔状态驱动工具栏(下载、新标签页打开、分享、保存图片)的可用性,canSaveToAlbum 专门服务 Bilibili App 内保存到相册的场景:

ts
1let isReady = $state(false); 2let isGenerating = $state(false); 3let canDownload = $state(false); 4let canShare = $state(false); 5let isSavingImage = $state(false); 6/** True when the Bilibili App can save images to the album. */ 7let canSaveToAlbum = $state(false); 8 9/** 10 * Whether saving images is possible in the current environment: 11 * in-app via the album, or in-browser via `<a download>`. 12 */ 13const saveImageSupported = $derived(canSaveToAlbum || canDownload);

Source: +page.svelte

saveImageSupported 是一个能力或运算:任一保存通道(App 相册、浏览器 <a download>)可用即启用保存图片入口,保证最差环境仍有出路。

示例 2:识别 Bilibili Toy 运行环境

ts
/** True when running under a Bilibili Toy path (`/toy/<slug>/…`). */ const isOnToyPath = () => typeof window !== 'undefined' && /^\/toy\/[^/]+(?:\/|$)/.test(window.location.pathname);

Source: +page.svelte

配合 initToy / supportsToy / toyEnv(来自 $lib/toy.svelte)与 buildToyPath / resolveToyPath(来自 utils/link),构成对 Bilibili 内嵌分发的完整适配:路径前缀判断是零成本、确定性的,因此放在 rasterRequired 的最后一环作为兜底。

示例 3:工具函数的选取

ts
import { pick, randomId, triggerDownload } from '$lib/utils';

Source: +page.svelte

  • triggerDownload:以编程方式触发浏览器下载(配合 bytesToBase64Url 可生成数据 URI 下载链接)。
  • randomId:为新建文档库条目生成 id(配合 docIds 结构)。
  • pick:从对象中挑选子集(典型用途是从表单值中提取入库/分享所需的字段)。

示例 4:编译期加载进度信号

ts
1import typst, { 2 loadingState, // WASM 编译器加载状态 3 packageLoadingState, // Typst 包加载状态 4 downloadProgress, // 下载进度 5 waitForTypst // 等待编译器可用的 Promise 6} from '$lib/typst.svelte';

Source: +page.svelte

页面把这些信号对接到 Spinner / 加载 UI(来自 $lib/components/ui/spinner),首屏在编译器下载完成前展示进度而非空白。相关构建配置见 vite.config.ts 中的注释——pdf.js 的 worker 是 ESM-only,默认 IIFE 打包会静默失败,因此 worker: { format: 'es' } 被显式设置:

ts
// pdf.js's worker is ESM-only; the default IIFE bundle breaks it silently. worker: { format: 'es' },

Source: vite.config.ts

配置项(Configuration Options)

主页面本身没有独立的配置对象,其可配置面通过 URL 查询参数、存储键与常量暴露:

配置项类型默认值说明
?pdfURL 查询参数不存在强制内联 <object> PDF 预览(调试用),覆盖一切环境判定
?nopdfURL 查询参数不存在强制栅格图片回退(测试图片路径用)
endfield-doc:metalocalStorage 键—存 { templateId, docIds } 全局元信息
endfield-doc:<templateId>localStorage 键—存 { version, values } 每模板表单值
pdf-object-brokenlocalStorage 键'0'/未设置PDF 内联查看器失效的跨会话结论,'1' 表示失效
THUMBNAIL_PPP常量 number0.75缩略图栅格比例(px/pt),A4 约 446px 宽
PROBE_DEADLINE_MS常量 number8000fallback 探测死线(毫秒)
探测轮询间隔常量 number400每次探测间隔(毫秒)
探测命中阈值常量 number2连续命中次数达到即判死
pdfjs-distnpm 依赖^4.10.38栅格回退所用的 PDF 渲染库

API 参考(组件内部关键状态与方法)

本节列出主页面内部值得关注的响应式状态与函数(非公开导出 API,而是该组件的"接缝面",供维护者定位)。

pdfInlineSupported: boolean($derived)

  • 职责:判定当前环境能否以内联 <object type="pdf"> 展示 PDF。
  • 求值顺序:SSR → ?nopdf → navigator.pdfViewerEnabled → 默认 true。
  • 注意:返回 true 不代表一定能用——还需通过 pdfObjectBroken 运行时探测。

rasterRequired: boolean($derived)

  • 职责:最终决定预览是否必须走栅格图片回退。
  • 短路顺序:?pdf → pdfInlineSupported → pdfObjectBroken → isOnToyPath()。

pdfObjectBroken: boolean($state)

  • 职责:运行时学习到的"内联 PDF 已失效"结论。
  • 写入时机:fallback 探测连续两次命中;写入后同步持久化到 localStorage['pdf-object-broken'] = '1'。
  • 读取时机:组件初始化时防御式读回,localStorage 抛异常则交给运行时探测决定。

saveToStorage(): void

  • 参数:无。
  • 行为:先写 endfield-doc:meta({ templateId, docIds }),再写 endfield-doc:<templateId>({ version: template.storageVersion, values })。
  • 异常:内部捕获所有写入错误,仅 console.error,不上抛。

setValues(v: Record<string, unknown>): void / getValues(): Record<string, unknown>

  • 职责:以当前 templateId 为键读写 valuesMap,供 DynamicForm 双向绑定与编译提交复用。
  • 缺省行为:getValues 在值未初始化时返回 {}(初始化由惰性 $effect 负责)。

currentDocId(): string | undefined

  • 职责:返回当前模板对应的文档库条目 id(docIds[templateId])。

失败模式、边界情况与并发(Failure Modes & Concurrency)

场景触发条件处理方式源码证据
浏览器不支持内联 PDFnavigator.pdfViewerEnabled === false静态判定直接进入栅格回退pdfInlineSupported
沙盒 iframe 谎报支持<object> fallback 被布局连续 2 次探测命中后记死并持久化探测 $effect
探测永不收敛沙盒既不加载也不布局 fallback8 秒死线后停止轮询,视为健康PROBE_DEADLINE_MS
localStorage 访问被拒隐私模式 / 沙盒 SecurityError捕获并降级为会话内记忆;保存失败仅打日志读取/写入的 try/catch
栅格渲染失败沙盒栅格化挂起previewFailed 标志阻止无限重试;generatePDF 重置它以允许新文档重试$effect 顶部守卫注释
重复探测/重复渲染$effect 依赖翻转重跑if (!previewSvg && !isRenderingPreview) 守卫防止重启渲染rasterRequired 分支
定时器泄漏$effect 依赖变化返回清理函数 clearInterval$effect return
模板切换用户换模板表单值按 templateId 隔离,无需重新加载valuesMap 双层键控
编译失败Typst 语法/参数错误渲染 CompileError 组件展示错误,修正后重试状态机 CompileError 态
存储配额满localStorage 写入抛异常捕获 + console.error,不打断 UIsaveToStorage

并发要点:

  • 所有预览状态都是 Svelte 5 runes($state/$derived/$effect),由响应式系统调度,无手写订阅——但 $effect 内部的 setInterval 是命令式副作用,必须自清理(已实现)。
  • 自动保存以快照字符串比较去重(lastSavedSnapshot + isDirty),即便高频编辑也只在与上次快照不同才写库,天然避免了写风暴。

性能与运维要点(Performance & Operational Notes)

  • 首屏关键路径是 Typst WASM 与字体包下载(loadingState / downloadProgress),主页面把这部分交给 typst.svelte 管理,仅消费信号渲染进度——字体资产位于 src/lib/assets/fonts/(含 FZXIAOBIAOSONG-B05.TTF、NotoSansCJKsc-Regular.ttf、JetBrainsMono-VariableFont_wght.ttf 等多字体),是包体主要构成。
  • SVG 预览优先于 pdf.js 栅格化:整篇文档一次向量渲染(typst.svg())避免逐页位图化的开销与沙盒挂起风险,同时缩放零成本。
  • 缩略图与整页预览分离预算:THUMBNAIL_PPP = 0.75 让文档库缩略图保持约 446px 宽的 A4 低保真,避免为列表视图付出整页渲染成本。
  • 探测成本可控:仅在非栅格分支运行,400ms 轮询、8 秒死线、一次判死后不再探测(跨会话记忆),最坏情况约 20 次轮询。
  • worker 打包格式:pdf.js 的 worker 必须以 ESM 打包,否则静默失败——这是运维/构建层面最容易踩的坑,已在 vite.config.ts 显式固化。

扩展点(Extension Points)

  • 新增模板:模板系统通过 TEMPLATES 注册,主页面零改动即可获得新表单与预览(每模板自动获得隔离的 valuesMap 槽位与存储键)。详见模板系统页。
  • 新增预览模式:预览判定集中在 rasterRequired / pdfInlineSupported 两个 $derived 与一个探测 $effect 中,新增模式(如分页 SVG)只需扩展该判定链与渲染分支。
  • 新增分享通道:utils/link(buildShareUrl)与 utils/paste(uploadDocState)已把编码/上传抽象出来,主页面只负责调用与弹窗编排。
  • 环境适配:新的内嵌宿主(类似 Bilibili Toy)只需:① 路径识别函数(参照 isOnToyPath);② 探测结论持久化键(参照 PDF_OBJECT_BROKEN_KEY)。
  • 源文件:+page.svelte
  • 构建配置:vite.config.ts
  • 项目说明:README.md
  • 依赖清单:package.json
  • 兄弟主题(模板系统、Typst 编译封装、文档库与云同步、分享链接与持久化、动态表单)见各自目录页

Sources

(1 files)