@kedataindo/docflow-core 0.0.2 → 0.0.3

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/index.d.cts CHANGED
@@ -1,10 +1,13 @@
1
1
  import { AnyExtension, Editor, Extension } from '@tiptap/core';
2
2
  import { Awareness } from 'y-protocols/awareness';
3
+ import { IndexeddbPersistence } from 'y-indexeddb';
3
4
  import { WebrtcProvider } from 'y-webrtc';
4
5
  import { WebsocketProvider } from 'y-websocket';
5
6
  import * as Y from 'yjs';
6
7
  import { PaginationPlusOptions } from 'tiptap-pagination-plus';
7
8
  export { PAGE_SIZES, PageSize, PaginationPlus, PaginationPlusOptions } from 'tiptap-pagination-plus';
9
+ import { PluginKey } from '@tiptap/pm/state';
10
+ import { Node } from '@tiptap/pm/model';
8
11
 
9
12
  interface ToolbarItem {
10
13
  id: string;
@@ -45,6 +48,15 @@ interface AwarenessState {
45
48
  from: number;
46
49
  to: number;
47
50
  } | null;
51
+ /**
52
+ * Phase 9 PR2 — *present* flag. False when the local user has navigated
53
+ * away from the editor route (the Yjs provider is still alive but the
54
+ * host has gated cursor emission off, per the §6 *\"TOC sidebar orphan
55
+ * wiring\"* / REST-heartbeat scope notes). Peers see this instantly
56
+ * via the awareness `change` event — far faster than the 15s REST
57
+ * heartbeat fallback. Defaults to true when the user is in the editor.
58
+ */
59
+ present: boolean;
48
60
  }
49
61
  interface CollaborationOptions {
50
62
  room: string;
@@ -57,16 +69,215 @@ interface CollaborationOptions {
57
69
  };
58
70
  onAwarenessChange?: (states: AwarenessState[]) => void;
59
71
  initialStorageState?: Uint8Array;
72
+ /**
73
+ * Phase 9 PR2 — when false, the local user is treated as
74
+ * out-of-editor: their cursor field is nulled and `present` is false.
75
+ * Default is true (in editor). Toggle at runtime via
76
+ * `setLocalCursorEnabled()` returned from `createCollaboration`.
77
+ */
78
+ emitCursor?: boolean;
79
+ /**
80
+ * Phase 9 OF1 — when true, mirror the room's Y.Doc into IndexedDB
81
+ * (`docflow-<room>`) via y-indexeddb so edits survive offline and reload.
82
+ * Reconcile-on-reconnect is plain Yjs merge — no conflict resolution
83
+ * needed. The IndexedDB mirror is NEVER a seed source: the server (or the
84
+ * legacy-JSON seed path) still owns initial state; the mirror only
85
+ * contributes local offline edits via the normal Yjs update exchange.
86
+ */
87
+ offline?: boolean;
60
88
  }
61
89
  interface CollaborationSetup {
62
90
  ydoc: Y.Doc;
63
91
  provider: WebrtcProvider | WebsocketProvider | null;
64
92
  awareness: Awareness;
93
+ /**
94
+ * Phase 9 OF1 — y-indexeddb persistence instance, present only when
95
+ * `options.offline` is true. Exposed so hosts/tests can `await
96
+ * persistence.whenSynced` before asserting the mirror contents. Do not
97
+ * use it as a seed source.
98
+ */
99
+ persistence?: IndexeddbPersistence;
65
100
  destroy: () => void;
101
+ /**
102
+ * Phase 9 PR2 — toggle the local cursor emission + present flag.
103
+ * Pass `false` when the editor route unmounts, `true` when it remounts.
104
+ * Triggers an awareness `change` event so peers see the leave/rejoin
105
+ * instantly (no REST-heartbeat lag).
106
+ */
107
+ setLocalCursorEnabled: (enabled: boolean) => void;
66
108
  }
