Document preview and file addresses
文档预览与文件地址
File viewers need different loading policies and may offer several implementations for one extension. A change stream cannot also express an on-demand read without mixing live data with callable capabilities. HTML dependencies additionally need the Host's filesystem authorization and path resolution, not the browser's current directory.
English
Problem
File viewers need different loading policies and may offer several implementations for one extension. A change stream cannot also express an on-demand read without mixing live data with callable capabilities. HTML dependencies additionally need the Host's filesystem authorization and path resolution, not the browser's current directory.
Decision
Document Preview separates resource observation from content reads. The resource model shares observations by address alone: source(address), pin(address, signal), and provider open(address, { signal }) carry no consuming Session. Providers return AsyncIterable<RemoteResult<ResourceProtocolMap[P]>>; useResource exposes only { status, value, failure }. Holds start and stop observation, not the underlying file or Session. Content reads use ordinary injected Preview callbacks.
Workspace Files retains Host line reads, byte windows, bounded complete reads, and bounded reads relative to another file's directory. Its Client file provider observes only stat and changes, with ResourceProtocolMap.file directly naming WorkspaceFileStat. The Host resolves every path through the Session filesystem; file reads inherit that backend's read authority, while directory listing and change observation stay workspace-scoped.
Readable files use dsh-resource://file/session/<sessionId>/<path>. The path may be workspace-relative or absolute; an encoded absolute path retains its leading slash. fileAddressFor always emits this Session-address form. The provider and Preview RPC take the Session only from that address, never from the current selection, first holder, or owning tab. A Session-less absolute URI cannot be read; the provider reports workspace-file/unknown-workspace. Session authorization is a file-protocol rule, not an additional Resource identity.
Document Preview owns format selection and loading policy. Metadata registers with ctx.documentPreviews; components register separately into the keyed sidebar.right.tab.document Slot. Extension registrations precede builtins, then longer suffixes and registration order decide. The toolbar lists supported alternatives and remembers a manual choice per tab. Plain text remains available for unknown extensions and text-compatible renderers. Registered binary suffixes determine text compatibility independently of loading mode; HTML and SVG remain outside those suffixes and retain source viewing. A single candidate renders no viewer control. Registered binary suffixes omit plain text; known binary suffixes without a renderer show an unsupported state without reading the file (sidebar preview polish). The child receives accumulated text or complete native bytes, the original resource address, and the standard useResource and useTabInfo hooks. Preview calls ctx.remote.workspaceFiles.read for text and ctx.remote.workspaceFiles.readBytes for native bytes, with baseFile for related resources. Refresh remains per tab, with no resource reload, shared changed acknowledgement, extra resource wrapper, or content Session.
Markdown and code reuse the incremental primitives with cumulative paged text. HTML, PDF, and images read complete Uint8Array<ArrayBuffer> data; Host transport uses the binary workspace file transfer. Published buffers are borrowed read-only and never persist into layout or Session JSON. PDF.js runs in an owned Worker with version-matched bundled font and decoder data, and copies input before transfer to preserve Preview's retained buffer. With Developer tools enabled, HTML runs in a Blob iframe with sandbox="allow-scripts", without same-origin, popup, form, download, or top-navigation privileges. The browser retains its normal external-network rules. Bounded static local JS/CSS reads stay in the parent; the opaque frame creates its own asset Blobs, because it cannot load parent-origin Blobs. PNG, JPEG, GIF, WebP, BMP, ICO, and SVG use image-specific Blob URLs in an <img> static-image context. An image wider than the pane scales down to its width at its aspect ratio; a smaller image keeps its intrinsic CSS-pixel dimensions centred by auto margins, and a taller image extends the shared scroller's vertical range (sidebar preview polish). The renderer provides no zoom or drag-to-pan. SVG markup never enters the application DOM or an iframe, so scripts remain inert and cannot reach the parent page. Replacing HTML or an image revokes its root Blob URL.
PDF.js's official TextLayerBuilder owns selection boundaries and copy normalization over the width-fitted canvas, with shared page cleanup and a component-owned resize observer. Responsive sizing uses the CSS scale property independently of PDF.js's page rotation and translation transforms. Its end-of-content marker and stacking rules constrain selection in blank regions; line-break highlighting is suppressed. Per-page cancellation uses the builder's cleanup rather than aborting the first page's signal, because the official selection listeners are shared across pages.
Alternatives considered
Load converted content through callbacks in preview metadata. A callback makes the shared file store hold both original file bytes and format-specific conversion results. Renderer-owned loading keeps conversion caches, failures, and font metadata with Office while preserving shared file identity and toolbar controls. Definitions declare loading: 'renderer'; the body receives a format-independent loading revision and delegates reads and store updates to its private injected face. The face reports the displayed source version or a failed load to the shared owner. Reload or implementation replacement advances the revision, stale reports are ignored, and the body cancels pending work on replacement or unmount. Office retains settled contents for the tab lifetime and composes its own PDF child slot.
Methods attached to an Iterator or its values. This conflates observation with commands and repeats capability identity in data frames. Frames carry data and failures; explicit Preview RPC callbacks perform reads.
A core public-projection factory, or the same assembly inside open. Separate stream values, operations bundles, and public interfaces add assembly without another current consumer that needs it. Preview's shared RPC adapter already keeps Session addressing and byte transport out of renderers. Resource offers no provider-agnostic command interface or opening-bound command lifetime; adding either needs consumer evidence beyond file preview.
UI Session as extra Resource identity, or authorization from the first holder or current selection. A retained tab can belong to a different Session from the selected one, and the UI location does not identify the addressed file. Encoding the required Session in the file address preserves Host authorization while letting all readers of one address share observation.
File-reading methods on every resource. Chat and terminal resources have independent data and operation semantics; only observation registration and lifetime are common.
A preview resource wrapper, content Session, or second resource Hook. These duplicate addressing, cancellation, subscriptions, and ownership already provided by Resource and Workspace Files. Loading policy belongs to the preview owner.
A local server, virtual host, or file: iframe. These require extra hosting or filesystem authority. The preview is for static generated pages, not a complete application runtime; modules, dynamic filesystem requests, and arbitrary nested asset graphs are outside its support.
Sanitize SVG into the application DOM or an iframe. A sanitizer would add a second SVG parser and an evolving active-content policy before placing untrusted markup in an interactive document. The <img> static-image context preserves native SVG rendering and intrinsic dimensions without giving the markup a script-capable DOM.
Consequences
Renderers can be replaced without changing the tab or file protocol. Full-file formats pay bounded whole-file memory and PDF adds bundled Worker/font/decoder bytes. Format selection and view state are page-local, not durable Session data. Preview owns RPC cancellation and native buffers independently of metadata observation. A tab retains its read version and the observation version captured at read start; refreshing it neither discards another tab's content nor clears its change notice. File reads remain non-transactional, and opaque versions are compared for equality, not ordering. The recorded browser scenario exercises the shared toolbar, incremental text, isolated HTML dependencies, width-fitted raster and SVG rendering, inert SVG scripts, and lazy continuous PDF Worker rendering.
中文
问题
文件预览器需要不同的加载策略,同一扩展名也可能对应多种实现。变更流无法同时表达按需读取而又不把实时数据与可调用能力混在一起。HTML 依赖还需要 Host 的文件系统授权和路径解析,而非浏览器的当前目录。
决策
Document Preview 将资源观察与内容读取分开。资源模型只按地址共享观察:source(address)、pin(address, signal) 和提供方的 open(address, { signal }) 均不携带消费 Session。提供方返回 AsyncIterable<RemoteResult<ResourceProtocolMap[P]>>;useResource 只暴露 { status, value, failure }。持有只控制观察的启停,不控制底层文件或 Session 的生灭。内容通过普通注入的 Preview 回调读取。
Workspace Files 保留 Host 的行读取、字节窗口、有上限的全文读取和相对另一文件目录的有界读取。Client file 提供方只观察 stat 与 changes,ResourceProtocolMap.file 直接为 WorkspaceFileStat。Host 通过 Session 文件系统解析每条路径;文件读取继承该后端的读取权限,目录列举与变更观察仍限定于工作区。
可读取的文件使用 dsh-resource://file/session/<sessionId>/<path>。路径可以相对工作区,也可以是绝对路径;编码后的绝对路径保留前导斜杠。fileAddressFor 始终生成这种 Session 地址。提供方与 Preview RPC 只从该地址取 Session,不取当前选择、首个持有者或 tab 所属 Session。不带 Session 的 absolute URI 无法读取;提供方报告 workspace-file/unknown-workspace。Session 授权是文件协议规则,不是额外的 Resource 身份。
Document Preview 负责格式选择和加载策略。元数据通过 ctx.documentPreviews 注册;组件单独注册到 keyed sidebar.right.tab.document Slot。扩展注册优先于内置注册,其次比较后缀长度和注册顺序。工具栏列出受支持的候选,按 tab 记住手动选择。未知扩展名及文本兼容的渲染器仍可使用纯文本。文本兼容性由注册的二进制后缀决定,与加载方式无关;HTML 和 SVG 不属于二进制后缀,保留源码查看。只有一个候选时不显示查看器控件。注册声明为二进制的后缀不提供纯文本;已知二进制后缀没有注册渲染器时不发起读取,并显示不支持预览的空态(侧边栏预览打磨)。子组件收到累积文本或完整原生字节、原始资源地址,以及标准 useResource 和 useTabInfo 钩子。Preview 通过 ctx.remote.workspaceFiles.read 读取文本,通过 ctx.remote.workspaceFiles.readBytes 读取原生字节,关联资源使用 baseFile。刷新仍按 tab 独立进行,不引入资源 reload、共享 changed 确认、额外资源包装层或内容 Session。
Markdown 和代码通过累积的分页文本复用增量渲染原语。HTML、PDF 和图片读取完整 Uint8Array<ArrayBuffer> 数据;Host 传输使用工作区文件二进制传输。发布后的缓冲区只读借用,绝不持久化进布局或 Session JSON。PDF.js 在自有 Worker 中运行,字体和解码数据以相同版本随包发布,转移输入前先复制,以保留 Preview 的缓冲区。开启开发者工具时,HTML 在 Blob iframe 中运行,设置 sandbox="allow-scripts",不授予同源、弹窗、表单、下载或顶层导航权限。浏览器保持正常的外部网络规则。有上限的静态本地 JS/CSS 读取由父页面负责;不透明源 iframe 创建自己的资源 Blob,因为它不能加载父源创建的 Blob。PNG、JPEG、GIF、WebP、BMP、ICO 和 SVG 使用图片专用 Blob URL,在 <img> 静态图片上下文中渲染。比面板宽的图片按纵横比缩小到面板宽度;较小的图片保留固有 CSS 像素尺寸并由 auto margin 居中,较高的图片扩展共享滚动区的纵向范围(侧边栏预览打磨)。渲染器不提供缩放或拖拽平移。SVG 标记绝不进入应用 DOM 或 iframe,因此脚本保持不可执行,也无法访问父页面。替换 HTML 或图片时会撤销其根 Blob URL。
PDF.js 官方 TextLayerBuilder 在适配宽度的 canvas 上负责选择边界和复制规范化,共享页面清理,并使用由组件拥有的 resize observer。响应式尺寸适配使用独立的 CSS scale 属性,与 PDF.js 的页面旋转和平移变换组合。其内容结束标记和堆叠规则限制空白区域中的选择;换行高亮被抑制。逐页取消使用 builder 的清理操作,而不 abort 第一页的信号,因为官方选择监听器跨页面共享。
考虑过的替代方案
通过预览元数据中的回调加载转换内容。 这种回调会让共享文件 store 同时持有源文件字节和格式专属的转换结果。由渲染器自行加载可将转换缓存、错误和字体元数据留在 Office,同时保留共享文件身份和工具栏控件。定义声明 loading: 'renderer';正文接收与格式无关的加载 revision,并将读取与 store 更新交给其私有注入 face。face 向共享 owner 报告已展示的源版本或加载失败。重新加载或替换实现会增加 revision,过期报告会被忽略,正文在替换或卸载时取消待处理工作。Office 在 tab 生命周期内保留已完成的内容,并组合自己的 PDF 子 slot。
把方法挂到 Iterator 或其值上。 这会混淆观察与命令,并在数据帧中重复能力身份。帧携带数据和失败;显式 Preview RPC 回调负责读取。
核心公开投影工厂,或在 open 内做同样的组装。 分开的流值、operations 组合与公开接口增加了组装步骤,没有另一个当前消费方需要它。Preview 的共享 RPC 适配已让渲染器无需处理 Session 寻址和字节传输。Resource 不提供与提供方无关的命令接口,也不提供绑定于打开实例的命令生命周期;增加任一种都需要文件预览之外的消费方证据。
把 UI Session 作为额外 Resource 身份,或由首个持有者、当前选择决定授权。 保留的 tab 可以属于不同于当前选择的 Session,UI 所在位置也不能标识地址指向的文件。将所需 Session 编入文件地址,既保留 Host 授权,也让同地址的所有读者共享观察。
让所有资源提供文件读取方法。 Chat 与终端资源的数据和操作语义各自独立,只有观察的注册和生命周期是共用机制。
预览资源包装层、内容 Session 或第二个资源 Hook。 这些方案重复了 Resource 和 Workspace Files 已提供的寻址、取消、订阅和归属。加载策略属于预览所有者。
本地服务器、虚拟主机或 file: iframe。 这些方案需要额外托管或文件系统权限。预览面向静态生成页面,而非完整应用运行时;模块、动态文件系统请求和任意嵌套资源图不在支持范围内。
清理 SVG 后放入应用 DOM 或 iframe。 sanitizer 会增加第二套 SVG parser 和一套持续演进的主动内容策略,之后仍要把不可信标记放进可交互文档。<img> 静态图片上下文保留浏览器原生 SVG 渲染与固有尺寸,同时不给标记一个能运行脚本的 DOM。
影响
替换渲染器不需要改变 Tab 或文件协议。全文格式承担有上限的整文件内存成本,PDF 增加随包发布的 Worker、字体和解码器字节。格式选择和查看状态仅属于当前页面,不是持久 Session 数据。Preview 独立于元数据观察,拥有 RPC 取消和原生缓冲区。tab 保留读取版本及读取开始时捕获的观察版本;刷新它既不丢弃其他 tab 的内容,也不清除其变更提示。文件读取仍非事务,不透明版本只比较相等性、不排序。录制的浏览器场景 覆盖共用工具栏、增量文本、隔离的 HTML 依赖、按宽度适配的位图与 SVG 渲染、不可执行的 SVG 脚本,以及惰性连续 PDF Worker 渲染。