diff --git a/packages/super-editor/src/index.d.ts b/packages/super-editor/src/index.d.ts index 5e21c11318..862a4c8ff7 100644 --- a/packages/super-editor/src/index.d.ts +++ b/packages/super-editor/src/index.d.ts @@ -77,7 +77,20 @@ export type Command = (props: CommandProps) => boolean; * Chainable command object returned by editor.chain() */ export interface ChainableCommandObject { - run: () => boolean; + /** Execute the chained commands */ + run(): boolean; + /** Chain any command - returns self for further chaining */ + toggleBold(): ChainableCommandObject; + toggleItalic(): ChainableCommandObject; + toggleUnderline(): ChainableCommandObject; + toggleStrike(): ChainableCommandObject; + setFontSize(size: string | number): ChainableCommandObject; + setFontFamily(family: string): ChainableCommandObject; + setTextColor(color: string): ChainableCommandObject; + setTextAlign(alignment: 'left' | 'center' | 'right' | 'justify'): ChainableCommandObject; + insertContent(content: any): ChainableCommandObject; + focus(position?: 'start' | 'end' | 'all' | number | boolean | null): ChainableCommandObject; + /** Allow any other command */ [commandName: string]: ((...args: any[]) => ChainableCommandObject) | (() => boolean); } @@ -199,6 +212,67 @@ export interface OpenOptions { fonts?: Record; } +// ============================================ +// COMMENT TYPES +// ============================================ + +/** A comment element (paragraph, text run, etc.) */ +export interface CommentElement { + type: string; + content?: CommentElement[]; + text?: string; +} + +/** A comment in the document */ +export interface Comment { + /** Unique comment identifier */ + commentId: string; + /** Timestamp when comment was created */ + createdTime: number; + /** Email of the comment author */ + creatorEmail: string; + /** Display name of the comment author */ + creatorName: string; + /** Comment content elements */ + elements: CommentElement[]; + /** Original ID from imported document */ + importedId?: string; + /** Whether the comment is resolved */ + isDone: boolean; + /** Parent comment ID for replies */ + parentCommentId?: string; + /** Raw JSON representation */ + commentJSON?: unknown; +} + +/** Event data for comments loaded event */ +export interface CommentsLoadedEventData { + comments: Comment[]; +} + +/** Event data for content error event */ +export interface ContentErrorEventData { + error: Error; +} + +/** Font configuration */ +export interface FontConfig { + key: string; + label: string; + fontWeight?: number; + props?: { + style?: { + fontFamily?: string; + }; + }; +} + +/** Font support information */ +export interface FontSupportInfo { + documentFonts: string[]; + unsupportedFonts: string[]; +} + // ============================================ // PRESENTATION EDITOR TYPES // ============================================ @@ -586,6 +660,131 @@ export declare class Editor { */ isEmpty: boolean; + // ============================================ + // EVENT METHODS + // ============================================ + + /** + * Register an event listener. + * @param event - Event name ('update', 'create', 'transaction', etc.) + * @param handler - Event handler function + */ + on(event: string, handler: (...args: any[]) => void): void; + + /** + * Remove an event listener. + * @param event - Event name + * @param handler - Event handler function to remove + */ + off(event: string, handler: (...args: any[]) => void): void; + + /** + * Emit an event. + * @param event - Event name + * @param args - Arguments to pass to handlers + */ + emit(event: string, ...args: any[]): void; + + // ============================================ + // DOCUMENT EXPORT METHODS + // ============================================ + + /** + * Export the document to DOCX format. + * @param options - Export options + * @returns Promise resolving to Blob (browser) or Buffer (Node.js) + */ + exportDocx(options?: { + isFinalDoc?: boolean; + commentsType?: string; + comments?: Comment[]; + fieldsHighlightColor?: string | null; + compression?: 'DEFLATE' | 'STORE'; + }): Promise; + + /** + * Export the document (alias for exportDocx). + */ + exportDocument(options?: { + isFinalDoc?: boolean; + commentsType?: string; + comments?: Comment[]; + }): Promise; + + /** + * Save the document to the original source path (Node.js only). + */ + save(options?: { isFinalDoc?: boolean; commentsType?: string; comments?: Comment[] }): Promise; + + /** + * Save the document to a specific path (Node.js only). + */ + saveTo( + path: string, + options?: { + isFinalDoc?: boolean; + commentsType?: string; + comments?: Comment[]; + }, + ): Promise; + + // ============================================ + // TOOLBAR & UI METHODS + // ============================================ + + /** + * Set the toolbar for this editor. + */ + setToolbar(toolbar: any): void; + + /** + * Set whether the editor is editable. + */ + setEditable(editable: boolean, emitUpdate?: boolean): void; + + /** + * Set the document mode. + */ + setDocumentMode(mode: 'editing' | 'viewing' | 'suggesting'): void; + + /** + * Focus the editor. + */ + focus(): void; + + /** + * Blur the editor. + */ + blur(): void; + + // ============================================ + // DOCUMENT METHODS + // ============================================ + + /** + * Open a document. + */ + open(source?: string | File | Blob | BinaryData, options?: OpenOptions): Promise; + + /** + * Close the current document. + */ + close(): void; + + /** + * Replace the current file. + */ + replaceFile(newFile: File | Blob | BinaryData): Promise; + + /** + * Get the document as Markdown. + */ + getMarkdown(): Promise; + + /** + * Check if the editor is currently active/focused. + */ + isFocused: boolean; // --- Tracked selection handle API --- /** Capture the live PM selection as a tracked handle. Local-only. */ @@ -997,8 +1196,15 @@ export declare class PresentationEditor { */ emit(event: string, ...args: any[]): void; - /** Allow additional properties */ - [key: string]: any; + /** + * Replace the current file. + */ + replaceFile(newFile: File | Blob | BinaryData): Promise; + + /** + * Set the toolbar for the editor. + */ + setToolbar(toolbar: any): void; } // ============================================ diff --git a/packages/superdoc/src/index.js b/packages/superdoc/src/index.js index 34047a9504..5a8d876a4c 100644 --- a/packages/superdoc/src/index.js +++ b/packages/superdoc/src/index.js @@ -1,6 +1,8 @@ import { SuperConverter, Editor, + PresentationEditor, + getStarterExtensions, getRichTextExtensions, createZip, Extensions, @@ -10,11 +12,87 @@ import { trackChangesHelpers, AnnotatorHelpers, SectionHelpers, + // Additional runtime exports + DocxZipper, + SuperToolbar, + getMarksFromSelection, + getActiveFormatting, + getAllowedImageDimensions, + isNodeType, + assertNodeType, + isMarkType, + defineNode, + defineMark, + TrackChangesBasePluginKey, + CommentsPluginKey, + // Vue components + SuperEditor, + SuperInput, + BasicUpload, + Toolbar, + AIWriter, + ContextMenu, + SlashMenu, } from '@superdoc/super-editor'; import { DOCX, PDF, HTML, getFileObject, compareVersions } from '@superdoc/common'; import BlankDOCX from '@superdoc/common/data/blank.docx?url'; import { getSchemaIntrospection } from './helpers/schema-introspection.js'; +// ============================================ +// TYPE RE-EXPORTS +// These types are defined in @superdoc/super-editor and re-exported for consumers +// ============================================ + +/** + * @typedef {import('@superdoc/super-editor').EditorState} EditorState + * @typedef {import('@superdoc/super-editor').Transaction} Transaction + * @typedef {import('@superdoc/super-editor').Schema} Schema + * @typedef {import('@superdoc/super-editor').EditorView} EditorView + * @typedef {import('@superdoc/super-editor').EditorCommands} EditorCommands + * @typedef {import('@superdoc/super-editor').ChainedCommand} ChainedCommand + * @typedef {import('@superdoc/super-editor').ChainableCommandObject} ChainableCommandObject + * @typedef {import('@superdoc/super-editor').CommandProps} CommandProps + * @typedef {import('@superdoc/super-editor').Command} Command + * @typedef {import('@superdoc/super-editor').CanObject} CanObject + * @typedef {import('@superdoc/super-editor').PresentationEditorOptions} PresentationEditorOptions + * @typedef {import('@superdoc/super-editor').LayoutEngineOptions} LayoutEngineOptions + * @typedef {import('@superdoc/super-editor').PageSize} PageSize + * @typedef {import('@superdoc/super-editor').PageMargins} PageMargins + * @typedef {import('@superdoc/super-editor').VirtualizationOptions} VirtualizationOptions + * @typedef {import('@superdoc/super-editor').TrackedChangesMode} TrackedChangesMode + * @typedef {import('@superdoc/super-editor').TrackedChangesOverrides} TrackedChangesOverrides + * @typedef {import('@superdoc/super-editor').LayoutMode} LayoutMode + * @typedef {import('@superdoc/super-editor').PresenceOptions} PresenceOptions + * @typedef {import('@superdoc/super-editor').RemoteUserInfo} RemoteUserInfo + * @typedef {import('@superdoc/super-editor').RemoteCursorState} RemoteCursorState + * @typedef {import('@superdoc/super-editor').Layout} Layout + * @typedef {import('@superdoc/super-editor').LayoutPage} LayoutPage + * @typedef {import('@superdoc/super-editor').LayoutFragment} LayoutFragment + * @typedef {import('@superdoc/super-editor').RangeRect} RangeRect + * @typedef {import('@superdoc/super-editor').BoundingRect} BoundingRect + * @typedef {import('@superdoc/super-editor').LayoutError} LayoutError + * @typedef {import('@superdoc/super-editor').LayoutMetrics} LayoutMetrics + * @typedef {import('@superdoc/super-editor').PositionHit} PositionHit + * @typedef {import('@superdoc/super-editor').FlowBlock} FlowBlock + * @typedef {import('@superdoc/super-editor').Measure} Measure + * @typedef {import('@superdoc/super-editor').SectionMetadata} SectionMetadata + * @typedef {import('@superdoc/super-editor').PaintSnapshot} PaintSnapshot + * @typedef {import('@superdoc/super-editor').LayoutUpdatePayload} LayoutUpdatePayload + * @typedef {import('@superdoc/super-editor').OpenOptions} OpenOptions + * @typedef {import('@superdoc/super-editor').DocxFileEntry} DocxFileEntry + * @typedef {import('@superdoc/super-editor').BinaryData} BinaryData + * @typedef {import('@superdoc/super-editor').UnsupportedContentItem} UnsupportedContentItem + * @typedef {import('@superdoc/super-editor').Comment} Comment + * @typedef {import('@superdoc/super-editor').CommentElement} CommentElement + * @typedef {import('@superdoc/super-editor').CommentsLoadedEventData} CommentsLoadedEventData + * @typedef {import('@superdoc/super-editor').ContentErrorEventData} ContentErrorEventData + * @typedef {import('@superdoc/super-editor').FontConfig} FontConfig + * @typedef {import('@superdoc/super-editor').FontSupportInfo} FontSupportInfo + * @typedef {import('@superdoc/super-editor').SelectionHandle} SelectionHandle + * @typedef {import('@superdoc/super-editor').SelectionCommandContext} SelectionCommandContext + * @typedef {import('@superdoc/super-editor').ResolveRangeOutput} ResolveRangeOutput + */ + // Public exports export { SuperDoc } from './core/SuperDoc.js'; export { @@ -22,6 +100,8 @@ export { getFileObject, compareVersions, Editor, + PresentationEditor, + getStarterExtensions, getRichTextExtensions, getSchemaIntrospection, @@ -31,7 +111,7 @@ export { HTML, // Helpers - superEditorHelpers, + superEditorHelpers as helpers, fieldAnnotationHelpers, trackChangesHelpers, AnnotatorHelpers, @@ -45,4 +125,33 @@ export { Extensions, /** @internal */ registeredHandlers, + + // Additional classes + DocxZipper, + SuperToolbar, + + // Helper functions + getMarksFromSelection, + getActiveFormatting, + getAllowedImageDimensions, + + // Type guards and extension helpers + isNodeType, + assertNodeType, + isMarkType, + defineNode, + defineMark, + + // Plugin keys + TrackChangesBasePluginKey, + CommentsPluginKey, + + // Vue components + SuperEditor, + SuperInput, + BasicUpload, + Toolbar, + AIWriter, + ContextMenu, + SlashMenu, }; diff --git a/tests/consumer-typecheck/package.json b/tests/consumer-typecheck/package.json new file mode 100644 index 0000000000..fd02e045d9 --- /dev/null +++ b/tests/consumer-typecheck/package.json @@ -0,0 +1,14 @@ +{ + "name": "consumer-typecheck", + "private": true, + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "superdoc": "file:../../packages/superdoc/superdoc.tgz" + }, + "devDependencies": { + "typescript": "^5.0.0" + } +} diff --git a/tests/consumer-typecheck/src/index.ts b/tests/consumer-typecheck/src/index.ts new file mode 100644 index 0000000000..6166db25f9 --- /dev/null +++ b/tests/consumer-typecheck/src/index.ts @@ -0,0 +1,761 @@ +/** + * Consumer TypeScript compilation test. + * + * This file attempts to use SuperDoc's public API the way a real customer would. + * If this file compiles, the types are sufficient. If it fails, we know what's missing. + * + * Based on customer workaround from SD-2227. + */ + +// ============================================ +// BASIC IMPORTS +// These should all resolve from the main entry point +// ============================================ + +import { + SuperDoc, + Editor, + SuperConverter, + PresentationEditor, + getRichTextExtensions, + getStarterExtensions, + DOCX, + PDF, + HTML, + Extensions, + createZip, + // Additional exports + DocxZipper, + SuperToolbar, + getMarksFromSelection, + getActiveFormatting, + getAllowedImageDimensions, + // Type guards + isNodeType, + assertNodeType, + isMarkType, + // Extension helpers + defineNode, + defineMark, + // Helper modules + helpers, + fieldAnnotationHelpers, + trackChangesHelpers, + AnnotatorHelpers, + SectionHelpers, + registeredHandlers, + // Plugin keys + TrackChangesBasePluginKey, + CommentsPluginKey, + // Vue components + SuperEditor, + SuperInput, + BasicUpload, + Toolbar, + AIWriter, + ContextMenu, + SlashMenu, +} from 'superdoc'; + +// ============================================ +// EDITOR CLASS - STATIC METHODS +// Customer uses Editor.loadXmlData() for headless processing +// ============================================ + +async function testEditorStaticMethods() { + const file = new File([''], 'test.docx'); + + // Static method to load DOCX data + const xmlData = await Editor.loadXmlData(file); + + // Static method to open a document + const editor = await Editor.open(file); +} + +// ============================================ +// EDITOR CLASS - INSTANCE METHODS +// Customer needs exportDocx, on/off event handlers, setToolbar +// ============================================ + +async function testEditorInstanceMethods() { + const editor = new Editor(); + + // Export to DOCX - customer's primary use case + const docxBlob = await editor.exportDocx({ + comments: [], + commentsType: 'none', + }); + + // Event handlers - customer uses these extensively + editor.on('update', () => {}); + editor.on('create', () => {}); + editor.off('update', () => {}); + + // Toolbar integration + editor.setToolbar({}); + + // Basic properties + const state = editor.state; + const schema = editor.schema; + const isEditable = editor.isEditable; + const commands = editor.commands; + + // Commands + editor.commands.toggleBold(); + editor.commands.setFontSize('14pt'); + + // Chain commands + editor.chain().toggleBold().toggleItalic().run(); + + // Content methods + const html = editor.getHTML(); + const json = editor.getJSON(); + const text = editor.getText(); + + // Cleanup + editor.destroy(); +} + +// ============================================ +// PRESENTATION EDITOR CLASS +// Customer's main entry point for paginated editing +// ============================================ + +async function testPresentationEditor() { + const container = document.createElement('div'); + + // Constructor with options - customer passes many options + const presentationEditor = new PresentationEditor({ + element: container, + documentMode: 'editing', + content: {}, + extensions: [], + editable: true, + isCommentsEnabled: true, + layoutEngineOptions: { + pageSize: { w: 612, h: 792 }, + margins: { top: 72, right: 72, bottom: 72, left: 72 }, + zoom: 1, + layoutMode: 'vertical', + virtualization: { enabled: true }, + trackedChanges: { mode: 'review' }, + presence: { enabled: true }, + }, + }); + + // Access underlying editor + const editor = presentationEditor.editor; + + // Get active editor (body or header/footer) + const activeEditor: Editor = presentationEditor.getActiveEditor(); + + // State and schema access + const state = presentationEditor.state; + const isEditable = presentationEditor.isEditable; + + // Commands - customer uses comment commands + presentationEditor.commands.insertComment?.({ id: '123' }); + presentationEditor.commands.insertContent?.('Hello'); + + // Event handlers + presentationEditor.on('update', () => {}); + presentationEditor.on('create', () => {}); + presentationEditor.off('update', () => {}); + + // Document mode switching + presentationEditor.setDocumentMode('viewing'); + presentationEditor.setDocumentMode('suggesting'); + presentationEditor.setDocumentMode('editing'); + + // Tracked changes overrides + presentationEditor.setTrackedChangesOverrides({ mode: 'final', enabled: true }); + + // Viewing comment options + presentationEditor.setViewingCommentOptions({ + emitCommentPositionsInViewing: true, + enableCommentsInViewing: true, + }); + + // Context menu toggle + presentationEditor.setContextMenuDisabled(true); + + // File replacement + const newFile = new File([''], 'new.docx'); + await presentationEditor.replaceFile?.(newFile); + + // Zoom + presentationEditor.setZoom(1.5); + + // Layout mode + presentationEditor.setLayoutMode('horizontal'); + + // Layout methods + const pages: LayoutPage[] = presentationEditor.getPages(); + const rects: RangeRect[] = presentationEditor.getSelectionRects(); + const rangeRects: RangeRect[] = presentationEditor.getRangeRects(0, 100); + + // Selection bounds + const bounds = presentationEditor.getSelectionBounds(0, 100); + if (bounds) { + const { bounds: boundingRect, rects: selRects, pageIndex } = bounds; + } + + // Comment bounds mapping + const commentBounds = presentationEditor.getCommentBounds({ + 'comment-1': { start: 0, end: 50 }, + 'comment-2': { pos: 75 }, + }); + + // Layout snapshot + const snapshot = presentationEditor.getLayoutSnapshot(); + const { blocks, measures, layout, sectionMetadata } = snapshot; + + // Layout options + const layoutOptions: LayoutEngineOptions = presentationEditor.getLayoutOptions(); + + // Paint snapshot (debugging) + const paintSnapshot: PaintSnapshot | null = presentationEditor.getPaintSnapshot(); + + // Section-aware page styles + const sectionStyles = presentationEditor.getCurrentSectionPageStyles(); + + // Remote cursors (collaboration) + const cursors: RemoteCursorState[] = presentationEditor.getRemoteCursors(); + + // Layout health + const layoutError: LayoutError | null = presentationEditor.getLayoutError(); + const isHealthy: boolean = presentationEditor.isLayoutHealthy(); + const healthState: 'healthy' | 'degraded' | 'failed' = presentationEditor.getLayoutHealthState(); + + // Layout event subscriptions + const unsubLayout = presentationEditor.onLayoutUpdated((payload: LayoutUpdatePayload) => { + console.log('Layout updated', payload.layout.pages.length); + }); + const unsubError = presentationEditor.onLayoutError((error: LayoutError) => { + console.error('Layout error', error); + }); + unsubLayout(); // cleanup + unsubError(); + + // Hit testing + const hit: PositionHit | null = presentationEditor.hitTest(100, 200); + + // Coordinate conversions + const normalizedPoint = presentationEditor.normalizeClientPoint(100, 200); + const denormalizedPoint = presentationEditor.denormalizeClientPoint(50, 100, 0, 20); + + // Position/coordinate mappings + const coords = presentationEditor.coordsAtPos(42); + const posResult = presentationEditor.posAtCoords({ clientX: 100, clientY: 200 }); + const elementAtPos = presentationEditor.getElementAtPos(42); + + // Scrolling + await presentationEditor.scrollToPosition(100); + await presentationEditor.scrollToPositionAsync(100, { behavior: 'smooth' }); + await presentationEditor.scrollToPage(2, 'smooth'); + presentationEditor.scrollThreadAnchorToClientY('thread-1', 300); + + // Navigation + await presentationEditor.goToAnchor('bookmark-1'); + + // Caret computation + const caretRect = presentationEditor.computeCaretLayoutRect(42); + + // Undo/redo + presentationEditor.undo(); + presentationEditor.redo(); + + // Dispatch in active editor + presentationEditor.dispatchInActiveEditor((ed) => { + ed.commands.toggleBold(); + }); + + // Transaction dispatch + const tr = presentationEditor.state.tr; + presentationEditor.dispatch(tr); + + // Cleanup + presentationEditor.destroy(); +} + +// ============================================ +// SUPER TOOLBAR CLASS +// Customer creates toolbar with specific options +// ============================================ + +function testSuperToolbar() { + const editor = new Editor(); + + // Toolbar construction - customer uses these options + // const toolbar = new SuperToolbar({ + // editor, + // selector: '#toolbar', + // fonts: [], + // toolbarGroups: ['formatting', 'lists'], + // hideButtons: false, + // pagination: true, + // icons: {}, + // }); + + // Note: SuperToolbar is typed as `any` in hand-written types + // This section tests if it's exported at all +} + +// ============================================ +// COMMENT TYPES +// Customer works extensively with comments +// ============================================ + +function testCommentTypes() { + // This tests the exported Comment type + // Customer needs to type their comment handlers + + const comment: Comment = { + commentId: '123', + createdTime: Date.now(), + creatorEmail: 'user@example.com', + creatorName: 'User', + elements: [{ type: 'paragraph', text: 'Comment text' }], + isDone: false, + parentCommentId: undefined, + importedId: 'imported-123', + }; + + // Comment element type + const element: CommentElement = { + type: 'paragraph', + content: [{ type: 'text', text: 'nested' }], + text: 'Element text', + }; + + // CommentsLoadedEventData type + const handleCommentsLoaded = (data: CommentsLoadedEventData) => { + for (const c of data.comments) { + console.log(c.commentId, c.creatorName); + } + }; + + // ContentErrorEventData type + const handleContentError = (data: ContentErrorEventData) => { + console.error(data.error.message); + }; +} + +// ============================================ +// SELECTION HANDLE API +// New API for deferred selection-based operations (AI, dialogs, async chains) +// ============================================ + +async function testSelectionHandleAPI() { + const editor = new Editor(); + + // Capture selection as a tracked handle + const handle: SelectionHandle = editor.captureCurrentSelectionHandle('body'); + + // Handle properties + const handleId: number = handle.id; + const surface: 'body' | 'header' | 'footer' = handle.surface; + const wasNonEmpty: boolean = handle.wasNonEmpty; + + // Capture effective selection + const effectiveHandle: SelectionHandle = editor.captureEffectiveSelectionHandle('body'); + + // Resolve the handle to get current range + const range: ResolveRangeOutput | null = editor.resolveSelectionHandle(handle); + + // Snapshot convenience methods (no handle, immediate resolution) + const currentRange: ResolveRangeOutput = editor.getCurrentSelectionRange(); + const effectiveRange: ResolveRangeOutput = editor.getEffectiveSelectionRange(); + + // Release the handle when done + editor.releaseSelectionHandle(handle); + editor.releaseSelectionHandle(effectiveHandle); + + // Editor dispatch method (for headless mode) + const tr = editor.state.tr; + editor.dispatch(tr); +} + +async function testPresentationEditorSelectionAPI() { + const container = document.createElement('div'); + const presentationEditor = new PresentationEditor({ + element: container, + }); + + // Capture selection handles + const handle: SelectionHandle = presentationEditor.captureCurrentSelectionHandle(); + const effectiveHandle: SelectionHandle = presentationEditor.captureEffectiveSelectionHandle(); + + // Resolve to SelectionCommandContext (includes editor, doc, surface, range) + const context: SelectionCommandContext | null = presentationEditor.resolveSelectionHandle(handle); + if (context) { + const { editor, doc, surface, range } = context; + // editor: Editor instance + // doc: DocumentApi instance + // surface: 'body' | 'header' | 'footer' + // range: ResolveRangeOutput + } + + // Snapshot convenience methods + const currentRange: ResolveRangeOutput = presentationEditor.getCurrentSelectionRange(); + const effectiveRange: ResolveRangeOutput = presentationEditor.getEffectiveSelectionRange(); + const currentContext: SelectionCommandContext = presentationEditor.getCurrentSelectionContext(); + const effectiveContext: SelectionCommandContext = presentationEditor.getEffectiveSelectionContext(); + + // Release handles + presentationEditor.releaseSelectionHandle(handle); + presentationEditor.releaseSelectionHandle(effectiveHandle); +} + +// ============================================ +// EXTENSION HELPERS +// Customer uses these to configure the editor +// ============================================ + +function testExtensionHelpers() { + // Get default extensions + const starterExtensions = getStarterExtensions(); + const richTextExtensions = getRichTextExtensions(); + + // Extensions namespace + const { Node, Mark, Extension, Plugin, PluginKey } = Extensions; +} + +// ============================================ +// SUPERDOC CLASS (Vue component wrapper) +// ============================================ + +function testSuperDoc() { + // SuperDoc is the main Vue component + // Type should exist even if it's `any` + const superdoc: typeof SuperDoc = SuperDoc; +} + +// ============================================ +// UTILITY FUNCTIONS +// ============================================ + +async function testUtilities() { + // createZip for manual DOCX assembly + const files = [{ name: 'document.xml', content: '' }]; + const zip = await createZip(files); + + // MIME type constants + const docxMime: typeof DOCX = DOCX; + const pdfMime: typeof PDF = PDF; + const htmlMime: typeof HTML = HTML; +} + +// ============================================ +// TYPE EXPORTS +// These types should be importable by consumers +// ============================================ + +import type { + // Editor types + EditorState, + Transaction, + Schema, + EditorView, + + // Command types + EditorCommands, + ChainedCommand, + CommandProps, + Command, + CanObject, + + // Selection handle types (new) + SelectionHandle, + SelectionCommandContext, + ResolveRangeOutput, + + // Presentation types + PresentationEditorOptions, + LayoutEngineOptions, + PageSize, + PageMargins, + VirtualizationOptions, + TrackedChangesMode, + TrackedChangesOverrides, + LayoutMode, + PresenceOptions, + + // Remote collaboration types + RemoteUserInfo, + RemoteCursorState, + + // Layout types + Layout, + LayoutPage, + LayoutFragment, + RangeRect, + BoundingRect, + LayoutError, + LayoutMetrics, + PositionHit, + FlowBlock, + Measure, + SectionMetadata, + PaintSnapshot, + LayoutUpdatePayload, + + // Comment types + Comment, + CommentElement, + CommentsLoadedEventData, + ContentErrorEventData, + + // Font types + FontConfig, + FontSupportInfo, + + // Data types + OpenOptions, + DocxFileEntry, + BinaryData, + UnsupportedContentItem, +} from 'superdoc'; + +function testTypeImports() { + // Verify types are usable + const options: PresentationEditorOptions = { + element: document.createElement('div'), + }; + + const pageSize: PageSize = { w: 612, h: 792 }; + const margins: PageMargins = { top: 72, right: 72, bottom: 72, left: 72 }; + + // Virtualization options + const virtualization: VirtualizationOptions = { + enabled: true, + window: 5, + overscan: 2, + gap: 20, + paddingTop: 0, + }; + + // Tracked changes configuration + const trackedChangesMode: TrackedChangesMode = 'review'; + const trackedChanges: TrackedChangesOverrides = { + mode: trackedChangesMode, + enabled: true, + }; + + // Layout mode + const layoutMode: LayoutMode = 'vertical'; + + // Presence/collaboration options + const presenceOptions: PresenceOptions = { + enabled: true, + showLabels: true, + maxVisible: 5, + labelFormatter: (user: RemoteUserInfo) => user.name || 'Anonymous', + highlightOpacity: 0.3, + staleTimeout: 5000, + }; + + const layoutOptions: LayoutEngineOptions = { + pageSize, + margins, + zoom: 1, + virtualization, + trackedChanges, + layoutMode, + presence: presenceOptions, + }; + + // Remote cursor state + const remoteCursor: RemoteCursorState = { + clientId: 1, + user: { name: 'User', email: 'user@example.com', color: '#ff0000' }, + anchor: 0, + head: 10, + updatedAt: Date.now(), + }; + + // Layout types + const layoutError: LayoutError = { + phase: 'render', + error: new Error('Layout failed'), + timestamp: Date.now(), + }; + + const layoutMetrics: LayoutMetrics = { + durationMs: 100, + blockCount: 50, + pageCount: 3, + }; + + const positionHit: PositionHit = { + pos: 42, + layoutEpoch: 1, + blockId: 'block-1', + pageIndex: 0, + column: 0, + lineIndex: 5, + }; + + const flowBlock: FlowBlock = { + id: 'block-1', + type: 'paragraph', + pmStart: 0, + pmEnd: 100, + }; + + const measure: Measure = { + blockId: 'block-1', + width: 612, + height: 24, + lines: [{ width: 500, ascent: 12, descent: 4, lineHeight: 16 }], + }; + + const sectionMetadata: SectionMetadata = { + sectionIndex: 0, + startPage: 1, + endPage: 3, + }; + + const layoutUpdatePayload: LayoutUpdatePayload = { + blocks: [flowBlock], + measures: [measure], + layout: { pageSize, pages: [] }, + metrics: layoutMetrics, + }; + + // Font types + const fontConfig: FontConfig = { + key: 'arial', + label: 'Arial', + fontWeight: 400, + props: { style: { fontFamily: 'Arial, sans-serif' } }, + }; + + const fontSupport: FontSupportInfo = { + documentFonts: ['Arial', 'Times New Roman'], + unsupportedFonts: ['CustomFont'], + }; + + // Binary data type + const binaryData: BinaryData = new ArrayBuffer(100); + + // Unsupported content item + const unsupported: UnsupportedContentItem = { + tagName: 'HR', + outerHTML: '
', + count: 1, + }; + + // Command types + const commandProps: CommandProps = { + editor: new Editor(), + tr: {} as Transaction, + state: {} as EditorState, + view: {} as EditorView, + dispatch: () => {}, + }; + + const command: Command = (props: CommandProps) => true; +} + +// ============================================ +// TYPE GUARDS AND HELPER FUNCTIONS +// ============================================ + +function testTypeGuards() { + // isNodeType - narrows node type + const node = { type: { name: 'paragraph' }, attrs: { align: 'left' } }; + if (isNodeType(node, 'paragraph')) { + // node.type.name is now 'paragraph' + const name: 'paragraph' = node.type.name; + } + + // assertNodeType - throws if wrong type + try { + assertNodeType(node, 'heading'); + } catch (e) { + // Expected - node is 'paragraph', not 'heading' + } + + // isMarkType - narrows mark type + const mark = { type: { name: 'bold' }, attrs: {} }; + if (isMarkType(mark, 'bold')) { + const markName: 'bold' = mark.type.name; + } +} + +function testHelperModules() { + // Helper modules should be accessible (typed as any for flexibility) + const h = helpers; + const fa = fieldAnnotationHelpers; + const tc = trackChangesHelpers; + const ah = AnnotatorHelpers; + const sh = SectionHelpers; + const rh = registeredHandlers; + + // Plugin keys + const trackChangesKey = TrackChangesBasePluginKey; + const commentsKey = CommentsPluginKey; +} + +async function testAdditionalFunctions() { + // getMarksFromSelection + const selection = {}; // Mock selection + const marks = getMarksFromSelection(selection); + + // getActiveFormatting + const state = {}; // Mock state + const formatting = getActiveFormatting(state); + + // getAllowedImageDimensions + const file = new File([''], 'image.png', { type: 'image/png' }); + const dimensions = await getAllowedImageDimensions(file); + const { width, height } = dimensions; + + // defineNode and defineMark helpers + const nodeConfig = defineNode({ name: 'custom-node' }); + const markConfig = defineMark({ name: 'custom-mark' }); +} + +function testAdditionalClasses() { + // DocxZipper class + const zipper = new DocxZipper(); + + // SuperToolbar class + const toolbar = new SuperToolbar(); +} + +function testVueComponents() { + // Vue components should be exported (typed as any) + const superEditor = SuperEditor; + const superInput = SuperInput; + const basicUpload = BasicUpload; + const toolbarComponent = Toolbar; + const aiWriter = AIWriter; + const contextMenu = ContextMenu; + const slashMenu = SlashMenu; // deprecated +} + +// ============================================ +// Run type checks (these are just for TypeScript, not runtime) +// ============================================ + +export { + testEditorStaticMethods, + testEditorInstanceMethods, + testPresentationEditor, + testSuperToolbar, + testCommentTypes, + testExtensionHelpers, + testSuperDoc, + testUtilities, + testTypeImports, + // New test functions + testSelectionHandleAPI, + testPresentationEditorSelectionAPI, + testTypeGuards, + testHelperModules, + testAdditionalFunctions, + testAdditionalClasses, + testVueComponents, +}; diff --git a/tests/consumer-typecheck/tsconfig.json b/tests/consumer-typecheck/tsconfig.json new file mode 100644 index 0000000000..c38e3f9f65 --- /dev/null +++ b/tests/consumer-typecheck/tsconfig.json @@ -0,0 +1,12 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "skipLibCheck": true, + "noEmit": true, + "esModuleInterop": true + }, + "include": ["src/**/*.ts"] +}