web-doc 0.6.2 → 0.8.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.
- package/THIRD_PARTY_NOTICES.md +33 -1
- package/dist/adapters/docx-images.d.ts +13 -4
- package/dist/adapters/docx-images.js +3 -0
- package/dist/adapters/docx-paragraphs.d.ts +65 -0
- package/dist/adapters/docx-paragraphs.js +165 -0
- package/dist/adapters/docx-prepass.d.ts +17 -0
- package/dist/adapters/docx-prepass.js +230 -0
- package/dist/adapters/office.d.ts +31 -5
- package/dist/adapters/office.js +60 -29
- package/dist/adapters/pdf.d.ts +9 -0
- package/dist/adapters/pdf.js +10 -0
- package/dist/assets/pdfium/pdfium.wasm +0 -0
- package/dist/contracts.d.ts +53 -2
- package/dist/edit/ai/outline.d.ts +47 -0
- package/dist/edit/ai/outline.js +338 -0
- package/dist/edit/ai/targets.d.ts +16 -0
- package/dist/edit/ai/targets.js +309 -0
- package/dist/edit/ai/tools.d.ts +28 -0
- package/dist/edit/ai/tools.js +605 -0
- package/dist/edit/ai/types.d.ts +175 -0
- package/dist/edit/ai/types.js +1 -0
- package/dist/edit/assets.d.ts +23 -0
- package/dist/edit/assets.js +75 -0
- package/dist/edit/docx/elements.d.ts +6 -0
- package/dist/edit/docx/elements.js +90 -0
- package/dist/edit/docx/engine.d.ts +57 -0
- package/dist/edit/docx/engine.js +547 -0
- package/dist/edit/docx/handlers.d.ts +3 -0
- package/dist/edit/docx/handlers.js +15 -0
- package/dist/edit/docx/ids.d.ts +45 -0
- package/dist/edit/docx/ids.js +101 -0
- package/dist/edit/docx/model.d.ts +94 -0
- package/dist/edit/docx/model.js +350 -0
- package/dist/edit/docx/operations.d.ts +47 -0
- package/dist/edit/docx/operations.js +3 -0
- package/dist/edit/docx/provider.d.ts +16 -0
- package/dist/edit/docx/provider.js +37 -0
- package/dist/edit/docx/schemas.d.ts +6 -0
- package/dist/edit/docx/schemas.js +192 -0
- package/dist/edit/docx/session.d.ts +48 -0
- package/dist/edit/docx/session.js +467 -0
- package/dist/edit/docx/structure-ops.d.ts +6 -0
- package/dist/edit/docx/structure-ops.js +529 -0
- package/dist/edit/docx/style.d.ts +45 -0
- package/dist/edit/docx/style.js +375 -0
- package/dist/edit/docx/table-ops.d.ts +4 -0
- package/dist/edit/docx/table-ops.js +241 -0
- package/dist/edit/docx/text-ops.d.ts +40 -0
- package/dist/edit/docx/text-ops.js +491 -0
- package/dist/edit/docx/text.d.ts +34 -0
- package/dist/edit/docx/text.js +284 -0
- package/dist/edit/docx/tracked.d.ts +52 -0
- package/dist/edit/docx/tracked.js +347 -0
- package/dist/edit/docx/types.d.ts +206 -0
- package/dist/edit/docx/types.js +1 -0
- package/dist/edit/docx/write.d.ts +64 -0
- package/dist/edit/docx/write.js +375 -0
- package/dist/edit/engine.d.ts +128 -0
- package/dist/edit/engine.js +1 -0
- package/dist/edit/history.d.ts +77 -0
- package/dist/edit/history.js +123 -0
- package/dist/edit/ooxml/names.d.ts +20 -0
- package/dist/edit/ooxml/names.js +61 -0
- package/dist/edit/ooxml/opc.d.ts +50 -0
- package/dist/edit/ooxml/opc.js +150 -0
- package/dist/edit/ooxml/package.d.ts +82 -0
- package/dist/edit/ooxml/package.js +233 -0
- package/dist/edit/ooxml/patch.d.ts +51 -0
- package/dist/edit/ooxml/patch.js +250 -0
- package/dist/edit/ooxml/transaction.d.ts +39 -0
- package/dist/edit/ooxml/transaction.js +316 -0
- package/dist/edit/ooxml/worker.d.ts +8 -0
- package/dist/edit/ooxml/worker.js +12 -0
- package/dist/edit/ooxml/writer.d.ts +21 -0
- package/dist/edit/ooxml/writer.js +187 -0
- package/dist/edit/ooxml/xml.d.ts +74 -0
- package/dist/edit/ooxml/xml.js +451 -0
- package/dist/edit/ooxml/zip.d.ts +54 -0
- package/dist/edit/ooxml/zip.js +280 -0
- package/dist/edit/operations.d.ts +19 -0
- package/dist/edit/operations.js +137 -0
- package/dist/edit/pdf/engine/compact.d.ts +6 -0
- package/dist/edit/pdf/engine/compact.js +442 -0
- package/dist/edit/pdf/engine/document.d.ts +95 -0
- package/dist/edit/pdf/engine/document.js +868 -0
- package/dist/edit/pdf/engine/elements.d.ts +45 -0
- package/dist/edit/pdf/engine/elements.js +313 -0
- package/dist/edit/pdf/engine/existing-text.d.ts +4 -0
- package/dist/edit/pdf/engine/existing-text.js +424 -0
- package/dist/edit/pdf/engine/fonts.d.ts +78 -0
- package/dist/edit/pdf/engine/fonts.js +466 -0
- package/dist/edit/pdf/engine/geometry.d.ts +36 -0
- package/dist/edit/pdf/engine/geometry.js +97 -0
- package/dist/edit/pdf/engine/handler.d.ts +21 -0
- package/dist/edit/pdf/engine/handler.js +108 -0
- package/dist/edit/pdf/engine/images.d.ts +33 -0
- package/dist/edit/pdf/engine/images.js +188 -0
- package/dist/edit/pdf/engine/layout.d.ts +23 -0
- package/dist/edit/pdf/engine/layout.js +177 -0
- package/dist/edit/pdf/engine/operations.d.ts +67 -0
- package/dist/edit/pdf/engine/operations.js +3 -0
- package/dist/edit/pdf/engine/pages.d.ts +6 -0
- package/dist/edit/pdf/engine/pages.js +98 -0
- package/dist/edit/pdf/engine/pdfium.d.ts +195 -0
- package/dist/edit/pdf/engine/pdfium.js +249 -0
- package/dist/edit/pdf/engine/shapes.d.ts +4 -0
- package/dist/edit/pdf/engine/shapes.js +183 -0
- package/dist/edit/pdf/engine/tables.d.ts +44 -0
- package/dist/edit/pdf/engine/tables.js +315 -0
- package/dist/edit/pdf/engine/text-box.d.ts +71 -0
- package/dist/edit/pdf/engine/text-box.js +317 -0
- package/dist/edit/pdf/engine/text-layout.d.ts +28 -0
- package/dist/edit/pdf/engine/text-layout.js +67 -0
- package/dist/edit/pdf/engine/transform.d.ts +5 -0
- package/dist/edit/pdf/engine/transform.js +137 -0
- package/dist/edit/pdf/provider.d.ts +35 -0
- package/dist/edit/pdf/provider.js +92 -0
- package/dist/edit/pdf/range-map.d.ts +16 -0
- package/dist/edit/pdf/range-map.js +107 -0
- package/dist/edit/pdf/schemas.d.ts +11 -0
- package/dist/edit/pdf/schemas.js +291 -0
- package/dist/edit/pdf/selection.d.ts +5 -0
- package/dist/edit/pdf/selection.js +152 -0
- package/dist/edit/pdf/session.d.ts +63 -0
- package/dist/edit/pdf/session.js +312 -0
- package/dist/edit/pdf/types.d.ts +336 -0
- package/dist/edit/pdf/types.js +1 -0
- package/dist/edit/pptx/elements.d.ts +74 -0
- package/dist/edit/pptx/elements.js +301 -0
- package/dist/edit/pptx/engine.d.ts +37 -0
- package/dist/edit/pptx/engine.js +466 -0
- package/dist/edit/pptx/geometry.d.ts +72 -0
- package/dist/edit/pptx/geometry.js +193 -0
- package/dist/edit/pptx/handler.d.ts +7 -0
- package/dist/edit/pptx/handler.js +99 -0
- package/dist/edit/pptx/handlers.d.ts +3 -0
- package/dist/edit/pptx/handlers.js +21 -0
- package/dist/edit/pptx/image-table-ops.d.ts +5 -0
- package/dist/edit/pptx/image-table-ops.js +233 -0
- package/dist/edit/pptx/model.d.ts +69 -0
- package/dist/edit/pptx/model.js +170 -0
- package/dist/edit/pptx/operations.d.ts +50 -0
- package/dist/edit/pptx/operations.js +11 -0
- package/dist/edit/pptx/provider.d.ts +17 -0
- package/dist/edit/pptx/provider.js +40 -0
- package/dist/edit/pptx/schemas.d.ts +6 -0
- package/dist/edit/pptx/schemas.js +220 -0
- package/dist/edit/pptx/session.d.ts +54 -0
- package/dist/edit/pptx/session.js +145 -0
- package/dist/edit/pptx/shape-ops.d.ts +10 -0
- package/dist/edit/pptx/shape-ops.js +486 -0
- package/dist/edit/pptx/slide-ops.d.ts +6 -0
- package/dist/edit/pptx/slide-ops.js +353 -0
- package/dist/edit/pptx/style.d.ts +31 -0
- package/dist/edit/pptx/style.js +181 -0
- package/dist/edit/pptx/text-ops.d.ts +13 -0
- package/dist/edit/pptx/text-ops.js +267 -0
- package/dist/edit/pptx/text-write.d.ts +67 -0
- package/dist/edit/pptx/text-write.js +293 -0
- package/dist/edit/pptx/text.d.ts +41 -0
- package/dist/edit/pptx/text.js +99 -0
- package/dist/edit/pptx/types.d.ts +232 -0
- package/dist/edit/pptx/types.js +1 -0
- package/dist/edit/schema.d.ts +11 -0
- package/dist/edit/schema.js +275 -0
- package/dist/edit/session.d.ts +77 -0
- package/dist/edit/session.js +943 -0
- package/dist/edit/sessions.d.ts +8 -0
- package/dist/edit/sessions.js +1 -0
- package/dist/edit/types.d.ts +290 -0
- package/dist/edit/types.js +1 -0
- package/dist/edit/worker-engine.d.ts +31 -0
- package/dist/edit/worker-engine.js +91 -0
- package/dist/fonts/THIRD_PARTY_NOTICES.md +3 -0
- package/dist/fonts/manifest.json +6 -1
- package/dist/fonts/noto-sans-latin-cyrillic.ttf +0 -0
- package/dist/fonts.d.ts +2 -0
- package/dist/fonts.js +4 -0
- package/dist/fuzzy-alignment.d.ts +11 -4
- package/dist/fuzzy-alignment.js +3 -9
- package/dist/headless.d.ts +5 -1
- package/dist/headless.js +8 -1
- package/dist/index.d.ts +10 -2
- package/dist/index.js +16 -2
- package/dist/limits.js +6 -0
- package/dist/ooxml-edit-worker.d.ts +1 -0
- package/dist/ooxml-edit-worker.js +8 -0
- package/dist/pdf-edit-worker.d.ts +1 -0
- package/dist/pdf-edit-worker.js +35 -0
- package/dist/spreadsheet-viewport.d.ts +5 -0
- package/dist/spreadsheet-viewport.js +11 -0
- package/dist/ui.js +7 -0
- package/dist/viewer.d.ts +6 -0
- package/dist/viewer.js +293 -19
- package/dist/viewport.d.ts +13 -0
- package/dist/viewport.js +126 -1
- package/dist/worker-protocol.d.ts +39 -2
- package/dist/workers/fuzzy-search-worker.js +1 -1
- package/dist/workers/ooxml-edit-worker.js +9212 -0
- package/dist/workers/pdf-edit-worker.js +10847 -0
- package/package.json +3 -3
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
import type { DocumentFormat, RegisteredFont, ResourceLimits, TextRun, ViewerWarning } from "../contracts.js";
|
|
2
|
+
import type { EditSession } from "./sessions.js";
|
|
3
|
+
import type { ChangeMode, EditableFormat, EditElement, EditFindOptions, EditOperation, EditSessionBase, ElementQuery, OperationIssue, OperationSchemaSet, PagePoint, ReadItem, ReadOptions, ReadResult, TextTarget } from "./types.js";
|
|
4
|
+
export interface EditEngineContext {
|
|
5
|
+
readonly format: EditableFormat;
|
|
6
|
+
readonly fileName?: string;
|
|
7
|
+
readonly limits: ResourceLimits;
|
|
8
|
+
readonly assetBaseUrl?: URL;
|
|
9
|
+
/** Fonts the host registered with the client, for text the engine writes. */
|
|
10
|
+
readonly fonts?: readonly RegisteredFont[];
|
|
11
|
+
readonly signal: AbortSignal;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* The format-independent session a provider wraps into its typed session.
|
|
15
|
+
* The read hooks let a typed session add engine reads that queue behind
|
|
16
|
+
* earlier calls and carry the same envelope as the core's own reads.
|
|
17
|
+
*/
|
|
18
|
+
export interface EditSessionCore extends EditSessionBase<EditOperation, EditElement> {
|
|
19
|
+
/** The host's limits, for reads a typed session builds over the core. */
|
|
20
|
+
readonly limits: ResourceLimits;
|
|
21
|
+
readItem<T>(options: ReadOptions | undefined, task: (engine: EditEngine, signal: AbortSignal) => Promise<T | undefined>): Promise<ReadItem<T>>;
|
|
22
|
+
readItems<T>(options: ReadOptions | undefined, task: (engine: EditEngine, signal: AbortSignal) => Promise<readonly T[]>): Promise<ReadResult<T>>;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* What the viewer lends a typed session beyond the core: the text runs of
|
|
26
|
+
* the shown document, for formats whose engine never lays out (DOCX joins
|
|
27
|
+
* its elements with the renderer's runs on the main thread).
|
|
28
|
+
*/
|
|
29
|
+
export interface EditSessionAccess {
|
|
30
|
+
/** Text runs of a page of the shown document; computed on demand and cached by the viewer. */
|
|
31
|
+
getTextRuns(pageIndex: number, signal?: AbortSignal): Promise<readonly TextRun[]>;
|
|
32
|
+
/** Pages whose text runs are already known, in no particular order. */
|
|
33
|
+
cachedPages(): readonly number[];
|
|
34
|
+
}
|
|
35
|
+
/** Advertised by a `DocumentAdapter` that can edit some of its formats. */
|
|
36
|
+
export interface EditEngineProvider {
|
|
37
|
+
/** Document formats the provider edits; variants map to a session format (pptm → pptx). */
|
|
38
|
+
readonly formats: readonly DocumentFormat[];
|
|
39
|
+
load(original: Uint8Array, context: EditEngineContext): Promise<EditEngine>;
|
|
40
|
+
/** Adds the format's typed methods on top of the core session. */
|
|
41
|
+
createSession(core: EditSessionCore, access: EditSessionAccess): EditSession;
|
|
42
|
+
}
|
|
43
|
+
/** Bytes of a state, with what the engine could not guarantee about them. */
|
|
44
|
+
export interface MaterializedDocument {
|
|
45
|
+
readonly bytes: Uint8Array;
|
|
46
|
+
/** For example `privacy-not-guaranteed` when a PDF full save could not be compacted. */
|
|
47
|
+
readonly warnings: readonly ViewerWarning[];
|
|
48
|
+
}
|
|
49
|
+
/** How a batch is written; travels with the batch to the engine and into the history, so a replay writes it the same way. */
|
|
50
|
+
export interface BatchMode {
|
|
51
|
+
/** Default `direct`; `tracked` writes revisions where the format has them. */
|
|
52
|
+
readonly changeMode?: ChangeMode;
|
|
53
|
+
/** The author of tracked changes. */
|
|
54
|
+
readonly author?: string;
|
|
55
|
+
/** ISO 8601, the date of tracked changes. */
|
|
56
|
+
readonly timestamp?: string;
|
|
57
|
+
}
|
|
58
|
+
/** A batch with the identity the core assigned to the state after it. */
|
|
59
|
+
export interface EngineBatch extends BatchMode {
|
|
60
|
+
/** Unique within the session and never reused; engines derive created ids from it. */
|
|
61
|
+
readonly stateId: number;
|
|
62
|
+
readonly operations: readonly EditOperation[];
|
|
63
|
+
}
|
|
64
|
+
/** Format-specific save fields, as the session's `save()` received them without the signal. */
|
|
65
|
+
export type MaterializeOptions = Readonly<Record<string, unknown>>;
|
|
66
|
+
/** What `restore` rebuilds: a base document (a checkpoint, else the original) plus batches. */
|
|
67
|
+
export interface RestoreTarget {
|
|
68
|
+
readonly base?: Uint8Array;
|
|
69
|
+
readonly batches: readonly EngineBatch[];
|
|
70
|
+
}
|
|
71
|
+
export interface EngineChange {
|
|
72
|
+
readonly createdIds: readonly string[];
|
|
73
|
+
/** Ids that no longer exist after the batch, including every element of a deleted page. */
|
|
74
|
+
readonly removedIds: readonly string[];
|
|
75
|
+
/** Old id → new id, when the format had to rename an element. */
|
|
76
|
+
readonly remappedIds?: Readonly<Record<string, string>>;
|
|
77
|
+
/** A superset of the pages whose content changed. */
|
|
78
|
+
readonly changedPages: readonly number[];
|
|
79
|
+
/**
|
|
80
|
+
* Flow formats: the `paragraphId` of the text runs where the document
|
|
81
|
+
* reflows; the host turns it into every page from that paragraph's first
|
|
82
|
+
* page to the end, and `changedPages` is ignored.
|
|
83
|
+
*/
|
|
84
|
+
readonly reflowFrom?: string;
|
|
85
|
+
/** Optional: the core takes the count from the renderer and only cross-checks this. */
|
|
86
|
+
readonly pageCount?: number;
|
|
87
|
+
readonly warnings: readonly ViewerWarning[];
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* A mutable working copy of one document. The core serializes every call, so
|
|
91
|
+
* an engine never sees two calls at once.
|
|
92
|
+
*/
|
|
93
|
+
export interface EditEngine {
|
|
94
|
+
readonly schemas: OperationSchemaSet;
|
|
95
|
+
/** Validates against the current state; an empty result means the batch can be applied. */
|
|
96
|
+
validate(operations: readonly EditOperation[], signal: AbortSignal, mode?: BatchMode): Promise<readonly OperationIssue[]>;
|
|
97
|
+
/**
|
|
98
|
+
* Applies an already validated batch to the working copy. Same-batch
|
|
99
|
+
* references (`"$<n>"` targets) are resolved here; one that resolves to an
|
|
100
|
+
* element the operation cannot act on rejects with `invalid-operation`.
|
|
101
|
+
*/
|
|
102
|
+
apply(batch: EngineBatch, signal: AbortSignal): Promise<EngineChange>;
|
|
103
|
+
/**
|
|
104
|
+
* Bytes of the current state; the original bytes when nothing changed.
|
|
105
|
+
* `show` asks for what the viewer reopens, `save` for what the host keeps;
|
|
106
|
+
* formats read their own fields from `options` (the PDF save mode).
|
|
107
|
+
*/
|
|
108
|
+
materialize(purpose: "show" | "save", options: MaterializeOptions, signal: AbortSignal): Promise<Uint8Array>;
|
|
109
|
+
/**
|
|
110
|
+
* `materialize` with what the bytes do not guarantee; an engine without it
|
|
111
|
+
* is taken to guarantee everything.
|
|
112
|
+
*/
|
|
113
|
+
materializeDocument?(purpose: "show" | "save", options: MaterializeOptions, signal: AbortSignal): Promise<MaterializedDocument>;
|
|
114
|
+
/**
|
|
115
|
+
* Rebuilds a state: the base document (a checkpoint, else the original)
|
|
116
|
+
* with the batches applied in order. Used by undo, redo, reset, dry runs
|
|
117
|
+
* and recovery.
|
|
118
|
+
*/
|
|
119
|
+
restore(target: RestoreTarget, signal: AbortSignal): Promise<void>;
|
|
120
|
+
/** Keeps asset bytes for the session; operations refer to them by id. */
|
|
121
|
+
putAsset(id: string, data: Uint8Array, signal: AbortSignal): Promise<void>;
|
|
122
|
+
getElements(query: ElementQuery, signal: AbortSignal): Promise<readonly EditElement[]>;
|
|
123
|
+
/** Direct lookup; without it the core scans `getElements`. */
|
|
124
|
+
getElement?(id: string, signal: AbortSignal): Promise<EditElement | undefined>;
|
|
125
|
+
elementsAt(pageIndex: number, point: PagePoint, signal: AbortSignal): Promise<readonly EditElement[]>;
|
|
126
|
+
findText(query: string, options: EditFindOptions, signal: AbortSignal): Promise<readonly TextTarget[]>;
|
|
127
|
+
dispose(): Promise<void>;
|
|
128
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { BatchMode, EngineBatch } from "./engine.js";
|
|
2
|
+
import type { EditOperation } from "./types.js";
|
|
3
|
+
export interface HistoryEntry extends BatchMode {
|
|
4
|
+
readonly operations: readonly EditOperation[];
|
|
5
|
+
readonly label?: string;
|
|
6
|
+
/** Ids the batch created; an undo removes them again. */
|
|
7
|
+
readonly createdIds: readonly string[];
|
|
8
|
+
/** Ids the batch removed; a redo removes them again. */
|
|
9
|
+
readonly removedIds: readonly string[];
|
|
10
|
+
/** Page indexes the batch changed, in the document after it. */
|
|
11
|
+
readonly changedPages: readonly number[];
|
|
12
|
+
/** Flow formats: the paragraph the document reflows from, for undo and redo. */
|
|
13
|
+
readonly reflowFrom?: string;
|
|
14
|
+
readonly pageCountBefore: number;
|
|
15
|
+
readonly pageCountAfter: number;
|
|
16
|
+
/** Identifies the content after this batch; equal ids mean equal content. */
|
|
17
|
+
readonly stateId: number;
|
|
18
|
+
/**
|
|
19
|
+
* A restore to a named checkpoint: the content after this entry is the
|
|
20
|
+
* checkpoint's, so a replay starts from its retained bytes or from the
|
|
21
|
+
* original plus `batches`, never from the entries before.
|
|
22
|
+
*/
|
|
23
|
+
readonly base?: HistoryBase;
|
|
24
|
+
}
|
|
25
|
+
/** Where a restore entry's content comes from. */
|
|
26
|
+
export interface HistoryBase {
|
|
27
|
+
/** The checkpoint's state id; its bytes may be retained under it. */
|
|
28
|
+
readonly stateId: number;
|
|
29
|
+
/** The batches that build the checkpoint's state from the original. */
|
|
30
|
+
readonly batches: readonly EngineBatch[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Linear undo history over an immutable original. Entries beyond the limit are
|
|
34
|
+
* folded into the starting point: they stay applied but can no longer be
|
|
35
|
+
* undone. Every reachable content state has a numeric id, so "unchanged since
|
|
36
|
+
* the last save" is an id comparison.
|
|
37
|
+
*/
|
|
38
|
+
export declare class EditHistory {
|
|
39
|
+
#private;
|
|
40
|
+
constructor(limit: number, originalPageCount: number);
|
|
41
|
+
get canUndo(): boolean;
|
|
42
|
+
get canRedo(): boolean;
|
|
43
|
+
get isPristine(): boolean;
|
|
44
|
+
/** The id the next pushed entry will get; engines derive created ids from it. */
|
|
45
|
+
get nextStateId(): number;
|
|
46
|
+
/** Content id of the current state; 0 is the original document. */
|
|
47
|
+
get stateId(): number;
|
|
48
|
+
get pageCount(): number;
|
|
49
|
+
/** State ids of every entry still in the history, folded and redo tail included. */
|
|
50
|
+
get stateIds(): readonly number[];
|
|
51
|
+
/** Every entry still in the history: folded ones, then the undoable ones and the redo tail. */
|
|
52
|
+
get allEntries(): readonly HistoryEntry[];
|
|
53
|
+
/** The entry `undo()` would revert, if any. */
|
|
54
|
+
get undoEntry(): HistoryEntry | undefined;
|
|
55
|
+
/** The entry `redo()` would re-apply, if any. */
|
|
56
|
+
get redoEntry(): HistoryEntry | undefined;
|
|
57
|
+
/** Batches applied to the original in the current state. */
|
|
58
|
+
applied(): readonly EngineBatch[];
|
|
59
|
+
/** Batches applied to the original when `position` undoable entries are applied. */
|
|
60
|
+
batchesAt(position: number): readonly EngineBatch[];
|
|
61
|
+
/** Entries applied when `position` undoable entries are applied, folded ones first. */
|
|
62
|
+
entriesAt(position: number): readonly HistoryEntry[];
|
|
63
|
+
get position(): number;
|
|
64
|
+
/**
|
|
65
|
+
* Appends an entry with a fresh state id, or with `stateId` when the entry
|
|
66
|
+
* reproduces a known state (a restore to a checkpoint): the same id means
|
|
67
|
+
* the same content, so `dirty` stays exact.
|
|
68
|
+
*/
|
|
69
|
+
push(entry: Omit<HistoryEntry, "stateId">, stateId?: number): HistoryEntry;
|
|
70
|
+
undo(): void;
|
|
71
|
+
redo(): void;
|
|
72
|
+
clear(): void;
|
|
73
|
+
}
|
|
74
|
+
/** The engine batch an entry replays as: its operations and how they were written. */
|
|
75
|
+
export declare function batchOf(entry: HistoryEntry): EngineBatch;
|
|
76
|
+
/** The write mode fields of a batch or an entry, only those set. */
|
|
77
|
+
export declare function modeOf(mode: BatchMode): BatchMode;
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Linear undo history over an immutable original. Entries beyond the limit are
|
|
3
|
+
* folded into the starting point: they stay applied but can no longer be
|
|
4
|
+
* undone. Every reachable content state has a numeric id, so "unchanged since
|
|
5
|
+
* the last save" is an id comparison.
|
|
6
|
+
*/
|
|
7
|
+
export class EditHistory {
|
|
8
|
+
#limit;
|
|
9
|
+
#originalPageCount;
|
|
10
|
+
/** Folded batches, always applied before the undoable entries. */
|
|
11
|
+
#folded = [];
|
|
12
|
+
#entries = [];
|
|
13
|
+
#position = 0;
|
|
14
|
+
#nextStateId = 1;
|
|
15
|
+
constructor(limit, originalPageCount) {
|
|
16
|
+
this.#limit = limit;
|
|
17
|
+
this.#originalPageCount = originalPageCount;
|
|
18
|
+
}
|
|
19
|
+
get canUndo() {
|
|
20
|
+
return this.#position > 0;
|
|
21
|
+
}
|
|
22
|
+
get canRedo() {
|
|
23
|
+
return this.#position < this.#entries.length;
|
|
24
|
+
}
|
|
25
|
+
get isPristine() {
|
|
26
|
+
return this.#folded.length === 0 && this.#entries.length === 0;
|
|
27
|
+
}
|
|
28
|
+
/** The id the next pushed entry will get; engines derive created ids from it. */
|
|
29
|
+
get nextStateId() {
|
|
30
|
+
return this.#nextStateId;
|
|
31
|
+
}
|
|
32
|
+
/** Content id of the current state; 0 is the original document. */
|
|
33
|
+
get stateId() {
|
|
34
|
+
return this.#position > 0
|
|
35
|
+
? this.#entries[this.#position - 1].stateId
|
|
36
|
+
: (this.#folded.at(-1)?.stateId ?? 0);
|
|
37
|
+
}
|
|
38
|
+
get pageCount() {
|
|
39
|
+
return this.#position > 0
|
|
40
|
+
? this.#entries[this.#position - 1].pageCountAfter
|
|
41
|
+
: (this.#folded.at(-1)?.pageCountAfter ?? this.#originalPageCount);
|
|
42
|
+
}
|
|
43
|
+
/** State ids of every entry still in the history, folded and redo tail included. */
|
|
44
|
+
get stateIds() {
|
|
45
|
+
return this.allEntries.map((entry) => entry.stateId);
|
|
46
|
+
}
|
|
47
|
+
/** Every entry still in the history: folded ones, then the undoable ones and the redo tail. */
|
|
48
|
+
get allEntries() {
|
|
49
|
+
return [...this.#folded, ...this.#entries];
|
|
50
|
+
}
|
|
51
|
+
/** The entry `undo()` would revert, if any. */
|
|
52
|
+
get undoEntry() {
|
|
53
|
+
return this.#position > 0 ? this.#entries[this.#position - 1] : undefined;
|
|
54
|
+
}
|
|
55
|
+
/** The entry `redo()` would re-apply, if any. */
|
|
56
|
+
get redoEntry() {
|
|
57
|
+
return this.#entries[this.#position];
|
|
58
|
+
}
|
|
59
|
+
/** Batches applied to the original in the current state. */
|
|
60
|
+
applied() {
|
|
61
|
+
return this.batchesAt(this.#position);
|
|
62
|
+
}
|
|
63
|
+
/** Batches applied to the original when `position` undoable entries are applied. */
|
|
64
|
+
batchesAt(position) {
|
|
65
|
+
return this.entriesAt(position).map(batchOf);
|
|
66
|
+
}
|
|
67
|
+
/** Entries applied when `position` undoable entries are applied, folded ones first. */
|
|
68
|
+
entriesAt(position) {
|
|
69
|
+
return [...this.#folded, ...this.#entries.slice(0, position)];
|
|
70
|
+
}
|
|
71
|
+
get position() {
|
|
72
|
+
return this.#position;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Appends an entry with a fresh state id, or with `stateId` when the entry
|
|
76
|
+
* reproduces a known state (a restore to a checkpoint): the same id means
|
|
77
|
+
* the same content, so `dirty` stays exact.
|
|
78
|
+
*/
|
|
79
|
+
push(entry, stateId) {
|
|
80
|
+
const stored = Object.freeze({
|
|
81
|
+
...entry,
|
|
82
|
+
stateId: stateId ?? this.#nextStateId++,
|
|
83
|
+
});
|
|
84
|
+
// A new change after an undo drops the redo tail.
|
|
85
|
+
this.#entries.length = this.#position;
|
|
86
|
+
this.#entries.push(stored);
|
|
87
|
+
this.#position = this.#entries.length;
|
|
88
|
+
while (this.#entries.length > this.#limit) {
|
|
89
|
+
this.#folded.push(this.#entries.shift());
|
|
90
|
+
this.#position -= 1;
|
|
91
|
+
}
|
|
92
|
+
return stored;
|
|
93
|
+
}
|
|
94
|
+
undo() {
|
|
95
|
+
if (this.canUndo)
|
|
96
|
+
this.#position -= 1;
|
|
97
|
+
}
|
|
98
|
+
redo() {
|
|
99
|
+
if (this.canRedo)
|
|
100
|
+
this.#position += 1;
|
|
101
|
+
}
|
|
102
|
+
clear() {
|
|
103
|
+
this.#folded = [];
|
|
104
|
+
this.#entries = [];
|
|
105
|
+
this.#position = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
/** The engine batch an entry replays as: its operations and how they were written. */
|
|
109
|
+
export function batchOf(entry) {
|
|
110
|
+
return {
|
|
111
|
+
stateId: entry.stateId,
|
|
112
|
+
operations: entry.operations,
|
|
113
|
+
...modeOf(entry),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
/** The write mode fields of a batch or an entry, only those set. */
|
|
117
|
+
export function modeOf(mode) {
|
|
118
|
+
return {
|
|
119
|
+
...(mode.changeMode === undefined ? {} : { changeMode: mode.changeMode }),
|
|
120
|
+
...(mode.author === undefined ? {} : { author: mode.author }),
|
|
121
|
+
...(mode.timestamp === undefined ? {} : { timestamp: mode.timestamp }),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/** "/ppt/slides/slide1.xml" for a ZIP entry name or a loosely written name. */
|
|
2
|
+
export declare function partNameOf(entryName: string): string;
|
|
3
|
+
/** The ZIP entry name of a part: no leading slash. */
|
|
4
|
+
export declare function entryNameOf(partName: string): string;
|
|
5
|
+
/** The key two part names are compared by. */
|
|
6
|
+
export declare function partKey(name: string): string;
|
|
7
|
+
/** Whether a ZIP entry is a directory marker rather than a part. */
|
|
8
|
+
export declare function isDirectoryEntry(entryName: string): boolean;
|
|
9
|
+
/** "/ppt/slides/" for "/ppt/slides/slide1.xml"; "/" at the root. */
|
|
10
|
+
export declare function folderOf(partName: string): string;
|
|
11
|
+
/** The extension without the dot, lower-case; "" when there is none. */
|
|
12
|
+
export declare function extensionOf(partName: string): string;
|
|
13
|
+
/** The relationships part of a part ("/" for the package): "/ppt/slides/_rels/slide1.xml.rels". */
|
|
14
|
+
export declare function relationshipsPartOf(partName: string): string;
|
|
15
|
+
/**
|
|
16
|
+
* Resolves a relationship target against its source part: relative targets
|
|
17
|
+
* against the source's folder, absolute ones as they are; "." and ".."
|
|
18
|
+
* segments collapse. The result is an absolute part name.
|
|
19
|
+
*/
|
|
20
|
+
export declare function resolveTarget(sourcePart: string, target: string): string;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* OPC part names (ECMA-376 Part 2 §9.1): absolute, "/"-separated, compared
|
|
3
|
+
* case-insensitively. ZIP entry names carry them without the leading slash.
|
|
4
|
+
*/
|
|
5
|
+
/** "/ppt/slides/slide1.xml" for a ZIP entry name or a loosely written name. */
|
|
6
|
+
export function partNameOf(entryName) {
|
|
7
|
+
const name = entryName.replaceAll("\\", "/");
|
|
8
|
+
return name.startsWith("/") ? name : `/${name}`;
|
|
9
|
+
}
|
|
10
|
+
/** The ZIP entry name of a part: no leading slash. */
|
|
11
|
+
export function entryNameOf(partName) {
|
|
12
|
+
return partNameOf(partName).slice(1);
|
|
13
|
+
}
|
|
14
|
+
/** The key two part names are compared by. */
|
|
15
|
+
export function partKey(name) {
|
|
16
|
+
return partNameOf(name).toLowerCase();
|
|
17
|
+
}
|
|
18
|
+
/** Whether a ZIP entry is a directory marker rather than a part. */
|
|
19
|
+
export function isDirectoryEntry(entryName) {
|
|
20
|
+
return entryName.endsWith("/");
|
|
21
|
+
}
|
|
22
|
+
/** "/ppt/slides/" for "/ppt/slides/slide1.xml"; "/" at the root. */
|
|
23
|
+
export function folderOf(partName) {
|
|
24
|
+
const name = partNameOf(partName);
|
|
25
|
+
return name.slice(0, name.lastIndexOf("/") + 1);
|
|
26
|
+
}
|
|
27
|
+
/** The extension without the dot, lower-case; "" when there is none. */
|
|
28
|
+
export function extensionOf(partName) {
|
|
29
|
+
const name = partNameOf(partName);
|
|
30
|
+
const slash = name.lastIndexOf("/");
|
|
31
|
+
const dot = name.lastIndexOf(".");
|
|
32
|
+
return dot > slash ? name.slice(dot + 1).toLowerCase() : "";
|
|
33
|
+
}
|
|
34
|
+
/** The relationships part of a part ("/" for the package): "/ppt/slides/_rels/slide1.xml.rels". */
|
|
35
|
+
export function relationshipsPartOf(partName) {
|
|
36
|
+
if (partName === "/" || partName === "")
|
|
37
|
+
return "/_rels/.rels";
|
|
38
|
+
const name = partNameOf(partName);
|
|
39
|
+
const slash = name.lastIndexOf("/");
|
|
40
|
+
return `${name.slice(0, slash + 1)}_rels/${name.slice(slash + 1)}.rels`;
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* Resolves a relationship target against its source part: relative targets
|
|
44
|
+
* against the source's folder, absolute ones as they are; "." and ".."
|
|
45
|
+
* segments collapse. The result is an absolute part name.
|
|
46
|
+
*/
|
|
47
|
+
export function resolveTarget(sourcePart, target) {
|
|
48
|
+
const base = sourcePart === "/" || sourcePart === "" ? "/" : folderOf(sourcePart);
|
|
49
|
+
const raw = target.replaceAll("\\", "/");
|
|
50
|
+
const combined = raw.startsWith("/") ? raw : `${base}${raw}`;
|
|
51
|
+
const segments = [];
|
|
52
|
+
for (const segment of combined.split("/")) {
|
|
53
|
+
if (segment === "" || segment === ".")
|
|
54
|
+
continue;
|
|
55
|
+
if (segment === "..")
|
|
56
|
+
segments.pop();
|
|
57
|
+
else
|
|
58
|
+
segments.push(segment);
|
|
59
|
+
}
|
|
60
|
+
return `/${segments.join("/")}`;
|
|
61
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { XmlElement, XmlPart } from "./xml.js";
|
|
2
|
+
export declare const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
3
|
+
export declare const RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
4
|
+
export declare const RELATIONSHIPS_CONTENT_TYPE = "application/vnd.openxmlformats-package.relationships+xml";
|
|
5
|
+
export declare const IMAGE_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
|
|
6
|
+
export interface Relationship {
|
|
7
|
+
readonly id: string;
|
|
8
|
+
readonly type: string;
|
|
9
|
+
/** As written. */
|
|
10
|
+
readonly target: string;
|
|
11
|
+
readonly targetMode: "Internal" | "External";
|
|
12
|
+
/** Absolute part name for internal targets. */
|
|
13
|
+
readonly targetPart?: string;
|
|
14
|
+
/** The element in the scanned .rels part, for patches. */
|
|
15
|
+
readonly node?: XmlElement;
|
|
16
|
+
}
|
|
17
|
+
export declare class RelationshipSet {
|
|
18
|
+
#private;
|
|
19
|
+
/** "/" for the package. */
|
|
20
|
+
readonly sourcePart: string;
|
|
21
|
+
/** The .rels part, when it exists. */
|
|
22
|
+
readonly partName: string | undefined;
|
|
23
|
+
readonly items: readonly Relationship[];
|
|
24
|
+
constructor(
|
|
25
|
+
/** "/" for the package. */
|
|
26
|
+
sourcePart: string,
|
|
27
|
+
/** The .rels part, when it exists. */
|
|
28
|
+
partName: string | undefined, items: readonly Relationship[]);
|
|
29
|
+
byId(id: string): Relationship | undefined;
|
|
30
|
+
byType(type: string): readonly Relationship[];
|
|
31
|
+
/** The first free "rId<n>", counting from 1 and skipping `taken`. */
|
|
32
|
+
nextId(taken?: ReadonlySet<string>): string;
|
|
33
|
+
}
|
|
34
|
+
/** Reads a relationships part; `part` undefined means the source has none yet. */
|
|
35
|
+
export declare function parseRelationships(sourcePart: string, partName: string | undefined, part: XmlPart | undefined): RelationshipSet;
|
|
36
|
+
export interface ContentTypes {
|
|
37
|
+
/** Lower-case extension → content type. */
|
|
38
|
+
readonly defaults: ReadonlyMap<string, string>;
|
|
39
|
+
/** Part-name key → content type. */
|
|
40
|
+
readonly overrides: ReadonlyMap<string, string>;
|
|
41
|
+
typeOf(partName: string): string | undefined;
|
|
42
|
+
/** The Override element of a part, for patches. */
|
|
43
|
+
overrideNode(partName: string): XmlElement | undefined;
|
|
44
|
+
defaultNode(extension: string): XmlElement | undefined;
|
|
45
|
+
}
|
|
46
|
+
export declare function parseContentTypes(part: XmlPart): ContentTypes;
|
|
47
|
+
/** The file extension conventionally used for a media type. */
|
|
48
|
+
export declare function extensionForMime(mimeType: string): string;
|
|
49
|
+
/** The relationships part a .rels file belongs to, or undefined for other parts. */
|
|
50
|
+
export declare function sourceOfRelationshipsPart(partName: string): string | undefined;
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { extensionOf, partKey, partNameOf, resolveTarget } from "./names.js";
|
|
2
|
+
/*
|
|
3
|
+
* The Open Packaging Conventions parts the layer understands as models:
|
|
4
|
+
* [Content_Types].xml (Default by extension, Override by part name) and
|
|
5
|
+
* relationship parts (ordered, with ids, types and resolved targets). Both
|
|
6
|
+
* are read from scanned parts and written back as patches by transactions,
|
|
7
|
+
* so untouched entries keep their bytes.
|
|
8
|
+
*/
|
|
9
|
+
export const CONTENT_TYPES_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/content-types";
|
|
10
|
+
export const RELATIONSHIPS_NAMESPACE = "http://schemas.openxmlformats.org/package/2006/relationships";
|
|
11
|
+
export const RELATIONSHIPS_CONTENT_TYPE = "application/vnd.openxmlformats-package.relationships+xml";
|
|
12
|
+
export const IMAGE_RELATIONSHIP_TYPE = "http://schemas.openxmlformats.org/officeDocument/2006/relationships/image";
|
|
13
|
+
export class RelationshipSet {
|
|
14
|
+
sourcePart;
|
|
15
|
+
partName;
|
|
16
|
+
items;
|
|
17
|
+
#byId = new Map();
|
|
18
|
+
#byType = new Map();
|
|
19
|
+
constructor(
|
|
20
|
+
/** "/" for the package. */
|
|
21
|
+
sourcePart,
|
|
22
|
+
/** The .rels part, when it exists. */
|
|
23
|
+
partName, items) {
|
|
24
|
+
this.sourcePart = sourcePart;
|
|
25
|
+
this.partName = partName;
|
|
26
|
+
this.items = items;
|
|
27
|
+
for (const item of items) {
|
|
28
|
+
this.#byId.set(item.id, item);
|
|
29
|
+
const list = this.#byType.get(item.type);
|
|
30
|
+
if (list)
|
|
31
|
+
list.push(item);
|
|
32
|
+
else
|
|
33
|
+
this.#byType.set(item.type, [item]);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
byId(id) {
|
|
37
|
+
return this.#byId.get(id);
|
|
38
|
+
}
|
|
39
|
+
byType(type) {
|
|
40
|
+
return this.#byType.get(type) ?? [];
|
|
41
|
+
}
|
|
42
|
+
/** The first free "rId<n>", counting from 1 and skipping `taken`. */
|
|
43
|
+
nextId(taken = new Set()) {
|
|
44
|
+
for (let n = 1;; n += 1) {
|
|
45
|
+
const id = `rId${n}`;
|
|
46
|
+
if (!this.#byId.has(id) && !taken.has(id))
|
|
47
|
+
return id;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Reads a relationships part; `part` undefined means the source has none yet. */
|
|
52
|
+
export function parseRelationships(sourcePart, partName, part) {
|
|
53
|
+
if (!part)
|
|
54
|
+
return new RelationshipSet(sourcePart, partName, []);
|
|
55
|
+
const items = [];
|
|
56
|
+
for (const node of part.root.children) {
|
|
57
|
+
if (node.local !== "Relationship")
|
|
58
|
+
continue;
|
|
59
|
+
const id = part.attribute(node, "Id");
|
|
60
|
+
const type = part.attribute(node, "Type");
|
|
61
|
+
const target = part.attribute(node, "Target");
|
|
62
|
+
if (id === undefined || type === undefined || target === undefined)
|
|
63
|
+
continue;
|
|
64
|
+
const external = part.attribute(node, "TargetMode") === "External";
|
|
65
|
+
items.push({
|
|
66
|
+
id,
|
|
67
|
+
type,
|
|
68
|
+
target,
|
|
69
|
+
targetMode: external ? "External" : "Internal",
|
|
70
|
+
...(external ? {} : { targetPart: resolveTarget(sourcePart, target) }),
|
|
71
|
+
node,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return new RelationshipSet(sourcePart, partName, items);
|
|
75
|
+
}
|
|
76
|
+
export function parseContentTypes(part) {
|
|
77
|
+
const defaults = new Map();
|
|
78
|
+
const overrides = new Map();
|
|
79
|
+
const defaultNodes = new Map();
|
|
80
|
+
const overrideNodes = new Map();
|
|
81
|
+
for (const node of part.root.children) {
|
|
82
|
+
const contentType = part.attribute(node, "ContentType");
|
|
83
|
+
if (contentType === undefined)
|
|
84
|
+
continue;
|
|
85
|
+
if (node.local === "Default") {
|
|
86
|
+
const extension = part.attribute(node, "Extension")?.toLowerCase();
|
|
87
|
+
if (extension === undefined)
|
|
88
|
+
continue;
|
|
89
|
+
defaults.set(extension, contentType);
|
|
90
|
+
defaultNodes.set(extension, node);
|
|
91
|
+
}
|
|
92
|
+
else if (node.local === "Override") {
|
|
93
|
+
const name = part.attribute(node, "PartName");
|
|
94
|
+
if (name === undefined)
|
|
95
|
+
continue;
|
|
96
|
+
overrides.set(partKey(name), contentType);
|
|
97
|
+
overrideNodes.set(partKey(name), node);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
defaults,
|
|
102
|
+
overrides,
|
|
103
|
+
typeOf(partName) {
|
|
104
|
+
return (overrides.get(partKey(partName)) ?? defaults.get(extensionOf(partName)));
|
|
105
|
+
},
|
|
106
|
+
overrideNode(partName) {
|
|
107
|
+
return overrideNodes.get(partKey(partName));
|
|
108
|
+
},
|
|
109
|
+
defaultNode(extension) {
|
|
110
|
+
return defaultNodes.get(extension.toLowerCase());
|
|
111
|
+
},
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/** The file extension conventionally used for a media type. */
|
|
115
|
+
export function extensionForMime(mimeType) {
|
|
116
|
+
switch (mimeType.toLowerCase()) {
|
|
117
|
+
case "image/png":
|
|
118
|
+
return "png";
|
|
119
|
+
case "image/jpeg":
|
|
120
|
+
case "image/jpg":
|
|
121
|
+
return "jpeg";
|
|
122
|
+
case "image/gif":
|
|
123
|
+
return "gif";
|
|
124
|
+
case "image/bmp":
|
|
125
|
+
return "bmp";
|
|
126
|
+
case "image/tiff":
|
|
127
|
+
return "tiff";
|
|
128
|
+
case "image/svg+xml":
|
|
129
|
+
return "svg";
|
|
130
|
+
case "image/x-emf":
|
|
131
|
+
case "image/emf":
|
|
132
|
+
return "emf";
|
|
133
|
+
case "image/x-wmf":
|
|
134
|
+
case "image/wmf":
|
|
135
|
+
return "wmf";
|
|
136
|
+
default:
|
|
137
|
+
return "bin";
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** The relationships part a .rels file belongs to, or undefined for other parts. */
|
|
141
|
+
export function sourceOfRelationshipsPart(partName) {
|
|
142
|
+
const name = partNameOf(partName);
|
|
143
|
+
const match = /^(.*\/)_rels\/([^/]*)\.rels$/.exec(name);
|
|
144
|
+
if (!match)
|
|
145
|
+
return undefined;
|
|
146
|
+
const [, folder, base] = match;
|
|
147
|
+
if (folder === "/" && base === "")
|
|
148
|
+
return "/";
|
|
149
|
+
return `${folder}${base}`;
|
|
150
|
+
}
|