frond-js 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/LICENSE +21 -0
  2. package/README.en.md +247 -0
  3. package/README.md +211 -0
  4. package/dist/abort-BY8vBk0v.d.cts +99 -0
  5. package/dist/abort-BY8vBk0v.d.ts +99 -0
  6. package/dist/adapter-3G46J3CA.cjs +503 -0
  7. package/dist/adapter-3ONQWJVQ.js +501 -0
  8. package/dist/adapter-55QHWRSE.js +124 -0
  9. package/dist/adapter-DF34GBWJ.cjs +19 -0
  10. package/dist/adapter-EXTNILTC.cjs +126 -0
  11. package/dist/adapter-GOFC7TMC.js +284 -0
  12. package/dist/adapter-LRJTQ47I.cjs +286 -0
  13. package/dist/adapter-TWWZML4A.js +17 -0
  14. package/dist/adapter-ZM5FQTJT.js +1415 -0
  15. package/dist/adapter-ZRNSDQUV.cjs +1417 -0
  16. package/dist/chunk-2SUG7YFZ.cjs +108 -0
  17. package/dist/chunk-B2L2YVXD.js +89 -0
  18. package/dist/chunk-D4KWSEZD.js +393 -0
  19. package/dist/chunk-EZTIZO6R.cjs +430 -0
  20. package/dist/chunk-G7DLWGBW.cjs +103 -0
  21. package/dist/chunk-GTGLDLJD.cjs +479 -0
  22. package/dist/chunk-IIV6VIUJ.cjs +83 -0
  23. package/dist/chunk-JDTHZQUK.js +102 -0
  24. package/dist/chunk-JJVXT3AC.js +30 -0
  25. package/dist/chunk-LIXRYQL2.js +473 -0
  26. package/dist/chunk-NABYHI6X.cjs +400 -0
  27. package/dist/chunk-SJVOYNTF.js +425 -0
  28. package/dist/chunk-SLI2YL25.cjs +252 -0
  29. package/dist/chunk-U4264IQH.js +78 -0
  30. package/dist/chunk-UY2YRCFC.js +250 -0
  31. package/dist/chunk-WCZTTQ7Z.cjs +32 -0
  32. package/dist/core/index.cjs +162 -0
  33. package/dist/core/index.d.cts +321 -0
  34. package/dist/core/index.d.ts +321 -0
  35. package/dist/core/index.js +49 -0
  36. package/dist/default-DRLIJX73.js +1183 -0
  37. package/dist/default-UK52WOO5.cjs +1192 -0
  38. package/dist/formats/epub/index.cjs +29 -0
  39. package/dist/formats/epub/index.d.cts +286 -0
  40. package/dist/formats/epub/index.d.ts +286 -0
  41. package/dist/formats/epub/index.js +11 -0
  42. package/dist/index.cjs +655 -0
  43. package/dist/index.d.cts +335 -0
  44. package/dist/index.d.ts +335 -0
  45. package/dist/index.js +600 -0
  46. package/dist/render/index.cjs +2 -0
  47. package/dist/render/index.d.cts +116 -0
  48. package/dist/render/index.d.ts +116 -0
  49. package/dist/render/index.js +1 -0
  50. package/dist/types-B76GOMxj.d.ts +129 -0
  51. package/dist/types-B7mslPBY.d.cts +166 -0
  52. package/dist/types-B7mslPBY.d.ts +166 -0
  53. package/dist/types-BH88rUYt.d.cts +129 -0
  54. package/dist/types-C-5eHSRH.d.ts +379 -0
  55. package/dist/types-CPUqTEPW.d.cts +379 -0
  56. package/dist/types-DQYmArgv.d.cts +17 -0
  57. package/dist/types-DQYmArgv.d.ts +17 -0
  58. package/package.json +115 -0
