Skip to content

Improve code block overflow handling in PDF exports via universal print.css#130

Merged
schuyler merged 3 commits intomainfrom
claude/resolve-issue-28-01Po8de4pN6bYu7jY3VuzEQc
Nov 20, 2025
Merged

Improve code block overflow handling in PDF exports via universal print.css#130
schuyler merged 3 commits intomainfrom
claude/resolve-issue-28-01Po8de4pN6bYu7jY3VuzEQc

Conversation

@schuyler
Copy link
Copy Markdown
Owner

Summary

Fixes code blocks with long lines being cut off when exporting to PDF. Uses an elegant architecture: a single universal print.css file instead of modifying 6 individual theme CSS files.

Changes

New File: MacDown/Resources/Extensions/print.css

Universal print stylesheet with @media print rules for code overflow handling:

  • Comprehensive selectors: pre, pre code, code, p code, li code, td code, th code
  • Uses white-space: pre-wrap !important and overflow-wrap: break-word !important
  • Handles edge cases: long URLs, no-space strings, inline code, table cells

Modified: MacDown/Code/Document/MPRenderer.m (lines 503-505)

Loads print.css LAST in stylesheet cascade (after all theme CSS):

// Load print.css last to ensure it overrides theme defaults for PDF export
NSURL *printURL = MPExtensionURL(@"print", @"css");
[stylesheets addObject:[MPStyleSheet CSSWithURL:printURL]];

Updated Documentation

Added resolution sections to investigation docs in plans/:

  • ISSUE_28_INVESTIGATION.md - Documents final solution vs. abandoned approach
  • PDF_EXPORT_CSS_DEBUG_REPORT.md - Notes proposed fix was superseded
  • CSS_SPECIFICITY_ISSUE.md - Clarifies alternative implementation

Architecture

