diff --git a/eslint.config.mjs b/eslint.config.mjs index abcd2134c5..8a07838de4 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -260,4 +260,22 @@ export default [ '@typescript-eslint/no-empty-object-type': 'off', }, }, + // Logging discipline: shipped package source must use the shared logger + // (`@superdoc/common/logger`) instead of raw `console.*`, so embedding + // SuperDoc does not spam the host console and output stays level-gated and + // redirectable. Build scripts (scripts/**) and tests are exempt. + { + files: ['packages/*/src/**/*.{js,ts}', 'shared/**/*.{js,ts}'], + ignores: [ + // The logger implementation itself owns the only sanctioned console sink. + 'shared/common/logger.ts', + // @superdoc-dev/ai is deprecated and keeps its own legacy logger. + 'packages/ai/src/shared/logger.ts', + // Zero-dependency security leaf: a single env-gated diagnostic warning. + 'shared/url-validation/index.js', + ], + rules: { + 'no-console': 'error', + }, + }, ]; diff --git a/packages/collaboration-yjs/package.json b/packages/collaboration-yjs/package.json index adcdc57d7a..58aedf8373 100644 --- a/packages/collaboration-yjs/package.json +++ b/packages/collaboration-yjs/package.json @@ -36,6 +36,7 @@ "access": "public" }, "dependencies": { + "@superdoc/common": "workspace:*", "lib0": "catalog:", "y-protocols": "catalog:", "yjs": "catalog:" diff --git a/packages/collaboration-yjs/src/internal-logger/logger.ts b/packages/collaboration-yjs/src/internal-logger/logger.ts index 4ea3b0cbab..83a1b55bff 100644 --- a/packages/collaboration-yjs/src/internal-logger/logger.ts +++ b/packages/collaboration-yjs/src/internal-logger/logger.ts @@ -1,16 +1,15 @@ -const COLORS = { - ConnectionHandler: '\x1b[34m', // blue - DocumentManager: '\x1b[32m', // green - SuperDocCollaboration: '\x1b[35m', // magenta - reset: '\x1b[0m', -}; +import { createLogger as createBaseLogger } from '@superdoc/common/logger'; export type Logger = (...args: unknown[]) => void; -export function createLogger(label: keyof typeof COLORS | string): Logger { - const color = (COLORS as Record)[label] || COLORS.reset; - - return (...args: unknown[]) => { - console.log(`${color}[${label}]${COLORS.reset}`, ...args); - }; +/** + * Always-on, label-prefixed logger for the collaboration server. + * + * Thin adapter over the shared `@superdoc/common/logger`. The level is pinned + * to `info` so server diagnostics stay visible regardless of the global level, + * preserving the previous always-print behavior. + */ +export function createLogger(label: string): Logger { + const base = createBaseLogger(label, { level: 'info' }); + return (...args: unknown[]) => base.info(...args); } diff --git a/packages/collaboration-yjs/src/shared-doc/callback.ts b/packages/collaboration-yjs/src/shared-doc/callback.ts index bc4904ccd6..9650a93e79 100644 --- a/packages/collaboration-yjs/src/shared-doc/callback.ts +++ b/packages/collaboration-yjs/src/shared-doc/callback.ts @@ -1,7 +1,10 @@ import http from 'node:http'; import * as number from 'lib0/number'; +import { createLogger } from '@superdoc/common/logger'; import type { SharedSuperDoc } from './shared-doc.js'; +const log = createLogger('callback'); + type CallbackObjects = Record; type CallbackPayload = { room: string; @@ -54,12 +57,12 @@ const callbackRequest = (url: URL, timeout: number, data: CallbackPayload) => { }; const req = http.request(options); req.on('timeout', () => { - console.warn('Callback request timed out.'); + log.warn('Callback request timed out.'); req.abort(); }); req.on('error', (e) => { const sanitizedError = String(e).replace(/\n|\r/g, ''); - console.error('Callback request error:', sanitizedError); + log.error('Callback request error:', sanitizedError); req.abort(); }); req.write(serialized); diff --git a/packages/collaboration-yjs/src/shared-doc/shared-doc.test.ts b/packages/collaboration-yjs/src/shared-doc/shared-doc.test.ts index 53dd7b6e54..d994380687 100644 --- a/packages/collaboration-yjs/src/shared-doc/shared-doc.test.ts +++ b/packages/collaboration-yjs/src/shared-doc/shared-doc.test.ts @@ -362,11 +362,11 @@ describe('callback handler', () => { }); req.trigger('timeout'); - expect(consoleWarn).toHaveBeenCalledWith('Callback request timed out.'); + expect(consoleWarn).toHaveBeenCalledWith('[callback]', 'Callback request timed out.'); expect(req.abort).toHaveBeenCalledTimes(1); req.trigger('error', new Error('failed\nreason')); - expect(consoleError).toHaveBeenCalledWith('Callback request error:', 'Error: failedreason'); + expect(consoleError).toHaveBeenCalledWith('[callback]', 'Callback request error:', 'Error: failedreason'); expect(req.abort).toHaveBeenCalledTimes(2); consoleWarn.mockRestore(); @@ -545,7 +545,7 @@ describe('SharedSuperDoc', () => { } messageHandler(new Uint8Array([99])); - expect(consoleWarn).toHaveBeenCalledWith('Unknown message type:', 99); + expect(consoleWarn).toHaveBeenCalledWith('[shared-doc]', 'Unknown message type:', 99); consoleWarn.mockRestore(); }); @@ -569,7 +569,7 @@ describe('SharedSuperDoc', () => { readSyncBehavior = 'throw'; messageHandler(new Uint8Array([0])); - expect(consoleError).toHaveBeenCalledWith('Error in messageListener:', expect.any(Error)); + expect(consoleError).toHaveBeenCalledWith('[shared-doc]', 'Error in messageListener:', expect.any(Error)); expect(errorListener).toHaveBeenCalledWith(expect.any(Error)); consoleError.mockRestore(); }); diff --git a/packages/collaboration-yjs/src/shared-doc/shared-doc.ts b/packages/collaboration-yjs/src/shared-doc/shared-doc.ts index 1558c0b5fc..c6a82d3a74 100644 --- a/packages/collaboration-yjs/src/shared-doc/shared-doc.ts +++ b/packages/collaboration-yjs/src/shared-doc/shared-doc.ts @@ -3,11 +3,14 @@ import { createEncoder, writeVarUint, writeVarUint8Array, toUint8Array, length a import { readVarUint8Array, createDecoder, readVarUint } from 'lib0/decoding'; import { Awareness, encodeAwarenessUpdate, removeAwarenessStates, applyAwarenessUpdate } from 'y-protocols/awareness'; import { Doc as YDoc } from 'yjs'; +import { createLogger } from '@superdoc/common/logger'; import { callbackHandler, isCallbackSet } from './callback.js'; import { debouncer } from './utils.js'; import { messageSync, messageAwareness, wsReadyStateConnecting, wsReadyStateOpen } from './constants.js'; import type { CollaborationWebSocket } from '../types/service-types.js'; +const log = createLogger('shared-doc'); + type AwarenessChange = { added: number[]; updated: number[]; removed: number[] }; interface YDocWithEmit extends YDoc { @@ -153,10 +156,10 @@ const messageListener = (conn: CollaborationWebSocket, doc: SharedSuperDoc, mess } default: - console.warn('Unknown message type:', messageType); + log.warn('Unknown message type:', messageType); } } catch (err) { - console.error('Error in messageListener:', err); + log.error('Error in messageListener:', err); (doc as YDocWithEmit).emit('error', [err]); } }; diff --git a/packages/collaboration-yjs/src/tests/internal-logger.test.ts b/packages/collaboration-yjs/src/tests/internal-logger.test.ts index df79fe17b6..47377c70bd 100644 --- a/packages/collaboration-yjs/src/tests/internal-logger.test.ts +++ b/packages/collaboration-yjs/src/tests/internal-logger.test.ts @@ -6,27 +6,29 @@ describe('createLogger', () => { let consoleSpy: ReturnType; beforeEach(() => { - consoleSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); + // The logger now routes through the shared @superdoc/common logger, whose + // console sink writes `info`-level records via console.info. + consoleSpy = vi.spyOn(console, 'info').mockImplementation(() => {}); }); afterEach(() => { consoleSpy.mockRestore(); }); - test('logs with configured color for known label', () => { + test('prefixes output with the label', () => { const logger = createLogger('ConnectionHandler'); logger('connected', 123); - expect(consoleSpy).toHaveBeenCalledWith('\x1b[34m[ConnectionHandler]\x1b[0m', 'connected', 123); + expect(consoleSpy).toHaveBeenCalledWith('[ConnectionHandler]', 'connected', 123); }); - test('falls back to reset color for unknown label', () => { + test('prefixes output for any label', () => { const logger = createLogger('Custom'); logger('info'); - expect(consoleSpy).toHaveBeenCalledWith('\x1b[0m[Custom]\x1b[0m', 'info'); + expect(consoleSpy).toHaveBeenCalledWith('[Custom]', 'info'); }); test('returns a stable logging function', () => { diff --git a/packages/layout-engine/layout-bridge/src/debounced-passes.ts b/packages/layout-engine/layout-bridge/src/debounced-passes.ts index e90533ba53..660a5e0a98 100644 --- a/packages/layout-engine/layout-bridge/src/debounced-passes.ts +++ b/packages/layout-engine/layout-bridge/src/debounced-passes.ts @@ -20,6 +20,10 @@ * @module debounced-passes */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:debounced-passes'); + /** * Configuration for a debounced pass. */ @@ -157,9 +161,7 @@ export class DebouncedPassManager { } catch (error) { // Silently handle errors to prevent cascading failures // In production, this would be logged - if (typeof console !== 'undefined' && console.error) { - console.error(`Error executing debounced pass "${passId}":`, error); - } + log.error(`Error executing debounced pass "${passId}":`, error); } } diff --git a/packages/layout-engine/layout-bridge/src/dirty-tracker.ts b/packages/layout-engine/layout-bridge/src/dirty-tracker.ts index 2c1a82bf77..d09750f2e3 100644 --- a/packages/layout-engine/layout-bridge/src/dirty-tracker.ts +++ b/packages/layout-engine/layout-bridge/src/dirty-tracker.ts @@ -14,6 +14,10 @@ * @module dirty-tracker */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:dirty-tracker'); + /** * Represents a range of dirty content with reason for tracking. */ @@ -50,7 +54,7 @@ export class DirtyTracker { */ markPageDirty(pageIndex: number, reason: DirtyRange['reason']): void { if (pageIndex < 0) { - console.warn(`[DirtyTracker] Invalid page index: ${pageIndex}. Must be non-negative. Ignoring.`); + log.warn(`[DirtyTracker] Invalid page index: ${pageIndex}. Must be non-negative. Ignoring.`); return; // Ignore invalid page indices } @@ -76,7 +80,7 @@ export class DirtyTracker { */ markBlocksDirty(startBlock: number, endBlock: number, reason: DirtyRange['reason']): void { if (startBlock < 0 || endBlock < startBlock) { - console.warn( + log.warn( `[DirtyTracker] Invalid block range: [${startBlock}, ${endBlock}]. Start must be non-negative and end must be >= start. Ignoring.`, ); return; // Ignore invalid ranges @@ -105,7 +109,7 @@ export class DirtyTracker { */ markDirtyFrom(startPage: number, reason: DirtyRange['reason']): void { if (startPage < 0) { - console.warn(`[DirtyTracker] Invalid start page: ${startPage}. Must be non-negative. Ignoring.`); + log.warn(`[DirtyTracker] Invalid start page: ${startPage}. Must be non-negative. Ignoring.`); return; // Ignore invalid page indices } diff --git a/packages/layout-engine/layout-bridge/src/dom-mapping.ts b/packages/layout-engine/layout-bridge/src/dom-mapping.ts index 16fcc01d13..e099652bf0 100644 --- a/packages/layout-engine/layout-bridge/src/dom-mapping.ts +++ b/packages/layout-engine/layout-bridge/src/dom-mapping.ts @@ -25,12 +25,15 @@ */ import { DOM_CLASS_NAMES, STRUCTURED_CONTENT_CHROME_LABEL_CLASS_NAMES } from '@superdoc/dom-contract'; +import { createLogger } from '@superdoc/common/logger'; + +const logger = createLogger('layout-bridge:dom-map'); // Debug logging for click-to-position pipeline (disabled - enable for debugging) const DEBUG_CLICK_MAPPING = false; const log = (...args: unknown[]) => { if (DEBUG_CLICK_MAPPING) { - console.log('[DOM-MAP]', ...args); + logger.debug(...args); } }; diff --git a/packages/layout-engine/layout-bridge/src/focus-watchdog.ts b/packages/layout-engine/layout-bridge/src/focus-watchdog.ts index 0ffc79ec97..073abe84ae 100644 --- a/packages/layout-engine/layout-bridge/src/focus-watchdog.ts +++ b/packages/layout-engine/layout-bridge/src/focus-watchdog.ts @@ -16,6 +16,10 @@ * @module focus-watchdog */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:focus-watchdog'); + /** * Configuration for focus watchdog. */ @@ -115,7 +119,7 @@ export class FocusWatchdog { } if (!this.expectedFocusElement) { - console.warn('FocusWatchdog: Cannot start without expected focus element'); + log.warn('FocusWatchdog: Cannot start without expected focus element'); return; } @@ -124,7 +128,7 @@ export class FocusWatchdog { this.check(); }, this.config.checkInterval); - console.log('FocusWatchdog: Started monitoring'); + log.debug('FocusWatchdog: Started monitoring'); } /** @@ -148,7 +152,7 @@ export class FocusWatchdog { } this.running = false; - console.log('FocusWatchdog: Stopped monitoring'); + log.debug('FocusWatchdog: Stopped monitoring'); } /** @@ -234,15 +238,15 @@ export class FocusWatchdog { const restored = document.activeElement === this.expectedFocusElement; if (restored) { - console.log('FocusWatchdog: Focus restored successfully'); + log.debug('FocusWatchdog: Focus restored successfully'); this.config.onRecovery(); return true; } else { - console.warn('FocusWatchdog: Failed to restore focus'); + log.warn('FocusWatchdog: Failed to restore focus'); return false; } } catch (err) { - console.error('FocusWatchdog: Error restoring focus:', err); + log.error('FocusWatchdog: Error restoring focus:', err); return false; } } @@ -255,7 +259,7 @@ export class FocusWatchdog { private handleDrift(target: Element | null): void { this.driftCount++; - console.warn( + log.warn( `FocusWatchdog: Focus drift detected (${this.driftCount}/${this.config.maxDriftCount})`, 'Target:', target, @@ -266,7 +270,7 @@ export class FocusWatchdog { // Attempt recovery if threshold reached if (this.driftCount >= this.config.maxDriftCount) { - console.warn('FocusWatchdog: Drift threshold reached, attempting recovery'); + log.warn('FocusWatchdog: Drift threshold reached, attempting recovery'); this.restoreFocus(); this.driftCount = 0; // Reset after recovery attempt } diff --git a/packages/layout-engine/layout-bridge/src/font-metrics-cache.ts b/packages/layout-engine/layout-bridge/src/font-metrics-cache.ts index 309e63cc64..a61c984a59 100644 --- a/packages/layout-engine/layout-bridge/src/font-metrics-cache.ts +++ b/packages/layout-engine/layout-bridge/src/font-metrics-cache.ts @@ -12,6 +12,10 @@ * @module font-metrics-cache */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:font-metrics-cache'); + /** * Metrics for a specific font configuration. */ @@ -164,7 +168,7 @@ export class FontMetricsCache { for (const config of fonts) { // Validate fontKey format (should be "family|size|weight|style") if (!this.isValidFontKey(config.fontKey)) { - console.warn( + log.warn( `[FontMetricsCache] Invalid fontKey format: "${config.fontKey}". Expected format: "family|size|weight|style". Skipping.`, ); continue; diff --git a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts index d035f504f8..bf025bcb53 100644 --- a/packages/layout-engine/layout-bridge/src/incrementalLayout.ts +++ b/packages/layout-engine/layout-bridge/src/incrementalLayout.ts @@ -35,6 +35,9 @@ import { FeatureFlags } from './featureFlags'; import { PageTokenLogger, HeaderFooterCacheLogger, globalMetrics } from './instrumentation'; import { HeaderFooterCacheState, invalidateHeaderFooterCache } from './cacheInvalidation'; import { getPreferredReserveCandidates, getPreferredReserveTrialTargets, scoreFootnoteWindow } from './footnote-scorer'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:incremental'); export type HeaderFooterMeasureFn = ( block: FlowBlock, @@ -75,7 +78,7 @@ const layoutDebugEnabled = const perfLog = (...args: unknown[]): void => { if (!layoutDebugEnabled) return; - console.log(...args); + log.debug(...args); }; type FootnoteReference = { id: string; pos: number }; @@ -1197,7 +1200,7 @@ export async function incrementalLayout( footerContentHeightsBySectionRef = measuredHeights.heightsBySectionRef; } } catch (error) { - console.error('[Layout] Footer pre-layout failed:', error); + log.error('[Layout] Footer pre-layout failed:', error); footerContentHeights = undefined; } @@ -1319,7 +1322,7 @@ export async function incrementalLayout( if (iteration >= maxIterations) { converged = false; - console.warn( + log.warn( `[incrementalLayout] Page token resolution did not converge after ${maxIterations} iterations - stopping`, ); } @@ -1872,14 +1875,14 @@ export async function incrementalLayout( } if (cappedPages.size > 0) { - console.warn('[layout] Footnote reserve capped to preserve body area', { + log.warn('[layout] Footnote reserve capped to preserve body area', { pages: Array.from(cappedPages), }); } if (pendingByColumn.size > 0) { const pendingIds = new Set(); pendingByColumn.forEach((entries) => entries.forEach((entry) => pendingIds.add(entry.id))); - console.warn('[layout] Footnote content truncated: extends beyond document pages', { + log.warn('[layout] Footnote content truncated: extends beyond document pages', { ids: Array.from(pendingIds), }); } @@ -2329,7 +2332,7 @@ export async function incrementalLayout( } } if (!reservesStabilized) { - console.warn( + log.warn( `[incrementalLayout] Footnote reserve loop did not converge (max ${MAX_FOOTNOTE_LAYOUT_PASSES} passes); layout may have suboptimal footnote placement.`, ); } @@ -2507,7 +2510,7 @@ export async function incrementalLayout( if (trialConverged && score.accept) { if (layoutDebugEnabled) { - console.log('[incrementalLayout] Accepted footnote preferred-reserve trial', { + log.debug('[incrementalLayout] Accepted footnote preferred-reserve trial', { pageIndex: candidate.pageIndex, targetReserve, score, @@ -2519,7 +2522,7 @@ export async function incrementalLayout( } if (layoutDebugEnabled) { - console.log('[incrementalLayout] Rejected footnote preferred-reserve trial', { + log.debug('[incrementalLayout] Rejected footnote preferred-reserve trial', { pageIndex: candidate.pageIndex, targetReserve, trialConverged, @@ -2540,7 +2543,7 @@ export async function incrementalLayout( } if (layoutDebugEnabled && (acceptedPreferredTrials > 0 || rejectedPreferredTrials > 0)) { - console.log('[incrementalLayout] Footnote preferred-reserve trials', { + log.debug('[incrementalLayout] Footnote preferred-reserve trials', { accepted: acceptedPreferredTrials, rejected: rejectedPreferredTrials, }); @@ -2570,7 +2573,7 @@ export async function incrementalLayout( if (needsWork) { if (!(await growReserves(GROW_MAX_PASSES))) { - console.warn( + log.warn( '[incrementalLayout] Footnote post-reserve loop did not converge; some pages may have footnotes overflowing the reserved band.', ); } @@ -3068,7 +3071,7 @@ async function remeasureAffectedBlocks( measureCache?.set(block, blockConstraints.maxWidth, blockConstraints.maxHeight, newMeasure); } catch (error) { // Error handling per plan: log warning, keep prior layout for block - console.warn(`[incrementalLayout] Failed to re-measure block ${block.id} after token resolution:`, error); + log.warn(`[incrementalLayout] Failed to re-measure block ${block.id} after token resolution:`, error); // Keep the old measure - don't update updatedMeasures[i] } } diff --git a/packages/layout-engine/layout-bridge/src/index.ts b/packages/layout-engine/layout-bridge/src/index.ts index 62a30c97aa..ae34dbda32 100644 --- a/packages/layout-engine/layout-bridge/src/index.ts +++ b/packages/layout-engine/layout-bridge/src/index.ts @@ -22,6 +22,7 @@ import { import { describeCellRenderBlocks, computeCellSliceContentHeight, getEmbeddedRowLines } from '@superdoc/layout-engine'; import { measureCharacterX } from './text-measurement.js'; import { clickToPositionDom, findPageElement } from './dom-mapping.js'; +import { createLogger } from '@superdoc/common/logger'; import { isListItem, getWordLayoutConfig, @@ -244,6 +245,8 @@ import { clickToPositionGeometry, } from './position-hit.js'; +const log = createLogger('layout-bridge'); + export type Rect = { x: number; y: number; width: number; height: number; pageIndex: number }; type AtomicFragment = DrawingFragment | ImageFragment; @@ -256,9 +259,9 @@ const SELECTION_DEBUG_ENABLED = false; const logSelectionDebug = (payload: Record): void => { if (!SELECTION_DEBUG_ENABLED) return; try { - console.log('[SELECTION-DEBUG]', JSON.stringify(payload)); + log.debug('[SELECTION-DEBUG]', JSON.stringify(payload)); } catch { - console.log('[SELECTION-DEBUG]', payload); + log.debug('[SELECTION-DEBUG]', payload); } }; @@ -276,9 +279,9 @@ const DEBUG_POSITION_MAPPING = false; const logPositionDebug = (payload: Record): void => { if (!DEBUG_POSITION_MAPPING) return; try { - console.log('[CLICK-POS]', JSON.stringify(payload)); + log.debug('[CLICK-POS]', JSON.stringify(payload)); } catch { - console.log('[CLICK-POS]', payload); + log.debug('[CLICK-POS]', payload); } }; @@ -289,9 +292,9 @@ const logPositionDebug = (payload: Record): void => { const logSelectionMapDebug = (payload: Record): void => { if (!DEBUG_POSITION_MAPPING) return; try { - console.log('[SELECTION-MAP]', JSON.stringify(payload)); + log.debug('[SELECTION-MAP]', JSON.stringify(payload)); } catch { - console.log('[SELECTION-MAP]', payload); + log.debug('[SELECTION-MAP]', payload); } }; @@ -1547,7 +1550,7 @@ const mapPmToX = ( // Validation: Warn when indents exceed fragment width (potential layout issue) if (totalIndent > fragmentWidth) { - console.warn( + log.warn( `[mapPmToX] Paragraph indents (${totalIndent}px) exceed fragment width (${fragmentWidth}px) ` + `for block ${block.id}. This may indicate a layout miscalculation. ` + `Available width clamped to 0.`, diff --git a/packages/layout-engine/layout-bridge/src/instrumentation.ts b/packages/layout-engine/layout-bridge/src/instrumentation.ts index 5e19b0a9f3..0af93fff2a 100644 --- a/packages/layout-engine/layout-bridge/src/instrumentation.ts +++ b/packages/layout-engine/layout-bridge/src/instrumentation.ts @@ -14,6 +14,9 @@ import { FeatureFlags } from './featureFlags'; import type { MeasureCacheStats } from './cache'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:instrumentation'); /** * Performance metrics for page token resolution. @@ -84,7 +87,7 @@ export const PageTokenLogger = { logIterationStart(iteration: number, totalPages: number): void { if (!FeatureFlags.DEBUG_PAGE_TOKENS) return; - console.log(`[PageTokens] Iteration ${iteration}: Resolving tokens for ${totalPages} pages`); + log.debug(`[PageTokens] Iteration ${iteration}: Resolving tokens for ${totalPages} pages`); }, /** @@ -100,10 +103,7 @@ export const PageTokenLogger = { const count = affectedBlockIds.size; const samples = blockSamples.slice(0, 5).join(', '); - console.log( - `[PageTokens] Iteration ${iteration}: ${count} blocks affected`, - samples ? `(samples: ${samples})` : '', - ); + log.debug(`[PageTokens] Iteration ${iteration}: ${count} blocks affected`, samples ? `(samples: ${samples})` : ''); }, /** @@ -117,9 +117,9 @@ export const PageTokenLogger = { if (!FeatureFlags.DEBUG_PAGE_TOKENS) return; if (converged) { - console.log(`[PageTokens] Converged after ${iteration} iterations in ${totalTimeMs.toFixed(2)}ms`); + log.debug(`[PageTokens] Converged after ${iteration} iterations in ${totalTimeMs.toFixed(2)}ms`); } else { - console.warn(`[PageTokens] Did NOT converge after ${iteration} iterations (${totalTimeMs.toFixed(2)}ms)`); + log.warn(`[PageTokens] Did NOT converge after ${iteration} iterations (${totalTimeMs.toFixed(2)}ms)`); } }, @@ -132,7 +132,7 @@ export const PageTokenLogger = { logError(blockId: string, error: unknown): void { if (!FeatureFlags.DEBUG_PAGE_TOKENS) return; - console.error(`[PageTokens] Error resolving tokens in block ${blockId}:`, error); + log.error(`[PageTokens] Error resolving tokens in block ${blockId}:`, error); }, /** @@ -144,7 +144,7 @@ export const PageTokenLogger = { logRemeasure(blockCount: number, timeMs: number): void { if (!FeatureFlags.DEBUG_PAGE_TOKENS) return; - console.log(`[PageTokens] Re-measured ${blockCount} blocks in ${timeMs.toFixed(2)}ms`); + log.debug(`[PageTokens] Re-measured ${blockCount} blocks in ${timeMs.toFixed(2)}ms`); }, }; @@ -163,7 +163,7 @@ export const HeaderFooterCacheLogger = { logCacheHit(variantType: string, pageNumber: number, bucket: string): void { if (!FeatureFlags.DEBUG_HF_CACHE) return; - console.log(`[HF Cache] HIT: variant=${variantType}, page=${pageNumber}, bucket=${bucket}`); + log.debug(`[HF Cache] HIT: variant=${variantType}, page=${pageNumber}, bucket=${bucket}`); }, /** @@ -176,7 +176,7 @@ export const HeaderFooterCacheLogger = { logCacheMiss(variantType: string, pageNumber: number, bucket: string): void { if (!FeatureFlags.DEBUG_HF_CACHE) return; - console.log(`[HF Cache] MISS: variant=${variantType}, page=${pageNumber}, bucket=${bucket}`); + log.debug(`[HF Cache] MISS: variant=${variantType}, page=${pageNumber}, bucket=${bucket}`); }, /** @@ -188,7 +188,7 @@ export const HeaderFooterCacheLogger = { logInvalidation(reason: string, affectedBlockIds: string[]): void { if (!FeatureFlags.DEBUG_HF_CACHE) return; - console.log(`[HF Cache] INVALIDATE: reason=${reason}, blocks=${affectedBlockIds.length}`); + log.debug(`[HF Cache] INVALIDATE: reason=${reason}, blocks=${affectedBlockIds.length}`); }, /** @@ -202,7 +202,7 @@ export const HeaderFooterCacheLogger = { const hitRate = stats.hits + stats.misses > 0 ? ((stats.hits / (stats.hits + stats.misses)) * 100).toFixed(1) : '0.0'; - console.log( + log.debug( `[HF Cache] Stats: hits=${stats.hits}, misses=${stats.misses}, hitRate=${hitRate}%, size=${stats.size}, evictions=${stats.evictions}`, ); }, @@ -218,9 +218,9 @@ export const HeaderFooterCacheLogger = { if (!FeatureFlags.DEBUG_HF_CACHE) return; if (useBucketing && buckets) { - console.log(`[HF Cache] Bucketing enabled: ${totalPages} pages, buckets=${buckets.join(', ')}`); + log.debug(`[HF Cache] Bucketing enabled: ${totalPages} pages, buckets=${buckets.join(', ')}`); } else { - console.log(`[HF Cache] Bucketing disabled: ${totalPages} pages (per-page layouts)`); + log.debug(`[HF Cache] Bucketing disabled: ${totalPages} pages (per-page layouts)`); } }, }; @@ -314,7 +314,7 @@ export class MetricsCollector { private checkPageTokenRollbackTriggers(metrics: PageTokenMetrics): void { // Trigger 1: Too many iterations if (metrics.iterations > 2 && !metrics.converged) { - console.warn( + log.warn( `[Rollback Trigger] Page token resolution did not converge after ${metrics.iterations} iterations. ` + `Consider disabling SD_BODY_PAGE_TOKENS if this persists.`, ); @@ -322,7 +322,7 @@ export class MetricsCollector { // Trigger 2: Slow token resolution if (metrics.totalTimeMs > 100 && metrics.iterations > 0) { - console.warn( + log.warn( `[Rollback Trigger] Page token resolution took ${metrics.totalTimeMs.toFixed(2)}ms (>100ms threshold). ` + `Consider disabling SD_BODY_PAGE_TOKENS if performance is unacceptable.`, ); @@ -344,7 +344,7 @@ export class MetricsCollector { // Trigger 1: Low hit rate (cache thrash) if (metrics.hits + metrics.misses > 10 && metrics.hitRate < MIN_HIT_RATE) { - console.warn( + log.warn( `[Rollback Trigger] Header/footer cache hit rate is low (${metrics.hitRate.toFixed(1)}% < ${MIN_HIT_RATE}%). ` + `Consider disabling SD_HF_DIGIT_BUCKETING if cache thrashing persists.`, ); @@ -353,7 +353,7 @@ export class MetricsCollector { // Trigger 2: Excessive memory usage (approximate check) // Note: This is a rough heuristic - actual page count would need to be passed in if (metrics.memoryEstimate > MAX_MEMORY_PER_100_PAGES) { - console.warn( + log.warn( `[Rollback Trigger] Header/footer cache memory usage is high (${(metrics.memoryEstimate / 1_000_000).toFixed(2)}MB). ` + `Monitor for excessive growth.`, ); @@ -381,7 +381,7 @@ export const LayoutVersionLogger = { logStaleLayoutRead(versionGap: number, stalenessDuration: number): void { if (!FeatureFlags.DEBUG_LAYOUT_VERSION) return; - console.warn( + log.warn( `[LayoutVersion] Selection overlay using STALE layout ` + `(gap: ${versionGap} versions, stale for: ${stalenessDuration}ms)`, ); @@ -396,7 +396,7 @@ export const LayoutVersionLogger = { logGeometryFallback(reason: string, pos: number): void { if (!FeatureFlags.DEBUG_LAYOUT_VERSION) return; - console.warn(`[LayoutVersion] Geometry fallback used: ${reason} (pos: ${pos})`); + log.warn(`[LayoutVersion] Geometry fallback used: ${reason} (pos: ${pos})`); }, /** @@ -409,7 +409,7 @@ export const LayoutVersionLogger = { if (!FeatureFlags.DEBUG_LAYOUT_VERSION) return; if (versionGap > 0) { - console.log(`[LayoutVersion] Layout caught up after ${versionGap} versions (stale for ${stalenessDuration}ms)`); + log.debug(`[LayoutVersion] Layout caught up after ${versionGap} versions (stale for ${stalenessDuration}ms)`); } }, @@ -421,7 +421,7 @@ export const LayoutVersionLogger = { logPmTransaction(newVersion: number): void { if (!FeatureFlags.DEBUG_LAYOUT_VERSION) return; - console.log(`[LayoutVersion] PM transaction → v${newVersion}`); + log.debug(`[LayoutVersion] PM transaction → v${newVersion}`); }, /** @@ -434,6 +434,6 @@ export const LayoutVersionLogger = { if (!FeatureFlags.DEBUG_LAYOUT_VERSION) return; const status = isStale ? 'STALE' : 'CURRENT'; - console.log(`[LayoutVersion] Layout complete → v${version} (${status})`); + log.debug(`[LayoutVersion] Layout complete → v${version} (${status})`); }, }; diff --git a/packages/layout-engine/layout-bridge/src/page-geometry-helper.ts b/packages/layout-engine/layout-bridge/src/page-geometry-helper.ts index 8c5268bd1d..bbab961b5d 100644 --- a/packages/layout-engine/layout-bridge/src/page-geometry-helper.ts +++ b/packages/layout-engine/layout-bridge/src/page-geometry-helper.ts @@ -13,6 +13,9 @@ */ import type { Layout } from '@superdoc/contracts'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:page-geometry'); /** * Configuration for page geometry calculations. @@ -289,7 +292,7 @@ export class PageGeometryHelper { if (this.cache !== null) { // Validate cache integrity if (!Array.isArray(this.cache.cumulativeY) || !Array.isArray(this.cache.pageHeights)) { - console.warn('[PageGeometryHelper] Cache corruption detected, rebuilding cache'); + log.warn('[PageGeometryHelper] Cache corruption detected, rebuilding cache'); this.cache = null; } else { return; @@ -365,7 +368,7 @@ export class PageGeometryHelper { } catch (error) { // Log error and create a safe fallback cache with empty data const errorMessage = error instanceof Error ? error.message : String(error); - console.error(`[PageGeometryHelper] Cache build failed: ${errorMessage}. Using fallback empty cache.`); + log.error(`[PageGeometryHelper] Cache build failed: ${errorMessage}. Using fallback empty cache.`); // Provide safe fallback cache with no pages this.cache = { diff --git a/packages/layout-engine/layout-bridge/src/position-hit.ts b/packages/layout-engine/layout-bridge/src/position-hit.ts index fbcb239695..97c5f2341b 100644 --- a/packages/layout-engine/layout-bridge/src/position-hit.ts +++ b/packages/layout-engine/layout-bridge/src/position-hit.ts @@ -31,6 +31,9 @@ import { } from '@superdoc/contracts'; import { charOffsetToPm, findCharacterAtX } from './text-measurement.js'; import type { PageGeometryHelper } from './page-geometry-helper.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:position-hit'); // --------------------------------------------------------------------------- // Types @@ -828,7 +831,7 @@ export function clickToPositionGeometry( let availableWidth = Math.max(0, fragment.width - totalIndent); if (totalIndent > fragment.width) { - console.warn( + log.warn( `[clickToPosition] Paragraph indents (${totalIndent}px) exceed fragment width (${fragment.width}px) ` + `for block ${fragment.blockId}. This may indicate a layout miscalculation. ` + `Available width clamped to 0.`, @@ -905,7 +908,7 @@ export function clickToPositionGeometry( let availableWidth = Math.max(0, tableHit.fragment.width - totalIndent); if (totalIndent > tableHit.fragment.width) { - console.warn( + log.warn( `[clickToPosition:table] Paragraph indents (${totalIndent}px) exceed fragment width (${tableHit.fragment.width}px) ` + `for block ${tableHit.fragment.blockId}. This may indicate a layout miscalculation. ` + `Available width clamped to 0.`, diff --git a/packages/layout-engine/layout-bridge/src/resolveHeaderFooterTokens.ts b/packages/layout-engine/layout-bridge/src/resolveHeaderFooterTokens.ts index d7eeaade60..1ffd00a0ba 100644 --- a/packages/layout-engine/layout-bridge/src/resolveHeaderFooterTokens.ts +++ b/packages/layout-engine/layout-bridge/src/resolveHeaderFooterTokens.ts @@ -11,6 +11,9 @@ */ import type { FlowBlock, ParagraphBlock, TableBlock } from '@superdoc/contracts'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:header-footer-tokens'); /** * Walk every paragraph block reachable through `blocks`, including those @@ -79,12 +82,12 @@ export function resolveHeaderFooterTokens( } if (!Number.isFinite(pageNumber) || pageNumber < 1) { - console.warn('[resolveHeaderFooterTokens] Invalid pageNumber:', pageNumber, '- using 1 as fallback'); + log.warn('[resolveHeaderFooterTokens] Invalid pageNumber:', pageNumber, '- using 1 as fallback'); pageNumber = 1; } if (!Number.isFinite(totalPages) || totalPages < 1) { - console.warn('[resolveHeaderFooterTokens] Invalid totalPages:', totalPages, '- using 1 as fallback'); + log.warn('[resolveHeaderFooterTokens] Invalid totalPages:', totalPages, '- using 1 as fallback'); totalPages = 1; } diff --git a/packages/layout-engine/layout-bridge/src/safety-net.ts b/packages/layout-engine/layout-bridge/src/safety-net.ts index 7e6bce907a..4050a1a909 100644 --- a/packages/layout-engine/layout-bridge/src/safety-net.ts +++ b/packages/layout-engine/layout-bridge/src/safety-net.ts @@ -18,6 +18,10 @@ * @module safety-net */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:safety-net'); + /** * Reason for triggering fallback to legacy behavior. */ @@ -149,7 +153,7 @@ export class SafetyNet { // Log error for debugging if (this.config.maxConsecutiveErrors > 0) { - console.error(`SafetyNet: Error ${this.errorCount}/${this.config.maxConsecutiveErrors}:`, error.message); + log.error(`SafetyNet: Error ${this.errorCount}/${this.config.maxConsecutiveErrors}:`, error.message); } // Trigger fallback if threshold exceeded @@ -177,12 +181,10 @@ export class SafetyNet { if (metric === 'layout' && value > this.config.maxLayoutDuration) { exceeded = true; - console.warn( - `SafetyNet: Layout duration ${value.toFixed(2)}ms exceeds budget ${this.config.maxLayoutDuration}ms`, - ); + log.warn(`SafetyNet: Layout duration ${value.toFixed(2)}ms exceeds budget ${this.config.maxLayoutDuration}ms`); } else if (metric === 'cursor' && value > this.config.maxCursorLatency) { exceeded = true; - console.warn(`SafetyNet: Cursor latency ${value.toFixed(2)}ms exceeds budget ${this.config.maxCursorLatency}ms`); + log.warn(`SafetyNet: Cursor latency ${value.toFixed(2)}ms exceeds budget ${this.config.maxCursorLatency}ms`); } if (exceeded) { @@ -246,7 +248,7 @@ export class SafetyNet { this.fallbackActive = true; this.fallbackReason = reason; - console.warn(`SafetyNet: Triggering fallback (reason: ${reason})`); + log.warn(`SafetyNet: Triggering fallback (reason: ${reason})`); // Notify handler if (this.onFallback) { @@ -280,7 +282,7 @@ export class SafetyNet { return false; // Still in cooldown } - console.log('SafetyNet: Attempting recovery from fallback'); + log.debug('SafetyNet: Attempting recovery from fallback'); this.fallbackActive = false; this.fallbackReason = null; diff --git a/packages/layout-engine/layout-bridge/src/text-measurement.ts b/packages/layout-engine/layout-bridge/src/text-measurement.ts index 6e04509441..d7c2648b85 100644 --- a/packages/layout-engine/layout-bridge/src/text-measurement.ts +++ b/packages/layout-engine/layout-bridge/src/text-measurement.ts @@ -5,6 +5,9 @@ import { sliceRunsForLine, SPACE_CHARS as SHARED_SPACE_CHARS, } from '@superdoc/contracts'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-bridge:text-measurement'); /** * Shared text measurement utility for accurate character positioning. @@ -105,7 +108,7 @@ function getMeasurementContext(): CanvasRenderingContext2D | null { if (typeof document === 'undefined') { // Only warn in non-test environments - Canvas fallback is expected in tests if (process.env.NODE_ENV !== 'test') { - console.warn('[text-measurement] Canvas not available (non-browser environment)'); + log.warn('[text-measurement] Canvas not available (non-browser environment)'); } return null; } @@ -118,7 +121,7 @@ function getMeasurementContext(): CanvasRenderingContext2D | null { } if (!measurementCtx && process.env.NODE_ENV !== 'test') { - console.warn('[text-measurement] Failed to create 2D context'); + log.warn('[text-measurement] Failed to create 2D context'); } return measurementCtx; @@ -618,7 +621,7 @@ function measureCharacterXSegmentBased( export function charOffsetToPm(block: FlowBlock, line: Line, charOffset: number, fallbackPmStart: number): number { // Validate inputs if (!Number.isFinite(charOffset) || !Number.isFinite(fallbackPmStart)) { - console.warn('[charOffsetToPm] Invalid input:', { charOffset, fallbackPmStart }); + log.warn('[charOffsetToPm] Invalid input:', { charOffset, fallbackPmStart }); return fallbackPmStart; } diff --git a/packages/layout-engine/layout-bridge/test/instrumentation.test.ts b/packages/layout-engine/layout-bridge/test/instrumentation.test.ts index a61d65f139..90f0ef6f14 100644 --- a/packages/layout-engine/layout-bridge/test/instrumentation.test.ts +++ b/packages/layout-engine/layout-bridge/test/instrumentation.test.ts @@ -136,7 +136,7 @@ describe('Instrumentation', () => { }); expect(consoleWarnSpy).toHaveBeenCalled(); - const call = consoleWarnSpy.mock.calls[0][0]; + const call = consoleWarnSpy.mock.calls[0][1]; expect(call).toContain('[Rollback Trigger]'); expect(call).toContain('did not converge'); }); @@ -152,7 +152,7 @@ describe('Instrumentation', () => { }); expect(consoleWarnSpy).toHaveBeenCalled(); - const call = consoleWarnSpy.mock.calls[0][0]; + const call = consoleWarnSpy.mock.calls[0][1]; expect(call).toContain('[Rollback Trigger]'); expect(call).toContain('>100ms threshold'); }); @@ -170,7 +170,7 @@ describe('Instrumentation', () => { }); expect(consoleWarnSpy).toHaveBeenCalled(); - const call = consoleWarnSpy.mock.calls[0][0]; + const call = consoleWarnSpy.mock.calls[0][1]; expect(call).toContain('[Rollback Trigger]'); expect(call).toContain('hit rate is low'); }); @@ -188,7 +188,7 @@ describe('Instrumentation', () => { }); expect(consoleWarnSpy).toHaveBeenCalled(); - const call = consoleWarnSpy.mock.calls[0][0]; + const call = consoleWarnSpy.mock.calls[0][1]; expect(call).toContain('[Rollback Trigger]'); expect(call).toContain('memory usage is high'); }); diff --git a/packages/layout-engine/layout-engine/src/index.ts b/packages/layout-engine/layout-engine/src/index.ts index 00cb67ea06..5e23df4dc1 100644 --- a/packages/layout-engine/layout-engine/src/index.ts +++ b/packages/layout-engine/layout-engine/src/index.ts @@ -54,6 +54,9 @@ import { formatPageNumber } from './pageNumbering.js'; import { shouldSuppressSpacingForEmpty, shouldSuppressOwnSpacing } from './layout-utils.js'; import { balanceSectionOnPage, type BalancingFragment, type MeasureData } from './column-balancing.js'; import { cloneColumnLayout, widthsEqual } from './column-utils.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-engine'); type PageSize = { w: number; h: number }; type Margins = { @@ -664,7 +667,7 @@ const layoutDebugEnabled = const layoutLog = (...args: unknown[]): void => { if (!layoutDebugEnabled) return; - console.log(...args); + log.debug(...args); }; /** diff --git a/packages/layout-engine/layout-engine/src/resolvePageRefs.ts b/packages/layout-engine/layout-engine/src/resolvePageRefs.ts index be000f550b..4ca5ba97a4 100644 --- a/packages/layout-engine/layout-engine/src/resolvePageRefs.ts +++ b/packages/layout-engine/layout-engine/src/resolvePageRefs.ts @@ -7,6 +7,9 @@ */ import type { Layout, FlowBlock, ParagraphBlock } from '@superdoc/contracts'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('layout-engine:page-refs'); /** * Build an anchor map from bookmarks and layout fragments. @@ -38,7 +41,7 @@ export function buildAnchorMap(bookmarks: Map, layout: Layout): } // Bookmark not found in any fragment - log warning but continue - console.warn(`[resolvePageRefs] Bookmark "${bookmarkName}" at PM position ${pmPosition} not found in layout`); + log.warn(`[resolvePageRefs] Bookmark "${bookmarkName}" at PM position ${pmPosition} not found in layout`); }); return anchorMap; @@ -77,7 +80,7 @@ export function resolvePageRefTokens(blocks: FlowBlock[], anchorMap: Map Math.round(px * TWIPS_PER_PX); export { getCellSpacingPx } from '@superdoc/contracts'; import { getCellSpacingPx } from '@superdoc/contracts'; +const log = createLogger('measuring-dom'); + /** * Returns the border width in pixels for a table border value (matches painter border-utils logic). * Used so total table dimensions include outer border sizes and there is enough space for last row/column spacing. @@ -1021,7 +1024,7 @@ async function measureParagraphBlock(block: ParagraphBlock, maxWidth: number): P if (dropCapDescriptor) { // Validate required fields before measuring if (!dropCapDescriptor.run || !dropCapDescriptor.run.text || !dropCapDescriptor.lines) { - console.warn('Invalid drop cap descriptor - missing required fields:', dropCapDescriptor); + log.warn('Invalid drop cap descriptor - missing required fields:', dropCapDescriptor); } else { const dropCapMeasured = measureDropCap(ctx, dropCapDescriptor, spacing); dropCapMeasure = dropCapMeasured; diff --git a/packages/layout-engine/painters/dom/src/images/image-fragment.ts b/packages/layout-engine/painters/dom/src/images/image-fragment.ts index 015c28fd7a..a2d8d246ef 100644 --- a/packages/layout-engine/painters/dom/src/images/image-fragment.ts +++ b/packages/layout-engine/painters/dom/src/images/image-fragment.ts @@ -5,6 +5,9 @@ import { CLASS_NAMES, fragmentStyles } from '../styles.js'; import { applyStyles } from '../utils/apply-styles.js'; import { createBlockImageContent } from './image-block.js'; import type { BuildImageHyperlinkAnchor } from './types.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:image'); type RenderImageFragmentOptions = { doc: Document | null; @@ -144,7 +147,7 @@ export const renderImageFragment = ({ return fragmentEl; } catch (error) { - console.error('[DomPainter] Image fragment rendering failed:', { fragment, error }); + log.error('[DomPainter] Image fragment rendering failed:', { fragment, error }); return createErrorPlaceholder(fragment.blockId, error); } }; diff --git a/packages/layout-engine/painters/dom/src/paragraph/renderParagraphFragment.ts b/packages/layout-engine/painters/dom/src/paragraph/renderParagraphFragment.ts index d4857f5c7a..1e97dcc008 100644 --- a/packages/layout-engine/painters/dom/src/paragraph/renderParagraphFragment.ts +++ b/packages/layout-engine/painters/dom/src/paragraph/renderParagraphFragment.ts @@ -8,11 +8,14 @@ import type { } from '@superdoc/contracts'; import { isMinimalWordLayout as isMinimalWordLayoutShared } from '@superdoc/common/list-marker-utils'; import type { MinimalWordLayout } from '@superdoc/common/list-marker-utils'; +import { createLogger } from '@superdoc/common/logger'; import { CLASS_NAMES, fragmentStyles } from '../styles.js'; import { shouldRenderSdtContainerChrome, type SdtBoundaryOptions } from '../sdt/container.js'; import type { BetweenBorderInfo } from './borders/index.js'; import { renderParagraphContent, type ParagraphRenderLineInput } from './renderParagraphContent.js'; +const log = createLogger('painter-dom:paragraph'); + type ApplyStyles = (el: HTMLElement, styles: Partial) => void; type RenderParagraphFragmentParams = { @@ -139,7 +142,7 @@ export const renderParagraphFragment = (params: RenderParagraphFragmentParams): return fragmentEl; } catch (error) { - console.error('[DomPainter] Fragment rendering failed:', { fragment, error }); + log.error('[DomPainter] Fragment rendering failed:', { fragment, error }); return createErrorPlaceholder(fragment.blockId, error); } }; diff --git a/packages/layout-engine/painters/dom/src/pm-position-validation.ts b/packages/layout-engine/painters/dom/src/pm-position-validation.ts index 84b712c8e5..0e9fca03d1 100644 --- a/packages/layout-engine/painters/dom/src/pm-position-validation.ts +++ b/packages/layout-engine/painters/dom/src/pm-position-validation.ts @@ -12,6 +12,10 @@ * 4. Provides fallback guidance when positions are missing */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:pm-position-validation'); + /** * Environment check for dev-mode warnings. * Only emit warnings when explicitly in development to keep test output clean. @@ -92,7 +96,7 @@ class ValidationStatsCollector { const s = this.stats; if (coverage < 100) { - console.warn('[PmPositionValidation] PM position coverage:', { + log.warn('[PmPositionValidation] PM position coverage:', { coverage: `${coverage.toFixed(1)}%`, totalSpans: s.totalSpans, validSpans: s.validSpans, @@ -143,7 +147,7 @@ export function assertPmPositions( if (!hasPmStart || !hasPmEnd) { const textPreview = run.text ? run.text.substring(0, 20) + (run.text.length > 20 ? '...' : '') : '(no text)'; - console.warn(`[PmPositionValidation] Missing PM positions in ${context}:`, { + log.warn(`[PmPositionValidation] Missing PM positions in ${context}:`, { hasPmStart, hasPmEnd, textPreview, @@ -200,7 +204,7 @@ export function validateRenderedElement(element: HTMLElement, context: string): if (!isDevelopment()) return; if (!hasPmStart || !hasPmEnd) { - console.warn(`[PmPositionValidation] Rendered element missing PM attributes in ${context}:`, { + log.warn(`[PmPositionValidation] Rendered element missing PM attributes in ${context}:`, { element: element.tagName, className: element.className, hasPmStart, diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 910e320008..52a1367da7 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -99,12 +99,15 @@ import { buildImageHyperlinkAnchor as buildSharedImageHyperlinkAnchor } from './ import { applyStyles } from './utils/apply-styles.js'; import { applyTrackedChangeDecorations, resolveTrackedChangesConfig } from './runs/tracked-changes.js'; import { applySourceAnchorDataset } from './utils/source-anchor.js'; +import { createLogger } from '@superdoc/common/logger'; export type { PaintSnapshotStructuredContentBlockEntity, PaintSnapshotStructuredContentInlineEntity, } from './sdt/snapshot.js'; +const log = createLogger('painter-dom'); + const ACTIVE_HEADER_FOOTER_WATERMARK_PREVIEW_OPACITY = '1'; const INACTIVE_HEADER_FOOTER_WATERMARK_PREVIEW_OPACITY = '0.5'; @@ -1754,13 +1757,13 @@ export class DomPainter { */ private renderPageRuler(pageWidthPx: number, page: ResolvedPage): HTMLElement | null { if (!this.doc) { - console.warn('[renderPageRuler] Cannot render ruler: document is not available.'); + log.warn('[renderPageRuler] Cannot render ruler: document is not available.'); return null; } const margins = page.margins; if (!margins) { - console.warn(`[renderPageRuler] Cannot render ruler for page ${page.number}: margins not available.`); + log.warn(`[renderPageRuler] Cannot render ruler for page ${page.number}: margins not available.`); return null; } @@ -1791,7 +1794,7 @@ export class DomPainter { const marginInches = side === 'left' ? x / ppi : (pageWidthPx - x) / ppi; onMarginChange(side, marginInches); } catch (error) { - console.error('[renderPageRuler] Error in onMarginChange callback:', error); + log.error('[renderPageRuler] Error in onMarginChange callback:', error); } } : undefined, @@ -1806,7 +1809,7 @@ export class DomPainter { return rulerEl; } catch (error) { - console.error(`[renderPageRuler] Failed to create ruler for page ${page.number}:`, error); + log.error(`[renderPageRuler] Failed to create ruler for page ${page.number}:`, error); return null; } } @@ -2409,7 +2412,7 @@ export class DomPainter { } } catch (error) { // Log the error but don't crash the paint cycle - corrupted mappings shouldn't break rendering - console.error('Error updating position attributes with mapping:', error); + log.error('Error updating position attributes with mapping:', error); } } @@ -2716,7 +2719,7 @@ export class DomPainter { return fragmentEl; } catch (error) { - console.error('[DomPainter] Drawing fragment rendering failed:', { fragment, error }); + log.error('[DomPainter] Drawing fragment rendering failed:', { fragment, error }); return this.createErrorPlaceholder(fragment.blockId, error); } } @@ -3222,7 +3225,7 @@ export class DomPainter { height: heightOverride ?? block.geometry.height, }); } catch (error) { - console.warn(`[DomPainter] Unable to render preset shape "${block.shapeKind}":`, error); + log.warn(`[DomPainter] Unable to render preset shape "${block.shapeKind}":`, error); return null; } } @@ -3768,7 +3771,7 @@ export class DomPainter { return el; } catch (error) { - console.error('[DomPainter] Table fragment rendering failed:', { fragment, error }); + log.error('[DomPainter] Table fragment rendering failed:', { fragment, error }); return this.createErrorPlaceholder(fragment.blockId, error); } } diff --git a/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.test.ts b/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.test.ts index 182757f279..142aa918e4 100644 --- a/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.test.ts +++ b/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.test.ts @@ -212,7 +212,10 @@ describe('createRulerElement', () => { const ruler = createRulerElement({ definition: invalidDefinition, doc }); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[createRulerElement] Invalid ruler width')); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[painter-dom:ruler]', + expect.stringContaining('[createRulerElement] Invalid ruler width'), + ); expect(ruler.style.width).toBe('1px'); // Fallback to 1px consoleWarnSpy.mockRestore(); @@ -224,7 +227,10 @@ describe('createRulerElement', () => { const ruler = createRulerElement({ definition: invalidDefinition, doc }); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('[createRulerElement] Invalid ruler width')); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[painter-dom:ruler]', + expect.stringContaining('[createRulerElement] Invalid ruler width'), + ); expect(ruler.style.width).toBe('1px'); consoleWarnSpy.mockRestore(); @@ -237,6 +243,7 @@ describe('createRulerElement', () => { createRulerElement({ definition: emptyTicksDefinition, doc }); expect(consoleWarnSpy).toHaveBeenCalledWith( + '[painter-dom:ruler]', expect.stringContaining('[createRulerElement] Ruler definition has no ticks'), ); diff --git a/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.ts b/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.ts index 5a3532f730..4f45ecb40f 100644 --- a/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.ts +++ b/packages/layout-engine/painters/dom/src/ruler/ruler-renderer.ts @@ -9,6 +9,9 @@ */ import type { RulerDefinition, RulerTick } from './ruler-core.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:ruler'); /** * CSS class names for ruler elements @@ -88,12 +91,12 @@ export function createRulerElement(options: CreateRulerElementOptions): HTMLElem // Validate definition if (!Number.isFinite(definition.widthPx) || definition.widthPx <= 0) { - console.warn(`[createRulerElement] Invalid ruler width: ${definition.widthPx}px. Using minimum width of 1px.`); + log.warn(`[createRulerElement] Invalid ruler width: ${definition.widthPx}px. Using minimum width of 1px.`); definition.widthPx = Math.max(1, definition.widthPx || 1); } if (!definition.ticks || definition.ticks.length === 0) { - console.warn('[createRulerElement] Ruler definition has no ticks. Ruler will be empty.'); + log.warn('[createRulerElement] Ruler definition has no ticks. Ruler will be empty.'); } const ruler = doc.createElement('div'); diff --git a/packages/layout-engine/painters/dom/src/runs/hash.ts b/packages/layout-engine/painters/dom/src/runs/hash.ts index 94002063a2..3213a7d146 100644 --- a/packages/layout-engine/painters/dom/src/runs/hash.ts +++ b/packages/layout-engine/painters/dom/src/runs/hash.ts @@ -1,4 +1,7 @@ import type { Run, TextRun } from '@superdoc/contracts'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:runs'); /** * Type guard to check if a run has a string property. @@ -130,7 +133,7 @@ export const applyRunDataAttributes = (element: HTMLElement, dataAttrs?: Record< element.setAttribute(key, value); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn(`[DomPainter] Failed to set data attribute "${key}":`, error); + log.warn(`[DomPainter] Failed to set data attribute "${key}":`, error); } } }); diff --git a/packages/layout-engine/painters/dom/src/runs/links.ts b/packages/layout-engine/painters/dom/src/runs/links.ts index 653e74fab8..d230ea0792 100644 --- a/packages/layout-engine/painters/dom/src/runs/links.ts +++ b/packages/layout-engine/painters/dom/src/runs/links.ts @@ -1,6 +1,9 @@ import type { FlowRunLink } from '@superdoc/contracts'; import { encodeTooltip, sanitizeHref } from '@superdoc/url-validation'; import type { LinkRenderData, RunRenderContext } from './types.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:runs'); const LINK_DATASET_KEYS = { blocked: 'linkBlocked', @@ -268,7 +271,7 @@ export const buildLinkRenderData = (link: FlowRunLink): LinkRenderData | null => // Defense-in-depth: Enforce maximum URL length even if sanitization was bypassed if (sanitized && sanitized.href.length > MAX_HREF_LENGTH) { - console.warn(`[DomPainter] Rejecting URL exceeding ${MAX_HREF_LENGTH} characters`); + log.warn(`[DomPainter] Rejecting URL exceeding ${MAX_HREF_LENGTH} characters`); linkMetrics.blocked++; return { blocked: true, dataset: { [LINK_DATASET_KEYS.blocked]: 'true' } }; } @@ -276,7 +279,7 @@ export const buildLinkRenderData = (link: FlowRunLink): LinkRenderData | null => if (!href) { if (typeof link.href === 'string' && link.href.trim()) { dataset[LINK_DATASET_KEYS.blocked] = 'true'; - console.warn(`[DomPainter] Blocked potentially unsafe URL: ${link.href.slice(0, 50)}`); + log.warn(`[DomPainter] Blocked potentially unsafe URL: ${link.href.slice(0, 50)}`); linkMetrics.blocked++; // Track invalid protocol if sanitized was null if (!sanitized) { @@ -353,7 +356,7 @@ export const applyTooltipAccessibility = ( } else { // Element not yet in DOM - accessibility feature will degrade gracefully // The title attribute will still provide tooltip functionality - console.warn('[DomPainter] Unable to add aria-describedby for tooltip (element not in DOM)'); + log.warn('[DomPainter] Unable to add aria-describedby for tooltip (element not in DOM)'); } return linkId; diff --git a/packages/layout-engine/painters/dom/src/svg-utils.ts b/packages/layout-engine/painters/dom/src/svg-utils.ts index 936a3627ca..210e30e444 100644 --- a/packages/layout-engine/painters/dom/src/svg-utils.ts +++ b/packages/layout-engine/painters/dom/src/svg-utils.ts @@ -4,6 +4,9 @@ */ import type { GradientFill, GradientStop, SolidFillWithAlpha, ShapeTextContent, TextPart } from '@superdoc/contracts'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:svg-utils'); /** * Validates and sanitizes a hex color string to prevent XSS attacks. @@ -283,7 +286,7 @@ export function applyGradientToSVG(svg: SVGElement, gradientData: GradientFill): }); } catch (error) { // Gracefully handle DOM manipulation errors - console.error('Failed to apply gradient to SVG:', error); + log.error('Failed to apply gradient to SVG:', error); } } @@ -325,7 +328,7 @@ export function applyAlphaToSVG(svg: SVGElement, alphaData: SolidFillWithAlpha): }); } catch (error) { // Gracefully handle DOM manipulation errors - console.error('Failed to apply alpha to SVG:', error); + log.error('Failed to apply alpha to SVG:', error); } } diff --git a/packages/layout-engine/painters/dom/src/table/border-utils.ts b/packages/layout-engine/painters/dom/src/table/border-utils.ts index 6dee312c35..9e0a67189d 100644 --- a/packages/layout-engine/painters/dom/src/table/border-utils.ts +++ b/packages/layout-engine/painters/dom/src/table/border-utils.ts @@ -7,6 +7,9 @@ import type { TableFragment, } from '@superdoc/contracts'; import { getTableCellGridBounds, type TableCellGridPosition } from './grid-geometry.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:table'); const ALLOWED_BORDER_STYLES = new Set([ 'none', @@ -27,7 +30,7 @@ const borderStyleToCSS = (style?: BorderStyle): string => { // SECURITY: Validate style is in allowed set if (!ALLOWED_BORDER_STYLES.has(style)) { - console.warn(`Invalid border style: ${style}, using 'solid' fallback`); + log.warn(`Invalid border style: ${style}, using 'solid' fallback`); return 'solid'; } diff --git a/packages/layout-engine/painters/dom/src/table/renderTableFragment.test.ts b/packages/layout-engine/painters/dom/src/table/renderTableFragment.test.ts index b77aa3edd4..d49f510b74 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableFragment.test.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableFragment.test.ts @@ -903,7 +903,7 @@ describe('renderTableFragment', () => { expect(element.textContent).toContain('Document not available'); // Verify error was logged - expect(consoleErrorSpy).toHaveBeenCalledWith('DomPainter: document is not available'); + expect(consoleErrorSpy).toHaveBeenCalledWith('[painter-dom:table]', 'DomPainter: document is not available'); consoleErrorSpy.mockRestore(); }); diff --git a/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts b/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts index 4a0faf4db7..7f7844c000 100644 --- a/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts +++ b/packages/layout-engine/painters/dom/src/table/renderTableFragment.ts @@ -23,6 +23,9 @@ import { } from '../sdt/container.js'; import { applyBorder, borderValueToSpec, hasExplicitCellBorders } from './border-utils.js'; import { getTableCellGridBounds } from './grid-geometry.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('painter-dom:table'); type ApplyStylesFn = (el: HTMLElement, styles: Partial) => void; /** @@ -179,7 +182,7 @@ export const renderTableFragment = (deps: TableRenderDependencies): HTMLElement // Check document first before using it in error handlers if (!doc) { - console.error('DomPainter: document is not available'); + log.error('DomPainter: document is not available'); // Use global document as fallback for error placeholder when available if (typeof document !== 'undefined') { diff --git a/packages/super-editor/src/editors/v1/components/ImageResizeOverlay.vue b/packages/super-editor/src/editors/v1/components/ImageResizeOverlay.vue index c77d97d44f..b020920669 100644 --- a/packages/super-editor/src/editors/v1/components/ImageResizeOverlay.vue +++ b/packages/super-editor/src/editors/v1/components/ImageResizeOverlay.vue @@ -23,6 +23,9 @@ import { ref, computed, watch, onBeforeUnmount } from 'vue'; import { measureCache } from '@superdoc/layout-bridge'; import { isContentLockedMode } from '../extensions/structured-content/lockModes.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('image-resize-overlay'); // Configuration constants const OVERLAY_EXPANSION_PX = 2000; @@ -344,7 +347,7 @@ function parseImageMetadata() { ]; for (const field of required) { if (!Number.isFinite(parsed[field]) || parsed[field] <= 0) { - console.warn(`[ImageResizeOverlay] Invalid or missing metadata field: ${field}`); + log.warn(`[ImageResizeOverlay] Invalid or missing metadata field: ${field}`); imageMetadata.value = null; return; } diff --git a/packages/super-editor/src/editors/v1/components/SuperEditor.test.js b/packages/super-editor/src/editors/v1/components/SuperEditor.test.js index 243aac2ea2..bc4c0f9a8b 100644 --- a/packages/super-editor/src/editors/v1/components/SuperEditor.test.js +++ b/packages/super-editor/src/editors/v1/components/SuperEditor.test.js @@ -520,7 +520,10 @@ describe('SuperEditor.vue', () => { await flushPromises(); expect(onException).toHaveBeenCalledWith({ error, editor: null }); - expect(consoleWarnSpy).toHaveBeenCalledWith('Unable to load the file. Please verify the .docx is valid.'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[SuperDoc]', + 'Unable to load the file. Please verify the .docx is valid.', + ); expect(getFileObjectMock).toHaveBeenCalledWith('blank-docx-url', 'blank.docx', DOCX_MIME); expect(EditorConstructor.loadXmlData).toHaveBeenCalledTimes(2); expect(EditorConstructor).toHaveBeenCalledTimes(1); diff --git a/packages/super-editor/src/editors/v1/components/SuperEditor.vue b/packages/super-editor/src/editors/v1/components/SuperEditor.vue index 84c3827a6a..c1ef1cc650 100644 --- a/packages/super-editor/src/editors/v1/components/SuperEditor.vue +++ b/packages/super-editor/src/editors/v1/components/SuperEditor.vue @@ -21,6 +21,9 @@ import BlankDOCX from '@superdoc/common/data/blank.docx?url'; import { isHeadless } from '@utils/headless-helpers.js'; import { isMacOS } from '@core/utilities/isMacOS.js'; import { DOM_CLASS_NAMES, buildImagePmSelector, buildInlineImagePmSelector } from '@superdoc/dom-contract'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('SuperDoc'); const emit = defineEmits(['editor-ready', 'editor-click', 'editor-keydown', 'comments-loaded', 'selection-update']); const DOCX = 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'; @@ -418,7 +421,7 @@ const getEditorZoom = () => { return active.presentationEditor.zoom; } // Fallback to default zoom when editor instance doesn't have zoom configured - console.warn( + log.warn( '[SuperEditor] getEditorZoom: Unable to retrieve zoom from editor instance, using fallback value of 1. ' + 'This may indicate the editor is not fully initialized or is not a PresentationEditor instance.', ); @@ -441,13 +444,13 @@ let lastUpdateTableResizeTimestamp = 0; const isNearColumnBoundary = (event, tableElement) => { // Input validation: event must have clientX and clientY properties if (!event || typeof event.clientX !== 'number' || typeof event.clientY !== 'number') { - console.warn('[isNearColumnBoundary] Invalid event: missing clientX or clientY', event); + log.warn('[isNearColumnBoundary] Invalid event: missing clientX or clientY', event); return false; } // Input validation: tableElement must be a valid DOM element if (!tableElement || !(tableElement instanceof HTMLElement)) { - console.warn('[isNearColumnBoundary] Invalid tableElement: not an HTMLElement', tableElement); + log.warn('[isNearColumnBoundary] Invalid tableElement: not an HTMLElement', tableElement); return false; } @@ -480,15 +483,15 @@ const isNearColumnBoundary = (event, tableElement) => { // Validate column data structure before using col.x and col.w if (!col || typeof col !== 'object') { - console.warn(`[isNearColumnBoundary] Invalid column at index ${i}: not an object`, col); + log.warn(`[isNearColumnBoundary] Invalid column at index ${i}: not an object`, col); continue; } if (typeof col.x !== 'number' || !Number.isFinite(col.x)) { - console.warn(`[isNearColumnBoundary] Invalid column.x at index ${i}:`, col.x); + log.warn(`[isNearColumnBoundary] Invalid column.x at index ${i}:`, col.x); continue; } if (typeof col.w !== 'number' || !Number.isFinite(col.w) || col.w <= 0) { - console.warn(`[isNearColumnBoundary] Invalid column.w at index ${i}:`, col.w); + log.warn(`[isNearColumnBoundary] Invalid column.w at index ${i}:`, col.w); continue; } @@ -536,7 +539,7 @@ const isNearColumnBoundary = (event, tableElement) => { return false; } catch (e) { // Log parsing errors for debugging while falling back to safe default - console.warn('[isNearColumnBoundary] Failed to parse table boundary metadata:', e); + log.warn('[isNearColumnBoundary] Failed to parse table boundary metadata:', e); return false; } }; @@ -855,11 +858,11 @@ const loadNewFileData = async () => { } // Not handled — return undefined so initializeData falls back to a blank // document instead of leaving the component in an unusable empty state. - console.debug('[SuperDoc] Error loading file:', err); + log.debug('Error loading file:', err); return; } - console.debug('[SuperDoc] Error loading file:', err); + log.debug('Error loading file:', err); if (typeof props.options.onException === 'function') { props.options.onException({ error: err, editor: null }); } @@ -890,7 +893,7 @@ const waitForCollaborativeFragmentSettling = async (ydoc, maxWaitMs = 200) => { }; const notifyFileLoadError = () => { - console.warn(FILE_LOAD_ERROR_MESSAGE); + log.warn(FILE_LOAD_ERROR_MESSAGE); }; const initializeData = async () => { diff --git a/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue b/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue index 9b89b407a0..6c46cad094 100644 --- a/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue +++ b/packages/super-editor/src/editors/v1/components/TableResizeOverlay.vue @@ -55,6 +55,9 @@ import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'; import { pixelsToTwips, twipsToPixels } from '@core/super-converter/helpers.js'; import { measureCache } from '@superdoc/layout-bridge'; import { buildWidthAuthoringTableAttrs } from '../document-api-adapters/helpers/table-attr-sync.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('table-resize-overlay'); /** * Props for the TableResizeOverlay component @@ -152,7 +155,7 @@ const getZoom = () => { return editor.presentationEditor.zoom; } // Fallback to default zoom when editor instance doesn't have zoom configured - console.warn( + log.warn( '[TableResizeOverlay] getZoom: Unable to retrieve zoom from editor instance, using fallback value of 1. ' + 'This may indicate the editor is not fully initialized or is not a PresentationEditor instance. ' + 'Table resize handles may be misaligned.', diff --git a/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue b/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue index c7b5daef28..d34dbb0a7f 100644 --- a/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue +++ b/packages/super-editor/src/editors/v1/components/context-menu/ContextMenu.vue @@ -11,6 +11,9 @@ import { getItems } from './menuItems.js'; import { getEditorContext } from './utils.js'; import { CONTEXT_MENU_HANDLED_FLAG } from './event-flags.js'; import { isMacOS } from '../../core/utilities/isMacOS.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('context-menu'); const props = defineProps({ editor: { @@ -203,7 +206,7 @@ const renderCustomItem = async (itemId) => { element.hasCustomContent = true; } } catch (error) { - console.warn(`[ContextMenu] Error rendering custom item ${itemId}:`, error); + log.warn(`Error rendering custom item ${itemId}:`, error); // Fallback to default rendering const fallbackElement = defaultRender({ ...(currentContext.value || {}), currentItem: item }); element.innerHTML = ''; @@ -380,7 +383,7 @@ const handleRightClickCapture = (event) => { } catch (error) { // Prevent handler crashes from breaking the event flow // Log warning but don't throw to allow other handlers to run - console.warn('[ContextMenu] Error in capture phase context menu handler:', error); + log.warn('Error in capture phase context menu handler:', error); } }; @@ -451,7 +454,7 @@ const handleRightClick = async (event) => { }), ); } catch (error) { - console.error('[ContextMenu] Error opening context menu:', error); + log.error('Error opening context menu:', error); } }; @@ -615,7 +618,7 @@ onBeforeUnmount(() => { } props.editor.off('update', handleEditorUpdate); } catch (error) { - console.warn('[ContextMenu] Error during cleanup:', error); + log.warn('Error during cleanup:', error); } } }); diff --git a/packages/super-editor/src/editors/v1/components/context-menu/menuItems.js b/packages/super-editor/src/editors/v1/components/context-menu/menuItems.js index 89c9b1e50c..24a8e56ddb 100644 --- a/packages/super-editor/src/editors/v1/components/context-menu/menuItems.js +++ b/packages/super-editor/src/editors/v1/components/context-menu/menuItems.js @@ -8,6 +8,9 @@ import { resolveContextMenuCommandEditor } from './utils.js'; import { isTrackedChangeActionAllowed } from '@extensions/track-changes/permission-helpers.js'; import { readClipboardRaw } from '../../core/utilities/clipboardUtils.js'; import { handleClipboardPaste } from '../../core/InputRule.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('context-menu'); /** * Build a minimal clipboard event-like object so ProseMirror paste hooks @@ -67,7 +70,7 @@ const shouldShowItem = (item, context) => { try { return Boolean(item.showWhen(context)); } catch (error) { - console.warn('[ContextMenu] showWhen error for item', item.id, ':', error); + log.warn('showWhen error for item', item.id, ':', error); return false; } } @@ -166,7 +169,7 @@ export function getItems(context, customItems = [], includeDefaultItems = true) const { selectedText, editor } = context; if (editor?.options?.slashMenuConfig && !editor?.options?.contextMenuConfig) { - console.warn('[ContextMenu] editor.options.slashMenuConfig is deprecated. Use contextMenuConfig instead.'); + log.warn('editor.options.slashMenuConfig is deprecated. Use contextMenuConfig instead.'); } const menuConfig = editor?.options?.contextMenuConfig ?? editor?.options?.slashMenuConfig; if (arguments.length === 1 && menuConfig) { @@ -389,7 +392,7 @@ export function getItems(context, customItems = [], includeDefaultItems = true) mode: 'all', }); } catch (error) { - console.warn('[ContextMenu] toc.update failed:', error); + log.warn('toc.update failed:', error); } }, showWhen: (context) => context.trigger === TRIGGERS.click && !!context.tocAncestor?.sdBlockId, @@ -521,7 +524,7 @@ export function getItems(context, customItems = [], includeDefaultItems = true) try { allSections = menuConfig.menuProvider(enhancedContext, allSections) || allSections; } catch (error) { - console.warn('[ContextMenu] menuProvider error:', error); + log.warn('menuProvider error:', error); } } diff --git a/packages/super-editor/src/editors/v1/components/context-menu/tests/ContextMenu.test.js b/packages/super-editor/src/editors/v1/components/context-menu/tests/ContextMenu.test.js index 4581d1836f..f0ebb8706b 100644 --- a/packages/super-editor/src/editors/v1/components/context-menu/tests/ContextMenu.test.js +++ b/packages/super-editor/src/editors/v1/components/context-menu/tests/ContextMenu.test.js @@ -1032,7 +1032,8 @@ describe('ContextMenu.vue', () => { // Should not throw, error should be caught expect(() => captureHandler(event)).not.toThrow(); expect(warnSpy).toHaveBeenCalledWith( - '[ContextMenu] Error in capture phase context menu handler:', + '[context-menu]', + 'Error in capture phase context menu handler:', expect.any(Error), ); diff --git a/packages/super-editor/src/editors/v1/components/context-menu/tests/utils.test.js b/packages/super-editor/src/editors/v1/components/context-menu/tests/utils.test.js index 373880052e..973c4f6226 100644 --- a/packages/super-editor/src/editors/v1/components/context-menu/tests/utils.test.js +++ b/packages/super-editor/src/editors/v1/components/context-menu/tests/utils.test.js @@ -713,7 +713,8 @@ describe('utils.js', () => { expect(result).toEqual({ isCellSelection: true, tableSelectionKind: 'cells' }); expect(warnSpy).toHaveBeenCalledWith( - '[ContextMenu] Unable to resolve cell selection rectangle:', + '[context-menu]', + 'Unable to resolve cell selection rectangle:', expect.any(Error), ); warnSpy.mockRestore(); diff --git a/packages/super-editor/src/editors/v1/components/context-menu/utils.js b/packages/super-editor/src/editors/v1/components/context-menu/utils.js index c03c883f0d..c1a9fafbbf 100644 --- a/packages/super-editor/src/editors/v1/components/context-menu/utils.js +++ b/packages/super-editor/src/editors/v1/components/context-menu/utils.js @@ -13,6 +13,9 @@ import { isCellSelection } from '@extensions/table/tableHelpers/isCellSelection. import { hasExpandedSelection } from '@utils/selectionUtils.js'; import { DOM_CLASS_NAMES } from '@superdoc/dom-contract'; import { selectedRect } from 'prosemirror-tables'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('context-menu'); export const resolveContextMenuCommandEditor = (editor) => { return typeof editor?.getActiveEditor === 'function' ? editor.getActiveEditor() : editor; @@ -305,7 +308,7 @@ function computeCanUndo(editor, state) { return !!can.undo(); } } catch (error) { - console.warn('[ContextMenu] Unable to determine undo availability via editor.can():', error); + log.warn('Unable to determine undo availability via editor.can():', error); } } @@ -314,14 +317,14 @@ function computeCanUndo(editor, state) { const undoManager = yUndoPluginKey.getState(state)?.undoManager; return !!undoManager && undoManager.undoStack.length > 0; } catch (error) { - console.warn('[ContextMenu] Unable to determine undo availability via y-prosemirror:', error); + log.warn('Unable to determine undo availability via y-prosemirror:', error); } } try { return undoDepth(state) > 0; } catch (error) { - console.warn('[ContextMenu] Unable to determine undo availability via history plugin:', error); + log.warn('Unable to determine undo availability via history plugin:', error); return false; } } @@ -334,7 +337,7 @@ function computeCanRedo(editor, state) { return !!can.redo(); } } catch (error) { - console.warn('[ContextMenu] Unable to determine redo availability via editor.can():', error); + log.warn('Unable to determine redo availability via editor.can():', error); } } @@ -343,14 +346,14 @@ function computeCanRedo(editor, state) { const undoManager = yUndoPluginKey.getState(state)?.undoManager; return !!undoManager && undoManager.redoStack.length > 0; } catch (error) { - console.warn('[ContextMenu] Unable to determine redo availability via y-prosemirror:', error); + log.warn('Unable to determine redo availability via y-prosemirror:', error); } } try { return redoDepth(state) > 0; } catch (error) { - console.warn('[ContextMenu] Unable to determine redo availability via history plugin:', error); + log.warn('Unable to determine redo availability via history plugin:', error); return false; } } @@ -411,7 +414,7 @@ function getCellSelectionInfo(state) { tableSelectionKind = 'column'; } } catch (error) { - console.warn('[ContextMenu] Unable to resolve cell selection rectangle:', error); + log.warn('Unable to resolve cell selection rectangle:', error); } return { isCellSelection: true, tableSelectionKind }; @@ -451,7 +454,7 @@ function getStructureFromResolvedPos(state, pos) { isInSectionNode, }; } catch (error) { - console.warn('[ContextMenu] Unable to resolve position for structural context:', error); + log.warn('Unable to resolve position for structural context:', error); return null; } } diff --git a/packages/super-editor/src/editors/v1/components/link-click/LinkClickHandler.vue b/packages/super-editor/src/editors/v1/components/link-click/LinkClickHandler.vue index e29ecf4dbf..5fae1e8a93 100644 --- a/packages/super-editor/src/editors/v1/components/link-click/LinkClickHandler.vue +++ b/packages/super-editor/src/editors/v1/components/link-click/LinkClickHandler.vue @@ -4,6 +4,9 @@ import { TextSelection } from 'prosemirror-state'; import { getEditorSurfaceElement } from '../../core/helpers/editorSurface.js'; import { moveCursorToMouseEvent, selectionHasNodeOrMark } from '../cursor-helpers.js'; import LinkInput from '../toolbar/LinkInput.vue'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('link-click'); const props = defineProps({ editor: { @@ -384,7 +387,7 @@ const handleLinkClick = (event) => { const tr = state.tr.setSelection(TextSelection.create(doc, pos)); props.editor.dispatch(tr); } else { - console.warn(`Invalid PM position from data-pm-start: ${pmStart}, falling back to coordinate-based positioning`); + log.warn(`Invalid PM position from data-pm-start: ${pmStart}, falling back to coordinate-based positioning`); moveCursorToMouseEvent(detail, props.editor); } } else { diff --git a/packages/super-editor/src/editors/v1/components/toolbar/AIWriter.vue b/packages/super-editor/src/editors/v1/components/toolbar/AIWriter.vue index d418032dd0..89123fcdfe 100644 --- a/packages/super-editor/src/editors/v1/components/toolbar/AIWriter.vue +++ b/packages/super-editor/src/editors/v1/components/toolbar/AIWriter.vue @@ -4,6 +4,9 @@ import { writeStreaming, rewriteStreaming, formatDocument } from './ai-helpers'; import { TextSelection } from 'prosemirror-state'; import edit from '@superdoc/common/icons/edit-regular.svg?raw'; import paperPlane from '@superdoc/common/icons/paper-plane-regular.svg?raw'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('ai-writer'); const props = defineProps({ selectedText: { @@ -136,7 +139,7 @@ const getDocumentXml = () => { // This is a placeholder, implement according to your editor's capability return props.editor.state.doc.textContent || ''; } catch (error) { - console.error('Error getting document XML:', error); + log.error('Error getting document XML:', error); return ''; } }; @@ -166,7 +169,7 @@ const handleTextChunk = async (text) => { // Dispatch the transaction to update the editor state props.editor.view.dispatch(tr); } else { - console.warn('[AIWriter] No stored selection to restore'); + log.warn('[AIWriter] No stored selection to restore'); } // Now delete the selection @@ -211,7 +214,7 @@ const handleTextChunk = async (text) => { // Hide the AI Writer after content is received props.handleClose(); } catch (error) { - console.error('Error handling text chunk:', error); + log.error('Error handling text chunk:', error); } }; @@ -325,7 +328,7 @@ const handleSubmit = async () => { await writeStreaming(promptText.value, options, handleTextChunk, handleDone); } } catch (error) { - console.error('AI generation error:', error); + log.error('AI generation error:', error); isError.value = error.message || 'An error occurred'; } finally { // Clear the input after submission diff --git a/packages/super-editor/src/editors/v1/components/toolbar/ai-helpers.js b/packages/super-editor/src/editors/v1/components/toolbar/ai-helpers.js index 36b2723bc6..d8cc2b43f5 100644 --- a/packages/super-editor/src/editors/v1/components/toolbar/ai-helpers.js +++ b/packages/super-editor/src/editors/v1/components/toolbar/ai-helpers.js @@ -16,6 +16,10 @@ * ``` */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('ai'); + // Default API endpoint if none is provided in config // Default is the SuperDoc gateway (passthrough to Harbour API) const DEFAULT_API_ENDPOINT = 'https://sd-dev-express-gateway-i6xtm.ondigitalocean.app/insights'; @@ -58,7 +62,7 @@ async function baseInsightsFetch(payload, options = {}) { return response; } catch (error) { - console.error('Error calling Harbour API:', error); + log.error('Error calling Harbour API:', error); throw error; } } @@ -102,7 +106,7 @@ async function processStream(stream, onChunk, onDone) { return result || ''; } catch (error) { - console.error('Error reading stream:', error); + log.error('Error reading stream:', error); throw error; } finally { reader.releaseLock(); @@ -465,11 +469,11 @@ export function formatDocument(editor) { } } } catch (error) { - console.error('Error processing match:', error); + log.error('Error processing match:', error); } } }); } catch (error) { - console.error('Error formatting document:', error); + log.error('Error formatting document:', error); } } diff --git a/packages/super-editor/src/editors/v1/components/toolbar/super-toolbar.js b/packages/super-editor/src/editors/v1/components/toolbar/super-toolbar.js index 0478506e6e..e314d983aa 100644 --- a/packages/super-editor/src/editors/v1/components/toolbar/super-toolbar.js +++ b/packages/super-editor/src/editors/v1/components/toolbar/super-toolbar.js @@ -22,6 +22,9 @@ import { parseSizeUnit } from '@core/utilities'; import { findElementBySelector, getParagraphFontFamilyFromProperties } from './helpers/general.js'; import { markerTextToBulletStyle } from '@helpers/list-numbering-helpers.js'; import { insertTableOfContentsAtSelection } from '@extensions/table-of-contents/table-of-contents-insertion.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-toolbar'); /** * @typedef {function(CommandItem): void} CommandCallback @@ -964,7 +967,7 @@ export class SuperToolbar extends EventEmitter { } catch (error) { const err = new Error(`[super-toolbar 🎨] Failed to execute pending command: ${command}`); this.emit('exception', { error: err, editor: this.activeEditor, originalError: error }); - console.error(err, error); + log.error(err, error); } }); diff --git a/packages/super-editor/src/editors/v1/core/CommandService.js b/packages/super-editor/src/editors/v1/core/CommandService.js index a7be3fa113..8055054725 100644 --- a/packages/super-editor/src/editors/v1/core/CommandService.js +++ b/packages/super-editor/src/editors/v1/core/CommandService.js @@ -1,5 +1,8 @@ //@ts-check import { chainableEditorState } from './helpers/chainableEditorState.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('command-service'); /** * @typedef {import('prosemirror-state').Transaction} Transaction @@ -243,7 +246,7 @@ export class CommandService { */ #dispatchWithFallback(tr, { editor, view }) { if (editor?.isDestroyed) { - console.warn('[CommandService] Cannot dispatch: editor is destroyed'); + log.warn('[CommandService] Cannot dispatch: editor is destroyed'); return false; } @@ -253,7 +256,7 @@ export class CommandService { } else if (typeof editor?.dispatch === 'function') { editor.dispatch(tr); } else { - console.warn('[CommandService] No dispatch method available (editor may not be initialized)'); + log.warn('[CommandService] No dispatch method available (editor may not be initialized)'); return false; } return true; diff --git a/packages/super-editor/src/editors/v1/core/CommandService.test.js b/packages/super-editor/src/editors/v1/core/CommandService.test.js index 8c84d8efaf..20410d0f74 100644 --- a/packages/super-editor/src/editors/v1/core/CommandService.test.js +++ b/packages/super-editor/src/editors/v1/core/CommandService.test.js @@ -219,7 +219,10 @@ describe('CommandService', () => { expect(result).toBe(false); expect(view.dispatch).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith('[CommandService] Cannot dispatch: editor is destroyed'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[command-service]', + '[CommandService] Cannot dispatch: editor is destroyed', + ); }); it('skips chain dispatch when editor.isDestroyed is true', () => { @@ -239,7 +242,10 @@ describe('CommandService', () => { expect(result).toBe(false); expect(view.dispatch).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith('[CommandService] Cannot dispatch: editor is destroyed'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[command-service]', + '[CommandService] Cannot dispatch: editor is destroyed', + ); }); it('returns false when editor is destroyed with preventDispatch', () => { @@ -287,7 +293,10 @@ describe('CommandService', () => { expect(result).toBe(false); expect(view.dispatch).not.toHaveBeenCalled(); - expect(consoleWarnSpy).toHaveBeenCalledWith('[CommandService] Cannot dispatch: editor is destroyed'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[command-service]', + '[CommandService] Cannot dispatch: editor is destroyed', + ); }); }); @@ -318,6 +327,7 @@ describe('CommandService', () => { expect(result).toBe(false); expect(consoleWarnSpy).toHaveBeenCalledWith( + '[command-service]', '[CommandService] No dispatch method available (editor may not be initialized)', ); }); @@ -340,6 +350,7 @@ describe('CommandService', () => { expect(result).toBe(false); expect(consoleWarnSpy).toHaveBeenCalledWith( + '[command-service]', '[CommandService] No dispatch method available (editor may not be initialized)', ); }); diff --git a/packages/super-editor/src/editors/v1/core/DocxZipper.js b/packages/super-editor/src/editors/v1/core/DocxZipper.js index 56794df963..7839135e59 100644 --- a/packages/super-editor/src/editors/v1/core/DocxZipper.js +++ b/packages/super-editor/src/editors/v1/core/DocxZipper.js @@ -6,6 +6,9 @@ import { DOCX } from '@superdoc/common'; import { COMMENT_FILE_BASENAMES } from './super-converter/constants.js'; import { syncPackageMetadata } from './opc/sync-package-metadata.js'; import { reconcileDocumentRelationships, MANAGED_DOCUMENT_PARTS } from './opc/reconcile-document-relationships.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('docx-zipper'); /** Image file extensions recognized during import and export. */ const IMAGE_EXTS = new Set(['png', 'jpg', 'jpeg', 'gif', 'bmp', 'tiff', 'tif', 'emf', 'wmf', 'svg', 'webp']); @@ -411,7 +414,7 @@ class DocxZipper { partNames.add(partName); }); } catch (error) { - console.warn('Failed to parse document relationships while updating content types', error); + log.warn('Failed to parse document relationships while updating content types', error); } } diff --git a/packages/super-editor/src/editors/v1/core/Editor.ts b/packages/super-editor/src/editors/v1/core/Editor.ts index 6f06e6dd1f..67a8ebd5b4 100644 --- a/packages/super-editor/src/editors/v1/core/Editor.ts +++ b/packages/super-editor/src/editors/v1/core/Editor.ts @@ -100,6 +100,9 @@ import { getViewModeSelectionWithoutStructuredContent } from './helpers/getViewM import { resolveMainBodyEditor } from '../document-api-adapters/helpers/word-statistics.js'; import { commitLiveStorySessionRuntimes } from '../document-api-adapters/story-runtime/live-story-session-runtime-registry.js'; import { buildFilteredMetadataXml } from '../document-api-adapters/plan-engine/anchored-metadata-wrappers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('editor'); type TrackChangesRuntimeConfig = NonNullable; @@ -878,7 +881,7 @@ export class Editor extends EventEmitter { text: () => this.#initRichText(), html: () => this.#initRichText(), default: () => { - console.log('Not implemented.'); + log.debug('Not implemented.'); }, }; @@ -955,7 +958,7 @@ export class Editor extends EventEmitter { licenseKey: resolvedLicenseKey, metadata: telemetryConfig.metadata, }); - console.debug('[super-editor] Telemetry: enabled'); + log.debug('[super-editor] Telemetry: enabled'); } catch { // Fail silently - telemetry should never break the app } @@ -1198,7 +1201,7 @@ export class Editor extends EventEmitter { // Browser: fetch the file const response = await fetch(source); if (!response.ok) { - console.debug('[SuperDoc] Fetch failed:', response.status, response.statusText); + log.debug('[SuperDoc] Fetch failed:', response.status, response.statusText); throw new Error(`Fetch failed: ${response.status} ${response.statusText}`); } const blob = await response.blob(); @@ -1361,11 +1364,11 @@ export class Editor extends EventEmitter { // Encryption errors are structured and recoverable — surface them directly // so consumers can inspect error.code (PASSWORD_REQUIRED, PASSWORD_INVALID, etc.) if (error instanceof DocxEncryptionError) { - console.debug('[SuperDoc] Document load error:', error.message); + log.debug('[SuperDoc] Document load error:', error.message); throw error; } const err = error instanceof Error ? error : new Error(String(error)); - console.debug('[SuperDoc] Document load error:', err.message); + log.debug('[SuperDoc] Document load error:', err.message); throw new DocumentLoadError(`Failed to load document: ${err.message}`, err); } } @@ -1672,7 +1675,7 @@ export class Editor extends EventEmitter { // Deprecation warnings for legacy mock options if (options.mockDocument) { - console.warn( + log.warn( '[super-editor] `mockDocument` is deprecated and will be removed in a future version. ' + 'Use `document` instead (e.g., `new Editor({ document: jsdomDocument })`). ' + 'See https://docs.superdoc.dev/guide/headless for migration guidance.', @@ -1680,7 +1683,7 @@ export class Editor extends EventEmitter { (global as typeof globalThis).document = options.mockDocument; } if (options.mockWindow) { - console.warn( + log.warn( '[super-editor] `mockWindow` is deprecated and will be removed in a future version. ' + 'Prefer passing `document` only. Global window assignment is no longer required for headless mode.', ); @@ -2618,7 +2621,7 @@ export class Editor extends EventEmitter { this.emit('fonts-resolved', payload); } catch { - console.warn('[SuperDoc] Could not determine document fonts and unsupported fonts'); + log.warn('[SuperDoc] Could not determine document fonts and unsupported fonts'); } } @@ -2718,7 +2721,7 @@ export class Editor extends EventEmitter { * @deprecated use setDocumentVersion instead */ static updateDocumentVersion(doc: DocxFileEntry[], version: string): string { - console.warn('updateDocumentVersion is deprecated, use setDocumentVersion instead'); + log.warn('updateDocumentVersion is deprecated, use setDocumentVersion instead'); return Editor.setDocumentVersion(doc, version); } @@ -2845,7 +2848,7 @@ export class Editor extends EventEmitter { else doc = this.schema.topNodeType.createAndFill()!; } } catch (err) { - console.error(err); + log.error(err); const error = err instanceof Error ? err : new Error(String(err)); this.emit('contentError', { editor: this, error }); } @@ -3035,7 +3038,7 @@ export class Editor extends EventEmitter { // nothing valid to initialize. if (this.isDestroyed || !this.converter || !this.state) return; - console.debug('🔗 [super-editor] Collaboration ready'); + log.debug('🔗 [super-editor] Collaboration ready'); this.#validateDocumentInit(); @@ -3154,7 +3157,7 @@ export class Editor extends EventEmitter { if (forceTrackChanges) throw error; // just in case nextState = prevState.apply(transactionToApply); - console.log(error); + log.debug(error); } const selectionHasChanged = !prevState.selection.eq(nextState.selection); @@ -3208,7 +3211,7 @@ export class Editor extends EventEmitter { if (transaction.docChanged && this.converter) { if (!this.converter.documentGuid) { this.converter.promoteToGuid(); - console.debug('Document modified - assigned GUID:', this.converter.documentGuid); + log.debug('Document modified - assigned GUID:', this.converter.documentGuid); } this.converter.documentModified = true; } @@ -3366,7 +3369,7 @@ export class Editor extends EventEmitter { * @deprecated use getDocumentGuid instead */ getDocumentId(): string | null { - console.warn('getDocumentId is deprecated, use getDocumentGuid instead'); + log.warn('getDocumentId is deprecated, use getDocumentGuid instead'); return this.getDocumentGuid(); } @@ -4049,7 +4052,7 @@ export class Editor extends EventEmitter { } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.emit('exception', { error: err, editor: this }); - console.error(err); + log.error(err); } } @@ -4059,13 +4062,13 @@ export class Editor extends EventEmitter { #endCollaboration(): void { if (!this.options.ydoc) return; try { - console.debug('🔗 [super-editor] Ending collaboration'); + log.debug('🔗 [super-editor] Ending collaboration'); this.options.collaborationProvider?.disconnect?.(); (this.options.ydoc as { destroy: () => void }).destroy(); } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.emit('exception', { error: err, editor: this }); - console.error(err); + log.error(err); } } @@ -4434,7 +4437,7 @@ export class Editor extends EventEmitter { } catch (error) { const err = error instanceof Error ? error : new Error(String(error)); this.emit('exception', { error: err, editor: this }); - console.error(err); + log.error(err); } } @@ -4444,7 +4447,7 @@ export class Editor extends EventEmitter { static checkIfMigrationsNeeded(): boolean { const dataVersion = version ?? 'initial'; const migrations = getNecessaryMigrations(dataVersion) || []; - console.debug('[checkVersionMigrations] Migrations needed:', dataVersion, migrations.length); + log.debug('[checkVersionMigrations] Migrations needed:', dataVersion, migrations.length); return migrations.length > 0; } @@ -4452,13 +4455,13 @@ export class Editor extends EventEmitter { * Process collaboration migrations */ processCollaborationMigrations(): unknown | void { - console.debug('[checkVersionMigrations] Current editor version', CURRENT_APP_VERSION); + log.debug('[checkVersionMigrations] Current editor version', CURRENT_APP_VERSION); if (!this.options.ydoc) return; const metaMap = (this.options.ydoc as { getMap: (name: string) => Map }).getMap('meta'); let docVersion = metaMap.get('version'); if (!docVersion) docVersion = 'initial'; - console.debug('[checkVersionMigrations] Document version', docVersion); + log.debug('[checkVersionMigrations] Document version', docVersion); const migrations = getNecessaryMigrations(docVersion) || []; const plugins = this.state.plugins; @@ -4467,7 +4470,7 @@ export class Editor extends EventEmitter { let hasRunMigrations = false; for (const migration of migrations) { - console.debug('🏃‍♂️ Running migration', migration.name); + log.debug('🏃‍♂️ Running migration', migration.name); const result = migration(this); if (!result) throw new Error('Migration failed at ' + migration.name); else hasRunMigrations = true; @@ -4580,7 +4583,7 @@ export class Editor extends EventEmitter { */ getInternalXmlFile(name: string, type: 'json' | 'string' = 'json'): unknown | string | null { if (!this.converter.convertedXml[name]) { - console.warn('Cannot find file in docx'); + log.warn('Cannot find file in docx'); return null; } diff --git a/packages/super-editor/src/editors/v1/core/InputRule.js b/packages/super-editor/src/editors/v1/core/InputRule.js index 51c09fc51c..e750a98f76 100644 --- a/packages/super-editor/src/editors/v1/core/InputRule.js +++ b/packages/super-editor/src/editors/v1/core/InputRule.js @@ -33,6 +33,9 @@ import { applySuperdocClipboardMedia, } from './helpers/superdocClipboardSlice.js'; import { annotateFragmentDomWithClipboardData } from './helpers/clipboardFragmentAnnotate.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('input-rule'); /** Heuristic: clipboard HTML from SuperDoc copy (slice attrs, list/section metadata). */ export function isSuperdocOriginClipboardHtml(html) { @@ -126,7 +129,7 @@ const inputRuleMatcherHandler = (text, match) => { if (inputRuleMatch.replaceWith) { if (!inputRuleMatch.text.includes(inputRuleMatch.replaceWith)) { - console.warn('[super-editor warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".'); + log.warn('[super-editor warn]: "inputRuleMatch.replaceWith" must be part of "inputRuleMatch.text".'); } result.push(inputRuleMatch.replaceWith); @@ -331,7 +334,7 @@ export const inputRulesPlugin = ({ editor, rules }) => { try { if (handleSuperdocSlicePaste(superdocSliceData, editor, view, embeddedBodySectPr)) return true; } catch (err) { - console.warn('Failed to paste SuperDoc slice, falling back to HTML:', err); + log.warn('Failed to paste SuperDoc slice, falling back to HTML:', err); } } @@ -567,7 +570,7 @@ export function cleanHtmlUnnecessaryTags(html) { export function sanitizeHtml(html, forbiddenTags = ['meta', 'svg', 'script', 'style', 'button'], domDocument) { const resolvedDocument = domDocument ?? (typeof document !== 'undefined' ? document : null); if (!resolvedDocument) { - console.warn( + log.warn( '[super-editor] HTML sanitization requires a DOM. Provide { document } (e.g. from JSDOM), set DOM globals, or run in a browser environment. Skipping sanitization.', ); return null; @@ -669,7 +672,7 @@ export function handleClipboardPaste({ editor, view }, html, plainText) { try { if (handleSuperdocSlicePaste(superdocSliceData, editor, view, embeddedBodySectPr)) return true; } catch (err) { - console.warn('Failed to paste SuperDoc slice, falling back to HTML:', err); + log.warn('Failed to paste SuperDoc slice, falling back to HTML:', err); } } @@ -762,7 +765,7 @@ function handleCutEvent(view, event, editor) { view.dispatch(view.state.tr.deleteSelection().scrollIntoView()); return true; } catch (error) { - console.warn('Failed to handle cut:', error); + log.warn('Failed to handle cut:', error); return false; } } diff --git a/packages/super-editor/src/editors/v1/core/commands/insertContent.js b/packages/super-editor/src/editors/v1/core/commands/insertContent.js index 5ca4dc50c1..3227de46d4 100644 --- a/packages/super-editor/src/editors/v1/core/commands/insertContent.js +++ b/packages/super-editor/src/editors/v1/core/commands/insertContent.js @@ -1,5 +1,8 @@ //@ts-check import { processContent } from '../helpers/contentProcessor.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('insertContent'); /** * Command to insert content at the current selection or replace the current selection. @@ -23,7 +26,7 @@ export const insertContent = if (options.contentType) { const validTypes = ['html', 'markdown', 'text', 'schema']; if (!validTypes.includes(options.contentType)) { - console.error(`[insertContent] Invalid contentType: "${options.contentType}". Use: ${validTypes.join(', ')}`); + log.error(`Invalid contentType: "${options.contentType}". Use: ${validTypes.join(', ')}`); return false; } @@ -52,7 +55,7 @@ export const insertContent = return ok; } catch (error) { - console.error(`[insertContent] Failed to process ${options.contentType}:`, error); + log.error(`Failed to process ${options.contentType}:`, error); return false; } } diff --git a/packages/super-editor/src/editors/v1/core/commands/insertContent.test.js b/packages/super-editor/src/editors/v1/core/commands/insertContent.test.js index 660418c58e..03b7611f65 100644 --- a/packages/super-editor/src/editors/v1/core/commands/insertContent.test.js +++ b/packages/super-editor/src/editors/v1/core/commands/insertContent.test.js @@ -68,7 +68,7 @@ describe('insertContent', () => { const result = command({ tr: mockTr, state: mockState, commands: mockCommands, editor: mockEditor }); expect(result).toBe(false); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Invalid contentType')); + expect(consoleSpy).toHaveBeenCalledWith('[insertContent]', expect.stringContaining('Invalid contentType')); expect(mockCommands.insertContentAt).not.toHaveBeenCalled(); consoleSpy.mockRestore(); @@ -85,7 +85,11 @@ describe('insertContent', () => { const result = command({ tr: mockTr, state: mockState, commands: mockCommands, editor: mockEditor }); expect(result).toBe(false); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('Failed to process html'), expect.any(Error)); + expect(consoleSpy).toHaveBeenCalledWith( + '[insertContent]', + expect.stringContaining('Failed to process html'), + expect.any(Error), + ); consoleSpy.mockRestore(); }); diff --git a/packages/super-editor/src/editors/v1/core/commands/insertContentAt.js b/packages/super-editor/src/editors/v1/core/commands/insertContentAt.js index fecd1618b4..b0df1f0a79 100644 --- a/packages/super-editor/src/editors/v1/core/commands/insertContentAt.js +++ b/packages/super-editor/src/editors/v1/core/commands/insertContentAt.js @@ -1,5 +1,8 @@ import { createNodeFromContent } from '../helpers/createNodeFromContent'; import { selectionToInsertionEnd } from '../helpers/selectionToInsertionEnd'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('insertContentAt'); /** * @typedef {import("prosemirror-model").Node} ProseMirrorNode @@ -123,7 +126,7 @@ export const insertContentAt = editor, error: e, disableCollaboration: () => { - console.error('[super-editor error]: Unable to disable collaboration at this point in time'); + log.error('[super-editor error]: Unable to disable collaboration at this point in time'); }, }); return false; diff --git a/packages/super-editor/src/editors/v1/core/commands/insertSectionBreakAtSelection.js b/packages/super-editor/src/editors/v1/core/commands/insertSectionBreakAtSelection.js index 3d4a2d8ab7..9c2e580125 100644 --- a/packages/super-editor/src/editors/v1/core/commands/insertSectionBreakAtSelection.js +++ b/packages/super-editor/src/editors/v1/core/commands/insertSectionBreakAtSelection.js @@ -1,4 +1,7 @@ import { updateSectionMargins, getSectPrMargins } from '@converter/section-properties.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('insertSectionBreakAtSelection'); /** * Insert (or ensure) a paragraph-level sectPr at the selection. @@ -17,17 +20,17 @@ export const insertSectionBreakAtSelection = ({ headerInches, footerInches } = {}) => ({ tr, state, editor }) => { if (!state || !editor) { - console.warn('[insertSectionBreakAtSelection] Missing state or editor'); + log.warn('Missing state or editor'); return false; } // Validate margin values if provided if (typeof headerInches === 'number' && headerInches < 0) { - console.warn('[insertSectionBreakAtSelection] headerInches must be >= 0, got:', headerInches); + log.warn('headerInches must be >= 0, got:', headerInches); return false; } if (typeof footerInches === 'number' && footerInches < 0) { - console.warn('[insertSectionBreakAtSelection] footerInches must be >= 0, got:', footerInches); + log.warn('footerInches must be >= 0, got:', footerInches); return false; } @@ -45,7 +48,7 @@ export const insertSectionBreakAtSelection = } if (!paragraph || paraPos <= 0) { - console.warn('[insertSectionBreakAtSelection] No paragraph found at selection'); + log.warn('No paragraph found at selection'); return false; } diff --git a/packages/super-editor/src/editors/v1/core/commands/setBodyHeaderFooter.js b/packages/super-editor/src/editors/v1/core/commands/setBodyHeaderFooter.js index 3df4be8135..bf6e576e7e 100644 --- a/packages/super-editor/src/editors/v1/core/commands/setBodyHeaderFooter.js +++ b/packages/super-editor/src/editors/v1/core/commands/setBodyHeaderFooter.js @@ -1,3 +1,7 @@ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('setBodyHeaderFooter'); + /** * Update document default header/footer distances (inches) via editor.updatePageStyle. * Triggers pagination update. @@ -11,7 +15,7 @@ export const setBodyHeaderFooter = ({ headerInches, footerInches } = {}) => ({ editor }) => { if (!editor) { - console.warn('[setBodyHeaderFooter] No editor instance provided'); + log.warn('No editor instance provided'); return false; } @@ -19,22 +23,22 @@ export const setBodyHeaderFooter = const hasFooter = typeof footerInches === 'number'; if (!hasHeader && !hasFooter) { - console.warn('[setBodyHeaderFooter] No margin values provided'); + log.warn('No margin values provided'); return false; } // Validate positive values if (hasHeader && headerInches < 0) { - console.warn('[setBodyHeaderFooter] headerInches must be >= 0, got:', headerInches); + log.warn('headerInches must be >= 0, got:', headerInches); return false; } if (hasFooter && footerInches < 0) { - console.warn('[setBodyHeaderFooter] footerInches must be >= 0, got:', footerInches); + log.warn('footerInches must be >= 0, got:', footerInches); return false; } if (!editor.updatePageStyle) { - console.warn('[setBodyHeaderFooter] editor.updatePageStyle is not available'); + log.warn('editor.updatePageStyle is not available'); return false; } diff --git a/packages/super-editor/src/editors/v1/core/commands/setSectionHeaderFooterAtSelection.js b/packages/super-editor/src/editors/v1/core/commands/setSectionHeaderFooterAtSelection.js index 401f6933ce..df94af81fa 100644 --- a/packages/super-editor/src/editors/v1/core/commands/setSectionHeaderFooterAtSelection.js +++ b/packages/super-editor/src/editors/v1/core/commands/setSectionHeaderFooterAtSelection.js @@ -1,4 +1,7 @@ import { updateSectionMargins, getSectPrMargins } from '@converter/section-properties.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('setSectionHeaderFooterAtSelection'); /** * Find the nearest paragraph at or before a given document position that carries a sectPr. @@ -36,7 +39,7 @@ export const setSectionHeaderFooterAtSelection = ({ headerInches, footerInches } = {}) => ({ tr, state, editor }) => { if (!state || !editor) { - console.warn('[setSectionHeaderFooterAtSelection] Missing state or editor'); + log.warn('Missing state or editor'); return false; } @@ -44,17 +47,17 @@ export const setSectionHeaderFooterAtSelection = const hasFooter = typeof footerInches === 'number'; if (!hasHeader && !hasFooter) { - console.warn('[setSectionHeaderFooterAtSelection] No margin values provided'); + log.warn('No margin values provided'); return false; } // Validate positive values if (hasHeader && headerInches < 0) { - console.warn('[setSectionHeaderFooterAtSelection] headerInches must be >= 0, got:', headerInches); + log.warn('headerInches must be >= 0, got:', headerInches); return false; } if (hasFooter && footerInches < 0) { - console.warn('[setSectionHeaderFooterAtSelection] footerInches must be >= 0, got:', footerInches); + log.warn('footerInches must be >= 0, got:', footerInches); return false; } @@ -62,7 +65,7 @@ export const setSectionHeaderFooterAtSelection = const found = findNearestParagraphWithSectPr(state.doc, from); if (!found) { - console.warn('[setSectionHeaderFooterAtSelection] No section break found at or before selection'); + log.warn('No section break found at or before selection'); return false; } @@ -71,7 +74,7 @@ export const setSectionHeaderFooterAtSelection = const existingSectPr = paraProps?.sectPr || null; if (!existingSectPr) { - console.warn('[setSectionHeaderFooterAtSelection] Paragraph found but has no sectPr'); + log.warn('Paragraph found but has no sectPr'); return false; } @@ -85,7 +88,7 @@ export const setSectionHeaderFooterAtSelection = try { updateSectionMargins({ type: 'sectPr', sectPr }, updates); } catch (err) { - console.error('[setSectionHeaderFooterAtSelection] Failed to update sectPr:', err); + log.error('Failed to update sectPr:', err); return false; } diff --git a/packages/super-editor/src/editors/v1/core/commands/setSectionPageMarginsAtSelection.js b/packages/super-editor/src/editors/v1/core/commands/setSectionPageMarginsAtSelection.js index 3f9a962019..ec74483d0a 100644 --- a/packages/super-editor/src/editors/v1/core/commands/setSectionPageMarginsAtSelection.js +++ b/packages/super-editor/src/editors/v1/core/commands/setSectionPageMarginsAtSelection.js @@ -1,4 +1,7 @@ import { updateSectionMargins, getSectPrMargins } from '@converter/section-properties.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('setSectionPageMarginsAtSelection'); /** * Find the governing section break (paragraph with sectPr) for the current selection. @@ -35,7 +38,7 @@ export const setSectionPageMarginsAtSelection = ({ topInches, rightInches, bottomInches, leftInches } = {}) => ({ tr, state, editor }) => { if (!state || !editor) { - console.warn('[setSectionPageMarginsAtSelection] Missing state or editor'); + log.warn('Missing state or editor'); return false; } @@ -44,7 +47,7 @@ export const setSectionPageMarginsAtSelection = const hasBottom = typeof bottomInches === 'number'; const hasLeft = typeof leftInches === 'number'; if (!hasTop && !hasRight && !hasBottom && !hasLeft) { - console.warn('[setSectionPageMarginsAtSelection] No margin values provided'); + log.warn('No margin values provided'); return false; } if ( @@ -53,7 +56,7 @@ export const setSectionPageMarginsAtSelection = (hasBottom && bottomInches < 0) || (hasLeft && leftInches < 0) ) { - console.warn('[setSectionPageMarginsAtSelection] Margin values must be >= 0'); + log.warn('Margin values must be >= 0'); return false; } @@ -71,7 +74,7 @@ export const setSectionPageMarginsAtSelection = const paraProps = node.attrs?.paragraphProperties || null; const existingSectPr = paraProps?.sectPr || null; if (!existingSectPr) { - console.warn('[setSectionPageMarginsAtSelection] Paragraph found but has no sectPr'); + log.warn('Paragraph found but has no sectPr'); return false; } @@ -79,7 +82,7 @@ export const setSectionPageMarginsAtSelection = try { updateSectionMargins({ type: 'sectPr', sectPr }, updates); } catch (err) { - console.error('[setSectionPageMarginsAtSelection] Failed to update sectPr:', err); + log.error('Failed to update sectPr:', err); return false; } @@ -117,7 +120,7 @@ export const setSectionPageMarginsAtSelection = try { updateSectionMargins({ type: 'sectPr', sectPr }, updates); } catch (err) { - console.error('[setSectionPageMarginsAtSelection] Failed to update sectPr:', err); + log.error('Failed to update sectPr:', err); return false; } diff --git a/packages/super-editor/src/editors/v1/core/header-footer/EditorOverlayManager.ts b/packages/super-editor/src/editors/v1/core/header-footer/EditorOverlayManager.ts index 420b3b10bc..63516f5b5b 100644 --- a/packages/super-editor/src/editors/v1/core/header-footer/EditorOverlayManager.ts +++ b/packages/super-editor/src/editors/v1/core/header-footer/EditorOverlayManager.ts @@ -20,6 +20,9 @@ */ import type { HeaderFooterRegion } from './types.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('editor-overlay-manager'); export type { HeaderFooterRegion } from './types.js'; @@ -222,7 +225,7 @@ export class EditorOverlayManager { this.#hideHeaderFooterBorder(); const errorMessage = error instanceof Error ? error.message : String(error); - console.error('[EditorOverlayManager] Failed to show editing overlay:', error); + log.error('[EditorOverlayManager] Failed to show editing overlay:', error); return { success: false, @@ -415,7 +418,7 @@ export class EditorOverlayManager { const pageElement = editorHost.parentElement; if (!pageElement) { - console.error('[EditorOverlayManager] Editor host has no parent element'); + log.error('[EditorOverlayManager] Editor host has no parent element'); return; } diff --git a/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterPerRidLayout.ts b/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterPerRidLayout.ts index 1228456dc2..c4f7e9f4c8 100644 --- a/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterPerRidLayout.ts +++ b/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterPerRidLayout.ts @@ -10,6 +10,9 @@ import { } from '@superdoc/layout-bridge'; import type { HeaderFooterLayoutResult, HeaderFooterConstraints } from '@superdoc/layout-bridge'; import { measureBlock } from '@superdoc/measuring-dom'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('header-footer-per-rid-layout'); export type HeaderFooterPerRidLayoutInput = { headerBlocks?: unknown; @@ -138,7 +141,7 @@ async function layoutBlocksByRId( }); } } catch (error) { - console.warn(`[PresentationEditor] Failed to layout ${kind} rId=${rId}:`, error); + log.warn(`[PresentationEditor] Failed to layout ${kind} rId=${rId}:`, error); } } } @@ -268,7 +271,7 @@ async function layoutWithPerSectionConstraints( } } } catch (error) { - console.warn(`[PresentationEditor] Failed to layout ${kind} rId=${group.rId}:`, error); + log.warn(`[PresentationEditor] Failed to layout ${kind} rId=${group.rId}:`, error); } } } diff --git a/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistry.ts b/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistry.ts index edfb9ad782..462c23dd83 100644 --- a/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistry.ts +++ b/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistry.ts @@ -7,6 +7,9 @@ import { EventEmitter } from '@core/EventEmitter.js'; import { createHeaderFooterEditor, onHeaderFooterDataUpdate } from '@extensions/pagination/pagination-helpers.js'; import type { ConverterContext } from '@core/layout-adapter/converter-context.js'; import { buildStoryKey } from '../../document-api-adapters/story-runtime/story-key.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('header-footer-registry'); const HEADER_FOOTER_VARIANTS = ['default', 'first', 'even', 'odd'] as const; const DEFAULT_HEADER_FOOTER_HEIGHT = 100; @@ -284,7 +287,7 @@ export class HeaderFooterEditorManager extends EventEmitter { if (options) { // Validate editorHost type if (options.editorHost !== undefined && !(options.editorHost instanceof HTMLElement)) { - console.error('[HeaderFooterEditorManager] editorHost must be an HTMLElement'); + log.error('[HeaderFooterEditorManager] editorHost must be an HTMLElement'); this.emit('error', { descriptor, error: new TypeError('editorHost must be an HTMLElement'), @@ -299,7 +302,7 @@ export class HeaderFooterEditorManager extends EventEmitter { !Number.isFinite(options.availableWidth) || options.availableWidth <= 0 ) { - console.error('[HeaderFooterEditorManager] availableWidth must be a positive number'); + log.error('[HeaderFooterEditorManager] availableWidth must be a positive number'); this.emit('error', { descriptor, error: new TypeError('availableWidth must be a positive number'), @@ -314,7 +317,7 @@ export class HeaderFooterEditorManager extends EventEmitter { !Number.isFinite(options.availableHeight) || options.availableHeight <= 0 ) { - console.error('[HeaderFooterEditorManager] availableHeight must be a positive number'); + log.error('[HeaderFooterEditorManager] availableHeight must be a positive number'); this.emit('error', { descriptor, error: new TypeError('availableHeight must be a positive number'), @@ -329,7 +332,7 @@ export class HeaderFooterEditorManager extends EventEmitter { !Number.isInteger(options.currentPageNumber) || options.currentPageNumber < 1 ) { - console.error('[HeaderFooterEditorManager] currentPageNumber must be a positive integer'); + log.error('[HeaderFooterEditorManager] currentPageNumber must be a positive integer'); this.emit('error', { descriptor, error: new TypeError('currentPageNumber must be a positive integer'), @@ -344,7 +347,7 @@ export class HeaderFooterEditorManager extends EventEmitter { !Number.isInteger(options.totalPageCount) || options.totalPageCount < 1 ) { - console.error('[HeaderFooterEditorManager] totalPageCount must be a positive integer'); + log.error('[HeaderFooterEditorManager] totalPageCount must be a positive integer'); this.emit('error', { descriptor, error: new TypeError('totalPageCount must be a positive integer'), @@ -363,7 +366,7 @@ export class HeaderFooterEditorManager extends EventEmitter { this.#updateAccessOrder(descriptor.id); await existing.ready.catch((error) => { - console.error('[HeaderFooterEditorManager] Editor initialization failed:', error); + log.error('[HeaderFooterEditorManager] Editor initialization failed:', error); this.emit('error', { descriptor, error }); }); this.#mountAndUpdateEntry(existing, options); @@ -396,7 +399,7 @@ export class HeaderFooterEditorManager extends EventEmitter { this.#enforceCacheSizeLimit(); await entry.ready.catch((error) => { - console.error('[HeaderFooterEditorManager] Editor initialization failed:', error); + log.error('[HeaderFooterEditorManager] Editor initialization failed:', error); this.emit('error', { descriptor, error }); }); return entry.editor; @@ -694,7 +697,7 @@ export class HeaderFooterEditorManager extends EventEmitter { try { entry.disposer(); } catch (error) { - console.warn('[HeaderFooterEditorManager] Cleanup failed for editor:', key, error); + log.warn('[HeaderFooterEditorManager] Cleanup failed for editor:', key, error); } toRemove.push({ key, descriptor: entry.descriptor }); } @@ -713,7 +716,7 @@ export class HeaderFooterEditorManager extends EventEmitter { try { entry.disposer(); } catch (error) { - console.warn('[HeaderFooterEditorManager] Cleanup failed:', error); + log.warn('[HeaderFooterEditorManager] Cleanup failed:', error); } }); this.#editorEntries.clear(); @@ -753,7 +756,7 @@ export class HeaderFooterEditorManager extends EventEmitter { totalPageCount: options?.totalPageCount ?? 1, }) as Editor; } catch (error) { - console.error('[HeaderFooterEditorManager] Editor creation failed:', error); + log.error('[HeaderFooterEditorManager] Editor creation failed:', error); return null; } @@ -765,7 +768,7 @@ export class HeaderFooterEditorManager extends EventEmitter { // and the parts publisher propagates to Yjs automatically. onHeaderFooterDataUpdate({ editor, transaction }, this.#editor, descriptor.id, descriptor.kind); } catch (error) { - console.error('[HeaderFooterEditorManager] Failed to sync header/footer update', { descriptor, error }); + log.error('[HeaderFooterEditorManager] Failed to sync header/footer update', { descriptor, error }); // Emit error event so consumers can handle sync failures // This prevents silent failures and allows for retry logic or user notification this.emit('syncError', { descriptor, error }); @@ -782,14 +785,14 @@ export class HeaderFooterEditorManager extends EventEmitter { // Remove event listener to prevent memory leaks from accumulating handlers editor.off?.('update', handleUpdate); } catch (error) { - console.warn('[HeaderFooterEditorManager] Failed to remove update listener:', error); + log.warn('[HeaderFooterEditorManager] Failed to remove update listener:', error); } try { // Destroy the editor instance to free up resources editor.destroy?.(); } catch (error) { - console.warn('[HeaderFooterEditorManager] Failed to destroy editor:', error); + log.warn('[HeaderFooterEditorManager] Failed to destroy editor:', error); } try { @@ -798,14 +801,14 @@ export class HeaderFooterEditorManager extends EventEmitter { container.parentNode.removeChild(container); } } catch (error) { - console.warn('[HeaderFooterEditorManager] Failed to remove container from DOM:', error); + log.warn('[HeaderFooterEditorManager] Failed to remove container from DOM:', error); } try { // Unregister from converter to maintain consistency this.#unregisterConverterEditor(descriptor); } catch (error) { - console.warn('[HeaderFooterEditorManager] Failed to unregister converter editor:', error); + log.warn('[HeaderFooterEditorManager] Failed to unregister converter editor:', error); } }; @@ -1024,7 +1027,7 @@ export class HeaderFooterEditorManager extends EventEmitter { oldEntry.disposer(); this.#evictions += 1; } catch (error) { - console.warn('[HeaderFooterEditorManager] LRU eviction cleanup failed:', error); + log.warn('[HeaderFooterEditorManager] LRU eviction cleanup failed:', error); } this.#editorEntries.delete(id); this.emit('editorDisposed', { descriptor: oldEntry.descriptor } as EditorDisposedPayload); diff --git a/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistryInit.ts b/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistryInit.ts index 55b04b0920..eeb0991189 100644 --- a/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistryInit.ts +++ b/packages/super-editor/src/editors/v1/core/header-footer/HeaderFooterRegistryInit.ts @@ -6,6 +6,9 @@ import { HeaderFooterLayoutAdapter, type HeaderFooterDescriptor, } from './HeaderFooterRegistry.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('header-footer-registry-init'); export type InitHeaderFooterRegistryDeps = { editor: Editor; @@ -45,7 +48,7 @@ export function initHeaderFooterRegistry({ try { fn(); } catch (error) { - console.warn('[PresentationEditor] Header/footer cleanup failed:', error); + log.warn('[PresentationEditor] Header/footer cleanup failed:', error); } }); previousAdapter?.clear(); @@ -73,7 +76,7 @@ export function initHeaderFooterRegistry({ const duration = performance.now() - startTime; if (isDebug && duration > initBudgetMs) { - console.warn( + log.warn( `[PresentationEditor] Header/footer initialization took ${duration.toFixed(2)}ms (budget: ${initBudgetMs}ms)`, ); // TODO: Consider showing loading spinner if bootstrap exceeds budget in production diff --git a/packages/super-editor/src/editors/v1/core/helpers/annotator.js b/packages/super-editor/src/editors/v1/core/helpers/annotator.js index aa9449006c..3cc94071ca 100644 --- a/packages/super-editor/src/editors/v1/core/helpers/annotator.js +++ b/packages/super-editor/src/editors/v1/core/helpers/annotator.js @@ -1,4 +1,7 @@ import { Fragment } from 'prosemirror-model'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('annotator'); /** * Get the field attributes based on the field type and value @@ -75,7 +78,7 @@ export const processTables = ({ state, tr, annotationValues }) => { try { generateTableIfNecessary({ tableNode: { node: currentTableNode, pos }, annotationValues, tr, state }); } catch (error) { - console.error('Error generating table at pos', pos, ':', error); + log.error('Error generating table at pos', pos, ':', error); // Continue processing other tables even if one fails } }); @@ -187,7 +190,7 @@ const generateTableIfNecessary = ({ tableNode, annotationValues, tr, state }) => const rawExtraAttrs = getFieldAttrs(inlineNode, value, null); extraAttrs = validateAttributes(rawExtraAttrs || {}); } catch (error) { - console.error('Error getting field attrs:', error); + log.error('Error getting field attrs:', error); extraAttrs = {}; } @@ -203,7 +206,7 @@ const generateTableIfNecessary = ({ tableNode, annotationValues, tr, state }) => try { return FieldType.create(newAttrs, inlineNode.content || Fragment.empty, inlineNode.marks || []); } catch (error) { - console.error('Error creating field node:', error); + log.error('Error creating field node:', error); // Fallback: minimal attributes try { @@ -218,7 +221,7 @@ const generateTableIfNecessary = ({ tableNode, annotationValues, tr, state }) => inlineNode.marks || [], ); } catch (fallbackError) { - console.error('Fallback also failed:', fallbackError); + log.error('Fallback also failed:', fallbackError); return inlineNode; // Return original node as last resort } } @@ -232,7 +235,7 @@ const generateTableIfNecessary = ({ tableNode, annotationValues, tr, state }) => blockNode.marks || [], ); } catch (error) { - console.error('Error creating paragraph node:', error); + log.error('Error creating paragraph node:', error); return blockNode; } }); @@ -244,7 +247,7 @@ const generateTableIfNecessary = ({ tableNode, annotationValues, tr, state }) => cellNode.marks || [], ); } catch (error) { - console.error(`Failed to rebuild cell for row ${rowIndex}:`, error); + log.error(`Failed to rebuild cell for row ${rowIndex}:`, error); throw error; } }; @@ -269,7 +272,7 @@ const generateTableIfNecessary = ({ tableNode, annotationValues, tr, state }) => tr.replaceWith(mappedRowStart, rowEnd, Fragment.from(newRows)); tr.setMeta('tableGeneration', true); } catch (error) { - console.error('Error during row generation:', error); + log.error('Error during row generation:', error); throw error; } }; diff --git a/packages/super-editor/src/editors/v1/core/helpers/catchAllSchema.test.js b/packages/super-editor/src/editors/v1/core/helpers/catchAllSchema.test.js index 274879e56d..83653767f8 100644 --- a/packages/super-editor/src/editors/v1/core/helpers/catchAllSchema.test.js +++ b/packages/super-editor/src/editors/v1/core/helpers/catchAllSchema.test.js @@ -172,7 +172,7 @@ describe('createDocFromHTML — unsupported content detection', () => { }); expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toContain('Unsupported HTML content'); + expect(warnSpy.mock.calls[0][1]).toContain('Unsupported HTML content'); }); it('does NOT emit console.warn when callback is provided', () => { diff --git a/packages/super-editor/src/editors/v1/core/helpers/createNodeFromContent.js b/packages/super-editor/src/editors/v1/core/helpers/createNodeFromContent.js index 7202e33728..67e18d9500 100644 --- a/packages/super-editor/src/editors/v1/core/helpers/createNodeFromContent.js +++ b/packages/super-editor/src/editors/v1/core/helpers/createNodeFromContent.js @@ -1,5 +1,8 @@ import { DOMParser, Schema, Fragment } from 'prosemirror-model'; import { htmlHandler } from '../InputRule.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('create-node-from-content'); const removeWhitespaces = (node) => { const children = node.childNodes; @@ -62,7 +65,7 @@ export function createNodeFromContent(content, editor, options) { throw new Error('[super-editor error]: Invalid JSON content', { cause: error }); } - console.warn('[super-editor warn]: Invalid content.', 'Passed value:', content, 'Error:', error); + log.warn('[super-editor warn]: Invalid content.', 'Passed value:', content, 'Error:', error); return createNodeFromContent('', editor, options); } @@ -74,7 +77,7 @@ export function createNodeFromContent(content, editor, options) { // If elementFromString returned null (no DOM available), we can't parse HTML if (element === null) { - console.warn( + log.warn( '[super-editor] Cannot parse HTML content without a DOM. HTML insertion requires a browser environment or JSDOM. Skipping insertion.', ); return null; diff --git a/packages/super-editor/src/editors/v1/core/helpers/domWarnings.js b/packages/super-editor/src/editors/v1/core/helpers/domWarnings.js index aeb160f8a9..472c537a2f 100644 --- a/packages/super-editor/src/editors/v1/core/helpers/domWarnings.js +++ b/packages/super-editor/src/editors/v1/core/helpers/domWarnings.js @@ -1,10 +1,14 @@ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('dom-warnings'); + /** * Log a standard warning when a DOM document is required but unavailable. * * @param {string} feature - Short description of the DOM-dependent feature. */ export const warnNoDOM = (feature = 'This feature') => { - console.warn( + log.warn( `[super-editor] ${feature} requires a DOM document. ` + 'This environment has no DOM. Provide a DOM (e.g., JSDOM) and set globalThis.document ' + 'or pass { document } to the editor.', diff --git a/packages/super-editor/src/editors/v1/core/helpers/importHtml.js b/packages/super-editor/src/editors/v1/core/helpers/importHtml.js index 53526aa774..e4c40dc157 100644 --- a/packages/super-editor/src/editors/v1/core/helpers/importHtml.js +++ b/packages/super-editor/src/editors/v1/core/helpers/importHtml.js @@ -4,6 +4,9 @@ import { stripHtmlStyles } from './htmlSanitizer.js'; import { htmlHandler } from '../InputRule.js'; import { wrapTextsInRuns } from '../inputRules/docx-paste/docx-paste.js'; import { detectUnsupportedContent } from './catchAllSchema.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('import-html'); /** * @typedef {import('./catchAllSchema.js').UnsupportedContentItem} UnsupportedContentItem @@ -59,7 +62,7 @@ export function createDocFromHTML(content, editor, options = {}) { if (options.onUnsupportedContent) { options.onUnsupportedContent(unsupported); } else { - console.warn('[super-editor] Unsupported HTML content dropped during import:', unsupported); + log.warn('[super-editor] Unsupported HTML content dropped during import:', unsupported); } } } diff --git a/packages/super-editor/src/editors/v1/core/helpers/importMarkdown.js b/packages/super-editor/src/editors/v1/core/helpers/importMarkdown.js index 9244849569..15116d2a27 100644 --- a/packages/super-editor/src/editors/v1/core/helpers/importMarkdown.js +++ b/packages/super-editor/src/editors/v1/core/helpers/importMarkdown.js @@ -1,5 +1,8 @@ // @ts-check import { markdownToPmDoc } from './markdown/markdownToPmContent.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('import-markdown'); /** * Create a ProseMirror document from Markdown content. @@ -37,7 +40,7 @@ export function createDocFromMarkdown(markdown, editor, options = {}) { if (options.onUnsupportedContent) { options.onUnsupportedContent(items); } else if (options.warnOnUnsupportedContent) { - console.warn('[super-editor] Unsupported Markdown content during import:', items); + log.warn('[super-editor] Unsupported Markdown content during import:', items); } } diff --git a/packages/super-editor/src/editors/v1/core/inputRules/html/html-helpers.js b/packages/super-editor/src/editors/v1/core/inputRules/html/html-helpers.js index 579ab565c8..83fac9f3ba 100644 --- a/packages/super-editor/src/editors/v1/core/inputRules/html/html-helpers.js +++ b/packages/super-editor/src/editors/v1/core/inputRules/html/html-helpers.js @@ -1,4 +1,7 @@ import { ListHelpers, createListIdAllocator } from '@helpers/list-numbering-helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('html-helpers'); const removeWhitespaces = (node) => { const children = node.childNodes; @@ -30,7 +33,7 @@ export function flattenListsInHtml(html, editor, domDocument) { const win = resolvedDocument?.defaultView ?? (typeof window !== 'undefined' ? window : null); const DOMParserConstructor = win?.DOMParser ?? (typeof DOMParser !== 'undefined' ? DOMParser : null); if (!DOMParserConstructor) { - console.warn( + log.warn( '[super-editor] HTML list processing requires a DOM. Provide { document } (e.g. from JSDOM), set DOM globals, or run in a browser environment. Skipping list flattening.', ); return html; @@ -282,7 +285,7 @@ export function unflattenListsInHtml(html, domDocument) { const win = domDocument?.defaultView ?? (typeof window !== 'undefined' ? window : null); const DOMParserConstructor = win?.DOMParser ?? (typeof DOMParser !== 'undefined' ? DOMParser : null); if (!DOMParserConstructor) { - console.warn( + log.warn( '[super-editor] HTML list processing requires a DOM. Provide { document } (e.g. from JSDOM), set DOM globals, or run in a browser environment. Skipping list unflattening.', ); return html; diff --git a/packages/super-editor/src/editors/v1/core/layout-adapter/marks/application.ts b/packages/super-editor/src/editors/v1/core/layout-adapter/marks/application.ts index d1ea4c9c31..6004522552 100644 --- a/packages/super-editor/src/editors/v1/core/layout-adapter/marks/application.ts +++ b/packages/super-editor/src/editors/v1/core/layout-adapter/marks/application.ts @@ -22,6 +22,9 @@ import { normalizeColor, isFiniteNumber, ptToPx } from '../utilities.js'; import { buildFlowRunLink, migrateLegacyLink } from './links.js'; import { sanitizeHref } from '@superdoc/url-validation'; import { resolveThemeColorValue } from './theme-color.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('pm-adapter'); /** * Track change mark type constants from ProseMirror schema. @@ -319,7 +322,7 @@ export const extractDataAttributes = ( // Enforce maximum number of data attributes if (attrCount >= MAX_DATA_ATTR_COUNT) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Rejecting data attributes exceeding ${MAX_DATA_ATTR_COUNT} limit`); + log.warn(`[PM-Adapter] Rejecting data attributes exceeding ${MAX_DATA_ATTR_COUNT} limit`); } break; } @@ -327,7 +330,7 @@ export const extractDataAttributes = ( // Enforce maximum attribute name length if (key.length > MAX_DATA_ATTR_NAME_LENGTH) { if (process.env.NODE_ENV === 'development') { - console.warn( + log.warn( `[PM-Adapter] Rejecting data attribute name exceeding ${MAX_DATA_ATTR_NAME_LENGTH} chars: ${key.substring(0, 50)}...`, ); } @@ -344,7 +347,7 @@ export const extractDataAttributes = ( // Enforce maximum value length if (stringValue.length > MAX_DATA_ATTR_VALUE_LENGTH) { if (process.env.NODE_ENV === 'development') { - console.warn( + log.warn( `[PM-Adapter] Rejecting data attribute value exceeding ${MAX_DATA_ATTR_VALUE_LENGTH} chars for key: ${key}`, ); } @@ -373,7 +376,7 @@ export const normalizeRunMarkList = (value: unknown): RunMark[] | undefined => { // Prevent DoS attacks from extremely large JSON payloads if (value.length > MAX_RUN_MARK_JSON_LENGTH) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Rejecting run mark JSON payload exceeding ${MAX_RUN_MARK_JSON_LENGTH} chars`); + log.warn(`[PM-Adapter] Rejecting run mark JSON payload exceeding ${MAX_RUN_MARK_JSON_LENGTH} chars`); } return undefined; } @@ -381,7 +384,7 @@ export const normalizeRunMarkList = (value: unknown): RunMark[] | undefined => { entries = JSON.parse(value); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Failed to parse run mark JSON:', error); + log.warn('[PM-Adapter] Failed to parse run mark JSON:', error); } return undefined; } @@ -391,13 +394,13 @@ export const normalizeRunMarkList = (value: unknown): RunMark[] | undefined => { } if (entries.length > MAX_RUN_MARK_ARRAY_LENGTH) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Rejecting run mark array exceeding ${MAX_RUN_MARK_ARRAY_LENGTH} entries`); + log.warn(`[PM-Adapter] Rejecting run mark array exceeding ${MAX_RUN_MARK_ARRAY_LENGTH} entries`); } return undefined; } if (!validateDepth(entries)) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Rejecting run mark array exceeding depth ${MAX_RUN_MARK_DEPTH}`); + log.warn(`[PM-Adapter] Rejecting run mark array exceeding depth ${MAX_RUN_MARK_DEPTH}`); } return undefined; } @@ -777,7 +780,7 @@ const sanitizeFontFamily = (fontFamily: string): string | undefined => { // Enforce maximum length to prevent DoS if (sanitized.length > MAX_FONT_FAMILY_LENGTH) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Font family name exceeds ${MAX_FONT_FAMILY_LENGTH} character limit`); + log.warn(`[PM-Adapter] Font family name exceeds ${MAX_FONT_FAMILY_LENGTH} character limit`); } return undefined; } @@ -787,7 +790,7 @@ const sanitizeFontFamily = (fontFamily: string): string | undefined => { const dangerousSchemes = ['javascript:', 'data:', 'vbscript:']; if (dangerousSchemes.some((scheme) => lowerCased.includes(scheme))) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Rejected font family containing dangerous URI scheme'); + log.warn('[PM-Adapter] Rejected font family containing dangerous URI scheme'); } return undefined; } @@ -800,7 +803,7 @@ const sanitizeFontFamily = (fontFamily: string): string | undefined => { const cssInjectionPattern = /[;{}()@<>]/; if (cssInjectionPattern.test(sanitized)) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Rejected font family containing CSS injection characters'); + log.warn('[PM-Adapter] Rejected font family containing CSS injection characters'); } return undefined; } @@ -1059,7 +1062,7 @@ export const applyMarksToRun = ( } } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Failed to build rich hyperlink:', error); + log.warn('[PM-Adapter] Failed to build rich hyperlink:', error); } // Fall through to legacy link handling or skip } @@ -1075,7 +1078,7 @@ export const applyMarksToRun = ( } } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Failed to sanitize link href:', error); + log.warn('[PM-Adapter] Failed to sanitize link href:', error); } // Skip this link if sanitization fails } @@ -1088,7 +1091,7 @@ export const applyMarksToRun = ( } } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Failed to apply mark ${mark.type}:`, error); + log.warn(`[PM-Adapter] Failed to apply mark ${mark.type}:`, error); } // Continue processing other marks } diff --git a/packages/super-editor/src/editors/v1/core/layout-adapter/tracked-changes.ts b/packages/super-editor/src/editors/v1/core/layout-adapter/tracked-changes.ts index 6de09ca50c..d9620896ba 100644 --- a/packages/super-editor/src/editors/v1/core/layout-adapter/tracked-changes.ts +++ b/packages/super-editor/src/editors/v1/core/layout-adapter/tracked-changes.ts @@ -38,6 +38,9 @@ import { MAX_RUN_MARK_DEPTH, DEFAULT_HYPERLINK_CONFIG, } from './constants.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('pm-adapter'); /** * Type guard to validate that a value is a valid TrackedChangesMode. @@ -122,7 +125,7 @@ export const normalizeRunMarkList = (value: unknown): RunMark[] | undefined => { // Prevent DoS attacks from extremely large JSON payloads if (value.length > MAX_RUN_MARK_JSON_LENGTH) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Rejecting run mark JSON payload exceeding ${MAX_RUN_MARK_JSON_LENGTH} chars`); + log.warn(`[PM-Adapter] Rejecting run mark JSON payload exceeding ${MAX_RUN_MARK_JSON_LENGTH} chars`); } return undefined; } @@ -130,7 +133,7 @@ export const normalizeRunMarkList = (value: unknown): RunMark[] | undefined => { entries = JSON.parse(value); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Failed to parse run mark JSON:', error); + log.warn('[PM-Adapter] Failed to parse run mark JSON:', error); } return undefined; } @@ -140,13 +143,13 @@ export const normalizeRunMarkList = (value: unknown): RunMark[] | undefined => { } if (entries.length > MAX_RUN_MARK_ARRAY_LENGTH) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Rejecting run mark array exceeding ${MAX_RUN_MARK_ARRAY_LENGTH} entries`); + log.warn(`[PM-Adapter] Rejecting run mark array exceeding ${MAX_RUN_MARK_ARRAY_LENGTH} entries`); } return undefined; } if (!validateDepth(entries)) { if (process.env.NODE_ENV === 'development') { - console.warn(`[PM-Adapter] Rejecting run mark array exceeding depth ${MAX_RUN_MARK_DEPTH}`); + log.warn(`[PM-Adapter] Rejecting run mark array exceeding depth ${MAX_RUN_MARK_DEPTH}`); } return undefined; } @@ -441,7 +444,7 @@ export const applyFormatChangeMarks = ( if (!isValidMarkArray) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Invalid before marks in tracked change, resetting formatting'); + log.warn('[PM-Adapter] Invalid before marks in tracked change, resetting formatting'); } resetRunFormatting(run); return; @@ -454,7 +457,7 @@ export const applyFormatChangeMarks = ( applyMarksToRun(run, beforeMarks as PMMark[], hyperlinkConfig, themeColors, undefined, enableComments, storyKey); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PM-Adapter] Error applying format change marks, resetting formatting:', error); + log.warn('[PM-Adapter] Error applying format change marks, resetting formatting:', error); } // On error, ensure run is in clean state with no formatting resetRunFormatting(run); diff --git a/packages/super-editor/src/editors/v1/core/migrations/0.14-listsv2/listsv2migration.js b/packages/super-editor/src/editors/v1/core/migrations/0.14-listsv2/listsv2migration.js index d7b9677d3b..194840ff4b 100644 --- a/packages/super-editor/src/editors/v1/core/migrations/0.14-listsv2/listsv2migration.js +++ b/packages/super-editor/src/editors/v1/core/migrations/0.14-listsv2/listsv2migration.js @@ -1,9 +1,12 @@ import { getAllFieldAnnotations } from '@extensions/field-annotation/fieldAnnotationHelpers/index.js'; import { ListHelpers } from '@helpers/list-numbering-helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const logger = createLogger('lists-v2-migration'); const isDebugging = false; const log = (...args) => { - if (isDebugging) console.debug('[lists v2 migration]', ...args); + if (isDebugging) logger.debug('[lists v2 migration]', ...args); }; /** diff --git a/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-part-descriptor.ts b/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-part-descriptor.ts index 9e4bc83f3e..307e66a67b 100644 --- a/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-part-descriptor.ts +++ b/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-part-descriptor.ts @@ -14,6 +14,9 @@ import type { Editor } from '../../Editor.js'; import type { PartDescriptor, CommitContext, DeleteContext, PartId } from '../types.js'; import { registerPartDescriptor, hasPartDescriptor } from '../registry/part-registry.js'; import { registerInvalidationHandler } from '../invalidation/part-invalidation-registry.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('parts'); // --------------------------------------------------------------------------- // Converter shape @@ -153,7 +156,7 @@ export function ensureHeaderFooterDescriptor(partId: PartId, sectionId: string): collection[resolvedSectionId] = pmJson; } } catch (err) { - console.warn(`[parts] Failed to re-import ${ctx.partId}:`, err); + log.warn(`Failed to re-import ${ctx.partId}:`, err); } } @@ -207,7 +210,7 @@ function refreshActiveSubEditors( try { entry.editor.replaceContent(pmJson); } catch (err) { - console.warn(`[parts] Failed to refresh sub-editor for ${type}:${sectionId}:`, err); + log.warn(`Failed to refresh sub-editor for ${type}:${sectionId}:`, err); } } } diff --git a/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-sync.ts b/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-sync.ts index c95a726408..21d8b3bd84 100644 --- a/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-sync.ts +++ b/packages/super-editor/src/editors/v1/core/parts/adapters/header-footer-sync.ts @@ -17,6 +17,9 @@ import { } from './header-footer-part-descriptor.js'; import { getRelationshipsRoot } from '../../helpers/rels-part-helpers.js'; import { getWordPartRelsPath, normalizeWordPartPath } from '../../helpers/word-part-path.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('header-footer-sync'); // --------------------------------------------------------------------------- // Converter shape @@ -217,7 +220,7 @@ export function exportSubEditorToPart( }); bodyContent = result?.elements?.[0]?.elements ?? []; } catch (err) { - console.warn(`[header-footer-sync] Export failed for ${partId}:`, err); + log.warn(`Export failed for ${partId}:`, err); return false; } @@ -263,7 +266,7 @@ export function exportSubEditorToPart( return true; } catch (err) { - console.warn(`[header-footer-sync] mutatePart failed for ${partId}:`, err); + log.warn(`mutatePart failed for ${partId}:`, err); return false; } } diff --git a/packages/super-editor/src/editors/v1/core/parts/adapters/notes-part-descriptor.ts b/packages/super-editor/src/editors/v1/core/parts/adapters/notes-part-descriptor.ts index eeb3c3475b..e5d6c9e14f 100644 --- a/packages/super-editor/src/editors/v1/core/parts/adapters/notes-part-descriptor.ts +++ b/packages/super-editor/src/editors/v1/core/parts/adapters/notes-part-descriptor.ts @@ -13,6 +13,9 @@ import type { Editor } from '../../Editor.js'; import type { PartDescriptor, PartId } from '../types.js'; import { clearPartCacheStale } from '../cache-staleness.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('parts'); // --------------------------------------------------------------------------- // Part IDs @@ -289,7 +292,7 @@ function rebuildDerivedCache(editor: Editor, config: NotePartConfig, part: unkno converter[config.converterKey] = converter.reimportNotePart(config.partId); return; } catch (err) { - console.warn(`[parts] reimportNotePart failed for ${config.partId}, using fallback:`, err); + log.warn(`reimportNotePart failed for ${config.partId}, using fallback:`, err); } } diff --git a/packages/super-editor/src/editors/v1/core/parts/adapters/relationships-mutation.ts b/packages/super-editor/src/editors/v1/core/parts/adapters/relationships-mutation.ts index 761bb2436d..e2db3b956d 100644 --- a/packages/super-editor/src/editors/v1/core/parts/adapters/relationships-mutation.ts +++ b/packages/super-editor/src/editors/v1/core/parts/adapters/relationships-mutation.ts @@ -16,6 +16,9 @@ import { mutatePart } from '../mutation/mutate-part.js'; import { hasPart } from '../store/part-store.js'; import { RELATIONSHIP_TYPES } from '../../super-converter/docx-helpers/docx-constants.js'; import { createRelationshipsPart, getRelationshipsRoot } from '../../helpers/rels-part-helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('parts'); const RELS_PART_ID = 'word/_rels/document.xml.rels' as const; @@ -108,7 +111,7 @@ export function findOrCreateRelationship(editor: Editor, source: string, options const mappedType = RELATIONSHIP_TYPES[type]; if (!mappedType) { - console.warn(`findOrCreateRelationship: unsupported type "${type}"`); + log.warn(`findOrCreateRelationship: unsupported type "${type}"`); return null; } diff --git a/packages/super-editor/src/editors/v1/core/parts/adapters/styles-part-descriptor.ts b/packages/super-editor/src/editors/v1/core/parts/adapters/styles-part-descriptor.ts index 33500d35a0..d0d3b6919a 100644 --- a/packages/super-editor/src/editors/v1/core/parts/adapters/styles-part-descriptor.ts +++ b/packages/super-editor/src/editors/v1/core/parts/adapters/styles-part-descriptor.ts @@ -10,6 +10,9 @@ import type { PartDescriptor, CommitContext } from '../types.js'; import { translateStyleDefinitions } from '../../super-converter/v2/importer/docxImporter.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('parts'); const STYLES_PART_ID = 'word/styles.xml' as const; @@ -45,7 +48,7 @@ export const stylesPartDescriptor: PartDescriptor = { try { converter.translatedLinkedStyles = translateStyleDefinitions(converter.convertedXml); } catch (err) { - console.warn('[parts] Failed to rebuild translatedLinkedStyles:', err); + log.warn('Failed to rebuild translatedLinkedStyles:', err); } } } diff --git a/packages/super-editor/src/editors/v1/core/parts/invalidation/part-invalidation-registry.ts b/packages/super-editor/src/editors/v1/core/parts/invalidation/part-invalidation-registry.ts index 6245a5fb1e..044b5d520a 100644 --- a/packages/super-editor/src/editors/v1/core/parts/invalidation/part-invalidation-registry.ts +++ b/packages/super-editor/src/editors/v1/core/parts/invalidation/part-invalidation-registry.ts @@ -9,6 +9,9 @@ import type { Editor } from '../../Editor.js'; import type { PartId, PartChangedEvent } from '../types.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('parts'); type InvalidationHandler = (editor: Editor, event: PartChangedEvent) => void; @@ -36,7 +39,7 @@ export function applyPartInvalidation(editor: Editor, event: PartChangedEvent): try { handler(editor, event); } catch (err) { - console.error(`[parts] Invalidation handler failed for "${part.partId}":`, err); + log.error(`Invalidation handler failed for "${part.partId}":`, err); } } } diff --git a/packages/super-editor/src/editors/v1/core/parts/mutation/mutate-part.ts b/packages/super-editor/src/editors/v1/core/parts/mutation/mutate-part.ts index 457f2340ce..238f60cdef 100644 --- a/packages/super-editor/src/editors/v1/core/parts/mutation/mutate-part.ts +++ b/packages/super-editor/src/editors/v1/core/parts/mutation/mutate-part.ts @@ -37,6 +37,9 @@ import { diffPartPaths } from './diff-part-paths.js'; import { checkRevision, incrementRevision } from '../../../document-api-adapters/plan-engine/revision-tracker.js'; import { applyPartInvalidation } from '../invalidation/part-invalidation-registry.js'; import { markPartCacheStale } from '../cache-staleness.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('parts'); // --------------------------------------------------------------------------- // Converter shape (minimal interface to avoid importing SuperConverter) @@ -258,7 +261,7 @@ function runPostCommitSideEffects( } catch (err) { degraded = true; markPartCacheStale(editor, outcome.partId); - console.error(`[parts] afterCommit hook failed for "${outcome.partId}":`, err); + log.error(`afterCommit hook failed for "${outcome.partId}":`, err); } } @@ -303,7 +306,7 @@ function emitPartChanged( if (typeof editor.safeEmit === 'function') { const errors = editor.safeEmit('partChanged', event); for (const err of errors) { - console.error('[parts] partChanged listener threw:', err); + log.error('partChanged listener threw:', err); } } return event; diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts index dad4441c10..5f163148cc 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/PresentationEditor.ts @@ -394,6 +394,9 @@ export type SelectionCommandContext = { // Mark name constants import { CommentMarkName } from '@extensions/comment/comments-constants.js'; import { TrackInsertMarkName, TrackDeleteMarkName, TrackFormatMarkName } from '@extensions/track-changes/constants.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('presentation-editor'); const DEFAULT_PAGE_SIZE: PageSize = { w: 612, h: 792 }; // Letter @ 72dpi const DEFAULT_MARGINS: PageMargins = { top: 72, right: 72, bottom: 72, left: 72 }; @@ -415,7 +418,7 @@ const layoutDebugEnabled = /** Log performance metrics when debug is enabled */ const perfLog = (...args: unknown[]): void => { if (!layoutDebugEnabled) return; - console.log(...args); + log.debug(...args); }; /** Budget for header/footer initialization before warning (milliseconds) */ const HEADER_FOOTER_INIT_BUDGET_MS = 200; @@ -1016,7 +1019,7 @@ export class PresentationEditor extends EventEmitter { #warnUnsupportedNumberingRestart(kind: 'footnote' | 'endnote'): void { if (this.#warnedUnsupportedRestart[kind]) return; this.#warnedUnsupportedRestart[kind] = true; - console.warn( + log.warn( `[PresentationEditor] ${kind} numRestart="eachPage" is not yet supported (requires a two-pass pagination handshake). Falling back to "continuous". Tracked for follow-up.`, ); } @@ -2147,7 +2150,7 @@ export class PresentationEditor extends EventEmitter { } catch (error) { // DOM operations can throw exceptions - fall back to geometry-only positioning if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] DOM caret computation failed in getRectsForRange:', error); + log.warn('[PresentationEditor] DOM caret computation failed in getRectsForRange:', error); } } const layoutCaretStart = this.#computeCaretLayoutRectGeometry(start, false); @@ -3420,7 +3423,7 @@ export class PresentationEditor extends EventEmitter { pos: number, ): { top: number; bottom: number; left: number; right: number; width: number; height: number } | null { if (!Number.isFinite(pos)) { - console.warn('[PresentationEditor] coordsAtPos called with invalid position:', pos); + log.warn('[PresentationEditor] coordsAtPos called with invalid position:', pos); return null; } @@ -3429,7 +3432,7 @@ export class PresentationEditor extends EventEmitter { if (sessionMode !== 'body') { const context = this.#getHeaderFooterContext(); if (!context) { - console.warn('[PresentationEditor] Header/footer context not available for coordsAtPos'); + log.warn('[PresentationEditor] Header/footer context not available for coordsAtPos'); return null; } @@ -3942,7 +3945,7 @@ export class PresentationEditor extends EventEmitter { timeout: PresentationEditor.ANCHOR_NAV_TIMEOUT_MS, }); if (!mounted) { - console.warn(`[PresentationEditor] scrollToPositionAsync: Page ${pageIndex} failed to mount within timeout`); + log.warn(`[PresentationEditor] scrollToPositionAsync: Page ${pageIndex} failed to mount within timeout`); return false; } @@ -4174,7 +4177,7 @@ export class PresentationEditor extends EventEmitter { const clientY = coords?.clientY ?? coords?.top ?? null; if (!Number.isFinite(clientX) || !Number.isFinite(clientY)) { - console.warn('[PresentationEditor] posAtCoords called with invalid coordinates:', coords); + log.warn('[PresentationEditor] posAtCoords called with invalid coordinates:', coords); return null; } @@ -4245,7 +4248,7 @@ export class PresentationEditor extends EventEmitter { throw new RangeError('[PresentationEditor] setZoom expects a positive number greater than 0'); } if (zoom > MAX_ZOOM_WARNING_THRESHOLD) { - console.warn( + log.warn( `[PresentationEditor] Zoom level ${zoom} exceeds recommended maximum of ${MAX_ZOOM_WARNING_THRESHOLD}. Performance may degrade.`, ); } @@ -4587,7 +4590,7 @@ export class PresentationEditor extends EventEmitter { try { this.#postPaintPipeline.syncInlineStyleLayers(state, this.#domPositionIndex); } catch (error) { - console.warn('[PresentationEditor] Inline style layer sync failed:', error); + log.warn('[PresentationEditor] Inline style layer sync failed:', error); } } @@ -4609,7 +4612,7 @@ export class PresentationEditor extends EventEmitter { } catch (error) { // Sync can call findRangeByText and other doc-dependent logic; if it throws // (e.g. edge-case doc state), avoid breaking the RAF or observer sync loop. - console.warn('[PresentationEditor] Decoration sync failed:', error); + log.warn('[PresentationEditor] Decoration sync failed:', error); } } @@ -5808,11 +5811,11 @@ export class PresentationEditor extends EventEmitter { } if (detail && Object.keys(detail).length > 0) { - console.debug('[PresentationEditor][UnifiedHistory]', message, detail); + log.debug('[PresentationEditor][UnifiedHistory]', message, detail); return; } - console.debug('[PresentationEditor][UnifiedHistory]', message); + log.debug('[PresentationEditor][UnifiedHistory]', message); } #recordNoteHitDebug(entry: Record): void { @@ -5944,7 +5947,7 @@ export class PresentationEditor extends EventEmitter { try { commitHook(this.#editor, noteEditor); } catch (error) { - console.warn('[PresentationEditor] Note commit after replay failed:', error); + log.warn('[PresentationEditor] Note commit after replay failed:', error); } } this.#pendingDocChange = true; @@ -6107,7 +6110,7 @@ export class PresentationEditor extends EventEmitter { try { cleanup(); } catch (error) { - console.warn('[PresentationEditor] Unified history cleanup failed:', error); + log.warn('[PresentationEditor] Unified history cleanup failed:', error); } }); this.#historyCoordinatorCleanup.length = 0; @@ -6177,7 +6180,7 @@ export class PresentationEditor extends EventEmitter { return true; } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to select word:', error); + log.warn('[PresentationEditor] Failed to select word:', error); } return false; } @@ -6214,7 +6217,7 @@ export class PresentationEditor extends EventEmitter { return true; } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to select paragraph:', error); + log.warn('[PresentationEditor] Failed to select paragraph:', error); } return false; } @@ -7314,7 +7317,7 @@ export class PresentationEditor extends EventEmitter { // DOM manipulation can fail if element is detached or in invalid state // Log but don't throw to prevent breaking editor if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to clear selection layer in viewing mode:', error); + log.warn('[PresentationEditor] Failed to clear selection layer in viewing mode:', error); } } return; @@ -7354,7 +7357,7 @@ export class PresentationEditor extends EventEmitter { this.#localSelectionLayer.innerHTML = ''; } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to clear selection layer (no selection):', error); + log.warn('[PresentationEditor] Failed to clear selection layer (no selection):', error); } } return; @@ -7387,7 +7390,7 @@ export class PresentationEditor extends EventEmitter { this.#localSelectionLayer.innerHTML = ''; this.#renderCellSelectionOverlay(selection, layout); } catch (error) { - console.warn('[PresentationEditor] Failed to render cell selection overlay:', error); + log.warn('[PresentationEditor] Failed to render cell selection overlay:', error); } return; } @@ -7410,7 +7413,7 @@ export class PresentationEditor extends EventEmitter { } catch (error) { // DOM manipulation can fail if element is detached or in invalid state if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to render caret overlay:', error); + log.warn('[PresentationEditor] Failed to render caret overlay:', error); } } if (shouldScrollIntoView && !isDragDropIndicatorActive) { @@ -7455,7 +7458,7 @@ export class PresentationEditor extends EventEmitter { } catch (error) { // DOM manipulation can fail if element is detached or in invalid state if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to render selection rects:', error); + log.warn('[PresentationEditor] Failed to render selection rects:', error); } } @@ -7821,13 +7824,13 @@ export class PresentationEditor extends EventEmitter { // Validate that margins are finite numbers and don't exceed page height if (!Number.isFinite(marginTop) || !Number.isFinite(marginBottom)) { - console.warn('[PresentationEditor] Invalid top or bottom margin: not a finite number'); + log.warn('[PresentationEditor] Invalid top or bottom margin: not a finite number'); return null; } const totalVerticalMargins = marginTop + marginBottom; if (totalVerticalMargins >= pageSize.h) { - console.warn( + log.warn( `[PresentationEditor] Invalid margins: top (${marginTop}) + bottom (${marginBottom}) = ${totalVerticalMargins} >= page height (${pageSize.h})`, ); return null; @@ -8783,7 +8786,7 @@ export class PresentationEditor extends EventEmitter { return false; } catch (error) { - console.error('[PresentationEditor] navigateTo failed:', error); + log.error('[PresentationEditor] navigateTo failed:', error); this.emit('error', { error, context: 'navigateTo' }); return false; } @@ -9443,7 +9446,7 @@ export class PresentationEditor extends EventEmitter { timeoutMs: PresentationEditor.ANCHOR_NAV_TIMEOUT_MS, }); } catch (error) { - console.error('[PresentationEditor] goToAnchor failed:', error); + log.error('[PresentationEditor] goToAnchor failed:', error); this.emit('error', { error, context: 'goToAnchor', @@ -9628,7 +9631,7 @@ export class PresentationEditor extends EventEmitter { return measureVisibleTextOffsetFromHelper(root, domPoint.node, domPoint.offset); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to measure active editor visible text offset:', error); + log.warn('[PresentationEditor] Failed to measure active editor visible text offset:', error); } return null; } @@ -9810,7 +9813,7 @@ export class PresentationEditor extends EventEmitter { } catch (error) { // Plugin may not be loaded or state may be invalid if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to get track changes plugin state:', error); + log.warn('[PresentationEditor] Failed to get track changes plugin state:', error); } return null; } @@ -10236,7 +10239,7 @@ export class PresentationEditor extends EventEmitter { } catch (error) { // DOM operations can throw exceptions - fall back to geometry-only positioning if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] DOM caret computation failed in #computeCaretLayoutRect:', error); + log.warn('[PresentationEditor] DOM caret computation failed in #computeCaretLayoutRect:', error); } } if (dom && geometry) { @@ -10318,7 +10321,7 @@ export class PresentationEditor extends EventEmitter { } #handleLayoutError(phase: LayoutError['phase'], error: Error) { - console.error('[PresentationEditor] Layout error', error); + log.error('[PresentationEditor] Layout error', error); this.#layoutError = { phase, error, timestamp: Date.now() }; // Update error state based on phase @@ -10442,7 +10445,7 @@ export class PresentationEditor extends EventEmitter { }); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to render header/footer caret:', error); + log.warn('[PresentationEditor] Failed to render header/footer caret:', error); } } if (shouldScrollIntoView) { @@ -10474,7 +10477,7 @@ export class PresentationEditor extends EventEmitter { }); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to render header/footer selection rects:', error); + log.warn('[PresentationEditor] Failed to render header/footer selection rects:', error); } } @@ -10528,7 +10531,7 @@ export class PresentationEditor extends EventEmitter { }); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to render note caret:', error); + log.warn('[PresentationEditor] Failed to render note caret:', error); } } if (shouldScrollIntoView) { @@ -10553,7 +10556,7 @@ export class PresentationEditor extends EventEmitter { }); } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to render note selection rects:', error); + log.warn('[PresentationEditor] Failed to render note selection rects:', error); } } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/dom/CoordinateTransform.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/dom/CoordinateTransform.ts index c76bc81c02..744cf7934a 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/dom/CoordinateTransform.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/dom/CoordinateTransform.ts @@ -1,4 +1,7 @@ import { getPageElementByIndex } from '../../../dom-observer/PageDom.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('coordinate-transform'); /** * Calculates the offset of a page element within the viewport. @@ -155,7 +158,7 @@ export function convertPageLocalToOverlayCoords(options: { }): { x: number; y: number } | null { // Validate pageIndex: must be finite and non-negative if (!Number.isFinite(options.pageIndex) || options.pageIndex < 0) { - console.warn( + log.warn( `[PresentationEditor] #convertPageLocalToOverlayCoords: Invalid pageIndex ${options.pageIndex}. ` + 'Expected a finite non-negative number.', ); @@ -164,7 +167,7 @@ export function convertPageLocalToOverlayCoords(options: { // Validate pageLocalX: must be finite if (!Number.isFinite(options.pageLocalX)) { - console.warn( + log.warn( `[PresentationEditor] #convertPageLocalToOverlayCoords: Invalid pageLocalX ${options.pageLocalX}. ` + 'Expected a finite number.', ); @@ -173,7 +176,7 @@ export function convertPageLocalToOverlayCoords(options: { // Validate pageLocalY: must be finite if (!Number.isFinite(options.pageLocalY)) { - console.warn( + log.warn( `[PresentationEditor] #convertPageLocalToOverlayCoords: Invalid pageLocalY ${options.pageLocalY}. ` + 'Expected a finite number.', ); diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/header-footer/HeaderFooterSessionManager.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/header-footer/HeaderFooterSessionManager.ts index 15a1b3052a..0ad01c95dc 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/header-footer/HeaderFooterSessionManager.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/header-footer/HeaderFooterSessionManager.ts @@ -65,6 +65,9 @@ import { resolveSectionProjections } from '../../../document-api-adapters/helper import { computeCaretLayoutRectGeometry as computeCaretLayoutRectGeometryFromHelper } from '../selection/CaretGeometry.js'; import { ensureExplicitHeaderFooterSlot } from '../../../document-api-adapters/helpers/header-footer-slot-materialization.js'; import { normalizeVariant } from './header-footer-variant.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('header-footer-session-manager'); // ============================================================================= // Types @@ -831,10 +834,10 @@ export class HeaderFooterSessionManager { // Debug-mode assertion: every region must have concrete section identity if (this.#options.isDebug) { for (const [, region] of this.#headerRegions) { - if (!region.sectionId) console.error('[HeaderFooterSessionManager] Header region missing sectionId', region); + if (!region.sectionId) log.error('[HeaderFooterSessionManager] Header region missing sectionId', region); } for (const [, region] of this.#footerRegions) { - if (!region.sectionId) console.error('[HeaderFooterSessionManager] Footer region missing sectionId', region); + if (!region.sectionId) log.error('[HeaderFooterSessionManager] Footer region missing sectionId', region); } } @@ -1098,15 +1101,12 @@ export class HeaderFooterSessionManager { } if (!descriptor) { - console.warn( - '[HeaderFooterSessionManager] No descriptor found for region after materialization attempt:', - region, - ); + log.warn('[HeaderFooterSessionManager] No descriptor found for region after materialization attempt:', region); this.clearHover(); return null; } if (!descriptor.id) { - console.warn('[HeaderFooterSessionManager] Descriptor missing id:', descriptor); + log.warn('[HeaderFooterSessionManager] Descriptor missing id:', descriptor); this.clearHover(); return null; } @@ -1118,7 +1118,7 @@ export class HeaderFooterSessionManager { this.#deps?.scrollPageIntoView(region.pageIndex); const mounted = await this.#deps?.waitForPageMount(region.pageIndex, { timeout: 2000 }); if (!mounted) { - console.error('[HeaderFooterSessionManager] Failed to mount page for header/footer editing'); + log.error('[HeaderFooterSessionManager] Failed to mount page for header/footer editing'); this.clearHover(); this.#callbacks.onError?.({ error: new Error('Failed to mount page for editing'), @@ -1128,7 +1128,7 @@ export class HeaderFooterSessionManager { } pageElement = this.#deps?.getPageElement(region.pageIndex) ?? null; } catch (scrollError) { - console.error('[HeaderFooterSessionManager] Error mounting page:', scrollError); + log.error('[HeaderFooterSessionManager] Error mounting page:', scrollError); this.clearHover(); this.#callbacks.onError?.({ error: scrollError, @@ -1139,7 +1139,7 @@ export class HeaderFooterSessionManager { } if (!pageElement) { - console.error('[HeaderFooterSessionManager] Page element not found after mount attempt'); + log.error('[HeaderFooterSessionManager] Page element not found after mount attempt'); this.clearHover(); this.#callbacks.onError?.({ error: new Error('Page element not found after mount'), @@ -1162,7 +1162,7 @@ export class HeaderFooterSessionManager { try { editor = this.#activateStorySessionForRegion(region, descriptor); } catch (editorError) { - console.error('[HeaderFooterSessionManager] Error creating story session:', editorError); + log.error('[HeaderFooterSessionManager] Error creating story session:', editorError); this.clearHover(); this.#callbacks.onError?.({ error: editorError, @@ -1172,7 +1172,7 @@ export class HeaderFooterSessionManager { } if (!editor) { - console.warn('[HeaderFooterSessionManager] Failed to ensure editor for descriptor:', descriptor); + log.warn('[HeaderFooterSessionManager] Failed to ensure editor for descriptor:', descriptor); this.clearHover(); this.#callbacks.onError?.({ error: new Error('Failed to create editor instance'), @@ -1190,7 +1190,7 @@ export class HeaderFooterSessionManager { this.#applyDefaultSelectionAtStoryEnd(editor, 'Could not set cursor to end'); } } catch (editableError) { - console.error('[HeaderFooterSessionManager] Error setting editor editable:', editableError); + log.error('[HeaderFooterSessionManager] Error setting editor editable:', editableError); this.clearHover(); this.#callbacks.onError?.({ error: editableError, @@ -1215,7 +1215,7 @@ export class HeaderFooterSessionManager { try { editor.view?.focus(); } catch (focusError) { - console.warn('[HeaderFooterSessionManager] Could not focus editor:', focusError); + log.warn('[HeaderFooterSessionManager] Could not focus editor:', focusError); } if (shouldRestoreInitialSelection) { @@ -1226,7 +1226,7 @@ export class HeaderFooterSessionManager { try { editor.view?.focus(); } catch (focusError) { - console.warn('[HeaderFooterSessionManager] Could not refocus editor after restoring selection:', focusError); + log.warn('[HeaderFooterSessionManager] Could not refocus editor after restoring selection:', focusError); } this.#scheduleSelectionRestoreAfterFocus(editor); } @@ -1238,7 +1238,7 @@ export class HeaderFooterSessionManager { this.#deps?.scheduleRerender(); return editor; } catch (error) { - console.error('[HeaderFooterSessionManager] Unexpected error in enterMode:', error); + log.error('[HeaderFooterSessionManager] Unexpected error in enterMode:', error); // Attempt cleanup try { @@ -1248,7 +1248,7 @@ export class HeaderFooterSessionManager { this.#activeEditor = null; this.#session = { mode: 'body' }; } catch (cleanupError) { - console.error('[HeaderFooterSessionManager] Error during cleanup:', cleanupError); + log.error('[HeaderFooterSessionManager] Error during cleanup:', cleanupError); } this.#callbacks.onError?.({ @@ -1298,7 +1298,7 @@ export class HeaderFooterSessionManager { try { editor.commands?.setTextSelection?.(selection); } catch (error) { - console.warn(`[HeaderFooterSessionManager] ${warningMessage}:`, error); + log.warn(`[HeaderFooterSessionManager] ${warningMessage}:`, error); } } @@ -1321,7 +1321,7 @@ export class HeaderFooterSessionManager { try { editor.view?.focus(); } catch (focusError) { - console.warn('[HeaderFooterSessionManager] Could not refocus editor on the next frame:', focusError); + log.warn('[HeaderFooterSessionManager] Could not refocus editor on the next frame:', focusError); } }); } @@ -1430,7 +1430,7 @@ export class HeaderFooterSessionManager { try { this.#activeEditorEventCleanup?.(); } catch (error) { - console.warn('[HeaderFooterSessionManager] Failed to clean up active editor bridge:', error); + log.warn('[HeaderFooterSessionManager] Failed to clean up active editor bridge:', error); } finally { this.#activeEditorEventCleanup = null; } @@ -1612,13 +1612,13 @@ export class HeaderFooterSessionManager { const marginBottom = margins.bottom ?? this.#options.defaultMargins.bottom ?? 0; if (!Number.isFinite(marginTop) || !Number.isFinite(marginBottom)) { - console.warn('[HeaderFooterSessionManager] Invalid top or bottom margin: not a finite number'); + log.warn('[HeaderFooterSessionManager] Invalid top or bottom margin: not a finite number'); return null; } const totalVerticalMargins = marginTop + marginBottom; if (totalVerticalMargins >= pageSize.h) { - console.warn( + log.warn( `[HeaderFooterSessionManager] Invalid margins: top (${marginTop}) + bottom (${marginBottom}) = ${totalVerticalMargins} >= page height (${pageSize.h})`, ); return null; @@ -1788,7 +1788,7 @@ export class HeaderFooterSessionManager { // Resolve layout context for the active header/footer region. const context = this.getContext(); if (!context) { - console.warn('[HeaderFooterSessionManager] Header/footer context unavailable for selection rects', { + log.warn('[HeaderFooterSessionManager] Header/footer context unavailable for selection rects', { mode: this.#session.mode, pageIndex: this.#session.pageIndex, }); @@ -2220,13 +2220,13 @@ export class HeaderFooterSessionManager { const regionMap = this.#session.mode === 'header' ? this.#headerRegions : this.#footerRegions; const region = regionMap.get(pageIndex); if (!region) { - console.warn('[HeaderFooterSessionManager] Header/footer region not found for pageIndex:', pageIndex); + log.warn('[HeaderFooterSessionManager] Header/footer region not found for pageIndex:', pageIndex); return null; } const activeLayoutResult = this.#resolveActiveLayoutResult(region); if (!activeLayoutResult) { - console.warn('[HeaderFooterSessionManager] Header/footer layout results not available'); + log.warn('[HeaderFooterSessionManager] Header/footer layout results not available'); return null; } @@ -2276,7 +2276,7 @@ export class HeaderFooterSessionManager { getPageHeight(): number { const context = this.getContext(); if (!context) { - console.warn('[HeaderFooterSessionManager] Header/footer context missing when computing page height'); + log.warn('[HeaderFooterSessionManager] Header/footer context missing when computing page height'); return 1; } return context.layout.pageSize?.h ?? context.region.height ?? 1; @@ -2317,7 +2317,7 @@ export class HeaderFooterSessionManager { return cachedItems; } if (cachedItems) { - console.warn( + log.warn( `[HeaderFooterSessionManager] Resolved items length (${cachedItems.length}) does not match fragments length (${fragments.length}) for ${contextLabel}. Recomputing items.`, ); } @@ -2329,7 +2329,7 @@ export class HeaderFooterSessionManager { return freshItems; } if (freshItems) { - console.warn( + log.warn( `[HeaderFooterSessionManager] Fresh resolved items length (${freshItems.length}) does not match fragments length (${fragments.length}) for ${contextLabel}. Dropping items.`, ); } @@ -2422,7 +2422,7 @@ export class HeaderFooterSessionManager { if (rIdLayoutKey) { const rIdLayout = layoutsByRId.get(rIdLayoutKey); if (!rIdLayout) { - console.warn( + log.warn( `[HeaderFooterSessionManager] Inconsistent state: layoutsByRId.has('${sectionRId}') returned true but get() returned undefined`, ); } else { @@ -2588,7 +2588,7 @@ export class HeaderFooterSessionManager { try { fn(); } catch (e) { - console.error('[HeaderFooterSessionManager] Cleanup error:', e); + log.error('[HeaderFooterSessionManager] Cleanup error:', e); } }); this.#managerCleanups = []; diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/history/NoteEditorRegistry.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/history/NoteEditorRegistry.ts index 0eafcebdd2..35bf7e89e1 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/history/NoteEditorRegistry.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/history/NoteEditorRegistry.ts @@ -19,6 +19,9 @@ import type { FootnoteStoryLocator, EndnoteStoryLocator } from '@superdoc/document-api'; import type { Editor } from '../../Editor.js'; import { EventEmitter } from '../../EventEmitter.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('note-editor-registry'); type NoteLocator = FootnoteStoryLocator | EndnoteStoryLocator; @@ -288,13 +291,13 @@ export class NoteEditorRegistry extends EventEmitter { try { disposer(); } catch (error) { - console.warn('[NoteEditorRegistry] disposer threw:', error); + log.warn('[NoteEditorRegistry] disposer threw:', error); } }); try { entry.editor.destroy?.(); } catch (error) { - console.warn('[NoteEditorRegistry] editor.destroy threw:', error); + log.warn('[NoteEditorRegistry] editor.destroy threw:', error); } this.emit('editorDisposed', { storyKey: entry.storyKey, reason }); } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/input/PresentationInputBridge.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/input/PresentationInputBridge.ts index 41f54261d0..b00c010bb9 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/input/PresentationInputBridge.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/input/PresentationInputBridge.ts @@ -1,5 +1,8 @@ import { isInRegisteredSurface } from '../utils/uiSurfaceRegistry.js'; import { CONTEXT_MENU_HANDLED_FLAG } from '../../../components/context-menu/event-flags.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('presentation-input-bridge'); const BRIDGE_FORWARDED_FLAG = Symbol('presentation-input-bridge-forwarded'); @@ -138,7 +141,7 @@ export class PresentationInputBridge { } catch (error) { // Ignore dispatch failures - can happen if target was removed from DOM if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to dispatch composition event:', error); + log.warn('[PresentationEditor] Failed to dispatch composition event:', error); } } } @@ -184,7 +187,7 @@ export class PresentationInputBridge { } } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[PresentationEditor] Failed to dispatch event to target:', error); + log.warn('[PresentationEditor] Failed to dispatch event to target:', error); } } } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts index b9f15a31f0..9c4b5dcccd 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/pointer-events/EditorInputManager.ts @@ -50,6 +50,9 @@ import { findStructuredContentInlineById, type StructuredContentSelection, } from '../input/structured-content-resolution.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('editor-input-manager'); // ============================================================================= // Constants @@ -2447,7 +2450,7 @@ export class EditorInputManager { this.#lastSelectedImageBlockId = newSelectionId; } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn(`[EditorInputManager] Failed to create NodeSelection for inline image:`, error); + log.warn(`[EditorInputManager] Failed to create NodeSelection for inline image:`, error); } } @@ -2489,7 +2492,7 @@ export class EditorInputManager { } } catch (error) { if (process.env.NODE_ENV === 'development') { - console.warn('[EditorInputManager] Failed to create NodeSelection for atomic fragment:', error); + log.warn('[EditorInputManager] Failed to create NodeSelection for atomic fragment:', error); } } @@ -2509,7 +2512,7 @@ export class EditorInputManager { editor.view?.dispatch(tr); this.#callbacks.scheduleSelectionUpdate?.(); } catch (error) { - console.warn('[SELECTION] Failed to extend selection on shift+click:', error); + log.warn('[SELECTION] Failed to extend selection on shift+click:', error); } this.#focusEditor(); @@ -2587,7 +2590,7 @@ export class EditorInputManager { editor.view?.dispatch(tr); this.#callbacks.scheduleSelectionUpdate?.(); } catch (error) { - console.warn('[SELECTION] Failed to extend selection during drag:', error); + log.warn('[SELECTION] Failed to extend selection during drag:', error); } } @@ -2613,7 +2616,7 @@ export class EditorInputManager { editor.view?.dispatch(tr); this.#callbacks.scheduleSelectionUpdate?.(); } catch (error) { - console.warn('[CELL-SELECTION] Failed to create CellSelection, falling back to TextSelection:', error); + log.warn('[CELL-SELECTION] Failed to create CellSelection, falling back to TextSelection:', error); // Fall back to text selection const anchor = this.#dragAnchor!; const head = hit.pos; diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorAwareness.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorAwareness.ts index f5528c44f1..ea127c61ae 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorAwareness.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorAwareness.ts @@ -4,6 +4,9 @@ import * as Y from 'yjs'; import { getFallbackCursorColor } from './RemoteCursorColors.js'; import type { RemoteCursorState } from '../types.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('remote-cursor-awareness'); /** * Minimal interface for Yjs awareness object. @@ -124,7 +127,7 @@ export function normalizeAwarenessStates(options: { updatedAt: positionChanged ? Date.now() : (previousState?.updatedAt ?? Date.now()), }); } catch (error) { - console.warn(`Failed to normalize cursor for client ${clientId}:`, error); + log.warn(`Failed to normalize cursor for client ${clientId}:`, error); } }); diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorManager.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorManager.ts index 53dc40139d..ca5e3f3e18 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorManager.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/remote-cursors/RemoteCursorManager.ts @@ -26,6 +26,9 @@ import type { } from '../types.js'; import { normalizeAwarenessStates } from './RemoteCursorAwareness.js'; import { renderRemoteCursors } from './RemoteCursorRendering.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('remote-cursor-manager'); /** * Minimal interface for collaboration provider with awareness. @@ -207,9 +210,7 @@ export class RemoteCursorManager { if (awareness) { // Warn so consumer misconfigurations (provider present but missing // event hooks) are debuggable; remote cursors stay disabled. - console.warn( - '[remote-cursors] provider.awareness is missing on/off methods; remote cursors will not be wired.', - ); + log.warn('[remote-cursors] provider.awareness is missing on/off methods; remote cursors will not be wired.'); } return; } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/selection/CellSelectionOverlay.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/selection/CellSelectionOverlay.ts index 6d24921c2e..03ff1ddd0c 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/selection/CellSelectionOverlay.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/selection/CellSelectionOverlay.ts @@ -1,6 +1,9 @@ import { TableMap } from 'prosemirror-tables'; import type { CellSelection } from 'prosemirror-tables'; import type { FlowBlock, Layout, Measure, TableBlock, TableFragment, TableMeasure } from '@superdoc/contracts'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('cell-selection-overlay'); /** * Coordinate pair in overlay space (absolute positioning within the selection overlay container). @@ -87,14 +90,14 @@ export function renderCellSelectionOverlay({ }: RenderCellSelectionOverlayDeps): void { // Validate input parameters if (!selection || !layout || !layout.pages) { - console.warn('[renderCellSelectionOverlay] Invalid input parameters'); + log.warn('[renderCellSelectionOverlay] Invalid input parameters'); return; } // Find the table node by walking up from the anchor cell const $anchorCell = selection.$anchorCell; if (!$anchorCell) { - console.warn('[renderCellSelectionOverlay] No anchor cell in selection'); + log.warn('[renderCellSelectionOverlay] No anchor cell in selection'); return; } @@ -105,7 +108,7 @@ export function renderCellSelectionOverlay({ // Validate we found a table node if (tableDepth === 0 && $anchorCell.node(0).type.name !== 'table') { - console.warn('[renderCellSelectionOverlay] Could not find table node in selection hierarchy'); + log.warn('[renderCellSelectionOverlay] Could not find table node in selection hierarchy'); return; } @@ -154,7 +157,7 @@ export function renderCellSelectionOverlay({ try { tableMap = TableMap.get(tableNode); } catch (error: unknown) { - console.error('[renderCellSelectionOverlay] TableMap.get failed:', error); + log.error('[renderCellSelectionOverlay] TableMap.get failed:', error); return; } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/selection/SelectionDebug.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/selection/SelectionDebug.ts index 252bfa08e9..7e8251328b 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/selection/SelectionDebug.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/selection/SelectionDebug.ts @@ -1,3 +1,7 @@ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('selection-debug'); + export type SelectionDebugLogLevel = 'off' | 'error' | 'warn' | 'info' | 'verbose'; export type SelectionDebugConfig = { @@ -77,9 +81,9 @@ export function debugLog( const prefix = '[Selection]'; if (data) { - console.log(prefix, message, data); + log.debug(prefix, message, data); } else { - console.log(prefix, message); + log.debug(prefix, message); } } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tables/TableSelectionUtilities.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tables/TableSelectionUtilities.ts index 107975a9d8..d1237beb15 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tables/TableSelectionUtilities.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tables/TableSelectionUtilities.ts @@ -19,6 +19,9 @@ import { type PageHit, type TableHitResult, } from '@superdoc/layout-bridge'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('table-selection-utilities'); /** * Calculates the ProseMirror document position for a table cell identified by a hit test result. @@ -60,7 +63,7 @@ export function getCellPosFromTableHit( ): number | null { // Input validation: Check for valid tableHit structure if (!tableHit || !tableHit.block || typeof tableHit.block.id !== 'string') { - console.warn('[getCellPosFromTableHit] Invalid tableHit input:', tableHit); + log.warn('[getCellPosFromTableHit] Invalid tableHit input:', tableHit); return null; } @@ -71,7 +74,7 @@ export function getCellPosFromTableHit( tableHit.cellRowIndex < 0 || tableHit.cellColIndex < 0 ) { - console.warn('[getCellPosFromTableHit] Invalid cell indices:', { + log.warn('[getCellPosFromTableHit] Invalid cell indices:', { row: tableHit.cellRowIndex, col: tableHit.cellColIndex, }); @@ -101,7 +104,7 @@ export function getCellPosFromTableHit( return true; }); } catch (error: unknown) { - console.error('[getCellPosFromTableHit] Error during document traversal:', error); + log.error('[getCellPosFromTableHit] Error during document traversal:', error); return null; } @@ -116,7 +119,7 @@ export function getCellPosFromTableHit( // Bounds check: Validate target row exists in table if (targetRowIndex >= tableNode.childCount) { - console.warn('[getCellPosFromTableHit] Target row index out of bounds:', { + log.warn('[getCellPosFromTableHit] Target row index out of bounds:', { targetRowIndex, tableChildCount: tableNode.childCount, }); @@ -155,7 +158,7 @@ export function getCellPosFromTableHit( } // Target column not found in this row (shouldn't happen in valid tables) - console.warn('[getCellPosFromTableHit] Target column not found in row:', { + log.warn('[getCellPosFromTableHit] Target column not found in row:', { targetColIndex, logicalColReached: logicalCol, rowCellCount: row.childCount, diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/CellSelectionOverlay.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/CellSelectionOverlay.test.ts index 23c22d87a4..21c752cfeb 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/CellSelectionOverlay.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/CellSelectionOverlay.test.ts @@ -586,7 +586,10 @@ describe('renderCellSelectionOverlay', () => { renderCellSelectionOverlay(deps); - expect(consoleWarnSpy).toHaveBeenCalledWith('[renderCellSelectionOverlay] No anchor cell in selection'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[cell-selection-overlay]', + '[renderCellSelectionOverlay] No anchor cell in selection', + ); consoleWarnSpy.mockRestore(); }); diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts index f61b637c50..cfa3ad4bef 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.goToAnchor.test.ts @@ -536,6 +536,7 @@ describe('PresentationEditor - goToAnchor', () => { const result = await editor.goToAnchor('bookmark1'); expect(result).toBe(true); expect(consoleWarnSpy).toHaveBeenCalledWith( + '[anchor-navigation]', expect.stringContaining('Navigation succeeded but could not move caret'), ); @@ -563,7 +564,11 @@ describe('PresentationEditor - goToAnchor', () => { const result = await editor.goToAnchor('bookmark1'); expect(result).toBe(false); - expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('goToAnchor failed'), expect.any(Error)); + expect(consoleErrorSpy).toHaveBeenCalledWith( + '[presentation-editor]', + expect.stringContaining('goToAnchor failed'), + expect.any(Error), + ); expect(errorListener).toHaveBeenCalledWith( expect.objectContaining({ error: expect.any(Error), diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts index d2dce39afd..8703788844 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.scrollToPosition.test.ts @@ -709,7 +709,10 @@ describe('PresentationEditor - scrollToPosition', () => { // The page never mounted, so it should fail expect(result).toBe(false); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('failed to mount within timeout')); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[presentation-editor]', + expect.stringContaining('failed to mount within timeout'), + ); consoleWarnSpy.mockRestore(); }); diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts index d7c5eb78e8..e9947dafae 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.test.ts @@ -4131,7 +4131,10 @@ describe('PresentationEditor', () => { }); expect(() => editor.setZoom(15)).not.toThrow(); - expect(consoleWarnSpy).toHaveBeenCalledWith(expect.stringContaining('exceeds recommended maximum')); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[presentation-editor]', + expect.stringContaining('exceeds recommended maximum'), + ); consoleWarnSpy.mockRestore(); }); diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts index 9e06c72dd1..4de85d3b4e 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/tests/PresentationEditor.zoom.test.ts @@ -446,7 +446,10 @@ describe('PresentationEditor - Zoom Functionality', () => { // MAX_ZOOM_WARNING_THRESHOLD is 10, so we need > 10 to trigger warning editor.setZoom(10.1); - expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('Zoom level 10.1 exceeds recommended maximum')); + expect(warnSpy).toHaveBeenCalledWith( + '[presentation-editor]', + expect.stringContaining('Zoom level 10.1 exceeds recommended maximum'), + ); warnSpy.mockRestore(); }); diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/utils/AnchorNavigation.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/utils/AnchorNavigation.ts index 3492c3719b..d2b4ec84b2 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/utils/AnchorNavigation.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/utils/AnchorNavigation.ts @@ -2,6 +2,9 @@ import { selectionToRects, type PageGeometryHelper } from '@superdoc/layout-brid import type { FlowBlock, Layout, Measure } from '@superdoc/contracts'; import type { Editor } from '../../Editor.js'; import { getPageElementByIndex } from '../../../dom-observer/PageDom.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('anchor-navigation'); /** * Build an anchor map (bookmark name -> page index) using fragment PM ranges. @@ -204,7 +207,7 @@ export async function goToAnchor({ if (activeEditor?.commands?.setTextSelection) { activeEditor.commands.setTextSelection({ from: pmPos, to: pmPos }); } else { - console.warn( + log.warn( '[PresentationEditor] goToAnchor: Navigation succeeded but could not move caret (editor commands unavailable)', ); } diff --git a/packages/super-editor/src/editors/v1/core/presentation-editor/utils/SafeCleanup.ts b/packages/super-editor/src/editors/v1/core/presentation-editor/utils/SafeCleanup.ts index 1c21854ffd..b59a2d3fd3 100644 --- a/packages/super-editor/src/editors/v1/core/presentation-editor/utils/SafeCleanup.ts +++ b/packages/super-editor/src/editors/v1/core/presentation-editor/utils/SafeCleanup.ts @@ -1,3 +1,7 @@ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('safe-cleanup'); + /** * Executes a cleanup function and catches any errors, logging them with context. * @@ -18,6 +22,6 @@ export function safeCleanup(fn: () => void, context: string): void { try { fn(); } catch (error) { - console.warn(`[PresentationEditor] ${context} cleanup failed:`, error); + log.warn(`[PresentationEditor] ${context} cleanup failed:`, error); } } diff --git a/packages/super-editor/src/editors/v1/core/renderers/ProseMirrorRenderer.ts b/packages/super-editor/src/editors/v1/core/renderers/ProseMirrorRenderer.ts index 476d17bd99..5defe6c477 100644 --- a/packages/super-editor/src/editors/v1/core/renderers/ProseMirrorRenderer.ts +++ b/packages/super-editor/src/editors/v1/core/renderers/ProseMirrorRenderer.ts @@ -18,6 +18,9 @@ import { canUseDOM } from '../../utils/canUseDOM.js'; import type { EditorRenderer, EditorRendererAttachParams } from './EditorRenderer.js'; import type { Editor } from '../Editor.js'; import type { EditorOptions } from '../types/EditorConfig.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('prosemirror-renderer'); /** Heading[1-9] styleId regex — paste/copy must keep these paragraph wrappers intact. */ const HEADING_STYLE_RE = /^Heading[1-9]$/i; @@ -609,7 +612,7 @@ export class ProseMirrorRenderer implements EditorRenderer { editor.fontsImported = results.fontsImported; } catch (error) { // Log error but don't crash - fonts are a progressive enhancement - console.warn('Failed to inject fonts into DOM:', error); + log.warn('Failed to inject fonts into DOM:', error); } } } @@ -936,7 +939,7 @@ export class ProseMirrorRenderer implements EditorRenderer { clipboardData.setData('text/html', embedSliceInHtml(html, sliceJson, bodySectPrJson, mediaJson)); clipboardData.setData('text/plain', this.view.state.doc.textBetween(from, to, '\n')); } catch (error) { - console.warn('Failed to transform copied content:', error); + log.warn('Failed to transform copied content:', error); } }; @@ -971,7 +974,7 @@ export class ProseMirrorRenderer implements EditorRenderer { }; } catch (error) { // Log but don't crash - dev tools are not critical - console.warn('Failed to initialize developer tools:', error); + log.warn('Failed to initialize developer tools:', error); } } } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.js b/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.js index cb8ee0b0f2..2fd65b131d 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.js @@ -35,6 +35,10 @@ import { syncBibliographyPartToPackage, getBibliographyPartExportPaths, } from './citation-sources.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter'); + const FONT_FAMILY_FALLBACKS = Object.freeze({ swiss: 'Arial, sans-serif', roman: 'Times New Roman, serif', @@ -492,13 +496,13 @@ class SuperConverter { // Add null safety for nested property structure if (!property.elements?.[0]?.elements?.[0]?.text) { - console.warn(`Malformed property structure for "${propertyName}"`); + log.warn(`Malformed property structure for "${propertyName}"`); return null; } return property.elements[0].elements[0].text; } catch (e) { - console.warn(`Error getting custom property ${propertyName}:`, e); + log.warn(`Error getting custom property ${propertyName}:`, e); return null; } } @@ -561,7 +565,7 @@ class SuperConverter { if (property && preserveExisting) { // Add null safety when returning existing value if (!property.elements?.[0]?.elements?.[0]?.text) { - console.warn(`Malformed existing property structure for "${propertyName}"`); + log.warn(`Malformed existing property structure for "${propertyName}"`); return null; } return property.elements[0].elements[0].text; @@ -610,7 +614,7 @@ class SuperConverter { // Add null safety when updating existing property if (!property.elements?.[0]?.elements?.[0]) { - console.warn(`Malformed property structure for "${propertyName}", recreating structure`); + log.warn(`Malformed property structure for "${propertyName}", recreating structure`); property.elements = [ { type: 'element', @@ -630,7 +634,7 @@ class SuperConverter { return finalValue; } catch (e) { - console.warn(`Error setting custom property ${propertyName}:`, e); + log.warn(`Error setting custom property ${propertyName}:`, e); return null; } } @@ -834,7 +838,7 @@ class SuperConverter { return `HASH-${computeCrc32Hex(data).toUpperCase()}`; } catch (e) { - console.warn('[super-converter] Could not generate content hash:', e); + log.warn('Could not generate content hash:', e); return `HASH-${uuidv4().replace(/-/g, '').substring(0, 8).toUpperCase()}`; } } @@ -1751,12 +1755,12 @@ class SuperConverter { // Deprecated methods for backward compatibility static getStoredSuperdocId(docx) { - console.warn('getStoredSuperdocId is deprecated, use getDocumentGuid instead'); + log.warn('getStoredSuperdocId is deprecated, use getDocumentGuid instead'); return SuperConverter.extractDocumentGuid(docx); } static updateDocumentVersion(docx, version) { - console.warn('updateDocumentVersion is deprecated, use setStoredSuperdocVersion instead'); + log.warn('updateDocumentVersion is deprecated, use setStoredSuperdocVersion instead'); return SuperConverter.setStoredSuperdocVersion(docx, version); } } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.test.js b/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.test.js index 4267f103c0..eabfbbf567 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.test.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/SuperConverter.test.js @@ -825,7 +825,10 @@ describe('SuperConverter Document GUID', () => { }; const value = SuperConverter.getStoredCustomProperty([docx], 'MalformedProp'); expect(value).toBeNull(); - expect(consoleWarnSpy).toHaveBeenCalledWith('Malformed property structure for "MalformedProp"'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[super-converter]', + 'Malformed property structure for "MalformedProp"', + ); consoleWarnSpy.mockRestore(); }); @@ -844,7 +847,10 @@ describe('SuperConverter Document GUID', () => { }; const value = SuperConverter.getStoredCustomProperty([docx], 'EmptyProp'); expect(value).toBeNull(); - expect(consoleWarnSpy).toHaveBeenCalledWith('Malformed property structure for "EmptyProp"'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[super-converter]', + 'Malformed property structure for "EmptyProp"', + ); consoleWarnSpy.mockRestore(); }); @@ -874,7 +880,10 @@ describe('SuperConverter Document GUID', () => { const value = SuperConverter.setStoredCustomProperty(docx, 'MalformedProp', 'NewValue', true); expect(value).toBeNull(); - expect(consoleWarnSpy).toHaveBeenCalledWith('Malformed existing property structure for "MalformedProp"'); + expect(consoleWarnSpy).toHaveBeenCalledWith( + '[super-converter]', + 'Malformed existing property structure for "MalformedProp"', + ); consoleWarnSpy.mockRestore(); }); @@ -905,6 +914,7 @@ describe('SuperConverter Document GUID', () => { const value = SuperConverter.setStoredCustomProperty(docx, 'MalformedProp', 'NewValue'); expect(value).toBe('NewValue'); expect(consoleWarnSpy).toHaveBeenCalledWith( + '[super-converter]', 'Malformed property structure for "MalformedProp", recreating structure', ); @@ -1005,6 +1015,7 @@ describe('SuperConverter Document GUID', () => { SuperConverter.updateDocumentVersion(mockDocx, '1.0.0'); expect(consoleWarnSpy).toHaveBeenCalledWith( + '[super-converter]', 'updateDocumentVersion is deprecated, use setStoredSuperdocVersion instead', ); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.js b/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.js index 9b5e653c86..c00133053b 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.js @@ -1,5 +1,8 @@ // @ts-check import { RELATIONSHIP_TYPES } from './docx-constants.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:export'); /** @typedef {import('../types.js').Editor} Editor */ /** @typedef {import('../types.js').XmlRelationshipElement} XmlRelationshipElement */ @@ -83,35 +86,33 @@ export const insertNewRelationship = (target, type, editor) => { // Check if relationship type is supported const mappedType = RELATIONSHIP_TYPES[type]; if (!mappedType) { - console.warn( - `Unsupported relationship type: ${type}. Available types: ${Object.keys(RELATIONSHIP_TYPES).join(', ')}`, - ); + log.warn(`Unsupported relationship type: ${type}. Available types: ${Object.keys(RELATIONSHIP_TYPES).join(', ')}`); return null; } // Check for existing relationship const existingRelId = findRelationshipIdFromTarget(target, editor); if (existingRelId) { - console.info(`Reusing existing relationship for target: ${target} (ID: ${existingRelId})`); + log.info(`Reusing existing relationship for target: ${target} (ID: ${existingRelId})`); return existingRelId; } // Validate document structure const docx = editor.converter?.convertedXml; if (!docx) { - console.error('No converted XML found in editor'); + log.error('No converted XML found in editor'); return null; } const documentRels = docx['word/_rels/document.xml.rels']; if (!documentRels) { - console.error('No document relationships found in the docx'); + log.error('No document relationships found in the docx'); return null; } const relationshipsTag = documentRels.elements?.find((el) => el.name === 'Relationships'); if (!relationshipsTag) { - console.error('No Relationships tag found in document relationships'); + log.error('No Relationships tag found in document relationships'); return null; } @@ -123,7 +124,7 @@ export const insertNewRelationship = (target, type, editor) => { // Generate new relationship ID const newId = getNewRelationshipId(editor); if (!newId) { - console.error('Failed to generate new relationship ID'); + log.error('Failed to generate new relationship ID'); return null; } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.test.js b/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.test.js index 174f7eae5f..7131b08c07 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.test.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/docx-helpers/document-rels.test.js @@ -1,4 +1,5 @@ import { describe, it, expect } from 'vitest'; +import { configureLogging } from '@superdoc/common/logger'; import { getNewRelationshipId, getDocumentRelationshipElements, @@ -220,11 +221,17 @@ describe('insertNewRelationship', () => { }, }; + configureLogging({ level: 'debug' }); + vi.spyOn(console, 'warn').mockImplementation(() => {}); vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(console, 'info').mockImplementation(() => {}); }); + afterEach(() => { + configureLogging({ level: 'warn' }); + }); + it('throws if target is not a non-empty string', () => { expect(() => insertNewRelationship(null, 'hyperlink', mockEditor)).toThrow(); expect(() => insertNewRelationship(123, 'hyperlink', mockEditor)).toThrow(); @@ -244,7 +251,10 @@ describe('insertNewRelationship', () => { it('returns null and warns on unsupported type', () => { const result = insertNewRelationship('foo', 'unsupportedType', mockEditor); expect(result).toBeNull(); - expect(console.warn).toHaveBeenCalledWith(expect.stringContaining('Unsupported relationship type')); + expect(console.warn).toHaveBeenCalledWith( + '[super-converter:export]', + expect.stringContaining('Unsupported relationship type'), + ); }); it('returns existing relationship if already present', () => { @@ -252,14 +262,17 @@ describe('insertNewRelationship', () => { const result = insertNewRelationship('foo', 'hyperlink', mockEditor); expect(result).toBe('rId42'); - expect(console.info).toHaveBeenCalledWith(expect.stringContaining('Reusing existing relationship for target')); + expect(console.info).toHaveBeenCalledWith( + '[super-converter:export]', + expect.stringContaining('Reusing existing relationship for target'), + ); }); it('returns null if editor.converter.convertedXml is missing', () => { const badEditor = { converter: {} }; const result = insertNewRelationship('foo', 'hyperlink', badEditor); expect(result).toBeNull(); - expect(console.error).toHaveBeenCalledWith('No converted XML found in editor'); + expect(console.error).toHaveBeenCalledWith('[super-converter:export]', 'No converted XML found in editor'); }); it('returns null if documentRels is missing', () => { @@ -270,7 +283,10 @@ describe('insertNewRelationship', () => { }; const result = insertNewRelationship('foo', 'hyperlink', badEditor); expect(result).toBeNull(); - expect(console.error).toHaveBeenCalledWith('No document relationships found in the docx'); + expect(console.error).toHaveBeenCalledWith( + '[super-converter:export]', + 'No document relationships found in the docx', + ); }); it('returns null if Relationships tag is missing', () => { @@ -286,7 +302,10 @@ describe('insertNewRelationship', () => { const result = insertNewRelationship('foo', 'hyperlink', editor); expect(result).toBeNull(); - expect(console.error).toHaveBeenCalledWith('No Relationships tag found in document relationships'); + expect(console.error).toHaveBeenCalledWith( + '[super-converter:export]', + 'No Relationships tag found in document relationships', + ); }); it('returns null if getNewRelationshipId fails', () => { diff --git a/packages/super-editor/src/editors/v1/core/super-converter/exporter.js b/packages/super-editor/src/editors/v1/core/super-converter/exporter.js index a7a3882f9b..5cdc6ff7bf 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/exporter.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/exporter.js @@ -42,6 +42,9 @@ import { translator as wTextTranslator } from '@converter/v3/handlers/w/t'; import { translator as wFootnoteReferenceTranslator } from './v3/handlers/w/footnoteReference/footnoteReference-translator.js'; import { carbonCopy } from '@core/utilities/carbonCopy.js'; import { DEFAULT_XML_DECLARATION } from './constants.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:export'); const DEFAULT_SECTION_PROPS_TWIPS = Object.freeze({ pageSize: Object.freeze({ width: '12240', height: '15840' }), @@ -262,7 +265,7 @@ export function exportSchemaToJson(params) { const entry = router[type]; if (!entry) { - console.error('No translation function found for node type:', type); + log.error('No translation function found for node type:', type); return null; } @@ -741,10 +744,10 @@ export class DocxExporter { // Validate that the first child element has valid text content if (elements.length === 0) { // Empty elements array - will be handled as self-closing tag, which is an error state - console.error(`${name} element has no child elements. Expected text node. Element will be self-closing.`); + log.error(`${name} element has no child elements. Expected text node. Element will be self-closing.`); } else if (elements[0] == null || typeof elements[0].text !== 'string') { // Invalid or missing text content - push empty string to maintain XML structure - console.error( + log.error( `${name} element's first child is missing or does not have a valid text property. ` + `Received: ${JSON.stringify(elements[0])}. Pushing empty string to maintain XML structure.`, ); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/helpers.js b/packages/super-editor/src/editors/v1/core/super-converter/helpers.js index a18381b3d8..80c3a34ceb 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/helpers.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/helpers.js @@ -1,6 +1,9 @@ import { parseSizeUnit } from '../utilities/index.js'; import { xml2js } from 'xml-js'; import { getDataUriMetadata, tryDecodeDataUriText } from './helpers/mediaHelpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter'); // --- Browser-compatible CRC32 (replaces buffer-crc32 to avoid Node.js Buffer dependency) --- const CRC32_TABLE = new Uint32Array(256); @@ -389,7 +392,7 @@ const getContentTypesFromXml = (contentTypesXml) => { .map((el) => el.attributes?.Extension) .filter(Boolean); } catch (err) { - console.warn('[super-editor] Failed to parse [Content_Types].xml', err); + log.warn('[super-editor] Failed to parse [Content_Types].xml', err); return []; } }; @@ -583,7 +586,7 @@ const deobfuscateFont = (arrayBuffer, guidHex) => { const guidStr = guidHex.replace(/[-{}]/g, ''); if (guidStr.length !== 32) { - console.error('Invalid GUID'); + log.error('Invalid GUID'); return; } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v2/importer/docxImporter.js b/packages/super-editor/src/editors/v1/core/super-converter/v2/importer/docxImporter.js index 239825d532..7a1a1bff82 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v2/importer/docxImporter.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v2/importer/docxImporter.js @@ -50,6 +50,9 @@ import { baseNumbering } from '@converter/v2/exporter/helpers/base-list.definiti import { patchNumberingDefinitions } from './patchNumberingDefinitions.js'; import { startCollection, drainDiagnostics } from '@converter/v3/handlers/import-diagnostics.js'; import { TRACKED_CHANGE_SOURCE_ID_MAP_PROPERTY } from '@extensions/track-changes/review-model/word-id-allocator.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:import'); /** * @typedef {import()} XmlNode @@ -488,14 +491,14 @@ const createNodeListHandler = (nodeHandlers) => { }); } } catch (error) { - console.debug('Import error', error); + log.debug('Import error', error); editor?.emit('exception', { error, editor }); } } return processedElements; } catch (error) { - console.debug('Error during import', error); + log.debug('Error during import', error); editor?.emit('exception', { error, editor }); throw error; diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-image-watermark-import.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-image-watermark-import.js index c662307a22..95b0109afe 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-image-watermark-import.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-image-watermark-import.js @@ -1,4 +1,7 @@ import { carbonCopy } from '@core/utilities/carbonCopy.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:import'); /** * Handles VML shape elements with v:imagedata (image watermarks). @@ -27,7 +30,7 @@ export function handleShapeImageWatermarkImport({ params, pict }) { // Extract relationship ID const rId = imagedataAttrs['r:id']; if (!rId) { - console.warn('v:imagedata missing r:id attribute'); + log.warn('v:imagedata missing r:id attribute'); return null; } @@ -41,7 +44,7 @@ export function handleShapeImageWatermarkImport({ params, pict }) { const rel = elements?.find((el) => el.attributes['Id'] === rId); if (!rel) { - console.warn(`Relationship not found for r:id="${rId}"`); + log.warn(`Relationship not found for r:id="${rId}"`); return null; } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.js index c762dce10c..c400fcb58d 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.js @@ -1,4 +1,7 @@ import { encodeUtf8Base64 } from '../../../../../../helpers/base64.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:import'); /** * Handles VML shape elements with v:textpath (text watermarks). @@ -39,7 +42,7 @@ export function handleShapeTextWatermarkImport({ pict }) { // Extract the watermark text const watermarkText = textpathAttrs['string'] || ''; if (!watermarkText) { - console.warn('v:textpath missing string attribute'); + log.warn('v:textpath missing string attribute'); return null; } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.test.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.test.js index e4f630450a..610bad3f63 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.test.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/pict/helpers/handle-shape-text-watermark-import.test.js @@ -156,7 +156,7 @@ describe('handleShapeTextWatermarkImport', () => { const result = handleShapeTextWatermarkImport({ params: {}, pict }); expect(result).toBeNull(); - expect(consoleSpy).toHaveBeenCalledWith('v:textpath missing string attribute'); + expect(consoleSpy).toHaveBeenCalledWith('[super-converter:import]', 'v:textpath missing string attribute'); consoleSpy.mockRestore(); }); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.js index 80ec7c6a04..e263cef4a2 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.js @@ -8,6 +8,9 @@ import { sanitizeHtml } from '@core/InputRule'; import { getTextNodeForExport } from '@converter/v3/handlers/w/t/helpers/translate-text-node.js'; import he from 'he'; import { translator as wHyperlinkTranslator } from '@converter/v3/handlers/w/hyperlink/index.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:export'); /** * Translate a field annotation node @@ -373,7 +376,7 @@ export function getFieldHighlightJson(fieldsHighlightColor) { const hexRegex = /^#?([A-Fa-f0-9]{3}|[A-Fa-f0-9]{4}|[A-Fa-f0-9]{6}|[A-Fa-f0-9]{8})$/; if (!hexRegex.test(parsedColor)) { - console.warn(`Invalid HEX color provided to fieldsHighlightColor export param: ${fieldsHighlightColor}`); + log.warn(`Invalid HEX color provided to fieldsHighlightColor export param: ${fieldsHighlightColor}`); return null; } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.test.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.test.js index 23c052d160..28f7a1d414 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.test.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/w/sdt/helpers/translate-field-annotation.test.js @@ -58,7 +58,7 @@ describe('getFieldHighlightJson (non-throwing)', () => { const out = getFieldHighlightJson(' '); expect(out).toBeNull(); expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toMatch(/Invalid HEX color/i); + expect(warnSpy.mock.calls[0][1]).toMatch(/Invalid HEX color/i); }); it('returns null and warns for invalid HEX formats', () => { @@ -84,7 +84,7 @@ describe('getFieldHighlightJson (non-throwing)', () => { } expect(warnSpy).toHaveBeenCalledTimes(invalid.length); for (let i = 0; i < invalid.length; i++) { - expect(warnSpy.mock.calls[i][0]).toMatch(/Invalid HEX color/i); + expect(warnSpy.mock.calls[i][1]).toMatch(/Invalid HEX color/i); } }); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js index 6be57550c8..cb7339c3bc 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.js @@ -10,6 +10,9 @@ import { generateDocxRandomId } from '@core/helpers/index.js'; import { readImageDimensionsFromDataUri } from '@converter/image-dimensions.js'; import { simpleStringHash } from '@core/utilities/hash.js'; import { isValidImageDataUrl } from '@superdoc/url-validation'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:image'); const DECORATIVE_EXT_URI = '{C183D7F6-B498-43B3-948B-1728B52AA6E4}'; const DECORATIVE_NAMESPACE = 'http://schemas.microsoft.com/office/drawing/2017/decorative'; @@ -58,7 +61,7 @@ function getMediaTargetForImageSrc(params, src) { function fallbackForMissingMediaTarget(params) { if (params.node.type === 'fieldAnnotation') return prepareTextAnnotation(params); - console.warn('Skipping image export because media target could not be resolved.', { + log.warn('Skipping image export because media target could not be resolved.', { nodeType: params.node.type, src: params.node.attrs?.src, }); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.test.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.test.js index 4034cfac60..e26ad5ef1c 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.test.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/decode-image-node-helpers.test.js @@ -347,6 +347,7 @@ describe('translateImageNode', () => { expect(baseParams.relationships).toHaveLength(0); expect(baseParams.media).toEqual({}); expect(warn).toHaveBeenCalledWith( + '[super-converter:image]', 'Skipping image export because media target could not be resolved.', expect.objectContaining({ nodeType: 'image', src: 'data:,payload' }), ); diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/metafile-converter.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/metafile-converter.js index 680d6e500e..a500941f7e 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/metafile-converter.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/metafile-converter.js @@ -22,6 +22,9 @@ import { EMFJS, WMFJS } from './rtfjs'; import { dataUriToArrayBuffer } from '../../../../helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:image'); // Disable verbose logging from the renderers EMFJS.loggingEnabled(false); @@ -741,7 +744,7 @@ export function convertEmfToSvg(data, size = {}) { try { // Check if we're in a browser environment with DOM support (or a provided mock) if (!ensureDomEnvironment()) { - console.warn('EMF conversion requires browser environment with DOM support'); + log.warn('EMF conversion requires browser environment with DOM support'); return null; } @@ -790,7 +793,7 @@ export function convertEmfToSvg(data, size = {}) { return { dataUri: svgToDataUri(svgElement), format: 'svg' }; } catch (error) { - console.warn('Failed to convert EMF to SVG:', error.message); + log.warn('Failed to convert EMF to SVG:', error.message); return null; } } @@ -806,7 +809,7 @@ export function convertWmfToSvg(data, size = {}) { try { // Check if we're in a browser environment with DOM support (or a provided mock) if (!ensureDomEnvironment()) { - console.warn('WMF conversion requires browser environment with DOM support'); + log.warn('WMF conversion requires browser environment with DOM support'); return null; } @@ -832,7 +835,7 @@ export function convertWmfToSvg(data, size = {}) { if (!svgElement || !svgElement.childNodes?.length) return null; return { dataUri: svgToDataUri(svgElement), format: 'svg' }; } catch (error) { - console.warn('Failed to convert WMF to SVG:', error.message); + log.warn('Failed to convert WMF to SVG:', error.message); return null; } } @@ -856,7 +859,7 @@ export function convertMetafileToSvg(dataUri, extension, size = {}) { return convertWmfToSvg(dataUri, size); } - console.warn(`Unsupported metafile extension: ${extension}`); + log.warn(`Unsupported metafile extension: ${extension}`); return null; } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/emfjs/Helper.ts b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/emfjs/Helper.ts index b370ca6377..3063ae48c9 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/emfjs/Helper.ts +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/emfjs/Helper.ts @@ -25,6 +25,10 @@ SOFTWARE. */ +import { createLogger } from '@superdoc/common/logger'; + +const logger = createLogger('super-converter:image'); + export class EMFJSError extends Error { constructor(message: string) { super(message); // 'Error' breaks prototype chain here @@ -325,7 +329,7 @@ export class Helper { public static log(message: string): void { if (isLoggingEnabled) { - console.log(message); + logger.debug(message); } } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/wmfjs/Helper.ts b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/wmfjs/Helper.ts index 92e7b383ec..e204631334 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/wmfjs/Helper.ts +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/rtfjs/wmfjs/Helper.ts @@ -24,6 +24,10 @@ SOFTWARE. */ +import { createLogger } from '@superdoc/common/logger'; + +const logger = createLogger('super-converter:image'); + export class WMFJSError extends Error { constructor(message: string) { super(message); // 'Error' breaks prototype chain here @@ -272,7 +276,7 @@ export class Helper { public static log(message: string): void { if (isLoggingEnabled) { - console.log(message); + logger.debug(message); } } diff --git a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/tiff-converter.js b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/tiff-converter.js index cf13e12462..16c1ead66e 100644 --- a/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/tiff-converter.js +++ b/packages/super-editor/src/editors/v1/core/super-converter/v3/handlers/wp/helpers/tiff-converter.js @@ -10,6 +10,9 @@ import * as UTIF from 'utif2'; import { dataUriToArrayBuffer } from '../../../../helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('super-converter:image'); // Optional DOM environment provided by callers (e.g., JSDOM in Node) let domEnvironment = null; @@ -94,7 +97,7 @@ export function convertTiffToPng(data) { // Render to canvas and export as PNG const canvas = createCanvas(); if (!canvas) { - console.warn('TIFF conversion requires a DOM environment with canvas support'); + log.warn('TIFF conversion requires a DOM environment with canvas support'); return null; } @@ -113,7 +116,7 @@ export function convertTiffToPng(data) { return { dataUri, format: 'png' }; } catch (error) { - console.warn('Failed to convert TIFF to PNG:', error.message); + log.warn('Failed to convert TIFF to PNG:', error.message); return null; } } diff --git a/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.js b/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.js index 2a8570175b..a83ae50f68 100644 --- a/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.js +++ b/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.js @@ -1,25 +1,24 @@ // @ts-check +import { createLogger as createBaseLogger } from '@superdoc/common/logger'; + /** - * Create special debug logger for SuperValidator and validators. + * Create a debug logger for SuperValidator and validators. + * + * Thin adapter over the shared `@superdoc/common/logger`: the validator's + * `debug` flag maps to the logger level (`debug` when on, `silent` when off), + * and nested prefixes compose into the logger namespace. + * * @param {boolean} debug * @param {string[]} [additionalPrefixes] * @returns {import('../types.js').ValidatorLogger} */ export function createLogger(debug, additionalPrefixes = []) { - const basePrefix = '[SuperValidator]'; - const style = 'color: teal; font-weight: bold;'; - - const allPrefixes = [basePrefix, ...additionalPrefixes.map((p) => `[${p}]`)]; - const format = allPrefixes.map(() => '%c%s').join(' '); - const styledPrefixes = allPrefixes.map((p) => [style, p]).flat(); + const namespace = ['SuperValidator', ...additionalPrefixes.map(String)].join(':'); + const base = createBaseLogger(namespace, { level: debug ? 'debug' : 'silent' }); return { - debug: (...args) => { - if (!debug) return; - console.debug(format, ...styledPrefixes, ...args); - }, - + debug: (...args) => base.debug(...args), withPrefix: (prefix) => createLogger(debug, [...additionalPrefixes, prefix]), }; } diff --git a/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.test.js b/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.test.js index d61695f0a0..d509118115 100644 --- a/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.test.js +++ b/packages/super-editor/src/editors/v1/core/super-validator/logger/logger.test.js @@ -1,6 +1,10 @@ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { createLogger } from './logger.js'; +// The validator logger is a thin adapter over @superdoc/common/logger. When +// `debug` is true it logs at the shared logger's `debug` level, whose console +// sink writes via console.debug and prepends a single `[namespace]` prefix. +// Nested prefixes compose into the namespace as `[SuperValidator:Prefix:...]`. describe('createLogger', () => { let consoleDebugSpy; @@ -18,84 +22,47 @@ describe('createLogger', () => { expect(consoleDebugSpy).not.toHaveBeenCalled(); }); - it('logs with correct format and styles when debug is true', () => { + it('logs with the SuperValidator prefix when debug is true', () => { const logger = createLogger(true); logger.debug('hello', 'world'); expect(consoleDebugSpy).toHaveBeenCalledTimes(1); - - const [format, ...rest] = consoleDebugSpy.mock.calls[0]; - expect(format).toBe('%c%s'); - - const [style, prefix, ...args] = rest; - expect(style).toBe('color: teal; font-weight: bold;'); - expect(prefix).toBe('[SuperValidator]'); - expect(args).toEqual(['hello', 'world']); + expect(consoleDebugSpy).toHaveBeenCalledWith('[SuperValidator]', 'hello', 'world'); }); - it('adds additional prefix correctly', () => { + it('composes an additional prefix into the namespace', () => { const logger = createLogger(true, ['MyValidator']); logger.debug('test'); - expect(consoleDebugSpy).toHaveBeenCalledTimes(1); - - const [format, ...rest] = consoleDebugSpy.mock.calls[0]; - expect(format).toBe('%c%s %c%s'); - - const [style1, prefix1, style2, prefix2, ...args] = rest; - expect(style1).toBe('color: teal; font-weight: bold;'); - expect(prefix1).toBe('[SuperValidator]'); - expect(style2).toBe('color: teal; font-weight: bold;'); - expect(prefix2).toBe('[MyValidator]'); - expect(args).toEqual(['test']); + expect(consoleDebugSpy).toHaveBeenCalledWith('[SuperValidator:MyValidator]', 'test'); }); it('allows chaining withPrefix to add more prefixes', () => { const logger = createLogger(true).withPrefix('ValidatorA').withPrefix('Nested'); logger.debug('deep'); - expect(consoleDebugSpy).toHaveBeenCalledTimes(1); - - const [format, ...rest] = consoleDebugSpy.mock.calls[0]; - expect(format).toBe('%c%s %c%s %c%s'); - - const styled = rest.slice(0, 6); - const expectedPrefixes = ['[SuperValidator]', '[ValidatorA]', '[Nested]']; - for (let i = 0; i < styled.length; i += 2) { - expect(styled[i]).toBe('color: teal; font-weight: bold;'); - expect(styled[i + 1]).toBe(expectedPrefixes[i / 2]); - } - - expect(rest.slice(6)).toEqual(['deep']); + expect(consoleDebugSpy).toHaveBeenCalledWith('[SuperValidator:ValidatorA:Nested]', 'deep'); }); it('stringifies non-string prefixes', () => { const logger = createLogger(true, [123, null, undefined]); logger.debug('mixed'); - const [format, ...rest] = consoleDebugSpy.mock.calls[0]; - expect(format).toBe('%c%s %c%s %c%s %c%s'); - expect(rest).toContain('[123]'); - expect(rest).toContain('[null]'); - expect(rest).toContain('[undefined]'); + expect(consoleDebugSpy).toHaveBeenCalledWith('[SuperValidator:123:null:undefined]', 'mixed'); }); it('works with no additionalPrefixes provided', () => { const logger = createLogger(true); logger.debug('only base'); - const [format, ...rest] = consoleDebugSpy.mock.calls[0]; - expect(format).toBe('%c%s'); - expect(rest).toContain('[SuperValidator]'); + expect(consoleDebugSpy).toHaveBeenCalledWith('[SuperValidator]', 'only base'); }); it('handles empty debug call gracefully', () => { const logger = createLogger(true); logger.debug(); - const [format, ...rest] = consoleDebugSpy.mock.calls[0]; - expect(format).toBe('%c%s'); - expect(rest).toEqual(['color: teal; font-weight: bold;', '[SuperValidator]']); + expect(consoleDebugSpy).toHaveBeenCalledWith('[SuperValidator]'); }); it('does not log from chained logger if debug is false', () => { diff --git a/packages/super-editor/src/editors/v1/core/utilities/carbonCopy.js b/packages/super-editor/src/editors/v1/core/utilities/carbonCopy.js index 0d561a2886..ed0c291cca 100644 --- a/packages/super-editor/src/editors/v1/core/utilities/carbonCopy.js +++ b/packages/super-editor/src/editors/v1/core/utilities/carbonCopy.js @@ -1,3 +1,7 @@ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('carbon-copy'); + /** * * @template T @@ -9,7 +13,7 @@ export const carbonCopy = (obj) => { try { return JSON.parse(JSON.stringify(obj)); } catch (e) { - console.error('Error in carbonCopy', obj, e); + log.error('Error in carbonCopy', obj, e); return undefined; } }; diff --git a/packages/super-editor/src/editors/v1/core/utilities/clipboardUtils.js b/packages/super-editor/src/editors/v1/core/utilities/clipboardUtils.js index b501a833a4..21dafbd2e1 100644 --- a/packages/super-editor/src/editors/v1/core/utilities/clipboardUtils.js +++ b/packages/super-editor/src/editors/v1/core/utilities/clipboardUtils.js @@ -2,6 +2,9 @@ // clipboardUtils.js import { DOMParser } from 'prosemirror-model'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('clipboard-utils'); /** * @typedef {import('prosemirror-state').EditorState} EditorState @@ -112,7 +115,7 @@ export async function readFromClipboard(state) { new window.DOMParser().parseFromString(`${html}`, 'text/html').body, ).content; } catch (e) { - console.error('error parsing html', e); + log.error('error parsing html', e); // fallback to text content = state.schema.text(text); } diff --git a/packages/super-editor/src/editors/v1/dev/components/DeveloperPlayground.vue b/packages/super-editor/src/editors/v1/dev/components/DeveloperPlayground.vue index 5834e158c3..5a1550fff2 100644 --- a/packages/super-editor/src/editors/v1/dev/components/DeveloperPlayground.vue +++ b/packages/super-editor/src/editors/v1/dev/components/DeveloperPlayground.vue @@ -13,6 +13,9 @@ import { SuperToolbar } from '@components/toolbar/super-toolbar'; import { PaginationPluginKey } from '@extensions/pagination/pagination-helpers.js'; import BasicUpload from './BasicUpload.vue'; import BlankDOCX from '@superdoc/common/data/blank.docx?url'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('dev'); // Import the component the same you would in your app let activeEditor; @@ -34,9 +37,9 @@ const handleNewFile = async (file) => { }; const onCreate = ({ editor }) => { - console.debug('[Dev] Editor created', editor); - console.debug('[Dev] Page styles (pixels)', editor.getPageStyles()); - console.debug('[Dev] document styles', editor.converter?.getDocumentDefaultStyles()); + log.debug('Editor created', editor); + log.debug('Page styles (pixels)', editor.getPageStyles()); + log.debug('document styles', editor.converter?.getDocumentDefaultStyles()); pageStyles.value = editor.converter?.pageStyles; activeEditor = editor; @@ -60,7 +63,7 @@ const onCreate = ({ editor }) => { }; const onCommentClicked = ({ conversation }) => { - console.debug('💬 [Dev] Comment active', conversation); + log.debug('💬 [Dev] Comment active', conversation); }; const user = { @@ -85,7 +88,7 @@ const editorOptions = computed(() => { }); const onCommentsLoaded = ({ comments }) => { - console.debug('💬 [Dev] Comments loaded', comments); + log.debug('💬 [Dev] Comments loaded', comments); }; const exportDocx = async () => { @@ -100,15 +103,15 @@ const exportDocx = async () => { const attachAnnotationEventHandlers = () => { activeEditor?.on('fieldAnnotationClicked', (params) => { - console.log('fieldAnnotationClicked', { params }); + log.debug('fieldAnnotationClicked', { params }); }); activeEditor?.on('fieldAnnotationSelected', (params) => { - console.log('fieldAnnotationSelected', { params }); + log.debug('fieldAnnotationSelected', { params }); }); activeEditor?.on('fieldAnnotationDeleted', (params) => { - console.log('fieldAnnotationDeleted', { params }); + log.debug('fieldAnnotationDeleted', { params }); }); }; @@ -125,7 +128,7 @@ const debugPageStyle = computed(() => { const injectContent = () => { if (!activeEditor || !contentInput.value.trim()) { - console.warn('[Dev] No editor instance or empty content'); + log.warn('No editor instance or empty content'); return; } @@ -137,10 +140,10 @@ const injectContent = () => { contentType: contentType.value, // 'html', 'markdown', or 'text' }); - console.debug(`[Dev] ${contentType.value} content injected successfully`); + log.debug(`${contentType.value} content injected successfully`); contentInput.value = ''; } catch (error) { - console.error('[Dev] Failed to inject content:', error); + log.error('Failed to inject content:', error); } finally { isInjectingContent.value = false; } diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/helpers/header-footer-slot-materialization.ts b/packages/super-editor/src/editors/v1/document-api-adapters/helpers/header-footer-slot-materialization.ts index 70e128df98..e137ab9efc 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/helpers/header-footer-slot-materialization.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/helpers/header-footer-slot-materialization.ts @@ -24,8 +24,11 @@ import { compoundMutation } from '../../core/parts/mutation/compound-mutation.js import { removePart, hasPart } from '../../core/parts/store/part-store.js'; import { removeInvalidationHandler } from '../../core/parts/invalidation/part-invalidation-registry.js'; import type { PartId } from '../../core/parts/types.js'; +import { createLogger } from '@superdoc/common/logger'; export { normalizeVariant } from '../../core/presentation-editor/header-footer/header-footer-variant.js'; +const log = createLogger('header-footer-slot-materialization'); + // --------------------------------------------------------------------------- // Types // --------------------------------------------------------------------------- @@ -98,7 +101,7 @@ export function ensureExplicitHeaderFooterSlot( const sections = resolveSectionProjections(editor); const projection = sections.find((s) => s.sectionId === sectionId); if (!projection) { - console.warn(`[header-footer-slot-materialization] Section "${sectionId}" not found.`); + log.warn(`Section "${sectionId}" not found.`); return null; } @@ -177,7 +180,7 @@ export function ensureExplicitHeaderFooterSlot( }); if (!mutationResult.success) { - console.warn('[header-footer-slot-materialization] Materialization failed, state rolled back.'); + log.warn('Materialization failed, state rolled back.'); return null; } diff --git a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts index 20d0439a40..4e10e6dd98 100644 --- a/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts +++ b/packages/super-editor/src/editors/v1/document-api-adapters/plan-engine/executor.ts @@ -65,6 +65,9 @@ import { TrackFormatMarkName, TrackInsertMarkName, } from '../../extensions/track-changes/constants.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('text-rewrite'); // --------------------------------------------------------------------------- // Character-offset → document-position mapping @@ -165,7 +168,7 @@ const DEBUG_TEXT_REWRITE = function debugTextRewrite(message: string, details?: Record): void { if (!DEBUG_TEXT_REWRITE) return; - console.error('[text-rewrite]', message, details ?? {}); + log.error(message, details ?? {}); } type StructuredTextPayload = { diff --git a/packages/super-editor/src/editors/v1/dom-observer/DomPointerMapping.ts b/packages/super-editor/src/editors/v1/dom-observer/DomPointerMapping.ts index 226180eed8..c2d28ca6b7 100644 --- a/packages/super-editor/src/editors/v1/dom-observer/DomPointerMapping.ts +++ b/packages/super-editor/src/editors/v1/dom-observer/DomPointerMapping.ts @@ -17,6 +17,9 @@ */ import { DOM_CLASS_NAMES, STRUCTURED_CONTENT_CHROME_LABEL_CLASS_NAMES } from '@superdoc/dom-contract'; +import { createLogger } from '@superdoc/common/logger'; + +const logger = createLogger('DOM-MAP'); // --------------------------------------------------------------------------- // Debug logging (disabled by default — flip to true for click-mapping traces) @@ -24,7 +27,7 @@ import { DOM_CLASS_NAMES, STRUCTURED_CONTENT_CHROME_LABEL_CLASS_NAMES } from '@s const DEBUG = false; const log = (...args: unknown[]) => { - if (DEBUG) console.log('[DOM-MAP]', ...args); + if (DEBUG) logger.debug(...args); }; // --------------------------------------------------------------------------- diff --git a/packages/super-editor/src/editors/v1/extensions/block-node/block-node.js b/packages/super-editor/src/editors/v1/extensions/block-node/block-node.js index e39a2dc5b7..507e5d3703 100644 --- a/packages/super-editor/src/editors/v1/extensions/block-node/block-node.js +++ b/packages/super-editor/src/editors/v1/extensions/block-node/block-node.js @@ -6,6 +6,9 @@ import { Plugin, PluginKey } from 'prosemirror-state'; import { ReplaceStep, ReplaceAroundStep, AddMarkStep, RemoveMarkStep } from 'prosemirror-transform'; import { v4 as uuidv4 } from 'uuid'; import { ySyncPluginKey } from 'y-prosemirror'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('block-node'); const { findChildren } = helpers; const SD_BLOCK_ID_ATTRIBUTE_NAME = 'sdBlockId'; @@ -409,7 +412,7 @@ export const BlockNode = Extension.create({ updateNodeAt(node, pos); }); } catch (error) { - console.warn('Block node plugin: nodesBetween failed, falling back to full traversal', error); + log.warn('Block node plugin: nodesBetween failed, falling back to full traversal', error); shouldFallbackToFullTraversal = true; break; } diff --git a/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.js b/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.js index 8ee67e36db..c5e6181af7 100644 --- a/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.js +++ b/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.js @@ -9,6 +9,9 @@ import { import { bootstrapPartSync } from './part-sync/index.js'; import { seedPartsFromEditor } from './part-sync/seed-parts.js'; import { normalizeYjsFragmentEventsForSchema, normalizeYjsFragmentForSchema } from './normalize-yjs-fragment.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('collaboration'); export const CollaborationPluginKey = new PluginKey('collaboration'); const headlessBindingStateByEditor = new WeakMap(); @@ -431,7 +434,7 @@ const initHeadlessBinding = (editor) => { const syncState = ySyncPluginKey.getState(editor.state); if (!syncState?.binding) { if (!state.warnedMissingBinding) { - console.warn('[Collaboration] Headless binding init: no sync state or binding found'); + log.warn('Headless binding init: no sync state or binding found'); state.warnedMissingBinding = true; } return null; diff --git a/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.test.js b/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.test.js index 73f5ae57fe..ded15b65e7 100644 --- a/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.test.js +++ b/packages/super-editor/src/editors/v1/extensions/collaboration/collaboration.test.js @@ -686,7 +686,10 @@ describe('collaboration extension', () => { Collaboration.config.addPmPlugins.call(context); expect(() => Collaboration.config.onCreate.call(context)).not.toThrow(); - expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining('no sync state or binding found')); + expect(consoleSpy).toHaveBeenCalledWith( + '[collaboration]', + expect.stringContaining('no sync state or binding found'), + ); consoleSpy.mockRestore(); }); diff --git a/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/bootstrap.ts b/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/bootstrap.ts index 1051767a2e..184a4d305a 100644 --- a/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/bootstrap.ts +++ b/packages/super-editor/src/editors/v1/extensions/collaboration/part-sync/bootstrap.ts @@ -38,6 +38,9 @@ import { ensureHeaderFooterDescriptor, isHeaderFooterPartId, } from '../../../core/parts/adapters/header-footer-part-descriptor.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('part-sync'); // --------------------------------------------------------------------------- // Public API @@ -90,7 +93,7 @@ export function bootstrapPartSync(editor: Editor, ydoc: Y.Doc): PartSyncHandle { ), editor, }); - console.error('[part-sync] Migration failed — entering degraded mode:', result.error); + log.error('Migration failed — entering degraded mode:', result.error); return createNoopHandle(); } } @@ -99,7 +102,7 @@ export function bootstrapPartSync(editor: Editor, ydoc: Y.Doc): PartSyncHandle { if (!capabilityActive && hasNonDocumentEntries(partsMap)) { backfillCapability(metaMap, ydoc); capabilityActive = true; - console.info('[part-sync] Backfilled partsCapability marker for existing parts data'); + log.info('Backfilled partsCapability marker for existing parts data'); } // Step 4: No parts, no meta.docx — seed from local converter. @@ -119,8 +122,8 @@ export function bootstrapPartSync(editor: Editor, ydoc: Y.Doc): PartSyncHandle { reason: 'existing-room-no-parts', failures: ['Room has shared document content from remote clients but no parts data — cannot seed safely'], }); - console.warn( - '[part-sync] Degraded: room has Y fragment content with remote client state but no parts/meta.docx.' + + log.warn( + 'Degraded: room has Y fragment content with remote client state but no parts/meta.docx.' + ' Skipping local seed to avoid publishing non-authoritative data.', ); return createNoopHandle(); @@ -128,7 +131,7 @@ export function bootstrapPartSync(editor: Editor, ydoc: Y.Doc): PartSyncHandle { seedPartsFromEditor(editor, ydoc); capabilityActive = true; - console.info('[part-sync] Seeded parts from local converter'); + log.info('Seeded parts from local converter'); } // Step 5: Register header/footer descriptors before hydration @@ -153,7 +156,7 @@ export function bootstrapPartSync(editor: Editor, ydoc: Y.Doc): PartSyncHandle { ), editor, }); - console.error('[part-sync] Degraded mode — publisher/consumer NOT activated:', hydration.failures); + log.error('Degraded mode — publisher/consumer NOT activated:', hydration.failures); return createNoopHandle(); } @@ -221,7 +224,7 @@ function hydrateFromPartsMap(editor: Editor, ydoc: Y.Doc, partsMap: Y.Map 0) { - console.error('[part-sync] Critical part hydration failures:', criticalFailures); + log.error('Critical part hydration failures:', criticalFailures); return { ok: false, failures: criticalFailures }; } @@ -282,7 +285,7 @@ function hydrateFromPartsMap(editor: Editor, ydoc: Y.Doc, partsMap: Y.Map { const result = command({ editor }); expect(result).toBe(false); - expect(warnSpy).toHaveBeenCalledWith('addCommentReply requires a parentId'); + expect(warnSpy).toHaveBeenCalledWith('[comments]', 'addCommentReply requires a parentId'); expect(editor.emit).not.toHaveBeenCalled(); warnSpy.mockRestore(); diff --git a/packages/super-editor/src/editors/v1/extensions/comment/comments.test.js b/packages/super-editor/src/editors/v1/extensions/comment/comments.test.js index c9c08bfcd0..c6c5e782a2 100644 --- a/packages/super-editor/src/editors/v1/extensions/comment/comments.test.js +++ b/packages/super-editor/src/editors/v1/extensions/comment/comments.test.js @@ -882,6 +882,7 @@ describe('comments plugin commands', () => { expect(result).toBe(false); expect(warnSpy).toHaveBeenCalledWith( + '[comments]', 'addComment requires a text selection. Please select text before adding a comment.', ); expect(dispatch).not.toHaveBeenCalled(); diff --git a/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.js b/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.js index 9a8387030b..ecbecb6167 100644 --- a/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.js +++ b/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.js @@ -1,6 +1,9 @@ import { Plugin, PluginKey } from 'prosemirror-state'; import { Extension } from '@core/Extension.js'; import { getEditorSurfaceElement, getSurfaceRelativePoint } from '../../core/helpers/editorSurface.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('context-menu'); /** * Find the nearest ancestor element that creates a containing block for `position: fixed`. @@ -69,7 +72,7 @@ export function findContainingBlockAncestor(element) { } } catch (error) { // Element may be detached from DOM or otherwise invalid - console.warn('ContextMenu: Failed to get computed style for element', current, error); + log.warn('Failed to get computed style for element', current, error); // Continue checking parent elements } @@ -210,7 +213,7 @@ export const ContextMenu = Extension.create({ case 'open': { // Validate position if (typeof meta.pos !== 'number' || meta.pos < 0 || meta.pos > tr.doc.content.size) { - console.warn('ContextMenu: Invalid position', meta.pos); + log.warn('Invalid position', meta.pos); return ensureStateShape(value); } @@ -233,7 +236,7 @@ export const ContextMenu = Extension.create({ left = rect.left + relativePoint.left; top = rect.top + relativePoint.top; } catch (error) { - console.warn('ContextMenu: Failed to get surface bounds', error); + log.warn('Failed to get surface bounds', error); return ensureStateShape(value); } } else if (surface) { @@ -244,7 +247,7 @@ export const ContextMenu = Extension.create({ left = rect.left; top = rect.top; } catch (error) { - console.warn('ContextMenu: Failed to get surface bounds for fallback', error); + log.warn('Failed to get surface bounds for fallback', error); return ensureStateShape(value); } } @@ -294,7 +297,7 @@ export const ContextMenu = Extension.create({ left += containingBlock.scrollLeft || 0; top += containingBlock.scrollTop || 0; } catch (error) { - console.warn('ContextMenu: Failed to adjust for containing block', error); + log.warn('Failed to adjust for containing block', error); } } diff --git a/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.test.js b/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.test.js index 58bba62567..0d931ac887 100644 --- a/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.test.js +++ b/packages/super-editor/src/editors/v1/extensions/context-menu/context-menu.test.js @@ -491,7 +491,7 @@ describe('ContextMenu extension', () => { const pluginState = ContextMenuPluginKey.getState(view.state); expect(pluginState.open).toBe(false); // Should remain closed due to error - expect(consoleWarnSpy).toHaveBeenCalledWith('ContextMenu: Failed to get surface bounds', expect.any(Error)); + expect(consoleWarnSpy).toHaveBeenCalledWith('[context-menu]', 'Failed to get surface bounds', expect.any(Error)); consoleWarnSpy.mockRestore(); }); @@ -715,7 +715,7 @@ describe('ContextMenu extension', () => { const pluginState = ContextMenuPluginKey.getState(view.state); expect(pluginState.open).toBe(false); // Should remain closed - expect(consoleWarnSpy).toHaveBeenCalledWith('ContextMenu: Invalid position', 99999); + expect(consoleWarnSpy).toHaveBeenCalledWith('[context-menu]', 'Invalid position', 99999); consoleWarnSpy.mockRestore(); }); @@ -1583,7 +1583,8 @@ describe('findContainingBlockAncestor', () => { const result = findContainingBlockAncestor(child); expect(result).toBe(null); expect(consoleWarnSpy).toHaveBeenCalledWith( - 'ContextMenu: Failed to get computed style for element', + '[context-menu]', + 'Failed to get computed style for element', expect.any(Object), expect.any(Error), ); @@ -1621,7 +1622,8 @@ describe('findContainingBlockAncestor', () => { const result = findContainingBlockAncestor(child); expect(result).toBe(grandparent); expect(consoleWarnSpy).toHaveBeenCalledWith( - 'ContextMenu: Failed to get computed style for element', + '[context-menu]', + 'Failed to get computed style for element', parent, expect.any(Error), ); diff --git a/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationPlugin.js b/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationPlugin.js index ba833f97bc..2c4b6cfbe5 100644 --- a/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationPlugin.js +++ b/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationPlugin.js @@ -2,6 +2,9 @@ import { Plugin, PluginKey } from 'prosemirror-state'; import { mergeRanges, clampRange } from '@utils/rangeUtils.js'; import { trackFieldAnnotationsDeletion } from './fieldAnnotationHelpers/trackFieldAnnotationsDeletion.js'; import { getAllFieldAnnotations } from './fieldAnnotationHelpers/getAllFieldAnnotations.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('field-annotation'); /** * Creates a ProseMirror plugin for managing field annotations. @@ -176,7 +179,7 @@ export const FieldAnnotationPlugin = (options = {}) => { } }); } catch (error) { - console.warn('FieldAnnotationPlugin: range check failed, assuming annotations exist', error); + log.warn('FieldAnnotationPlugin: range check failed, assuming annotations exist', error); // If range check fails, assume there might be annotations and continue to main logic hasExistingAnnotations = true; break; @@ -226,7 +229,7 @@ export const FieldAnnotationPlugin = (options = {}) => { } }); } catch (error) { - console.warn('FieldAnnotationPlugin: nodesBetween failed, falling back to full scan', error); + log.warn('FieldAnnotationPlugin: nodesBetween failed, falling back to full scan', error); // Range-based scan failed due to document structure changes, fall back to full scan shouldFallbackToFullScan = true; break; diff --git a/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationView.js b/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationView.js index 734ce4c311..578f4ddcc3 100644 --- a/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationView.js +++ b/packages/super-editor/src/editors/v1/extensions/field-annotation/FieldAnnotationView.js @@ -1,5 +1,8 @@ import { Attribute } from '@core/Attribute.js'; import { NodeSelection } from 'prosemirror-state'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('field-annotation'); export class FieldAnnotationView { editor; @@ -144,7 +147,7 @@ export class FieldAnnotationView { }); rawHtml = childEditor.view.dom.innerHTML; } catch (error) { - console.warn('Error parsing HTML in FieldAnnotationView:', error); + log.warn('Error parsing HTML in FieldAnnotationView:', error); } } diff --git a/packages/super-editor/src/editors/v1/extensions/field-annotation/field-annotation.js b/packages/super-editor/src/editors/v1/extensions/field-annotation/field-annotation.js index 94b764feb5..7fe4ad5cc0 100644 --- a/packages/super-editor/src/editors/v1/extensions/field-annotation/field-annotation.js +++ b/packages/super-editor/src/editors/v1/extensions/field-annotation/field-annotation.js @@ -13,6 +13,9 @@ import { NodeSelection, Selection } from 'prosemirror-state'; import { generateDocxRandomId } from '../../core/helpers/index.js'; import { commands as cleanupCommands } from './cleanup-commands/index.js'; import { isHeadless } from '@utils/headless-helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('field-annotation'); export const fieldAnnotationName = 'fieldAnnotation'; export const annotationClass = 'annotation'; @@ -102,7 +105,7 @@ export const FieldAnnotation = Node.create({ if (!isHtmlType) return null; return JSON.parse(elem.getAttribute('data-raw-html')); } catch (e) { - console.warn('Paste parse error', e); + log.warn('Paste parse error', e); } return null; }, diff --git a/packages/super-editor/src/editors/v1/extensions/field-update/field-update.js b/packages/super-editor/src/editors/v1/extensions/field-update/field-update.js index 2c05458f75..f13fc30e87 100644 --- a/packages/super-editor/src/editors/v1/extensions/field-update/field-update.js +++ b/packages/super-editor/src/editors/v1/extensions/field-update/field-update.js @@ -6,6 +6,9 @@ import { resolveDocumentStatFieldValue, resolveMainBodyEditor, } from '../../document-api-adapters/helpers/word-statistics.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('field-update'); /** Stat-field types refreshed by F9 when the doc has no TOCs. */ const UPDATABLE_FIELD_TYPES = new Set(['NUMWORDS', 'NUMCHARS', 'NUMPAGES']); @@ -72,7 +75,7 @@ export const FieldUpdate = Extension.create({ mode: 'all', }); } catch (error) { - console.warn('[FieldUpdate] toc.update failed for', sdBlockId, error); + log.warn('[FieldUpdate] toc.update failed for', sdBlockId, error); } } diff --git a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/handleUrl.js b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/handleUrl.js index 90182b4ee8..49b3eace95 100644 --- a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/handleUrl.js +++ b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/handleUrl.js @@ -2,6 +2,10 @@ * Handles URL to File conversion with comprehensive CORS error handling */ +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('image'); + /** * Converts a URL to a File object with proper CORS error handling * @param {string} url - The image URL to fetch @@ -22,7 +26,7 @@ export const urlToFile = async (url, filename, mimeType) => { }); if (!response.ok) { - console.warn(`Failed to fetch image from ${url}: ${response.status} ${response.statusText}`); + log.warn(`Failed to fetch image from ${url}: ${response.status} ${response.statusText}`); return null; } @@ -37,11 +41,11 @@ export const urlToFile = async (url, filename, mimeType) => { return new File([blob], finalFilename, { type: finalMimeType }); } catch (error) { if (isCorsError(error)) { - console.warn(`CORS policy prevents accessing image from ${url}:`, error.message); + log.warn(`CORS policy prevents accessing image from ${url}:`, error.message); return null; } - console.error(`Error fetching image from ${url}:`, error); + log.error(`Error fetching image from ${url}:`, error); return null; } }; diff --git a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/imageRegistrationPlugin.js b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/imageRegistrationPlugin.js index 80f6aadc7a..e0a9365e82 100644 --- a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/imageRegistrationPlugin.js +++ b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/imageRegistrationPlugin.js @@ -7,6 +7,9 @@ import { checkAndProcessImage, MAX_IMAGE_FILE_BYTES, uploadAndInsertImage } from import { buildMediaPath, ensureUniqueFileName } from './fileNameUtils.js'; import { addImageRelationship } from '@extensions/image/imageHelpers/startImageUpload.js'; import { getDataUriMetadata, isRelativeUrl, isValidImageDataUrl, tryDecodeDataUriText } from '@superdoc/url-validation'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('image'); const key = new PluginKey('ImageRegistration'); /** @@ -495,7 +498,7 @@ const registerRelativeImages = async (images, editor, view) => { } } } catch (error) { - console.error(`Error registering relative image ${src}:`, error); + log.error(`Error registering relative image ${src}:`, error); } finally { pendingRelativeRegistrations.delete(src); } @@ -516,7 +519,7 @@ const registerImages = async (foundImages, editor, view) => { // Download image first, create fileobject, then proceed with registration. file = await urlToFile(src); } else { - console.warn(`Image URL ${src} is not accessible due to CORS or other restrictions. Using original URL.`); + log.warn(`Image URL ${src} is not accessible due to CORS or other restrictions. Using original URL.`); // Fallback: Remove the placeholder. const tr = view.state.tr; removeImagePlaceholder(view.state, tr, id); @@ -526,7 +529,7 @@ const registerImages = async (foundImages, editor, view) => { } else if (src.startsWith('data:')) { file = base64ToFile(src); } else { - console.error(`Unsupported image source: ${src}`); + log.error(`Unsupported image source: ${src}`); } if (!file) { @@ -557,7 +560,7 @@ const registerImages = async (foundImages, editor, view) => { await uploadAndInsertImage({ editor, view, file: process.file, size: process.size, id }); } } catch (error) { - console.error(`Error processing image from ${src}:`, error); + log.error(`Error processing image from ${src}:`, error); // Ensure placeholder is removed even on error const tr = view.state.tr; removeImagePlaceholder(view.state, tr, id); diff --git a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.js b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.js index 5d7fa062fb..83d04f5b64 100644 --- a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.js +++ b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.js @@ -5,6 +5,9 @@ import { buildMediaPath, ensureUniqueFileName } from './fileNameUtils.js'; import { generateDocxRandomId } from '@core/helpers/index.js'; import { findOrCreateRelationship } from '@core/parts/adapters/relationships-mutation.js'; import { resolveHeaderFooterRelsPartIdFromRefId } from '@core/parts/adapters/header-footer-sync.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('image'); export const MAX_IMAGE_FILE_BYTES = 5 * 1024 * 1024; @@ -27,7 +30,7 @@ export const checkAndProcessImage = async ({ getMaxContentSize, file }) => { const process = processedImageResult; return { file: process.file, size: { width: process.width, height: process.height } }; } catch (err) { - console.warn('Error processing image:', err); + log.warn('Error processing image:', err); return { file: null, size: { width: 0, height: 0 } }; } }; diff --git a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.test.js b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.test.js index 813a66508f..77bf855bd8 100644 --- a/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.test.js +++ b/packages/super-editor/src/editors/v1/extensions/image/imageHelpers/startImageUpload.test.js @@ -71,7 +71,7 @@ describe('checkAndProcessImage', () => { file, }); - expect(consoleSpy).toHaveBeenCalledWith('Error processing image:', expect.any(Error)); + expect(consoleSpy).toHaveBeenCalledWith('[image]', 'Error processing image:', expect.any(Error)); expect(result).toEqual({ file: null, size: { width: 0, height: 0 } }); consoleSpy.mockRestore(); diff --git a/packages/super-editor/src/editors/v1/extensions/pagination/pagination-helpers.js b/packages/super-editor/src/editors/v1/extensions/pagination/pagination-helpers.js index 29b4217b72..24ea250378 100644 --- a/packages/super-editor/src/editors/v1/extensions/pagination/pagination-helpers.js +++ b/packages/super-editor/src/editors/v1/extensions/pagination/pagination-helpers.js @@ -4,6 +4,9 @@ import { exportSubEditorToPart } from '@core/parts/adapters/header-footer-sync.j import { createStoryEditor } from '@core/story-editor-factory.js'; import { applyStyleIsolationClass } from '@utils/styleIsolation.js'; import { isHeadless } from '@utils/headless-helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('pagination'); export const PaginationPluginKey = new PluginKey('paginationPlugin'); @@ -195,7 +198,7 @@ export const createHeaderFooterEditor = ({ editorHost.appendChild(editorContainer); } else { // Fallback to body for backward compatibility (should not happen in new code) - console.warn('[createHeaderFooterEditor] No editorHost provided, falling back to document.body'); + log.warn('[createHeaderFooterEditor] No editorHost provided, falling back to document.body'); document.body.appendChild(editorContainer); } @@ -249,7 +252,7 @@ export const broadcastEditorEvents = (editor, sectionEditor) => { eventNames.forEach((eventName) => { sectionEditor.on(eventName, (...args) => { editor.emit(eventName, ...args); - console.debug('broadcastEditorEvents', { eventName, args }); + log.debug('broadcastEditorEvents', { eventName, args }); }); }); }; diff --git a/packages/super-editor/src/editors/v1/extensions/shape-group/ShapeGroupView.js b/packages/super-editor/src/editors/v1/extensions/shape-group/ShapeGroupView.js index 0b85757946..8f9b1b58b8 100644 --- a/packages/super-editor/src/editors/v1/extensions/shape-group/ShapeGroupView.js +++ b/packages/super-editor/src/editors/v1/extensions/shape-group/ShapeGroupView.js @@ -2,6 +2,9 @@ import { getPresetShapeSvg } from '@superdoc/preset-geometry'; import { createGradient, createTextElement } from '../shared/svg-utils.js'; import { OOXML_Z_INDEX_BASE } from '@extensions/shared/constants.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('shape-group'); export class ShapeGroupView { node; @@ -72,7 +75,7 @@ export class ShapeGroupView { } catch (error) { // Silently handle DOM manipulation errors (e.g., detached node, read-only style) // These are edge cases that should not break rendering - console.warn('Failed to position parent element for shape group:', error); + log.warn('Failed to position parent element for shape group:', error); } }); } @@ -469,7 +472,7 @@ export class ShapeGroupView { } } } catch (error) { - console.warn('Failed to generate shape SVG:', error); + log.warn('Failed to generate shape SVG:', error); // Fallback to a simple rectangle const rect = document.createElementNS('http://www.w3.org/2000/svg', 'rect'); rect.setAttribute('width', width.toString()); diff --git a/packages/super-editor/src/editors/v1/extensions/structured-content/document-section.js b/packages/super-editor/src/editors/v1/extensions/structured-content/document-section.js index 2a2e25c20b..75562bb297 100644 --- a/packages/super-editor/src/editors/v1/extensions/structured-content/document-section.js +++ b/packages/super-editor/src/editors/v1/extensions/structured-content/document-section.js @@ -8,6 +8,9 @@ import { Selection } from 'prosemirror-state'; import { DOMParser as PMDOMParser } from 'prosemirror-model'; import { findParentNode } from '@helpers/index.js'; import { SectionHelpers } from './document-section/helpers.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('structured-content'); /** * Document section creation options @@ -224,7 +227,7 @@ export const DocumentSection = Node.create({ editor.view.dispatch(newTr); } } catch (e) { - console.warn('Could not set delayed selection:', e); + log.warn('Could not set delayed selection:', e); } }, 0); } diff --git a/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.js b/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.js index 1334e92f27..97a07a2920 100644 --- a/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.js +++ b/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.js @@ -9,6 +9,9 @@ import { getStructuredContentTagsById } from './structuredContentHelpers/getStru import { getStructuredContentByGroup } from './structuredContentHelpers/getStructuredContentByGroup.js'; import { createTagObject } from './structuredContentHelpers/tagUtils.js'; import * as structuredContentHelpers from './structuredContentHelpers/index.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('structured-content'); const STRUCTURED_CONTENT_NAMES = ['structuredContent', 'structuredContentBlock']; @@ -379,7 +382,7 @@ export const StructuredContentCommands = Extension.create({ const nodeForValidation = editor.validateJSON(updatedNode.toJSON()); nodeForValidation.check(); } catch (error) { - console.error('Invalid content.', 'Passed value:', content, 'Error:', error); + log.error('Invalid content.', 'Passed value:', content, 'Error:', error); return false; } @@ -548,7 +551,7 @@ export const StructuredContentCommands = Extension.create({ const nodeForValidation = editor.validateJSON(updatedNode.toJSON()); nodeForValidation.check(); } catch (error) { - console.error('Invalid content.', 'Passed value:', content, 'Error:', error); + log.error('Invalid content.', 'Passed value:', content, 'Error:', error); return false; } diff --git a/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.test.js b/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.test.js index ea0caf5275..9ed7f5e5fe 100644 --- a/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.test.js +++ b/packages/super-editor/src/editors/v1/extensions/structured-content/structured-content-commands.test.js @@ -302,7 +302,7 @@ describe('updateStructuredContentById', () => { // Verify that console.error was called with validation error expect(consoleErrorSpy).toHaveBeenCalled(); - expect(consoleErrorSpy.mock.calls[0][0]).toBe('Invalid content.'); + expect(consoleErrorSpy.mock.calls[0][1]).toBe('Invalid content.'); // Verify the original node was NOT modified let originalNode = null; @@ -370,7 +370,7 @@ describe('updateStructuredContentById', () => { // Verify validation error was logged expect(consoleErrorSpy).toHaveBeenCalled(); - expect(consoleErrorSpy.mock.calls[0][0]).toBe('Invalid content.'); + expect(consoleErrorSpy.mock.calls[0][1]).toBe('Invalid content.'); // Verify the original node was NOT modified let originalNode = null; @@ -519,7 +519,7 @@ describe('updateStructuredContentByGroup', () => { // Verify that console.error was called with validation error expect(consoleErrorSpy).toHaveBeenCalled(); - expect(consoleErrorSpy.mock.calls[0][0]).toBe('Invalid content.'); + expect(consoleErrorSpy.mock.calls[0][1]).toBe('Invalid content.'); // Verify the original nodes were NOT modified const originalNodes = []; diff --git a/packages/super-editor/src/editors/v1/extensions/tab/helpers/tabAdapter.js b/packages/super-editor/src/editors/v1/extensions/tab/helpers/tabAdapter.js index fe7b6c1522..76d64fea7a 100644 --- a/packages/super-editor/src/editors/v1/extensions/tab/helpers/tabAdapter.js +++ b/packages/super-editor/src/editors/v1/extensions/tab/helpers/tabAdapter.js @@ -13,6 +13,9 @@ import { extractParagraphContext, } from './tabDecorations.js'; import { getParagraphContext } from './paragraphContextCache.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('tab'); const leaderStyles = { dot: 'border-bottom: 1px dotted black;', @@ -257,19 +260,19 @@ export function applyLayoutResult(result, paragraph, paragraphPos) { const walk = (node, pos, depth = 0) => { // Guard against excessive recursion depth if (depth > MAX_WALK_DEPTH) { - console.error(`applyLayoutResult: Maximum recursion depth (${MAX_WALK_DEPTH}) exceeded`); + log.error(`applyLayoutResult: Maximum recursion depth (${MAX_WALK_DEPTH}) exceeded`); return; } // Guard against missing node.type or node.type.name if (!node?.type?.name) { - console.error('applyLayoutResult: Node missing type.name', { node, pos, depth }); + log.error('applyLayoutResult: Node missing type.name', { node, pos, depth }); return; } // Guard against invalid nodeSize if (typeof node.nodeSize !== 'number' || node.nodeSize < 0 || !Number.isFinite(node.nodeSize)) { - console.error('applyLayoutResult: Invalid nodeSize', { nodeSize: node.nodeSize, nodeName: node.type.name, pos }); + log.error('applyLayoutResult: Invalid nodeSize', { nodeSize: node.nodeSize, nodeName: node.type.name, pos }); return; } @@ -297,7 +300,7 @@ export function applyLayoutResult(result, paragraph, paragraphPos) { offset += child.nodeSize; }); } catch (error) { - console.error('applyLayoutResult: Error during recursion', { + log.error('applyLayoutResult: Error during recursion', { error, nodeName: node.type.name, pos, diff --git a/packages/super-editor/src/editors/v1/extensions/track-changes/track-changes.js b/packages/super-editor/src/editors/v1/extensions/track-changes/track-changes.js index e839e0b236..1a79adaf2b 100644 --- a/packages/super-editor/src/editors/v1/extensions/track-changes/track-changes.js +++ b/packages/super-editor/src/editors/v1/extensions/track-changes/track-changes.js @@ -14,6 +14,9 @@ import { sliceFromText, } from './review-model/edit-intent.js'; import { decideTrackedChanges, buildDecisionBubbleEvents } from './review-model/decision-engine.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('track-changes'); /** * @typedef {{ code: string, message: string, details?: unknown }} TrackChangesFailure @@ -446,7 +449,7 @@ export const TrackChanges = Extension.create({ // Validate bounds to prevent RangeError const docSize = state.doc.content.size; if (from < 0 || to > docSize || from > to) { - console.warn('insertTrackedChange: invalid range', { from, to, docSize }); + log.warn('insertTrackedChange: invalid range', { from, to, docSize }); return false; } @@ -464,7 +467,7 @@ export const TrackChanges = Extension.create({ // Warn if user info is missing - marks will have undefined author if (!resolvedUser.name && !resolvedUser.email) { - console.warn('insertTrackedChange: no user name/email provided, track change will have undefined author'); + log.warn('insertTrackedChange: no user name/email provided, track change will have undefined author'); } const date = new Date().toISOString(); @@ -792,7 +795,7 @@ const dispatchCompiledInsertTrackedChange = ({ return false; } } catch (error) { - console.warn('insertTrackedChange: could not build intent', error); + log.warn('insertTrackedChange: could not build intent', error); return false; } diff --git a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markSnapshotHelpers.js b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markSnapshotHelpers.js index 3a0f81accf..b8589dadb6 100644 --- a/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markSnapshotHelpers.js +++ b/packages/super-editor/src/editors/v1/extensions/track-changes/trackChangesHelpers/markSnapshotHelpers.js @@ -1,4 +1,7 @@ import { isEqual, isMatch } from 'lodash'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('track-changes'); /** * @param {import('./types.js').Attrs} [attrs] @@ -334,6 +337,6 @@ export const findMarkInRangeBySnapshot = ({ doc, from, to, snapshot }) => { }); const liveMark = exactMatch || subsetMatch || overlapMatch || (shouldFallbackToTypeOnly ? typeOnlyMatch : null); - if (!liveMark) console.debug('[track-changes] could not find live mark for snapshot', snapshot); + if (!liveMark) log.debug('could not find live mark for snapshot', snapshot); return liveMark; }; diff --git a/packages/super-editor/src/editors/v1/extensions/vector-shape/VectorShapeView.js b/packages/super-editor/src/editors/v1/extensions/vector-shape/VectorShapeView.js index 417c98a514..4cd7898139 100644 --- a/packages/super-editor/src/editors/v1/extensions/vector-shape/VectorShapeView.js +++ b/packages/super-editor/src/editors/v1/extensions/vector-shape/VectorShapeView.js @@ -9,6 +9,9 @@ import { applyAlphaToSVG, generateTransforms, } from '../shared/svg-utils.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('vector-shape'); export class VectorShapeView { node; @@ -79,7 +82,7 @@ export class VectorShapeView { } catch (error) { // Silently handle DOM manipulation errors (e.g., detached node, read-only style) // These are edge cases that should not break rendering - console.warn('Failed to position parent element for vector shape:', error); + log.warn('Failed to position parent element for vector shape:', error); } }); } @@ -407,7 +410,7 @@ export class VectorShapeView { } } } catch (error) { - console.warn('Failed to generate SVG for shape:', kind, error); + log.warn('Failed to generate SVG for shape:', kind, error); return null; } return null; diff --git a/packages/super-editor/src/headless-toolbar/helpers/formatting.ts b/packages/super-editor/src/headless-toolbar/helpers/formatting.ts index d4104951b1..baaa6fa315 100644 --- a/packages/super-editor/src/headless-toolbar/helpers/formatting.ts +++ b/packages/super-editor/src/headless-toolbar/helpers/formatting.ts @@ -6,6 +6,9 @@ import { TextSelection, Selection } from 'prosemirror-state'; import { getCurrentResolvedParagraphProperties, isFieldAnnotationSelection, resolveStateEditor } from './context.js'; import { createDirectCommandExecute, isMutationCommandDisabled } from './general.js'; import type { ToolbarContext } from '../types.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('headless-toolbar'); /** * Local mirror of `ActiveFormattingEntry` from `getActiveFormatting.js` @@ -555,7 +558,7 @@ export const createImageExecute = const originalError = err instanceof Error ? err : new Error(String(err)); const error = new Error(`[headless-toolbar] Image insertion failed: ${originalError.message}`); editor?.emit?.('exception', { error, editor }); - console.error(error, originalError); + log.error(error, originalError); }); return true; diff --git a/packages/super-editor/src/ui/create-super-doc-ui.ts b/packages/super-editor/src/ui/create-super-doc-ui.ts index e577285f62..51e4ecbb7a 100644 --- a/packages/super-editor/src/ui/create-super-doc-ui.ts +++ b/packages/super-editor/src/ui/create-super-doc-ui.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@superdoc/common/logger'; import { createHeadlessToolbar } from '../headless-toolbar/index.js'; import { resolveToolbarSources } from '../headless-toolbar/resolve-toolbar-sources.js'; import { createToolbarRegistry } from '../headless-toolbar/toolbar-registry.js'; @@ -74,6 +75,8 @@ import type { ViewportRectResult, } from './types.js'; +const log = createLogger('superdoc/ui'); + /** * Source events the controller listens to today. Domain tickets may * widen this list as they land — the only invariant is that every @@ -2267,7 +2270,7 @@ export function createSuperDocUI(options: SuperDocUIOptions): SuperDocUI { try { setter.call(superdoc, mode); } catch (err) { - console.error('[superdoc/ui] ui.document.setMode failed:', err); + log.error('[superdoc/ui] ui.document.setMode failed:', err); } }, async export(options?: DocumentExportInput): Promise { @@ -2322,7 +2325,7 @@ export function createSuperDocUI(options: SuperDocUIOptions): SuperDocUI { comments: editor.converter?.comments ?? [], }); } catch (err) { - console.error('[superdoc/ui] ui.document.replaceFile commentsLoaded re-emit failed:', err); + log.error('[superdoc/ui] ui.document.replaceFile commentsLoaded re-emit failed:', err); } } }, @@ -2569,7 +2572,7 @@ export function createSuperDocUI(options: SuperDocUIOptions): SuperDocUI { try { scope.destroy(); } catch (err) { - console.error('[superdoc/ui] scope destroy threw during ui.destroy()', err); + log.error('[superdoc/ui] scope destroy threw during ui.destroy()', err); } } stateChangeListeners.clear(); diff --git a/packages/super-editor/src/ui/custom-commands.test.ts b/packages/super-editor/src/ui/custom-commands.test.ts index 4e31e47e7a..a5929c5f9f 100644 --- a/packages/super-editor/src/ui/custom-commands.test.ts +++ b/packages/super-editor/src/ui/custom-commands.test.ts @@ -235,8 +235,8 @@ describe('ui.commands.register', () => { }); expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toContain("'bold'"); - expect(warnSpy.mock.calls[0][0]).toContain('built-in'); + expect(warnSpy.mock.calls[0][1]).toContain("'bold'"); + expect(warnSpy.mock.calls[0][1]).toContain('built-in'); // Calling execute on the refused handle returns false and warns. const result = reg.handle.execute(); @@ -288,7 +288,7 @@ describe('ui.commands.register', () => { const second = ui.commands.register({ id: 'company.x', execute: secondExecute }); expect(warnSpy).toHaveBeenCalledTimes(1); - expect(warnSpy.mock.calls[0][0]).toContain('Replacing'); + expect(warnSpy.mock.calls[0][1]).toContain('Replacing'); second.handle.execute(); expect(secondExecute).toHaveBeenCalledTimes(1); @@ -338,7 +338,7 @@ describe('ui.commands.register', () => { }); expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.mock.calls[0][0]).toContain('boom'); + expect(errorSpy.mock.calls[0][1]).toContain('boom'); // Force a rebuild — same error message → no second log. reg.invalidate(); @@ -381,7 +381,7 @@ describe('ui.commands.register', () => { const result = reg.handle.execute(); expect(result).toBe(false); expect(errorSpy).toHaveBeenCalledTimes(1); - expect(errorSpy.mock.calls[0][0]).toContain("'company.throws'"); + expect(errorSpy.mock.calls[0][1]).toContain("'company.throws'"); ui.destroy(); }); diff --git a/packages/super-editor/src/ui/custom-commands.ts b/packages/super-editor/src/ui/custom-commands.ts index 30d0a4f84b..65fa631e1b 100644 --- a/packages/super-editor/src/ui/custom-commands.ts +++ b/packages/super-editor/src/ui/custom-commands.ts @@ -1,3 +1,4 @@ +import { createLogger } from '@superdoc/common/logger'; import { normalizeShortcut } from './keyboard-shortcuts.js'; import { isViewportContextBundle } from './viewport-context.js'; import type { @@ -16,6 +17,8 @@ import type { ViewportEntityHit, } from './types.js'; +const log = createLogger('superdoc/ui'); + const DEFAULT_SHORTCUT_COLLISION_MESSAGE = (shortcut: string, oldId: string, newId: string) => `[superdoc/ui] ui.commands.register(): shortcut '${shortcut}' was already bound to '${oldId}'. Replacing with '${newId}'.`; @@ -232,12 +235,12 @@ export function createCustomCommandsRegistry(deps: CustomCommandsRegistryDeps): for (const item of list) { const normalized = normalizeShortcut(item); if (!normalized) { - console.warn(DEFAULT_INVALID_SHORTCUT_MESSAGE(id, item)); + log.warn(DEFAULT_INVALID_SHORTCUT_MESSAGE(id, item)); continue; } const prior = shortcutIndex.get(normalized); if (prior && prior !== id) { - console.warn(DEFAULT_SHORTCUT_COLLISION_MESSAGE(normalized, prior, id)); + log.warn(DEFAULT_SHORTCUT_COLLISION_MESSAGE(normalized, prior, id)); } shortcutIndex.set(normalized, id); claimed.push(normalized); @@ -367,7 +370,7 @@ export function createCustomCommandsRegistry(deps: CustomCommandsRegistryDeps): // a no-op result here keeps the call site safe (handle.execute // still callable) and warns once. if (RESERVED_PROXY_PROPERTY_NAMES.has(id)) { - console.warn(DEFAULT_RESERVED_NAME_MESSAGE(id)); + log.warn(DEFAULT_RESERVED_NAME_MESSAGE(id)); return { handle: buildNoOpHandle(id), invalidate() { @@ -384,7 +387,7 @@ export function createCustomCommandsRegistry(deps: CustomCommandsRegistryDeps): // crash on `result.handle.execute(...)` — they just see a warned // disabled command, matching the "warn and refuse" decision. if (deps.isBuiltIn(id) && !override) { - console.warn(DEFAULT_BUILTIN_COLLISION_MESSAGE(id)); + log.warn(DEFAULT_BUILTIN_COLLISION_MESSAGE(id)); return { handle: buildNoOpHandle(id), invalidate() { @@ -402,7 +405,7 @@ export function createCustomCommandsRegistry(deps: CustomCommandsRegistryDeps): // observer's `entries.has(id)` short-circuit will then detach. const priorEntry = entries.get(id); if (priorEntry) { - console.warn(DEFAULT_REPLACEMENT_MESSAGE(id)); + log.warn(DEFAULT_REPLACEMENT_MESSAGE(id)); disposeAllObservers(id); // Drop the prior registration's shortcuts before claiming the // new ones so a re-registration that drops a binding doesn't @@ -489,7 +492,7 @@ export function createCustomCommandsRegistry(deps: CustomCommandsRegistryDeps): if (entry.lastErrorMessage !== message) { entry.lastErrorMessage = message; - console.error(`[superdoc/ui] custom command '${entry.id}' getState threw: ${message}`); + log.error(`[superdoc/ui] custom command '${entry.id}' getState threw: ${message}`); } } } @@ -541,14 +544,14 @@ export function createCustomCommandsRegistry(deps: CustomCommandsRegistryDeps): return result.then( (value) => value !== false, (err) => { - console.error(`[superdoc/ui] custom command '${id}' execute rejected:`, err); + log.error(`[superdoc/ui] custom command '${id}' execute rejected:`, err); return false; }, ); } return result !== false; } catch (err) { - console.error(`[superdoc/ui] custom command '${id}' execute threw:`, err); + log.error(`[superdoc/ui] custom command '${id}' execute threw:`, err); return false; } }, @@ -591,7 +594,7 @@ export function createCustomCommandsRegistry(deps: CustomCommandsRegistryDeps): const message = err instanceof Error ? err.message : String(err); if (entry.lastContextMenuErrorMessage !== message) { entry.lastContextMenuErrorMessage = message; - console.error(`[superdoc/ui] custom command '${entry.id}' contextMenu.when threw:`, err); + log.error(`[superdoc/ui] custom command '${entry.id}' contextMenu.when threw:`, err); } applies = false; } @@ -696,7 +699,7 @@ function buildNoOpHandle(id: string): CustomCommandHandle {}; }, execute: ((..._args: unknown[]) => { - console.warn( + log.warn( `[superdoc/ui] ui.commands['${id}'].execute(): registration was refused (built-in collision without override).`, ); return false; diff --git a/packages/super-editor/src/ui/document.test.ts b/packages/super-editor/src/ui/document.test.ts index e9b25d5044..40b4b46d90 100644 --- a/packages/super-editor/src/ui/document.test.ts +++ b/packages/super-editor/src/ui/document.test.ts @@ -324,7 +324,7 @@ describe('ui.document', () => { expect(() => ui.document.setMode('viewing')).not.toThrow(); expect(errorSpy).toHaveBeenCalled(); - expect(String(errorSpy.mock.calls[0][0])).toContain('ui.document.setMode failed'); + expect(String(errorSpy.mock.calls[0][1])).toContain('ui.document.setMode failed'); ui.destroy(); }); diff --git a/packages/super-editor/src/ui/scope.ts b/packages/super-editor/src/ui/scope.ts index abe6705b96..1c46a741d1 100644 --- a/packages/super-editor/src/ui/scope.ts +++ b/packages/super-editor/src/ui/scope.ts @@ -31,8 +31,11 @@ * methods follow the same rules. */ +import { createLogger } from '@superdoc/common/logger'; import type { CustomCommandRegistration, CustomCommandRegistrationResult, SuperDocUIScope } from './types.js'; +const log = createLogger('superdoc/ui'); + /** * Internal collaborator the scope needs from its owner (the * controller, or a parent scope). Kept narrow so the scope module @@ -131,7 +134,7 @@ export function createScope(owner: ScopeOwner): SuperDocUIScope { try { child.destroy(); } catch (err) { - console.error('[superdoc/ui] child scope destroy threw', err); + log.error('[superdoc/ui] child scope destroy threw', err); } } // Reverse order is the standard effect-cleanup convention: most @@ -160,6 +163,6 @@ function runTeardown(teardown: () => void): void { try { teardown(); } catch (err) { - console.error('[superdoc/ui] scope teardown threw', err); + log.error('[superdoc/ui] scope teardown threw', err); } } diff --git a/packages/super-editor/src/ui/selection-rects.ts b/packages/super-editor/src/ui/selection-rects.ts index ed219d4986..18e4cbf9ff 100644 --- a/packages/super-editor/src/ui/selection-rects.ts +++ b/packages/super-editor/src/ui/selection-rects.ts @@ -6,11 +6,14 @@ * DOM that `window.getSelection()` reports against. */ +import { createLogger } from '@superdoc/common/logger'; import type { Editor } from '../editors/v1/core/Editor.js'; import { DocumentApiAdapterError } from '../editors/v1/document-api-adapters/errors.js'; import { resolveTextTarget } from '../editors/v1/document-api-adapters/helpers/adapter-utils.js'; import type { SelectionCapture, SelectionAnchorRectOptions, ViewportRect } from './types.js'; +const log = createLogger('superdoc/ui'); + interface RawRangeRect { pageIndex: number; left: number; @@ -121,7 +124,7 @@ function getCapturedSelectionRects( // swallowing silently — bare `return []` would hide a real document // problem (two blocks sharing an id) behind "no rects". if (err instanceof DocumentApiAdapterError) { - console.warn(`[superdoc/ui] ui.selection.getRects: ${err.code}: ${err.message}`); + log.warn(`[superdoc/ui] ui.selection.getRects: ${err.code}: ${err.message}`); } return []; } diff --git a/packages/superdoc/src/SuperDoc.vue b/packages/superdoc/src/SuperDoc.vue index e4ee06252e..fe35a76edf 100644 --- a/packages/superdoc/src/SuperDoc.vue +++ b/packages/superdoc/src/SuperDoc.vue @@ -61,6 +61,9 @@ import { RIGHT_CLICK_COMMENT_SUPPRESS_MS, VALID_COMMENTS_DISPLAY_MODES, } from './helpers/comment-small-screen.js'; +import { createLogger } from '@superdoc/common/logger'; + +const log = createLogger('superdoc'); const PdfViewer = defineAsyncComponent(() => import('./components/PdfViewer/PdfViewer.vue')); const getDocumentLoadPassword = (doc) => doc.password ?? proxy.$superdoc.config.password; @@ -623,7 +626,7 @@ const processSelectionChange = (editor, transaction) => { try { return view.coordsAtPos(pos); } catch (err) { - console.warn('[superdoc] Ignoring selection coords error', err); + log.warn('Ignoring selection coords error', err); return null; } }; @@ -678,7 +681,7 @@ const processSelectionChange = (editor, transaction) => { try { return view.coordsAtPos(pos); } catch (err) { - console.warn('[superdoc] Ignoring selection coords error', err); + log.warn('Ignoring selection coords error', err); return null; } }; @@ -801,7 +804,7 @@ const editorOptions = (doc) => { isAiEnabled: proxy.$superdoc.config.modules?.ai, contextMenuConfig: (() => { if (proxy.$superdoc.config.modules?.slashMenu && !proxy.$superdoc.config.modules?.contextMenu) { - console.warn('[SuperDoc] modules.slashMenu is deprecated. Use modules.contextMenu instead.'); + log.warn('modules.slashMenu is deprecated. Use modules.contextMenu instead.'); } return proxy.$superdoc.config.modules?.contextMenu ?? proxy.$superdoc.config.modules?.slashMenu; })(), diff --git a/packages/superdoc/src/components/HtmlViewer/HtmlViewer.vue b/packages/superdoc/src/components/HtmlViewer/HtmlViewer.vue index c9db73ddd1..417ee4dcac 100644 --- a/packages/superdoc/src/components/HtmlViewer/HtmlViewer.vue +++ b/packages/superdoc/src/components/HtmlViewer/HtmlViewer.vue @@ -1,5 +1,9 @@