Skip to content

Commit 70915f6

Browse files
fix(pdf): resolve blockquote alerts and nested table list splitting via safety buffer and valid list item shifting
1 parent 5043e4e commit 70915f6

4 files changed

Lines changed: 145 additions & 73 deletions

File tree

desktop-app/resources/js/script.js

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7031,6 +7031,17 @@ document.addEventListener("DOMContentLoaded", function () {
70317031
return;
70327032
}
70337033

7034+
// Skip any elements nested inside list items that contain block children (treat list items as atomic)
7035+
if (el.parentElement) {
7036+
const liAncestor = el.parentElement.closest('li');
7037+
if (liAncestor) {
7038+
const hasBlockChildren = liAncestor.querySelector('p, blockquote, pre, table, ul, ol') !== null;
7039+
if (hasBlockChildren) {
7040+
return;
7041+
}
7042+
}
7043+
}
7044+
70347045
let type = '';
70357046

70367047
if (tag === 'img') type = 'img';
@@ -7048,9 +7059,11 @@ document.addEventListener("DOMContentLoaded", function () {
70487059
else if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
70497060
type = 'text';
70507061
} else if (tag === 'li') {
7051-
// Only target li if they don't contain other block elements to avoid double targeting
7062+
// Treat list items with block children as atomic containers, otherwise treat as text
70527063
const hasBlockChildren = el.querySelector('p, blockquote, pre, table, ul, ol') !== null;
7053-
if (!hasBlockChildren) {
7064+
if (hasBlockChildren) {
7065+
type = 'li';
7066+
} else {
70547067
type = 'text';
70557068
}
70567069
} else if (el.classList.contains('math-block') || tag === 'mjx-container') {
@@ -7335,6 +7348,10 @@ document.addEventListener("DOMContentLoaded", function () {
73357348
el.style.fontSize = el.dataset.pdfOriginalFontSize;
73367349
el.removeAttribute('data-pdf-original-font-size');
73377350
});
7351+
container.querySelectorAll('[data-pdf-original-overflow]').forEach(el => {
7352+
el.style.overflow = el.dataset.pdfOriginalOverflow;
7353+
el.removeAttribute('data-pdf-original-overflow');
7354+
});
73387355
}
73397356

73407357
function mergeSplitTables(container) {
@@ -7513,10 +7530,10 @@ document.addEventListener("DOMContentLoaded", function () {
75137530

75147531
// 2. If not already pushed by Keep-With-Next, perform standard page-split calculations
75157532
if (targetMargin === 0) {
7516-
// Check if this element crosses any page boundary
7533+
// Check if this element crosses any page boundary or starts extremely close to it (sub-pixel safety)
75177534
let splitPageIndex = -1;
75187535
for (let i = 0; i < pageBoundaries.length; i++) {
7519-
if (currentTop < pageBoundaries[i] && currentBottom > pageBoundaries[i]) {
7536+
if (currentTop < pageBoundaries[i] + 12 && currentBottom > pageBoundaries[i]) {
75207537
splitPageIndex = i;
75217538
break;
75227539
}
@@ -7533,15 +7550,16 @@ document.addEventListener("DOMContentLoaded", function () {
75337550
targetMargin = shift;
75347551
}
75357552
} else {
7536-
// Graphic element (svg, img, pre, math) splitting
7537-
const buffer = 5;
7553+
// Graphic element splitting (with larger buffer to ensure complete clearance)
7554+
const buffer = 15;
75387555
const scaleNeeded = (remainingSpace - buffer) / item.height;
75397556
const remainingSpacePercent = remainingSpace / pageHeightPxFromAnalysis;
75407557

7541-
// Rule 3: Enforce safety zone. If remaining page space is less than 20% of page height,
7542-
// or if the required scale factor to fit is less than 0.6, push the element entirely to the next page.
7543-
if (remainingSpacePercent >= 0.20 && scaleNeeded >= 0.6) {
7544-
// Fit on current page by scaling
7558+
const isTextContainer = ['blockquote', 'li', 'table', 'pre', 'math'].includes(item.type);
7559+
7560+
// Fit on current page by scaling if it's an image/svg and space/scale are acceptable.
7561+
// Otherwise, always push text/block containers to next page to prevent transform-scaling bugs.
7562+
if (!isTextContainer && remainingSpacePercent >= 0.20 && scaleNeeded >= 0.6) {
75457563
targetScale = Math.min(1.0, scaleNeeded);
75467564
} else {
75477565
// Push to next page
@@ -7553,15 +7571,15 @@ document.addEventListener("DOMContentLoaded", function () {
75537571
const newBottom = newTop + item.height;
75547572
const nextBoundaryY = pageBoundaries[splitPageIndex + 1] || (boundaryY + pageHeightPxFromAnalysis);
75557573
if (newBottom > nextBoundaryY) {
7556-
const scaleToFitPage = (pageHeightPxFromAnalysis - 10) / item.height;
7574+
const scaleToFitPage = (pageHeightPxFromAnalysis - 20) / item.height;
75577575
targetScale = Math.max(0.5, Math.min(1.0, scaleToFitPage));
75587576
}
75597577
}
75607578
}
75617579
} else {
75627580
// Element is not split. But graphic elements taller than a page must still scale to fit!
75637581
if (item.type !== 'text' && item.height > pageHeightPxFromAnalysis) {
7564-
const scaleToFitPage = (pageHeightPxFromAnalysis - 10) / item.height;
7582+
const scaleToFitPage = (pageHeightPxFromAnalysis - 20) / item.height;
75657583
targetScale = Math.max(0.5, Math.min(1.0, scaleToFitPage));
75667584
}
75677585
}
@@ -7600,16 +7618,24 @@ document.addEventListener("DOMContentLoaded", function () {
76007618
}
76017619
}
76027620

7603-
// Create a physical spacer element to avoid margin collapse issues entirely
7604-
const spacer = document.createElement('div');
7605-
spacer.className = 'pdf-page-break-spacer';
7606-
spacer.style.height = `${targetMargin}px`;
7607-
spacer.style.margin = '0';
7608-
spacer.style.padding = '0';
7609-
spacer.style.border = 'none';
7610-
spacer.style.display = 'block';
7611-
7612-
targetElement.parentNode.insertBefore(spacer, targetElement);
7621+
// If target is a list item, apply marginTop directly to avoid invalid HTML / collapsed spacers
7622+
if (targetElement.tagName.toLowerCase() === 'li') {
7623+
if (!targetElement.dataset.hasOwnProperty('pdfOriginalMarginTop')) {
7624+
targetElement.dataset.pdfOriginalMarginTop = targetElement.style.marginTop || '';
7625+
}
7626+
targetElement.style.marginTop = `${targetMargin}px`;
7627+
} else {
7628+
// Create a physical spacer element to avoid margin collapse issues entirely
7629+
const spacer = document.createElement('div');
7630+
spacer.className = 'pdf-page-break-spacer';
7631+
spacer.style.height = `${targetMargin}px`;
7632+
spacer.style.margin = '0';
7633+
spacer.style.padding = '0';
7634+
spacer.style.border = 'none';
7635+
spacer.style.display = 'block';
7636+
7637+
targetElement.parentNode.insertBefore(spacer, targetElement);
7638+
}
76137639
accumulatedShift += targetMargin;
76147640
}
76157641

@@ -7710,25 +7736,28 @@ document.addEventListener("DOMContentLoaded", function () {
77107736
if (elementType === 'svg') {
77117737
element.style.maxWidth = 'none';
77127738
}
7713-
} else if (elementType === 'math' || elementType === 'pre' || elementType === 'blockquote') {
7714-
if (!element.dataset.hasOwnProperty('pdfOriginalFontSize')) {
7715-
element.dataset.pdfOriginalFontSize = element.style.fontSize || '';
7739+
} else {
7740+
// For pre, table, blockquote, math, li, etc.
7741+
// Use transform: scale combined with physical height and overflow hidden to guarantee no native splits
7742+
if (!element.dataset.hasOwnProperty('pdfOriginalHeight')) {
7743+
element.dataset.pdfOriginalHeight = element.style.height || '';
77167744
}
7717-
let origFontSize = parseFloat(element.dataset.pdfOriginalClientFontSize);
7718-
if (isNaN(origFontSize)) {
7719-
const style = window.getComputedStyle(element);
7720-
origFontSize = parseFloat(style.fontSize) || 14;
7721-
element.dataset.pdfOriginalClientFontSize = String(origFontSize);
7745+
if (!element.dataset.hasOwnProperty('pdfOriginalOverflow')) {
7746+
element.dataset.pdfOriginalOverflow = element.style.overflow || '';
77227747
}
7723-
element.style.fontSize = `${origFontSize * scaleFactor}px`;
7724-
} else {
7748+
77257749
element.style.transform = `scale(${scaleFactor})`;
77267750
element.style.transformOrigin = 'top left';
77277751

7728-
const originalHeight = element.offsetHeight;
7729-
const scaledHeight = originalHeight * scaleFactor;
7730-
const marginAdjustment = originalHeight - scaledHeight;
7731-
element.style.marginBottom = `-${marginAdjustment}px`;
7752+
let origHeight = parseFloat(element.dataset.pdfOriginalClientHeight);
7753+
if (isNaN(origHeight)) {
7754+
origHeight = element.offsetHeight || element.getBoundingClientRect().height;
7755+
element.dataset.pdfOriginalClientHeight = String(origHeight);
7756+
}
7757+
7758+
const scaledHeight = origHeight * scaleFactor;
7759+
element.style.height = `${scaledHeight}px`;
7760+
element.style.overflow = 'hidden';
77327761
}
77337762
}
77347763

@@ -7952,10 +7981,15 @@ document.addEventListener("DOMContentLoaded", function () {
79527981
await waitForPdfFrame(progressState);
79537982
throwIfPdfExportAborted(progressState.signal);
79547983

7984+
console.log(`[PDF DEBUG] canvas.width = ${canvas.width}, canvas.height = ${canvas.height}`);
7985+
console.log(`[PDF DEBUG] tempElement.offsetWidth = ${tempElement.offsetWidth}, rect.width = ${tempElement.getBoundingClientRect().width}`);
79557986
const scaleFactor = canvas.width / contentWidth;
7987+
console.log(`[PDF DEBUG] scaleFactor = ${scaleFactor}, PAGE_CONFIG.scale = ${PAGE_CONFIG.scale}, captureScale = ${captureScale}`);
79567988
const imgHeight = canvas.height / scaleFactor;
7989+
console.log(`[PDF DEBUG] imgHeight = ${imgHeight}, contentHeight = ${pageHeight - margin * 2}`);
79577990
// Introduce a 0.5mm tolerance to prevent rounding errors from creating a trailing blank page
79587991
const pagesCount = Math.ceil((imgHeight - 0.5) / (pageHeight - margin * 2));
7992+
console.log(`[PDF DEBUG] pagesCount = ${pagesCount}`);
79597993

79607994
updatePdfProgress(progressState, 76, "Rendering pages");
79617995
for (let page = 0; page < pagesCount; page++) {

exported_document.pdf

2.07 KB
Binary file not shown.

script.js

Lines changed: 70 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -7031,6 +7031,17 @@ document.addEventListener("DOMContentLoaded", function () {
70317031
return;
70327032
}
70337033

7034+
// Skip any elements nested inside list items that contain block children (treat list items as atomic)
7035+
if (el.parentElement) {
7036+
const liAncestor = el.parentElement.closest('li');
7037+
if (liAncestor) {
7038+
const hasBlockChildren = liAncestor.querySelector('p, blockquote, pre, table, ul, ol') !== null;
7039+
if (hasBlockChildren) {
7040+
return;
7041+
}
7042+
}
7043+
}
7044+
70347045
let type = '';
70357046

70367047
if (tag === 'img') type = 'img';
@@ -7048,9 +7059,11 @@ document.addEventListener("DOMContentLoaded", function () {
70487059
else if (['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'].includes(tag)) {
70497060
type = 'text';
70507061
} else if (tag === 'li') {
7051-
// Only target li if they don't contain other block elements to avoid double targeting
7062+
// Treat list items with block children as atomic containers, otherwise treat as text
70527063
const hasBlockChildren = el.querySelector('p, blockquote, pre, table, ul, ol') !== null;
7053-
if (!hasBlockChildren) {
7064+
if (hasBlockChildren) {
7065+
type = 'li';
7066+
} else {
70547067
type = 'text';
70557068
}
70567069
} else if (el.classList.contains('math-block') || tag === 'mjx-container') {
@@ -7335,6 +7348,10 @@ document.addEventListener("DOMContentLoaded", function () {
73357348
el.style.fontSize = el.dataset.pdfOriginalFontSize;
73367349
el.removeAttribute('data-pdf-original-font-size');
73377350
});
7351+
container.querySelectorAll('[data-pdf-original-overflow]').forEach(el => {
7352+
el.style.overflow = el.dataset.pdfOriginalOverflow;
7353+
el.removeAttribute('data-pdf-original-overflow');
7354+
});
73387355
}
73397356

73407357
function mergeSplitTables(container) {
@@ -7513,10 +7530,10 @@ document.addEventListener("DOMContentLoaded", function () {
75137530

75147531
// 2. If not already pushed by Keep-With-Next, perform standard page-split calculations
75157532
if (targetMargin === 0) {
7516-
// Check if this element crosses any page boundary
7533+
// Check if this element crosses any page boundary or starts extremely close to it (sub-pixel safety)
75177534
let splitPageIndex = -1;
75187535
for (let i = 0; i < pageBoundaries.length; i++) {
7519-
if (currentTop < pageBoundaries[i] && currentBottom > pageBoundaries[i]) {
7536+
if (currentTop < pageBoundaries[i] + 12 && currentBottom > pageBoundaries[i]) {
75207537
splitPageIndex = i;
75217538
break;
75227539
}
@@ -7533,15 +7550,16 @@ document.addEventListener("DOMContentLoaded", function () {
75337550
targetMargin = shift;
75347551
}
75357552
} else {
7536-
// Graphic element (svg, img, pre, math) splitting
7537-
const buffer = 5;
7553+
// Graphic element splitting (with larger buffer to ensure complete clearance)
7554+
const buffer = 15;
75387555
const scaleNeeded = (remainingSpace - buffer) / item.height;
75397556
const remainingSpacePercent = remainingSpace / pageHeightPxFromAnalysis;
75407557

7541-
// Rule 3: Enforce safety zone. If remaining page space is less than 20% of page height,
7542-
// or if the required scale factor to fit is less than 0.6, push the element entirely to the next page.
7543-
if (remainingSpacePercent >= 0.20 && scaleNeeded >= 0.6) {
7544-
// Fit on current page by scaling
7558+
const isTextContainer = ['blockquote', 'li', 'table', 'pre', 'math'].includes(item.type);
7559+
7560+
// Fit on current page by scaling if it's an image/svg and space/scale are acceptable.
7561+
// Otherwise, always push text/block containers to next page to prevent transform-scaling bugs.
7562+
if (!isTextContainer && remainingSpacePercent >= 0.20 && scaleNeeded >= 0.6) {
75457563
targetScale = Math.min(1.0, scaleNeeded);
75467564
} else {
75477565
// Push to next page
@@ -7553,15 +7571,15 @@ document.addEventListener("DOMContentLoaded", function () {
75537571
const newBottom = newTop + item.height;
75547572
const nextBoundaryY = pageBoundaries[splitPageIndex + 1] || (boundaryY + pageHeightPxFromAnalysis);
75557573
if (newBottom > nextBoundaryY) {
7556-
const scaleToFitPage = (pageHeightPxFromAnalysis - 10) / item.height;
7574+
const scaleToFitPage = (pageHeightPxFromAnalysis - 20) / item.height;
75577575
targetScale = Math.max(0.5, Math.min(1.0, scaleToFitPage));
75587576
}
75597577
}
75607578
}
75617579
} else {
75627580
// Element is not split. But graphic elements taller than a page must still scale to fit!
75637581
if (item.type !== 'text' && item.height > pageHeightPxFromAnalysis) {
7564-
const scaleToFitPage = (pageHeightPxFromAnalysis - 10) / item.height;
7582+
const scaleToFitPage = (pageHeightPxFromAnalysis - 20) / item.height;
75657583
targetScale = Math.max(0.5, Math.min(1.0, scaleToFitPage));
75667584
}
75677585
}
@@ -7600,16 +7618,24 @@ document.addEventListener("DOMContentLoaded", function () {
76007618
}
76017619
}
76027620

7603-
// Create a physical spacer element to avoid margin collapse issues entirely
7604-
const spacer = document.createElement('div');
7605-
spacer.className = 'pdf-page-break-spacer';
7606-
spacer.style.height = `${targetMargin}px`;
7607-
spacer.style.margin = '0';
7608-
spacer.style.padding = '0';
7609-
spacer.style.border = 'none';
7610-
spacer.style.display = 'block';
7611-
7612-
targetElement.parentNode.insertBefore(spacer, targetElement);
7621+
// If target is a list item, apply marginTop directly to avoid invalid HTML / collapsed spacers
7622+
if (targetElement.tagName.toLowerCase() === 'li') {
7623+
if (!targetElement.dataset.hasOwnProperty('pdfOriginalMarginTop')) {
7624+
targetElement.dataset.pdfOriginalMarginTop = targetElement.style.marginTop || '';
7625+
}
7626+
targetElement.style.marginTop = `${targetMargin}px`;
7627+
} else {
7628+
// Create a physical spacer element to avoid margin collapse issues entirely
7629+
const spacer = document.createElement('div');
7630+
spacer.className = 'pdf-page-break-spacer';
7631+
spacer.style.height = `${targetMargin}px`;
7632+
spacer.style.margin = '0';
7633+
spacer.style.padding = '0';
7634+
spacer.style.border = 'none';
7635+
spacer.style.display = 'block';
7636+
7637+
targetElement.parentNode.insertBefore(spacer, targetElement);
7638+
}
76137639
accumulatedShift += targetMargin;
76147640
}
76157641

@@ -7710,25 +7736,28 @@ document.addEventListener("DOMContentLoaded", function () {
77107736
if (elementType === 'svg') {
77117737
element.style.maxWidth = 'none';
77127738
}
7713-
} else if (elementType === 'math' || elementType === 'pre' || elementType === 'blockquote') {
7714-
if (!element.dataset.hasOwnProperty('pdfOriginalFontSize')) {
7715-
element.dataset.pdfOriginalFontSize = element.style.fontSize || '';
7739+
} else {
7740+
// For pre, table, blockquote, math, li, etc.
7741+
// Use transform: scale combined with physical height and overflow hidden to guarantee no native splits
7742+
if (!element.dataset.hasOwnProperty('pdfOriginalHeight')) {
7743+
element.dataset.pdfOriginalHeight = element.style.height || '';
77167744
}
7717-
let origFontSize = parseFloat(element.dataset.pdfOriginalClientFontSize);
7718-
if (isNaN(origFontSize)) {
7719-
const style = window.getComputedStyle(element);
7720-
origFontSize = parseFloat(style.fontSize) || 14;
7721-
element.dataset.pdfOriginalClientFontSize = String(origFontSize);
7745+
if (!element.dataset.hasOwnProperty('pdfOriginalOverflow')) {
7746+
element.dataset.pdfOriginalOverflow = element.style.overflow || '';
77227747
}
7723-
element.style.fontSize = `${origFontSize * scaleFactor}px`;
7724-
} else {
7748+
77257749
element.style.transform = `scale(${scaleFactor})`;
77267750
element.style.transformOrigin = 'top left';
77277751

7728-
const originalHeight = element.offsetHeight;
7729-
const scaledHeight = originalHeight * scaleFactor;
7730-
const marginAdjustment = originalHeight - scaledHeight;
7731-
element.style.marginBottom = `-${marginAdjustment}px`;
7752+
let origHeight = parseFloat(element.dataset.pdfOriginalClientHeight);
7753+
if (isNaN(origHeight)) {
7754+
origHeight = element.offsetHeight || element.getBoundingClientRect().height;
7755+
element.dataset.pdfOriginalClientHeight = String(origHeight);
7756+
}
7757+
7758+
const scaledHeight = origHeight * scaleFactor;
7759+
element.style.height = `${scaledHeight}px`;
7760+
element.style.overflow = 'hidden';
77327761
}
77337762
}
77347763

@@ -7952,10 +7981,15 @@ document.addEventListener("DOMContentLoaded", function () {
79527981
await waitForPdfFrame(progressState);
79537982
throwIfPdfExportAborted(progressState.signal);
79547983

7984+
console.log(`[PDF DEBUG] canvas.width = ${canvas.width}, canvas.height = ${canvas.height}`);
7985+
console.log(`[PDF DEBUG] tempElement.offsetWidth = ${tempElement.offsetWidth}, rect.width = ${tempElement.getBoundingClientRect().width}`);
79557986
const scaleFactor = canvas.width / contentWidth;
7987+
console.log(`[PDF DEBUG] scaleFactor = ${scaleFactor}, PAGE_CONFIG.scale = ${PAGE_CONFIG.scale}, captureScale = ${captureScale}`);
79567988
const imgHeight = canvas.height / scaleFactor;
7989+
console.log(`[PDF DEBUG] imgHeight = ${imgHeight}, contentHeight = ${pageHeight - margin * 2}`);
79577990
// Introduce a 0.5mm tolerance to prevent rounding errors from creating a trailing blank page
79587991
const pagesCount = Math.ceil((imgHeight - 0.5) / (pageHeight - margin * 2));
7992+
console.log(`[PDF DEBUG] pagesCount = ${pagesCount}`);
79597993

79607994
updatePdfProgress(progressState, 76, "Rendering pages");
79617995
for (let page = 0; page < pagesCount; page++) {

test_breaks.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,11 @@ async function runAudit() {
9696
}
9797
else if (tag === 'li') {
9898
const hasBlockChildren = el.querySelector('p, blockquote, pre, table, ul, ol') !== null;
99-
if (!hasBlockChildren) type = 'text';
99+
if (hasBlockChildren) {
100+
type = 'li';
101+
} else {
102+
type = 'text';
103+
}
100104
} else if (el.classList.contains('math-block') || tag === 'mjx-container') {
101105
type = 'math';
102106
}

0 commit comments

Comments
 (0)