Why This Approach (vs. PR #125):

  • ✅ Single source of truth (1 file vs. 6 theme modifications)
  • ✅ Loads LAST in cascade (critical for overriding themes)
  • ✅ Universal across all themes (future themes automatically inherit fix)
  • ✅ Maintainable (one file to update)
  • ✅ Clean separation of concerns (print styles separate from theme styles)

Technical Details:

  • CSS cascade order: theme CSS → prism CSS → extensions CSS → print.css (last)
  • Specificity matching: pre code (0-0-2) matches theme CSS specificity
  • !important declarations force override when specificity is equal
  • @media print ensures no impact on preview rendering

Previous Investigation (PR #125):

  • Modifying 6 theme files individually showed 25% success rate (2/8 tests passing)
  • Cascade order issues caused inconsistent behavior
  • This universal approach solves those problems

Code Review

Chico (Code Reviewer):APPROVED

  • Architecture is superior to modifying individual theme files
  • CSS selectors are comprehensive and correct
  • Follows project conventions
  • !important usage is justified for print context
  • Null-safety follows existing codebase patterns

Groucho (Architect):APPROVED

  • Extensions directory is correct location
  • Loading LAST in cascade is critical for success
  • Universal solution works across all 6 themes
  • Matches existing patterns (similar to show-information.css)

Harpo (Documentation):UPDATED

  • Added resolution sections to investigation docs
  • Clarifies which approach was actually implemented
  • Prevents confusion about file modifications

Testing

CI Status

✅ All unit tests passed - Workflow run 19522611904 completed successfully in 2m8s

Manual Testing Required

macOS + Xcode required - PDF rendering cannot be automated

Test Files: plans/test-pdf-export-samples/

  • 00-comprehensive-test.md - All 8 scenarios in one file
  • 01-long-single-line.md through 05-edge-cases.md - Individual scenarios

Test Matrix (8 scenarios × 6 themes):

Scenario Expected Behavior Priority
1. Long single line Wraps without truncation 🔴 Critical
2. Multiple long lines All lines wrap, indentation preserved 🔴 Critical
3. Inline code Wraps within paragraphs 🟡 High
4. Tables with code Code wraps in cells, table intact 🔴 Critical
5. JavaScript block Long lines wrap 🟢 Medium
6. URL in block URL fully visible, wraps at natural points 🟡 High
7. No spaces (worst case) Force-breaks at container edge 🔴 Critical
8. Normal code NO unnecessary wrapping (regression test) 🟡 High

Themes to Test:

  • GitHub, GitHub 2, Clearness, Clearness Dark, Solarized (Light), Solarized (Dark)

Target Success Rate: 8/8 scenarios passing (vs. previous 2/8 = 300% improvement)

Manual Testing Instructions

Quick Test (15 min):

  1. Build MacDown in Xcode
  2. Open plans/test-pdf-export-samples/00-comprehensive-test.md
  3. For each of the 6 themes:
    • Select theme in Preferences → Rendering
    • File → Print → Save as PDF
    • Open PDF and verify all code blocks wrap (no horizontal overflow)

Detailed Test Plan: See Zeppo's comprehensive testing guide in MANUAL_TESTING.md (includes debugging steps, edge cases, success criteria)

Key Things to Verify:

  • No text cut off at page edges
  • Long lines wrap to multiple lines
  • Monospace font preserved
  • Tables don't break
  • URLs fully visible
  • Force-breaking works for strings without spaces

Related Issue

Related to #28

Follow-up Recommendations

From code review:

  1. Consider adding defensive null-check for printURL (consistent with codebase patterns, but not critical)
  2. Future: Remove redundant @media print rules from theme CSS files to reduce duplication
  3. Document in theme CSS files that print.css provides universal print styles

Notes

Create a single print.css file in Extensions directory that loads
universally for all themes, instead of modifying 6 theme files individually.
This provides cleaner architecture and consistent PDF export behavior.

Changes:
- Add MacDown/Resources/Extensions/print.css with @media print rules
  for code block wrapping and overflow handling
- Modify MPRenderer.m to load print.css last in stylesheet cascade,
  ensuring it overrides theme defaults with proper specificity
- CSS includes comprehensive selectors: pre, pre code, code, p code,
  td code, etc. with !important declarations
- Includes overflow-wrap: break-word for edge cases (URLs, no-space strings)

Technical approach:
- print.css loads LAST in cascade (critical for overriding themes)
- Uses matching specificity (0-0-2) with theme CSS
- @media print ensures no impact on preview rendering
- Single source of truth for all 6 themes

Related to #28
Add resolution sections to investigation documents clarifying that
the approach of modifying 6 individual theme files was abandoned
in favor of a universal print.css solution.

Changes:
- ISSUE_28_INVESTIGATION.md: Add resolution section with final solution details
- PDF_EXPORT_CSS_DEBUG_REPORT.md: Note that proposed fix was superseded
- CSS_SPECIFICITY_ISSUE.md: Clarify alternative implementation approach

These updates prevent confusion about which files were actually modified
and provide complete narrative of the investigation and resolution.

Related to #28
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Nov 20, 2025

Code Coverage Report

Current Coverage: 39.53%

Coverage Details (Summary)
Name                                                                                                                                   Coverage            
-------------------------------------------------------------------------------------------------------------------------------------- ------------------- 
MacDown.app                                                                                                                            53.03% (6558/12366) 
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Document/MPAsset.m                                                         91.96% (103/112)    
        -[MPAsset typeName]                                                                                                            100.00% (3/3)       
        -[MPAsset defaultTypeName]                                                                                                     100.00% (3/3)       
        +[MPAsset assetWithURL:andType:]                                                                                               100.00% (3/3)       
        -[MPAsset initWithURL:andType:]                                                                                                87.50% (7/8)        
        -[MPAsset init]                                                                                                                100.00% (3/3)       
        -[MPAsset templateForOption:]                                                                                                  0.00% (0/7)         
        -[MPAsset htmlForOption:]                                                                                                      96.43% (27/28)      
        -[MPStyleSheet defaultTypeName]                                                                                                100.00% (3/3)       
        +[MPStyleSheet CSSWithURL:]                                                                                                    100.00% (3/3)       
        -[MPStyleSheet templateForOption:]                                                                                             100.00% (20/20)     
        -[MPScript defaultTypeName]                                                                                                    100.00% (3/3)       
        +[MPScript javaScriptWithURL:]                                                                                                 100.00% (3/3)       
        -[MPScript templateForOption:]                                                                                                 100.00% (20/20)     
        -[MPEmbeddedScript htmlForOption:]                                                                                             100.00% (5/5)       
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Utility/MPMathJaxListener.m                                                0.00% (0/26)        
        -[MPMathJaxListener callbacks]                                                                                                 0.00% (0/5)         
        -[MPMathJaxListener addCallback:forKey:]                                                                                       0.00% (0/3)         
        -[MPMathJaxListener invokeCallbackForKey:]                                                                                     0.00% (0/8)         
        +[MPMathJaxListener isSelectorExcludedFromWebScript:]                                                                          0.00% (0/5)         
        +[MPMathJaxListener isKeyExcludedFromWebScript:]                                                                               0.00% (0/5)         
    /Users/runner/work/macdown3000/macdown3000/Dependency/peg-markdown-highlight/HGMarkdownHighlightingStyle.m                         87.69% (57/65)      
        +[HGMarkdownHighlightingStyle colorFromARGBColor:]                                                                             100.00% (6/6)       
        -[HGMarkdownHighlightingStyle initWithType:attributesToAdd:toRemove:fontTraitsToAdd:]                                          88.89% (8/9)        
        -[HGMarkdownHighlightingStyle initWithStyleAttributes:baseFont:]                                                               86.00% (43/50)      
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Utility/MPPlugIn.m                                                         0.00% (0/36)        
        -[MPPlugIn setName:]                                                                                                           0.00% (0/3)         
        -[MPPlugIn initWithBundle:]                                                                                                    0.00% (0/24)        
        -[MPPlugIn plugInDidInitialize]                                                                                                0.00% (0/4)         
        -[MPPlugIn run:]                                                                                                               0.00% (0/5)         
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Extension/hoedown_html_patch.c                                             70.99% (93/131)     
        hoedown_patch_render_blockcode                                                                                                 87.27% (48/55)      
        hoedown_patch_render_listitem                                                                                                  95.74% (45/47)      
        hoedown_patch_render_toc_header                                                                                                0.00% (0/29)        
    /Users/runner/work/macdown3000/macdown3000/MacDown/Code/Application/MPMainController.m                                             40.16% (100/249)    
        MPOpenBundledFile                                                                                                              95.45% (21/22)      
        __MPOpenBundledFile_block_invoke                                                                                               85.71% (6/7)        
        treat                                                                                                                          20.00% (6/30)       
        __treat_block_invoke                                                                                                           0.00% (0/4)         
        -[MPMainController applicationDidFinishLaunching:]                                                                             100.00% (6/6)       
        -[MPMainController openUrlSchemeAppleEvent:withReplyEvent:]                                                                    0.00% (0/45)        
        __59-[MPMainController openUrlSchemeAppleEvent:withReplyEvent:]_block_invoke                                                   0.00% (0/7)         
        -[MPMainController valueForKey:fromQueryItems:]                                                                                0.00% (0/5)         
        -[MPMainController preferences]                                                                                                100.00% (3/3)       
        -[MPMainController preferencesWindowController]                                                                                0.00% (0/18)        
        -[MPMainController showPreferencesWindow:]                                                                                     0.00% (0/3)         
        -[MPMainController showHelp:]                                                                                                  100.00% (3/3)       

... (1993 more lines truncated)

📊 **Full coverage report available in workflow artifacts**

Add table-specific CSS using :has() selector to fix Test 4 failure
where long code in table cells was not wrapping properly.

IMPORTANT: Uses :has() selector to target ONLY tables containing code.
Regular data tables keep natural column sizing (table-layout: auto).
This avoids breaking changes to existing PDF table layouts.

Graceful degradation: If :has() is not supported by WebKit PDF rendering,
the fix won't apply (code may overflow) but regular tables remain unaffected.
This is preferable to breaking all existing table layouts.

Changes:
- Add table:has(code) selector with table-layout: fixed and width: 100%
- Add max-width: 0 on cells ONLY in tables containing code (CSS hack)
- Use overflow-wrap: break-word for long identifiers without spaces
- All properties use !important to override theme CSS

Technical details:
- table-layout: fixed distributes columns evenly (only for tables with code)
- max-width: 0 forces content to wrap while fixed layout determines actual width
- :has() selector is CSS4 - surgical targeting of specific tables
- Graceful degradation if :has() unsupported

Expected result: Test 4 (Tables with Long Code) passes if :has() supported
Success rate: 8/8 tests (100%) if :has() works, 7/8 (87.5%) if not
No breaking changes to existing table layouts either way

Manual testing needed: Verify :has() works in WebKit PDF rendering

Related to #28
@schuyler schuyler force-pushed the claude/resolve-issue-28-01Po8de4pN6bYu7jY3VuzEQc branch from 5faeea3 to 22c482c Compare November 20, 2025 03:34
@schuyler
Copy link
Copy Markdown
Owner Author

Manual testing worked.
00-comprehensive-test.pdf
00-comprehensive-test.md

@schuyler schuyler merged commit e339643 into main Nov 20, 2025
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Improve code block rendering in PDF exports (handle overflow)

2 participants