67
109
  declare function createCollaboration(options: CollaborationOptions): CollaborationSetup;
68
110
  declare function collaborationExtensions(options: CollaborationOptions | CollaborationSetup): AnyExtension[];
69
111
 
112
+ /**
113
+ * Injectable ports — the narrow interfaces through which the library reaches
114
+ * host-provided backend capabilities (storage, persistence, …). The library
115
+ * declares them; the host app implements them. See docs/LIBRARY_CONTRACT.md.
116
+ */
117
+ interface ImageUploadResult {
118
+ /** Resolved URL/href the host stored the file at. */
119
+ src: string;
120
+ alt?: string;
121
+ title?: string;
122
+ }
123
+ /**
124
+ * Host-supplied image upload handler. Called with the file picked by the user;
125
+ * resolves to the stored location to insert into the document. Reject to abort
126
+ * the insert (the failure is logged, nothing is inserted).
127
+ */
128
+ type ImageUploadHandler = (file: File) => Promise<ImageUploadResult>;
129
+ /** CSL-JSON name (see https://citeproc-js.readthedocs.io/csl-json/). */
130
+ interface CslName {
131
+ family?: string;
132
+ given?: string;
133
+ literal?: string;
134
+ }
135
+ /** CSL-JSON date. */
136
+ interface CslDate {
137
+ 'date-parts'?: number[][];
138
+ raw?: string;
139
+ literal?: string;
140
+ }
141
+ /**
142
+ * A CSL-JSON item — the structured source metadata citeproc-js consumes.
143
+ * Nodes store only `{ sourceId, locator }`; all rendered text is derived from
144
+ * this data by the citation engine (never persisted in the document).
145
+ */
146
+ interface CslItemData {
147
+ id: string;
148
+ type: string;
149
+ title?: string;
150
+ author?: CslName[];
151
+ editor?: CslName[];
152
+ issued?: CslDate;
153
+ 'container-title'?: string;
154
+ publisher?: string;
155
+ 'publisher-place'?: string;
156
+ page?: string;
157
+ volume?: string;
158
+ issue?: string;
159
+ DOI?: string;
160
+ URL?: string;
161
+ ISBN?: string;
162
+ abstract?: string;
163
+ [k: string]: unknown;
164
+ }
165
+ /**
166
+ * Host-supplied citation port. The library never fetches sources itself — the
167
+ * host owns the reference library and injects CSL-JSON through this port,
168
+ * mirroring the `onImageUpload` injection pattern.
169
+ */
170
+ interface CitationPort {
171
+ /** CSL-JSON provider — a snapshot array or a getter for live data. */
172
+ sources: CslItemData[] | (() => CslItemData[]);
173
+ /** Active CSL style id (e.g. 'chicago-notes-bibliography'). */
174
+ style?: string;
175
+ /**
176
+ * Called when the user inserts a citation: the host opens its source
177
+ * picker and resolves with the chosen source id (null = cancelled).
178
+ */
179
+ onSourceRequest?: () => Promise<string | null>;
180
+ /**
181
+ * Called when the set of cited source ids changes, so the host can update
182
+ * its embedded per-document source snapshot.
183
+ */
184
+ onSourcesChange?: (ids: string[]) => void;
185
+ /**
186
+ * Optional importer (Phase 6C-2): resolve a DOI/URL into a new persisted
187
+ * source via the host's backend (CrossRef lookup). Returns the created
188
+ * (or deduplicated) source, null on failure.
189
+ */
190
+ onImportDoi?: (doi: string) => Promise<CslItemData | null>;
191
+ /**
192
+ * Optional importer (Phase 6C-3): parse + persist a BibTeX/RIS document
193
+ * via the host's backend. Returns the imported sources and how many
194
+ * entries failed.
195
+ */
196
+ onImportBibliography?: (payload: {
197
+ format: 'bibtex' | 'ris';
198
+ text: string;
199
+ }) => Promise<{
200
+ imported: CslItemData[];
201
+ failed: number;
202
+ }>;
203
+ }
204
+
205
+ /**
206
+ * Shared AI action types (Phase 7).
207
+ *
208
+ * The library (aiPlugin) is provider-agnostic: it never names a URL or holds a
209
+ * key. The app implements `AIStreamFn` (apps/web/src/ai/aiStream.ts) and injects
210
+ * it; the server owns provider selection via env config.
211
+ */
212
+ type AIAction = 'rewrite' | 'summarize' | 'grammar' | 'tone' | 'translate' | 'expand' | 'shorten' | 'generate' | 'chat' | 'draft';
213
+ interface AIActionRequest {
214
+ action: AIAction;
215
+ /** Selected text (7B). */
216
+ selection?: string;
217
+ /** Bounded surrounding text — never the whole document. */
218
+ context?: {
219
+ before: string;
220
+ after: string;
221
+ };
222
+ /** User instruction (/ai prompt, chat message, tone target, target language). */
223
+ prompt?: string;
224
+ /** For doc-aware chat / RAG scoping. */
225
+ documentId?: string;
226
+ options?: Record<string, unknown>;
227
+ }
228
+ /**
229
+ * The app implements this and injects it; the library never knows the URL.
230
+ * Yields streamed text chunks in order; stops at end of stream, throws on error.
231
+ */
232
+ type AIStreamFn = (req: AIActionRequest, signal: AbortSignal) => AsyncIterable<string>;
233
+ /** One entry in the citation table carried by the SSE `done` event (§3.4). */
234
+ interface AIDraftCitation {
235
+ /** 1-based index matching a `[n]` marker in the streamed text. */
236
+ ref: number;
237
+ /** `Source._id` string — the library maps markers through this table. */
238
+ sourceId: string;
239
+ /** "first author family + year" tag, e.g. "Doe (2024)". */
240
+ label: string;
241
+ }
242
+ /**
243
+ * Events yielded by `aiDraft` (§3.5). The `done` event carries the citation
244
+ * table; the `error` event does NOT (the bug-hunter carry-forward — the client
245
+ * treats BOTH `done` and `error` as terminal; Insert is enabled only on `done`).
246
+ */
247
+ type AIDraftEvent = {
248
+ type: 'delta';
249
+ text: string;
250
+ } | {
251
+ type: 'done';
252
+ citations: AIDraftCitation[];
253
+ } | {
254
+ type: 'error';
255
+ message: string;
256
+ };
257
+ /**
258
+ * The app implements this and injects it; the library never knows the URL.
259
+ * Mirrors `AIStreamFn` but yields richer `AIDraftEvent`s (carries the citation
260
+ * table on the terminal `done` event). Injected exactly like `aiStream`.
261
+ */
262
+ type AIDraftFn = (req: {
263
+ prompt: string;
264
+ context?: {
265
+ before: string;
266
+ after: string;
267
+ };
268
+ k?: number;
269
+ }, signal: AbortSignal) => AsyncIterable<AIDraftEvent>;
270
+
271
+ /**
272
+ * Sanitize pasted HTML content (e.g. from Google Docs) to prevent crashes
273
+ * during ProseMirror parsing. Strips non-content tags like <meta>, <style>,
274
+ * and HTML comments that are common in rich clipboard data but not valid
275
+ * in the ProseMirror schema.
276
+ *
277
+ * Also converts Google Docs-specific markup into standard TipTap-compatible
278
+ * HTML so that text color and background-color survive the paste.
279
+ */
280
+ declare function sanitizePastedHTML(html: string): string;
70
281
  interface EditorOptions {
71
282
  target?: HTMLElement;
72
283
  content?: object | string;
@@ -79,6 +290,14 @@ interface EditorOptions {
79
290
  blockIndex: number;
80
291
  }>;
81
292
  paginationOptions?: PaginationPlusOptions;
293
+ /** Host-injected image upload port (see docs/LIBRARY_CONTRACT.md). */
294
+ onImageUpload?: ImageUploadHandler;
295
+ /** Host-injected citation port (Phase 6 — reference library + CSL styles). */
296
+ citation?: CitationPort;
297
+ /** Host-injected AI transport (Phase 7 — editor → server → LLM). */
298
+ aiStream?: AIStreamFn;
299
+ /** Host-injected cited-draft transport (Phase 7E — editor → server → RAG LLM). */
300
+ aiDraft?: AIDraftFn;
82
301
  }
