@@ -2943,96 +2943,107 @@ export class PresentationEditor extends EventEmitter {
29432943 }
29442944 }
29452945
2946+ #convertDocToFlowBlocks( perfNow : ( ) => number ) : {
2947+ docJson : unknown ;
2948+ blocks : FlowBlock [ ] ;
2949+ bookmarks : Map < string , number > ;
2950+ converterContext : ConverterContext | undefined ;
2951+ sectionMetadata : SectionMetadata [ ] ;
2952+ layoutEpoch : number ;
2953+ } | null {
2954+ let docJson ;
2955+ try {
2956+ const start = perfNow ( ) ;
2957+ docJson = this . #editor. getJSON ( ) ;
2958+ perfLog ( `[Perf] getJSON: ${ ( perfNow ( ) - start ) . toFixed ( 2 ) } ms` ) ;
2959+ } catch ( error ) {
2960+ this . #handleLayoutError( 'render' , this . #decorateError( error , 'getJSON' ) ) ;
2961+ return null ;
2962+ }
2963+
2964+ const layoutEpoch = this . #epochMapper. getCurrentEpoch ( ) ;
2965+ const sectionMetadata : SectionMetadata [ ] = [ ] ;
2966+
2967+ let blocks : FlowBlock [ ] | undefined ;
2968+ let bookmarks : Map < string , number > = new Map ( ) ;
2969+ let converterContext : ConverterContext | undefined ;
2970+ try {
2971+ const converter = ( this . #editor as Editor & { converter ?: Record < string , unknown > } ) . converter ;
2972+ const { footnoteNumberById, footnoteOrder } = computeFootnoteNumbering ( this . #editor?. state ?. doc ) ;
2973+ const footnoteSignature = footnoteOrder . join ( '|' ) ;
2974+ if ( footnoteSignature !== this . #footnoteNumberSignature) {
2975+ this . #flowBlockCache. clear ( ) ;
2976+ this . #footnoteNumberSignature = footnoteSignature ;
2977+ }
2978+ try {
2979+ if ( converter && typeof converter === 'object' ) {
2980+ converter [ 'footnoteNumberById' ] = footnoteNumberById ;
2981+ }
2982+ } catch { }
2983+ converterContext = buildConverterContext ( converter , footnoteNumberById ) ;
2984+
2985+ const atomNodeTypes = getAtomNodeTypesFromSchema ( this . #editor?. schema ?? null ) ;
2986+ const positionMap =
2987+ this . #editor?. state ?. doc && docJson ? buildPositionMapFromPmDoc ( this . #editor. state . doc , docJson ) : null ;
2988+ const commentsEnabled = this . #documentMode !== 'viewing' || this . #layoutOptions. enableCommentsInViewing === true ;
2989+
2990+ const start = perfNow ( ) ;
2991+ const result = toFlowBlocks ( docJson , {
2992+ mediaFiles : ( this . #editor?. storage ?. image as { media ?: Record < string , string > } ) ?. media ,
2993+ emitSectionBreaks : true ,
2994+ sectionMetadata,
2995+ trackedChangesMode : this . #trackedChangesMode,
2996+ enableTrackedChanges : this . #trackedChangesEnabled,
2997+ enableComments : commentsEnabled ,
2998+ enableRichHyperlinks : true ,
2999+ themeColors : this . #editor?. converter ?. themeColors ?? undefined ,
3000+ converterContext,
3001+ flowBlockCache : this . #flowBlockCache,
3002+ ...( positionMap ? { positions : positionMap } : { } ) ,
3003+ ...( atomNodeTypes . length > 0 ? { atomNodeTypes } : { } ) ,
3004+ } ) ;
3005+ perfLog ( `[Perf] toFlowBlocks: ${ ( perfNow ( ) - start ) . toFixed ( 2 ) } ms (blocks=${ result . blocks . length } )` ) ;
3006+ blocks = result . blocks ;
3007+ bookmarks = result . bookmarks ?? new Map ( ) ;
3008+ } catch ( error ) {
3009+ this . #handleLayoutError( 'render' , this . #decorateError( error , 'toFlowBlocks' ) ) ;
3010+ return null ;
3011+ }
3012+
3013+ if ( ! blocks ) {
3014+ this . #handleLayoutError( 'render' , new Error ( 'toFlowBlocks returned undefined blocks' ) ) ;
3015+ return null ;
3016+ }
3017+
3018+ // Split runs at decoration boundaries for highlight rendering
3019+ const state = this . #editor?. view ?. state ;
3020+ const decorationRanges = state ? this . #decorationBridge. collectDecorationRanges ( state ) : [ ] ;
3021+ if ( decorationRanges . length > 0 ) {
3022+ blocks = splitRunsAtDecorationBoundaries (
3023+ blocks ,
3024+ decorationRanges . map ( ( r ) => ( { from : r . from , to : r . to } ) ) ,
3025+ ) ;
3026+ }
3027+
3028+ this . #applyHtmlAnnotationMeasurements( blocks ) ;
3029+ return { docJson, blocks, bookmarks, converterContext, sectionMetadata, layoutEpoch } ;
3030+ }
3031+
29463032 async #rerender( ) {
29473033 this . #selectionSync. onLayoutStart ( ) ;
29483034 let layoutCompleted = false ;
29493035
29503036 try {
2951- let docJson ;
29523037 const viewWindow = this . #visibleHost. ownerDocument ?. defaultView ?? window ;
29533038 const perf = viewWindow ?. performance ?? GLOBAL_PERFORMANCE ;
29543039 const perfNow = ( ) => ( perf ?. now ? perf . now ( ) : Date . now ( ) ) ;
29553040 const startMark = perf ?. now ?.( ) ;
2956- try {
2957- const getJsonStart = perfNow ( ) ;
2958- docJson = this . #editor. getJSON ( ) ;
2959- const getJsonEnd = perfNow ( ) ;
2960- perfLog ( `[Perf] getJSON: ${ ( getJsonEnd - getJsonStart ) . toFixed ( 2 ) } ms` ) ;
2961- } catch ( error ) {
2962- this . #handleLayoutError( 'render' , this . #decorateError( error , 'getJSON' ) ) ;
2963- return ;
2964- }
2965- const layoutEpoch = this . #epochMapper. getCurrentEpoch ( ) ;
29663041
2967- const sectionMetadata : SectionMetadata [ ] = [ ] ;
2968- let blocks : FlowBlock [ ] | undefined ;
2969- let bookmarks : Map < string , number > = new Map ( ) ;
2970- let converterContext : ConverterContext | undefined = undefined ;
2971- try {
2972- const converter = ( this . #editor as Editor & { converter ?: Record < string , unknown > } ) . converter ;
2973- const { footnoteNumberById, footnoteOrder } = computeFootnoteNumbering ( this . #editor?. state ?. doc ) ;
2974- const footnoteSignature = footnoteOrder . join ( '|' ) ;
2975- if ( footnoteSignature !== this . #footnoteNumberSignature) {
2976- this . #flowBlockCache. clear ( ) ;
2977- this . #footnoteNumberSignature = footnoteSignature ;
2978- }
2979- try {
2980- if ( converter && typeof converter === 'object' ) {
2981- converter [ 'footnoteNumberById' ] = footnoteNumberById ;
2982- }
2983- } catch { }
2984- converterContext = buildConverterContext ( converter , footnoteNumberById ) ;
2985- const atomNodeTypes = getAtomNodeTypesFromSchema ( this . #editor?. schema ?? null ) ;
2986- const positionMapStart = perfNow ( ) ;
2987- const positionMap =
2988- this . #editor?. state ?. doc && docJson ? buildPositionMapFromPmDoc ( this . #editor. state . doc , docJson ) : null ;
2989- const positionMapEnd = perfNow ( ) ;
2990- perfLog ( `[Perf] buildPositionMapFromPmDoc: ${ ( positionMapEnd - positionMapStart ) . toFixed ( 2 ) } ms` ) ;
2991- const commentsEnabled =
2992- this . #documentMode !== 'viewing' || this . #layoutOptions. enableCommentsInViewing === true ;
2993- const toFlowBlocksStart = perfNow ( ) ;
2994- const result = toFlowBlocks ( docJson , {
2995- mediaFiles : ( this . #editor?. storage ?. image as { media ?: Record < string , string > } ) ?. media ,
2996- emitSectionBreaks : true ,
2997- sectionMetadata,
2998- trackedChangesMode : this . #trackedChangesMode,
2999- enableTrackedChanges : this . #trackedChangesEnabled,
3000- enableComments : commentsEnabled ,
3001- enableRichHyperlinks : true ,
3002- themeColors : this . #editor?. converter ?. themeColors ?? undefined ,
3003- converterContext,
3004- flowBlockCache : this . #flowBlockCache,
3005- ...( positionMap ? { positions : positionMap } : { } ) ,
3006- ...( atomNodeTypes . length > 0 ? { atomNodeTypes } : { } ) ,
3007- } ) ;
3008- const toFlowBlocksEnd = perfNow ( ) ;
3009- perfLog (
3010- `[Perf] toFlowBlocks: ${ ( toFlowBlocksEnd - toFlowBlocksStart ) . toFixed ( 2 ) } ms (blocks=${ result . blocks . length } )` ,
3011- ) ;
3012- blocks = result . blocks ;
3013- bookmarks = result . bookmarks ?? new Map ( ) ;
3014- } catch ( error ) {
3015- this . #handleLayoutError( 'render' , this . #decorateError( error , 'toFlowBlocks' ) ) ;
3016- return ;
3017- }
3042+ // Phase 1: Serialize document + convert to FlowBlocks
3043+ const conversionResult = this . #convertDocToFlowBlocks( perfNow ) ;
3044+ if ( ! conversionResult ) return ;
3045+ const { docJson, blocks, bookmarks, converterContext, sectionMetadata, layoutEpoch } = conversionResult ;
30183046
3019- if ( ! blocks ) {
3020- this . #handleLayoutError( 'render' , new Error ( 'toFlowBlocks returned undefined blocks' ) ) ;
3021- return ;
3022- }
3023-
3024- // Split runs at decoration boundaries so bridge sync applies background only to the
3025- // selected portion (like highlight mark) without adding a document mark.
3026- const state = this . #editor?. view ?. state ;
3027- const decorationRanges = state ? this . #decorationBridge. collectDecorationRanges ( state ) : [ ] ;
3028- if ( decorationRanges . length > 0 ) {
3029- blocks = splitRunsAtDecorationBoundaries (
3030- blocks ,
3031- decorationRanges . map ( ( r ) => ( { from : r . from , to : r . to } ) ) ,
3032- ) ;
3033- }
3034-
3035- this . #applyHtmlAnnotationMeasurements( blocks ) ;
30363047 const isSemanticFlow = this . #isSemanticFlowMode( ) ;
30373048
30383049 const baseLayoutOptions = this . #resolveLayoutOptions( blocks , sectionMetadata ) ;
@@ -3187,59 +3198,60 @@ export class PresentationEditor extends EventEmitter {
31873198 painter . paint ( layout , this . #painterHost, mapping ?? undefined ) ;
31883199 const painterPaintEnd = perfNow ( ) ;
31893200 perfLog ( `[Perf] painter.paint: ${ ( painterPaintEnd - painterPaintStart ) . toFixed ( 2 ) } ms` ) ;
3190- const painterPostStart = perfNow ( ) ;
3191- this . #applyVertAlignToLayout( ) ;
3192- this . #rebuildDomPositionIndex( ) ;
3193- this . #syncDecorations( ) ;
3194- this . #domIndexObserverManager?. resume ( ) ;
3195- const painterPostEnd = perfNow ( ) ;
3196- perfLog ( `[Perf] painter.postPaint: ${ ( painterPostEnd - painterPostStart ) . toFixed ( 2 ) } ms` ) ;
3197- this . #layoutEpoch = layoutEpoch ;
3198- if ( this . #updateHtmlAnnotationMeasurements( layoutEpoch ) ) {
3199- this . #pendingDocChange = true ;
3200- this . #scheduleRerender( ) ;
3201- }
3202- this . #epochMapper. onLayoutComplete ( layoutEpoch ) ;
3203- this . #selectionSync. onLayoutComplete ( layoutEpoch ) ;
3201+ this . #postPaint( perfNow , layoutEpoch , blocksForLayout , measures , layout , perf , startMark ) ;
32043202 layoutCompleted = true ;
3205- this . #updatePermissionOverlay( ) ;
3203+ } finally {
3204+ if ( ! layoutCompleted ) {
3205+ this . #selectionSync. onLayoutAbort ( ) ;
3206+ }
3207+ }
3208+ }
32063209
3207- // Reset error state on successful layout
3208- this . #layoutError = null ;
3209- this . #layoutErrorState = 'healthy' ;
3210- this . #errorBanner. dismiss ( ) ;
3210+ #postPaint(
3211+ perfNow : ( ) => number ,
3212+ layoutEpoch : number ,
3213+ blocks : FlowBlock [ ] ,
3214+ measures : Measure [ ] ,
3215+ layout : Layout ,
3216+ perf : Performance | undefined ,
3217+ startMark : number | undefined ,
3218+ ) {
3219+ const postStart = perfNow ( ) ;
3220+ this . #applyVertAlignToLayout( ) ;
3221+ this . #rebuildDomPositionIndex( ) ;
3222+ this . #syncDecorations( ) ;
3223+ this . #domIndexObserverManager?. resume ( ) ;
3224+ perfLog ( `[Perf] painter.postPaint: ${ ( perfNow ( ) - postStart ) . toFixed ( 2 ) } ms` ) ;
3225+
3226+ this . #layoutEpoch = layoutEpoch ;
3227+ if ( this . #updateHtmlAnnotationMeasurements( layoutEpoch ) ) {
3228+ this . #pendingDocChange = true ;
3229+ this . #scheduleRerender( ) ;
3230+ }
3231+ this . #epochMapper. onLayoutComplete ( layoutEpoch ) ;
3232+ this . #selectionSync. onLayoutComplete ( layoutEpoch ) ;
3233+ this . #updatePermissionOverlay( ) ;
32113234
3212- // Update viewport dimensions after layout (page count may have changed)
3213- this . #applyZoom( ) ;
3235+ this . #layoutError = null ;
3236+ this . #layoutErrorState = 'healthy' ;
3237+ this . #errorBanner. dismiss ( ) ;
32143238
3215- const metrics = createLayoutMetricsFromHelper ( perf , startMark , layout , blocksForLayout ) ;
3216- const payload = { layout, blocks : blocksForLayout , measures, metrics } ;
3217- this . emit ( 'layoutUpdated' , payload ) ;
3218- this . emit ( 'paginationUpdate' , payload ) ;
3219-
3220- // Emit fresh comment positions after layout completes.
3221- // Always emit — even when empty — so the store can clear stale positions
3222- // (e.g. when undo removes the last tracked-change mark).
3223- const allowViewingCommentPositions = this . #layoutOptions. emitCommentPositionsInViewing === true ;
3224- if ( this . #documentMode !== 'viewing' || allowViewingCommentPositions ) {
3225- const commentPositions = this . #collectCommentPositions( ) ;
3226- this . emit ( 'commentPositions' , { positions : commentPositions } ) ;
3227- }
3239+ this . #applyZoom( ) ;
32283240
3229- this . #selectionSync. requestRender ( { immediate : true } ) ;
3241+ const metrics = createLayoutMetricsFromHelper ( perf , startMark , layout , blocks ) ;
3242+ const payload = { layout, blocks, measures, metrics } ;
3243+ this . emit ( 'layoutUpdated' , payload ) ;
3244+ this . emit ( 'paginationUpdate' , payload ) ;
32303245
3231- // Re-normalize remote cursor positions after layout completes.
3232- // Local document changes shift absolute positions, so Yjs relative positions
3233- // must be re-resolved against the updated editor state. Without this,
3234- // remote cursors appear offset by the number of characters the local user typed.
3235- if ( this . #remoteCursorManager?. hasRemoteCursors ( ) ) {
3236- this . #remoteCursorManager. markDirty ( ) ;
3237- this . #remoteCursorManager. scheduleUpdate ( ) ;
3238- }
3239- } finally {
3240- if ( ! layoutCompleted ) {
3241- this . #selectionSync. onLayoutAbort ( ) ;
3242- }
3246+ if ( this . #documentMode !== 'viewing' || this . #layoutOptions. emitCommentPositionsInViewing === true ) {
3247+ this . emit ( 'commentPositions' , { positions : this . #collectCommentPositions( ) } ) ;
3248+ }
3249+
3250+ this . #selectionSync. requestRender ( { immediate : true } ) ;
3251+
3252+ if ( this . #remoteCursorManager?. hasRemoteCursors ( ) ) {
3253+ this . #remoteCursorManager. markDirty ( ) ;
3254+ this . #remoteCursorManager. scheduleUpdate ( ) ;
32433255 }
32443256 }
32453257
0 commit comments