@@ -13,6 +13,47 @@ interface ResumePreviewPage {
1313 content : string ;
1414}
1515
16+ function getSectionItemsContainer ( section : HTMLElement ) : HTMLElement | null {
17+ let itemsContainer =
18+ section . tagName . toLowerCase ( ) === "section" && section . children . length >= 2
19+ ? ( section . children [ 1 ] as HTMLElement )
20+ : null ;
21+
22+ while (
23+ itemsContainer &&
24+ itemsContainer . children . length === 1 &&
25+ itemsContainer . children [ 0 ] instanceof HTMLElement &&
26+ itemsContainer . children [ 0 ] . tagName . toLowerCase ( ) === "div"
27+ ) {
28+ itemsContainer = itemsContainer . children [ 0 ] as HTMLElement ;
29+ }
30+
31+ return itemsContainer ;
32+ }
33+
34+ function createSectionFragment (
35+ section : HTMLElement ,
36+ items : HTMLElement [ ] ,
37+ includeHeader : boolean ,
38+ ) : HTMLElement {
39+ const sectionClone = section . cloneNode ( true ) as HTMLElement ;
40+ const clonedItemsContainer = getSectionItemsContainer ( sectionClone ) ;
41+
42+ if ( clonedItemsContainer ) {
43+ clonedItemsContainer . innerHTML = "" ;
44+
45+ items . forEach ( ( item ) => {
46+ clonedItemsContainer . appendChild ( item . cloneNode ( true ) ) ;
47+ } ) ;
48+ }
49+
50+ if ( ! includeHeader && sectionClone . children . length > 0 ) {
51+ sectionClone . removeChild ( sectionClone . children [ 0 ] ) ;
52+ }
53+
54+ return sectionClone ;
55+ }
56+
1657export function ResumePagedPreview ( { children } : { children : ReactNode } ) {
1758 const measureRef = useRef < HTMLDivElement | null > ( null ) ;
1859 const [ pages , setPages ] = useState < ResumePreviewPage [ ] > ( [ ] ) ;
@@ -30,43 +71,121 @@ export function ResumePagedPreview({ children }: { children: ReactNode }) {
3071 }
3172
3273 const computed = window . getComputedStyle ( container ) ;
33- const paddingTop = Number . parseFloat ( computed . paddingTop ) || 0 ;
34- const paddingBottom = Number . parseFloat ( computed . paddingBottom ) || 0 ;
35- const usableHeight = RESUME_PAGE_HEIGHT_PX - paddingTop - paddingBottom ;
74+ const nextPageStyle = {
75+ backgroundColor : computed . backgroundColor ,
76+ color : computed . color ,
77+ fontFamily : computed . fontFamily ,
78+ fontSize : computed . fontSize ,
79+ lineHeight : computed . lineHeight ,
80+ padding : computed . padding ,
81+ } satisfies CSSProperties ;
82+ const probe = document . createElement ( "article" ) ;
83+
84+ probe . className = "resume-page-preview mx-auto overflow-hidden bg-white" ;
85+ Object . assign ( probe . style , {
86+ ...nextPageStyle ,
87+ boxShadow : "none" ,
88+ height : `${ RESUME_PAGE_HEIGHT_PX } px` ,
89+ minHeight : `${ RESUME_PAGE_HEIGHT_PX } px` ,
90+ position : "absolute" ,
91+ width : `${ RESUME_PAGE_WIDTH_PX } px` ,
92+ } ) ;
93+
94+ measureRef . current . appendChild ( probe ) ;
95+
96+ const fitsPage = ( elements : HTMLElement [ ] ) => {
97+ probe . innerHTML = elements . map ( ( element ) => element . outerHTML ) . join ( "" ) ;
98+
99+ return probe . scrollHeight <= RESUME_PAGE_HEIGHT_PX + 1 ;
100+ } ;
101+
36102 const nextPages : ResumePreviewPage [ ] = [ ] ;
37- let current : string [ ] = [ ] ;
38- let usedHeight = 0 ;
103+ let current : HTMLElement [ ] = [ ] ;
104+
105+ const commitCurrentPage = ( ) => {
106+ if ( current . length === 0 ) {
107+ return ;
108+ }
109+
110+ nextPages . push ( {
111+ content : current . map ( ( element ) => element . outerHTML ) . join ( "" ) ,
112+ } ) ;
113+ current = [ ] ;
114+ } ;
115+
116+ const appendBlock = ( block : HTMLElement ) => {
117+ const candidate = [ ...current , block ] ;
118+
119+ if ( candidate . length === 1 || fitsPage ( candidate ) ) {
120+ current = candidate ;
121+ return ;
122+ }
123+
124+ commitCurrentPage ( ) ;
125+ current = [ block ] ;
126+ } ;
39127
40128 Array . from ( container . children ) . forEach ( ( child ) => {
41- const element = child as HTMLElement ;
42- const childStyle = window . getComputedStyle ( element ) ;
43- const blockHeight =
44- element . getBoundingClientRect ( ) . height +
45- ( Number . parseFloat ( childStyle . marginTop ) || 0 ) +
46- ( Number . parseFloat ( childStyle . marginBottom ) || 0 ) ;
47-
48- if ( current . length > 0 && usedHeight + blockHeight > usableHeight ) {
49- nextPages . push ( { content : current . join ( "" ) } ) ;
50- current = [ ] ;
51- usedHeight = 0 ;
129+ if ( ! ( child instanceof HTMLElement ) ) {
130+ return ;
131+ }
132+
133+ const isSection = child . tagName . toLowerCase ( ) === "section" ;
134+ const itemsContainer = getSectionItemsContainer ( child ) ;
135+ const items = itemsContainer
136+ ? ( Array . from ( itemsContainer . children ) . filter (
137+ ( item ) : item is HTMLElement => item instanceof HTMLElement ,
138+ ) as HTMLElement [ ] )
139+ : [ ] ;
140+
141+ if ( ! isSection || items . length <= 1 ) {
142+ appendBlock ( child ) ;
143+ return ;
52144 }
53145
54- current . push ( element . outerHTML ) ;
55- usedHeight += blockHeight ;
146+ let itemIndex = 0 ;
147+ let includeHeader = true ;
148+
149+ while ( itemIndex < items . length ) {
150+ let acceptedCount = 0 ;
151+
152+ for ( let count = 1 ; itemIndex + count <= items . length ; count += 1 ) {
153+ const fragment = createSectionFragment (
154+ child ,
155+ items . slice ( itemIndex , itemIndex + count ) ,
156+ includeHeader ,
157+ ) ;
158+ const candidate = [ ...current , fragment ] ;
159+
160+ if ( fitsPage ( candidate ) || ( current . length === 0 && count === 1 ) ) {
161+ acceptedCount = count ;
162+ } else {
163+ break ;
164+ }
165+ }
166+
167+ if ( acceptedCount === 0 ) {
168+ commitCurrentPage ( ) ;
169+ continue ;
170+ }
171+
172+ const acceptedFragment = createSectionFragment (
173+ child ,
174+ items . slice ( itemIndex , itemIndex + acceptedCount ) ,
175+ includeHeader ,
176+ ) ;
177+ current = [ ...current , acceptedFragment ] ;
178+ itemIndex += acceptedCount ;
179+ includeHeader = false ;
180+ }
56181 } ) ;
57182
58183 if ( current . length > 0 ) {
59- nextPages . push ( { content : current . join ( "" ) } ) ;
184+ commitCurrentPage ( ) ;
60185 }
61186
62- const nextPageStyle = {
63- backgroundColor : computed . backgroundColor ,
64- color : computed . color ,
65- fontFamily : computed . fontFamily ,
66- fontSize : computed . fontSize ,
67- lineHeight : computed . lineHeight ,
68- padding : computed . padding ,
69- } satisfies CSSProperties ;
187+ probe . remove ( ) ;
188+
70189 const resolvedPages = nextPages . length > 0 ? nextPages : [ { content : container . innerHTML } ] ;
71190 const pageKey = resolvedPages . map ( ( page ) => page . content ) . join ( "" ) ;
72191 const styleKey = JSON . stringify ( nextPageStyle ) ;
0 commit comments