Files
jobtrackingapp/job-tracker-ui/src/cvPreview.ts
T

44 lines
3.1 KiB
TypeScript

// Use browser fragmentation, rather than estimating breaks in a tall screen document.
// The renderer's print rules and physical geometry remain authoritative.
export function previewDocumentHtml(html: string, fallbackHeightPx: number, pageIndex = 0): string {
const script = `<script data-cv-preview-pagination>(async()=>{
const started=performance.now();
const page=document.querySelector('.page');if(!page)return;
await document.fonts.ready;
await Promise.all(Array.from(document.images).map(image=>image.decode().catch(()=>{})));
const width=page.getBoundingClientRect().width;
const height=parseFloat(getComputedStyle(page).minHeight)||${fallbackHeightPx};
const margin=parseFloat(getComputedStyle(document.documentElement).getPropertyValue('--cv-page-margin'))*96/25.4;
const printRules=Array.from(document.styleSheets).flatMap(sheet=>Array.from(sheet.cssRules))
.filter(rule=>rule instanceof CSSMediaRule&&rule.conditionText==='print')
.flatMap(rule=>Array.from(rule.cssRules).map(rule=>rule.cssText)).join(' ');
const style=document.createElement('style');
// Chromium's print fragmentainer rounds the usable CSS height up to a pixel.
// A native 1px ruler verifies this for A4, Letter and density-adjusted margins.
const usableHeight=Math.ceil(height-2*margin);
style.textContent=printRules+'html,body{overflow:hidden!important;background:transparent!important;}body{padding-top:'+margin+'px;}'+
'[data-cv-preview-flow]{width:'+width+'px;height:'+usableHeight+'px;column-width:'+width+'px;column-gap:0;column-fill:auto;transform-origin:top left;}';
document.head.append(style);
const flow=document.createElement('div');flow.setAttribute('data-cv-preview-flow','');page.before(flow);flow.append(page);
// Keep the existing print decisions, substituting the screen fragment type.
const breaks=[];
page.querySelectorAll('*').forEach(element=>{
const css=getComputedStyle(element);
['break-inside','break-before','break-after'].forEach(property=>{
if(css.getPropertyValue(property)==='avoid-page')breaks.push([element,property]);
});
});
breaks.forEach(([element,property])=>element.style.setProperty(property,'avoid-column'));
await new Promise(resolve=>requestAnimationFrame(()=>requestAnimationFrame(resolve)));
const pages=Math.max(1,Math.round(flow.scrollWidth/width));
const overflow=Array.from(page.querySelectorAll('*')).some(element=>
Array.from(element.getClientRects()).some(rect=>rect.width>width+2));
flow.style.transform='translateX(-'+(${Math.max(0, pageIndex)}*width)+'px)';
Object.assign(document.documentElement.dataset,{cvPreviewPages:String(pages),cvPreviewWidth:String(width),
cvPreviewHeight:String(height),cvPreviewUsableHeight:String(usableHeight),cvPreviewOverflow:String(overflow),
cvPreviewDurationMs:String(performance.now()-started),cvPreviewReady:'true'});
window.dispatchEvent(new Event('cv-preview-ready'));
})();</script>`;
return html.includes('</body>') ? html.replace('</body>', `${script}</body>`) : `${html}${script}`;
}