83
302
  interface DocsEditor {
84
303
  editor: Editor;
@@ -119,4 +338,121 @@ declare module '@tiptap/core' {
119
338
  */
120
339
  declare const FontSizeExtension: Extension<any, any>;
121
340
 
122
- export { type AwarenessState, BlockAttributesExtension, type CollaborationOptions, type CollaborationSetup, type DocsEditor, type DocsEditorPlugin, type EditorOptions, FontSizeExtension, type SlashCommand, type ToolbarItem, collaborationExtensions, collectExtensions, createActionMap, createCollaboration, createEditor, definePlugin, resolveAction };
341
+ interface EditorContextOptions {
342
+ onImageUpload?: ImageUploadHandler;
343
+ citation?: CitationPort;
344
+ aiStream?: AIStreamFn;
345
+ aiDraft?: AIDraftFn;
346
+ }
347
+ /**
348
+ * Internal carrier for host-injected ports. Always registered (like TextStyle)
349
+ * so plugin commands can reach the ports through the editor instance:
350
+ *
351
+ * editor.storage.editorContext.onImageUpload
352
+ * editor.storage.editorContext.citation
353
+ * editor.storage.editorContext.aiStream
354
+ * editor.storage.editorContext.aiDraft
355
+ *
356
+ * Plugins must never import backend concerns — they read injected ports from
357
+ * this storage instead. See docs/LIBRARY_CONTRACT.md.
358
+ */
359
+ declare const EditorContextExtension: Extension<EditorContextOptions, any>;
360
+
361
+ /**
362
+ * Find & Replace — core editing infrastructure (always registered, like
363
+ * TextStyle). Plain-string, case-insensitive search with ProseMirror
364
+ * decorations for match highlighting. No regex (deliberate — see the
365
+ * edit-menu plan §5.6).
366
+ */
367
+ interface SearchMatch {
368
+ from: number;
369
+ to: number;
370
+ }
371
+ interface SearchState {
372
+ query: string;
373
+ matches: SearchMatch[];
374
+ activeIndex: number;
375
+ }
376
+ declare const searchAndReplaceKey: PluginKey<SearchState>;
377
+ /**
378
+ * Case-insensitive plain-string search across all text nodes.
379
+ * Matches are **non-overlapping** (the scan advances by the needle length) so
380
+ * replace-all can apply them in one transaction without position drift.
381
+ */
382
+ declare function findMatches(doc: Node, query: string): SearchMatch[];
383
+ declare const SearchAndReplaceExtension: Extension<any, any>;
384
+ declare function getSearchState(editor: Editor): SearchState;
385
+ declare function setSearchQuery(editor: Editor, query: string): void;
386
+ declare function clearSearch(editor: Editor): void;
387
+ /** Move to the next match (wraps). Returns the newly active match, if any. */
388
+ declare function searchNext(editor: Editor): SearchMatch | null;
389
+ /** Move to the previous match (wraps). Returns the newly active match, if any. */
390
+ declare function searchPrev(editor: Editor): SearchMatch | null;
391
+ /** Replace the active match. The doc change recomputes matches automatically. */
392
+ declare function replaceCurrent(editor: Editor, replacement: string): boolean;
393
+ /** Replace every match in one transaction (applied back-to-front). */
394
+ declare function replaceAll(editor: Editor, replacement: string): number;
395
+
396
+ interface SubdocState {
397
+ id: string;
398
+ doc: Y.Doc;
399
+ provider: WebsocketProvider | null;
400
+ active: boolean;
401
+ }
402
+ interface SubdocumentProviderOptions {
403
+ /** WebSocket URL for the collaboration server. */
404
+ websocketUrl: string;
405
+ /** Room prefix — subdocuments use `${roomPrefix}/${id}` as room name. */
406
+ roomPrefix: string;
407
+ /** User info for awareness. */
408
+ user: {
409
+ name: string;
410
+ color: string;
411
+ };
412
+ /** Number of subdocuments to keep active (buffer around visible viewport). */
413
+ maxActive?: number;
414
+ /** Called when subdocument state changes (active/deactivated). */
415
+ onStateChange?: (states: SubdocState[]) => void;
416
+ }
417
+ /**
418
+ * Manages multiple Y.Doc subdocuments under a parent document.
419
+ *
420
+ * Use case: large documents (100+ pages) split into per-section subdocuments.
421
+ * Only active (visible) subdocuments are connected to the network via
422
+ * WebSocketProvider; inactive ones keep local state but stop syncing.
423
+ *
424
+ * Usage:
425
+ * const provider = new SubdocumentProvider({ websocketUrl, roomPrefix, user })
426
+ * provider.createSubdoc('section-1')
427
+ * provider.activate('section-1') // starts syncing
428
+ * provider.deactivate('section-1') // stops syncing
429
+ * const frag = provider.getFragment('section-1') // Y.XmlFragment for editor
430
+ */
431
+ declare class SubdocumentProvider {
432
+ readonly parentDoc: Y.Doc;
433
+ readonly subdocs: Y.Map<Y.Doc>;
434
+ readonly awareness: Awareness;
435
+ private readonly options;
436
+ private readonly states;
437
+ private readonly activeQueue;
438
+ private readonly parentProvider;
439
+ constructor(options: SubdocumentProviderOptions);
440
+ /** Create a new subdocument. Does NOT activate it. */
441
+ createSubdoc(id: string, initialState?: Uint8Array): Y.Doc;
442
+ /** Activate a subdocument — connects it to the network for live sync. */
443
+ activate(id: string): void;
444
+ /** Deactivate a subdocument — disconnects it from the network. */
445
+ deactivate(id: string): void;
446
+ /** Get the Y.XmlFragment for a subdocument. */
447
+ getFragment(id: string): Y.XmlFragment | null;
448
+ /** Check if a subdocument is currently active (syncing). */
449
+ isActive(id: string): boolean;
450
+ /** Get all subdocument states. */
451
+ getAllStates(): SubdocState[];
452
+ /** Get IDs of all active subdocuments. */
453
+ getActiveIds(): string[];
454
+ /** Destroy all subdocuments and providers. */
455
+ destroy(): void;
456
+ }
457
+
458
+ export { type AIAction, type AIActionRequest, type AIDraftCitation, type AIDraftEvent, type AIDraftFn, type AIStreamFn, type AwarenessState, BlockAttributesExtension, type CitationPort, type CollaborationOptions, type CollaborationSetup, type CslDate, type CslItemData, type CslName, type DocsEditor, type DocsEditorPlugin, EditorContextExtension, type EditorContextOptions, type EditorOptions, FontSizeExtension, type ImageUploadHandler, type ImageUploadResult, SearchAndReplaceExtension, type SearchMatch, type SearchState, type SlashCommand, type SubdocState, SubdocumentProvider, type SubdocumentProviderOptions, type ToolbarItem, clearSearch, collaborationExtensions, collectExtensions, createActionMap, createCollaboration, createEditor, definePlugin, findMatches, getSearchState, replaceAll, replaceCurrent, resolveAction, sanitizePastedHTML, searchAndReplaceKey, searchNext, searchPrev, setSearchQuery };