@@ -0,0 +1,116 @@
1
+ import { C as Cfi } from '../types-B7mslPBY.cjs';
2
+ import { O as OperationOptions } from '../types-DQYmArgv.cjs';
3
+
4
+ /** 一次渲染请求的目标位置。 */
5
+ interface RenderLocation {
6
+ /** 资源在容器内的相对路径。 */
7
+ readonly href: string;
8
+ /**
9
+ * 该位置对应的 **CFI**(章内位置)。**可选** —— T2.7 收窄。
10
+ *
11
+ * 收窄的理由:**章级跳转本来就没有章内位置**。`Reader` 的 `next` / `prev` /
12
+ * `goTo(spine 索引)` 只知道「第几章」,而 `core/cfi` 只提供字符串层的
13
+ * parse / serialize / collapse,**没有**「spine 索引 → CFI」的构造器。
14
+ *
15
+ * 合成的办法只有两种,都不能接受:① 在 `reader/` 里假定
16
+ * `epubcfi(/6/{2n}!/4)` 的前缀段 —— 把 EPUB 结构知识塞进编排层,且
17
+ * `cfi-locate.ts` 只解析**最后一段**,那段前缀**没有任何消费者会校验**;
18
+ * ② 只写 `epubcfi(/4)` —— 等于声称它是从 package 文档根出发的路径。
19
+ * 两者都是「假装知道」,而 `docs/PROMPT.md` 的纪律要求**如实表达**。
20
+ *
21
+ * 缺省时渲染层落到**章首**(`pageOfTarget` 直接返回 0)。
22
+ * 有章内位置时(`open({ initialLocation })` / `goTo(cfi)`)照常传入。
23
+ */
24
+ readonly cfi?: Cfi;
25
+ /**
26
+ * 章内**锚点**:内容文档内的**元素 id**(不含 `#`)。可选。
27
+ *
28
+ * ## 与 `cfi` 的分工(T3.2b ③c 新增,ADR-0011 修订)
29
+ *
30
+ * | 字段 | 是什么 | 来源 |
31
+ * | ---- | ------ | ---- |
32
+ * | `cfi` | **结构路径**(精确到字符偏移) | `goTo(CFI)` / `initialLocation` |
33
+ * | `fragment` | **元素 id**(粗粒度,只到元素) | 目录项(`TocItem.fragment`) |
34
+ *
35
+ * 二者**语义上互斥**:目录项的 `fragment` 要么是元素 id、要么本身是 CFI 字符串,
36
+ * 解析方(`reader/navigation.ts` 的 `locationForTocItem`)会把它归到**其中一个**字段,
37
+ * **不会同时给两个**。同时给出时**以 `cfi` 为准**(它是更精确的那一个)——
38
+ * 这是一条**优先级规则**,不是错误(两者都合法,只是精度不同)。
39
+ *
40
+ * ## 只接受元素 id —— 另外两种形态由上层先转换
41
+ *
42
+ * `TocItem.fragment` 有**三种**语义(元素 id / EPUB CFI / MOBI 字节偏移,
43
+ * 见 `core/model/types.ts` 的 `TocItem.fragment`)。本字段**只承载第一种**:
44
+ *
45
+ * - **CFI 字符串** → 由 `locationForTocItem` 解析进 `cfi`,**不会**流到这里;
46
+ * - **MOBI 的字节偏移** → **当前不可定位**,会被当作元素 id 去查、必然落空
47
+ * ⇒ 回退章首。⚠️ **这是如实登记的已知缺口**(不是已覆盖),
48
+ * 修法是「改写正文插 `id`」的独立批次;登记见 `docs/API.md` §14.4。
49
+ *
50
+ * ## 落空是**正常数据情况**,回退章首
51
+ *
52
+ * 与 `cfi` 落空**同族**(书换版本 / 内容被清洗 / 锚点被删),不是编程错误:
53
+ * 渲染层回退到**章首**,而不是让整次翻页失败。口径与 `cfi-locate.ts` 的模块头一致
54
+ * —— 那里是「CFI 可能过期」,这里是「元素 id 可能已不存在」。
55
+ *
56
+ * 缺省时渲染层落到**章首**(与 `cfi` 缺省同一落点,但**理由不同**:
57
+ * 这里是「本来就没有章内位置」,`cfi` 落空是「有过但已失效」)。
58
+ */
59
+ readonly fragment?: string;
60
+ /** spine 中的索引。 */
61
+ readonly index: number;
62
+ }
63
+ /**
64
+ * 渲染层的**宿主抽象**(T2.1 定稿,ADR-0011,REQ-RENDER-003)。
65
+ *
66
+ * **为什么不是直接收 `HTMLElement`**:渲染层实际需要的只有两件事 ——
67
+ * 一个可以创建自己视图节点的挂载点,以及创建 iframe 等节点所需的文档。
68
+ * 把这两件事**具名**声明出来,宿主的最小要求一目了然。
69
+ *
70
+ * 更关键的是:契约一旦写成裸 `HTMLElement`,「渲染层只能作用于某个元素」
71
+ * 就被写死了 —— 这与 FI-007 要修的「基线硬编码 `<foliate-view>` 自定义元素」
72
+ * 是同一类耦合。具名结构让**需要什么**成为契约的内容,而不是把
73
+ * **宿主是什么**写进契约。
74
+ *
75
+ * ⚠️ 契约**刻意只声明这两个字段**:尺寸测量、滚动位置、样式等都由渲染层
76
+ * 通过 `element` 自行完成,不进契约 —— 契约越大,宿主实现负担越重、可替换性越差。
77
+ */
78
+ interface RenderHost {
79
+ /** 挂载点。渲染层会在其中创建自己的视图节点。 */
80
+ readonly element: HTMLElement;
81
+ /** 该宿主所属的文档(用于创建 iframe 等节点)。 */
82
+ readonly ownerDocument: Document;
83
+ }
84
+ /**
85
+ * 渲染层接口(REQ-RENDER-003)。
86
+ *
87
+ * 设计意图:宿主可整体替换渲染策略(分页 / 滚动 / 自定义排版),
88
+ * 而无需触碰 core 的解析与建模逻辑。
89
+ *
90
+ * 实现者必须满足的契约(T2.1 定稿):
91
+ *
92
+ * 1. **取消(H6 / H7)**:`options.signal` 已 abort 时,`mount` / `render` 必须抛
93
+ * `AbortError`,且**不得留下半成品视图**(已创建的 iframe / `blob:` URL / 事件监听
94
+ * 都必须释放)。
95
+ * 2. **`unmount` 幂等且必须释放资源**:重复调用不抛错;必须 `revokeObjectURL`、
96
+ * 移除 iframe、解绑事件。
97
+ *
98
+ * > 更细的行为义务(`mount` 重复调用是否叠加视图、未挂载即 `render` 如何失败)
99
+ * > 留待默认实现落地时(T2.2+)在实现处钉死 —— 契约现在不写,是因为
100
+ * > **尚未有实现去兑现它**,先写下来只会变成无人验证的纸面条文。
101
+ */
102
+ interface Renderer {
103
+ /**
104
+ * 挂载到宿主。
105
+ *
106
+ * `host` 在 P0 是 `unknown` 占位;**T2.1 收窄为 {@link RenderHost}** ——
107
+ * 刻意**不是**裸 `HTMLElement`,理由见 {@link RenderHost}。
108
+ */
109
+ mount(host: RenderHost, options?: OperationOptions): Promise<void>;
110
+ /** 渲染指定位置。 */
111
+ render(target: RenderLocation, options?: OperationOptions): Promise<void>;
112
+ /** 卸载并释放全部资源。必须幂等。 */
113
+ unmount(): void;
114
+ }
115
+
116
+ export type { RenderHost, RenderLocation, Renderer };
@@ -0,0 +1,116 @@
1
+ import { C as Cfi } from '../types-B7mslPBY.js';
2
+ import { O as OperationOptions } from '../types-DQYmArgv.js';
3
+
4
+ /** 一次渲染请求的目标位置。 */
5
+ interface RenderLocation {
6
+ /** 资源在容器内的相对路径。 */
7
+ readonly href: string;
8
+ /**
9
+ * 该位置对应的 **CFI**(章内位置)。**可选** —— T2.7 收窄。
10
+ *
11
+ * 收窄的理由:**章级跳转本来就没有章内位置**。`Reader` 的 `next` / `prev` /
12
+ * `goTo(spine 索引)` 只知道「第几章」,而 `core/cfi` 只提供字符串层的
13
+ * parse / serialize / collapse,**没有**「spine 索引 → CFI」的构造器。
14
+ *
15
+ * 合成的办法只有两种,都不能接受:① 在 `reader/` 里假定
16
+ * `epubcfi(/6/{2n}!/4)` 的前缀段 —— 把 EPUB 结构知识塞进编排层,且
17
+ * `cfi-locate.ts` 只解析**最后一段**,那段前缀**没有任何消费者会校验**;
18
+ * ② 只写 `epubcfi(/4)` —— 等于声称它是从 package 文档根出发的路径。
19
+ * 两者都是「假装知道」,而 `docs/PROMPT.md` 的纪律要求**如实表达**。
20
+ *
21
+ * 缺省时渲染层落到**章首**(`pageOfTarget` 直接返回 0)。
22
+ * 有章内位置时(`open({ initialLocation })` / `goTo(cfi)`)照常传入。
23
+ */
24
+ readonly cfi?: Cfi;
25
+ /**
26
+ * 章内**锚点**:内容文档内的**元素 id**(不含 `#`)。可选。
27
+ *
28
+ * ## 与 `cfi` 的分工(T3.2b ③c 新增,ADR-0011 修订)
29
+ *
30
+ * | 字段 | 是什么 | 来源 |
31
+ * | ---- | ------ | ---- |
32
+ * | `cfi` | **结构路径**(精确到字符偏移) | `goTo(CFI)` / `initialLocation` |
33
+ * | `fragment` | **元素 id**(粗粒度,只到元素) | 目录项(`TocItem.fragment`) |
34
+ *
35
+ * 二者**语义上互斥**:目录项的 `fragment` 要么是元素 id、要么本身是 CFI 字符串,
36
+ * 解析方(`reader/navigation.ts` 的 `locationForTocItem`)会把它归到**其中一个**字段,
37
+ * **不会同时给两个**。同时给出时**以 `cfi` 为准**(它是更精确的那一个)——
38
+ * 这是一条**优先级规则**,不是错误(两者都合法,只是精度不同)。
39
+ *
40
+ * ## 只接受元素 id —— 另外两种形态由上层先转换
41
+ *
42
+ * `TocItem.fragment` 有**三种**语义(元素 id / EPUB CFI / MOBI 字节偏移,
43
+ * 见 `core/model/types.ts` 的 `TocItem.fragment`)。本字段**只承载第一种**:
44
+ *
45
+ * - **CFI 字符串** → 由 `locationForTocItem` 解析进 `cfi`,**不会**流到这里;
46
+ * - **MOBI 的字节偏移** → **当前不可定位**,会被当作元素 id 去查、必然落空
47
+ * ⇒ 回退章首。⚠️ **这是如实登记的已知缺口**(不是已覆盖),
48
+ * 修法是「改写正文插 `id`」的独立批次;登记见 `docs/API.md` §14.4。
49
+ *
50
+ * ## 落空是**正常数据情况**,回退章首
51
+ *
52
+ * 与 `cfi` 落空**同族**(书换版本 / 内容被清洗 / 锚点被删),不是编程错误:
53
+ * 渲染层回退到**章首**,而不是让整次翻页失败。口径与 `cfi-locate.ts` 的模块头一致
54
+ * —— 那里是「CFI 可能过期」,这里是「元素 id 可能已不存在」。
55
+ *
56
+ * 缺省时渲染层落到**章首**(与 `cfi` 缺省同一落点,但**理由不同**:
57
+ * 这里是「本来就没有章内位置」,`cfi` 落空是「有过但已失效」)。
58
+ */
59
+ readonly fragment?: string;
60
+ /** spine 中的索引。 */
61
+ readonly index: number;
62
+ }
63
+ /**
64
+ * 渲染层的**宿主抽象**(T2.1 定稿,ADR-0011,REQ-RENDER-003)。
65
+ *
66
+ * **为什么不是直接收 `HTMLElement`**:渲染层实际需要的只有两件事 ——
67
+ * 一个可以创建自己视图节点的挂载点,以及创建 iframe 等节点所需的文档。
68
+ * 把这两件事**具名**声明出来,宿主的最小要求一目了然。
69
+ *
70
+ * 更关键的是:契约一旦写成裸 `HTMLElement`,「渲染层只能作用于某个元素」
71
+ * 就被写死了 —— 这与 FI-007 要修的「基线硬编码 `<foliate-view>` 自定义元素」
72
+ * 是同一类耦合。具名结构让**需要什么**成为契约的内容,而不是把
73
+ * **宿主是什么**写进契约。
74
+ *
75
+ * ⚠️ 契约**刻意只声明这两个字段**:尺寸测量、滚动位置、样式等都由渲染层
76
+ * 通过 `element` 自行完成,不进契约 —— 契约越大,宿主实现负担越重、可替换性越差。
77
+ */
78
+ interface RenderHost {
79
+ /** 挂载点。渲染层会在其中创建自己的视图节点。 */
80
+ readonly element: HTMLElement;
81
+ /** 该宿主所属的文档(用于创建 iframe 等节点)。 */
82
+ readonly ownerDocument: Document;
83
+ }
84
+ /**
85
+ * 渲染层接口(REQ-RENDER-003)。
86
+ *
87
+ * 设计意图:宿主可整体替换渲染策略(分页 / 滚动 / 自定义排版),
88
+ * 而无需触碰 core 的解析与建模逻辑。
89
+ *
90
+ * 实现者必须满足的契约(T2.1 定稿):
91
+ *
92
+ * 1. **取消(H6 / H7)**:`options.signal` 已 abort 时,`mount` / `render` 必须抛
93
+ * `AbortError`,且**不得留下半成品视图**(已创建的 iframe / `blob:` URL / 事件监听
94
+ * 都必须释放)。
95
+ * 2. **`unmount` 幂等且必须释放资源**:重复调用不抛错;必须 `revokeObjectURL`、
96
+ * 移除 iframe、解绑事件。
97
+ *
98
+ * > 更细的行为义务(`mount` 重复调用是否叠加视图、未挂载即 `render` 如何失败)
99
+ * > 留待默认实现落地时(T2.2+)在实现处钉死 —— 契约现在不写,是因为
100
+ * > **尚未有实现去兑现它**,先写下来只会变成无人验证的纸面条文。
101
+ */
102
+ interface Renderer {
103
+ /**
104
+ * 挂载到宿主。
105
+ *
106
+ * `host` 在 P0 是 `unknown` 占位;**T2.1 收窄为 {@link RenderHost}** ——
107
+ * 刻意**不是**裸 `HTMLElement`,理由见 {@link RenderHost}。
108
+ */
109
+ mount(host: RenderHost, options?: OperationOptions): Promise<void>;
110
+ /** 渲染指定位置。 */
111
+ render(target: RenderLocation, options?: OperationOptions): Promise<void>;
112
+ /** 卸载并释放全部资源。必须幂等。 */
113
+ unmount(): void;
114
+ }
115
+
116
+ export type { RenderHost, RenderLocation, Renderer };
@@ -0,0 +1 @@
1
+
@@ -0,0 +1,129 @@
1
+ import { O as OperationOptions } from './types-DQYmArgv.js';
2
+
3
+ /**
4
+ * ZIP 读取的类型定义(ADR-0007)。
5
+ *
6
+ * 设计约束:
7
+ * - **零依赖**:只用 `DataView` / `TextDecoder` / `DecompressionStream`,不用 `@zip.js/zip.js`
8
+ * - **零 DOM**:可在 Node / Worker / 浏览器一致运行(REQ-CORE-001)
9
+ * - **只读**:本模块不写入、不修改 ZIP,也不解压到磁盘
10
+ *
11
+ * 明确不支持(ADR-0007 的已知边界):
12
+ * - ZIP64(>4 GB 条目)—— 检测到即抛 `FormatError`,**不静默按 32 位解析**
13
+ * - 加密条目 —— 属非目标(不做 DRM 解密)
14
+ * - 流式随机读 —— 当前一次性读入内存
15
+ */
16
+
17
+ /**
18
+ * ZIP 压缩方法。
19
+ *
20
+ * 仅这两种在 EPUB 中合法(OCF 规范要求 `mimetype` 必须为 stored)。
21
+ * 其他方法(如 12 = bzip2、14 = LZMA)一律抛 `FormatError`。
22
+ */
23
+ type ZipCompressionMethod = 0 | 8;
24
+ /** ZIP 中央目录中的一个条目。 */
25
+ interface ZipEntry {
26
+ /**
27
+ * 条目名,已按 UTF-8 解码,并**归一化到相对归档根的规范形**(FI-X22)。
28
+ *
29
+ * 规范化的规则与 `normalizeArchivePath`(`core/archive-path.ts`)**同一个函数**
30
+ * (该函数即查询键的产出者):`\` → `/`、折掉 `.` 段与重复 `/`、去掉前导 `/`。因此
31
+ * `entries[].name`、`entry()` / `read()` 的索引键、EPUB / CBZ / FB2 解析出的路径
32
+ * 四处**不可能分叉** —— 曾分叉的形态是归档里存 `./META-INF/container.xml`
33
+ * 时,条目名列得出、读不出,且探测层直接判不出格式。
34
+ *
35
+ * 以 `/` 结尾表示目录条目 —— 那个结尾斜杠**只在目录条目上保留**:
36
+ * `probeZipContainer` 与 `collectCbzPages` 都按 `endsWith('/')` 排除目录,
37
+ * 把它一并折掉会让 `book.fb2/` 这种目录被当成正文。
38
+ *
39
+ * ⚠️ 归一化后**越出归档根**、**含控制字符**、或**规范化后为空**的名字不会出现在这里 ——
40
+ * 那三种在 `parseZip` 阶段就抛 `FormatError`(不留「存进去但取不出」的哑条目)。
41
+ */
42
+ readonly name: string;
43
+ /** 压缩方法。 */
44
+ readonly method: ZipCompressionMethod;
45
+ /** 压缩后字节数(取自中央目录)。 */
46
+ readonly compressedSize: number;
47
+ /** 解压后字节数(取自中央目录)。 */
48
+ readonly uncompressedSize: number;
49
+ /** 未压缩数据的 CRC-32 校验和。 */
50
+ readonly crc32: number;
51
+ /** 是否为目录条目(名字以 `/` 结尾)。目录条目无内容。 */
52
+ readonly isDirectory: boolean;
53
+ }
54
+ /** {@link parseZip} 的选项。 */
55
+ interface ParseZipOptions {
56
+ /**
57
+ * 源标识,用于错误定位(REQ-EPUB-007)。
58
+ *
59
+ * 通常传文件名或 URL。失败时该值会出现在错误 message 中。
60
+ */
61
+ readonly source?: string;
62
+ /**
63
+ * **单个条目解压产物**的字节上限;超限即中止解压并抛 `ParseError`。
64
+ *
65
+ * 存在的理由:归档里的字节是不可信的,而 deflate 是**放大**通道 —— 实测 199 KB 的
66
+ * 压缩流可以解出 200 MB。中央目录声明的 `uncompressedSize` **也是文件说了算的数**,
67
+ * 拿它当校验依据只能事后拦(先物化、再比对),拦不住放大本身。
68
+ * 因此这里有两道:声明值的前置检查(拦「老实交代的大数」)+ 边解边数(拦「谎报小值」)。
69
+ *
70
+ * `stored`(未压缩)条目不受此上限约束 —— 其产物尺寸恒等于压缩尺寸,
71
+ * 而压缩尺寸已由「数据区间必须落在归档内」钉住,放大率天然是 1。
72
+ *
73
+ * @defaultValue 67108864(64 MiB)—— 本仓最大的合法单条目是 3.34 MB 的合成单章,
74
+ * 真实书最大章节约 2.9 MB,约 20 倍余量。
75
+ */
76
+ readonly maxEntrySizeBytes?: number;
77
+ }
78
+ /**
79
+ * 已解析的 ZIP 归档(只读视图)。
80
+ *
81
+ * 通过 {@link parseZip} 创建。实例持有原始字节,因此不要在归档存活期间修改
82
+ * 传入 `parseZip` 的那个 `Uint8Array`。
83
+ */
84
+ interface ZipArchive {
85
+ /** 全部条目,按中央目录顺序。包含目录条目;**原文**重复名全部保留。 */
86
+ readonly entries: readonly ZipEntry[];
87
+ /**
88
+ * 按名字查条目。
89
+ *
90
+ * 查询键先走与条目名**同一个**归一化函数(FI-X22),于是 `a.png`、`./a.png`、
91
+ * `/a.png`、`a/./b.png` 这类写法指向同一个条目;目录条目的两个键
92
+ * (`images` 与 `images/`)也命中同一条。
93
+ *
94
+ * 原文重名时返回**最后一个**(ZIP 惯例:后写覆盖)。
95
+ * 「归一化后同名而原文不同」的两条不会走到这里 —— `parseZip` 已判红。
96
+ *
97
+ * @returns 匹配条目;不存在、**或查询键本身不是合法的归档内路径**(`''`、`'../x'`)时
98
+ * 返回 `undefined`。本方法**不抛错**。
99
+ */
100
+ entry(name: string): ZipEntry | undefined;
101
+ /**
102
+ * 读取并解压条目。
103
+ *
104
+ * - 返回**独立副本**,调用方修改它不会影响归档内的原始字节
105
+ * - 解压后校验 CRC-32 与长度,不匹配抛 `ParseError`
106
+ * - 对目录条目抛 `FormatError`
107
+ *
108
+ * @param name - 条目名(**先归一化再查**,规则见 {@link ZipEntry.name};
109
+ * 大小写敏感,与 ZIP 内一致)。
110
+ * @param options - 取消信号。
111
+ * @throws {FormatError} 条目不存在、是目录、或压缩方法不支持。
112
+ * 查询键非法(`''`、`'../x'`)按**条目不存在**处理 —— 归档侧已保证不存在这类名字。
113
+ * @throws {ParseError} 数据损坏(CRC-32 或长度不匹配),**或解压产物超过
114
+ * `parseZip` 的 `maxEntrySizeBytes`**。
115
+ * @throws {AbortError} 操作被取消。
116
+ */
117
+ read(name: string, options?: OperationOptions): Promise<Uint8Array>;
118
+ /**
119
+ * 读取条目并按 UTF-8 解码为文本。
120
+ *
121
+ * 用于 `META-INF/container.xml`、OPF、NAV 等 XML 资源。
122
+ * BOM 会被自动去除。
123
+ *
124
+ * @throws 同 {@link ZipArchive.read}。
125
+ */
126
+ readText(name: string, options?: OperationOptions): Promise<string>;
127
+ }
128
+
129
+ export type { ParseZipOptions as P, ZipArchive as Z, ZipCompressionMethod as a, ZipEntry as b };
@@ -0,0 +1,166 @@
1
+ /**
2
+ * EPUB CFI(Canonical Fragment Identifier)类型契约。
3
+ *
4
+ * 设计依据(T1.7 决策,2026-09-14):
5
+ *
6
+ * - **判别联合**:单点与范围在类型上分开,`kind` 可穷尽判别。
7
+ * 范围的两端**恒非空** —— 省略 start 时显式归一,而不是留 `undefined`。
8
+ * 这正是基线 FI-010 的缺陷形状:foliate-js 的 `splitAt` 在分隔符只有 1 个时返回 2 段,
9
+ * 而 `parse` 按 3 段解构,导致 `end` 恒为 `undefined`、`collapse` 抛错或产出错误结果
10
+ * (见 `docs/KNOWN-ISSUES.md` FI-010 与 `scripts/refs/fi010-cfi-range.mjs`)。
11
+ * - **零 DOM**:本模块只做「字符串 ↔ 路径结构」,不触碰 `NodeFilter` 与真实节点
12
+ * (`docs/ARCHITECTURE.md` §3.1 / `docs/KNOWN-ISSUES.md` §3.3.5)。
13
+ * `toRange` / `fromRange` / `toElement` 属渲染层,不在此处。
14
+ * - **不静默降级**:非法输入一律抛错,不得返回 `[[]]` 或 `index: null`
15
+ * (`docs/KNOWN-ISSUES.md` §3.3.3)。
16
+ *
17
+ * 语法依据:EPUB CFI 规范 EBNF(idpf.org)——
18
+ *
19
+ * ```ebnf
20
+ * fragment = "epubcfi(" , ( path , [ range ] ) , ")" ;
21
+ * path = step , local_path ;
22
+ * range = "," , local_path , "," , local_path ;
23
+ * step = "/" , integer , [ "[" , assertion , "]" ] ;
24
+ * offset = ( ":" , integer ) , [ "[" , assertion , "]" ] ;
25
+ * assertion = ( ( value , [ "," , value ] ) | ( "," , value ) | ( parameter ) ) { parameter } ;
26
+ * ```
27
+ *
28
+ * 注意两个容易被忽略的点:
29
+ *
30
+ * 1. **`range` 恒为 2 个逗号** —— 范围是「父路径 + 起始子路径 + 结束子路径」的三元组。
31
+ * 「省略 start」的合法写法是 `epubcfi(P,,E)`(中间留空),**不是** 1 个逗号。
32
+ * 2. **断言分两类且位置不同** —— 元素步(偶数 index)上的断言是 **ID 断言**;
33
+ * 字符偏移(`:n`)后的断言是 **文本位置断言**。故 `id` 在 {@link CfiStep} 上,
34
+ * 而 `text` / `textAfter` 在 {@link CfiPoint} 上。
35
+ */
36
+ /**
37
+ * 单个路径步骤(step)。
38
+ *
39
+ * CFI 路径形如 `/6/4[chap01ref]!/4[body01]/10[para05]/2/1:3[yyy]`,
40
+ * 按 `/` 切分后每个片段即一个 step。
41
+ */
42
+ interface CfiStep {
43
+ /**
44
+ * 步骤索引。
45
+ *
46
+ * **奇偶有语义**:偶数指向子元素(含 `0` 与末尾虚拟元素),奇数指向字符数据块。
47
+ * 解析器不校验奇偶是否与真实文档树自洽 —— 那需要真实节点,属渲染层职责。
48
+ */
49
+ readonly index: number;
50
+ /**
51
+ * ID 断言,来自元素步的 `[chap01ref]`。
52
+ *
53
+ * 规范定位:出现在元素 step 之后,用于在位置漂移时按 ID 纠正目标位置。
54
+ * 排序/比较前必须逻辑上剥离(本模块不实现比较,仅保留原始信息)。
55
+ */
56
+ readonly id?: string;
57
+ /**
58
+ * side bias,来自参数 `;s=b`(before)或 `;s=a`(after)。
59
+ *
60
+ * 规范定位:**参数形式**,因此不参与 CFI 比较。
61
+ * 其他取值(非 `b` / `a`)抛 `FormatError`。
62
+ */
63
+ readonly side?: 'before' | 'after';
64
+ }
65
+ /**
66
+ * 一段路径:相邻两个 `!` 之间的部分。
67
+ *
68
+ * `!` 在 CFI 中表示 **indirection**(跳转进被引用的另一个文档),
69
+ * 因此一个 CFI 的路径是「段的序列」而非单一扁平数组。
70
+ */
71
+ interface CfiPath {
72
+ /** 该段内的步骤,按出现顺序。至少一个。 */
73
+ readonly steps: readonly CfiStep[];
74
+ }
75
+ /**
76
+ * 单点 CFI。
77
+ *
78
+ * 例:`epubcfi(/6/4!/4/2/2:0)`
79
+ */
80
+ interface CfiPoint {
81
+ readonly kind: 'point';
82
+ /** 按 `!` 分段的路径,至少一段。 */
83
+ readonly paths: readonly CfiPath[];
84
+ /** 字符偏移,来自末步之后的 `:3`。基于 UTF-16 码元,从 0 开始。 */
85
+ readonly offset?: number;
86
+ /**
87
+ * 文本断言的前导值,来自偏移后的 `[yyy]`。
88
+ *
89
+ * 表示期望出现在遇到点**之前**紧邻的子串(空白折叠后比较)。
90
+ */
91
+ readonly text?: string;
92
+ /**
93
+ * 文本断言的尾随值,来自 `[xx,y]` 或 `[,y]`。
94
+ *
95
+ * 表示期望出现在遇到点**之后**紧邻的子串。仅指定尾随值时写作 `[,y]`。
96
+ */
97
+ readonly textAfter?: string;
98
+ /** side bias,来自偏移后断言里的 `;s=b` / `;s=a`。 */
99
+ readonly side?: 'before' | 'after';
100
+ /**
101
+ * 规范化字符串,**含 `epubcfi()` 前缀**。
102
+ *
103
+ * 与 `serializeCfi()` 的返回值一致,便于下游直接使用而不必再序列化一次。
104
+ */
105
+ readonly value: string;
106
+ }
107
+ /**
108
+ * 范围 CFI。
109
+ *
110
+ * 规范(`range = "," , local_path , "," , local_path`)的两种合法写法:
111
+ *
112
+ * | 写法 | 语义 | `parent` |
113
+ * | ---------------- | ------------------------------------------------------------ | ---------- |
114
+ * | `epubcfi(P,S,E)` | 子路径拼接到父路径:`start = P+S`、`end = P+E` | 非 `null` |
115
+ * | `epubcfi(P,,E)` | 起始子路径留空 → `start = P`(退化为父路径位置)、`end = P+E` | 非 `null` |
116
+ *
117
+ * 另**有意接受一种简写**(登记于 `docs/API.md`「有意偏离规范之处」):
118
+ *
119
+ * | 写法 | 本项目语义 | `parent` |
120
+ * | -------------- | ------------------------------------------------- | -------- |
121
+ * | `epubcfi(A,B)` | 仅 1 个逗号 → `start = A`、`end = B`(两条独立绝对路径) | `null` |
122
+ *
123
+ * 简写形式按 EBNF 属**畸形输入**(`docs/KNOWN-ISSUES.md` FI-010 取证输入澄清)。
124
+ * 其语义定为「两条独立绝对路径」而非字面的 `epubcfi(A,,B)`,理由是后者会产出 `A+B`
125
+ * 的退化路径 —— 对 `scripts/refs/fi010-cfi-range.mjs` 的输入即 `/6/4!/4/2/2/6/4!/4/2/4`,
126
+ * 恰是基线错误输出的形状;而前者是唯一能让 `collapseCfi(range, true)` 产出有意义结果的解释。
127
+ *
128
+ * **省略 start 的范围是本项目的重点覆盖场景** —— 基线在此静默产出垃圾(FI-010)。
129
+ */
130
+ interface CfiRange {
131
+ readonly kind: 'range';
132
+ /**
133
+ * 起点,**恒非空**。
134
+ *
135
+ * 规范形态下由「父路径 + 起始子路径」拼接而成;简写形式下即第一个路径。
136
+ * **始终是绝对路径** —— 消费方永远不需要自己做路径拼接。
137
+ */
138
+ readonly start: CfiPoint;
139
+ /** 终点,**恒非空**。与 `start` 口径一致(始终是绝对路径)。 */
140
+ readonly end: CfiPoint;
141
+ /**
142
+ * 规范形态的共同父路径;**仅 1 逗号简写形式为 `null`**。
143
+ *
144
+ * 存在是为保留原始书写形式以便精确回写(round-trip):
145
+ * 非 `null` 时序列化输出 `epubcfi(P,S,E)` / `epubcfi(P,,E)`,
146
+ * `null` 时输出 `epubcfi(A,B)`。语义信息已完整包含在 `start` / `end` 中。
147
+ */
148
+ readonly parent: readonly CfiPath[] | null;
149
+ /** 规范化字符串,含 `epubcfi()` 前缀。 */
150
+ readonly value: string;
151
+ }
152
+ /**
153
+ * 解析后的 CFI。
154
+ *
155
+ * 用 `kind` 判别:
156
+ *
157
+ * ```ts
158
+ * const cfi = parseCfi(input);
159
+ * if (cfi.kind === 'range') {
160
+ * // cfi.start / cfi.end 恒可用,无需判空
161
+ * }
162
+ * ```
163
+ */
164
+ type Cfi = CfiPoint | CfiRange;
165
+
166
+ export type { Cfi as C, CfiPath as a, CfiPoint as b, CfiRange as c, CfiStep as d };