diff --git a/scripts/export.checks.test.ts b/scripts/export.checks.test.ts index cc64200d..ff6e7d8a 100644 --- a/scripts/export.checks.test.ts +++ b/scripts/export.checks.test.ts @@ -130,7 +130,7 @@ describe('exported content width', () => { test('the export cap loses to the print block, so the PDF route is unchanged', () => { // `@media print` in styles.css already forces `.markdown-body` to - // `max-width: 100% !important` with its own .75in padding, and that block + // `max-width: 100% !important` with its own 1cm padding, and that block // travels into the export with the rest of the sheet. Printing an exported // file therefore never saw the 900px and must not start seeing the setting // either — the two routes stay separate concerns. @@ -138,7 +138,7 @@ describe('exported content width', () => { const printed = declarationsFor(css, 'max-width', 'print'); assert.ok(printed.length >= 1, 'the print block must still cap .markdown-body'); assert.deepEqual(printed.at(-1), { value: '100%', important: true }); - assert.deepEqual(declarationsFor(css, 'padding', 'print').at(-1), { value: '0.75in', important: true }); + assert.deepEqual(declarationsFor(css, 'padding', 'print').at(-1), { value: '1cm', important: true }); }); test('the app stylesheet copied into an export does not out-rank the cap', () => { @@ -801,11 +801,12 @@ describe('oversized media in print', () => { const printBlock = extractPrintBlock(styles); // Paper geometry the cap has to satisfy. `@page { margin: 0 }` makes the page - // box the whole sheet, and `.markdown-body` supplies 0.75in of padding top and - // bottom, so the printable column is the sheet height minus 1.5in. - const LETTER_PRINTABLE_IN = 11 - 1.5; + // box the whole sheet, and `.markdown-body` supplies 1cm of padding top and + // bottom, so the printable column is the sheet height minus 2cm. + const MARGINS_IN = 2 / 2.54; + const LETTER_PRINTABLE_IN = 11 - MARGINS_IN; - const A4_PRINTABLE_IN = 11.69 - 1.5; + const A4_PRINTABLE_IN = 11.69 - MARGINS_IN; /** Print rules whose body sets `property` (never a vendor-prefixed variant). */ function printRulesSetting(property: string): { selector: string; body: string }[] { diff --git a/scripts/export.spec.ts b/scripts/export.spec.ts new file mode 100644 index 00000000..b7b9d26c --- /dev/null +++ b/scripts/export.spec.ts @@ -0,0 +1,68 @@ +import assert from 'node:assert/strict'; +import { describe, test } from 'vitest'; + +import { buildExportDocument } from '../src/lib/utils/export.ts'; +import { plainAppearance } from './exportFixtures.ts'; +import { readSource } from './sourceTree.js'; + +const appStyles = readSource('src/styles.css'); + +/** + * Loads `html` into the jsdom document as it would come out on `media`. jsdom + * applies no media type itself, so each `@media` block that names `media` (or + * `all`) is replaced in place by its own rules, and every other `@media` block + * is dropped. + */ +function render(html: string, media: 'print' | 'screen'): Document { + document.documentElement.innerHTML = html.replace(/^\s*]*>/, '').replace(/<\/html>\s*$/, ''); + for (const sheet of Array.from(document.styleSheets)) { + for (let index = sheet.cssRules.length - 1; index >= 0; index -= 1) { + const rule = sheet.cssRules[index]; + if (!(rule instanceof CSSMediaRule)) continue; + const applies = /\ball\b/.test(rule.media.mediaText) || rule.media.mediaText.includes(media); + const inner = applies ? Array.from(rule.cssRules, (r) => r.cssText) : []; + sheet.deleteRule(index); + inner.forEach((text, offset) => sheet.insertRule(text, index + offset)); + } + } + return document; +} + +function article(doc: Document): CSSStyleDeclaration { + const element = doc.querySelector('article.markdown-body'); + assert.ok(element, 'the page has no article'); + return getComputedStyle(element); +} + +const exported = buildExportDocument({ + theme: 'light', + title: 'Notes', + styles: appStyles, + articleHtml: '

body

', + contentWidth: null, + appearance: plainAppearance, +}); + +describe('printing an exported HTML file', () => { + test('prints the article instead of a blank page', () => { + // The copied print sheet hides everything outside #app, which an exported + // file does not have, so the article itself was hidden on paper. + assert.notEqual(article(render(exported, 'print')).display, 'none'); + }); + + test('gets the same paper margins as the app’s own PDF', () => { + // The PDF route prints #print-root inside #app; the export used to keep its + // screen padding on paper instead. + const pdf = render( + `
`, + 'print', + ); + const paper = article(pdf).paddingTop; + assert.equal(article(render(exported, 'print')).paddingTop, paper); + assert.equal(article(render(exported, 'print')).paddingLeft, paper); + }); + + test('keeps its 40px padding on screen', () => { + assert.equal(article(render(exported, 'screen')).paddingTop, '40px'); + }); +}); diff --git a/src/lib/utils/export.ts b/src/lib/utils/export.ts index d7432c4d..9150f5a4 100644 --- a/src/lib/utils/export.ts +++ b/src/lib/utils/export.ts @@ -226,7 +226,6 @@ html, body { padding: 0; } .markdown-body { - padding: 40px !important; max-width: ${exportContentMaxWidth(input.contentWidth)}; ${appearance} margin: 0 auto; @@ -234,6 +233,20 @@ ${appearance} overflow: visible !important; min-height: 100%; } +@media screen { + .markdown-body { + padding: 40px !important; + } +} +/* The copied print sheet hides everything outside #app, because the app prints + #print-root inside it. An exported file has no #app: its article is the page. + The :not(#app) is there for weight, to outrank that rule. Its padding is left + to the print sheet, so paper gets the same margins as the app's PDF. */ +@media print { + body > .markdown-body:not(#app) { + display: block !important; + } +} .lang-label { display: none !important; } diff --git a/src/styles.css b/src/styles.css index 350753af..5f0d5a86 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1440,7 +1440,7 @@ body { } .markdown-body { - padding: 0.75in !important; + padding: 1cm !important; margin: 0 !important; box-sizing: border-box !important; box-decoration-break: clone !important; @@ -1549,8 +1549,8 @@ body { margin: 1em auto; } /* An image or a diagram taller than the paper used to be clipped at the page - break rather than scaled; 9.5in is what both Letter and A4 hold with the - 0.75in margins above. */ + break rather than scaled; 9in fits both Letter and A4 inside the + 1cm margins above. */ .markdown-body img, .markdown-body .mermaid-diagram svg { max-height: 9in !important;