@embedpdf/engine-core 3.0.0-next.14 → 3.0.0-next.15
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.
- package/dist/AnnotationList-CkijRHet.d.cts +27163 -0
- package/dist/AnnotationList-CkijRHet.d.ts +27163 -0
- package/dist/Change-CKoxfTNY.js +889 -0
- package/dist/Change-CKoxfTNY.js.map +1 -0
- package/dist/Change-CjImsrN0.cjs +1020 -0
- package/dist/Change-CjImsrN0.cjs.map +1 -0
- package/dist/DocumentRenderService-BfFdFSwI.cjs +120 -0
- package/dist/DocumentRenderService-BfFdFSwI.cjs.map +1 -0
- package/dist/DocumentRenderService-DV3J9lMz.js +85 -0
- package/dist/DocumentRenderService-DV3J9lMz.js.map +1 -0
- package/dist/{DocumentSecurityService-Ca6EK3-3.d.ts → DocumentSecurityService-CU1M6Q7m.d.ts} +100 -112
- package/dist/{DocumentSecurityService-CIx1MT0T.d.cts → DocumentSecurityService-e6vp2de8.d.cts} +100 -112
- package/dist/LocalDocumentHandle-D02FMm-Z.cjs +53 -0
- package/dist/LocalDocumentHandle-D02FMm-Z.cjs.map +1 -0
- package/dist/LocalDocumentHandle-DAPdaOt_.js +30 -0
- package/dist/LocalDocumentHandle-DAPdaOt_.js.map +1 -0
- package/dist/LocalEngine-DFoBm4B3.d.cts +1565 -0
- package/dist/LocalEngine-DPriFujS.d.ts +1565 -0
- package/dist/PageInsertBlankInput-CXHAM7l6.cjs +17 -0
- package/dist/PageInsertBlankInput-CXHAM7l6.cjs.map +1 -0
- package/dist/PageInsertBlankInput-DhVYVJ_S.js +12 -0
- package/dist/PageInsertBlankInput-DhVYVJ_S.js.map +1 -0
- package/dist/authority-B3BklsB4.js +2239 -0
- package/dist/authority-B3BklsB4.js.map +1 -0
- package/dist/{viewport-BWeNHZmb.cjs → authority-DLMNR7ff.cjs} +820 -1647
- package/dist/authority-DLMNR7ff.cjs.map +1 -0
- package/dist/builders-CNj0POeY.js +161 -0
- package/dist/builders-CNj0POeY.js.map +1 -0
- package/dist/builders-Cgdp_YCn.cjs +184 -0
- package/dist/builders-Cgdp_YCn.cjs.map +1 -0
- package/dist/calibrate-7T3fnP8-.js +2404 -0
- package/dist/calibrate-7T3fnP8-.js.map +1 -0
- package/dist/calibrate-DJBXnhLE.cjs +2919 -0
- package/dist/calibrate-DJBXnhLE.cjs.map +1 -0
- package/dist/charmap-Cprti1ta.js +239 -0
- package/dist/charmap-Cprti1ta.js.map +1 -0
- package/dist/charmap-aYlefQP9.cjs +304 -0
- package/dist/charmap-aYlefQP9.cjs.map +1 -0
- package/dist/conformance.cjs +16466 -5247
- package/dist/conformance.cjs.map +1 -1
- package/dist/conformance.d.cts +924 -165
- package/dist/conformance.d.ts +924 -165
- package/dist/conformance.js +16429 -5252
- package/dist/conformance.js.map +1 -1
- package/dist/format-Bp3QFSH1.cjs +574 -0
- package/dist/format-Bp3QFSH1.cjs.map +1 -0
- package/dist/format-Dgg428Kb.js +461 -0
- package/dist/format-Dgg428Kb.js.map +1 -0
- package/dist/index.cjs +323 -116
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/dist/index.js +14 -5
- package/dist/pageRefs-B3ur7Ckm.cjs +51 -0
- package/dist/pageRefs-B3ur7Ckm.cjs.map +1 -0
- package/dist/pageRefs-DkbuxSCV.js +40 -0
- package/dist/pageRefs-DkbuxSCV.js.map +1 -0
- package/dist/public.cjs +57 -0
- package/dist/public.d.cts +5 -0
- package/dist/public.d.ts +5 -0
- package/dist/public.js +9 -0
- package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs +3646 -0
- package/dist/resolveAnnotationDraft-7Ke4nV7F.cjs.map +1 -0
- package/dist/resolveAnnotationDraft-CUpf0SMh.js +3167 -0
- package/dist/resolveAnnotationDraft-CUpf0SMh.js.map +1 -0
- package/dist/{resolver-inAVzsWt.js → resolver-CzxmD9Ck.js} +294 -144
- package/dist/resolver-CzxmD9Ck.js.map +1 -0
- package/dist/{resolver-CYVNV4hn.cjs → resolver-D2OCtE8F.cjs} +347 -143
- package/dist/resolver-D2OCtE8F.cjs.map +1 -0
- package/dist/runtime-Cl0v56mm.d.ts +2032 -0
- package/dist/runtime-p1WDzDo4.d.cts +2032 -0
- package/dist/runtime.cjs +726 -297
- package/dist/runtime.cjs.map +1 -1
- package/dist/runtime.d.cts +5 -1372
- package/dist/runtime.d.ts +5 -1372
- package/dist/runtime.js +396 -179
- package/dist/runtime.js.map +1 -1
- package/dist/schema-C-gU4vj7.cjs +3034 -0
- package/dist/schema-C-gU4vj7.cjs.map +1 -0
- package/dist/schema-DzlUNk38.js +2081 -0
- package/dist/schema-DzlUNk38.js.map +1 -0
- package/dist/schemas-Berf1swF.cjs +2481 -0
- package/dist/schemas-Berf1swF.cjs.map +1 -0
- package/dist/schemas-D6xOGjqg.js +1654 -0
- package/dist/schemas-D6xOGjqg.js.map +1 -0
- package/dist/subtype-CG4WpIRj.js +129 -0
- package/dist/subtype-CG4WpIRj.js.map +1 -0
- package/dist/subtype-aDt5TFmK.cjs +158 -0
- package/dist/subtype-aDt5TFmK.cjs.map +1 -0
- package/dist/validation.cjs +578 -0
- package/dist/validation.cjs.map +1 -0
- package/dist/validation.d.cts +174 -0
- package/dist/validation.d.ts +174 -0
- package/dist/validation.js +571 -0
- package/dist/validation.js.map +1 -0
- package/dist/value-BFS1R8ek.cjs +435 -0
- package/dist/value-BFS1R8ek.cjs.map +1 -0
- package/dist/value-Dhc8eYcx.js +340 -0
- package/dist/value-Dhc8eYcx.js.map +1 -0
- package/dist/viewport-BJW13EGZ.js +540 -0
- package/dist/viewport-BJW13EGZ.js.map +1 -0
- package/dist/viewport-DlL5Bxbg.cjs +671 -0
- package/dist/viewport-DlL5Bxbg.cjs.map +1 -0
- package/dist/wire.cjs +1051 -397
- package/dist/wire.cjs.map +1 -1
- package/dist/wire.d.cts +12403 -790
- package/dist/wire.d.ts +12403 -790
- package/dist/wire.js +821 -242
- package/dist/wire.js.map +1 -1
- package/package.json +10 -1
- package/dist/AbortError-DwuHPK_I.js +0 -27
- package/dist/AbortError-DwuHPK_I.js.map +0 -1
- package/dist/AbortError-P0RU0Mgg.cjs +0 -38
- package/dist/AbortError-P0RU0Mgg.cjs.map +0 -1
- package/dist/AnnotationListSnapshot-D9R5n2Kf.d.cts +0 -7101
- package/dist/AnnotationListSnapshot-D9R5n2Kf.d.ts +0 -7101
- package/dist/Engine-EWWHhEIi.d.ts +0 -1277
- package/dist/Engine-FNaAEjfb.d.cts +0 -1277
- package/dist/derive-BbsDk_Kz.cjs +0 -880
- package/dist/derive-BbsDk_Kz.cjs.map +0 -1
- package/dist/derive-DAFJolWu.js +0 -647
- package/dist/derive-DAFJolWu.js.map +0 -1
- package/dist/format-Bbphf4zR.js +0 -435
- package/dist/format-Bbphf4zR.js.map +0 -1
- package/dist/format-D_epWlob.cjs +0 -518
- package/dist/format-D_epWlob.cjs.map +0 -1
- package/dist/resolver-CYVNV4hn.cjs.map +0 -1
- package/dist/resolver-inAVzsWt.js.map +0 -1
- package/dist/schemas-CFQGvWiu.js +0 -3243
- package/dist/schemas-CFQGvWiu.js.map +0 -1
- package/dist/schemas-D0g6tKa9.cjs +0 -4694
- package/dist/schemas-D0g6tKa9.cjs.map +0 -1
- package/dist/viewport-BWeNHZmb.cjs.map +0 -1
- package/dist/viewport-CvMcadfQ.js +0 -3072
- package/dist/viewport-CvMcadfQ.js.map +0 -1
package/dist/format-D_epWlob.cjs
DELETED
|
@@ -1,518 +0,0 @@
|
|
|
1
|
-
//#region src/errors/EngineErrorCode.ts
|
|
2
|
-
const EngineErrorCode = {
|
|
3
|
-
Unknown: "Unknown",
|
|
4
|
-
InvalidArg: "InvalidArg",
|
|
5
|
-
DocNotOpen: "DocNotOpen",
|
|
6
|
-
DocOpenFailed: "DocOpenFailed",
|
|
7
|
-
DocPasswordRequired: "DocPasswordRequired",
|
|
8
|
-
DocPasswordIncorrect: "DocPasswordIncorrect",
|
|
9
|
-
/**
|
|
10
|
-
* A share-token exchange (`open({ kind: 'share' })`) needs the grant's
|
|
11
|
-
* passphrase: none was supplied, or the supplied one was rejected —
|
|
12
|
-
* indistinguishable by design, mirroring the server. Prompt and retry
|
|
13
|
-
* with `sharePassword`. Distinct from `DocPasswordRequired`, which is
|
|
14
|
-
* about the PDF's own encryption password.
|
|
15
|
-
*/
|
|
16
|
-
SharePasswordRequired: "SharePasswordRequired",
|
|
17
|
-
Aborted: "Aborted",
|
|
18
|
-
Network: "Network",
|
|
19
|
-
Unauthenticated: "Unauthenticated",
|
|
20
|
-
Forbidden: "Forbidden",
|
|
21
|
-
NotFound: "NotFound",
|
|
22
|
-
WireFormat: "WireFormat",
|
|
23
|
-
RuntimeUnavailable: "RuntimeUnavailable",
|
|
24
|
-
/**
|
|
25
|
-
* Annotation/page reference could not be resolved. Surfaced when:
|
|
26
|
-
* - `AnnotationRef.kind === 'objectNumber'` but the document has no
|
|
27
|
-
* annotation with that indirect object number on the addressed page
|
|
28
|
-
* - `AnnotationRef.kind === 'nm'` but no annotation on the page has
|
|
29
|
-
* that `/NM`
|
|
30
|
-
* - `AnnotationRef.kind === 'index'` but the supplied `RevisionToken`
|
|
31
|
-
* does not match the current per-page generation
|
|
32
|
-
* - `AnnotationRef.kind === 'index'` but `index` is out of range for
|
|
33
|
-
* the page's current annotation count
|
|
34
|
-
*/
|
|
35
|
-
InvalidReference: "InvalidReference",
|
|
36
|
-
/**
|
|
37
|
-
* A weak-annotation structural edit (delete/move) was attempted while the
|
|
38
|
-
* caller does not have sole active edit presence for that page. This is a
|
|
39
|
-
* collaboration guard for index-addressed annotations; stable pages and
|
|
40
|
-
* non-shifting annotation updates do not use it.
|
|
41
|
-
*/
|
|
42
|
-
WeakAnnotationSessionConflict: "WeakAnnotationSessionConflict",
|
|
43
|
-
/**
|
|
44
|
-
* A layer write lost its optimistic-concurrency check: the layer's
|
|
45
|
-
* durable version advanced (another writer — typically another server
|
|
46
|
-
* replica — committed) between the operation's prepare and its commit,
|
|
47
|
-
* and the server exhausted its rebase-and-retry budget. Retryable: the
|
|
48
|
-
* operation was NOT applied; re-issue it against the new state.
|
|
49
|
-
*/
|
|
50
|
-
LayerVersionConflict: "LayerVersionConflict",
|
|
51
|
-
/**
|
|
52
|
-
* The requested operation is typed but not yet wired in this engine
|
|
53
|
-
* version. The wire shape stays valid; clients can detect this with
|
|
54
|
-
* `EngineError.is(err, EngineErrorCode.NotImplemented)` and degrade
|
|
55
|
-
* gracefully.
|
|
56
|
-
*/
|
|
57
|
-
NotImplemented: "NotImplemented",
|
|
58
|
-
/**
|
|
59
|
-
* The PDF was loadable enough for PDFium to parse, but violates a
|
|
60
|
-
* structural invariant the engine relies on. Surfaced for the rare
|
|
61
|
-
* spec-violating PDF that ships a direct (non-indirect) page
|
|
62
|
-
* dictionary in the /Pages tree — see `EPDFPage_GetObjectNumber`
|
|
63
|
-
* returning `0`. PDFium creation paths (`FPDFPage_New`,
|
|
64
|
-
* `EPDFPage_CreateAnnot`) always produce indirect objects, so this
|
|
65
|
-
* error code is intentionally not used for engine-produced state;
|
|
66
|
-
* it specifically signals "this input file is broken in a way we
|
|
67
|
-
* cannot work around without compromising stable identity".
|
|
68
|
-
*
|
|
69
|
-
* Distinct from `DocOpenFailed` (PDFium refused to load the bytes
|
|
70
|
-
* at all) and `NotFound` (a well-formed page/annotation/etc. simply
|
|
71
|
-
* doesn't exist).
|
|
72
|
-
*/
|
|
73
|
-
MalformedPdf: "MalformedPdf",
|
|
74
|
-
/**
|
|
75
|
-
* A signing candidate is parked on this session: every mutation is
|
|
76
|
-
* refused until `signatures.complete` or `signatures.abort`.
|
|
77
|
-
*/
|
|
78
|
-
SigningPending: "SigningPending",
|
|
79
|
-
/** The candidate's TTL elapsed; prepare again. */
|
|
80
|
-
SigningExpired: "SigningExpired",
|
|
81
|
-
/**
|
|
82
|
-
* `complete` presented a version the candidate was not built on (the
|
|
83
|
-
* document or the layer moved since `prepare`). The CMS cannot be
|
|
84
|
-
* reused: it signs a digest of bytes that will never be the head.
|
|
85
|
-
* Prepare again on the current version.
|
|
86
|
-
*/
|
|
87
|
-
SigningVersionMismatch: "SigningVersionMismatch",
|
|
88
|
-
/**
|
|
89
|
-
* The engine refused to author the signature: the field is signed,
|
|
90
|
-
* read-only, or locked by an earlier signature; a certification is
|
|
91
|
-
* requested after a signature exists; the seed value requires what the
|
|
92
|
-
* engine does not implement; the request is inconsistent. The message
|
|
93
|
-
* names the reason.
|
|
94
|
-
*/
|
|
95
|
-
SignatureRefused: "SignatureRefused",
|
|
96
|
-
/**
|
|
97
|
-
* A signature already in the document forbids this change (a
|
|
98
|
-
* certification's permission, a FieldMDP or `/Lock`, or the approval
|
|
99
|
-
* baseline). The message names the signature and the restriction. The
|
|
100
|
-
* engine option `signedDocumentPolicy: 'permit'` disables the guard.
|
|
101
|
-
*/
|
|
102
|
-
ProtectedDocument: "ProtectedDocument",
|
|
103
|
-
/**
|
|
104
|
-
* The layer is built on a base version that is no longer the
|
|
105
|
-
* document's head (another signing completed); it can be read and
|
|
106
|
-
* edited but not signed from.
|
|
107
|
-
*/
|
|
108
|
-
StaleBase: "StaleBase"
|
|
109
|
-
};
|
|
110
|
-
//#endregion
|
|
111
|
-
//#region src/annotation/subtype.ts
|
|
112
|
-
/**
|
|
113
|
-
* Closed-world string catalog of annotation subtypes the v3 wire format knows
|
|
114
|
-
* about. The TypeScript discriminated union `AnnotationDTO` is keyed on this
|
|
115
|
-
* literal type. PDFium's integer subtype code is mapped onto these strings
|
|
116
|
-
* by the reader registry.
|
|
117
|
-
*
|
|
118
|
-
* `unsupported` is the forward-compat fallback for any PDF subtype that
|
|
119
|
-
* the engine has not implemented a reader for yet. Unknown subtypes never
|
|
120
|
-
* silently disappear from the wire — they round-trip as
|
|
121
|
-
* `{ subtype: 'unsupported', rawSubtypeCode }`.
|
|
122
|
-
*/
|
|
123
|
-
const ANNOTATION_SUBTYPES = [
|
|
124
|
-
"text",
|
|
125
|
-
"link",
|
|
126
|
-
"free-text",
|
|
127
|
-
"line",
|
|
128
|
-
"square",
|
|
129
|
-
"circle",
|
|
130
|
-
"polygon",
|
|
131
|
-
"polyline",
|
|
132
|
-
"highlight",
|
|
133
|
-
"underline",
|
|
134
|
-
"squiggly",
|
|
135
|
-
"strikeout",
|
|
136
|
-
"stamp",
|
|
137
|
-
"caret",
|
|
138
|
-
"ink",
|
|
139
|
-
"popup",
|
|
140
|
-
"file-attachment",
|
|
141
|
-
"redact",
|
|
142
|
-
"widget",
|
|
143
|
-
"unsupported"
|
|
144
|
-
];
|
|
145
|
-
/**
|
|
146
|
-
* PDFium subtype codes from `FPDFAnnot_GetSubtype`. Mirrors
|
|
147
|
-
* `FPDF_ANNOTATION_SUBTYPE` in `public/fpdf_annot.h`. We keep this here
|
|
148
|
-
* (rather than importing from @embedpdf/engine-runtime) so the engine-core
|
|
149
|
-
* package has zero PDFium dependency.
|
|
150
|
-
*/
|
|
151
|
-
const PdfAnnotationSubtypeCode = {
|
|
152
|
-
UNKNOWN: 0,
|
|
153
|
-
TEXT: 1,
|
|
154
|
-
LINK: 2,
|
|
155
|
-
FREETEXT: 3,
|
|
156
|
-
LINE: 4,
|
|
157
|
-
SQUARE: 5,
|
|
158
|
-
CIRCLE: 6,
|
|
159
|
-
POLYGON: 7,
|
|
160
|
-
POLYLINE: 8,
|
|
161
|
-
HIGHLIGHT: 9,
|
|
162
|
-
UNDERLINE: 10,
|
|
163
|
-
SQUIGGLY: 11,
|
|
164
|
-
STRIKEOUT: 12,
|
|
165
|
-
STAMP: 13,
|
|
166
|
-
CARET: 14,
|
|
167
|
-
INK: 15,
|
|
168
|
-
POPUP: 16,
|
|
169
|
-
FILEATTACHMENT: 17,
|
|
170
|
-
SOUND: 18,
|
|
171
|
-
MOVIE: 19,
|
|
172
|
-
WIDGET: 20,
|
|
173
|
-
SCREEN: 21,
|
|
174
|
-
PRINTERMARK: 22,
|
|
175
|
-
TRAPNET: 23,
|
|
176
|
-
WATERMARK: 24,
|
|
177
|
-
THREED: 25,
|
|
178
|
-
RICHMEDIA: 26,
|
|
179
|
-
XFAWIDGET: 27,
|
|
180
|
-
REDACT: 28
|
|
181
|
-
};
|
|
182
|
-
/**
|
|
183
|
-
* Map from PDFium codes to wire-stable string subtypes. Codes the engine
|
|
184
|
-
* recognises as a v3 subtype (or maps to 'unsupported' if not yet wired).
|
|
185
|
-
*/
|
|
186
|
-
const PDF_CODE_TO_SUBTYPE = Object.freeze({
|
|
187
|
-
[PdfAnnotationSubtypeCode.TEXT]: "text",
|
|
188
|
-
[PdfAnnotationSubtypeCode.LINK]: "link",
|
|
189
|
-
[PdfAnnotationSubtypeCode.FREETEXT]: "free-text",
|
|
190
|
-
[PdfAnnotationSubtypeCode.LINE]: "line",
|
|
191
|
-
[PdfAnnotationSubtypeCode.SQUARE]: "square",
|
|
192
|
-
[PdfAnnotationSubtypeCode.CIRCLE]: "circle",
|
|
193
|
-
[PdfAnnotationSubtypeCode.POLYGON]: "polygon",
|
|
194
|
-
[PdfAnnotationSubtypeCode.POLYLINE]: "polyline",
|
|
195
|
-
[PdfAnnotationSubtypeCode.HIGHLIGHT]: "highlight",
|
|
196
|
-
[PdfAnnotationSubtypeCode.UNDERLINE]: "underline",
|
|
197
|
-
[PdfAnnotationSubtypeCode.SQUIGGLY]: "squiggly",
|
|
198
|
-
[PdfAnnotationSubtypeCode.STRIKEOUT]: "strikeout",
|
|
199
|
-
[PdfAnnotationSubtypeCode.STAMP]: "stamp",
|
|
200
|
-
[PdfAnnotationSubtypeCode.CARET]: "caret",
|
|
201
|
-
[PdfAnnotationSubtypeCode.INK]: "ink",
|
|
202
|
-
[PdfAnnotationSubtypeCode.POPUP]: "popup",
|
|
203
|
-
[PdfAnnotationSubtypeCode.FILEATTACHMENT]: "file-attachment",
|
|
204
|
-
[PdfAnnotationSubtypeCode.WIDGET]: "widget",
|
|
205
|
-
[PdfAnnotationSubtypeCode.REDACT]: "redact"
|
|
206
|
-
});
|
|
207
|
-
/**
|
|
208
|
-
* Reverse map for write paths that need to turn a wire subtype back into the
|
|
209
|
-
* PDFium integer code without importing the zod-backed annotation registry.
|
|
210
|
-
*/
|
|
211
|
-
const PDF_SUBTYPE_TO_CODE = Object.freeze({
|
|
212
|
-
text: PdfAnnotationSubtypeCode.TEXT,
|
|
213
|
-
link: PdfAnnotationSubtypeCode.LINK,
|
|
214
|
-
"free-text": PdfAnnotationSubtypeCode.FREETEXT,
|
|
215
|
-
line: PdfAnnotationSubtypeCode.LINE,
|
|
216
|
-
square: PdfAnnotationSubtypeCode.SQUARE,
|
|
217
|
-
circle: PdfAnnotationSubtypeCode.CIRCLE,
|
|
218
|
-
polygon: PdfAnnotationSubtypeCode.POLYGON,
|
|
219
|
-
polyline: PdfAnnotationSubtypeCode.POLYLINE,
|
|
220
|
-
highlight: PdfAnnotationSubtypeCode.HIGHLIGHT,
|
|
221
|
-
underline: PdfAnnotationSubtypeCode.UNDERLINE,
|
|
222
|
-
squiggly: PdfAnnotationSubtypeCode.SQUIGGLY,
|
|
223
|
-
strikeout: PdfAnnotationSubtypeCode.STRIKEOUT,
|
|
224
|
-
stamp: PdfAnnotationSubtypeCode.STAMP,
|
|
225
|
-
caret: PdfAnnotationSubtypeCode.CARET,
|
|
226
|
-
ink: PdfAnnotationSubtypeCode.INK,
|
|
227
|
-
popup: PdfAnnotationSubtypeCode.POPUP,
|
|
228
|
-
"file-attachment": PdfAnnotationSubtypeCode.FILEATTACHMENT,
|
|
229
|
-
redact: PdfAnnotationSubtypeCode.REDACT,
|
|
230
|
-
widget: PdfAnnotationSubtypeCode.WIDGET,
|
|
231
|
-
unsupported: PdfAnnotationSubtypeCode.UNKNOWN
|
|
232
|
-
});
|
|
233
|
-
function subtypeFromCode(code) {
|
|
234
|
-
return PDF_CODE_TO_SUBTYPE[code] ?? "unsupported";
|
|
235
|
-
}
|
|
236
|
-
//#endregion
|
|
237
|
-
//#region src/text/charmap.ts
|
|
238
|
-
const clampInt = (value, lo, hi) => Math.min(Math.max(Math.floor(value), lo), hi);
|
|
239
|
-
/** The greatest anchor with `anchor.char <= boundary` (implicit head [0,0]). */
|
|
240
|
-
function anchorAtOrBefore(map, boundary) {
|
|
241
|
-
let lo = 0;
|
|
242
|
-
let hi = map.length - 1;
|
|
243
|
-
let best = null;
|
|
244
|
-
while (lo <= hi) {
|
|
245
|
-
const mid = lo + hi >> 1;
|
|
246
|
-
if (map[mid][0] <= boundary) {
|
|
247
|
-
best = map[mid];
|
|
248
|
-
lo = mid + 1;
|
|
249
|
-
} else hi = mid - 1;
|
|
250
|
-
}
|
|
251
|
-
return best ?? [0, 0];
|
|
252
|
-
}
|
|
253
|
-
/**
|
|
254
|
-
* Text offset of character boundary `boundary` (clamped into
|
|
255
|
-
* [0, charCount]). For a dropped character this is the offset of the next
|
|
256
|
-
* real text unit — exactly what half-open slicing needs at either end.
|
|
257
|
-
*/
|
|
258
|
-
function boundaryTextOffset(snapshot, boundary) {
|
|
259
|
-
const b = clampInt(boundary, 0, snapshot.charCount);
|
|
260
|
-
const map = snapshot.charMap;
|
|
261
|
-
if (!map || map.length === 0) return Math.min(b, snapshot.text.length);
|
|
262
|
-
const [char, text] = anchorAtOrBefore(map, b);
|
|
263
|
-
return text + (b - char);
|
|
264
|
-
}
|
|
265
|
-
/**
|
|
266
|
-
* The character boundary for a text offset, with explicit plateau bias:
|
|
267
|
-
*
|
|
268
|
-
* - `'start'`: the GREATEST boundary whose text offset is <= `offset` —
|
|
269
|
-
* scans forward past zero-width characters, so a range start never
|
|
270
|
-
* includes a dropped character that precedes its first real unit.
|
|
271
|
-
* - `'end'`: the SMALLEST boundary whose text offset is >= `offset` —
|
|
272
|
-
* stops before zero-width characters, so a range end never swallows a
|
|
273
|
-
* dropped character that follows its last real unit.
|
|
274
|
-
*
|
|
275
|
-
* An offset inside a surrogate pair resolves outward under both biases
|
|
276
|
-
* (start → the pair's character, end → past it), so a clipped astral
|
|
277
|
-
* character is always covered whole. Prefer {@link charRangeForTextOffsets}
|
|
278
|
-
* — the two biases exist to be used as a PAIR.
|
|
279
|
-
*/
|
|
280
|
-
function charBoundaryAtTextOffset(snapshot, offset, bias) {
|
|
281
|
-
const target = clampInt(offset, 0, snapshot.text.length);
|
|
282
|
-
let lo = 0;
|
|
283
|
-
let hi = snapshot.charCount;
|
|
284
|
-
if (bias === "start") {
|
|
285
|
-
while (lo < hi) {
|
|
286
|
-
const mid = lo + Math.ceil((hi - lo) / 2);
|
|
287
|
-
if (boundaryTextOffset(snapshot, mid) <= target) lo = mid;
|
|
288
|
-
else hi = mid - 1;
|
|
289
|
-
}
|
|
290
|
-
return lo;
|
|
291
|
-
}
|
|
292
|
-
while (lo < hi) {
|
|
293
|
-
const mid = lo + hi >> 1;
|
|
294
|
-
if (boundaryTextOffset(snapshot, mid) >= target) hi = mid;
|
|
295
|
-
else lo = mid + 1;
|
|
296
|
-
}
|
|
297
|
-
return lo;
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* The character range whose text projection is exactly
|
|
301
|
-
* `text.slice(startOffset, endOffset)` — THE inverse used by search-hit
|
|
302
|
-
* conversion and select-by-text. Start is right-biased and end left-biased
|
|
303
|
-
* (see {@link charBoundaryAtTextOffset}), so zero-width characters adjacent
|
|
304
|
-
* to the range fall OUTSIDE it on both sides.
|
|
305
|
-
*
|
|
306
|
-
* A non-empty text range can never invert: `end > start` implies
|
|
307
|
-
* `B(end) >= endOffset > startOffset >= B(start)`, which forces
|
|
308
|
-
* `end > start` by monotonicity. Empty input (`endOffset <= startOffset`)
|
|
309
|
-
* normalizes to the end-rule boundary of `startOffset` on both sides.
|
|
310
|
-
*/
|
|
311
|
-
function charRangeForTextOffsets(snapshot, startOffset, endOffset) {
|
|
312
|
-
if (endOffset <= startOffset) {
|
|
313
|
-
const b = charBoundaryAtTextOffset(snapshot, startOffset, "end");
|
|
314
|
-
return {
|
|
315
|
-
start: b,
|
|
316
|
-
end: b
|
|
317
|
-
};
|
|
318
|
-
}
|
|
319
|
-
return {
|
|
320
|
-
start: charBoundaryAtTextOffset(snapshot, startOffset, "start"),
|
|
321
|
-
end: charBoundaryAtTextOffset(snapshot, endOffset, "end")
|
|
322
|
-
};
|
|
323
|
-
}
|
|
324
|
-
/**
|
|
325
|
-
* THE copy primitive: the text projection of the half-open character range
|
|
326
|
-
* `[charStart, charEnd)`. Dropped characters inside the range contribute
|
|
327
|
-
* nothing; a supplementary character contributes its full surrogate pair.
|
|
328
|
-
* `sliceTextByChars(s, 0, s.charCount) === s.text` always.
|
|
329
|
-
*/
|
|
330
|
-
function sliceTextByChars(snapshot, charStart, charEnd) {
|
|
331
|
-
if (charEnd <= charStart) return "";
|
|
332
|
-
return snapshot.text.slice(boundaryTextOffset(snapshot, charStart), boundaryTextOffset(snapshot, charEnd));
|
|
333
|
-
}
|
|
334
|
-
/**
|
|
335
|
-
* First invariant violation of a snapshot's char map, or null when valid.
|
|
336
|
-
* Shared by the wire schema (rejects malformed bodies) and engine builders
|
|
337
|
-
* (dev-mode assertion). The invariants:
|
|
338
|
-
*
|
|
339
|
-
* 1. absent/empty map ⇒ identity, so `charCount === text.length`;
|
|
340
|
-
* 2. anchor chars strictly increasing, each in [1, charCount];
|
|
341
|
-
* 3. step rule: against the previous anchor (implicit head [0, 0]), the
|
|
342
|
-
* character just before each anchor contributed exactly 0 units
|
|
343
|
-
* (dropped) or 2 (surrogate pair):
|
|
344
|
-
* `t₂ - (t₁ + (c₂ - c₁ - 1)) ∈ {0, 2}`;
|
|
345
|
-
* 4. tail: `lastText + (charCount - lastChar) === text.length`
|
|
346
|
-
* (equivalently `boundaryTextOffset(s, charCount) === text.length`).
|
|
347
|
-
*/
|
|
348
|
-
function charMapViolation(charCount, textLength, charMap) {
|
|
349
|
-
if (!charMap || charMap.length === 0) return charCount === textLength ? null : `identity map requires charCount (${charCount}) === text length (${textLength})`;
|
|
350
|
-
let prevChar = 0;
|
|
351
|
-
let prevText = 0;
|
|
352
|
-
for (let i = 0; i < charMap.length; i++) {
|
|
353
|
-
const [char, text] = charMap[i];
|
|
354
|
-
if (!Number.isInteger(char) || !Number.isInteger(text)) return `anchor ${i} is not integer-valued`;
|
|
355
|
-
if (char <= prevChar) return `anchor ${i} char ${char} not strictly after ${prevChar}`;
|
|
356
|
-
if (char > charCount) return `anchor ${i} char ${char} outside [1, ${charCount}]`;
|
|
357
|
-
const step = text - (prevText + (char - prevChar - 1));
|
|
358
|
-
if (step !== 0 && step !== 2) return `anchor ${i} implies a character contributing ${step} text units (expected 0 or 2)`;
|
|
359
|
-
prevChar = char;
|
|
360
|
-
prevText = text;
|
|
361
|
-
}
|
|
362
|
-
const tail = prevText + (charCount - prevChar);
|
|
363
|
-
if (tail !== textLength) return `map projects charCount to ${tail}, but text length is ${textLength}`;
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
366
|
-
//#endregion
|
|
367
|
-
//#region src/mutation/PageInsertBlankInput.ts
|
|
368
|
-
/**
|
|
369
|
-
* Upper bound on `count` for a single `pages.insertBlank()` call. A CONTRACT
|
|
370
|
-
* cap, enforced identically by every engine (the cloud input schema carries
|
|
371
|
-
* the same number), so a UI never discovers a lower server limit by surprise.
|
|
372
|
-
* Larger batches are a loop at the call site.
|
|
373
|
-
*/
|
|
374
|
-
const PAGE_INSERT_BLANK_MAX_COUNT = 100;
|
|
375
|
-
//#endregion
|
|
376
|
-
//#region src/measure/format.ts
|
|
377
|
-
const group = (value, separator) => value.replace(/\B(?=(\d{3})+(?!\d))/g, () => separator);
|
|
378
|
-
const gcd = (a, b) => b ? gcd(b, a % b) : a;
|
|
379
|
-
function validNumberFormat(nf) {
|
|
380
|
-
const c = Math.fround(nf.conversion ?? NaN);
|
|
381
|
-
if (!Number.isFinite(c) || c <= 0) return false;
|
|
382
|
-
const d = nf.precision ?? (nf.fraction === "fraction" ? 16 : 100);
|
|
383
|
-
if (!Number.isInteger(d) || d < 1 || d > 2147483647) return false;
|
|
384
|
-
if ((nf.fraction ?? "decimal") === "decimal" && !Number.isInteger(Math.log10(d))) return false;
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
function last(value, nf) {
|
|
388
|
-
const mode = nf.fraction ?? "decimal";
|
|
389
|
-
const sep = nf.thousands ?? ",";
|
|
390
|
-
if (mode === "round") return group(String(Math.round(value)), sep);
|
|
391
|
-
if (mode === "truncate") return group(String(Math.trunc(value)), sep);
|
|
392
|
-
if (mode === "fraction") {
|
|
393
|
-
const denominator = nf.precision ?? 16;
|
|
394
|
-
let whole = Math.floor(value);
|
|
395
|
-
let numerator = Math.round((value - whole) * denominator);
|
|
396
|
-
if (numerator === denominator) {
|
|
397
|
-
whole++;
|
|
398
|
-
numerator = 0;
|
|
399
|
-
}
|
|
400
|
-
if (!numerator) return group(String(whole), sep);
|
|
401
|
-
const divisor = nf.fixed ? 1 : gcd(numerator, denominator);
|
|
402
|
-
return `${whole ? `${group(String(whole), sep)} ` : ""}${numerator / divisor}/${denominator / divisor}`;
|
|
403
|
-
}
|
|
404
|
-
const digits = Math.log10(nf.precision ?? 100);
|
|
405
|
-
const [integer, decimal = ""] = value.toFixed(digits).split(".");
|
|
406
|
-
const kept = nf.fixed ? decimal : decimal.replace(/0+$/, "");
|
|
407
|
-
return group(integer, sep) + (kept ? (nf.decimal || ".") + kept : "");
|
|
408
|
-
}
|
|
409
|
-
/** ISO 32000-2 §12.9.2 cascade. PDF float conversions are normalized before use.
|
|
410
|
-
* Unit text and explicit spacing are preserved, including trailing unit spaces. */
|
|
411
|
-
function formatMeasurement(value, formats) {
|
|
412
|
-
if (!Number.isFinite(value) || !formats.length || !formats.every(validNumberFormat)) throw new RangeError("Measurement requires a finite value and valid number formats");
|
|
413
|
-
let remainder = Math.abs(value);
|
|
414
|
-
const parts = [];
|
|
415
|
-
for (const [index, nf] of formats.entries()) {
|
|
416
|
-
remainder *= Math.fround(nf.conversion);
|
|
417
|
-
if (!Number.isFinite(remainder)) throw new RangeError("Measurement overflow");
|
|
418
|
-
const whole = Math.floor(remainder);
|
|
419
|
-
const fraction = remainder - whole;
|
|
420
|
-
const terminal = index === formats.length - 1 || fraction === 0;
|
|
421
|
-
const number = terminal ? last(remainder, nf) : group(String(whole), nf.thousands ?? ",");
|
|
422
|
-
const prefix = nf.labelPosition === "prefix";
|
|
423
|
-
const ps = nf.prefixSpacing ?? (prefix && index === 0 ? "" : " ");
|
|
424
|
-
const ss = nf.suffixSpacing ?? (!prefix && terminal ? "" : " ");
|
|
425
|
-
const label = ps + nf.unit + ss;
|
|
426
|
-
parts.push(prefix ? label + number : number + label);
|
|
427
|
-
if (terminal) break;
|
|
428
|
-
remainder = fraction;
|
|
429
|
-
}
|
|
430
|
-
return (value < 0 ? "-" : "") + parts.join("");
|
|
431
|
-
}
|
|
432
|
-
//#endregion
|
|
433
|
-
Object.defineProperty(exports, "ANNOTATION_SUBTYPES", {
|
|
434
|
-
enumerable: true,
|
|
435
|
-
get: function() {
|
|
436
|
-
return ANNOTATION_SUBTYPES;
|
|
437
|
-
}
|
|
438
|
-
});
|
|
439
|
-
Object.defineProperty(exports, "EngineErrorCode", {
|
|
440
|
-
enumerable: true,
|
|
441
|
-
get: function() {
|
|
442
|
-
return EngineErrorCode;
|
|
443
|
-
}
|
|
444
|
-
});
|
|
445
|
-
Object.defineProperty(exports, "PAGE_INSERT_BLANK_MAX_COUNT", {
|
|
446
|
-
enumerable: true,
|
|
447
|
-
get: function() {
|
|
448
|
-
return PAGE_INSERT_BLANK_MAX_COUNT;
|
|
449
|
-
}
|
|
450
|
-
});
|
|
451
|
-
Object.defineProperty(exports, "PDF_CODE_TO_SUBTYPE", {
|
|
452
|
-
enumerable: true,
|
|
453
|
-
get: function() {
|
|
454
|
-
return PDF_CODE_TO_SUBTYPE;
|
|
455
|
-
}
|
|
456
|
-
});
|
|
457
|
-
Object.defineProperty(exports, "PDF_SUBTYPE_TO_CODE", {
|
|
458
|
-
enumerable: true,
|
|
459
|
-
get: function() {
|
|
460
|
-
return PDF_SUBTYPE_TO_CODE;
|
|
461
|
-
}
|
|
462
|
-
});
|
|
463
|
-
Object.defineProperty(exports, "PdfAnnotationSubtypeCode", {
|
|
464
|
-
enumerable: true,
|
|
465
|
-
get: function() {
|
|
466
|
-
return PdfAnnotationSubtypeCode;
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
Object.defineProperty(exports, "boundaryTextOffset", {
|
|
470
|
-
enumerable: true,
|
|
471
|
-
get: function() {
|
|
472
|
-
return boundaryTextOffset;
|
|
473
|
-
}
|
|
474
|
-
});
|
|
475
|
-
Object.defineProperty(exports, "charBoundaryAtTextOffset", {
|
|
476
|
-
enumerable: true,
|
|
477
|
-
get: function() {
|
|
478
|
-
return charBoundaryAtTextOffset;
|
|
479
|
-
}
|
|
480
|
-
});
|
|
481
|
-
Object.defineProperty(exports, "charMapViolation", {
|
|
482
|
-
enumerable: true,
|
|
483
|
-
get: function() {
|
|
484
|
-
return charMapViolation;
|
|
485
|
-
}
|
|
486
|
-
});
|
|
487
|
-
Object.defineProperty(exports, "charRangeForTextOffsets", {
|
|
488
|
-
enumerable: true,
|
|
489
|
-
get: function() {
|
|
490
|
-
return charRangeForTextOffsets;
|
|
491
|
-
}
|
|
492
|
-
});
|
|
493
|
-
Object.defineProperty(exports, "formatMeasurement", {
|
|
494
|
-
enumerable: true,
|
|
495
|
-
get: function() {
|
|
496
|
-
return formatMeasurement;
|
|
497
|
-
}
|
|
498
|
-
});
|
|
499
|
-
Object.defineProperty(exports, "sliceTextByChars", {
|
|
500
|
-
enumerable: true,
|
|
501
|
-
get: function() {
|
|
502
|
-
return sliceTextByChars;
|
|
503
|
-
}
|
|
504
|
-
});
|
|
505
|
-
Object.defineProperty(exports, "subtypeFromCode", {
|
|
506
|
-
enumerable: true,
|
|
507
|
-
get: function() {
|
|
508
|
-
return subtypeFromCode;
|
|
509
|
-
}
|
|
510
|
-
});
|
|
511
|
-
Object.defineProperty(exports, "validNumberFormat", {
|
|
512
|
-
enumerable: true,
|
|
513
|
-
get: function() {
|
|
514
|
-
return validNumberFormat;
|
|
515
|
-
}
|
|
516
|
-
});
|
|
517
|
-
|
|
518
|
-
//# sourceMappingURL=format-D_epWlob.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"format-D_epWlob.cjs","names":[],"sources":["../src/errors/EngineErrorCode.ts","../src/annotation/subtype.ts","../src/text/charmap.ts","../src/mutation/PageInsertBlankInput.ts","../src/measure/format.ts"],"sourcesContent":["export const EngineErrorCode = {\n Unknown: 'Unknown',\n InvalidArg: 'InvalidArg',\n DocNotOpen: 'DocNotOpen',\n DocOpenFailed: 'DocOpenFailed',\n DocPasswordRequired: 'DocPasswordRequired',\n DocPasswordIncorrect: 'DocPasswordIncorrect',\n /**\n * A share-token exchange (`open({ kind: 'share' })`) needs the grant's\n * passphrase: none was supplied, or the supplied one was rejected —\n * indistinguishable by design, mirroring the server. Prompt and retry\n * with `sharePassword`. Distinct from `DocPasswordRequired`, which is\n * about the PDF's own encryption password.\n */\n SharePasswordRequired: 'SharePasswordRequired',\n Aborted: 'Aborted',\n Network: 'Network',\n Unauthenticated: 'Unauthenticated',\n Forbidden: 'Forbidden',\n NotFound: 'NotFound',\n WireFormat: 'WireFormat',\n RuntimeUnavailable: 'RuntimeUnavailable',\n /**\n * Annotation/page reference could not be resolved. Surfaced when:\n * - `AnnotationRef.kind === 'objectNumber'` but the document has no\n * annotation with that indirect object number on the addressed page\n * - `AnnotationRef.kind === 'nm'` but no annotation on the page has\n * that `/NM`\n * - `AnnotationRef.kind === 'index'` but the supplied `RevisionToken`\n * does not match the current per-page generation\n * - `AnnotationRef.kind === 'index'` but `index` is out of range for\n * the page's current annotation count\n */\n InvalidReference: 'InvalidReference',\n /**\n * A weak-annotation structural edit (delete/move) was attempted while the\n * caller does not have sole active edit presence for that page. This is a\n * collaboration guard for index-addressed annotations; stable pages and\n * non-shifting annotation updates do not use it.\n */\n WeakAnnotationSessionConflict: 'WeakAnnotationSessionConflict',\n /**\n * A layer write lost its optimistic-concurrency check: the layer's\n * durable version advanced (another writer — typically another server\n * replica — committed) between the operation's prepare and its commit,\n * and the server exhausted its rebase-and-retry budget. Retryable: the\n * operation was NOT applied; re-issue it against the new state.\n */\n LayerVersionConflict: 'LayerVersionConflict',\n /**\n * The requested operation is typed but not yet wired in this engine\n * version. The wire shape stays valid; clients can detect this with\n * `EngineError.is(err, EngineErrorCode.NotImplemented)` and degrade\n * gracefully.\n */\n NotImplemented: 'NotImplemented',\n /**\n * The PDF was loadable enough for PDFium to parse, but violates a\n * structural invariant the engine relies on. Surfaced for the rare\n * spec-violating PDF that ships a direct (non-indirect) page\n * dictionary in the /Pages tree — see `EPDFPage_GetObjectNumber`\n * returning `0`. PDFium creation paths (`FPDFPage_New`,\n * `EPDFPage_CreateAnnot`) always produce indirect objects, so this\n * error code is intentionally not used for engine-produced state;\n * it specifically signals \"this input file is broken in a way we\n * cannot work around without compromising stable identity\".\n *\n * Distinct from `DocOpenFailed` (PDFium refused to load the bytes\n * at all) and `NotFound` (a well-formed page/annotation/etc. simply\n * doesn't exist).\n */\n MalformedPdf: 'MalformedPdf',\n /**\n * A signing candidate is parked on this session: every mutation is\n * refused until `signatures.complete` or `signatures.abort`.\n */\n SigningPending: 'SigningPending',\n /** The candidate's TTL elapsed; prepare again. */\n SigningExpired: 'SigningExpired',\n /**\n * `complete` presented a version the candidate was not built on (the\n * document or the layer moved since `prepare`). The CMS cannot be\n * reused: it signs a digest of bytes that will never be the head.\n * Prepare again on the current version.\n */\n SigningVersionMismatch: 'SigningVersionMismatch',\n /**\n * The engine refused to author the signature: the field is signed,\n * read-only, or locked by an earlier signature; a certification is\n * requested after a signature exists; the seed value requires what the\n * engine does not implement; the request is inconsistent. The message\n * names the reason.\n */\n SignatureRefused: 'SignatureRefused',\n /**\n * A signature already in the document forbids this change (a\n * certification's permission, a FieldMDP or `/Lock`, or the approval\n * baseline). The message names the signature and the restriction. The\n * engine option `signedDocumentPolicy: 'permit'` disables the guard.\n */\n ProtectedDocument: 'ProtectedDocument',\n /**\n * The layer is built on a base version that is no longer the\n * document's head (another signing completed); it can be read and\n * edited but not signed from.\n */\n StaleBase: 'StaleBase',\n} as const;\n\nexport type EngineErrorCode = (typeof EngineErrorCode)[keyof typeof EngineErrorCode];\n","/**\n * Closed-world string catalog of annotation subtypes the v3 wire format knows\n * about. The TypeScript discriminated union `AnnotationDTO` is keyed on this\n * literal type. PDFium's integer subtype code is mapped onto these strings\n * by the reader registry.\n *\n * `unsupported` is the forward-compat fallback for any PDF subtype that\n * the engine has not implemented a reader for yet. Unknown subtypes never\n * silently disappear from the wire — they round-trip as\n * `{ subtype: 'unsupported', rawSubtypeCode }`.\n */\nexport const ANNOTATION_SUBTYPES = [\n 'text',\n 'link',\n 'free-text',\n 'line',\n 'square',\n 'circle',\n 'polygon',\n 'polyline',\n 'highlight',\n 'underline',\n 'squiggly',\n 'strikeout',\n 'stamp',\n 'caret',\n 'ink',\n 'popup',\n 'file-attachment',\n 'redact',\n 'widget',\n 'unsupported',\n] as const;\n\nexport type AnnotationSubtype = (typeof ANNOTATION_SUBTYPES)[number];\n\n/**\n * PDFium subtype codes from `FPDFAnnot_GetSubtype`. Mirrors\n * `FPDF_ANNOTATION_SUBTYPE` in `public/fpdf_annot.h`. We keep this here\n * (rather than importing from @embedpdf/engine-runtime) so the engine-core\n * package has zero PDFium dependency.\n */\nexport const PdfAnnotationSubtypeCode = {\n UNKNOWN: 0,\n TEXT: 1,\n LINK: 2,\n FREETEXT: 3,\n LINE: 4,\n SQUARE: 5,\n CIRCLE: 6,\n POLYGON: 7,\n POLYLINE: 8,\n HIGHLIGHT: 9,\n UNDERLINE: 10,\n SQUIGGLY: 11,\n STRIKEOUT: 12,\n STAMP: 13,\n CARET: 14,\n INK: 15,\n POPUP: 16,\n FILEATTACHMENT: 17,\n SOUND: 18,\n MOVIE: 19,\n WIDGET: 20,\n SCREEN: 21,\n PRINTERMARK: 22,\n TRAPNET: 23,\n WATERMARK: 24,\n THREED: 25,\n RICHMEDIA: 26,\n XFAWIDGET: 27,\n REDACT: 28,\n} as const;\n\nexport type PdfAnnotationSubtypeCode =\n (typeof PdfAnnotationSubtypeCode)[keyof typeof PdfAnnotationSubtypeCode];\n\n/**\n * Map from PDFium codes to wire-stable string subtypes. Codes the engine\n * recognises as a v3 subtype (or maps to 'unsupported' if not yet wired).\n */\nexport const PDF_CODE_TO_SUBTYPE: Readonly<Record<number, AnnotationSubtype>> = Object.freeze({\n [PdfAnnotationSubtypeCode.TEXT]: 'text',\n [PdfAnnotationSubtypeCode.LINK]: 'link',\n [PdfAnnotationSubtypeCode.FREETEXT]: 'free-text',\n [PdfAnnotationSubtypeCode.LINE]: 'line',\n [PdfAnnotationSubtypeCode.SQUARE]: 'square',\n [PdfAnnotationSubtypeCode.CIRCLE]: 'circle',\n [PdfAnnotationSubtypeCode.POLYGON]: 'polygon',\n [PdfAnnotationSubtypeCode.POLYLINE]: 'polyline',\n [PdfAnnotationSubtypeCode.HIGHLIGHT]: 'highlight',\n [PdfAnnotationSubtypeCode.UNDERLINE]: 'underline',\n [PdfAnnotationSubtypeCode.SQUIGGLY]: 'squiggly',\n [PdfAnnotationSubtypeCode.STRIKEOUT]: 'strikeout',\n [PdfAnnotationSubtypeCode.STAMP]: 'stamp',\n [PdfAnnotationSubtypeCode.CARET]: 'caret',\n [PdfAnnotationSubtypeCode.INK]: 'ink',\n [PdfAnnotationSubtypeCode.POPUP]: 'popup',\n [PdfAnnotationSubtypeCode.FILEATTACHMENT]: 'file-attachment',\n [PdfAnnotationSubtypeCode.WIDGET]: 'widget',\n [PdfAnnotationSubtypeCode.REDACT]: 'redact',\n});\n\n/**\n * Reverse map for write paths that need to turn a wire subtype back into the\n * PDFium integer code without importing the zod-backed annotation registry.\n */\nexport const PDF_SUBTYPE_TO_CODE: Readonly<Record<AnnotationSubtype, PdfAnnotationSubtypeCode>> =\n Object.freeze({\n text: PdfAnnotationSubtypeCode.TEXT,\n link: PdfAnnotationSubtypeCode.LINK,\n 'free-text': PdfAnnotationSubtypeCode.FREETEXT,\n line: PdfAnnotationSubtypeCode.LINE,\n square: PdfAnnotationSubtypeCode.SQUARE,\n circle: PdfAnnotationSubtypeCode.CIRCLE,\n polygon: PdfAnnotationSubtypeCode.POLYGON,\n polyline: PdfAnnotationSubtypeCode.POLYLINE,\n highlight: PdfAnnotationSubtypeCode.HIGHLIGHT,\n underline: PdfAnnotationSubtypeCode.UNDERLINE,\n squiggly: PdfAnnotationSubtypeCode.SQUIGGLY,\n strikeout: PdfAnnotationSubtypeCode.STRIKEOUT,\n stamp: PdfAnnotationSubtypeCode.STAMP,\n caret: PdfAnnotationSubtypeCode.CARET,\n ink: PdfAnnotationSubtypeCode.INK,\n popup: PdfAnnotationSubtypeCode.POPUP,\n 'file-attachment': PdfAnnotationSubtypeCode.FILEATTACHMENT,\n redact: PdfAnnotationSubtypeCode.REDACT,\n widget: PdfAnnotationSubtypeCode.WIDGET,\n unsupported: PdfAnnotationSubtypeCode.UNKNOWN,\n });\n\nexport function subtypeFromCode(code: number): AnnotationSubtype {\n return PDF_CODE_TO_SUBTYPE[code] ?? 'unsupported';\n}\n","import type { PageTextSnapshot } from '../dto/PageTextSnapshot';\n\n/**\n * Character↔text index translation — the ONE place the two text index\n * spaces of a page meet.\n *\n * A page has two index spaces:\n *\n * - CHARACTER space: PDFium's internal character list, the space geometry\n * runs tile (`PageGeometryRun.charStart`), hit-testing addresses, and\n * selection ranges live in. Size: `PageTextSnapshot.charCount`.\n * - TEXT space: UTF-16 code-unit offsets into the extracted string\n * `PageTextSnapshot.text` — what search matching and slicing operate on.\n *\n * They diverge in exactly two ways, both encoded by `charMap` anchors:\n * - a non-printing character occupies a character slot but contributes\n * ZERO text units (PDFium's `char_indices_` skip list);\n * - a supplementary-plane character contributes TWO text units (a\n * surrogate pair) once extraction is UTF-16-faithful.\n *\n * `charMap` semantics (see {@link CharMapAnchor}): an implicit head anchor\n * `[0, 0]`; between anchors every character advances the text by exactly one\n * unit; an anchor sits at `i + 1` whenever character `i` contributed\n * something other than one unit. Absent/empty map = identity\n * (`charCount === text.length`). `charMapViolation` is the validator both\n * the wire schema and engine builders share — malformed maps are rejected,\n * never silently repaired.\n *\n * Boundary model: a \"character boundary\" `c` ∈ [0, charCount] sits BEFORE\n * character `c`; `boundaryTextOffset(s, charCount) === text.length` always.\n * All ranges are half-open in both spaces.\n */\n\n/**\n * One anchor of the character→text mapping: from character index `char`\n * onward (until the next anchor), boundary `c` maps to text offset\n * `text + (c - char)`.\n */\nexport type CharMapAnchor = readonly [char: number, text: number];\n\n/** Direction to resolve a text offset that falls on a zero-width plateau —\n * see {@link charBoundaryAtTextOffset}. */\nexport type CharBoundaryBias = 'start' | 'end';\n\nconst clampInt = (value: number, lo: number, hi: number): number =>\n Math.min(Math.max(Math.floor(value), lo), hi);\n\n/** The greatest anchor with `anchor.char <= boundary` (implicit head [0,0]). */\nfunction anchorAtOrBefore(map: ReadonlyArray<CharMapAnchor>, boundary: number): CharMapAnchor {\n let lo = 0;\n let hi = map.length - 1;\n let best: CharMapAnchor | null = null;\n while (lo <= hi) {\n const mid = (lo + hi) >> 1;\n if (map[mid][0] <= boundary) {\n best = map[mid];\n lo = mid + 1;\n } else {\n hi = mid - 1;\n }\n }\n return best ?? [0, 0];\n}\n\n/**\n * Text offset of character boundary `boundary` (clamped into\n * [0, charCount]). For a dropped character this is the offset of the next\n * real text unit — exactly what half-open slicing needs at either end.\n */\nexport function boundaryTextOffset(snapshot: PageTextSnapshot, boundary: number): number {\n const b = clampInt(boundary, 0, snapshot.charCount);\n const map = snapshot.charMap;\n if (!map || map.length === 0) return Math.min(b, snapshot.text.length);\n const [char, text] = anchorAtOrBefore(map, b);\n return text + (b - char);\n}\n\n/**\n * The character boundary for a text offset, with explicit plateau bias:\n *\n * - `'start'`: the GREATEST boundary whose text offset is <= `offset` —\n * scans forward past zero-width characters, so a range start never\n * includes a dropped character that precedes its first real unit.\n * - `'end'`: the SMALLEST boundary whose text offset is >= `offset` —\n * stops before zero-width characters, so a range end never swallows a\n * dropped character that follows its last real unit.\n *\n * An offset inside a surrogate pair resolves outward under both biases\n * (start → the pair's character, end → past it), so a clipped astral\n * character is always covered whole. Prefer {@link charRangeForTextOffsets}\n * — the two biases exist to be used as a PAIR.\n */\nexport function charBoundaryAtTextOffset(\n snapshot: PageTextSnapshot,\n offset: number,\n bias: CharBoundaryBias,\n): number {\n const target = clampInt(offset, 0, snapshot.text.length);\n let lo = 0;\n let hi = snapshot.charCount;\n if (bias === 'start') {\n // Greatest boundary with B(boundary) <= target. B(0) = 0 <= target always.\n while (lo < hi) {\n const mid = lo + Math.ceil((hi - lo) / 2);\n if (boundaryTextOffset(snapshot, mid) <= target) lo = mid;\n else hi = mid - 1;\n }\n return lo;\n }\n // Smallest boundary with B(boundary) >= target. B(charCount) = text.length\n // >= target always (offset is clamped).\n while (lo < hi) {\n const mid = (lo + hi) >> 1;\n if (boundaryTextOffset(snapshot, mid) >= target) hi = mid;\n else lo = mid + 1;\n }\n return lo;\n}\n\n/**\n * The character range whose text projection is exactly\n * `text.slice(startOffset, endOffset)` — THE inverse used by search-hit\n * conversion and select-by-text. Start is right-biased and end left-biased\n * (see {@link charBoundaryAtTextOffset}), so zero-width characters adjacent\n * to the range fall OUTSIDE it on both sides.\n *\n * A non-empty text range can never invert: `end > start` implies\n * `B(end) >= endOffset > startOffset >= B(start)`, which forces\n * `end > start` by monotonicity. Empty input (`endOffset <= startOffset`)\n * normalizes to the end-rule boundary of `startOffset` on both sides.\n */\nexport function charRangeForTextOffsets(\n snapshot: PageTextSnapshot,\n startOffset: number,\n endOffset: number,\n): { start: number; end: number } {\n if (endOffset <= startOffset) {\n const b = charBoundaryAtTextOffset(snapshot, startOffset, 'end');\n return { start: b, end: b };\n }\n return {\n start: charBoundaryAtTextOffset(snapshot, startOffset, 'start'),\n end: charBoundaryAtTextOffset(snapshot, endOffset, 'end'),\n };\n}\n\n/**\n * THE copy primitive: the text projection of the half-open character range\n * `[charStart, charEnd)`. Dropped characters inside the range contribute\n * nothing; a supplementary character contributes its full surrogate pair.\n * `sliceTextByChars(s, 0, s.charCount) === s.text` always.\n */\nexport function sliceTextByChars(\n snapshot: PageTextSnapshot,\n charStart: number,\n charEnd: number,\n): string {\n if (charEnd <= charStart) return '';\n return snapshot.text.slice(\n boundaryTextOffset(snapshot, charStart),\n boundaryTextOffset(snapshot, charEnd),\n );\n}\n\n/**\n * First invariant violation of a snapshot's char map, or null when valid.\n * Shared by the wire schema (rejects malformed bodies) and engine builders\n * (dev-mode assertion). The invariants:\n *\n * 1. absent/empty map ⇒ identity, so `charCount === text.length`;\n * 2. anchor chars strictly increasing, each in [1, charCount];\n * 3. step rule: against the previous anchor (implicit head [0, 0]), the\n * character just before each anchor contributed exactly 0 units\n * (dropped) or 2 (surrogate pair):\n * `t₂ - (t₁ + (c₂ - c₁ - 1)) ∈ {0, 2}`;\n * 4. tail: `lastText + (charCount - lastChar) === text.length`\n * (equivalently `boundaryTextOffset(s, charCount) === text.length`).\n */\nexport function charMapViolation(\n charCount: number,\n textLength: number,\n charMap: ReadonlyArray<CharMapAnchor> | undefined,\n): string | null {\n if (!charMap || charMap.length === 0) {\n return charCount === textLength\n ? null\n : `identity map requires charCount (${charCount}) === text length (${textLength})`;\n }\n let prevChar = 0;\n let prevText = 0;\n for (let i = 0; i < charMap.length; i++) {\n const [char, text] = charMap[i];\n if (!Number.isInteger(char) || !Number.isInteger(text)) {\n return `anchor ${i} is not integer-valued`;\n }\n if (char <= prevChar) {\n return `anchor ${i} char ${char} not strictly after ${prevChar}`;\n }\n if (char > charCount) {\n return `anchor ${i} char ${char} outside [1, ${charCount}]`;\n }\n const step = text - (prevText + (char - prevChar - 1));\n if (step !== 0 && step !== 2) {\n return `anchor ${i} implies a character contributing ${step} text units (expected 0 or 2)`;\n }\n prevChar = char;\n prevText = text;\n }\n const tail = prevText + (charCount - prevChar);\n if (tail !== textLength) {\n return `map projects charCount to ${tail}, but text length is ${textLength}`;\n }\n return null;\n}\n","import type { PdfSize } from '../geometry/primitives';\n\n/**\n * Upper bound on `count` for a single `pages.insertBlank()` call. A CONTRACT\n * cap, enforced identically by every engine (the cloud input schema carries\n * the same number), so a UI never discovers a lower server limit by surprise.\n * Larger batches are a loop at the call site.\n */\nexport const PAGE_INSERT_BLANK_MAX_COUNT = 100;\n\n/**\n * Spec for `pages.insertBlank()`: create `count` blank pages of `size`.\n *\n * `size` is explicit and in PDF points, un-rotated — the same convention as\n * `PageLayout.size`. The \"match my neighbour\" default developers expect is a\n * registry read, so it lives in the page-edit capability, not here: the\n * engine wire never guesses (the `rotateBy` law).\n */\nexport interface PageInsertBlankSpec {\n /** Page size in PDF points. Both dimensions must be finite and > 0. */\n size: PdfSize;\n /** Pages to create, in `[1, PAGE_INSERT_BLANK_MAX_COUNT]`. Default 1. */\n count?: number;\n}\n","import type { PdfNumberFormat } from '../dto/Measure';\n\nconst group = (value: string, separator: string): string =>\n value.replace(/\\B(?=(\\d{3})+(?!\\d))/g, () => separator);\nconst gcd = (a: number, b: number): number => (b ? gcd(b, a % b) : a);\n\nexport function validNumberFormat(nf: PdfNumberFormat): boolean {\n const c = Math.fround(nf.conversion ?? NaN);\n if (!Number.isFinite(c) || c <= 0) return false;\n const d = nf.precision ?? (nf.fraction === 'fraction' ? 16 : 100);\n if (!Number.isInteger(d) || d < 1 || d > 2147483647) return false;\n if ((nf.fraction ?? 'decimal') === 'decimal' && !Number.isInteger(Math.log10(d))) return false;\n return true;\n}\n\nfunction last(value: number, nf: PdfNumberFormat): string {\n const mode = nf.fraction ?? 'decimal';\n const sep = nf.thousands ?? ',';\n if (mode === 'round') return group(String(Math.round(value)), sep);\n if (mode === 'truncate') return group(String(Math.trunc(value)), sep);\n if (mode === 'fraction') {\n const denominator = nf.precision ?? 16;\n let whole = Math.floor(value);\n let numerator = Math.round((value - whole) * denominator);\n if (numerator === denominator) {\n whole++;\n numerator = 0;\n }\n if (!numerator) return group(String(whole), sep);\n const divisor = nf.fixed ? 1 : gcd(numerator, denominator);\n return `${whole ? `${group(String(whole), sep)} ` : ''}${numerator / divisor}/${denominator / divisor}`;\n }\n const digits = Math.log10(nf.precision ?? 100);\n const [integer, decimal = ''] = value.toFixed(digits).split('.');\n const kept = nf.fixed ? decimal : decimal.replace(/0+$/, '');\n return group(integer, sep) + (kept ? (nf.decimal || '.') + kept : '');\n}\n\n/** ISO 32000-2 §12.9.2 cascade. PDF float conversions are normalized before use.\n * Unit text and explicit spacing are preserved, including trailing unit spaces. */\nexport function formatMeasurement(value: number, formats: readonly PdfNumberFormat[]): string {\n if (!Number.isFinite(value) || !formats.length || !formats.every(validNumberFormat)) {\n throw new RangeError('Measurement requires a finite value and valid number formats');\n }\n let remainder = Math.abs(value);\n const parts: string[] = [];\n for (const [index, nf] of formats.entries()) {\n remainder *= Math.fround(nf.conversion!);\n if (!Number.isFinite(remainder)) throw new RangeError('Measurement overflow');\n const whole = Math.floor(remainder);\n const fraction = remainder - whole;\n const terminal = index === formats.length - 1 || fraction === 0;\n const number = terminal ? last(remainder, nf) : group(String(whole), nf.thousands ?? ',');\n // Omit only the implicit outer padding, never trim the unit or explicit separators.\n const prefix = nf.labelPosition === 'prefix';\n const ps = nf.prefixSpacing ?? (prefix && index === 0 ? '' : ' ');\n const ss = nf.suffixSpacing ?? (!prefix && terminal ? '' : ' ');\n const label = ps + nf.unit + ss;\n parts.push(prefix ? label + number : number + label);\n if (terminal) break;\n remainder = fraction;\n }\n return (value < 0 ? '-' : '') + parts.join('');\n}\n"],"mappings":"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"}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"resolver-CYVNV4hn.cjs","names":[],"sources":["../src/signature/protection.ts","../src/auth/scope/errors.ts","../src/auth/scope/parser.ts","../src/auth/scope/resolver.ts"],"sourcesContent":["import type { DocCapability } from '../auth/scope/types';\nimport type {\n DocMdpPermission,\n DocumentFieldLock,\n DocumentProtection,\n FieldLockSpec,\n ModificationLevel,\n SignatureDTO,\n} from './types';\n\n/**\n * Bumped whenever the derivation below (or a judgement rule) changes meaning.\n * Rides every protection and every analysis, and keys the cloud's immutable\n * analysis URLs. v4 (the `signature-compat` corpus, 91 Acrobat observations):\n * each signature is judged on the net state of the document against the\n * revision it sealed; an object written again with the sealed value is not\n * a modification; an approval signature permits commenting; field\n * properties are judged per level; a lock binds only the signature that\n * declares it; a changed shared resource must be allowed by every use.\n */\nexport const SIGNATURE_POLICY_VERSION = 4;\n\n/**\n * How a validator reads an approval signature. ISO 32000 defines permitted\n * changes only for certification signatures; Acrobat reads an approval\n * signature as \"Form Fill-in, Signing and Commenting are allowed\" (its own\n * permission text under every such signature) and keeps it valid through a\n * Square annotation added, deleted or recoloured afterwards (corpus\n * `signature-compat` v3 cases 88, 90, 91: \"Annotations Created/Deleted/\n * Modified\", signature valid). pyHanko judges stricter (fill only); Acrobat\n * is the validator recipients use, so the baseline is `annotate`.\n */\nexport const APPROVAL_BASELINE: ModificationLevel = 'annotate';\n\nconst LEVEL_RANK: Record<ModificationLevel, number> = { none: 0, lta: 1, fill: 2, annotate: 3 };\n\n/** DocMDP `/P` → the modification level it permits. */\nexport function levelFromPermission(permission: DocMdpPermission): ModificationLevel {\n return permission === 1 ? 'lta' : permission === 2 ? 'fill' : 'annotate';\n}\n\nexport function minLevel(a: ModificationLevel, b: ModificationLevel): ModificationLevel {\n return LEVEL_RANK[a] <= LEVEL_RANK[b] ? a : b;\n}\n\n/** Whether `level` (null = unsigned, everything allowed) permits changes of kind `needed`. */\nexport function levelAllows(level: ModificationLevel | null, needed: ModificationLevel): boolean {\n return level === null || LEVEL_RANK[level] >= LEVEL_RANK[needed];\n}\n\n/** Hierarchical name match: `group` covers `group.total`. */\nexport function lockNameCovers(lockedName: string, fieldName: string): boolean {\n return fieldName === lockedName || fieldName.startsWith(`${lockedName}.`);\n}\n\nexport function lockCovers(spec: FieldLockSpec, fieldName: string): boolean {\n switch (spec.action) {\n case 'all':\n return true;\n case 'include':\n return spec.fields.some((name) => lockNameCovers(name, fieldName));\n case 'exclude':\n return !spec.fields.some((name) => lockNameCovers(name, fieldName));\n }\n}\n\n/**\n * Derive what the signatures already in a document mean for what comes\n * after. Pure and versioned (`SIGNATURE_POLICY_VERSION`); the same function\n * runs in the browser worker, on the native server, and in the conformance\n * suite.\n *\n * enforced = min(certification P, every signed field's /Lock /P), or null\n * when nothing was declared. What the engine refuses.\n * judged = enforced, or the approval baseline when signatures exist\n * without a declaration, or null when nothing is signed. What\n * the revision analysis judges later changes against. A\n * declaration governs the judgement too: a P=3 certifier really\n * did allow annotations.\n * fieldLocks = every signed signature's FieldMDP and the /Lock of every\n * signed field. Unsigned fields' /Lock entries describe a\n * FUTURE signature and lock nothing yet.\n */\nexport function deriveProtection(signatures: ReadonlyArray<SignatureDTO>): DocumentProtection {\n let signed = false;\n let enforced: ModificationLevel | null = null;\n let certification: DocumentProtection['certification'] = null;\n const fieldLocks: DocumentFieldLock[] = [];\n\n for (const sig of signatures) {\n if (!sig.signed) continue;\n signed = true;\n if (sig.catalogCertification && sig.docMdp !== null && certification === null) {\n certification = { signatureIndex: sig.index, permission: sig.docMdp };\n enforced = minLevel(enforced ?? 'annotate', levelFromPermission(sig.docMdp));\n }\n if (sig.fieldMdp) {\n fieldLocks.push({ signatureIndex: sig.index, source: 'fieldmdp', spec: sig.fieldMdp });\n }\n if (sig.lock) {\n fieldLocks.push({ signatureIndex: sig.index, source: 'lock', spec: sig.lock });\n if (sig.lock.permission !== undefined) {\n enforced = minLevel(enforced ?? 'annotate', levelFromPermission(sig.lock.permission));\n }\n }\n }\n\n const judged = signed ? (enforced ?? APPROVAL_BASELINE) : null;\n return { enforced, judged, certification, fieldLocks, policyVersion: SIGNATURE_POLICY_VERSION };\n}\n\n/** The lock that freezes `fieldName`, if any. */\nexport function fieldLockFor(\n protection: DocumentProtection,\n fieldName: string,\n): DocumentFieldLock | null {\n for (const lock of protection.fieldLocks) {\n if (lockCovers(lock.spec, fieldName)) return lock;\n }\n return null;\n}\n\n/**\n * The capabilities a protection removes from every caller, admin scope\n * included — document-derived authority, exactly like encryption bits.\n *\n * Only what was DECLARED is refused: a refusal is a promise a signer made,\n * never a guess about a validator. A certification (or a lock with /P)\n * declares what may follow, and everything outside that — page edits,\n * redaction, field authoring, attachments, and below P=3 annotations, below\n * P=2 form fill — is refused. A plain approval signature declares nothing:\n * the same edits stay possible and are then JUDGED, the verdict saying the\n * signature no longer holds, exactly as Acrobat does. The one exception is a\n * rewrite: it does not invalidate signatures, it erases them, so any signed\n * document refuses it. Per-field locks are enforced by the form mutator.\n */\nexport function protectedCapabilities(protection: DocumentProtection | null): Set<DocCapability> {\n const out = new Set<DocCapability>();\n if (!protection || protection.judged === null) return out;\n out.add('doc.download.flattened');\n if (protection.enforced === null) return out; // nothing declared: judged, not refused\n out.add('doc.pages.modify');\n out.add('doc.pages.assemble');\n out.add('doc.redact');\n out.add('doc.attachments.modify');\n out.add('doc.forms.modify');\n if (!levelAllows(protection.enforced, 'annotate')) out.add('doc.annotate.modify');\n if (!levelAllows(protection.enforced, 'fill')) out.add('doc.forms.fill');\n return out;\n}\n","/**\n * Thrown by {@link parseScope} / {@link validateScopeArray} when a scope\n * string does not match any known grammar (capability, collab, virtual,\n * or wildcard).\n *\n * Carries the offending scope verbatim so route/JWT-layer error\n * handlers can echo it to the customer.\n */\nexport class InvalidScope extends Error {\n constructor(\n public readonly scope: string,\n reason: string,\n ) {\n super(`invalid scope \"${scope}\": ${reason}`);\n this.name = 'InvalidScope';\n }\n}\n\n/**\n * Thrown by route guards and engine-local enforcement when a capability\n * or collab action is denied for the current request/handle.\n *\n * `required` names what the caller needed (e.g., \"doc.render\" or\n * \"annotations:update\"). `context` is an optional label such as \"local\"\n * or \"/v1/docs/.../pages/2/render\".\n */\nexport class PermissionDenied extends Error {\n constructor(\n public readonly required: string,\n public readonly context?: string,\n ) {\n super(`permission denied${context ? ` (${context})` : ''}: ${required}`);\n this.name = 'PermissionDenied';\n }\n}\n\n/**\n * Thrown at engine-local open time when the supplied scope includes\n * collab filters (`:self`, `:group=...`) but the identity claims needed\n * to resolve them are missing. Fails loudly at open so the configuration\n * mistake is visible immediately instead of producing silent denies at\n * every annotation mutation.\n */\nexport class MissingIdentity extends Error {\n constructor(scope: string) {\n super(`scope \"${scope}\" requires identity claims (user_id and/or groups)`);\n this.name = 'MissingIdentity';\n }\n}\n","import { InvalidScope } from './errors';\nimport type { CollabAction, CollabFilter, DocCapability, ParsedScope } from './types';\n\n/**\n * Closed set of recognized capability strings. Membership is the\n * authoritative source of truth — adding a new capability requires\n * extending the `DocCapability` union AND adding it here. The parser\n * rejects anything outside this set.\n *\n * Removed legacy names (e.g., `doc.read`, `doc.edit-pages`, `doc.save`)\n * are deliberately absent so any JWT carrying them fails at verify\n * time with a clear error.\n */\nconst KNOWN_CAPABILITIES: ReadonlySet<DocCapability> = new Set([\n 'doc.open',\n 'doc.render',\n 'doc.text.select',\n 'doc.text.copy',\n 'doc.text.search',\n 'doc.content.copy',\n 'doc.download',\n 'doc.download.flattened',\n 'doc.print',\n 'doc.print.high',\n 'doc.pages.modify',\n 'doc.pages.assemble',\n 'doc.forms.read',\n 'doc.forms.fill',\n 'doc.forms.modify',\n 'doc.forms.submit',\n 'doc.annotate.read',\n 'doc.annotate.modify',\n 'doc.metadata.modify',\n 'doc.attachments.modify',\n 'doc.redact',\n 'doc.sign',\n 'doc.sign.certify',\n]);\n\n/**\n * Parse a single scope string into a structured `ParsedScope`.\n *\n * Recognised shapes:\n * - `\"*\"` → wildcard\n * - `\"pdf.permissions\"` → virtual (expands at resolve time)\n * - `\"a.b.c...\"` (dotted) → capability\n * - `\"entity:action:filter\"` (colons) → collab\n *\n * Throws {@link InvalidScope} for any string that fails to match one of\n * those shapes, or whose capability/entity/action/filter is not in the\n * closed enum.\n */\nexport function parseScope(raw: string): ParsedScope {\n if (raw === '*') return { kind: 'wildcard' };\n if (raw === 'pdf.permissions') return { kind: 'virtual', name: 'pdf.permissions' };\n if (raw.includes(':')) return parseCollab(raw);\n return parseCapability(raw);\n}\n\n/**\n * Validate every entry in a scope array by attempting to parse it.\n * Throws on the first invalid string. Discards the parse output —\n * callers re-parse at resolution time to keep the array as the\n * authoritative payload.\n */\nexport function validateScopeArray(raw: ReadonlyArray<string>): void {\n for (const s of raw) parseScope(s);\n}\n\nfunction parseCollab(raw: string): ParsedScope {\n // Split on the FIRST two colons only. Filter values may contain\n // colons (UUIDs with `urn:uuid:...`, subject ids like `auth0|user:1`).\n const idx1 = raw.indexOf(':');\n const idx2 = raw.indexOf(':', idx1 + 1);\n if (idx1 === -1 || idx2 === -1) {\n throw new InvalidScope(raw, 'collab scope requires entity:action:filter');\n }\n const entity = raw.slice(0, idx1);\n const action = raw.slice(idx1 + 1, idx2);\n const filterStr = raw.slice(idx2 + 1);\n\n if (entity !== 'annotations') {\n throw new InvalidScope(raw, `unknown collab entity: ${entity}`);\n }\n if (\n action !== 'create' &&\n action !== 'update' &&\n action !== 'delete' &&\n action !== 'set-group' &&\n action !== '*'\n ) {\n throw new InvalidScope(raw, `unknown collab action: ${action}`);\n }\n const filter = parseFilter(filterStr, raw);\n\n // `set-group` is a group-assignment authority, not a per-record collab\n // filter. The only meaningful filter values are `all` (assign to any\n // group) and `group=X` (assign to group X). `:self` and `:createdBy=Y`\n // can't be sensibly applied to \"what group is this annotation in\", so\n // we reject them at parse time rather than silently always-denying.\n if (action === 'set-group' && (filter.kind === 'self' || filter.kind === 'createdBy')) {\n throw new InvalidScope(\n raw,\n `set-group only supports :all or :group=<id> filters (got :${filter.kind})`,\n );\n }\n\n return {\n kind: 'collab',\n entity,\n action: action as CollabAction | '*',\n filter,\n };\n}\n\nfunction parseFilter(s: string, raw: string): CollabFilter {\n if (s === 'all') return { kind: 'all' };\n if (s === 'self') return { kind: 'self' };\n if (s.startsWith('createdBy=')) {\n const v = s.slice('createdBy='.length);\n if (!v) throw new InvalidScope(raw, 'createdBy= requires a value');\n return { kind: 'createdBy', userId: v };\n }\n if (s.startsWith('group=')) {\n const v = s.slice('group='.length);\n if (!v) throw new InvalidScope(raw, 'group= requires a value');\n return { kind: 'group', groupId: v };\n }\n throw new InvalidScope(raw, `unknown filter: ${s}`);\n}\n\n// Capability shape: lowercase dotted segments, e.g. \"doc.text.copy\".\n// Validates shape first, then membership in the closed set.\nconst CAPABILITY_SHAPE = /^[a-z][a-z0-9_]*(\\.[a-z][a-z0-9_]*)+$/;\n\nfunction parseCapability(raw: string): ParsedScope {\n if (!CAPABILITY_SHAPE.test(raw)) {\n throw new InvalidScope(raw, 'invalid capability name shape');\n }\n if (!KNOWN_CAPABILITIES.has(raw as DocCapability)) {\n throw new InvalidScope(raw, `unknown capability: ${raw}`);\n }\n return { kind: 'capability', name: raw as DocCapability };\n}\n","import { parseScope } from './parser';\nimport type {\n CollabAction,\n CollabFilter,\n DocCapability,\n IdentityClaims,\n ParsedScope,\n PdfBits,\n} from './types';\nimport { protectedCapabilities } from '../../signature/protection';\nimport type { DocumentProtection } from '../../signature/types';\n\n/**\n * Resolved collab subject — the per-record identity bits used to test\n * collab filters against. Sourced from the target record's stored\n * `/EMBD_Metadata/UserID` and `/GroupID` at mutation time.\n */\nexport interface CollabTarget {\n userId?: string;\n groupId?: string;\n}\n\n/**\n * True iff the given capability is granted by the scope array plus the\n * PDF bits visible via `pdf.permissions` expansion.\n *\n * Wildcard `*` short-circuits to true. Anything not explicitly granted\n * (or expanded from `pdf.permissions`) is denied.\n */\nexport function checkCapability(\n capability: DocCapability,\n rawScope: ReadonlyArray<string>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): boolean {\n // Document-derived authority comes first: what the signatures in the\n // file forbid, no scope grants — the wildcard included.\n if (protectedCapabilities(protection).has(capability)) return false;\n const parsed = rawScope.map(parseScope);\n if (parsed.some((s) => s.kind === 'wildcard')) return true;\n return expandedCapabilities(parsed, pdfBits, protection).has(capability);\n}\n\n/**\n * True iff the scope grants AT LEAST ONE of `capabilities`. Convenience\n * for routes like `/text` (`doc.text.copy` OR `doc.text.search`) and\n * `/geometry` (`doc.text.select` OR `doc.text.search`).\n */\nexport function checkAnyCapability(\n capabilities: ReadonlyArray<DocCapability>,\n rawScope: ReadonlyArray<string>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): boolean {\n return capabilities.some((c) => checkCapability(c, rawScope, pdfBits, protection));\n}\n\n/**\n * True iff the scope grants the collab action against the target record.\n *\n * Narrowing model. For each action independently:\n * 1. wildcard `*` → allow (global escape hatch)\n * 2. if any collab scope applies to this action → NARROW: only those\n * collab filters decide. If none match the target, deny — even if\n * `doc.annotate.modify` is also present. This is what makes\n * `[modify, update:self]` correctly mean \"edit own only\" rather\n * than \"edit anyone via modify-bypass.\"\n * 3. otherwise, if `doc.annotate.modify` is present → allow. This is\n * the broad PDF-bit-derived default for create/update/delete when\n * no per-action collab filter has been written.\n * 4. otherwise → deny.\n *\n * For create: `target` should be built by the caller from JWT identity\n * (`{ userId: caller.user_id, groupId: caller.group_id }`). `:self` and\n * `:all` then trivially pass; `:group=X` is the meaningful filter (only\n * matches when the caller's default group is X).\n *\n * For update / delete: `target` is the existing row's owner.\n */\nexport function checkCollab(\n action: CollabAction,\n target: CollabTarget,\n rawScope: ReadonlyArray<string>,\n identity: IdentityClaims,\n pdfBits: PdfBits,\n): boolean {\n const parsed = rawScope.map(parseScope);\n if (parsed.some((s) => s.kind === 'wildcard')) return true;\n\n const applicableCollab = parsed.filter(\n (s): s is Extract<ParsedScope, { kind: 'collab' }> =>\n s.kind === 'collab' &&\n s.entity === 'annotations' &&\n (s.action === action || s.action === '*'),\n );\n\n if (applicableCollab.length > 0) {\n // Narrow: presence of any applicable collab scope shadows\n // `modify`-as-default for this action.\n return applicableCollab.some((s) => filterMatches(s.filter, target, identity));\n }\n\n // No collab scope for this action — fall back to the broad default.\n return expandedCapabilities(parsed, pdfBits).has('doc.annotate.modify');\n}\n\n/**\n * Compute the full set of granted capabilities after expanding\n * `pdf.permissions` and applying implication rules.\n *\n * Implications applied:\n * - `doc.annotate.modify` implies `doc.annotate.read` (you can't\n * sensibly modify what you can't see).\n * - `doc.forms.modify` implies `doc.forms.fill` and `doc.forms.read`.\n * - `doc.forms.fill` implies `doc.forms.read`.\n * - any annotation collab scope implies `doc.annotate.read`, because\n * mutation routes need to see the target row to evaluate the\n * collab filter against its current owner.\n *\n * Does NOT short-circuit on wildcard — callers do that themselves\n * before calling this. Returning the expanded set is useful for the\n * `/access` response's `effectiveScope` and for advisory UI surfacing.\n */\nexport function expandedCapabilities(\n parsed: ReadonlyArray<ParsedScope>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): Set<DocCapability> {\n const out = new Set<DocCapability>();\n let hasAnnotationCollab = false;\n\n for (const s of parsed) {\n if (s.kind === 'capability') {\n out.add(s.name);\n } else if (s.kind === 'virtual' && s.name === 'pdf.permissions') {\n addPdfPermissions(out, pdfBits);\n } else if (s.kind === 'collab' && s.entity === 'annotations') {\n hasAnnotationCollab = true;\n }\n }\n\n // Implications (apply after the explicit additions above)\n if (out.has('doc.annotate.modify')) out.add('doc.annotate.read');\n if (out.has('doc.forms.modify')) {\n out.add('doc.forms.fill');\n out.add('doc.forms.read');\n }\n if (out.has('doc.forms.fill')) out.add('doc.forms.read');\n if (hasAnnotationCollab) out.add('doc.annotate.read');\n\n // Subtraction last: a signed document's own restrictions win over any\n // grant or implication.\n for (const removed of protectedCapabilities(protection)) out.delete(removed);\n\n return out;\n}\n\n/**\n * Convenience wrapper: takes the raw scope array, parses each entry,\n * returns the expanded capability set. Exposed for the `/access`\n * endpoint and for the SDK helper that surfaces `effectiveScope`.\n */\nexport function expandRawScope(\n rawScope: ReadonlyArray<string>,\n pdfBits: PdfBits,\n protection: DocumentProtection | null = null,\n): Set<DocCapability> {\n return expandedCapabilities(rawScope.map(parseScope), pdfBits, protection);\n}\n\n/**\n * Test a single collab filter against a target record + the caller's\n * identity. Pure function — no side effects, no implicit rules.\n *\n * all → always matches\n * self → matches if identity.user_id === target.userId\n * createdBy=<X> → matches if target.userId === X\n * group=<X> → matches if target.groupId === X\n * AND identity.groups includes X\n *\n * The group-membership check on `group=X` prevents a token from\n * matching annotations in a group it doesn't belong to, even if the\n * target row carries that groupId.\n */\n/**\n * Authority to assign a specific groupId to an annotation.\n *\n * Set-group is decoupled from `doc.annotate.modify`. The reasoning:\n * capabilities like `modify` describe row access — what kind of write\n * you can do to which existing rows — and map to PDF permission bits.\n * Set-group is a cloud-only *assignment authority*: which destination\n * group can you put an annotation into? There is no PDF-bit\n * counterpart, so it doesn't inherit from `modify`.\n *\n * Resolution order:\n * 1. newGroupId === callerDefaultGroupId → true (no real reassignment\n * is happening; the annotation gets the caller's default group)\n * 2. wildcard `*` → true (global escape hatch)\n * 3. `annotations:set-group:all` → true\n * 4. `annotations:set-group:group=<newGroupId>` → true\n * 5. `annotations:*:all` or `annotations:*:group=<newGroupId>` → true\n * (action wildcard includes set-group)\n * 6. otherwise → false\n *\n * Note: independent from membership. A user with\n * `set-group:group=legal` can assign annotations to the legal group\n * even if they're not a member — that's the whole point.\n */\nexport function checkSetGroup(\n newGroupId: string,\n callerDefaultGroupId: string | undefined,\n rawScope: ReadonlyArray<string>,\n _pdfBits: PdfBits,\n): boolean {\n // No authority needed when the caller is assigning their default group.\n if (newGroupId === callerDefaultGroupId) return true;\n\n const parsed = rawScope.map(parseScope);\n if (parsed.some((s) => s.kind === 'wildcard')) return true;\n\n for (const s of parsed) {\n if (s.kind !== 'collab') continue;\n if (s.entity !== 'annotations') continue;\n if (s.action !== 'set-group' && s.action !== '*') continue;\n if (s.filter.kind === 'all') return true;\n if (s.filter.kind === 'group' && s.filter.groupId === newGroupId) return true;\n // :self and :createdBy are rejected at parse time for set-group, so\n // they never appear here. The action-wildcard path could carry those\n // filters legitimately for other actions — silently skip those, they\n // can't satisfy a set-group check.\n }\n return false;\n}\n\nexport function filterMatches(\n filter: CollabFilter,\n target: CollabTarget,\n id: IdentityClaims,\n): boolean {\n switch (filter.kind) {\n case 'all':\n return true;\n case 'self':\n return !!id.user_id && target.userId === id.user_id;\n case 'createdBy':\n return target.userId === filter.userId;\n case 'group':\n return target.groupId === filter.groupId && (id.groups?.includes(filter.groupId) ?? false);\n }\n}\n\n/**\n * Translate `pdf.permissions` (virtual scope) into the concrete\n * capabilities it represents under the current PDF bit configuration.\n *\n * Always adds `doc.open` and `doc.render` — these are cloud-only\n * capabilities with no PDF bit, but `pdf.permissions` is meant to be\n * the \"give the user a working session\" shorthand. Without them, a\n * token with just `['pdf.permissions']` would be useless.\n *\n * Reads are unconditional: ISO 32000 / Acrobat let any reader see\n * existing annotations and form values regardless of permission bits.\n * Bit 6 governs *writing*, not visibility.\n *\n * Bit-derived expansions follow ISO 32000:\n * bit 5 → doc.text.{select, copy, search}, doc.content.copy\n * bit 3 → doc.print\n * bit 12 → doc.print.high (requires bit 3 also set)\n * bit 4 → doc.pages.modify, doc.redact, doc.metadata.modify, doc.attachments.modify\n * bit 11 → doc.pages.assemble\n * bit 6 → doc.annotate.modify\n * bit 6/9 → doc.forms.fill\n * bit 6+4 → doc.forms.modify\n *\n * Note: this same expansion lives in builders.ts as\n * `materializePdfPermissions` for SDK-side use. The two MUST stay in\n * sync; a test in resolver.test.ts pins them together.\n */\nfunction addPdfPermissions(out: Set<DocCapability>, b: PdfBits): void {\n // Always — pdf.permissions means \"give me a working session\"\n out.add('doc.open');\n out.add('doc.render');\n // Reading existing annotations and form values is unconditional —\n // PDF bit 6 governs writes, not visibility.\n out.add('doc.annotate.read');\n out.add('doc.forms.read');\n\n if (b.bit5) {\n out.add('doc.text.select');\n out.add('doc.text.copy');\n out.add('doc.text.search');\n out.add('doc.content.copy');\n }\n if (b.bit3) out.add('doc.print');\n if (b.bit12 && b.bit3) out.add('doc.print.high');\n if (b.bit4) {\n out.add('doc.pages.modify');\n out.add('doc.redact');\n out.add('doc.metadata.modify');\n out.add('doc.attachments.modify');\n }\n if (b.bit11) out.add('doc.pages.assemble');\n if (b.bit6) {\n out.add('doc.annotate.modify');\n }\n if (b.bit6 || b.bit9) {\n out.add('doc.forms.fill');\n out.add('doc.sign');\n }\n if (b.bit6 && b.bit4) out.add('doc.forms.modify');\n}\n"],"mappings":";;;;;;;;;;;AAoBA,MAAa,2BAA2B;;;;;;;;;;;AAYxC,MAAa,oBAAuC;AAEpD,MAAM,aAAgD;CAAE,MAAM;CAAG,KAAK;CAAG,MAAM;CAAG,UAAU;AAAE;;AAG9F,SAAgB,oBAAoB,YAAiD;CACnF,OAAO,eAAe,IAAI,QAAQ,eAAe,IAAI,SAAS;AAChE;AAEA,SAAgB,SAAS,GAAsB,GAAyC;CACtF,OAAO,WAAW,MAAM,WAAW,KAAK,IAAI;AAC9C;;AAGA,SAAgB,YAAY,OAAiC,QAAoC;CAC/F,OAAO,UAAU,QAAQ,WAAW,UAAU,WAAW;AAC3D;;AAGA,SAAgB,eAAe,YAAoB,WAA4B;CAC7E,OAAO,cAAc,cAAc,UAAU,WAAW,GAAG,WAAW,EAAE;AAC1E;AAEA,SAAgB,WAAW,MAAqB,WAA4B;CAC1E,QAAQ,KAAK,QAAb;EACE,KAAK,OACH,OAAO;EACT,KAAK,WACH,OAAO,KAAK,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS,CAAC;EACnE,KAAK,WACH,OAAO,CAAC,KAAK,OAAO,MAAM,SAAS,eAAe,MAAM,SAAS,CAAC;CACtE;AACF;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,iBAAiB,YAA6D;CAC5F,IAAI,SAAS;CACb,IAAI,WAAqC;CACzC,IAAI,gBAAqD;CACzD,MAAM,aAAkC,CAAC;CAEzC,KAAK,MAAM,OAAO,YAAY;EAC5B,IAAI,CAAC,IAAI,QAAQ;EACjB,SAAS;EACT,IAAI,IAAI,wBAAwB,IAAI,WAAW,QAAQ,kBAAkB,MAAM;GAC7E,gBAAgB;IAAE,gBAAgB,IAAI;IAAO,YAAY,IAAI;GAAO;GACpE,WAAW,SAAS,YAAY,YAAY,oBAAoB,IAAI,MAAM,CAAC;EAC7E;EACA,IAAI,IAAI,UACN,WAAW,KAAK;GAAE,gBAAgB,IAAI;GAAO,QAAQ;GAAY,MAAM,IAAI;EAAS,CAAC;EAEvF,IAAI,IAAI,MAAM;GACZ,WAAW,KAAK;IAAE,gBAAgB,IAAI;IAAO,QAAQ;IAAQ,MAAM,IAAI;GAAK,CAAC;GAC7E,IAAI,IAAI,KAAK,eAAe,KAAA,GAC1B,WAAW,SAAS,YAAY,YAAY,oBAAoB,IAAI,KAAK,UAAU,CAAC;EAExF;CACF;CAGA,OAAO;EAAE;EAAU,QADJ,SAAU,YAAA,aAAiC;EAC/B;EAAe;EAAY,eAAA;CAAwC;AAChG;;AAGA,SAAgB,aACd,YACA,WAC0B;CAC1B,KAAK,MAAM,QAAQ,WAAW,YAC5B,IAAI,WAAW,KAAK,MAAM,SAAS,GAAG,OAAO;CAE/C,OAAO;AACT;;;;;;;;;;;;;;;AAgBA,SAAgB,sBAAsB,YAA2D;CAC/F,MAAM,sBAAM,IAAI,IAAmB;CACnC,IAAI,CAAC,cAAc,WAAW,WAAW,MAAM,OAAO;CACtD,IAAI,IAAI,wBAAwB;CAChC,IAAI,WAAW,aAAa,MAAM,OAAO;CACzC,IAAI,IAAI,kBAAkB;CAC1B,IAAI,IAAI,oBAAoB;CAC5B,IAAI,IAAI,YAAY;CACpB,IAAI,IAAI,wBAAwB;CAChC,IAAI,IAAI,kBAAkB;CAC1B,IAAI,CAAC,YAAY,WAAW,UAAU,UAAU,GAAG,IAAI,IAAI,qBAAqB;CAChF,IAAI,CAAC,YAAY,WAAW,UAAU,MAAM,GAAG,IAAI,IAAI,gBAAgB;CACvE,OAAO;AACT;;;;;;;;;;;AC7IA,IAAa,eAAb,cAAkC,MAAM;CACtC,YACE,OACA,QACA;EACA,MAAM,kBAAkB,MAAM,KAAK,QAAQ;EAH3B,KAAA,QAAA;EAIhB,KAAK,OAAO;CACd;AACF;;;;;;;;;AAUA,IAAa,mBAAb,cAAsC,MAAM;CAC1C,YACE,UACA,SACA;EACA,MAAM,oBAAoB,UAAU,KAAK,QAAQ,KAAK,GAAG,IAAI,UAAU;EAHvD,KAAA,WAAA;EACA,KAAA,UAAA;EAGhB,KAAK,OAAO;CACd;AACF;;;;;;;;AASA,IAAa,kBAAb,cAAqC,MAAM;CACzC,YAAY,OAAe;EACzB,MAAM,UAAU,MAAM,mDAAmD;EACzE,KAAK,OAAO;CACd;AACF;;;;;;;;;;;;;ACnCA,MAAM,qCAAiD,IAAI,IAAI;CAC7D;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;CACA;AACF,CAAC;;;;;;;;;;;;;;AAeD,SAAgB,WAAW,KAA0B;CACnD,IAAI,QAAQ,KAAK,OAAO,EAAE,MAAM,WAAW;CAC3C,IAAI,QAAQ,mBAAmB,OAAO;EAAE,MAAM;EAAW,MAAM;CAAkB;CACjF,IAAI,IAAI,SAAS,GAAG,GAAG,OAAO,YAAY,GAAG;CAC7C,OAAO,gBAAgB,GAAG;AAC5B;;;;;;;AAQA,SAAgB,mBAAmB,KAAkC;CACnE,KAAK,MAAM,KAAK,KAAK,WAAW,CAAC;AACnC;AAEA,SAAS,YAAY,KAA0B;CAG7C,MAAM,OAAO,IAAI,QAAQ,GAAG;CAC5B,MAAM,OAAO,IAAI,QAAQ,KAAK,OAAO,CAAC;CACtC,IAAI,SAAS,MAAM,SAAS,IAC1B,MAAM,IAAI,aAAa,KAAK,4CAA4C;CAE1E,MAAM,SAAS,IAAI,MAAM,GAAG,IAAI;CAChC,MAAM,SAAS,IAAI,MAAM,OAAO,GAAG,IAAI;CACvC,MAAM,YAAY,IAAI,MAAM,OAAO,CAAC;CAEpC,IAAI,WAAW,eACb,MAAM,IAAI,aAAa,KAAK,0BAA0B,QAAQ;CAEhE,IACE,WAAW,YACX,WAAW,YACX,WAAW,YACX,WAAW,eACX,WAAW,KAEX,MAAM,IAAI,aAAa,KAAK,0BAA0B,QAAQ;CAEhE,MAAM,SAAS,YAAY,WAAW,GAAG;CAOzC,IAAI,WAAW,gBAAgB,OAAO,SAAS,UAAU,OAAO,SAAS,cACvE,MAAM,IAAI,aACR,KACA,6DAA6D,OAAO,KAAK,EAC3E;CAGF,OAAO;EACL,MAAM;EACN;EACQ;EACR;CACF;AACF;AAEA,SAAS,YAAY,GAAW,KAA2B;CACzD,IAAI,MAAM,OAAO,OAAO,EAAE,MAAM,MAAM;CACtC,IAAI,MAAM,QAAQ,OAAO,EAAE,MAAM,OAAO;CACxC,IAAI,EAAE,WAAW,YAAY,GAAG;EAC9B,MAAM,IAAI,EAAE,MAAM,EAAmB;EACrC,IAAI,CAAC,GAAG,MAAM,IAAI,aAAa,KAAK,6BAA6B;EACjE,OAAO;GAAE,MAAM;GAAa,QAAQ;EAAE;CACxC;CACA,IAAI,EAAE,WAAW,QAAQ,GAAG;EAC1B,MAAM,IAAI,EAAE,MAAM,CAAe;EACjC,IAAI,CAAC,GAAG,MAAM,IAAI,aAAa,KAAK,yBAAyB;EAC7D,OAAO;GAAE,MAAM;GAAS,SAAS;EAAE;CACrC;CACA,MAAM,IAAI,aAAa,KAAK,mBAAmB,GAAG;AACpD;AAIA,MAAM,mBAAmB;AAEzB,SAAS,gBAAgB,KAA0B;CACjD,IAAI,CAAC,iBAAiB,KAAK,GAAG,GAC5B,MAAM,IAAI,aAAa,KAAK,+BAA+B;CAE7D,IAAI,CAAC,mBAAmB,IAAI,GAAoB,GAC9C,MAAM,IAAI,aAAa,KAAK,uBAAuB,KAAK;CAE1D,OAAO;EAAE,MAAM;EAAc,MAAM;CAAqB;AAC1D;;;;;;;;;;AClHA,SAAgB,gBACd,YACA,UACA,SACA,aAAwC,MAC/B;CAGT,IAAI,sBAAsB,UAAU,CAAC,CAAC,IAAI,UAAU,GAAG,OAAO;CAC9D,MAAM,SAAS,SAAS,IAAI,UAAU;CACtC,IAAI,OAAO,MAAM,MAAM,EAAE,SAAS,UAAU,GAAG,OAAO;CACtD,OAAO,qBAAqB,QAAQ,SAAS,UAAU,CAAC,CAAC,IAAI,UAAU;AACzE;;;;;;AAOA,SAAgB,mBACd,cACA,UACA,SACA,aAAwC,MAC/B;CACT,OAAO,aAAa,MAAM,MAAM,gBAAgB,GAAG,UAAU,SAAS,UAAU,CAAC;AACnF;;;;;;;;;;;;;;;;;;;;;;;AAwBA,SAAgB,YACd,QACA,QACA,UACA,UACA,SACS;CACT,MAAM,SAAS,SAAS,IAAI,UAAU;CACtC,IAAI,OAAO,MAAM,MAAM,EAAE,SAAS,UAAU,GAAG,OAAO;CAEtD,MAAM,mBAAmB,OAAO,QAC7B,MACC,EAAE,SAAS,YACX,EAAE,WAAW,kBACZ,EAAE,WAAW,UAAU,EAAE,WAAW,IACzC;CAEA,IAAI,iBAAiB,SAAS,GAG5B,OAAO,iBAAiB,MAAM,MAAM,cAAc,EAAE,QAAQ,QAAQ,QAAQ,CAAC;CAI/E,OAAO,qBAAqB,QAAQ,OAAO,CAAC,CAAC,IAAI,qBAAqB;AACxE;;;;;;;;;;;;;;;;;;AAmBA,SAAgB,qBACd,QACA,SACA,aAAwC,MACpB;CACpB,MAAM,sBAAM,IAAI,IAAmB;CACnC,IAAI,sBAAsB;CAE1B,KAAK,MAAM,KAAK,QACd,IAAI,EAAE,SAAS,cACb,IAAI,IAAI,EAAE,IAAI;MACT,IAAI,EAAE,SAAS,aAAa,EAAE,SAAS,mBAC5C,kBAAkB,KAAK,OAAO;MACzB,IAAI,EAAE,SAAS,YAAY,EAAE,WAAW,eAC7C,sBAAsB;CAK1B,IAAI,IAAI,IAAI,qBAAqB,GAAG,IAAI,IAAI,mBAAmB;CAC/D,IAAI,IAAI,IAAI,kBAAkB,GAAG;EAC/B,IAAI,IAAI,gBAAgB;EACxB,IAAI,IAAI,gBAAgB;CAC1B;CACA,IAAI,IAAI,IAAI,gBAAgB,GAAG,IAAI,IAAI,gBAAgB;CACvD,IAAI,qBAAqB,IAAI,IAAI,mBAAmB;CAIpD,KAAK,MAAM,WAAW,sBAAsB,UAAU,GAAG,IAAI,OAAO,OAAO;CAE3E,OAAO;AACT;;;;;;AAOA,SAAgB,eACd,UACA,SACA,aAAwC,MACpB;CACpB,OAAO,qBAAqB,SAAS,IAAI,UAAU,GAAG,SAAS,UAAU;AAC3E;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAwCA,SAAgB,cACd,YACA,sBACA,UACA,UACS;CAET,IAAI,eAAe,sBAAsB,OAAO;CAEhD,MAAM,SAAS,SAAS,IAAI,UAAU;CACtC,IAAI,OAAO,MAAM,MAAM,EAAE,SAAS,UAAU,GAAG,OAAO;CAEtD,KAAK,MAAM,KAAK,QAAQ;EACtB,IAAI,EAAE,SAAS,UAAU;EACzB,IAAI,EAAE,WAAW,eAAe;EAChC,IAAI,EAAE,WAAW,eAAe,EAAE,WAAW,KAAK;EAClD,IAAI,EAAE,OAAO,SAAS,OAAO,OAAO;EACpC,IAAI,EAAE,OAAO,SAAS,WAAW,EAAE,OAAO,YAAY,YAAY,OAAO;CAK3E;CACA,OAAO;AACT;AAEA,SAAgB,cACd,QACA,QACA,IACS;CACT,QAAQ,OAAO,MAAf;EACE,KAAK,OACH,OAAO;EACT,KAAK,QACH,OAAO,CAAC,CAAC,GAAG,WAAW,OAAO,WAAW,GAAG;EAC9C,KAAK,aACH,OAAO,OAAO,WAAW,OAAO;EAClC,KAAK,SACH,OAAO,OAAO,YAAY,OAAO,YAAY,GAAG,QAAQ,SAAS,OAAO,OAAO,KAAK;CACxF;AACF;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAS,kBAAkB,KAAyB,GAAkB;CAEpE,IAAI,IAAI,UAAU;CAClB,IAAI,IAAI,YAAY;CAGpB,IAAI,IAAI,mBAAmB;CAC3B,IAAI,IAAI,gBAAgB;CAExB,IAAI,EAAE,MAAM;EACV,IAAI,IAAI,iBAAiB;EACzB,IAAI,IAAI,eAAe;EACvB,IAAI,IAAI,iBAAiB;EACzB,IAAI,IAAI,kBAAkB;CAC5B;CACA,IAAI,EAAE,MAAM,IAAI,IAAI,WAAW;CAC/B,IAAI,EAAE,SAAS,EAAE,MAAM,IAAI,IAAI,gBAAgB;CAC/C,IAAI,EAAE,MAAM;EACV,IAAI,IAAI,kBAAkB;EAC1B,IAAI,IAAI,YAAY;EACpB,IAAI,IAAI,qBAAqB;EAC7B,IAAI,IAAI,wBAAwB;CAClC;CACA,IAAI,EAAE,OAAO,IAAI,IAAI,oBAAoB;CACzC,IAAI,EAAE,MACJ,IAAI,IAAI,qBAAqB;CAE/B,IAAI,EAAE,QAAQ,EAAE,MAAM;EACpB,IAAI,IAAI,gBAAgB;EACxB,IAAI,IAAI,UAAU;CACpB;CACA,IAAI,EAAE,QAAQ,EAAE,MAAM,IAAI,IAAI,kBAAkB;AAClD"}
|