22 * Bookmark node resolver — finds, resolves, and extracts info from bookmarkStart nodes.
33 */
44
5+ import type { Editor } from '../../core/Editor.js' ;
56import type { Node as ProseMirrorNode } from 'prosemirror-model' ;
67import type {
78 BookmarkAddress ,
@@ -13,6 +14,7 @@ import type {
1314} from '@superdoc/document-api' ;
1415import { buildDiscoveryItem , buildResolvedHandle } from '@superdoc/document-api' ;
1516import { DocumentApiAdapterError } from '../errors.js' ;
17+ import { BODY_STORY_KEY , buildStoryKey } from '../story-runtime/story-key.js' ;
1618
1719// ---------------------------------------------------------------------------
1820// Types
@@ -26,6 +28,33 @@ export interface ResolvedBookmark {
2628 endPos : number | null ;
2729}
2830
31+ export interface DocumentBookmarkEntry {
32+ name : string ;
33+ bookmarkId : string ;
34+ storyKey : string ;
35+ }
36+
37+ type StoryEditorEntry = {
38+ id ?: unknown ;
39+ editor ?: Editor ;
40+ } ;
41+
42+ type NoteEntry = {
43+ id ?: unknown ;
44+ content ?: unknown [ ] ;
45+ doc ?: Record < string , unknown > ;
46+ type ?: unknown ;
47+ } ;
48+
49+ type ConverterWithStories = {
50+ headers ?: Record < string , unknown > ;
51+ footers ?: Record < string , unknown > ;
52+ headerEditors ?: StoryEditorEntry [ ] ;
53+ footerEditors ?: StoryEditorEntry [ ] ;
54+ footnotes ?: NoteEntry [ ] ;
55+ endnotes ?: NoteEntry [ ] ;
56+ } ;
57+
2958export function normalizeStory ( locator ?: StoryLocator ) : StoryLocator | undefined {
3059 if ( ! locator || locator . storyType === 'body' ) return undefined ;
3160 return locator ;
@@ -38,6 +67,24 @@ export function buildBookmarkAddress(name: string, story?: StoryLocator): Bookma
3867 : { kind : 'entity' , entityType : 'bookmark' , name } ;
3968}
4069
70+ export function findAllBookmarksInDocument ( editor : Editor ) : DocumentBookmarkEntry [ ] {
71+ const results : DocumentBookmarkEntry [ ] = [ ] ;
72+ const seenStoryKeys = new Set < string > ( ) ;
73+ const converter = ( editor as unknown as { converter ?: ConverterWithStories } ) . converter ;
74+
75+ seenStoryKeys . add ( BODY_STORY_KEY ) ;
76+ collectBookmarksFromDoc ( editor . state . doc , BODY_STORY_KEY , results ) ;
77+
78+ collectBookmarksFromHeaderFooterEditors ( converter ?. headerEditors , results , seenStoryKeys ) ;
79+ collectBookmarksFromHeaderFooterEditors ( converter ?. footerEditors , results , seenStoryKeys ) ;
80+ collectBookmarksFromHeaderFooterCache ( converter ?. headers , results , seenStoryKeys ) ;
81+ collectBookmarksFromHeaderFooterCache ( converter ?. footers , results , seenStoryKeys ) ;
82+ collectBookmarksFromNotes ( converter ?. footnotes , 'footnote' , results , seenStoryKeys ) ;
83+ collectBookmarksFromNotes ( converter ?. endnotes , 'endnote' , results , seenStoryKeys ) ;
84+
85+ return results ;
86+ }
87+
4188// ---------------------------------------------------------------------------
4289// Node resolution
4390// ---------------------------------------------------------------------------
@@ -77,6 +124,122 @@ function collectBookmarkEndPositions(doc: ProseMirrorNode): Map<string, number>
77124 return map ;
78125}
79126
127+ function collectBookmarksFromDoc ( doc : ProseMirrorNode , storyKey : string , results : DocumentBookmarkEntry [ ] ) : void {
128+ doc . descendants ( ( node ) => {
129+ if ( node . type . name === 'bookmarkStart' ) {
130+ results . push ( {
131+ name : ( node . attrs ?. name as string ) ?? '' ,
132+ bookmarkId : ( node . attrs ?. id as string ) ?? '' ,
133+ storyKey,
134+ } ) ;
135+ }
136+ return true ;
137+ } ) ;
138+ }
139+
140+ function collectBookmarksFromHeaderFooterEditors (
141+ editors : StoryEditorEntry [ ] | undefined ,
142+ results : DocumentBookmarkEntry [ ] ,
143+ seenStoryKeys : Set < string > ,
144+ ) : void {
145+ if ( ! Array . isArray ( editors ) ) return ;
146+
147+ for ( const entry of editors ) {
148+ const refId = typeof entry ?. id === 'string' && entry . id . length > 0 ? entry . id : null ;
149+ const storyEditor = entry ?. editor ;
150+ if ( ! refId || ! storyEditor ?. state ?. doc ) continue ;
151+
152+ const storyKey = buildStoryKey ( { kind : 'story' , storyType : 'headerFooterPart' , refId } ) ;
153+ if ( seenStoryKeys . has ( storyKey ) ) continue ;
154+ seenStoryKeys . add ( storyKey ) ;
155+ collectBookmarksFromDoc ( storyEditor . state . doc , storyKey , results ) ;
156+ }
157+ }
158+
159+ function collectBookmarksFromHeaderFooterCache (
160+ collection : Record < string , unknown > | undefined ,
161+ results : DocumentBookmarkEntry [ ] ,
162+ seenStoryKeys : Set < string > ,
163+ ) : void {
164+ if ( ! collection || typeof collection !== 'object' ) return ;
165+
166+ for ( const [ refId , pmJson ] of Object . entries ( collection ) ) {
167+ if ( typeof refId !== 'string' || refId . length === 0 ) continue ;
168+
169+ const storyKey = buildStoryKey ( { kind : 'story' , storyType : 'headerFooterPart' , refId } ) ;
170+ if ( seenStoryKeys . has ( storyKey ) ) continue ;
171+ seenStoryKeys . add ( storyKey ) ;
172+ collectBookmarksFromPmJson ( pmJson , storyKey , results ) ;
173+ }
174+ }
175+
176+ function collectBookmarksFromNotes (
177+ notes : NoteEntry [ ] | undefined ,
178+ storyType : 'footnote' | 'endnote' ,
179+ results : DocumentBookmarkEntry [ ] ,
180+ seenStoryKeys : Set < string > ,
181+ ) : void {
182+ if ( ! Array . isArray ( notes ) ) return ;
183+
184+ for ( const note of notes ) {
185+ const noteId = note ?. id != null ? String ( note . id ) : '' ;
186+ if ( ! noteId ) continue ;
187+
188+ const storyKey = buildStoryKey ( { kind : 'story' , storyType, noteId } ) ;
189+ if ( seenStoryKeys . has ( storyKey ) ) continue ;
190+ seenStoryKeys . add ( storyKey ) ;
191+
192+ const pmJson = getNotePmJson ( note ) ;
193+ if ( ! pmJson ) continue ;
194+ collectBookmarksFromPmJson ( pmJson , storyKey , results ) ;
195+ }
196+ }
197+
198+ function getNotePmJson ( note : NoteEntry ) : Record < string , unknown > | null {
199+ if ( Array . isArray ( note . content ) ) {
200+ return {
201+ type : 'doc' ,
202+ content : note . content . length > 0 ? note . content : [ { type : 'paragraph' } ] ,
203+ } ;
204+ }
205+
206+ if ( note . doc && typeof note . doc === 'object' ) {
207+ return note . doc ;
208+ }
209+
210+ return null ;
211+ }
212+
213+ function collectBookmarksFromPmJson ( pmJson : unknown , storyKey : string , results : DocumentBookmarkEntry [ ] ) : void {
214+ if ( ! isObjectRecord ( pmJson ) ) return ;
215+
216+ visitPmJson ( pmJson , ( node ) => {
217+ if ( node . type !== 'bookmarkStart' ) return ;
218+
219+ const attrs = isObjectRecord ( node . attrs ) ? node . attrs : undefined ;
220+ const name = typeof attrs ?. name === 'string' ? attrs . name : '' ;
221+ const bookmarkId = attrs ?. id != null ? String ( attrs . id ) : '' ;
222+ results . push ( { name, bookmarkId, storyKey } ) ;
223+ } ) ;
224+ }
225+
226+ function visitPmJson ( node : Record < string , unknown > , visitor : ( node : Record < string , unknown > ) => void ) : void {
227+ visitor ( node ) ;
228+
229+ const content = node . content ;
230+ if ( ! Array . isArray ( content ) ) return ;
231+
232+ for ( const child of content ) {
233+ if ( isObjectRecord ( child ) ) {
234+ visitPmJson ( child , visitor ) ;
235+ }
236+ }
237+ }
238+
239+ function isObjectRecord ( value : unknown ) : value is Record < string , unknown > {
240+ return typeof value === 'object' && value !== null ;
241+ }
242+
80243/**
81244 * Resolves a BookmarkAddress to its ProseMirror node and position.
82245 * @throws DocumentApiAdapterError with code TARGET_NOT_FOUND if not found.
0 commit comments